feat: one sidebar column, shown as a rail or as a panel

The rail and the drawer held two copies of the same column — the same
sections, in the same order, in two pieces of markup, so a change to one
silently skipped the other. Both now render `sidebarColumn`, and the
shells keep only what differs: the rail its width, resize handle and
collapse button, the panel its outside-click catcher and its transform.

A phone gets that panel rather than a modal. There was a width test
deciding between two presentations; the card under the band's handle works
at any width, so it goes. What is left of the width test is the choice
between a rail and a panel, plus a `(hover: hover)` test — the old
`innerWidth >= 768` was standing in for "has a pointer", which is what
decides whether the panel closes when the pointer leaves it.

With the card everywhere, the backdrop, the `aria-modal` (nothing dims and
nothing traps focus) and the drawer's close cross all go: the band's
handle opens the panel and a tap outside closes it. The burger row goes
with them. Entering panel width now collapses the rail transiently, so
coming back to a wide window does not land on a rail the page was resized
away from.

The band also stops easing its right edge. That transition belongs to the
rail, which moves the band's left; its right follows a splitter the page
owns, and easing it left the band lying over a session's side panel — a
blank strip across that panel's own header for 200ms after every drag.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Guilhem Lemouel
2026-09-30 02:46:20 -07:00
co-authored by Claude Opus 5
parent 8fbff019c0
commit 93e96dde8e
2 changed files with 204 additions and 323 deletions
@@ -10,8 +10,6 @@
import { copilotInfo } from '$lib/aiStore'
import { aiChatManager } from './AIChatManager.svelte'
import { onDestroy } from 'svelte'
import Button from '$lib/components/common/button/Button.svelte'
import { Menu } from 'lucide-svelte'
import CreatedResourceActionDrawers from './CreatedResourceActionDrawers.svelte'
interface Props {
@@ -21,7 +19,6 @@
transitionClass?: string
isMobile?: boolean
children: any
onMenuOpen?: () => void
disableAi?: boolean
// Whether this layout loads the workspace AI config. It gates far more than
// the docked pane (code completion, metadata generation, the "open in AI
@@ -41,7 +38,6 @@
transitionClass = 'transition-all ease-in-out duration-200',
isMobile = false,
children,
onMenuOpen,
disableAi,
loadAiConfig = true,
showSessionsBetaBanner = false
@@ -82,24 +78,8 @@
aiChatManager.cancel('aiChatLayout destroyed')
historyManager.close()
})
</script>
{#snippet burgerRow()}
<div
class={classNames(
'py-0.5 px-4 sm:px-4 shadow-sm max-w-7xl md:hidden justify-start flex',
noBorder ? 'hidden' : ''
)}
>
<Button
variant="subtle"
unifiedSize="lg"
onClick={() => onMenuOpen?.()}
startIcon={{ icon: Menu }}
iconOnly
/>
</div>
{/snippet}
</script>
{#if !disableAi}
<CreatedResourceActionDrawers />
@@ -112,7 +92,6 @@
>
<main class="flex-1 flex flex-col min-h-0">
<div class="relative w-full flex-1 flex flex-col min-h-0">
{@render burgerRow()}
<div class="flex-1 min-h-0">
{@render children?.()}
</div>
@@ -136,12 +115,14 @@
{/if}
</Splitpanes>
{:else}
<!-- The scroll lives here, below the page header, exactly as it does in the docked-chat layout
above: a page taller than the window scrolls inside this box, so the band stays on screen
instead of leaving with the content. -->
<div
id="content"
class={classNames('flex-1 min-h-0 flex flex-col', transitionClass)}
class={classNames('flex-1 min-h-0 flex flex-col overflow-y-auto', transitionClass)}
style:padding-left="{contentPadLeft}rem"
>
{@render burgerRow()}
<div class="flex-1 min-h-0 flex flex-col">
{@render children?.()}
</div>
+199 -299
View File
@@ -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 @@
</div>
{/snippet}
<!-- The sidebar's one column, rendered in whichever shell the width calls for: the rail docked
at the left edge, or the same column as a panel over the page (`panel`) below 768px and
whenever the nav is detached. The shells differ, the sections do not, so a change to the
rail cannot miss the panel. -->
{#snippet sidebarColumn(panel: boolean)}
{@const collapsed = panel ? false : isCollapsed}
<div
class={twMerge(
'flex flex-col',
panel
? 'h-full w-52'
: 'flex-1 min-h-0 shadow-[inset_-1px_0_0_0_rgb(var(--color-border-light))] dark:shadow-[inset_-1px_0_0_0_#374151] [html.github-dark_&]:shadow-[inset_-1px_0_0_0_rgb(var(--color-border-light))]'
)}
style:background-color={darkMode ? SIDEBAR_BG_DARK : SIDEBAR_BG}
>
{#if !panel}
<!-- Resize handle straddling the right edge: drag to resize, into the
left snap zone to collapse, or out of it to expand. -->
<div
role="separator"
aria-orientation="vertical"
aria-label="Resize sidebar"
title="Drag to resize"
class={classNames(
'absolute inset-y-0 -right-0.5 w-1.5 cursor-col-resize z-50 transition-colors',
resizingSidebar ? '' : 'hover:bg-surface-hover'
)}
onpointerdown={startSidebarResize}
></div>
{/if}
<!-- Top row: the workspace ⇄ sessions switch, level with the page header's own
row so the two read as one band. The workspace picker itself lives in that
header, which is why nothing names the workspace here. -->
{#if !embedded && sessionsSwitchShown}
<div
class="flex-shrink-0 px-2 gap-1 flex {collapsed
? 'flex-col items-center py-2'
: 'h-12 items-center'}"
>
<div class="min-w-0 flex-1">
<SessionModeSwitch
mode={sessionMode ? 'session' : 'nav'}
isCollapsed={collapsed}
onToggle={panel ? () => (preserveMenuOnNextNav = true) : undefined}
/>
</div>
</div>
{/if}
{#if sessionMode}
<!-- Session mode: the session list owns the column. Workspace nav moves
into the preview side panel (the iframe renders its own nav). -->
<div class="px-2 py-2 flex flex-col gap-1 flex-1 min-h-0 overflow-y-auto">
<SessionPicker isCollapsed={collapsed} embedded collapsible={false} />
</div>
{@render settingsMenu(collapsed)}
{:else}
<!-- Navigation mode: Home → Settings scroll as ONE block with an
overflow fade hint. The three sections (Home/Runs + Favorites +
Search / workspace items / instance settings) are spaced by the
scroll area's gap rather than each scrolling on its own. -->
<SidebarScrollArea color={darkMode ? SIDEBAR_BG_DARK : SIDEBAR_BG}>
<!-- Section 1: Home/Runs + Favorites + Search -->
<div class="px-2 pt-1 flex flex-col gap-1">
{@render quickLinks(collapsed)}
<Menubar class="flex flex-col gap-1">
{#snippet children({ createMenu })}
<FavoriteMenu
{createMenu}
favoriteLinks={favoriteManager.current}
isCollapsed={collapsed}
/>
{/snippet}
</Menubar>
<MenuButton
stopPropagationOnClick={true}
on:click={() => openSearchModal()}
isCollapsed={collapsed}
icon={Search}
label="Search"
class="!text-xs"
shortcut={`${getModifierKey()}k`}
/>
{#if askAiShown}
<!-- Legacy Ask-AI pane, shown only when the user opted out of the
AI Sessions beta (otherwise SessionModeSwitch replaces it). -->
<MenuButton
stopPropagationOnClick={true}
on:click={() => aiChatManager.toggleOpen()}
isCollapsed={collapsed}
icon={WandSparkles}
iconProps={{ forceDarkMode: true }}
label="Ask AI"
class="!text-xs"
iconClasses="!text-ai"
shortcut={`${getModifierKey()}L`}
/>
{/if}
</div>
<!-- Section 2: workspace items -->
<SidebarContent
isCollapsed={collapsed}
showSecondary={false}
excludeMainLabels={['Home', 'Runs']}
numUnacknowledgedCriticalAlerts={isCriticalAlertsUiMuted
? 0
: numUnacknowledgedCriticalAlerts}
/>
<!-- Section 3: instance settings — mt-auto drops it to the bottom
when the column has spare room, and collapses back to the normal
inter-section gap once the content overflows and scrolls. -->
<div class="mt-auto">
{@render settingsMenu(collapsed)}
</div>
</SidebarScrollArea>
{/if}
<div class="flex-shrink-0">
{@render accountSetupBanner(collapsed)}
<SidebarUsage isCollapsed={collapsed} />
</div>
<div
class="flex-shrink-0 flex pt-3 pb-3.5 {collapsed
? 'flex-col items-center gap-3'
: 'items-center justify-between px-4'}"
>
{@render brandMark(collapsed)}
<div class="flex {collapsed ? 'flex-col gap-3' : 'gap-3'}">
{#if !panel || navDetached.val}
{@render sidebarToggle()}
{/if}
<!-- Collapsing belongs to the rail: the panel is over the page already, and the way
out of it is the handle in the band or a click outside. -->
{#if !panel && !$userStore?.operator}
<button
class={SIDEBAR_ICON_BUTTON}
title={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
onclick={() => {
collapsedState = !collapsedState
// Manual toggle is the persisted preference (auto-collapse isn't).
collapsePref.val = collapsedState
}}
>
{#if collapsed}
<PanelLeftOpen size={16} class="flex-shrink-0 text-hint" />
{:else}
<PanelLeftClose size={16} class="flex-shrink-0 text-hint" />
{/if}
</button>
{/if}
</div>
</div>
</div>
{/snippet}
<UserSettings bind:this={userSettings} showMcpMode={true} />
{#if accountSetup.pending}
<FinishAccountSetup
@@ -1130,7 +1303,7 @@
<div class="fixed inset-0 z-50 cursor-col-resize select-none"></div>
{/if}
{#if !menuHidden}
{#if detachedFloating && !menuOpen && !devOnly}
{#if navDetached.val && hoverPointer && !menuOpen && !devOnly}
<!-- Edge band: the other way to reach a detached sidebar. It opens only once its
tint has faded in, so a pointer brushing the screen edge does not open it. -->
<div
@@ -1154,33 +1327,21 @@
devOnly ? 'hidden' : ''
)}
role="dialog"
aria-modal="true"
>
<!-- The detached card floats over the page without dimming it: it opens on hover,
so a backdrop would flash on every pass of the left edge. -->
{#if !detachedFloating}
<div
class={classNames(
'fixed inset-0 bg-black/50 transition-opacity ease-linear duration-300 z-40',
menuOpen ? 'opacity-100' : 'opacity-0'
)}
></div>
{/if}
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
<!-- Detached, the drawer is a card floating over the page, inset and rounded. Over an
app it is not: the app owns the window, and the sidebar revealed beside it reads
as the panel the header sits on — flush to the left and bottom edges, square,
with one border down its right side. -->
<!-- Detached, this outside-click catcher starts where the card does rather than at
the top of the viewport: it would otherwise cover the header's toggle, whose
hover opened the card, and swallow the click meant to put the sidebar back. -->
<!-- The card floats over the page, inset and rounded. Over an app it is not: the app
owns the window, and the sidebar revealed beside it reads as the panel the header
sits on — flush to the left and bottom edges, square, with one border down its
right side. -->
<!-- This outside-click catcher starts where the card does rather than at the top of
the viewport: it would otherwise cover the header's toggle, whose hover opened the
card, and swallow the click meant to put the sidebar back. -->
<div
class={classNames(
'fixed left-0 right-0 bottom-0 flex z-40',
detachedFloating ? (fullBleed ? '' : 'pl-1 pb-2') : 'top-0'
fullBleed ? '' : 'pl-1 pb-2'
)}
style:top={detachedFloating ? `${navHandleSlot.cardTop}px` : undefined}
style:top="{navHandleSlot.cardTop}px"
onclick={(e) => {
if (e.target === e.currentTarget) menuOpen = false
}}
@@ -1199,143 +1360,16 @@
if (bandPeek) scheduleBandHide()
}}
class={classNames(
'relative flex-1 flex flex-col max-w-min w-full bg-surface transition ease-in-out duration-300 transform',
detachedFloating
? fullBleed
? 'border-r shadow-lg overflow-hidden'
: 'rounded-lg border shadow-lg overflow-hidden'
: '',
'relative flex-1 flex flex-col max-w-min w-full bg-surface transition ease-in-out duration-300 transform shadow-lg overflow-hidden',
fullBleed ? 'border-r' : 'rounded-lg border',
menuOpen
? 'translate-x-0'
: detachedFloating && !fullBleed
? '-translate-x-[calc(100%+0.5rem)]'
: '-translate-x-full'
: fullBleed
? '-translate-x-full'
: '-translate-x-[calc(100%+0.5rem)]'
)}
>
<div
class={classNames(
'absolute top-0 right-4 -mr-12 pt-2 ease-in-out duration-300',
menuOpen ? 'opacity-100' : 'opacity-0',
detachedFloating ? 'hidden' : ''
)}
>
<button
type="button"
onclick={() => {
menuOpen = !menuOpen
}}
class="ml-1 flex items-center justify-center h-6 w-6 rounded-full focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white border border-white"
aria-label="Close"
>
<svg
class="h-4 w-4 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
aria-hidden="true"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div
class="h-full flex flex-col"
style:background-color={darkMode ? SIDEBAR_BG_DARK : SIDEBAR_BG}
>
<!-- Top row: the workspace ⇄ sessions switch, as in the docked rail. -->
{#if !embedded && sessionsSwitchShown}
<div class="flex-shrink-0 px-2 h-12 w-52 flex items-center">
<SessionModeSwitch
mode={sessionMode ? 'session' : 'nav'}
onToggle={() => (preserveMenuOnNextNav = true)}
/>
</div>
{/if}
{#if sessionMode}
<!-- Session mode: the session list owns the rail.
w-52 cap (matches the desktop sidebar width): the drawer is
max-w-min, and long session titles (nowrap before truncation)
would otherwise inflate its min-content width to the full
text width. -->
<div class="px-2 py-2 w-52 flex-1 min-h-0 overflow-y-auto">
<SessionPicker isCollapsed={false} embedded collapsible={false} />
</div>
{@render settingsMenu(false)}
{:else}
<!-- Navigation mode: Home → Settings scroll as ONE block with an
overflow fade hint (same treatment as the desktop rail). -->
<SidebarScrollArea color={darkMode ? SIDEBAR_BG_DARK : SIDEBAR_BG}>
<!-- Section 1: Home/Runs + Favorites + Search -->
<div class="px-2 pt-1 w-52 flex flex-col gap-1">
{@render quickLinks(false)}
<Menubar>
{#snippet children({ createMenu })}
<FavoriteMenu {createMenu} favoriteLinks={favoriteManager.current} />
{/snippet}
</Menubar>
<MenuButton
stopPropagationOnClick={true}
on:click={() => openSearchModal()}
isCollapsed={false}
icon={Search}
label="Search"
class="!text-xs"
shortcut={`${getModifierKey()}k`}
/>
{#if askAiShown}
<!-- Legacy Ask-AI pane, shown only when the user opted out of the
AI Sessions beta (otherwise SessionModeSwitch replaces it). -->
<MenuButton
stopPropagationOnClick={true}
on:click={() => aiChatManager.toggleOpen()}
isCollapsed={false}
icon={WandSparkles}
iconProps={{ forceDarkMode: true }}
label="Ask AI"
class="!text-xs"
iconClasses="!text-ai"
shortcut={`${getModifierKey()}L`}
/>
{/if}
</div>
<!-- Section 2: workspace items -->
<SidebarContent
isCollapsed={false}
showSecondary={false}
excludeMainLabels={['Home', 'Runs']}
numUnacknowledgedCriticalAlerts={isCriticalAlertsUiMuted
? 0
: numUnacknowledgedCriticalAlerts}
/>
<!-- Section 3: instance settings — mt-auto drops it to the bottom
when there is spare room (see the desktop rail for the rationale). -->
<div class="mt-auto">
{@render settingsMenu(false)}
</div>
</SidebarScrollArea>
{/if}
<div class="w-52">
{@render accountSetupBanner(false)}
<SidebarUsage isCollapsed={false} />
</div>
<!-- Same footer slot the docked rail puts it in, so the control that hides the
sidebar and the one that brings it back sit in the same place. -->
<div class="px-4 pt-3 pb-3.5 w-52 flex items-center justify-between">
{@render brandMark(false)}
{#if detachedFloating}
<div class="flex gap-3">
{@render sidebarToggle()}
</div>
{/if}
</div>
</div>
{@render sidebarColumn(true)}
</div>
</div>
</div>
@@ -1353,141 +1387,7 @@
in:railMorph={{ offscreen: false }}
out:railMorph={{ offscreen: true }}
>
<div
class="flex-1 flex flex-col min-h-0 shadow-[inset_-1px_0_0_0_rgb(var(--color-border-light))] dark:shadow-[inset_-1px_0_0_0_#374151] [html.github-dark_&]:shadow-[inset_-1px_0_0_0_rgb(var(--color-border-light))]"
style:background-color={darkMode ? SIDEBAR_BG_DARK : SIDEBAR_BG}
>
<!-- Resize handle straddling the right edge: drag to resize, into the
left snap zone to collapse, or out of it to expand. -->
<div
role="separator"
aria-orientation="vertical"
aria-label="Resize sidebar"
title="Drag to resize"
class={classNames(
'absolute inset-y-0 -right-0.5 w-1.5 cursor-col-resize z-50 transition-colors',
resizingSidebar ? '' : 'hover:bg-surface-hover'
)}
onpointerdown={startSidebarResize}
></div>
<!-- Top row: the workspace ⇄ sessions switch, level with the page header's own
row so the two read as one band. The workspace picker itself lives in that
header, which is why nothing names the workspace here. -->
{#if !embedded && sessionsSwitchShown}
<div
class="flex-shrink-0 px-2 gap-1 flex {isCollapsed
? 'flex-col items-center py-2'
: 'h-12 items-center'}"
>
<div class="min-w-0 flex-1">
<SessionModeSwitch mode={sessionMode ? 'session' : 'nav'} {isCollapsed} />
</div>
</div>
{/if}
{#if sessionMode}
<!-- Session mode: the session list owns the rail. Workspace nav moves
into the preview side panel (the iframe renders its own nav). -->
<div class="px-2 py-2 flex flex-col gap-1 flex-1 min-h-0 overflow-y-auto">
<SessionPicker {isCollapsed} embedded collapsible={false} />
</div>
{@render settingsMenu(isCollapsed)}
{:else}
<!-- Navigation mode: Home → Settings scroll as ONE block with an
overflow fade hint. The three sections (Home/Runs + Favorites +
Search / workspace items / instance settings) are spaced by the
scroll area's gap rather than each scrolling on its own. -->
<SidebarScrollArea color={darkMode ? SIDEBAR_BG_DARK : SIDEBAR_BG}>
<!-- Section 1: Home/Runs + Favorites + Search -->
<div class="px-2 pt-1 flex flex-col gap-1">
{@render quickLinks(isCollapsed)}
<Menubar class="flex flex-col gap-1">
{#snippet children({ createMenu })}
<FavoriteMenu
{createMenu}
favoriteLinks={favoriteManager.current}
{isCollapsed}
/>
{/snippet}
</Menubar>
<MenuButton
stopPropagationOnClick={true}
on:click={() => openSearchModal()}
{isCollapsed}
icon={Search}
label="Search"
class="!text-xs"
shortcut={`${getModifierKey()}k`}
/>
{#if askAiShown}
<!-- Legacy Ask-AI pane, shown only when the user opted out of the
AI Sessions beta (otherwise SessionModeSwitch replaces it). -->
<MenuButton
stopPropagationOnClick={true}
on:click={() => aiChatManager.toggleOpen()}
{isCollapsed}
icon={WandSparkles}
iconProps={{ forceDarkMode: true }}
label="Ask AI"
class="!text-xs"
iconClasses="!text-ai"
shortcut={`${getModifierKey()}L`}
/>
{/if}
</div>
<!-- Section 2: workspace items -->
<SidebarContent
{isCollapsed}
showSecondary={false}
excludeMainLabels={['Home', 'Runs']}
numUnacknowledgedCriticalAlerts={isCriticalAlertsUiMuted
? 0
: numUnacknowledgedCriticalAlerts}
/>
<!-- Section 3: instance settings — mt-auto drops it to the bottom
when the column has spare room, and collapses back to the normal
inter-section gap once the content overflows and scrolls. -->
<div class="mt-auto">
{@render settingsMenu(isCollapsed)}
</div>
</SidebarScrollArea>
{/if}
<div class="flex-shrink-0">
{@render accountSetupBanner(isCollapsed)}
<SidebarUsage {isCollapsed} />
</div>
<div
class="flex-shrink-0 flex pt-3 pb-3.5 {isCollapsed
? 'flex-col items-center gap-3'
: 'items-center justify-between px-4'}"
>
{@render brandMark(isCollapsed)}
<div class="flex {isCollapsed ? 'flex-col gap-3' : 'gap-3'}">
{@render sidebarToggle()}
{#if !$userStore?.operator}
<button
class={SIDEBAR_ICON_BUTTON}
title={isCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
onclick={() => {
collapsedState = !collapsedState
// Manual toggle is the persisted preference (auto-collapse isn't).
collapsePref.val = collapsedState
}}
>
{#if isCollapsed}
<PanelLeftOpen size={16} class="flex-shrink-0 text-hint" />
{:else}
<PanelLeftClose size={16} class="flex-shrink-0 text-hint" />
{/if}
</button>
{/if}
</div>
</div>
</div>
{@render sidebarColumn(false)}
</div>
{/if}
<!-- Legacy menu -->
@@ -1608,7 +1508,7 @@
bandPeek
? (bandPeekSettled ? 'transition-transform duration-150 ease-out ' : '') +
(bandShown ? 'translate-y-0' : '-translate-y-full pointer-events-none')
: sidebarTransitionClass
: bandTransitionClass
)}
style:left={floatBand ? `${leftInset}rem` : undefined}
style:right={floatBand ? `${rightInset ?? 0}px` : undefined}
@@ -1633,6 +1533,7 @@
<PageHeaderBar
navHidden={menuHidden}
hideNavHandle={bandPeek}
panelled={useDrawer}
onUnpin={fullBleed && appHeaderPinned.val
? () => (appHeaderPinned.val = false)
: undefined}
@@ -1733,7 +1634,6 @@
sidebarWidth={railWidth}
transitionClass={sidebarTransitionClass}
isMobile={useDrawer}
onMenuOpen={() => navHandleSlot.open()}
/>
</div>
</div>