diff --git a/packages/chrome/src/components/Menu.tsx b/packages/chrome/src/components/Menu.tsx
index 7c4a474c..39c8686c 100644
--- a/packages/chrome/src/components/Menu.tsx
+++ b/packages/chrome/src/components/Menu.tsx
@@ -26,6 +26,8 @@ export function Menu(
closing: boolean;
x: number;
y: number;
+ transformOriginX: string;
+ transformOriginY: string;
}
>
) {
@@ -35,6 +37,8 @@ export function Menu(
});
this.x = 0;
this.y = 0;
+ this.transformOriginX = "left";
+ this.transformOriginY = "top";
const [lock, unlock] = requestUnfocusFrames();
@@ -84,8 +88,14 @@ export function Menu(
const maxX = docWidth - width - padding;
const maxY = docHeight - height - padding;
- if (this.x > maxX) this.x = maxX;
- if (this.y > maxY) this.y = maxY;
+ if (this.x > maxX) {
+ this.x = maxX;
+ this.transformOriginX = "right";
+ }
+ if (this.y > maxY) {
+ this.y = maxY;
+ this.transformOriginY = "bottom";
+ }
if (this.x < padding) this.x = padding;
if (this.y < padding) this.y = padding;
@@ -100,7 +110,7 @@ export function Menu(
};
return (
{this.items
@@ -163,16 +173,17 @@ Menu.style = css`
overflow: hidden;
transition:
- opacity 0.15s ease,
- transform 0.15s ease;
+ opacity 0.1s ease,
+ transform 0.12s cubic-bezier(0.35, 0.15, 0, 1.8);
opacity: 1;
- transform: scale(100%);
+ transform: scaleX(100%) scaleY(100%);
+ transform-origin: var(--transform-origin-x) var(--transform-origin-y);
}
.separator {
border-top: 1px solid var(--text-20);
}
:scope.closing {
- transform: scale(95%);
+ transform: scaleX(95%) scaleY(87%);
opacity: 0;
}
.item {
diff --git a/packages/chrome/src/components/TabStrip/DragTab.tsx b/packages/chrome/src/components/TabStrip/DragTab.tsx
index 9b517a24..7dace4fc 100644
--- a/packages/chrome/src/components/TabStrip/DragTab.tsx
+++ b/packages/chrome/src/components/TabStrip/DragTab.tsx
@@ -59,6 +59,7 @@ export function DragTab(
},
]);
+ // Open-tab animation: expands the tab container from width 0 to full computed width.
this.root.animate(
[
{
@@ -67,7 +68,8 @@ export function DragTab(
{},
],
{
- duration: 100,
+ duration: 200,
+ easing: "cubic-bezier(.25,.5,0,1.15)",
fill: "forwards",
}
);
@@ -81,6 +83,7 @@ export function DragTab(
class="tab"
data-id={this.id}
on:transitionend={() => {
+ // Clears programmatically assigned move transition/z-index after tab translate animation ends.
this.root.style.transition = "";
this.root.style.zIndex = "0";
this.transitionend();
diff --git a/packages/chrome/src/components/TabStrip/TabStrip.tsx b/packages/chrome/src/components/TabStrip/TabStrip.tsx
index 59aef445..61ce3f73 100644
--- a/packages/chrome/src/components/TabStrip/TabStrip.tsx
+++ b/packages/chrome/src/components/TabStrip/TabStrip.tsx
@@ -13,6 +13,7 @@ type VisualTab = {
dragoffset: number;
dragpos: number;
startdragpos: number;
+ closing: boolean;
width: number;
pos: number;
@@ -43,7 +44,10 @@ export function TabStrip(
const TAB_PADDING = 6;
const TAB_MAX_SIZE = 231;
- const TAB_TRANSITION = "250ms ease";
+ // Reorder/move animation for tabs and trailing controls in the strip.
+ const TAB_TRANSITION = "225ms cubic-bezier(.43,.52,0,1.15)";
+ const TAB_STAGGER_STEP = 18;
+ const TAB_STAGGER_MAX = 144;
let transitioningTabs = 0;
@@ -76,11 +80,15 @@ export function TabStrip(
const getTabWidth = () => {
let total = getRootWidth();
+ const visibleTabCount = this.visualtabs.filter(
+ (tab) => !tab.closing
+ ).length;
+ const count = Math.max(visibleTabCount, 1);
// remove padding
- total -= TAB_PADDING * (this.visualtabs.length - 1);
+ total -= TAB_PADDING * (count - 1);
- const each = total / this.visualtabs.length;
+ const each = total / count;
return Math.min(TAB_MAX_SIZE, Math.floor(each));
};
@@ -105,24 +113,52 @@ export function TabStrip(
let dragpos = -1;
let currpos = getLayoutStart();
+ let staggerIndex = 0;
+ let movedTabs = 0;
for (const tab of this.visualtabs) {
+ if (tab.closing) {
+ // Closing tabs animate their own width; keep their current transform while
+ // siblings/new-tab button reflow into post-close slots.
+ const tabPos = tab.dragpos != -1 ? tab.dragpos : tab.pos;
+ tab.root.style.transform = `translateX(${tabPos}px)`;
+ tab.pos = tabPos;
+ continue;
+ }
+
tab.root.style.width = width + "px";
const tabPos = tab.dragpos != -1 ? tab.dragpos : currpos;
+ // Moves each tab horizontally to its computed slot.
tab.root.style.transform = `translateX(${tabPos}px)`;
if (transition && tab.dragpos == -1 && tab.pos != tabPos) {
- tab.root.style.transition = `transform ${TAB_TRANSITION}`;
- this.afterEl.style.transition = `transform ${TAB_TRANSITION}`;
+ const delay = Math.min(
+ staggerIndex * TAB_STAGGER_STEP,
+ TAB_STAGGER_MAX
+ );
+ // Animates tab movement when tabs are inserted/removed/reordered.
+ tab.root.style.transition = `transform ${TAB_TRANSITION} ${delay}ms`;
transitioningTabs++;
+ movedTabs++;
}
dragpos = Math.max(dragpos, tab.dragpos + width + TAB_PADDING);
tab.pos = tabPos;
tab.width = width;
currpos += width + TAB_PADDING;
+ staggerIndex++;
+ }
+
+ if (transition && movedTabs > 0) {
+ const afterDelay = Math.min(
+ staggerIndex * TAB_STAGGER_STEP,
+ TAB_STAGGER_MAX
+ );
+ // Animate trailing "after" area (new-tab button container) with stagger too.
+ this.afterEl.style.transition = `transform ${TAB_TRANSITION} ${afterDelay}ms`;
}
const afterpos = Math.max(dragpos, currpos);
+ // Moves the trailing control area to stay after the last tab.
this.afterEl.style.transform = `translateX(${afterpos}px)`;
};
@@ -187,12 +223,11 @@ export function TabStrip(
};
const transitionend = () => {
- transitioningTabs--;
+ transitioningTabs = Math.max(transitioningTabs - 1, 0);
if (transitioningTabs == 0) {
+ this.afterEl.style.transition = "";
this.tabs = this.tabs;
}
-
- this.afterEl.style.transition = "";
};
use(this.tabs).listen(() => {
@@ -222,6 +257,7 @@ export function TabStrip(
dragoffset: -1,
dragpos: -1,
startdragpos: -1,
+ closing: false,
width: 0,
pos: getLayoutStart() + index * (getTabWidth() + TAB_PADDING),
};
@@ -233,7 +269,9 @@ export function TabStrip(
for (let vtab of this.visualtabs) {
if (!newvisualtabs.includes(vtab)) {
let indexof = this.visualtabs.indexOf(vtab);
+ vtab.closing = true;
newvisualtabs.splice(indexof, 0, vtab);
+ // Close-tab animation: collapses tab width to 0 before removal from DOM list.
let anim = vtab.root.animate(
[
{},
@@ -242,7 +280,8 @@ export function TabStrip(
},
],
{
- duration: 100,
+ duration: 150,
+ easing: "cubic-bezier(.29,.44,.3,.94)",
fill: "forwards",
}
);
@@ -302,7 +341,6 @@ TabStrip.style = css`
padding: var(--tab-padding) 12px;
height: calc(var(--tab-height) + calc(var(--tab-padding) * 2));
z-index: 2;
-
position: relative;
}
diff --git a/packages/chrome/src/components/TabStrip/TabTooltip.tsx b/packages/chrome/src/components/TabStrip/TabTooltip.tsx
index 593a9354..dedca0b1 100644
--- a/packages/chrome/src/components/TabStrip/TabTooltip.tsx
+++ b/packages/chrome/src/components/TabStrip/TabTooltip.tsx
@@ -16,14 +16,15 @@ export function TabTooltip(
) {
let wasActive = this.active;
- const duration = 150;
+ const duration = 125;
+ const curve = "cubic-bezier(.35,.15,0,1.5)";
const visible = {
opacity: "1",
- transform: "scale(100%)",
+ transform: "scaleX(100%) scaleY(100%)",
};
const hidden = {
opacity: "0",
- transform: "scale(95%)",
+ transform: "scaleX(95%) scaleY(87%)",
};
let isClosing = false;
@@ -46,24 +47,28 @@ export function TabTooltip(
if (this.animate) {
let shift = lastX - x;
+ // Instantly applies the hidden->visible visual state (opacity + scale) before slide alignment.
this.root.animate([hidden, visible], {
duration: 0,
fill: "forwards",
});
+ // Reposition animation between adjacent tab tooltips: translateX from previous tooltip X to current X.
this.root.animate(
[
{ transform: `translateX(${shift}px)` },
{ transform: "translateX(0px)" },
],
{
- duration: 300,
- easing: "ease-out",
+ duration: 150,
+ easing: "cubic-bezier(.45,.25,0,1.09)",
}
);
this.animate = false;
} else {
+ // Standard tooltip enter animation: fades in and scales from 95% -> 100%.
this.root.animate([hidden, visible], {
duration,
+ easing: curve,
fill: "forwards",
});
}
@@ -71,8 +76,10 @@ export function TabTooltip(
} else if (!active && wasActive) {
wasActive = false;
isClosing = true;
+ // Tooltip exit animation: fades out and scales down from 100% -> 95%.
this.root.animate([visible, hidden], {
duration,
+ easing: curve,
fill: "forwards",
}).onfinish = () => {
isClosing = false;
@@ -109,25 +116,28 @@ TabTooltip.style = css`
background: var(--popup);
border: 1px solid var(--popup_border);
border-radius: var(--radius);
- width: 17em;
+ width: 18em;
gap: 0.25em;
flex-direction: column;
opacity: 0;
border-radius: var(--radius);
}
.text {
- padding: 0.5em;
+ padding: 0.75em 0.67em;
display: flex;
flex-direction: column;
- gap: 0.1em;
+ gap: 0.25em;
}
.title {
- overflow: hidden;
+ overflow: clip visible;
white-space: nowrap;
text-overflow: ellipsis;
+ font-size: 0.9em;
+ font-weight: 500;
}
.hostname {
- font-size: 12px;
+ font-size: 0.7em;
+ color: var(--text-60);
}
.img {