+
diff --git a/frontend/src/lib/components/pageHeaderRegistry.svelte.ts b/frontend/src/lib/components/pageHeaderRegistry.svelte.ts
index 516e19965a..37bc7be0d3 100644
--- a/frontend/src/lib/components/pageHeaderRegistry.svelte.ts
+++ b/frontend/src/lib/components/pageHeaderRegistry.svelte.ts
@@ -43,8 +43,6 @@ export type PageHeaderContent = {
* running in a fork. The breadcrumb then names that workspace, and its fork part shows only
* when it really is a fork. */
actingWorkspaceId?: string
- /** False drops the band's bottom edge, for a page that draws its own first line. */
- barBorder?: boolean
/** Width in px, at the right of the viewport, that this page owns from the top down — a
* session's side panel. The band stops there instead of running over it, and the band floats
* above the page rather than pushing it down, so that column starts at the top. */
@@ -88,7 +86,6 @@ export const pageHeader = {
if (c.barPlacement !== undefined) merged.barPlacement = c.barPlacement
if (c.actingWorkspaceId !== undefined) merged.actingWorkspaceId = c.actingWorkspaceId
if (c.barRightInset !== undefined) merged.barRightInset = c.barRightInset
- if (c.barBorder !== undefined) merged.barBorder = c.barBorder
}
return merged
},
diff --git a/frontend/src/lib/components/sidebar/navHandlePlacement.svelte.ts b/frontend/src/lib/components/sidebar/navHandlePlacement.svelte.ts
index 3e8336eafd..a190b2726e 100644
--- a/frontend/src/lib/components/sidebar/navHandlePlacement.svelte.ts
+++ b/frontend/src/lib/components/sidebar/navHandlePlacement.svelte.ts
@@ -1,6 +1,9 @@
// Opening the detached sidebar's card, from the handle or from anywhere else that reaches for the
// nav (the left edge band, the AI chat layout's menu button).
let opener: (() => void) | undefined
+// Closing it once the pointer is off both the handle and the card, which the layout owns because
+// the card is its markup.
+let hoverClose: { schedule: () => void; cancel: () => void } | undefined
// Where the card's top edge goes: right under the handle, measured when it opens rather than fixed,
// so the card follows the handle's own height. The fallback matches the handle's resting bottom.
@@ -13,11 +16,17 @@ export const navHandleSlot = {
get cardTop(): number {
return cardTop
},
- /** The root layout owns the card, so it registers how to open it. */
- setOpener(open: () => void) {
+ /** The root layout owns the card, so it registers how to open and hover-close it. */
+ setOpener(open: () => void, close?: { schedule: () => void; cancel: () => void }) {
opener = open
+ hoverClose = close
+ },
+ /** Pointer left the handle: close, unless it lands on the card before the delay is up. */
+ scheduleClose() {
+ hoverClose?.schedule()
},
open() {
+ hoverClose?.cancel()
const handle = document.querySelector('[data-nav-handle]')
cardTop = handle
? Math.round(handle.getBoundingClientRect().bottom) + GAP_UNDER_HANDLE
diff --git a/frontend/src/routes/(root)/(logged)/+layout.svelte b/frontend/src/routes/(root)/(logged)/+layout.svelte
index af7e2878ff..9b52764d55 100644
--- a/frontend/src/routes/(root)/(logged)/+layout.svelte
+++ b/frontend/src/routes/(root)/(logged)/+layout.svelte
@@ -11,7 +11,7 @@
UserService,
WorkspaceService
} from '$lib/gen'
- import { capitalize, classNames, getModifierKey, sendUserToast } from '$lib/utils'
+ import { capitalize, classNames, debounce, getModifierKey, sendUserToast } from '$lib/utils'
import { useLocalStorageValue } from '$lib/svelte5Utils.svelte'
import { isSessionPreviewFrame } from '$lib/components/sessions/sessionMode.svelte'
import WorkspaceMenu from '$lib/components/sidebar/WorkspaceMenu.svelte'
@@ -507,8 +507,21 @@
let useDrawer = $derived(innerWidth < 768 || navDetached.val)
// Below 768px the burger row opens the drawer; wider, a detached sidebar has a floating handle.
let detachedFloating = $derived(navDetached.val && innerWidth >= 768)
+ // The peek card is a hover affordance, so it goes away when the pointer does — off the handle
+ // and off the card both. The delay is what lets the pointer cross the gap between them: they
+ // do not touch, and the leave of the one fires before the enter of the other.
+ const PEEK_CLOSE_DELAY_MS = 200
+ const { debounced: schedulePeekClose, clearDebounce: cancelPeekClose } = debounce(() => {
+ // Only the floating card peeks; the mobile drawer is opened deliberately and stays until
+ // it is dismissed.
+ if (detachedFloating) menuOpen = false
+ }, PEEK_CLOSE_DELAY_MS)
+
// The handle and the edge band both open the card this layout owns.
- navHandleSlot.setOpener(() => (menuOpen = true))
+ navHandleSlot.setOpener(() => (menuOpen = true), {
+ schedule: schedulePeekClose,
+ cancel: cancelPeekClose
+ })
// Set once the user docks or detaches, so the rail that mounts from then on skips its
// app-entry animation (`wm-sidebar-in`) and only plays `railMorph`.
@@ -1086,7 +1099,10 @@
clearTimeout(edgeOpenTimer)
edgeOpenTimer = setTimeout(() => navHandleSlot.open(), EDGE_OPEN_DELAY_MS)
}}
- onmouseleave={() => clearTimeout(edgeOpenTimer)}
+ onmouseleave={() => {
+ clearTimeout(edgeOpenTimer)
+ schedulePeekClose()
+ }}
>
{/if}
{#if useDrawer}
@@ -1126,8 +1142,11 @@
if (e.target === e.currentTarget) menuOpen = false
}}
>
+