import type { Snippet } from 'svelte'; import type { HTMLAttributes } from 'svelte/elements'; import type { SidebarSlots } from '../../primitives/Sidebar/sidebar.variants.js'; import type { DisclosureTriggerProps } from '../../utils/use-disclosure.svelte.js'; import type { SidebarLayoutSlots, SidebarLayoutVariants } from './sidebar-layout.variants.js'; /** * Snippet payload for the `mobileHeader` slot. Receives an opener for the * sidebar overlay so the consumer can wire a hamburger button without * threading state through the layout. */ export interface MobileHeaderContext { /** Open the sidebar (mobile overlay or collapsible panel). */ openSidebar: () => void; /** Flip the sidebar open or closed. The `toggle` snippet is the wired-up version of this. */ toggle: () => void; /** Current open state of the sidebar. */ sidebarOpen: boolean; } /** * Snippet payload for the `toggle` slot — Collapsible's `trigger` vocabulary * (`open`, `toggle`, `triggerId`, `contentId`) plus the ready-made attribute * record from `useDisclosure`. * * The layout renders the snippet at both seams it owns (the rail edge on * desktop, the header on mobile) and hands each render its **own** * `triggerId`, so spreading `triggerProps` on both cannot produce a duplicate * id. `contentId` is the sidebar panel and is the same for both. */ export interface SidebarToggleContext { /** Whether the sidebar is currently open. */ open: boolean; /** Flip it. */ toggle: () => void; /** DOM id for this render of the trigger — already inside `triggerProps`. */ triggerId: string; /** DOM id of the sidebar panel this trigger controls. */ contentId: string; /** `id` / `aria-expanded` / `aria-controls` — spread these onto your button. */ triggerProps: DisclosureTriggerProps; } /** * Slot keys for `slotClasses`. `SidebarLayoutSlots` is the layout's own set, * derived from its `tv()` config; each remaining key is one slot of the * embedded `` under a `sidebar` prefix. * * Both halves of that forwarding derive from `sidebarVariants` — this union by * template literal, the mapping in `SidebarLayout.svelte` by walking the same * config — so a Sidebar slot renamed, added or dropped moves the two together. * They used to be five literals here mapped by hand, and three edits to that * map type-checked while reaching no element: a mistyped source key, a swapped * pair, and a deleted line (#346). */ type SidebarLayoutSlot = SidebarLayoutSlots | SidebarForwardKey; /** * The `slotClasses` keys forwarded to the embedded `` — one per slot * it declares, under a `sidebar` prefix. * * Exported because `SidebarLayout.svelte` *builds* the key it reads and * annotates it with this type. That is what writes the prefix once for both * halves: mistyping it in the builder is a compile error, where the resolved * record it indexes is a `Record` that would otherwise accept * any string and quietly return nothing (measured). */ export type SidebarForwardKey = `sidebar${Capitalize}`; /** * @summary The app shell — sidebar, content, and the mobile header that opens it. * @description App-shell layout that wires a `` to a main content * region and an optional mobile header. Use this whenever you want a * permanent sidebar on desktop with a hamburger overlay on mobile — it * resolves the CSS-variable scoping so the main content offset works without * boilerplate. * * The component renders the sidebar internally; consumers configure it via * `sidebarHeader`, `sidebar`, and `sidebarFooter` snippets and bind `open` * for the mobile overlay (or for collapsible mode at all viewports). * * For non-shell sidebars (right-side detail panels, drawers inside a page), * keep using the `` primitive directly. * * @tag layout * @tag navigation * @related Sidebar * * @example Default app shell with mobile header * ```svelte * * * * {#snippet sidebarHeader()} * My App * {/snippet} * * {#snippet sidebar()} * * {/snippet} * * {#snippet mobileHeader({ openSidebar })} * * My App * {/snippet} * *

Page content

*
* ``` * * @example Collapsible — the toggle snippet, remembered across reloads * ```svelte * * * * {#snippet sidebarHeader()}App{/snippet} * {#snippet sidebar()}{/snippet} * * {#snippet toggle(rail)} * * {/snippet} * * * * ``` */ export interface SidebarLayoutProps extends Omit, 'children'> { /** * Sidebar visibility. In `responsive` mode this only affects the mobile * overlay. In `collapsible` mode it controls visibility at all viewports. * Supports `bind:open`. * @default false */ open?: boolean; /** * Sidebar mode. * - `responsive` (default): permanent on desktop (≥1024px), slide-in overlay on mobile. * - `collapsible`: toggleable at all viewports — width animation on desktop, overlay on mobile. * @default 'responsive' * @summary Whether the sidebar is permanent on desktop, or toggleable at every width. */ mode?: 'responsive' | 'collapsible'; /** Which edge the sidebar attaches to. @default 'left' */ side?: SidebarLayoutVariants['side']; /** * Sidebar panel width. Single source of truth — the layout exposes it as * `--sidebar-width` (constant) and `--sidebar-effective-width` (animates to * `0` when collapsed) on the layout root, so the main content offset stays * in sync automatically. * @default '16rem' * @summary How wide the sidebar panel is; the content offset follows it automatically. */ sidebarWidth?: string; /** Close the mobile sidebar overlay when pressing Escape. @default true */ closeOnEscape?: boolean; /** Close the mobile sidebar overlay when clicking the backdrop. @default true */ closeOnBackdropClick?: boolean; /** Maximum width of the centered content column. @default 'xl' */ contentMaxWidth?: SidebarLayoutVariants['contentMaxWidth']; /** Fires when the sidebar open state changes. */ onOpenChange?: (open: boolean) => void; /** Sidebar header (above the scrollable nav). */ sidebarHeader?: Snippet; /** Sidebar main content — typically a `