diff --git a/frontend/src/lib/components/sidebar/operatorRoutes.ts b/frontend/src/lib/components/sidebar/operatorRoutes.ts index 7898eaf7ed..9e67921939 100644 --- a/frontend/src/lib/components/sidebar/operatorRoutes.ts +++ b/frontend/src/lib/components/sidebar/operatorRoutes.ts @@ -2,7 +2,7 @@ import type { OperatorSettings } from '$lib/gen' import type { UserWorkspace } from '$lib/stores' /** An `operator_settings` key: each admits an operator to one page (`triggers` to all of them). */ -export type OperatorPageKey = keyof OperatorSettings +export type OperatorPageKey = keyof NonNullable /** * Whether the sidebar shows the page behind `key`. Everyone but an operator sees every page diff --git a/frontend/src/routes/(root)/(logged)/+layout.svelte b/frontend/src/routes/(root)/(logged)/+layout.svelte index 381bdfd4e4..62d25e1de4 100644 --- a/frontend/src/routes/(root)/(logged)/+layout.svelte +++ b/frontend/src/routes/(root)/(logged)/+layout.svelte @@ -228,7 +228,10 @@ const collapse = x < SIDEBAR_SNAP_COLLAPSE_REM // Operators have no icon-only rail to snap to, so the same gesture detaches // instead: dragging the sidebar off the left edge puts it behind the handle. + // Detaching is not a resize, so the width the drag started from is what gets + // persisted — re-attaching restores the sidebar at the size it had. if (collapse && $userStore?.operator) { + sidebarWidth = widthAtStart stop() setDetached(true) return @@ -540,6 +543,8 @@ (offscreen ? `transform:translateX(calc(${-u} * (100% + 0.5rem)));` : '') } } + // Shared by the name and the colour pastille below, which travel to the picker together. + const JOIN_PICKER_MS = 110 // The home-page workspace name travels to the card's workspace picker as the card opens, // and back as it closes, so the name reads as one label joining the picker. It aims where // the picker will be once the card has finished sliding (the card's current translate is @@ -562,13 +567,36 @@ const scale = parseFloat(getComputedStyle(target).fontSize) / parseFloat(getComputedStyle(node).fontSize) return { - duration: 180, + duration: JOIN_PICKER_MS, easing: cubicInOut, css: (_t: number, u: number) => `transform-origin: left center; transform: translate(${dx * u}px, ${dy * u}px) scale(${1 + (scale - 1) * u}); opacity: ${1 - u};` } } + // How far the handle's colour pastille must travel to land on the card's workspace pastille. + // Measured when the card opens (the two discs are the same size, so it is a plain slide) and + // aimed where the picker will be once the card has finished sliding in, like the name above. + let pastilleJoin = $state({ dx: 0, dy: 0 }) + function openNavCard() { + const from = document + .querySelector('[data-nav-handle] circle') + ?.closest('svg') + ?.getBoundingClientRect() + const card = document.querySelector('[data-nav-card]') + const to = card + ?.querySelector('.wm-workspace-name') + ?.closest('button') + ?.querySelector('circle') + ?.closest('svg') + ?.getBoundingClientRect() + if (from && card && to) { + const slide = new DOMMatrix(getComputedStyle(card).transform).m41 + pastilleJoin = { dx: to.left - slide - from.left, dy: to.top - from.top } + } + menuOpen = true + } + // Matches the edge band's `duration-200`, so the drawer opens as the tint finishes. const EDGE_OPEN_DELAY_MS = 200 let edgeOpenTimer: ReturnType | undefined @@ -1099,7 +1127,7 @@ aria-hidden="true" onmouseenter={() => { clearTimeout(edgeOpenTimer) - edgeOpenTimer = setTimeout(() => (menuOpen = true), EDGE_OPEN_DELAY_MS) + edgeOpenTimer = setTimeout(openNavCard, EDGE_OPEN_DELAY_MS) }} onmouseleave={() => clearTimeout(edgeOpenTimer)} > @@ -1109,18 +1137,38 @@ card; a click docks the sidebar again. -->
-
(menuOpen = true)}> - setDetached(false)} - /> + +
+ + setDetached(false)} + /> + {#snippet text()}Attach sidebar{/snippet} +