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 {