- {@render burgerRow()}
{@render children?.()}
diff --git a/frontend/src/routes/(root)/(logged)/+layout.svelte b/frontend/src/routes/(root)/(logged)/+layout.svelte
index acc71834ad..8f15a82cd3 100644
--- a/frontend/src/routes/(root)/(logged)/+layout.svelte
+++ b/frontend/src/routes/(root)/(logged)/+layout.svelte
@@ -197,6 +197,15 @@
let sidebarTransitionClass = $derived(
resizingSidebar && !sidebarSnapping ? '' : 'transition-all duration-200 ease-in-out'
)
+ // The band spans two edges that different things move. Its left follows the rail, so it
+ // eases with it; its right follows a splitter the page owns (a session's side panel),
+ // which tracks the pointer. Easing that one leaves the band lying over the panel it is
+ // meant to stop beside — an empty strip across that panel's own header until it catches up.
+ let bandTransitionClass = $derived(
+ resizingSidebar && !sidebarSnapping
+ ? ''
+ : 'transition-[left,padding-left] duration-200 ease-in-out'
+ )
// Set while a drag is live so it can be torn down if the layout unmounts
// mid-drag (otherwise the window listeners would leak).
let stopSidebarResize: (() => void) | null = null
@@ -505,18 +514,20 @@
})
let innerWidth = $state(BROWSER ? window.innerWidth : 2000)
- // The drawer holds the sidebar below 768px and, at any width, while it is detached.
+ // The sidebar leaves the layout and becomes a card under the band's handle when it is
+ // detached, and below 768px, where a 13rem rail would take a third of the screen. One
+ // presentation for both: a phone gets the card the handle already opens, not a modal.
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)
+ // Hover opened the card, so the pointer leaving closes it — where there is a pointer to
+ // leave. A tap opened it on a touch screen, and it stays until it is dismissed.
+ const hoverPointer = BROWSER && window.matchMedia('(hover: hover)').matches
+ const peeksOnHover = $derived(useDrawer && hoverPointer)
// 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
+ if (peeksOnHover) menuOpen = false
}, PEEK_CLOSE_DELAY_MS)
// A deployed app owns the viewport: the band floats above it until the corner handle calls it
@@ -742,8 +753,12 @@
}
})
+ // Narrow enough that the rail is in the way: collapse it, transiently (see `collapsedState`).
+ // The drawer widths below 768 are included — there the sidebar is a modal panel over the page,
+ // and a window that has been that narrow should come back to the icon rail rather than to a
+ // 13rem one it no longer has room for.
function changeCollapsed() {
- if (innerWidth < 1248 && innerWidth >= 768 && !collapsedState) {
+ if (innerWidth < 1248 && !collapsedState) {
collapsedState = true
}
}
@@ -1104,6 +1119,164 @@
{/snippet}
+
+{#snippet sidebarColumn(panel: boolean)}
+ {@const collapsed = panel ? false : isCollapsed}
+
+ {#if !panel}
+
+
+ {/if}
+
+ {#if !embedded && sessionsSwitchShown}
+
+
+ (preserveMenuOnNextNav = true) : undefined}
+ />
+
+
+ {/if}
+
+ {#if sessionMode}
+
+
+
+
+ {@render settingsMenu(collapsed)}
+ {:else}
+
+
+
+
+ {@render quickLinks(collapsed)}
+
+ {#snippet children({ createMenu })}
+
+ {/snippet}
+
+ openSearchModal()}
+ isCollapsed={collapsed}
+ icon={Search}
+ label="Search"
+ class="!text-xs"
+ shortcut={`${getModifierKey()}k`}
+ />
+ {#if askAiShown}
+
+ aiChatManager.toggleOpen()}
+ isCollapsed={collapsed}
+ icon={WandSparkles}
+ iconProps={{ forceDarkMode: true }}
+ label="Ask AI"
+ class="!text-xs"
+ iconClasses="!text-ai"
+ shortcut={`${getModifierKey()}L`}
+ />
+ {/if}
+
+
+
+
+
+
+
+ {@render settingsMenu(collapsed)}
+
+
+ {/if}
+
+
+ {@render accountSetupBanner(collapsed)}
+
+
+
+
+ {@render brandMark(collapsed)}
+
+ {#if !panel || navDetached.val}
+ {@render sidebarToggle()}
+ {/if}
+
+ {#if !panel && !$userStore?.operator}
+
+ {/if}
+
+
+