entasis 0.13.5 → 0.13.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/components/Address/Address.svelte +44 -0
  2. package/dist/components/Address/Address.svelte.d.ts +4 -0
  3. package/dist/components/Address/address.d.ts +99 -0
  4. package/dist/components/Address/address.google.d.ts +89 -0
  5. package/dist/components/Address/address.google.fixtures.json +808 -0
  6. package/dist/components/Address/address.google.js +184 -0
  7. package/dist/components/Address/address.js +109 -0
  8. package/dist/components/Address/address.mcp.d.ts +1 -0
  9. package/dist/components/Address/address.mcp.js +100 -0
  10. package/dist/components/Address/address.props.d.ts +26 -0
  11. package/dist/components/Address/address.props.js +1 -0
  12. package/dist/components/Address/address.provider.d.ts +19 -0
  13. package/dist/components/Address/address.provider.js +69 -0
  14. package/dist/components/Address/address.theme.d.ts +63 -0
  15. package/dist/components/Address/address.theme.js +42 -0
  16. package/dist/components/Address/index.d.ts +6 -0
  17. package/dist/components/Address/index.js +5 -0
  18. package/dist/components/Form/AddressInput/AddressInput.svelte +482 -0
  19. package/dist/components/Form/AddressInput/AddressInput.svelte.d.ts +4 -0
  20. package/dist/components/Form/AddressInput/addressInput.mcp.d.ts +1 -0
  21. package/dist/components/Form/AddressInput/addressInput.mcp.js +110 -0
  22. package/dist/components/Form/AddressInput/addressInput.props.d.ts +72 -0
  23. package/dist/components/Form/AddressInput/addressInput.props.js +1 -0
  24. package/dist/components/Form/AddressInput/addressInput.theme.d.ts +63 -0
  25. package/dist/components/Form/AddressInput/addressInput.theme.js +55 -0
  26. package/dist/components/Form/AddressInput/index.d.ts +3 -0
  27. package/dist/components/Form/AddressInput/index.js +2 -0
  28. package/dist/components/Form/Field/field.d.ts +5 -2
  29. package/dist/components/Form/Field/field.state.svelte.js +4 -0
  30. package/dist/components/Form/Field/index.d.ts +1 -1
  31. package/dist/components/Form/Field/schemas.js +8 -0
  32. package/dist/components/Form/Form/FormInputRenderer.svelte +6 -0
  33. package/dist/components/Form/Form/form.d.ts +6 -0
  34. package/dist/components/Form/Form/form.mcp.d.ts +1 -1
  35. package/dist/components/Form/Form/form.mcp.js +1 -1
  36. package/dist/components/Form/Form/form.state.svelte.d.ts +142 -11
  37. package/dist/components/Form/Form/form.state.svelte.js +2 -0
  38. package/dist/components/Form/Form/visibility.d.ts +186 -20
  39. package/dist/components/Form/MultiStepForm/multiStepForm.state.svelte.d.ts +136 -0
  40. package/dist/components/Form/SignatureInput/SignatureInput.svelte +327 -0
  41. package/dist/components/Form/SignatureInput/SignatureInput.svelte.d.ts +4 -0
  42. package/dist/components/Form/SignatureInput/index.d.ts +4 -0
  43. package/dist/components/Form/SignatureInput/index.js +2 -0
  44. package/dist/components/Form/SignatureInput/signatureInput.mcp.d.ts +1 -0
  45. package/dist/components/Form/SignatureInput/signatureInput.mcp.js +74 -0
  46. package/dist/components/Form/SignatureInput/signatureInput.props.d.ts +29 -0
  47. package/dist/components/Form/SignatureInput/signatureInput.props.js +1 -0
  48. package/dist/components/Form/SignatureInput/signatureInput.strokes.d.ts +77 -0
  49. package/dist/components/Form/SignatureInput/signatureInput.strokes.js +149 -0
  50. package/dist/components/Form/SignatureInput/signatureInput.theme.d.ts +99 -0
  51. package/dist/components/Form/SignatureInput/signatureInput.theme.js +70 -0
  52. package/dist/components/Sidebar/Sidebar.svelte +1 -1
  53. package/dist/components/Sidebar/SidebarMenuItem.svelte +59 -15
  54. package/dist/components/Sidebar/SidebarMenuSubItem.svelte +43 -12
  55. package/dist/components/Sidebar/SidebarMenuText.svelte +60 -0
  56. package/dist/components/Sidebar/SidebarMenuText.svelte.d.ts +18 -0
  57. package/dist/components/Sidebar/sidebar.mcp.d.ts +1 -1
  58. package/dist/components/Sidebar/sidebar.mcp.js +53 -0
  59. package/dist/components/Sidebar/sidebar.props.d.ts +31 -0
  60. package/dist/components/Sidebar/sidebar.theme.d.ts +99 -0
  61. package/dist/components/Sidebar/sidebar.theme.js +100 -9
  62. package/dist/generated/componentAliases.d.ts +3 -0
  63. package/dist/generated/componentAliases.js +3 -0
  64. package/dist/generated/componentContract.d.ts +46 -1
  65. package/dist/generated/componentContract.js +113 -0
  66. package/dist/generated/componentMcpRegistry.js +6 -0
  67. package/dist/i18n/ar.js +18 -0
  68. package/dist/i18n/de.js +18 -0
  69. package/dist/i18n/en.d.ts +18 -0
  70. package/dist/i18n/en.js +18 -0
  71. package/dist/i18n/es.js +18 -0
  72. package/dist/i18n/fr.js +18 -0
  73. package/dist/i18n/ja.js +18 -0
  74. package/dist/i18n/pt.js +18 -0
  75. package/dist/i18n/zh.js +18 -0
  76. package/package.json +25 -1
@@ -11,6 +11,7 @@
11
11
  import { sidebarInwardPhysicalSide, sidebarTooltip } from './sidebar-position.js';
12
12
  import SidebarAction from './SidebarAction.svelte';
13
13
  import SidebarIcon from './SidebarIcon.svelte';
14
+ import SidebarMenuText from './SidebarMenuText.svelte';
14
15
  import { useSidebarSuffixWidth } from './sidebar.suffix.svelte.js';
15
16
  import { useSidebarTheme, type SidebarThemeProps } from './sidebar.theme.js';
16
17
 
@@ -39,6 +40,9 @@
39
40
  // tile, so an untinted row never inherits the ambient `data-color`.
40
41
  const hasIconSurface = $derived(!!sub.icon && (sub.iconVariant === 'tile' || !!sub.iconColor));
41
42
  const hasBadge = $derived(sub.badge != null);
43
+ // On a two-line row the suffix and the badge sit on the description's line, so the label's
44
+ // line, and its meta, run to the same end on every row.
45
+ const twoLine = $derived(sub.description != null);
42
46
  const suffixWidth = useSidebarSuffixWidth();
43
47
  const uid = $props.id();
44
48
  const suffixId = `${uid}-suffix`;
@@ -63,13 +67,18 @@
63
67
  // stays visible before the action, so a row with one reserves both at every width.
64
68
  endReserve: sub.suffix
65
69
  ? sub.action
66
- ? 'suffixAction'
67
- : 'suffix'
70
+ ? twoLine
71
+ ? 'suffixLineAction'
72
+ : 'suffixAction'
73
+ : twoLine
74
+ ? 'suffixLine'
75
+ : 'suffix'
68
76
  : !sub.action
69
77
  ? 'none'
70
78
  : hasBadge
71
79
  ? 'actionBelowMd'
72
80
  : 'action',
81
+ tall: sub.description != null || sub.media != null,
73
82
  className: sub.class
74
83
  })
75
84
  );
@@ -85,7 +94,11 @@
85
94
  </script>
86
95
 
87
96
  {#snippet entryContent()}
88
- {#if hasIconSurface}
97
+ {#if sub.media}
98
+ <span data-slot="sidebar-menu-media" data-sidebar="menu-media" class={classes.menuMedia()}>
99
+ {@render sub.media(api, sub)}
100
+ </span>
101
+ {:else if hasIconSurface}
89
102
  <span
90
103
  data-slot="sidebar-menu-icon"
91
104
  data-color={sub.iconColor}
@@ -100,20 +113,37 @@
100
113
  {:else}
101
114
  <SidebarIcon icon={sub.icon} />
102
115
  {/if}
103
- {#if hasBadge}
116
+ {#if sub.description != null || sub.meta != null}
117
+ <SidebarMenuText
118
+ label={sub.label}
119
+ description={sub.description}
120
+ meta={sub.meta}
121
+ suffix={!!sub.suffix}
122
+ trailing={twoLine && hasBadge ? badge : undefined}
123
+ size={subSize}
124
+ {theme}
125
+ />
126
+ {#if !twoLine && hasBadge}
127
+ {@render badge()}
128
+ {/if}
129
+ {:else if hasBadge}
104
130
  <span data-slot="sidebar-menu-sub-label">{sub.label}</span>
105
- <span
106
- data-slot="sidebar-menu-sub-badge"
107
- data-sidebar="menu-sub-badge"
108
- class={classes.badge({ size: subSize, action: sub.action && !sub.suffix ? 'sub' : 'none' })}
109
- >
110
- {sub.badge}
111
- </span>
131
+ {@render badge()}
112
132
  {:else}
113
133
  <span>{sub.label}</span>
114
134
  {/if}
115
135
  {/snippet}
116
136
 
137
+ {#snippet badge()}
138
+ <span
139
+ data-slot="sidebar-menu-sub-badge"
140
+ data-sidebar="menu-sub-badge"
141
+ class={classes.badge({ size: subSize, action: sub.action && !sub.suffix ? 'sub' : 'none' })}
142
+ >
143
+ {sub.badge}
144
+ </span>
145
+ {/snippet}
146
+
117
147
  <li
118
148
  bind:this={itemRef}
119
149
  data-slot="sidebar-menu-sub-item"
@@ -175,7 +205,8 @@
175
205
  class={classes.suffix({
176
206
  size: subSize,
177
207
  density,
178
- action: sub.action ? 'action' : 'none'
208
+ action: sub.action ? 'action' : 'none',
209
+ line: twoLine ? 'two' : 'one'
179
210
  })}
180
211
  {@attach suffixWidth.measure}
181
212
  >
@@ -0,0 +1,60 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { SidebarSize } from './sidebar.props.js';
4
+ import { useSidebarTheme, type SidebarThemeProps } from './sidebar.theme.js';
5
+
6
+ let {
7
+ label,
8
+ description,
9
+ meta,
10
+ size,
11
+ suffix = false,
12
+ trailing,
13
+ theme
14
+ }: {
15
+ label: string;
16
+ description?: string | Snippet;
17
+ /** Left out while the Sidebar is collapsed to icons. */
18
+ meta?: string | Snippet;
19
+ size: SidebarSize;
20
+ /** The row has a suffix, centred on the description's line. */
21
+ suffix?: boolean;
22
+ /** End of the description's line: the row's badge. */
23
+ trailing?: Snippet;
24
+ theme?: SidebarThemeProps;
25
+ } = $props();
26
+
27
+ const classes = $derived(useSidebarTheme(theme));
28
+ </script>
29
+
30
+ {#snippet text(content: string | Snippet)}
31
+ {#if typeof content === 'string'}{content}{:else}{@render content()}{/if}
32
+ {/snippet}
33
+
34
+ <!-- The label's line (the label, then the meta at its end) over the description's line. -->
35
+ <span data-slot="sidebar-menu-text" class={classes.menuText()}>
36
+ <span class={classes.menuTitleRow()}>
37
+ <span data-slot="sidebar-menu-label" class={classes.menuTitle()}>{label}</span>
38
+ {#if meta != null}
39
+ <span
40
+ data-slot="sidebar-menu-meta"
41
+ data-sidebar="menu-meta"
42
+ class={classes.menuMeta({ size })}
43
+ >
44
+ {@render text(meta)}
45
+ </span>
46
+ {/if}
47
+ </span>
48
+ {#if description != null}
49
+ <span class={classes.menuDescriptionRow({ suffix })}>
50
+ <span
51
+ data-slot="sidebar-menu-description"
52
+ data-sidebar="menu-description"
53
+ class={classes.menuSecondary({ size })}
54
+ >
55
+ {@render text(description)}
56
+ </span>
57
+ {@render trailing?.()}
58
+ </span>
59
+ {/if}
60
+ </span>
@@ -0,0 +1,18 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { SidebarSize } from './sidebar.props.js';
3
+ import { type SidebarThemeProps } from './sidebar.theme.js';
4
+ type $$ComponentProps = {
5
+ label: string;
6
+ description?: string | Snippet;
7
+ /** Left out while the Sidebar is collapsed to icons. */
8
+ meta?: string | Snippet;
9
+ size: SidebarSize;
10
+ /** The row has a suffix, centred on the description's line. */
11
+ suffix?: boolean;
12
+ /** End of the description's line: the row's badge. */
13
+ trailing?: Snippet;
14
+ theme?: SidebarThemeProps;
15
+ };
16
+ declare const SidebarMenuText: import("svelte").Component<$$ComponentProps, {}, "">;
17
+ type SidebarMenuText = ReturnType<typeof SidebarMenuText>;
18
+ export default SidebarMenuText;
@@ -1 +1 @@
1
- export declare const sidebarDescription = "\n# Sidebar Component\n\nSidebar navigation with data-driven groups, icon collapse, mobile drawer behavior,\nrecursive tree groups, header/footer rows, search, actions, and snippet escape hatches.\n\n## Import\n\n```svelte\n<script lang=\"ts\">\n\timport { Sidebar, type SidebarGroup } from 'entasis/sidebar';\n</script>\n```\n\n## Basic Usage\n\n```svelte\n<script lang=\"ts\">\n\timport { Sidebar, type SidebarGroup } from 'entasis/sidebar';\n\timport { houseIcon } from 'entasis/icons/house';\n\timport { gearIcon } from 'entasis/icons/gear';\n\n\tconst items: SidebarGroup[] = [\n\t\t{\n\t\t\tlabel: 'Workspace',\n\t\t\titems: [\n\t\t\t\t{ label: 'Dashboard', href: '/', icon: houseIcon, isActive: true },\n\t\t\t\t{ label: 'Settings', href: '/settings', icon: gearIcon }\n\t\t\t]\n\t\t}\n\t];\n</script>\n\n<Sidebar items={items}>\n\t{#snippet children({ toggle })}\n\t\t<header>\n\t\t\t<button type=\"button\" onclick={toggle}>Toggle</button>\n\t\t</header>\n\t\t<main>Page content</main>\n\t{/snippet}\n</Sidebar>\n```\n\n## AI-Safe Usage Contract\n\n1. Use `items` for normal navigation. Use `content` only when data-driven rows cannot express the layout.\n2. Use local Entasis icon snippets such as `houseIcon`, not Lucide component constructors.\n3. Use `MenuItem[]` from `entasis/menu` for `menu` and action dropdowns.\n4. Do not combine `menu` with `href` or `onclick` on the same row; use `action` for a trailing row menu.\n5. Keep `children`, `header`, `content`, `footer`, `banner`, and action snippets pure; they receive `SidebarApi`.\n6. Use `collapsible=\"icon\"` for icon rail behavior, `collapsible=\"offcanvas\"` for hidden desktop panels, and `collapsible=\"none\"` for fixed sidebars. Icon collapse automatically falls back to offcanvas when any data-driven row lacks an icon.\n7. Offcanvas sidebars reveal over the content from the screen edge by default when hidden; set `edgeReveal={false}` to disable that. A revealed hidden sidebar keeps its resize handle and dismisses through a small rectangular pointer tolerance.\n8. Set `keyboardShortcut={false}` when embedding Sidebar inside another shortcut-heavy surface.\n9. Sidebar owns navigation, resize mechanics, the lower application wall, and variant surface geometry. AppShell forwards its variant and composes PageShell inside that surface.\n10. Use `size` for typography, icon scale, and item height. Use `density` independently for section padding, gaps, and submenu spacing.\n11. Use `activityBar` for a persistent icon rail outside the panel (section switching, workspaces). Every item needs an `icon` and a `label`; the label is the accessible name and the tooltip. It is layout mode only: `mode=\"panel\"` renders the navigation panel alone. The rail does not change the panel by itself: keep the selected rail item in state from `onSelect`, mark it `isActive`, and either derive the Sidebar's `items` from it or, for an animated switch, give each rail item a view in `views` and set `view` from it.\n12. Use `expandOnHover` only with `collapsible=\"icon\"`. It is a temporary peek, not a toggle: the persisted collapsed state never changes, while the peeked panel renders with expanded semantics.\n13. Use `views` when the panel's contents change as the user moves through the app: sections switched from the activity bar or the route, and nested menus that open inside the panel. Key each view, set `parent` on nested ones, open them with a row's `view`, and drive `view` from state or the URL. Do not hand-animate `items` swaps.\n\n## Data Model\n\n### SidebarGroup\n- **label**: string - Group label, hidden in icon-collapsed mode.\n- **items**: SidebarMenuEntry[] - Menu rows.\n- **tree**: SidebarTreeNode[] - Recursive tree rows instead of menu items.\n- **action**: SidebarMenuActionDescriptor | SidebarMenuActionDescriptor[] | Snippet<[SidebarApi]> - Group actions pinned to the label's end. The label truncates on one line before them. Pass an array to pin several affordances (a `+` and a drag handle) to one group header; each renders as its own icon-only ghost button.\n- **collapsible**: boolean - Makes the group label a toggle.\n- **defaultOpen**: boolean - Initial collapsible group state.\n- **separator**: boolean - Divider before the group.\n\n### SidebarMenuEntry\n- **label**: string - Visible row label.\n- **icon**: SidebarIcon - Entasis icon snippet or string.\n- **iconColor**: Colors - Role tint for the leading icon, applied through `data-color`.\n- **iconVariant**: 'bare' | 'tile' - Leading icon treatment. `tile` paints a rounded square (`bg-color-muted text-color-muted-readable`) around the glyph, so per-project colour chips come from the role scale instead of hand-built markup.\n- **href**: string - Render as an anchor. Mutually exclusive with menu.\n- **onclick**: (event: MouseEvent) => void - Native click handler for button or anchor rows. Mutually exclusive with menu. On a row with `items` and no `href`, it runs before the submenu toggles, inline or in the collapsed flyout; `event.preventDefault()` skips the toggle.\n- **isActive**: boolean - Adds active styling and `aria-current=\"page\"`.\n- **disabled**: boolean - Disables button rows and marks anchor rows disabled.\n- **badge**: string | number - Trailing count/status, hidden in icon mode. It follows the label inside the row (before any chevron), so the label truncates before it whatever its width. On a row with an `action`, the badge steps aside while the action shows (hover, focus, open menu) from `md` up, and sits just before the always-visible action below `md`: the two never overlap.\n- **tooltip**: string - Entasis Tooltip content in icon mode. Defaults to label.\n- **items**: SidebarMenuSubEntry[] - Inline nested menu. Collapsed to icons on desktop, a row with `items` and no `href`, `view` or `menu` opens them in a flyout of links instead.\n- **collapsible**: boolean - Set false for an always-open submenu.\n- **defaultOpen**: boolean - Initial nested menu state.\n- **menu**: MenuItem[] - Popup menu opened from the full row. Mutually exclusive with href/onclick.\n- **menuSide** / **menuAlign**: SidebarMenuSide / 'start' | 'center' | 'end' - Where that menu opens. `menuSide` takes a physical side (`top`, `right`, `bottom`, `left`) or a logical one (`inline-start`, `inline-end`, mirrored in right-to-left). Left out, a desktop menu opens toward the page (`inline-end` of a left sidebar, `inline-start` of a right one, so it follows the reading direction) aligned to the row's start, and a mobile one below the row aligned to its end. The same pair sits on action descriptors and on `headerButton` / `footerButton`.\n- **view**: string - Key of the `views` entry the row opens, sliding it in; the row shows a trailing chevron. Mutually exclusive with href, menu and items.\n- **action**: SidebarMenuActionDescriptor | Snippet<[SidebarApi]> - Hover/focus trailing action.\n- **suffix**: Snippet<[SidebarApi, SidebarMenuEntry]> - Content at the row's end, beside the label: a count, a `Chip`, an icon, a status dot, its own `PopupMenu` trigger. It receives the row's own entry, so rows built as data share one snippet declared in markup (`{#snippet suffix(api, entry)}`) and tell themselves apart by `entry.href`. It renders beside the row's link or button, never inside it (`data-sidebar=\"menu-suffix\"`, drawn by the `suffix` theme part), so it may hold controls: a click or a keypress inside it never follows the row's `href` nor toggles it, and Tab reaches its controls right after the row, before the `action`. The row's link or button is `aria-describedby` the suffix, so the row reads with it (\"Dossiers\", \"20\"). The label truncates before it: the row reserves its measured width (`--sidebar-suffix-width`), and the `suffix` part bounds it (`max-w-[40%]` of the row), so a wide suffix clips instead of breaking the row. With an `action`, it stays visible just before the action at every width (the row keeps the action's room, and a `badge` then stays put instead of stepping aside). Hidden, `inert` and `aria-hidden` in icon-collapsed mode; shown in a hover peek and in the mobile drawer.\n\n### SidebarMenuSubEntry\nA row of an inline submenu (`items` on a menu entry). It carries the same row features as a\ntop-level entry, so nested navigation (pages filed in folders) keeps its menus, pins and tints\nwithout flattening the tree. Every field below `size` is optional, and a sub entry that sets none\nof them renders exactly as a plain one.\n- **label**: string - Visible row label.\n- **href** / **onclick** - Render as an anchor, or a button with a native click handler.\n- **icon**: SidebarIcon - Leading icon.\n- **isActive** / **disabled** / **size** - As on a top-level entry; `size` defaults to the Sidebar size.\n- **iconColor**: Colors - Role tint for the leading icon, applied through `data-color`.\n- **iconVariant**: 'bare' | 'tile' - `tile` paints the same tinted rounded square as a top-level row.\n- **badge**: string | number - Trailing count or status; the label truncates before it. It shares the row's end column with the parent rows' badges and makes way for the row's `action` as a top-level badge does.\n- **tooltip**: string - Tooltip text. The label is always visible on an inline sub row, so the tooltip is extra information: shown whenever one is set, never on mobile. The collapsed row's flyout lists only each entry's label, icon, `href` or `onclick`, and `isActive`/`disabled` state: it shows no tooltip, `badge`, `suffix` or `action`.\n- **class**: string - Classes applied to the row button.\n- **action**: SidebarMenuActionDescriptor | Snippet<[SidebarApi]> - Trailing action pinned to the row's end (`data-sidebar=\"menu-sub-action\"`), drawn by the `subAction` theme part: hidden until the row is hovered or focused from `md` up, kept visible while its menu is open, always visible below `md`. The label truncates before it.\n- **suffix**: Snippet<[SidebarApi, SidebarMenuSubEntry]> - Content at the row's end, beside the label: a count, a `Chip`, an icon, a status dot, its own `PopupMenu` trigger. It receives the row's own entry, so rows built as data share one snippet declared in markup (`{#snippet suffix(api, entry)}`) and tell themselves apart by `entry.href`. It renders beside the row's link or button, never inside it (`data-sidebar=\"menu-sub-suffix\"`, drawn by the `suffix` theme part), so it may hold controls: a click or a keypress inside it never follows the row's `href` nor toggles it, and Tab reaches its controls right after the row, before the `action`. The row's link or button is `aria-describedby` the suffix, so the row reads with it (\"Dossiers\", \"20\"). The label truncates before it: the row reserves its measured width (`--sidebar-suffix-width`), and the `suffix` part bounds it (`max-w-[40%]` of the row), so a wide suffix clips instead of breaking the row. With an `action`, it stays visible just before the action at every width (the row keeps the action's room, and a `badge` then stays put instead of stepping aside). Hidden, `inert` and `aria-hidden` in icon-collapsed mode; shown in a hover peek and in the mobile drawer.\n\n```svelte\n<script lang=\"ts\">\n\timport { Sidebar, type SidebarGroup } from 'entasis/sidebar';\n\timport { folderIcon } from 'entasis/icons/folder';\n\timport { fileTextIcon } from 'entasis/icons/fileText';\n\timport { pencilSimpleIcon } from 'entasis/icons/pencilSimple';\n\timport { pushPinIcon } from 'entasis/icons/pushPin';\n\timport { trashIcon } from 'entasis/icons/trash';\n\n\tconst items: SidebarGroup[] = [\n\t\t{\n\t\t\tlabel: 'Pages',\n\t\t\titems: [\n\t\t\t\t{\n\t\t\t\t\tlabel: 'Planning',\n\t\t\t\t\ticon: folderIcon,\n\t\t\t\t\tdefaultOpen: true,\n\t\t\t\t\titems: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tlabel: 'Q3 roadmap',\n\t\t\t\t\t\t\thref: '/pages/q3-roadmap',\n\t\t\t\t\t\t\ticon: fileTextIcon,\n\t\t\t\t\t\t\ticonColor: 'success',\n\t\t\t\t\t\t\ticonVariant: 'tile',\n\t\t\t\t\t\t\tbadge: 2,\n\t\t\t\t\t\t\taction: {\n\t\t\t\t\t\t\t\tlabel: 'More options for Q3 roadmap',\n\t\t\t\t\t\t\t\tmenu: [\n\t\t\t\t\t\t\t\t\t{ type: 'option', title: 'Rename', prefix: pencilSimpleIcon },\n\t\t\t\t\t\t\t\t\t{ type: 'option', title: 'Pin', prefix: pushPinIcon },\n\t\t\t\t\t\t\t\t\t{ type: 'option', title: 'Delete', prefix: trashIcon, color: 'danger' }\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{ label: 'Retro notes', href: '/pages/retro-notes', icon: fileTextIcon }\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t];\n</script>\n\n<Sidebar {items} />\n```\n\n### SidebarView\nOne named panel content in `views`.\n- **label**: string - The view's name, shown on the back row of the views nested under it.\n- **parent**: string - Key of the view this one is nested under. A nested view opens with a back row to its parent (named \"Back, <parent label>\"), and on mobile a swipe toward the inline end goes back. A view without `parent` is a top-level section.\n- **items**: SidebarGroup[] / **content**: Snippet<[SidebarApi]> - The view's body.\n- **headerButton**, **search**, **headerMenu**, **header**, **footerButton**, **footerMenu**, **footer** - Header and footer props for this view. Each one left undefined comes from the parent view, then from the Sidebar's own prop; `null` removes an inherited one.\n\nChanging the view slides the two views side by side, like pages: a deeper view comes in from the inline end while the old one leaves to the start, a shallower one slides back the other way, and between views at the same depth (sections) the later one in `views` counts as forward. When both views get every header and footer prop from the same place, the header and footer stay still and only the menu slides; when a view changes any of them, the whole panel slides as one page. `api.view` reads the current view and `api.setView(key)` changes it from a snippet.\n\n### SidebarActivityBar\nIcon rail pinned to the outer edge of the sidebar, visible in every display state.\n- **items**: SidebarActivityBarItem[] - Items rendered from the top.\n- **footerItems**: SidebarActivityBarItem[] - Items pinned to the end of the column.\n- **header** / **footer**: Snippet - Custom content before the first item and after the pinned ones.\n- **width**: string (default '3rem') - Column thickness, published as `--sidebar-width-activity`.\n- **label**: string - Accessible name for the column landmark. Set it whenever the panel also renders navigation.\n- **onSelect**: ({ item, index }) => void - Fires after an item is activated. `index` counts `items` then `footerItems`.\n\n### SidebarActivityBarItem\n- **icon**: SidebarIcon (required) - Icon rendered in the square.\n- **label**: string (required) - Accessible name and default tooltip; the square shows no text.\n- **id**: string - Stable render key.\n- **href** / **target** / **rel** - Render an anchor instead of a button.\n- **onclick**: (event: MouseEvent) => void - Native click handler.\n- **isActive**: boolean - Adds active styling and `aria-current=\"page\"`.\n- **badge**: string | number | Snippet - Corner badge pinned to the outer top corner. An empty string renders a bare dot. A string or number badge joins the accessible name (`\"Alerts, 3\"`); a dot and a Snippet badge are decorative, so put their meaning in `label`.\n- **disabled**: boolean - Blocks activation and skips the item during keyboard navigation.\n- **tooltip**: string | false - Tooltip override; `false` suppresses it.\n\n### SidebarMenuButtonItem\nUse for `headerButton`, `footerButton`, or direct `<SidebarMenuButton />` rows.\n- **icon**: SidebarIcon - Leading logo/icon.\n- **avatar**: { src?: string; alt?: string; fallback?: string } - Leading avatar.\n- **variant**: 'default' | 'brand' | 'compact'.\n- **title**: string - Primary text.\n- **subtitle**: string - Secondary text.\n- **trailing**: SidebarIcon | false | SidebarMenuActionDescriptor - Trailing content. An icon is decorative; an action descriptor (`{ icon, label, onclick }`, the same shape as a group action) renders its own icon-only ghost button beside the row, so a workspace card can carry its own collapse control without a custom `header` snippet. Descriptor handlers are `onclick(event, api)`, so `api.toggle()` is reachable.\n- **href** / **onclick** / **menu** - Choose link, button, or popup behavior. `onclick(event, api)` receives the SidebarApi beside the event, so `api.toggle()` is reachable from the row itself.\n- **menuIconClass**: string - Class override for option icons inside the popup menu.\n\n## Props\n\n### State\n- **open**: boolean (bindable, default true) - Desktop expanded state.\n- **defaultOpen**: boolean (default true) - Initial desktop state when `open` is omitted.\n- **onOpenChange**: (open: boolean) => void - Called once for a library-originated desktop state change. Repeated requests and parent prop updates stay silent.\n- **onDisplayStateChange**: (state: SidebarDisplayState) => void - Called once for a library-originated semantic display-state change.\n- **view**: string (bindable) - Key of the view on screen when `views` is set. Defaults to `defaultView`, then the first view.\n- **defaultView**: string - Initial view when `view` is omitted.\n- **onViewChange**: (view: string) => void - Called once for a library-originated view change: a view row, a back row, a swipe. Parent prop updates stay silent; with a route-driven `view`, navigate here.\n- **api.displayState**: 'expanded' | 'collapsed' | 'hidden' - Semantic desktop state; hidden means closed offcanvas. A hover peek does not change it.\n- **api.isPeeking**: boolean - True while a hover peek renders the collapsed panel at full width. Read it alongside `displayState` when a snippet hides content in icon mode.\n- **keyboardShortcut**: string | false (default 'b') - Ctrl/Cmd shortcut key.\n\n### Layout\n- **side**: 'left' | 'right' - Desktop and mobile side.\n- **variant**: 'admin' | 'floating' | 'inset' | 'split' | 'framed' - Sidebar geometry. `framed` is the admin geometry for a sidebar hosted inside a raised card (AppShell variant framed), with a `surface-recessed` well. `admin` renders the conventional full-height navigation column; `inset` integrates navigation into the lower wall with an inset content surface; `split` renders detached sidebar and content surfaces.\n- **size**: 'small' | 'normal' | 'large' (default 'normal') - Typography, icon, avatar, badge, leading-media, item-height, and search-height scale.\n- **iconSize**: 'small' | 'normal' | 'large' - Icon and leading-media scale inside the panel on its own; defaults to `size`. Menu rows, sub rows, group labels and the header button follow it; the activity bar keeps `size`.\n- **activeVariant**: 'soft' | 'outline' | 'solid' (default 'soft') - How active rows are painted. `soft` is the shared selected recipe (`selectedSoft`: `bg-selected-muted text-selected-muted-readable`), `solid` its loud counterpart (`selectedSolid`: `bg-selected text-selected-contrast`), `outline` a bordered surface card (`bg-surface border border-neutral-muted text-neutral`) that reads as a raised card on a tinted well. Each row carries the choice as `data-active-variant`, so no descendant selector is needed to restyle selection.\n- **density**: 'compact' | 'normal' | 'comfortable' (default 'normal') - Section padding, group padding, gaps, horizontal inset, and submenu spacing.\n- **collapsible**: 'offcanvas' | 'icon' | 'none' - Collapse behavior. Icon mode requires icons on every data-driven row and otherwise resolves to offcanvas.\n- **collapseIcon**: DisclosureIndicator \u2014 'chevron' | 'plus-minus' | 'none' (default 'chevron') - Disclosure indicator drawn on collapsible menu rows. 'none' renders no indicator.\n- **mode**: 'layout' | 'panel' - Full resizing layout or only the visible navigation panel.\n- **frame**: 'viewport' | 'contained' - Standalone Sidebar positioning. Viewport mode uses Theme's dynamic window-height token; contained mode fills a positioned parent.\n- **width**: string - Expanded width.\n- **widthIcon**: string - Icon-collapsed width.\n- **widthMobile**: string - Mobile drawer width. The drawer runs the full screen height, flush with its edge (no inset); an activity bar adds its own width beside it. A swipe toward its edge closes it, from anywhere on it, rows included. Following a link in the panel closes it too (a plain click, also when a client-side router handles it); the activity bar's links, buttons, and clicks that open elsewhere (a modifier key, `target=\"_blank\"`, `download`) keep it open.\n- **rail**: boolean | 'line' | 'thumb' - Edge toggle rail. `true` keeps the thin line style; `thumb` renders a short visible handle with the same full-height hitbox. The appearance is preserved when the rail shares the resize control.\n- **activityBar**: SidebarActivityBar - Icon rail pinned outside the panel, in layout mode only (`mode=\"panel\"` renders the panel alone and ignores it). It never slides off screen: only the panel takes the offcanvas offset, and the reserved layout column is the panel width plus the rail width. It wears the surface of the variant's panel: a hairline column on the canvas for `admin`, the recessed well for `framed`, borderless on the canvas for `inset`, and a card of its own (the panel's radius, edge and gutter) for `floating` and `split`. Every gutter between the activity bar, the panel and the page, and between them and the frame edge, is `--space-md`, so it scales with the theme's spacing. With `inset` and `split`, hiding the panel normally lets the page drop its frame and fill the edge; with an activity bar the rail is still beside the page, so the page keeps its framed form (padding, rounding, AppShell's border and shadow), and a `split` page takes the gutter the panel used to supply. The wrapper carries `data-page-flush` and the `main` part a `flush` variant only when the page does fill the edge. On mobile it stays a column at the drawer's outer edge, beside the panel, as the hairline column on the canvas whatever the variant.\n- **expandOnHover**: boolean (default false) - With `collapsible=\"icon\"`, hovering or focusing into the collapsed panel expands it to `width` over the page (`data-peek=\"true\"`) while the reserved column stays at `widthIcon`, so page content does not reflow. The persisted collapsed state is untouched, and the peeked panel renders exactly like an expanded one: group headers, badges, search, inline submenus, and inline tree branches all come back, and the rail or resize handle travels to its inner edge.\n- **edgeReveal**: boolean (default true) - Pointer/focus edge preview for hidden offcanvas sidebars. Hover reveal overlays content, remains resizable when configured, and re-hides after the pointer leaves its small rectangular tolerance. Dragging the sidebar closed suppresses immediate hover reopening until the pointer leaves the edge trigger; toggle/click opens persistently.\n- **resizable**: boolean | SidebarResizableOptions - Enables pointer and keyboard resizing while expanded, icon-collapsed, or temporarily edge-revealed. `minWidth` and `maxWidth` (a CSS length or a number of px; defaults `12rem` and `32rem`) bound both the drag and the keyboard resize; a `maxWidth` below `minWidth` throws. By default, collapse requires dragging 75% of `minWidth` beyond the minimum; override `collapseThreshold` for a custom boundary. Use `storageKey` to restore and persist the expanded width across sessions.\n - `onWidthChange({ width, isUserInteraction })` reports every expanded-width change with one named payload: continuously while the user resizes (`isUserInteraction: true`) and once when a stored width is restored (`isUserInteraction: false`).\n\n### Content\n- **items**: SidebarGroup[] - Data-driven body navigation.\n- **views**: Record<string, SidebarView> - Named panel contents, one on screen at a time, replacing `items` and `content`. See SidebarView.\n- **headerButton** / **footerButton**: SidebarMenuButtonItem - Sticky large rows.\n- **search**: SidebarSearch - Header search input; use its native `oninput` handler.\n- **headerMenu** / **footerMenu**: SidebarMenuEntry[] - Sticky quick menus.\n- Menu entries and nested entries accept `size: 'small' | 'normal' | 'large'` for row geometry.\n- **header**, **content**, **footer**, **children**, **banner**: Snippet<[SidebarApi]> - Escape hatches. The `header` snippet renders **first** in the header region, above `headerButton`, `search` and `headerMenu`.\n\n### Styling\n- **class**: string - Classes applied to the Sidebar root.\n- **theme**: SidebarThemeProps - Semantic part overrides such as `panel`, `header`, `nav`, `footer`, menu, search, rail, and mobile drawer parts. Every part, with the element it lands on, its variants and their default classes, is listed in the entasis skill's `theme-parts/sidebar.md`; the registry key is `sidebar`.\n\n## Motion\n\n- **motion** theme slot: the height animation shared by collapsible groups, inline submenus, and\n tree branches. Its `in` / `out` `duration`, `delay` and `easing` (motion tokens) time the\n open and the close; the panel animates its height, block padding, borders and margins.\n- Ladder: `<Theme components={{ sidebar: { motion } }}>` \u2192 `setSidebarTheme({ motion })` \u2192\n `theme.motion`. Reduced motion collapses it to 0.\n- The `view` variant (`part: 'view'`) is the pager between views: `in.x` / `out.x` is how far a view travels (default `'100%'`: the two views slide side by side like pages) while it fades between `opacity` (default 0) and 1, at the `slow` duration on the `enter` easing. The back swipe moves the same layers the view change would, with the same travel and opacity. Reduced motion makes the switch instant while a swipe still follows the finger.\n\n## Restyle recipes\n\nThe five asks that come up first, as the override to copy. Each one is rendered and asserted by\n`sidebar-recipes.svelte.test.ts`, so it cannot drift from the component. One rule behind them: a\ndefault written under a variant prefix (`data-[active-variant=solid]:data-active:bg-selected`,\n`data-[side=left]:border-r`) is only replaced by an override carrying the same prefixes; an\nunprefixed class coexists with it and loses on specificity. `theme-parts/sidebar.md` shows every\ndefault verbatim, prefixes included.\n\n- **Dark panel.** `dark` scopes the dark theme's variables to the panel, so every neutral ink inside\n flips with it (every theme also emits its variables on `.<name>`, so the class scopes the dark theme to one subtree); needs a theme named `dark`, which the documented setup declares.\n `theme={{ panel: { base: 'dark bg-slate-900' }, mobilePanel: { base: 'dark bg-slate-900' } }}`\n- **Active row in your colour.** `activeVariant=\"solid\"` plus the same prefix chain as the default:\n `theme={{ menuButton: { base: 'rounded-full', activeVariant: { solid: 'data-[active-variant=solid]:data-active:bg-indigo-600 data-[active-variant=solid]:data-active:text-white' } } }}`\n- **No hover change.** The overlay is a `::before` and the default also brightens the ink on hover:\n `theme={{ menuButton: { base: 'state-layer-none hover:text-inherit' }, subButton: { base: 'state-layer-none hover:text-neutral/70' } }}`\n- **Flat panel.** The edge is a prefixed `border-r` / `border-l` on the admin variant, the shadow a\n `raised-*` on the floating ones:\n `theme={{ panel: { base: 'raised-none', variant: { admin: 'data-[side=left]:border-r-0 data-[side=right]:border-l-0' } } }}`\n- **Row height.** A plain height beats the compound `h-control-*`: `theme={{ menuButton: { base: 'h-11' } }}`\n- **Bigger icons.** A prop, not a theme: `iconSize=\"large\"`.\n\n## Styling hooks\n\n`data-open` on a disclosure toggle is a presence attribute (before 0.12.0 a submenu toggle carried `data-open=\"true\"`); match it with `[data-open]` or `data-open:`.\n\nAttributes are presence attributes (set while the state holds, omitted otherwise) unless they carry a value.\n\n**Data attributes**\n- `data-state`: On the root: `expanded` or `collapsed`.\n- `data-display-state`: On the root: what is on screen, `expanded`, `collapsed` (the icon rail) or `hidden`.\n- `data-collapsible`: On the root: `icon` while collapsed to the icon rail, `offcanvas` while hidden, empty while expanded.\n- `data-side`: On the root, the panel and its rail and handles: `left` or `right`.\n- `data-variant`: On the root: the resolved `variant`.\n- `data-size`: On the root: the resolved `size`.\n- `data-density`: On the root: the resolved `density`.\n- `data-peek`: On the layout wrapper: `true` while a hover peek shows the collapsed panel at full width.\n- `data-active`: On a row: `true` while it is the current page (`isActive`).\n- `data-active-variant`: On a row: the resolved `activeVariant`.\n- `data-open`: On the toggle of a collapsible group, a submenu or a tree branch, and on its panel: present while it is open.\n- `data-closed`: On the toggle of a collapsible group, a submenu or a tree branch, and on its panel: present while it is closed, including during the exit transition.\n- `data-entering`: On a group, submenu or tree branch panel: present while its opening animation plays.\n- `data-exiting`: On a group, submenu or tree branch panel: present while its closing animation plays.\n- `data-popup-open`: On a row: present while one of its popups is open, its `menu`, its flyout on the collapsed rail, or its tooltip (every row on the collapsed rail, and every row with `tooltips=\"always\"`). Match `[aria-expanded=\"true\"]` to style an open menu or flyout only.\n- `data-sidebar`: The part (`sidebar`, `menu-button`, `menu-sub`, `group-content`, `rail`, ...).\n\n**CSS variables**\n- `--sidebar-width`: On the root: the expanded panel width.\n- `--sidebar-width-icon`: On the root: the width of the collapsed icon rail.\n- `--sidebar-width-mobile`: On the root: the width of the mobile drawer.\n- `--sidebar-suffix-width`: On a row with a `suffix` (its `menu-item` row wrapper or `menu-sub-item`): the suffix's measured width in px, reserved at the row's end so the label truncates before it.\n- `--disclosure-panel-height`: On a group, submenu or tree branch panel: its measured height in px while it animates, `auto` once open and `0px` once closed.\n\nThe typed maps `sidebarDataAttributes` and `sidebarCssVariables` (from `entasis/sidebar`) list each name with its meaning.\n\n## Accessibility\n\n- The body navigation renders inside a named `<nav>` landmark (`data-sidebar=\"nav\"`), so assistive tech can jump straight to it and tell it apart from the activity bar's own landmark.\n- Active links set `aria-current=\"page\"`.\n- Collapsible rows and groups set `aria-expanded`.\n- Disabled buttons use `disabled`; disabled links omit `href`, use `aria-disabled` and `tabindex=-1`, and block activation.\n- Mobile drawer includes a backdrop button labelled \"Close Sidebar\".\n- Icon-collapsed rows keep their labels mounted and visually fade them, preserving accessible names and stable icon geometry.\n- Search, group controls, actions, and nested rows become inert before collapse can remove or hide them; focus returns to the owning visible row.\n- A row's `suffix` is a sibling of its link or button, between it and the row's `action`: Tab reaches its controls right after the row, its content describes the row (`aria-describedby`), and it is `inert` and `aria-hidden` whenever the Sidebar is collapsed to icons, focus inside it returning to the row.\n- A submenu row's `action` is a sibling of its link, so Tab reaches the action button right after the row; its trigger takes `label` as its accessible name, and Escape closes its menu and returns focus to the trigger. It is `inert` and `aria-hidden` whenever the Sidebar is collapsed to icons.\n- Collapsible groups, tree branches, and inline submenus are disclosure panels (the shared `disclosurePanel` Collapsible and Accordion use): the toggle sets `aria-expanded` and, while the panel is rendered, `aria-controls` to its id; the panel carries `data-open` / `data-closed` and `data-entering` / `data-exiting`, exposes `--disclosure-panel-height` while it animates, and stays `inert` while it closes. Open, close and sidebar-collapse changes animate the panel's height and reverse from where they are when interrupted; the `motion` slot's duration, delay and easing time them.\n- Tree roots use menu-row styling and nested tree nodes use submenu-row styling. In desktop icon mode, a root folder, and a menu row with `items` and no `href`, `view` or `menu`, is a disclosure button (`aria-expanded`, `aria-controls`) that opens a flyout holding a named `<nav>` of links, with NavigationMenu's semantics: nested folders are named sub-lists, a row with an `onclick` and no `href` is a button, a disabled sub entry stays listed without a destination, and a press on any row closes the flyout (the row's own handler runs first, and `preventDefault()` keeps it open). Opening from the keyboard keeps focus on the row; Tab enters the flyout's links, Tab past the last one continues after the row and closes it, Shift+Tab from the first returns to the row, and Escape closes it and refocuses the row. There is no `role=\"menu\"`; root leaves retain direct navigation and tooltips.\n- Row menus, flyouts and tooltips open toward the page: at the inline end of a `left` sidebar and the inline start of a `right` one, the side the layout row puts the page on in either reading direction (a `left` sidebar is the row's start, on the right in right-to-left). An explicit `menuSide` wins.\n- When `rail` and `resizable` are both enabled, one edge control owns click-to-toggle, drag resize, and keyboard resize without overlapping hitboxes.\n- Hidden offcanvas sidebars keep that combined edge control while temporarily revealed. Resizing does not pin the sidebar open; leaving the panel, trigger, and handle tolerance re-hides it without discarding the configured width.\n- Both peeks (edge reveal and `expandOnHover`) stay open while focus is inside the panel or while an overlay opened from inside it is open, including nested submenus. They release about 120ms after the pointer, focus, and every such overlay are gone.\n- The activity bar is its own `<nav>` landmark with a `<ul>` of items, a roving tabindex, and ArrowUp/ArrowDown/Home/End navigation that loops and skips disabled items. Tab lands on the `isActive` item. Each square takes its accessible name from `label`, with a string or number `badge` appended to it, and shows `label` as a tooltip on hover and focus.\n- A hidden offcanvas panel is `inert`, so Tab never lands in a panel parked off screen; a peek makes it interactive again.\n- Views: when focus sat in the view being replaced, it lands on the back row after going deeper and on the row that opened the view after coming back. A view change from outside the panel (a route) leaves focus where it is. The back swipe is a shortcut; the back row stays the accessible path.\n\n## Notes\n\n- Dropdown menus use Entasis `PopupMenu` and `MenuItem[]`.\n- The component uses semantic Entasis tokens. Do not add shadcn `sidebar-*` color tokens.\n- Snippet icons from `entasis/icons/*` are the preferred icon format.\n";
1
+ export declare const sidebarDescription = "\n# Sidebar Component\n\nSidebar navigation with data-driven groups, icon collapse, mobile drawer behavior,\nrecursive tree groups, header/footer rows, search, actions, and snippet escape hatches.\n\n## Import\n\n```svelte\n<script lang=\"ts\">\n\timport { Sidebar, type SidebarGroup } from 'entasis/sidebar';\n</script>\n```\n\n## Basic Usage\n\n```svelte\n<script lang=\"ts\">\n\timport { Sidebar, type SidebarGroup } from 'entasis/sidebar';\n\timport { houseIcon } from 'entasis/icons/house';\n\timport { gearIcon } from 'entasis/icons/gear';\n\n\tconst items: SidebarGroup[] = [\n\t\t{\n\t\t\tlabel: 'Workspace',\n\t\t\titems: [\n\t\t\t\t{ label: 'Dashboard', href: '/', icon: houseIcon, isActive: true },\n\t\t\t\t{ label: 'Settings', href: '/settings', icon: gearIcon }\n\t\t\t]\n\t\t}\n\t];\n</script>\n\n<Sidebar items={items}>\n\t{#snippet children({ toggle })}\n\t\t<header>\n\t\t\t<button type=\"button\" onclick={toggle}>Toggle</button>\n\t\t</header>\n\t\t<main>Page content</main>\n\t{/snippet}\n</Sidebar>\n```\n\n## AI-Safe Usage Contract\n\n1. Use `items` for normal navigation. Use `content` only when data-driven rows cannot express the layout.\n2. Use local Entasis icon snippets such as `houseIcon`, not Lucide component constructors.\n3. Use `MenuItem[]` from `entasis/menu` for `menu` and action dropdowns.\n4. Do not combine `menu` with `href` or `onclick` on the same row; use `action` for a trailing row menu.\n5. Keep `children`, `header`, `content`, `footer`, `banner`, and action snippets pure; they receive `SidebarApi`.\n6. Use `collapsible=\"icon\"` for icon rail behavior, `collapsible=\"offcanvas\"` for hidden desktop panels, and `collapsible=\"none\"` for fixed sidebars. Icon collapse automatically falls back to offcanvas when any data-driven row lacks an icon.\n7. Offcanvas sidebars reveal over the content from the screen edge by default when hidden; set `edgeReveal={false}` to disable that. A revealed hidden sidebar keeps its resize handle and dismisses through a small rectangular pointer tolerance.\n8. Set `keyboardShortcut={false}` when embedding Sidebar inside another shortcut-heavy surface.\n9. Sidebar owns navigation, resize mechanics, the lower application wall, and variant surface geometry. AppShell forwards its variant and composes PageShell inside that surface.\n10. Use `size` for typography, icon scale, and item height. Use `density` independently for section padding, gaps, and submenu spacing.\n11. Use `activityBar` for a persistent icon rail outside the panel (section switching, workspaces). Every item needs an `icon` and a `label`; the label is the accessible name and the tooltip. It is layout mode only: `mode=\"panel\"` renders the navigation panel alone. The rail does not change the panel by itself: keep the selected rail item in state from `onSelect`, mark it `isActive`, and either derive the Sidebar's `items` from it or, for an animated switch, give each rail item a view in `views` and set `view` from it.\n12. Use `expandOnHover` only with `collapsible=\"icon\"`. It is a temporary peek, not a toggle: the persisted collapsed state never changes, while the peeked panel renders with expanded semantics.\n13. Use `views` when the panel's contents change as the user moves through the app: sections switched from the activity bar or the route, and nested menus that open inside the panel. Key each view, set `parent` on nested ones, open them with a row's `view`, and drive `view` from state or the URL. Do not hand-animate `items` swaps.\n\n## Data Model\n\n### SidebarGroup\n- **label**: string - Group label, hidden in icon-collapsed mode.\n- **items**: SidebarMenuEntry[] - Menu rows.\n- **tree**: SidebarTreeNode[] - Recursive tree rows instead of menu items.\n- **action**: SidebarMenuActionDescriptor | SidebarMenuActionDescriptor[] | Snippet<[SidebarApi]> - Group actions pinned to the label's end. The label truncates on one line before them. Pass an array to pin several affordances (a `+` and a drag handle) to one group header; each renders as its own icon-only ghost button.\n- **collapsible**: boolean - Makes the group label a toggle.\n- **defaultOpen**: boolean - Initial collapsible group state.\n- **separator**: boolean - Divider before the group.\n\n### SidebarMenuEntry\n- **label**: string - Visible row label.\n- **icon**: SidebarIcon - Entasis icon snippet or string.\n- **iconColor**: Colors - Role tint for the leading icon, applied through `data-color`.\n- **iconVariant**: 'bare' | 'tile' - Leading icon treatment. `tile` paints a rounded square (`bg-color-muted text-color-muted-readable`) around the glyph, so per-project colour chips come from the role scale instead of hand-built markup.\n- **media**: Snippet<[SidebarApi, SidebarMenuEntry]> - Leading media in place of `icon`: a face, an avatar, a logo. It renders inside the row (`data-sidebar=\"menu-media\"`, drawn by the `menuMedia` theme part) in a box at the Sidebar's media size (32px at `size=\"normal\"`; `iconSize=\"large\"` gives 36px), which an `svg` or `img` placed directly inside fills. It receives the row's entry, so one snippet draws every face (`{#snippet face(api, entry)}`). Collapsed to icons it stays visible and centred, as an icon does, and it counts as the row's icon for icon-collapse eligibility.\n- **description**: string | Snippet - Second line under the label: the last message, a path, a status. Drawn by the `menuSecondary` recipe (lighter, regular weight) and truncated to one line. The row takes the large height (`h-14` at `size=\"normal\"`) unless the entry sets its own `size`. The label, its meta and the description sit in one column (`menuText`, `menuTitleRow`, `menuTitle`, `menuDescriptionRow` theme parts) that fades with the labels in icon mode. On such a two-line row the `badge` and the `suffix` sit at the end of the description's line, which reserves the suffix's width, so the label's line and its `meta` end on the same column on every row, with or without a suffix.\n- **meta**: string | Snippet - Short text at the end of the label's line: a time, \"Hier\" (`data-sidebar=\"menu-meta\"`, `menuMeta` theme part). The label truncates before it. On a one-line row it ends before any `suffix`; on a row with a `description` it ends at the row's end, above the suffix. Left out in icon-collapsed mode, like `badge`. The row's accessible name reads label, meta, then description.\n- **href**: string - Render as an anchor. Mutually exclusive with menu.\n- **onclick**: (event: MouseEvent) => void - Native click handler for button or anchor rows. Mutually exclusive with menu. On a row with `items` and no `href`, it runs before the submenu toggles, inline or in the collapsed flyout; `event.preventDefault()` skips the toggle.\n- **isActive**: boolean - Adds active styling and `aria-current=\"page\"`.\n- **disabled**: boolean - Disables button rows and marks anchor rows disabled.\n- **badge**: string | number - Trailing count/status, hidden in icon mode. It follows the label inside the row (before any chevron), so the label truncates before it whatever its width. On a row with an `action`, the badge steps aside while the action shows (hover, focus, open menu) from `md` up, and sits just before the always-visible action below `md`: the two never overlap.\n- **tooltip**: string - Entasis Tooltip content in icon mode. Defaults to label.\n- **items**: SidebarMenuSubEntry[] - Inline nested menu. Collapsed to icons on desktop, a row with `items` and no `href`, `view` or `menu` opens them in a flyout of links instead.\n- **collapsible**: boolean - Set false for an always-open submenu.\n- **defaultOpen**: boolean - Initial nested menu state.\n- **menu**: MenuItem[] - Popup menu opened from the full row. Mutually exclusive with href/onclick.\n- **menuSide** / **menuAlign**: SidebarMenuSide / 'start' | 'center' | 'end' - Where that menu opens. `menuSide` takes a physical side (`top`, `right`, `bottom`, `left`) or a logical one (`inline-start`, `inline-end`, mirrored in right-to-left). Left out, a desktop menu opens toward the page (`inline-end` of a left sidebar, `inline-start` of a right one, so it follows the reading direction) aligned to the row's start, and a mobile one below the row aligned to its end. The same pair sits on action descriptors and on `headerButton` / `footerButton`.\n- **view**: string - Key of the `views` entry the row opens, sliding it in; the row shows a trailing chevron. Mutually exclusive with href, menu and items.\n- **action**: SidebarMenuActionDescriptor | Snippet<[SidebarApi]> - Hover/focus trailing action.\n- **suffix**: Snippet<[SidebarApi, SidebarMenuEntry]> - Content at the row's end, beside the label: a count, a `Chip`, an icon, a status dot, its own `PopupMenu` trigger. It receives the row's own entry, so rows built as data share one snippet declared in markup (`{#snippet suffix(api, entry)}`) and tell themselves apart by `entry.href`. It renders beside the row's link or button, never inside it (`data-sidebar=\"menu-suffix\"`, drawn by the `suffix` theme part), so it may hold controls: a click or a keypress inside it never follows the row's `href` nor toggles it, and Tab reaches its controls right after the row, before the `action`. The row's link or button is `aria-describedby` the suffix, so the row reads with it (\"Dossiers\", \"20\"). The label truncates before it: the row reserves its measured width (`--sidebar-suffix-width`), and the `suffix` part bounds it (`max-w-[40%]` of the row), so a wide suffix clips instead of breaking the row. With an `action`, it stays visible just before the action at every width (the row keeps the action's room, and a `badge` then stays put instead of stepping aside). Hidden, `inert` and `aria-hidden` in icon-collapsed mode; shown in a hover peek and in the mobile drawer.\n\n### SidebarMenuSubEntry\nA row of an inline submenu (`items` on a menu entry). It carries the same row features as a\ntop-level entry, so nested navigation (pages filed in folders) keeps its menus, pins and tints\nwithout flattening the tree. Every field below `size` is optional, and a sub entry that sets none\nof them renders exactly as a plain one.\n- **label**: string - Visible row label.\n- **href** / **onclick** - Render as an anchor, or a button with a native click handler.\n- **icon**: SidebarIcon - Leading icon.\n- **isActive** / **disabled** / **size** - As on a top-level entry; `size` defaults to the Sidebar size.\n- **iconColor**: Colors - Role tint for the leading icon, applied through `data-color`.\n- **iconVariant**: 'bare' | 'tile' - `tile` paints the same tinted rounded square as a top-level row.\n- **media** / **description** / **meta** - As on a top-level entry (`media` receives the sub entry). A sub row with a `description` or a `media` takes the two-line height (40 / 48 / 56px by size).\n- **badge**: string | number - Trailing count or status; the label truncates before it. It shares the row's end column with the parent rows' badges and makes way for the row's `action` as a top-level badge does.\n- **tooltip**: string - Tooltip text. The label is always visible on an inline sub row, so the tooltip is extra information: shown whenever one is set, never on mobile. The collapsed row's flyout lists only each entry's label, icon, `href` or `onclick`, and `isActive`/`disabled` state: it shows no tooltip, `badge`, `suffix` or `action`.\n- **class**: string - Classes applied to the row button.\n- **action**: SidebarMenuActionDescriptor | Snippet<[SidebarApi]> - Trailing action pinned to the row's end (`data-sidebar=\"menu-sub-action\"`), drawn by the `subAction` theme part: hidden until the row is hovered or focused from `md` up, kept visible while its menu is open, always visible below `md`. The label truncates before it.\n- **suffix**: Snippet<[SidebarApi, SidebarMenuSubEntry]> - Content at the row's end, beside the label: a count, a `Chip`, an icon, a status dot, its own `PopupMenu` trigger. It receives the row's own entry, so rows built as data share one snippet declared in markup (`{#snippet suffix(api, entry)}`) and tell themselves apart by `entry.href`. It renders beside the row's link or button, never inside it (`data-sidebar=\"menu-sub-suffix\"`, drawn by the `suffix` theme part), so it may hold controls: a click or a keypress inside it never follows the row's `href` nor toggles it, and Tab reaches its controls right after the row, before the `action`. The row's link or button is `aria-describedby` the suffix, so the row reads with it (\"Dossiers\", \"20\"). The label truncates before it: the row reserves its measured width (`--sidebar-suffix-width`), and the `suffix` part bounds it (`max-w-[40%]` of the row), so a wide suffix clips instead of breaking the row. With an `action`, it stays visible just before the action at every width (the row keeps the action's room, and a `badge` then stays put instead of stepping aside). Hidden, `inert` and `aria-hidden` in icon-collapsed mode; shown in a hover peek and in the mobile drawer.\n\n```svelte\n<script lang=\"ts\">\n\timport { Sidebar, type SidebarGroup } from 'entasis/sidebar';\n\timport { folderIcon } from 'entasis/icons/folder';\n\timport { fileTextIcon } from 'entasis/icons/fileText';\n\timport { pencilSimpleIcon } from 'entasis/icons/pencilSimple';\n\timport { pushPinIcon } from 'entasis/icons/pushPin';\n\timport { trashIcon } from 'entasis/icons/trash';\n\n\tconst items: SidebarGroup[] = [\n\t\t{\n\t\t\tlabel: 'Pages',\n\t\t\titems: [\n\t\t\t\t{\n\t\t\t\t\tlabel: 'Planning',\n\t\t\t\t\ticon: folderIcon,\n\t\t\t\t\tdefaultOpen: true,\n\t\t\t\t\titems: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tlabel: 'Q3 roadmap',\n\t\t\t\t\t\t\thref: '/pages/q3-roadmap',\n\t\t\t\t\t\t\ticon: fileTextIcon,\n\t\t\t\t\t\t\ticonColor: 'success',\n\t\t\t\t\t\t\ticonVariant: 'tile',\n\t\t\t\t\t\t\tbadge: 2,\n\t\t\t\t\t\t\taction: {\n\t\t\t\t\t\t\t\tlabel: 'More options for Q3 roadmap',\n\t\t\t\t\t\t\t\tmenu: [\n\t\t\t\t\t\t\t\t\t{ type: 'option', title: 'Rename', prefix: pencilSimpleIcon },\n\t\t\t\t\t\t\t\t\t{ type: 'option', title: 'Pin', prefix: pushPinIcon },\n\t\t\t\t\t\t\t\t\t{ type: 'option', title: 'Delete', prefix: trashIcon, color: 'danger' }\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{ label: 'Retro notes', href: '/pages/retro-notes', icon: fileTextIcon }\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t];\n</script>\n\n<Sidebar {items} />\n```\n\nA conversation list built as data: each row leads with a face (`media`), reads a name and a time\n(`meta`) over the last message (`description`), and ends with an unread dot (`suffix`). The\nrows keep `isActive`, `href`, tooltips and icon collapse.\n\n```svelte\n<script lang=\"ts\">\n\timport {\n\t\tSidebar,\n\t\ttype SidebarApi,\n\t\ttype SidebarGroup,\n\t\ttype SidebarMenuEntry,\n\t\ttype SidebarMenuSubEntry\n\t} from 'entasis/sidebar';\n\n\tconst agents = [\n\t\t{ id: 'ada', name: 'Ada', time: '10:42', last: 'The report is ready.', unread: true },\n\t\t{ id: 'brune', name: 'Brune', time: 'Hier', last: 'Venue booked for Thursday.' }\n\t];\n\n\tconst items: SidebarGroup[] = [\n\t\t{\n\t\t\tlabel: 'Agents',\n\t\t\titems: agents.map((agent) => ({\n\t\t\t\tlabel: agent.name,\n\t\t\t\thref: `/agents/${agent.id}`,\n\t\t\t\tmedia: face,\n\t\t\t\tmeta: agent.time,\n\t\t\t\tdescription: agent.last,\n\t\t\t\tisActive: agent.id === 'ada',\n\t\t\t\tsuffix: agent.unread ? unread : undefined\n\t\t\t}))\n\t\t}\n\t];\n</script>\n\n{#snippet face(_api: SidebarApi, entry: SidebarMenuEntry | SidebarMenuSubEntry)}\n\t<svg viewBox=\"0 0 40 40\" aria-hidden=\"true\">\n\t\t<circle cx=\"20\" cy=\"20\" r=\"20\" fill=\"currentColor\" opacity=\"0.2\" />\n\t\t<text x=\"20\" y=\"25\" text-anchor=\"middle\" font-size=\"14\">{entry.label[0]}</text>\n\t</svg>\n{/snippet}\n\n{#snippet unread()}\n\t<span class=\"bg-primary block size-2.5 rounded-full\" aria-label=\"Unread\"></span>\n{/snippet}\n\n<Sidebar {items} iconSize=\"large\" collapsible=\"icon\" />\n```\n\n### SidebarView\nOne named panel content in `views`.\n- **label**: string - The view's name, shown on the back row of the views nested under it.\n- **parent**: string - Key of the view this one is nested under. A nested view opens with a back row to its parent (named \"Back, <parent label>\"), and on mobile a swipe toward the inline end goes back. A view without `parent` is a top-level section.\n- **items**: SidebarGroup[] / **content**: Snippet<[SidebarApi]> - The view's body.\n- **headerButton**, **search**, **headerMenu**, **header**, **footerButton**, **footerMenu**, **footer** - Header and footer props for this view. Each one left undefined comes from the parent view, then from the Sidebar's own prop; `null` removes an inherited one.\n\nChanging the view slides the two views side by side, like pages: a deeper view comes in from the inline end while the old one leaves to the start, a shallower one slides back the other way, and between views at the same depth (sections) the later one in `views` counts as forward. When both views get every header and footer prop from the same place, the header and footer stay still and only the menu slides; when a view changes any of them, the whole panel slides as one page. `api.view` reads the current view and `api.setView(key)` changes it from a snippet.\n\n### SidebarActivityBar\nIcon rail pinned to the outer edge of the sidebar, visible in every display state.\n- **items**: SidebarActivityBarItem[] - Items rendered from the top.\n- **footerItems**: SidebarActivityBarItem[] - Items pinned to the end of the column.\n- **header** / **footer**: Snippet - Custom content before the first item and after the pinned ones.\n- **width**: string (default '3rem') - Column thickness, published as `--sidebar-width-activity`.\n- **label**: string - Accessible name for the column landmark. Set it whenever the panel also renders navigation.\n- **onSelect**: ({ item, index }) => void - Fires after an item is activated. `index` counts `items` then `footerItems`.\n\n### SidebarActivityBarItem\n- **icon**: SidebarIcon (required) - Icon rendered in the square.\n- **label**: string (required) - Accessible name and default tooltip; the square shows no text.\n- **id**: string - Stable render key.\n- **href** / **target** / **rel** - Render an anchor instead of a button.\n- **onclick**: (event: MouseEvent) => void - Native click handler.\n- **isActive**: boolean - Adds active styling and `aria-current=\"page\"`.\n- **badge**: string | number | Snippet - Corner badge pinned to the outer top corner. An empty string renders a bare dot. A string or number badge joins the accessible name (`\"Alerts, 3\"`); a dot and a Snippet badge are decorative, so put their meaning in `label`.\n- **disabled**: boolean - Blocks activation and skips the item during keyboard navigation.\n- **tooltip**: string | false - Tooltip override; `false` suppresses it.\n\n### SidebarMenuButtonItem\nUse for `headerButton`, `footerButton`, or direct `<SidebarMenuButton />` rows.\n- **icon**: SidebarIcon - Leading logo/icon.\n- **avatar**: { src?: string; alt?: string; fallback?: string } - Leading avatar.\n- **variant**: 'default' | 'brand' | 'compact'.\n- **title**: string - Primary text.\n- **subtitle**: string - Secondary text.\n- **trailing**: SidebarIcon | false | SidebarMenuActionDescriptor - Trailing content. An icon is decorative; an action descriptor (`{ icon, label, onclick }`, the same shape as a group action) renders its own icon-only ghost button beside the row, so a workspace card can carry its own collapse control without a custom `header` snippet. Descriptor handlers are `onclick(event, api)`, so `api.toggle()` is reachable.\n- **href** / **onclick** / **menu** - Choose link, button, or popup behavior. `onclick(event, api)` receives the SidebarApi beside the event, so `api.toggle()` is reachable from the row itself.\n- **menuIconClass**: string - Class override for option icons inside the popup menu.\n\n## Props\n\n### State\n- **open**: boolean (bindable, default true) - Desktop expanded state.\n- **defaultOpen**: boolean (default true) - Initial desktop state when `open` is omitted.\n- **onOpenChange**: (open: boolean) => void - Called once for a library-originated desktop state change. Repeated requests and parent prop updates stay silent.\n- **onDisplayStateChange**: (state: SidebarDisplayState) => void - Called once for a library-originated semantic display-state change.\n- **view**: string (bindable) - Key of the view on screen when `views` is set. Defaults to `defaultView`, then the first view.\n- **defaultView**: string - Initial view when `view` is omitted.\n- **onViewChange**: (view: string) => void - Called once for a library-originated view change: a view row, a back row, a swipe. Parent prop updates stay silent; with a route-driven `view`, navigate here.\n- **api.displayState**: 'expanded' | 'collapsed' | 'hidden' - Semantic desktop state; hidden means closed offcanvas. A hover peek does not change it.\n- **api.isPeeking**: boolean - True while a hover peek renders the collapsed panel at full width. Read it alongside `displayState` when a snippet hides content in icon mode.\n- **keyboardShortcut**: string | false (default 'b') - Ctrl/Cmd shortcut key.\n\n### Layout\n- **side**: 'left' | 'right' - Desktop and mobile side.\n- **variant**: 'admin' | 'floating' | 'inset' | 'split' | 'framed' - Sidebar geometry. `framed` is the admin geometry for a sidebar hosted inside a raised card (AppShell variant framed), with a `surface-recessed` well. `admin` renders the conventional full-height navigation column; `inset` integrates navigation into the lower wall with an inset content surface; `split` renders detached sidebar and content surfaces.\n- **size**: 'small' | 'normal' | 'large' (default 'normal') - Typography, icon, avatar, badge, leading-media, item-height, and search-height scale.\n- **iconSize**: 'small' | 'normal' | 'large' - Icon and leading-media scale inside the panel on its own; defaults to `size`. Menu rows, sub rows, group labels and the header button follow it; the activity bar keeps `size`.\n- **activeVariant**: 'soft' | 'outline' | 'solid' (default 'soft') - How active rows are painted. `soft` is the shared selected recipe (`selectedSoft`: `bg-selected-muted text-selected-muted-readable`), `solid` its loud counterpart (`selectedSolid`: `bg-selected text-selected-contrast`), `outline` a bordered surface card (`bg-surface border border-neutral-muted text-neutral`) that reads as a raised card on a tinted well. Each row carries the choice as `data-active-variant`, so no descendant selector is needed to restyle selection.\n- **density**: 'compact' | 'normal' | 'comfortable' (default 'normal') - Section padding, group padding, gaps, horizontal inset, and submenu spacing.\n- **collapsible**: 'offcanvas' | 'icon' | 'none' - Collapse behavior. Icon mode requires icons on every data-driven row and otherwise resolves to offcanvas.\n- **collapseIcon**: DisclosureIndicator \u2014 'chevron' | 'plus-minus' | 'none' (default 'chevron') - Disclosure indicator drawn on collapsible menu rows. 'none' renders no indicator.\n- **mode**: 'layout' | 'panel' - Full resizing layout or only the visible navigation panel.\n- **frame**: 'viewport' | 'contained' - Standalone Sidebar positioning. Viewport mode uses Theme's dynamic window-height token; contained mode fills a positioned parent.\n- **width**: string - Expanded width.\n- **widthIcon**: string - Icon-collapsed width.\n- **widthMobile**: string - Mobile drawer width. The drawer runs the full screen height, flush with its edge (no inset); an activity bar adds its own width beside it. A swipe toward its edge closes it, from anywhere on it, rows included. Following a link in the panel closes it too (a plain click, also when a client-side router handles it); the activity bar's links, buttons, and clicks that open elsewhere (a modifier key, `target=\"_blank\"`, `download`) keep it open.\n- **rail**: boolean | 'line' | 'thumb' - Edge toggle rail. `true` keeps the thin line style; `thumb` renders a short visible handle with the same full-height hitbox. The appearance is preserved when the rail shares the resize control.\n- **activityBar**: SidebarActivityBar - Icon rail pinned outside the panel, in layout mode only (`mode=\"panel\"` renders the panel alone and ignores it). It never slides off screen: only the panel takes the offcanvas offset, and the reserved layout column is the panel width plus the rail width. It wears the surface of the variant's panel: a hairline column on the canvas for `admin`, the recessed well for `framed`, borderless on the canvas for `inset`, and a card of its own (the panel's radius, edge and gutter) for `floating` and `split`. Every gutter between the activity bar, the panel and the page, and between them and the frame edge, is `--space-md`, so it scales with the theme's spacing. With `inset` and `split`, hiding the panel normally lets the page drop its frame and fill the edge; with an activity bar the rail is still beside the page, so the page keeps its framed form (padding, rounding, AppShell's border and shadow), and a `split` page takes the gutter the panel used to supply. The wrapper carries `data-page-flush` and the `main` part a `flush` variant only when the page does fill the edge. On mobile it stays a column at the drawer's outer edge, beside the panel, as the hairline column on the canvas whatever the variant.\n- **expandOnHover**: boolean (default false) - With `collapsible=\"icon\"`, hovering or focusing into the collapsed panel expands it to `width` over the page (`data-peek=\"true\"`) while the reserved column stays at `widthIcon`, so page content does not reflow. The persisted collapsed state is untouched, and the peeked panel renders exactly like an expanded one: group headers, badges, search, inline submenus, and inline tree branches all come back, and the rail or resize handle travels to its inner edge.\n- **edgeReveal**: boolean (default true) - Pointer/focus edge preview for hidden offcanvas sidebars. Hover reveal overlays content, remains resizable when configured, and re-hides after the pointer leaves its small rectangular tolerance. Dragging the sidebar closed suppresses immediate hover reopening until the pointer leaves the edge trigger; toggle/click opens persistently.\n- **resizable**: boolean | SidebarResizableOptions - Enables pointer and keyboard resizing while expanded, icon-collapsed, or temporarily edge-revealed. `minWidth` and `maxWidth` (a CSS length or a number of px; defaults `12rem` and `32rem`) bound both the drag and the keyboard resize; a `maxWidth` below `minWidth` throws. By default, collapse requires dragging 75% of `minWidth` beyond the minimum; override `collapseThreshold` for a custom boundary. Use `storageKey` to restore and persist the expanded width across sessions.\n - `onWidthChange({ width, isUserInteraction })` reports every expanded-width change with one named payload: continuously while the user resizes (`isUserInteraction: true`) and once when a stored width is restored (`isUserInteraction: false`).\n\n### Content\n- **items**: SidebarGroup[] - Data-driven body navigation.\n- **views**: Record<string, SidebarView> - Named panel contents, one on screen at a time, replacing `items` and `content`. See SidebarView.\n- **headerButton** / **footerButton**: SidebarMenuButtonItem - Sticky large rows.\n- **search**: SidebarSearch - Header search input; use its native `oninput` handler.\n- **headerMenu** / **footerMenu**: SidebarMenuEntry[] - Sticky quick menus.\n- Menu entries and nested entries accept `size: 'small' | 'normal' | 'large'` for row geometry.\n- **header**, **content**, **footer**, **children**, **banner**: Snippet<[SidebarApi]> - Escape hatches. The `header` snippet renders **first** in the header region, above `headerButton`, `search` and `headerMenu`.\n\n### Styling\n- **class**: string - Classes applied to the Sidebar root.\n- **theme**: SidebarThemeProps - Semantic part overrides such as `panel`, `header`, `nav`, `footer`, menu, search, rail, and mobile drawer parts. Every part, with the element it lands on, its variants and their default classes, is listed in the entasis skill's `theme-parts/sidebar.md`; the registry key is `sidebar`.\n\n## Motion\n\n- **motion** theme slot: the height animation shared by collapsible groups, inline submenus, and\n tree branches. Its `in` / `out` `duration`, `delay` and `easing` (motion tokens) time the\n open and the close; the panel animates its height, block padding, borders and margins.\n- Ladder: `<Theme components={{ sidebar: { motion } }}>` \u2192 `setSidebarTheme({ motion })` \u2192\n `theme.motion`. Reduced motion collapses it to 0.\n- The `view` variant (`part: 'view'`) is the pager between views: `in.x` / `out.x` is how far a view travels (default `'100%'`: the two views slide side by side like pages) while it fades between `opacity` (default 0) and 1, at the `slow` duration on the `enter` easing. The back swipe moves the same layers the view change would, with the same travel and opacity. Reduced motion makes the switch instant while a swipe still follows the finger.\n\n## Restyle recipes\n\nThe five asks that come up first, as the override to copy. Each one is rendered and asserted by\n`sidebar-recipes.svelte.test.ts`, so it cannot drift from the component. One rule behind them: a\ndefault written under a variant prefix (`data-[active-variant=solid]:data-active:bg-selected`,\n`data-[side=left]:border-r`) is only replaced by an override carrying the same prefixes; an\nunprefixed class coexists with it and loses on specificity. `theme-parts/sidebar.md` shows every\ndefault verbatim, prefixes included.\n\n- **Dark panel.** `dark` scopes the dark theme's variables to the panel, so every neutral ink inside\n flips with it (every theme also emits its variables on `.<name>`, so the class scopes the dark theme to one subtree); needs a theme named `dark`, which the documented setup declares.\n `theme={{ panel: { base: 'dark bg-slate-900' }, mobilePanel: { base: 'dark bg-slate-900' } }}`\n- **Active row in your colour.** `activeVariant=\"solid\"` plus the same prefix chain as the default:\n `theme={{ menuButton: { base: 'rounded-full', activeVariant: { solid: 'data-[active-variant=solid]:data-active:bg-indigo-600 data-[active-variant=solid]:data-active:text-white' } } }}`\n- **No hover change.** The overlay is a `::before` and the default also brightens the ink on hover:\n `theme={{ menuButton: { base: 'state-layer-none hover:text-inherit' }, subButton: { base: 'state-layer-none hover:text-neutral/70' } }}`\n- **Flat panel.** The edge is a prefixed `border-r` / `border-l` on the admin variant, the shadow a\n `raised-*` on the floating ones:\n `theme={{ panel: { base: 'raised-none', variant: { admin: 'data-[side=left]:border-r-0 data-[side=right]:border-l-0' } } }}`\n- **Row height.** A plain height beats the compound `h-control-*`: `theme={{ menuButton: { base: 'h-11' } }}`\n- **Bigger icons.** A prop, not a theme: `iconSize=\"large\"`.\n\n## Styling hooks\n\n`data-open` on a disclosure toggle is a presence attribute (before 0.12.0 a submenu toggle carried `data-open=\"true\"`); match it with `[data-open]` or `data-open:`.\n\nAttributes are presence attributes (set while the state holds, omitted otherwise) unless they carry a value.\n\n**Data attributes**\n- `data-state`: On the root: `expanded` or `collapsed`.\n- `data-display-state`: On the root: what is on screen, `expanded`, `collapsed` (the icon rail) or `hidden`.\n- `data-collapsible`: On the root: `icon` while collapsed to the icon rail, `offcanvas` while hidden, empty while expanded.\n- `data-side`: On the root, the panel and its rail and handles: `left` or `right`.\n- `data-variant`: On the root: the resolved `variant`.\n- `data-size`: On the root: the resolved `size`.\n- `data-density`: On the root: the resolved `density`.\n- `data-peek`: On the layout wrapper: `true` while a hover peek shows the collapsed panel at full width.\n- `data-active`: On a row: `true` while it is the current page (`isActive`).\n- `data-active-variant`: On a row: the resolved `activeVariant`.\n- `data-open`: On the toggle of a collapsible group, a submenu or a tree branch, and on its panel: present while it is open.\n- `data-closed`: On the toggle of a collapsible group, a submenu or a tree branch, and on its panel: present while it is closed, including during the exit transition.\n- `data-entering`: On a group, submenu or tree branch panel: present while its opening animation plays.\n- `data-exiting`: On a group, submenu or tree branch panel: present while its closing animation plays.\n- `data-popup-open`: On a row: present while one of its popups is open, its `menu`, its flyout on the collapsed rail, or its tooltip (every row on the collapsed rail, and every row with `tooltips=\"always\"`). Match `[aria-expanded=\"true\"]` to style an open menu or flyout only.\n- `data-sidebar`: The part (`sidebar`, `menu-button`, `menu-sub`, `group-content`, `rail`, ...).\n\n**CSS variables**\n- `--sidebar-width`: On the root: the expanded panel width.\n- `--sidebar-width-icon`: On the root: the width of the collapsed icon rail.\n- `--sidebar-width-mobile`: On the root: the width of the mobile drawer.\n- `--sidebar-suffix-width`: On a row with a `suffix` (its `menu-item` row wrapper or `menu-sub-item`): the suffix's measured width in px, reserved at the row's end so the label truncates before it.\n- `--disclosure-panel-height`: On a group, submenu or tree branch panel: its measured height in px while it animates, `auto` once open and `0px` once closed.\n\nThe typed maps `sidebarDataAttributes` and `sidebarCssVariables` (from `entasis/sidebar`) list each name with its meaning.\n\n## Accessibility\n\n- The body navigation renders inside a named `<nav>` landmark (`data-sidebar=\"nav\"`), so assistive tech can jump straight to it and tell it apart from the activity bar's own landmark.\n- Active links set `aria-current=\"page\"`.\n- Collapsible rows and groups set `aria-expanded`.\n- Disabled buttons use `disabled`; disabled links omit `href`, use `aria-disabled` and `tabindex=-1`, and block activation.\n- Mobile drawer includes a backdrop button labelled \"Close Sidebar\".\n- Icon-collapsed rows keep their labels mounted and visually fade them, preserving accessible names and stable icon geometry.\n- Search, group controls, actions, and nested rows become inert before collapse can remove or hide them; focus returns to the owning visible row.\n- A row's `suffix` is a sibling of its link or button, between it and the row's `action`: Tab reaches its controls right after the row, its content describes the row (`aria-describedby`), and it is `inert` and `aria-hidden` whenever the Sidebar is collapsed to icons, focus inside it returning to the row.\n- A submenu row's `action` is a sibling of its link, so Tab reaches the action button right after the row; its trigger takes `label` as its accessible name, and Escape closes its menu and returns focus to the trigger. It is `inert` and `aria-hidden` whenever the Sidebar is collapsed to icons.\n- Collapsible groups, tree branches, and inline submenus are disclosure panels (the shared `disclosurePanel` Collapsible and Accordion use): the toggle sets `aria-expanded` and, while the panel is rendered, `aria-controls` to its id; the panel carries `data-open` / `data-closed` and `data-entering` / `data-exiting`, exposes `--disclosure-panel-height` while it animates, and stays `inert` while it closes. Open, close and sidebar-collapse changes animate the panel's height and reverse from where they are when interrupted; the `motion` slot's duration, delay and easing time them.\n- Tree roots use menu-row styling and nested tree nodes use submenu-row styling. In desktop icon mode, a root folder, and a menu row with `items` and no `href`, `view` or `menu`, is a disclosure button (`aria-expanded`, `aria-controls`) that opens a flyout holding a named `<nav>` of links, with NavigationMenu's semantics: nested folders are named sub-lists, a row with an `onclick` and no `href` is a button, a disabled sub entry stays listed without a destination, and a press on any row closes the flyout (the row's own handler runs first, and `preventDefault()` keeps it open). Opening from the keyboard keeps focus on the row; Tab enters the flyout's links, Tab past the last one continues after the row and closes it, Shift+Tab from the first returns to the row, and Escape closes it and refocuses the row. There is no `role=\"menu\"`; root leaves retain direct navigation and tooltips.\n- Row menus, flyouts and tooltips open toward the page: at the inline end of a `left` sidebar and the inline start of a `right` one, the side the layout row puts the page on in either reading direction (a `left` sidebar is the row's start, on the right in right-to-left). An explicit `menuSide` wins.\n- When `rail` and `resizable` are both enabled, one edge control owns click-to-toggle, drag resize, and keyboard resize without overlapping hitboxes.\n- Hidden offcanvas sidebars keep that combined edge control while temporarily revealed. Resizing does not pin the sidebar open; leaving the panel, trigger, and handle tolerance re-hides it without discarding the configured width.\n- Both peeks (edge reveal and `expandOnHover`) stay open while focus is inside the panel or while an overlay opened from inside it is open, including nested submenus. They release about 120ms after the pointer, focus, and every such overlay are gone.\n- The activity bar is its own `<nav>` landmark with a `<ul>` of items, a roving tabindex, and ArrowUp/ArrowDown/Home/End navigation that loops and skips disabled items. Tab lands on the `isActive` item. Each square takes its accessible name from `label`, with a string or number `badge` appended to it, and shows `label` as a tooltip on hover and focus.\n- A hidden offcanvas panel is `inert`, so Tab never lands in a panel parked off screen; a peek makes it interactive again.\n- Views: when focus sat in the view being replaced, it lands on the back row after going deeper and on the row that opened the view after coming back. A view change from outside the panel (a route) leaves focus where it is. The back swipe is a shortcut; the back row stays the accessible path.\n\n## Notes\n\n- Dropdown menus use Entasis `PopupMenu` and `MenuItem[]`.\n- The component uses semantic Entasis tokens. Do not add shadcn `sidebar-*` color tokens.\n- Snippet icons from `entasis/icons/*` are the preferred icon format.\n";
@@ -73,6 +73,9 @@ recursive tree groups, header/footer rows, search, actions, and snippet escape h
73
73
  - **icon**: SidebarIcon - Entasis icon snippet or string.
74
74
  - **iconColor**: Colors - Role tint for the leading icon, applied through \`data-color\`.
75
75
  - **iconVariant**: 'bare' | 'tile' - Leading icon treatment. \`tile\` paints a rounded square (\`bg-color-muted text-color-muted-readable\`) around the glyph, so per-project colour chips come from the role scale instead of hand-built markup.
76
+ - **media**: Snippet<[SidebarApi, SidebarMenuEntry]> - Leading media in place of \`icon\`: a face, an avatar, a logo. It renders inside the row (\`data-sidebar="menu-media"\`, drawn by the \`menuMedia\` theme part) in a box at the Sidebar's media size (32px at \`size="normal"\`; \`iconSize="large"\` gives 36px), which an \`svg\` or \`img\` placed directly inside fills. It receives the row's entry, so one snippet draws every face (\`{#snippet face(api, entry)}\`). Collapsed to icons it stays visible and centred, as an icon does, and it counts as the row's icon for icon-collapse eligibility.
77
+ - **description**: string | Snippet - Second line under the label: the last message, a path, a status. Drawn by the \`menuSecondary\` recipe (lighter, regular weight) and truncated to one line. The row takes the large height (\`h-14\` at \`size="normal"\`) unless the entry sets its own \`size\`. The label, its meta and the description sit in one column (\`menuText\`, \`menuTitleRow\`, \`menuTitle\`, \`menuDescriptionRow\` theme parts) that fades with the labels in icon mode. On such a two-line row the \`badge\` and the \`suffix\` sit at the end of the description's line, which reserves the suffix's width, so the label's line and its \`meta\` end on the same column on every row, with or without a suffix.
78
+ - **meta**: string | Snippet - Short text at the end of the label's line: a time, "Hier" (\`data-sidebar="menu-meta"\`, \`menuMeta\` theme part). The label truncates before it. On a one-line row it ends before any \`suffix\`; on a row with a \`description\` it ends at the row's end, above the suffix. Left out in icon-collapsed mode, like \`badge\`. The row's accessible name reads label, meta, then description.
76
79
  - **href**: string - Render as an anchor. Mutually exclusive with menu.
77
80
  - **onclick**: (event: MouseEvent) => void - Native click handler for button or anchor rows. Mutually exclusive with menu. On a row with \`items\` and no \`href\`, it runs before the submenu toggles, inline or in the collapsed flyout; \`event.preventDefault()\` skips the toggle.
78
81
  - **isActive**: boolean - Adds active styling and \`aria-current="page"\`.
@@ -99,6 +102,7 @@ of them renders exactly as a plain one.
99
102
  - **isActive** / **disabled** / **size** - As on a top-level entry; \`size\` defaults to the Sidebar size.
100
103
  - **iconColor**: Colors - Role tint for the leading icon, applied through \`data-color\`.
101
104
  - **iconVariant**: 'bare' | 'tile' - \`tile\` paints the same tinted rounded square as a top-level row.
105
+ - **media** / **description** / **meta** - As on a top-level entry (\`media\` receives the sub entry). A sub row with a \`description\` or a \`media\` takes the two-line height (40 / 48 / 56px by size).
102
106
  - **badge**: string | number - Trailing count or status; the label truncates before it. It shares the row's end column with the parent rows' badges and makes way for the row's \`action\` as a top-level badge does.
103
107
  - **tooltip**: string - Tooltip text. The label is always visible on an inline sub row, so the tooltip is extra information: shown whenever one is set, never on mobile. The collapsed row's flyout lists only each entry's label, icon, \`href\` or \`onclick\`, and \`isActive\`/\`disabled\` state: it shows no tooltip, \`badge\`, \`suffix\` or \`action\`.
104
108
  - **class**: string - Classes applied to the row button.
@@ -150,6 +154,55 @@ of them renders exactly as a plain one.
150
154
  <Sidebar {items} />
151
155
  \`\`\`
152
156
 
157
+ A conversation list built as data: each row leads with a face (\`media\`), reads a name and a time
158
+ (\`meta\`) over the last message (\`description\`), and ends with an unread dot (\`suffix\`). The
159
+ rows keep \`isActive\`, \`href\`, tooltips and icon collapse.
160
+
161
+ \`\`\`svelte
162
+ <script lang="ts">
163
+ import {
164
+ Sidebar,
165
+ type SidebarApi,
166
+ type SidebarGroup,
167
+ type SidebarMenuEntry,
168
+ type SidebarMenuSubEntry
169
+ } from './index.ts';
170
+
171
+ const agents = [
172
+ { id: 'ada', name: 'Ada', time: '10:42', last: 'The report is ready.', unread: true },
173
+ { id: 'brune', name: 'Brune', time: 'Hier', last: 'Venue booked for Thursday.' }
174
+ ];
175
+
176
+ const items: SidebarGroup[] = [
177
+ {
178
+ label: 'Agents',
179
+ items: agents.map((agent) => ({
180
+ label: agent.name,
181
+ href: \`/agents/\${agent.id}\`,
182
+ media: face,
183
+ meta: agent.time,
184
+ description: agent.last,
185
+ isActive: agent.id === 'ada',
186
+ suffix: agent.unread ? unread : undefined
187
+ }))
188
+ }
189
+ ];
190
+ </script>
191
+
192
+ {#snippet face(_api: SidebarApi, entry: SidebarMenuEntry | SidebarMenuSubEntry)}
193
+ <svg viewBox="0 0 40 40" aria-hidden="true">
194
+ <circle cx="20" cy="20" r="20" fill="currentColor" opacity="0.2" />
195
+ <text x="20" y="25" text-anchor="middle" font-size="14">{entry.label[0]}</text>
196
+ </svg>
197
+ {/snippet}
198
+
199
+ {#snippet unread()}
200
+ <span class="bg-primary block size-2.5 rounded-full" aria-label="Unread"></span>
201
+ {/snippet}
202
+
203
+ <Sidebar {items} iconSize="large" collapsible="icon" />
204
+ \`\`\`
205
+
153
206
  ### SidebarView
154
207
  One named panel content in \`views\`.
155
208
  - **label**: string - The view's name, shown on the back row of the views nested under it.
@@ -172,6 +172,21 @@ export type SidebarMenuSubEntry = {
172
172
  onclick?: (event: MouseEvent) => void;
173
173
  /** Leading icon. */
174
174
  icon?: SidebarIcon;
175
+ /**
176
+ * Leading media in place of `icon` (a face, an avatar, a logo), in a box at the Sidebar's
177
+ * media size (`iconSize` scales it). An `svg` or `img` placed directly inside fills the box.
178
+ * The row takes the two-line height.
179
+ */
180
+ media?: Snippet<[SidebarApi, SidebarMenuSubEntry]>;
181
+ /**
182
+ * Second line under the label: a preview, a path, a status. Lighter, and truncated to one
183
+ * line. The row takes the two-line height.
184
+ */
185
+ description?: string | Snippet;
186
+ /**
187
+ * Short text at the end of the label's line: a time, « Hier ». The label truncates before it.
188
+ */
189
+ meta?: string | Snippet;
175
190
  /** Whether this entry represents the current page. */
176
191
  isActive?: boolean;
177
192
  /** Disable interaction. */
@@ -211,6 +226,22 @@ type SidebarMenuEntryBase = {
211
226
  label: string;
212
227
  /** Leading icon. */
213
228
  icon?: SidebarIcon;
229
+ /**
230
+ * Leading media in place of `icon` (a face, an avatar, a logo), in a box at the Sidebar's
231
+ * media size (`iconSize` scales it). An `svg` or `img` placed directly inside fills the box.
232
+ * It stays visible when the Sidebar collapses to icons, as an icon does.
233
+ */
234
+ media?: Snippet<[SidebarApi, SidebarMenuEntry]>;
235
+ /**
236
+ * Second line under the label: a preview, a path, a status. Lighter, and truncated to one
237
+ * line. The row then takes the large height (`size: 'large'` geometry) unless `size` is set.
238
+ */
239
+ description?: string | Snippet;
240
+ /**
241
+ * Short text at the end of the label's line: a time, « Hier ». The label truncates before it.
242
+ * Hidden in icon-collapsed mode, like `badge`.
243
+ */
244
+ meta?: string | Snippet;
214
245
  /** Role tint applied to the leading icon, through `data-color`. */
215
246
  iconColor?: Colors;
216
247
  /** Leading icon treatment. 'tile' paints a tinted rounded square around the glyph. */