entasis 0.9.2 → 0.10.0

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 (85) hide show
  1. package/dist/components/AppShell/appShell.theme.js +3 -3
  2. package/dist/components/ButtonGroup/ButtonGroup.svelte +10 -4
  3. package/dist/components/ButtonGroup/buttonGroup.mcp.d.ts +1 -1
  4. package/dist/components/ButtonGroup/buttonGroup.mcp.js +23 -3
  5. package/dist/components/ButtonGroup/buttonGroup.props.d.ts +19 -5
  6. package/dist/components/Chart/Chart.svelte +1 -1
  7. package/dist/components/Chart/chart.cartesian.js +3 -2
  8. package/dist/components/Chart/chart.fit.d.ts +53 -0
  9. package/dist/components/Chart/chart.fit.js +81 -0
  10. package/dist/components/Chart/chart.mcp.d.ts +1 -1
  11. package/dist/components/Chart/chart.mcp.js +2 -1
  12. package/dist/components/Chart/chart.polar.js +137 -18
  13. package/dist/components/Chart/chart.polar.props.d.ts +5 -0
  14. package/dist/components/Chart/chart.proportion.js +12 -6
  15. package/dist/components/Chart/chart.relation.network.js +32 -4
  16. package/dist/components/Chart/chart.relation.sankey.js +53 -6
  17. package/dist/components/Chart/chart.relation.tree.js +84 -24
  18. package/dist/components/Chart/chart.series.props.d.ts +6 -0
  19. package/dist/components/Chart/chart.state.svelte.d.ts +1 -0
  20. package/dist/components/Chart/chart.state.svelte.js +9 -6
  21. package/dist/components/Chart/chart.viewport.svelte.d.ts +1 -0
  22. package/dist/components/Chart/chart.viewport.svelte.js +25 -7
  23. package/dist/components/DataTable/DataTable.svelte +25 -8
  24. package/dist/components/DataTable/DataTableRow.svelte +33 -10
  25. package/dist/components/DataTable/dataTable.mcp.d.ts +1 -1
  26. package/dist/components/DataTable/dataTable.mcp.js +12 -1
  27. package/dist/components/DataTable/dataTable.model.svelte.js +4 -3
  28. package/dist/components/DataTable/dataTable.props.d.ts +17 -0
  29. package/dist/components/DataTable/dataTable.theme.d.ts +12 -0
  30. package/dist/components/DataTable/dataTable.theme.js +3 -2
  31. package/dist/components/DataTable/index.d.ts +1 -1
  32. package/dist/components/Dialog/Dialog.svelte +12 -7
  33. package/dist/components/Dialog/dialog.state.svelte.d.ts +2 -1
  34. package/dist/components/Dialog/dialog.state.svelte.js +7 -7
  35. package/dist/components/Dialog/dialog.theme.js +1 -1
  36. package/dist/components/FloatingWindow/floatingWindow.theme.js +1 -1
  37. package/dist/components/Form/Select/Select.svelte +3 -1
  38. package/dist/components/PageShell/pageShell.state.svelte.d.ts +1 -1
  39. package/dist/components/PageShell/pageShell.state.svelte.js +6 -13
  40. package/dist/components/Popover/Popover.svelte +5 -5
  41. package/dist/components/Popover/index.d.ts +1 -1
  42. package/dist/components/Popover/popover.mcp.d.ts +1 -1
  43. package/dist/components/Popover/popover.mcp.js +33 -6
  44. package/dist/components/Popover/popover.state.svelte.d.ts +9 -1
  45. package/dist/components/Popover/popover.state.svelte.js +61 -8
  46. package/dist/components/Popover/popover.theme.js +1 -1
  47. package/dist/components/Sidebar/Sidebar.svelte +1 -1
  48. package/dist/components/Sidebar/SidebarDesktopShell.svelte +26 -6
  49. package/dist/components/Sidebar/sidebar-layout.js +7 -6
  50. package/dist/components/Sidebar/sidebar.mcp.d.ts +1 -1
  51. package/dist/components/Sidebar/sidebar.mcp.js +2 -2
  52. package/dist/components/Sidebar/sidebar.theme.js +36 -20
  53. package/dist/components/Theme/index.d.ts +1 -1
  54. package/dist/components/Theme/index.js +1 -1
  55. package/dist/components/Theme/theme.mcp.d.ts +1 -1
  56. package/dist/components/Theme/theme.mcp.js +3 -0
  57. package/dist/components/Theme/theme.state.svelte.d.ts +4 -0
  58. package/dist/components/Theme/theme.state.svelte.js +4 -0
  59. package/dist/components/Tooltip/Tooltip.svelte +2 -4
  60. package/dist/components/Tooltip/tooltip.attachment.svelte.js +5 -0
  61. package/dist/components/Tooltip/tooltip.mcp.d.ts +1 -1
  62. package/dist/components/Tooltip/tooltip.mcp.js +4 -2
  63. package/dist/generated/componentAliases.d.ts +1 -0
  64. package/dist/generated/componentAliases.js +1 -0
  65. package/dist/generated/componentContract.d.ts +13 -3
  66. package/dist/generated/componentContract.js +15 -1
  67. package/dist/generated/componentMcpRegistry.d.ts +8 -7
  68. package/dist/generated/componentMcpRegistry.js +2 -0
  69. package/dist/i18n/ar.js +1 -1
  70. package/dist/i18n/de.js +1 -1
  71. package/dist/i18n/en.js +1 -1
  72. package/dist/i18n/es.js +1 -1
  73. package/dist/i18n/fr.js +1 -1
  74. package/dist/i18n/pt.js +1 -1
  75. package/dist/i18n/zh.js +1 -1
  76. package/dist/tailwind/colors.d.ts +10 -3
  77. package/dist/tailwind/colors.js +6 -0
  78. package/dist/tailwind/palette.d.ts +5 -0
  79. package/dist/tailwind/palette.js +4 -0
  80. package/dist/tailwind/palette.mcp.d.ts +1 -0
  81. package/dist/tailwind/palette.mcp.js +34 -0
  82. package/dist/utils/layers.svelte.js +9 -8
  83. package/dist/utils/registry.svelte.d.ts +21 -0
  84. package/dist/utils/registry.svelte.js +49 -0
  85. package/package.json +8 -1
@@ -101,7 +101,7 @@
101
101
  !activityBar
102
102
  ? '0px'
103
103
  : variant === 'floating' || variant === 'split'
104
- ? `calc(${activityBarWidth} + var(--spacing) * 2)`
104
+ ? `calc(${activityBarWidth} + var(--space-md))`
105
105
  : activityBarWidth
106
106
  );
107
107
  function setOpen(nextOpen: boolean) {
@@ -101,6 +101,9 @@
101
101
  let edgeTriggerRef: HTMLButtonElement | null = $state(null);
102
102
  let activityBarRef: HTMLElement | null = $state(null);
103
103
 
104
+ const TEXT_ENTRY =
105
+ 'input:not([type="button"],[type="checkbox"],[type="radio"],[type="range"],[type="submit"],[type="reset"]), textarea, select, [contenteditable]:not([contenteditable="false"])';
106
+
104
107
  let focusInside = $state(false);
105
108
  let pointerInside = $state(false);
106
109
 
@@ -161,6 +164,22 @@
161
164
  focusInside = false;
162
165
  });
163
166
 
167
+ // Only the keyboard's focus pins a peek. A click focuses the button it lands on, and focus the
168
+ // page moves after a click (a navigation putting it back on the clicked link) belongs to that
169
+ // click too: either way the panel stayed open after the pointer left the rail. So the input
170
+ // used last decides, as `:focus-visible` does: Tab, or Escape handing focus back from a menu,
171
+ // pins; a press does not. A text field pins either way, so typing never loses the panel.
172
+ let lastInput: 'pointer' | 'keyboard' = 'keyboard';
173
+ $effect(() => {
174
+ const offs = [
175
+ on(window, 'pointerdown', () => (lastInput = 'pointer'), { capture: true }),
176
+ on(window, 'keydown', () => (lastInput = 'keyboard'), { capture: true })
177
+ ];
178
+ return () => offs.forEach((off) => off());
179
+ });
180
+ const pinsPeek = (target: EventTarget | null) =>
181
+ lastInput === 'keyboard' || (target instanceof Element && target.matches(TEXT_ENTRY));
182
+
164
183
  $effect(() => {
165
184
  const nodes = peekRegion;
166
185
  if (!nodes.length) return;
@@ -168,9 +187,9 @@
168
187
  const isInside = (target: EventTarget | null) =>
169
188
  target instanceof Node && nodes.some((node) => node.contains(target));
170
189
  const offs = nodes.flatMap((node) => [
171
- on(node, 'focusin', () => {
172
- focusInside = true;
173
- if (canHoverExpand) hoverExpanded = true;
190
+ on(node, 'focusin', (event) => {
191
+ focusInside = pinsPeek(event.target);
192
+ if (focusInside && canHoverExpand) hoverExpanded = true;
174
193
  }),
175
194
  on(node, 'focusout', (event) => {
176
195
  focusInside = isInside(event.relatedTarget);
@@ -189,7 +208,8 @@
189
208
  // The activity bar is part of the sidebar: hovering it opens the collapsed panel the way the
190
209
  // edge strip (or `expandOnHover`) does, and a peek stays open while the pointer or focus is on
191
210
  // the rail, so switching sections from it never closes the panel it is switching. Keyboard
192
- // focus only keeps a peek: tabbing through the rail does not pop the panel out.
211
+ // focus only keeps a peek: tabbing through the rail does not pop the panel out, and a click
212
+ // keeps it only until the pointer leaves.
193
213
  $effect(() => {
194
214
  const rail = activityBarRef;
195
215
  if (!rail) return;
@@ -204,8 +224,8 @@
204
224
  on(rail, 'pointerleave', (event) => {
205
225
  pointerInside = isInside(event.relatedTarget);
206
226
  }),
207
- on(rail, 'focusin', () => {
208
- if (isPeeking) focusInside = true;
227
+ on(rail, 'focusin', (event) => {
228
+ focusInside = isPeeking && pinsPeek(event.target);
209
229
  }),
210
230
  on(rail, 'focusout', (event) => {
211
231
  focusInside = isInside(event.relatedTarget);
@@ -5,11 +5,11 @@ import { cx } from '../../utils/cva/index.js';
5
5
  export function getSidebarGapClass(variant) {
6
6
  const hasInlineInset = variant === 'floating' || variant === 'split';
7
7
  return cx('relative w-[calc(var(--sidebar-width)+var(--sidebar-activity-offset,0px))] bg-transparent transition-[width] duration-normal ease-linear group-data-[width-prehydrating=true]/sidebar-wrapper:!transition-none group-data-[resizing=true]:!transition-none group-data-[collapsible=offcanvas]:w-[var(--sidebar-activity-offset,0px)]', hasInlineInset
8
- ? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+1rem+var(--sidebar-activity-offset,0px))]'
8
+ ? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+var(--space-md)*2+var(--sidebar-activity-offset,0px))]'
9
9
  : 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+var(--sidebar-activity-offset,0px))]',
10
10
  // A hover peek overlays the page, so the reserved column must stay at its icon width.
11
11
  hasInlineInset
12
- ? 'group-data-[peek=true]:!w-[calc(var(--sidebar-width-icon)+1rem+var(--sidebar-activity-offset,0px))]'
12
+ ? 'group-data-[peek=true]:!w-[calc(var(--sidebar-width-icon)+var(--space-md)*2+var(--sidebar-activity-offset,0px))]'
13
13
  : 'group-data-[peek=true]:!w-[calc(var(--sidebar-width-icon)+var(--sidebar-activity-offset,0px))]');
14
14
  }
15
15
  function getContainerGeometryClass(variant) {
@@ -17,9 +17,9 @@ function getContainerGeometryClass(variant) {
17
17
  return 'group-data-[collapsible=icon]:w-[var(--sidebar-width-icon)]';
18
18
  }
19
19
  if (variant === 'inset') {
20
- return 'py-2 group-data-[collapsible=icon]:w-[var(--sidebar-width-icon)]';
20
+ return 'py-md group-data-[collapsible=icon]:w-[var(--sidebar-width-icon)]';
21
21
  }
22
- return 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+1rem)]';
22
+ return 'p-md group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+var(--space-md)*2)]';
23
23
  }
24
24
  export function getSidebarContainerClass(side, variant, isEdgeRevealed, frame) {
25
25
  const panelOwnsShadow = variant === 'floating' || variant === 'split';
@@ -41,7 +41,7 @@ export function getSidebarContainerClass(side, variant, isEdgeRevealed, frame) {
41
41
  'group-data-[peek=true]:z-40 group-data-[peek=true]:!w-[var(--sidebar-width)]', isEdgeRevealed && 'z-40', isEdgeRevealed && !panelOwnsShadow && 'shadow-xl',
42
42
  // Inset keeps a vertical gutter so the resting column sits level with the page card. An
43
43
  // edge reveal overlays the page instead, so the peek runs the full height like any other
44
- // temporary drawer rather than floating 0.5rem short of the top and bottom edges.
44
+ // temporary drawer rather than floating a gutter short of the top and bottom edges.
45
45
  isEdgeRevealed && variant === 'inset' && '!py-0', side === 'left' && isEdgeRevealed && '!left-[var(--sidebar-activity-offset,0px)]', side === 'right' && isEdgeRevealed && '!right-[var(--sidebar-activity-offset,0px)]');
46
46
  }
47
47
  /**
@@ -59,5 +59,6 @@ export function getSidebarPanelPeekClass(variant) {
59
59
  * ones for floating and split (the panel's own gutter spaces the pair).
60
60
  */
61
61
  export function getSidebarActivityBarContainerClass(side, frame, variant) {
62
- return cx('inset-y-0 z-30 hidden w-[var(--sidebar-activity-offset,3rem)] md:flex', frame === 'viewport' ? 'fixed h-window' : 'absolute h-full', side === 'left' ? 'left-0' : 'right-0', variant === 'inset' && 'py-2', (variant === 'floating' || variant === 'split') && (side === 'left' ? 'py-2 pl-2' : 'py-2 pr-2'));
62
+ return cx('inset-y-0 z-30 hidden w-[var(--sidebar-activity-offset,3rem)] md:flex', frame === 'viewport' ? 'fixed h-window' : 'absolute h-full', side === 'left' ? 'left-0' : 'right-0', variant === 'inset' && 'py-md', (variant === 'floating' || variant === 'split') &&
63
+ (side === 'left' ? 'py-md pl-md' : 'py-md pr-md'));
63
64
  }
@@ -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]> - Top-right group actions. 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.\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.\n- **tooltip**: string - Entasis Tooltip content in icon mode. Defaults to label.\n- **items**: SidebarMenuSubEntry[] - Inline nested menu.\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- **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\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.\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`. 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 renders as a horizontal row at the top of the drawer.\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. 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 y-axis slide shared by collapsible groups, inline submenus, and\n tree branches. Takes `in` / `out` slide params plus a `duration` / `easing` motion token.\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## 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- Nested groups, tree branches, and inline submenus use reversible height transitions for open, close, and sidebar-collapse changes.\n- Tree roots use menu-row styling and nested tree nodes use submenu-row styling. In desktop icon mode, root folders open a PopupMenu and descendants remain navigable through recursive Menu submenu popovers; root leaves retain direct navigation and tooltips.\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]> - Top-right group actions. 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.\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.\n- **tooltip**: string - Entasis Tooltip content in icon mode. Defaults to label.\n- **items**: SidebarMenuSubEntry[] - Inline nested menu.\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- **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\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.\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 renders as a horizontal row at the top of the drawer.\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 y-axis slide shared by collapsible groups, inline submenus, and\n tree branches. Takes `in` / `out` slide params plus a `duration` / `easing` motion token.\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## 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- Nested groups, tree branches, and inline submenus use reversible height transitions for open, close, and sidebar-collapse changes.\n- Tree roots use menu-row styling and nested tree nodes use submenu-row styling. In desktop icon mode, root folders open a PopupMenu and descendants remain navigable through recursive Menu submenu popovers; root leaves retain direct navigation and tooltips.\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";
@@ -155,10 +155,10 @@ Use for \`headerButton\`, \`footerButton\`, or direct \`<SidebarMenuButton />\`
155
155
  - **widthIcon**: string - Icon-collapsed width.
156
156
  - **widthMobile**: string - Mobile drawer width.
157
157
  - **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.
158
- - **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\`. 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 renders as a horizontal row at the top of the drawer.
158
+ - **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 renders as a horizontal row at the top of the drawer.
159
159
  - **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.
160
160
  - **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.
161
- - **resizable**: boolean | SidebarResizableOptions - Enables pointer and keyboard resizing while expanded, icon-collapsed, or temporarily edge-revealed. 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.
161
+ - **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.
162
162
  - \`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\`).
163
163
 
164
164
  ### Content
@@ -75,7 +75,7 @@ const defaultPanel = cva({
75
75
  {
76
76
  variant: 'inset',
77
77
  placement: 'static',
78
- class: 'my-md h-[calc(100%_-_1rem)]'
78
+ class: 'my-md h-[calc(100%-var(--space-md)*2)]'
79
79
  }
80
80
  ],
81
81
  defaultVariants: {
@@ -514,9 +514,9 @@ const defaultRail = cva({
514
514
  variants: {
515
515
  variant: {
516
516
  admin: 'inset-y-0',
517
- floating: 'inset-y-2',
518
- inset: 'inset-y-2',
519
- split: 'inset-y-2',
517
+ floating: 'inset-y-[var(--space-md)]',
518
+ inset: 'inset-y-[var(--space-md)]',
519
+ split: 'inset-y-[var(--space-md)]',
520
520
  framed: 'inset-y-0'
521
521
  },
522
522
  // A peek widens the panel without widening the reserved column, so the rail has to travel
@@ -533,19 +533,27 @@ const defaultRail = cva({
533
533
  compoundVariants: [
534
534
  { variant: ['admin', 'framed', 'inset'], side: 'left', class: 'translate-x-1/2' },
535
535
  { variant: ['admin', 'framed', 'inset'], side: 'right', class: '-translate-x-1/2' },
536
- { variant: ['floating', 'split'], side: 'left', class: 'right-2 translate-x-1/2' },
537
- { variant: ['floating', 'split'], side: 'right', class: 'left-2 -translate-x-1/2' },
538
- // Floating and split reserve an extra 1rem gutter while collapsed, so their peek travel
539
- // is that much shorter.
540
536
  {
541
537
  variant: ['floating', 'split'],
542
538
  side: 'left',
543
- class: 'group-data-[peek=true]:translate-x-[calc(var(--sidebar-width)_-_var(--sidebar-width-icon)_-_1rem_+_50%)]'
539
+ class: 'right-[var(--space-md)] translate-x-1/2'
544
540
  },
545
541
  {
546
542
  variant: ['floating', 'split'],
547
543
  side: 'right',
548
- class: 'group-data-[peek=true]:translate-x-[calc(var(--sidebar-width-icon)_+_1rem_-_var(--sidebar-width)_-_50%)]'
544
+ class: 'left-[var(--space-md)] -translate-x-1/2'
545
+ },
546
+ // Floating and split reserve two extra gutters (`--space-md` each side) while collapsed, so
547
+ // their peek travel is that much shorter.
548
+ {
549
+ variant: ['floating', 'split'],
550
+ side: 'left',
551
+ class: 'group-data-[peek=true]:translate-x-[calc(var(--sidebar-width)_-_var(--sidebar-width-icon)_-_var(--space-md)*2_+_50%)]'
552
+ },
553
+ {
554
+ variant: ['floating', 'split'],
555
+ side: 'right',
556
+ class: 'group-data-[peek=true]:translate-x-[calc(var(--sidebar-width-icon)_+_var(--space-md)*2_-_var(--sidebar-width)_-_50%)]'
549
557
  }
550
558
  ],
551
559
  defaultVariants: {
@@ -559,9 +567,9 @@ const defaultResizeHandle = cva({
559
567
  variants: {
560
568
  variant: {
561
569
  admin: '',
562
- floating: 'inset-y-2',
563
- inset: 'inset-y-2',
564
- split: 'inset-y-2',
570
+ floating: 'inset-y-[var(--space-md)]',
571
+ inset: 'inset-y-[var(--space-md)]',
572
+ split: 'inset-y-[var(--space-md)]',
565
573
  framed: ''
566
574
  },
567
575
  side: {
@@ -588,17 +596,25 @@ const defaultResizeHandle = cva({
588
596
  compoundVariants: [
589
597
  { variant: ['admin', 'framed', 'inset'], side: 'left', class: 'translate-x-1/2' },
590
598
  { variant: ['admin', 'framed', 'inset'], side: 'right', class: '-translate-x-1/2' },
591
- { variant: ['floating', 'split'], side: 'left', class: 'right-2 translate-x-1/2' },
592
- { variant: ['floating', 'split'], side: 'right', class: 'left-2 -translate-x-1/2' },
593
599
  {
594
600
  variant: ['floating', 'split'],
595
601
  side: 'left',
596
- class: 'group-data-[peek=true]:translate-x-[calc(var(--sidebar-width)_-_var(--sidebar-width-icon)_-_1rem_+_50%)]'
602
+ class: 'right-[var(--space-md)] translate-x-1/2'
603
+ },
604
+ {
605
+ variant: ['floating', 'split'],
606
+ side: 'right',
607
+ class: 'left-[var(--space-md)] -translate-x-1/2'
608
+ },
609
+ {
610
+ variant: ['floating', 'split'],
611
+ side: 'left',
612
+ class: 'group-data-[peek=true]:translate-x-[calc(var(--sidebar-width)_-_var(--sidebar-width-icon)_-_var(--space-md)*2_+_50%)]'
597
613
  },
598
614
  {
599
615
  variant: ['floating', 'split'],
600
616
  side: 'right',
601
- class: 'group-data-[peek=true]:translate-x-[calc(var(--sidebar-width-icon)_+_1rem_-_var(--sidebar-width)_-_50%)]'
617
+ class: 'group-data-[peek=true]:translate-x-[calc(var(--sidebar-width-icon)_+_var(--space-md)*2_-_var(--sidebar-width)_-_50%)]'
602
618
  }
603
619
  ],
604
620
  defaultVariants: {
@@ -647,7 +663,7 @@ const defaultMain = cva({
647
663
  {
648
664
  variant: ['inset', 'split'],
649
665
  flush: false,
650
- class: 'md:[--page-shell-edge-inset:0.5rem]'
666
+ class: 'md:[--page-shell-edge-inset:var(--space-md)]'
651
667
  },
652
668
  {
653
669
  variant: 'inset',
@@ -703,7 +719,7 @@ const defaultEdgeTrigger = cva({
703
719
  base: 'absolute inset-y-0 z-50 hidden w-3 bg-transparent outline-none md:block after:absolute after:inset-y-0 after:w-px after:bg-transparent after:transition-[background-color] after:duration-normal after:ease-standard hover:after:bg-neutral/45 focus-visible:ring-2 focus-visible:ring-focus/50 data-[side=left]:left-0 data-[side=left]:cursor-e-resize data-[side=left]:after:left-0 data-[side=right]:right-0 data-[side=right]:cursor-w-resize data-[side=right]:after:right-0'
704
720
  });
705
721
  const defaultOverlay = cva({
706
- base: 'fixed inset-0 z-40 bg-neutral/35 md:hidden'
722
+ base: 'fixed inset-0 z-40 bg-black/40 md:hidden dark:bg-black/60'
707
723
  });
708
724
  const defaultMobilePanel = cva({
709
725
  base: 'flex h-full min-h-0 w-[var(--sidebar-width-mobile)] flex-col bg-surface-floating text-neutral md:hidden',
@@ -817,7 +833,7 @@ const defaultActivityBar = cva({
817
833
  orientation: 'vertical',
818
834
  variant: 'inset',
819
835
  placement: 'static',
820
- class: 'my-md h-[calc(100%_-_1rem)]'
836
+ class: 'my-md h-[calc(100%-var(--space-md)*2)]'
821
837
  }
822
838
  ],
823
839
  defaultVariants: {
@@ -1,5 +1,5 @@
1
1
  export { default as Theme } from './Theme.svelte';
2
- export { ThemeState, useDefaultColor } from './theme.state.svelte.js';
2
+ export { ThemeState, useDefaultColor, useTheme } from './theme.state.svelte.js';
3
3
  export type { ThemeProps } from './theme.props.js';
4
4
  export { defaultThemeSpacingScale, typeScalePresets, type ThemeDesignTokenMap, type ThemeDesignTokens, type ThemeRadius, type ThemeSpacing, type ThemeSpacingScale, type ThemeSpacingStep, type TypeScaleOptions, type TypeScalePreset, type TypeScaleRatio } from './theme.designTokens.js';
5
5
  export { themeTransitions, type ThemeTransition } from './themeTransition.js';
@@ -1,5 +1,5 @@
1
1
  export { default as Theme } from './Theme.svelte';
2
- export { ThemeState, useDefaultColor } from './theme.state.svelte.js';
2
+ export { ThemeState, useDefaultColor, useTheme } from './theme.state.svelte.js';
3
3
  export { defaultThemeSpacingScale, typeScalePresets } from './theme.designTokens.js';
4
4
  export { themeTransitions } from './themeTransition.js';
5
5
  export { focusRing, selectedSoft, selectedSolid } from './theme.recipes.js';
@@ -1 +1 @@
1
- export declare const themeDescription = "\n# Theme\n\n`Theme` owns global theme selection, runtime design tokens, shared overlay state, and theme\ntransitions. Wrap the application once and use the `ThemeState` received by the children snippet.\n\n## Runtime design tokens\n\n```svelte\n<script lang=\"ts\">\n\timport { Theme, type ThemeDesignTokenMap } from 'entasis/theme';\n\n\tlet spacing = $state<'small' | 'normal' | 'large'>('normal');\n\tconst designTokens = $derived({\n\t\tlight: {\n\t\t\tspacing,\n\t\t\tspacingScale: { xs: 1, sm: 1.5, md: 2, lg: 3, xl: 4 },\n\t\t\tradius: 'normal',\n\t\t\ttypeScale: 'default',\n\t\t\traisedWithBorder: true,\n\t\t\tdefaultColor: 'neutral'\n\t\t},\n\t\tdark: {\n\t\t\tspacing,\n\t\t\tspacingScale: { xs: 1, sm: 1.5, md: 2, lg: 3, xl: 4 },\n\t\t\tradius: 'small',\n\t\t\ttypeScale: 'compact',\n\t\t\traisedWithBorder: false,\n\t\t\tdefaultColor: 'neutral'\n\t\t}\n\t} satisfies ThemeDesignTokenMap<readonly ['light', 'dark']>);\n</script>\n\n<Theme {designTokens} transition=\"radial-top-right\">\n\t{#snippet children(theme)}\n\t\t<button onclick={() => (spacing = spacing === 'small' ? 'large' : 'small')}>\n\t\t\tChange density\n\t\t</button>\n\t\t<button onclick={() => (theme.theme = theme.resolvedTheme === 'dark' ? 'light' : 'dark')}>\n\t\t\tToggle color scheme\n\t\t</button>\n\t{/snippet}\n</Theme>\n```\n\n`designTokens` is keyed by logical theme name and respects the `attribute` and `value` props.\nEleven presets ship as `themePresets` (`dense`, `compact`, `balanced`, `comfortable`, `spacious`,\n`sharp`, `rounded`, `display`, `editorial`, `glass`, `terminal`): `designTokens={{ light: themePresets.glass.tokens, dark: themePresets.glass.tokens }}`.\nChanging the controlled object updates already-rendered Tailwind utilities without rebuilding CSS.\n\n### ThemeDesignTokens\n\n- `spacing`: `'small' | 'normal' | 'large' | number`. Globally scales density.\n- `spacingScale`: partial overrides for the strictly increasing `xs`, `sm`, `md`, `lg`,\n and `xl` spacing multipliers. Defaults to 1/1.5/2/3/4.\n- `radius`: `'none' | 'subtile' | 'small' | 'normal' | 'large' | 'round' | number`.\n Controls take the full multiplier; surface steps (`lg` and up) stop at `large` (1.5\u00D7).\n- `typeScale`: `'compact' | 'default' | 'comfortable' | 'large' | TypeScaleOptions`.\n- `raisedWithBorder`: toggles the border used by `raised-*` utilities.\n- `defaultColor`: `Colors` role kit chrome inherits when a control omits `color`.\n Defaults to `neutral`. Set `primary` to restore an accent-colored kit. Compiles the\n current-color family (`--color`, `--color-readable`, muted/contrast/light/dark variants)\n and `--default-color` onto the theme selector. Do not set `data-color` on `html`.\n- `focusColor`, `selectedColor`, `hoverColor`, `pressedColor`: the four `Colors` **state\n roles**. They pin, for the whole theme, what a focus ring, a persistent selection and the\n transient hover/pressed layer look like, independently of the role of the control the state\n lands on. They compile `--color-focus`, `--color-selected` (plus its `-contrast`,\n `-readable` and `-muted-readable` companions), `--color-hover` and\n `--color-pressed` onto the theme selector. There is no `--color-selected-muted`: the soft\n fill is a translucent tint of `--color-selected` at `--state-selected-opacity`, so it reads\n on any surface. None is declared at `:root`: every use site\n falls back to the matching current role (`ring-focus` is\n `var(--color-focus, var(--color))`, `bg-selected-muted` tints\n `var(--color-selected, var(--color))`, the state layer is\n `var(--color-hover, currentColor)` and on `:active`\n `var(--color-pressed, var(--color-hover, currentColor))`), so leaving them unset changes\n nothing and `data-color` keeps moving the states with `--color`. Theme-level only: there is\n no per-component override. An unknown role throws.\n\nComponent-level density remains a local variant. It selects utility classes whose values inherit\nthe active global spacing token.\n\nGenerated interfaces should use the public `xs | sm | md | lg | xl` vocabulary through component\nprops and named gap/padding utilities. `micro` and `layout-*` are internal recipe tokens. Prefer\nparent-owned gaps over child margins; do not emit arbitrary spacing or unsupported radius values.\n\n## Theme selection\n\nThe selection props wrap `svelte-themes`: `themes`, `defaultTheme`, `forcedTheme`,\n`systemTheme`, `syncColorScheme`, `transitionOnChange`, `storageKey`, `attribute`,\n`value`, and `colorScheme`. The default themes are light and dark, with system selection\nenabled. `systemTheme`, `syncColorScheme`, and `transitionOnChange` all default to\n`true`; they map onto the library's `enableSystem`, `enableColorScheme`, and\n`disableTransitionOnChange` options.\n\n`ThemeState` exposes `theme`, `resolvedTheme`, `themes`, and `systemTheme`. Assign\n`theme.theme` to switch themes. The optional `transition` prop applies a named view transition;\nunsupported browsers and reduced-motion users switch instantly.\n\n`spinnerVariant` sets the global default spinner animation. The children snippet is required.\n\n## Motion tokens\n\nMotion is a token scale like spacing and radius: five duration steps and four easing roles.\n`motion` retunes them app-wide; an omitted token keeps its default.\n\n| Duration | Default | | Easing role | Default |\n| ---------- | ------- | --- | ------------ | ------------- |\n| `instant` | 0ms | | `standard` | `cubicInOut` |\n| `fast` | 100ms | | `enter` | `cubicOut` |\n| `normal` | 200ms | | `exit` | `cubicIn` |\n| `slow` | 300ms | | `emphasized` | `backOut` |\n| `slower` | 500ms | | | |\n\n```svelte\n<script lang=\"ts\">\n\timport { Theme } from 'entasis/theme';\n\n\tlet { children } = $props();\n</script>\n\n<Theme motion={{ duration: { normal: 150, slow: 260 }, easing: { standard: 'quintOut' } }}>\n\t{@render children()}\n</Theme>\n```\n\nThe Tailwind plugin emits the same scale as CSS variables on `html` (`--duration-normal`,\n`--ease-standard`, ...) plus the matching `duration-*` / `ease-*` utilities, so CSS transitions\nand Svelte transitions read one set of numbers. The `motion` prop rewrites those variables on\n`html` at runtime and `designTokens.motion` rewrites them again per theme, layered over the prop;\n`ThemeState.motion` resolves through the same two rungs, so the utilities and the presets never\ndisagree. `ThemeState.transition` is a deprecated alias for its `normal` duration and `standard`\neasing. Reduced motion resolves every duration to 0 and collapses the `--duration-*` variables via\nthe `data-entasis-reduce-motion` attribute on `html`.\n\nComponents keep their own transition in a reserved `motion` slot on their theme, so the `theme`\nprop covers motion as well as classes:\n\n```svelte\n<script lang=\"ts\">\n\timport { Dialog } from 'entasis/dialog';\n</script>\n\n<Dialog theme={{ motion: { duration: 'fast', easing: 'emphasized' } }} title=\"Quick\">Body</Dialog>\n```\n\n## Component theme registry\n\n`components` sets app-wide component theme defaults without a wrapper component per component:\nit is keyed by theme name (`dialog`, `button`, ...) and each entry takes the same slots as that\ncomponent's `theme` prop, the `motion` slot included. A `set<Component>Theme` call in a subtree\nbeats the registry, and an instance `theme` prop beats both \u2014 per slot: each rung layers on the one\nbelow it, so a subtree that restyles one slot keeps the registry's others.\n\n```svelte\n<script lang=\"ts\">\n\timport { Theme } from 'entasis/theme';\n\n\tlet { children } = $props();\n</script>\n\n<Theme\n\tcomponents={{\n\t\tdialog: { motion: { duration: 'fast' }, content: { base: 'rounded-2xl' } },\n\t\tbutton: { root: { base: 'tracking-wide' } }\n\t}}\n>\n\t{@render children()}\n</Theme>\n```\n\n## Reduced motion\n\n`reduceMotion` forces reduced motion on (`true`) or off (`false`) for every entasis animation,\noverriding the OS `prefers-reduced-motion` setting; omit it to follow the OS. The live result is\nexposed as `ThemeState.preferReducesMotion` (reactive, so it updates when the OS setting changes)\nand mirrored as a `data-entasis-reduce-motion` attribute on `html` for CSS-only animations.\n\n```svelte\n<script>\n\timport { Theme } from 'entasis/theme';\n\n\tlet { children } = $props();\n</script>\n\n<Theme reduceMotion>{@render children()}</Theme>\n```\n\n## Build-time boundary\n\nThe Tailwind plugin still generates color palettes and registers utility names, variants,\nkeyframes, and spinner CSS. Spacing, radius, typography scale, raised borders,\n`defaultColor` and the four state roles (`focusColor`, `selectedColor`, `hoverColor`,\n`pressedColor`) belong to `Theme.designTokens`; colors remain CSS variables and can be\noverridden directly. `ThemeState.defaultColor` exposes the active role. Kit chrome should\nresolve omitted `color` props with `useDefaultColor`.\n";
1
+ export declare const themeDescription = "\n# Theme\n\n`Theme` owns global theme selection, runtime design tokens, shared overlay state, and theme\ntransitions. Wrap the application once and use the `ThemeState` received by the children snippet.\nComponents below it read the same state with `useTheme()` from `entasis/theme`, called during\ncomponent initialisation; it throws when no `Theme` is above. For a palette computed at runtime,\nsee `generateColorPalette` in `entasis/color-palette`.\n\n## Runtime design tokens\n\n```svelte\n<script lang=\"ts\">\n\timport { Theme, type ThemeDesignTokenMap } from 'entasis/theme';\n\n\tlet spacing = $state<'small' | 'normal' | 'large'>('normal');\n\tconst designTokens = $derived({\n\t\tlight: {\n\t\t\tspacing,\n\t\t\tspacingScale: { xs: 1, sm: 1.5, md: 2, lg: 3, xl: 4 },\n\t\t\tradius: 'normal',\n\t\t\ttypeScale: 'default',\n\t\t\traisedWithBorder: true,\n\t\t\tdefaultColor: 'neutral'\n\t\t},\n\t\tdark: {\n\t\t\tspacing,\n\t\t\tspacingScale: { xs: 1, sm: 1.5, md: 2, lg: 3, xl: 4 },\n\t\t\tradius: 'small',\n\t\t\ttypeScale: 'compact',\n\t\t\traisedWithBorder: false,\n\t\t\tdefaultColor: 'neutral'\n\t\t}\n\t} satisfies ThemeDesignTokenMap<readonly ['light', 'dark']>);\n</script>\n\n<Theme {designTokens} transition=\"radial-top-right\">\n\t{#snippet children(theme)}\n\t\t<button onclick={() => (spacing = spacing === 'small' ? 'large' : 'small')}>\n\t\t\tChange density\n\t\t</button>\n\t\t<button onclick={() => (theme.theme = theme.resolvedTheme === 'dark' ? 'light' : 'dark')}>\n\t\t\tToggle color scheme\n\t\t</button>\n\t{/snippet}\n</Theme>\n```\n\n`designTokens` is keyed by logical theme name and respects the `attribute` and `value` props.\nEleven presets ship as `themePresets` (`dense`, `compact`, `balanced`, `comfortable`, `spacious`,\n`sharp`, `rounded`, `display`, `editorial`, `glass`, `terminal`): `designTokens={{ light: themePresets.glass.tokens, dark: themePresets.glass.tokens }}`.\nChanging the controlled object updates already-rendered Tailwind utilities without rebuilding CSS.\n\n### ThemeDesignTokens\n\n- `spacing`: `'small' | 'normal' | 'large' | number`. Globally scales density.\n- `spacingScale`: partial overrides for the strictly increasing `xs`, `sm`, `md`, `lg`,\n and `xl` spacing multipliers. Defaults to 1/1.5/2/3/4.\n- `radius`: `'none' | 'subtile' | 'small' | 'normal' | 'large' | 'round' | number`.\n Controls take the full multiplier; surface steps (`lg` and up) stop at `large` (1.5\u00D7).\n- `typeScale`: `'compact' | 'default' | 'comfortable' | 'large' | TypeScaleOptions`.\n- `raisedWithBorder`: toggles the border used by `raised-*` utilities.\n- `defaultColor`: `Colors` role kit chrome inherits when a control omits `color`.\n Defaults to `neutral`. Set `primary` to restore an accent-colored kit. Compiles the\n current-color family (`--color`, `--color-readable`, muted/contrast/light/dark variants)\n and `--default-color` onto the theme selector. Do not set `data-color` on `html`.\n- `focusColor`, `selectedColor`, `hoverColor`, `pressedColor`: the four `Colors` **state\n roles**. They pin, for the whole theme, what a focus ring, a persistent selection and the\n transient hover/pressed layer look like, independently of the role of the control the state\n lands on. They compile `--color-focus`, `--color-selected` (plus its `-contrast`,\n `-readable` and `-muted-readable` companions), `--color-hover` and\n `--color-pressed` onto the theme selector. There is no `--color-selected-muted`: the soft\n fill is a translucent tint of `--color-selected` at `--state-selected-opacity`, so it reads\n on any surface. None is declared at `:root`: every use site\n falls back to the matching current role (`ring-focus` is\n `var(--color-focus, var(--color))`, `bg-selected-muted` tints\n `var(--color-selected, var(--color))`, the state layer is\n `var(--color-hover, currentColor)` and on `:active`\n `var(--color-pressed, var(--color-hover, currentColor))`), so leaving them unset changes\n nothing and `data-color` keeps moving the states with `--color`. Theme-level only: there is\n no per-component override. An unknown role throws.\n\nComponent-level density remains a local variant. It selects utility classes whose values inherit\nthe active global spacing token.\n\nGenerated interfaces should use the public `xs | sm | md | lg | xl` vocabulary through component\nprops and named gap/padding utilities. `micro` and `layout-*` are internal recipe tokens. Prefer\nparent-owned gaps over child margins; do not emit arbitrary spacing or unsupported radius values.\n\n## Theme selection\n\nThe selection props wrap `svelte-themes`: `themes`, `defaultTheme`, `forcedTheme`,\n`systemTheme`, `syncColorScheme`, `transitionOnChange`, `storageKey`, `attribute`,\n`value`, and `colorScheme`. The default themes are light and dark, with system selection\nenabled. `systemTheme`, `syncColorScheme`, and `transitionOnChange` all default to\n`true`; they map onto the library's `enableSystem`, `enableColorScheme`, and\n`disableTransitionOnChange` options.\n\n`ThemeState` exposes `theme`, `resolvedTheme`, `themes`, and `systemTheme`. Assign\n`theme.theme` to switch themes. The optional `transition` prop applies a named view transition;\nunsupported browsers and reduced-motion users switch instantly.\n\n`spinnerVariant` sets the global default spinner animation. The children snippet is required.\n\n## Motion tokens\n\nMotion is a token scale like spacing and radius: five duration steps and four easing roles.\n`motion` retunes them app-wide; an omitted token keeps its default.\n\n| Duration | Default | | Easing role | Default |\n| ---------- | ------- | --- | ------------ | ------------- |\n| `instant` | 0ms | | `standard` | `cubicInOut` |\n| `fast` | 100ms | | `enter` | `cubicOut` |\n| `normal` | 200ms | | `exit` | `cubicIn` |\n| `slow` | 300ms | | `emphasized` | `backOut` |\n| `slower` | 500ms | | | |\n\n```svelte\n<script lang=\"ts\">\n\timport { Theme } from 'entasis/theme';\n\n\tlet { children } = $props();\n</script>\n\n<Theme motion={{ duration: { normal: 150, slow: 260 }, easing: { standard: 'quintOut' } }}>\n\t{@render children()}\n</Theme>\n```\n\nThe Tailwind plugin emits the same scale as CSS variables on `html` (`--duration-normal`,\n`--ease-standard`, ...) plus the matching `duration-*` / `ease-*` utilities, so CSS transitions\nand Svelte transitions read one set of numbers. The `motion` prop rewrites those variables on\n`html` at runtime and `designTokens.motion` rewrites them again per theme, layered over the prop;\n`ThemeState.motion` resolves through the same two rungs, so the utilities and the presets never\ndisagree. `ThemeState.transition` is a deprecated alias for its `normal` duration and `standard`\neasing. Reduced motion resolves every duration to 0 and collapses the `--duration-*` variables via\nthe `data-entasis-reduce-motion` attribute on `html`.\n\nComponents keep their own transition in a reserved `motion` slot on their theme, so the `theme`\nprop covers motion as well as classes:\n\n```svelte\n<script lang=\"ts\">\n\timport { Dialog } from 'entasis/dialog';\n</script>\n\n<Dialog theme={{ motion: { duration: 'fast', easing: 'emphasized' } }} title=\"Quick\">Body</Dialog>\n```\n\n## Component theme registry\n\n`components` sets app-wide component theme defaults without a wrapper component per component:\nit is keyed by theme name (`dialog`, `button`, ...) and each entry takes the same slots as that\ncomponent's `theme` prop, the `motion` slot included. A `set<Component>Theme` call in a subtree\nbeats the registry, and an instance `theme` prop beats both \u2014 per slot: each rung layers on the one\nbelow it, so a subtree that restyles one slot keeps the registry's others.\n\n```svelte\n<script lang=\"ts\">\n\timport { Theme } from 'entasis/theme';\n\n\tlet { children } = $props();\n</script>\n\n<Theme\n\tcomponents={{\n\t\tdialog: { motion: { duration: 'fast' }, content: { base: 'rounded-2xl' } },\n\t\tbutton: { root: { base: 'tracking-wide' } }\n\t}}\n>\n\t{@render children()}\n</Theme>\n```\n\n## Reduced motion\n\n`reduceMotion` forces reduced motion on (`true`) or off (`false`) for every entasis animation,\noverriding the OS `prefers-reduced-motion` setting; omit it to follow the OS. The live result is\nexposed as `ThemeState.preferReducesMotion` (reactive, so it updates when the OS setting changes)\nand mirrored as a `data-entasis-reduce-motion` attribute on `html` for CSS-only animations.\n\n```svelte\n<script>\n\timport { Theme } from 'entasis/theme';\n\n\tlet { children } = $props();\n</script>\n\n<Theme reduceMotion>{@render children()}</Theme>\n```\n\n## Build-time boundary\n\nThe Tailwind plugin still generates color palettes and registers utility names, variants,\nkeyframes, and spinner CSS. Spacing, radius, typography scale, raised borders,\n`defaultColor` and the four state roles (`focusColor`, `selectedColor`, `hoverColor`,\n`pressedColor`) belong to `Theme.designTokens`; colors remain CSS variables and can be\noverridden directly. `ThemeState.defaultColor` exposes the active role. Kit chrome should\nresolve omitted `color` props with `useDefaultColor`.\n";
@@ -3,6 +3,9 @@ export const themeDescription = `
3
3
 
4
4
  \`Theme\` owns global theme selection, runtime design tokens, shared overlay state, and theme
5
5
  transitions. Wrap the application once and use the \`ThemeState\` received by the children snippet.
6
+ Components below it read the same state with \`useTheme()\` from \`entasis/theme\`, called during
7
+ component initialisation; it throws when no \`Theme\` is above. For a palette computed at runtime,
8
+ see \`generateColorPalette\` in \`entasis/color-palette\`.
6
9
 
7
10
  ## Runtime design tokens
8
11
 
@@ -112,6 +112,10 @@ export declare class ThemeState extends ThemeState_base {
112
112
  addEventListener: <E extends Events>(event: E, callback: (event: EventPayload[E]) => void) => () => void;
113
113
  addEventListenerOnMount: <E extends Events>(event: E, callback: (event: EventPayload[E]) => void) => void;
114
114
  }
115
+ /**
116
+ * The nearest `<Theme>`'s state: color scheme, design tokens, breakpoints and the methods that
117
+ * change them. Call it during component initialisation, like any Svelte context read.
118
+ */
115
119
  export declare const useTheme: () => ThemeState;
116
120
  /** Resolve a control color against Theme `defaultColor`. Call inside `$derived`. */
117
121
  export declare const useDefaultColor: (color?: Colors) => Colors;
@@ -193,6 +193,10 @@ export class ThemeState extends createBindableStateClass() {
193
193
  });
194
194
  };
195
195
  }
196
+ /**
197
+ * The nearest `<Theme>`'s state: color scheme, design tokens, breakpoints and the methods that
198
+ * change them. Call it during component initialisation, like any Svelte context read.
199
+ */
196
200
  export const useTheme = () => {
197
201
  const theme = getContext('entasisTheme');
198
202
  // A missing provider used to surface as `undefined is not an object` from whichever call site
@@ -76,8 +76,6 @@
76
76
  {#if typeof trigger === 'function'}
77
77
  {@render trigger(attach)}
78
78
  {:else}
79
- {@const { content: triggerContent, ...buttonProps } = trigger}
80
- <Button {...buttonProps} {@attach attach}>
81
- {triggerContent}
82
- </Button>
79
+ {@const { content: triggerContent, children, ...buttonProps } = trigger}
80
+ <Button {...buttonProps} children={children ?? triggerContent} {@attach attach} />
83
81
  {/if}
@@ -1,6 +1,7 @@
1
1
  import { useTheme } from '../Theme/theme.state.svelte.js';
2
2
  import { useHoverAction } from '../../utils/useHoverAction.svelte.js';
3
3
  import { on } from 'svelte/events';
4
+ const FOCUSABLE = 'a[href], button, input, select, textarea, summary, [tabindex], [contenteditable]:not([contenteditable="false"])';
4
5
  export const tooltip = (props) => {
5
6
  const theme = useTheme();
6
7
  let refElement = null;
@@ -37,6 +38,10 @@ export const tooltip = (props) => {
37
38
  };
38
39
  return (ref) => {
39
40
  refElement = ref;
41
+ // Keyboard users reach the tooltip by focusing its trigger, so an element the browser
42
+ // cannot focus (an icon, a badge, a truncated label) joins the tab order.
43
+ if (!ref.matches(FOCUSABLE))
44
+ ref.tabIndex = 0;
40
45
  const off = hoverAction.reference?.(ref);
41
46
  // Keyboard users get the tooltip on focus; screen readers get it via aria-describedby.
42
47
  const offFocus = on(ref, 'focusin', () => show(ref));
@@ -1 +1 @@
1
- export declare const tooltipDescription = "\n# Tooltip\n\nContextual information shown on hover or keyboard focus. Two forms share one surface:\n\n- `<Tooltip>` \u2014 a component with a `trigger` prop, like every other overlay.\n- `tooltip()` \u2014 the underlying attachment, for elements you already render yourself.\n\nBoth are rendered by the single tooltip surface that `<Theme>` mounts (`TooltipHost`).\n\n## Basic Usage\n\n```svelte\n<script lang=\"ts\">\n\timport { Tooltip } from 'entasis/tooltip';\n</script>\n\n<Tooltip content=\"Click to submit\" trigger={{ content: 'Submit', variant: 'outline' }} />\n```\n\n## Props\n\n- **content**: string | Snippet (required) - Tooltip body\n- **trigger**: Snippet<[Attachment<HTMLElement>]> | ButtonProps & { content?: string } (required) -\n A snippet receives the tooltip attachment and spreads it on its own element; Button props render\n a Button carrying it\n- **open**: boolean (default: false) - Shows the tooltip without hover or focus; bindable\n- **defaultOpen**: boolean (default: false) - Initial open state when `open` is not provided\n- **onOpenChange**: (open: boolean) => void - Called whenever the tooltip becomes visible or hidden,\n hover and focus included\n- **position**: Placement (default: 'top') - Tooltip position relative to the trigger\n - Options: 'top' | 'bottom' | 'left' | 'right' | 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end' | 'left-start' | 'left-end' | 'right-start' | 'right-end'\n- **size**: 'small' | 'normal' | 'large' (default: 'normal') - Visual size\n- **color**: Colors (default: 'neutral') - Color theme\n- **variant**: 'solid' | 'outline' | 'soft' (default: 'solid') - Visual style matching Chip\n- **delay**: number (default: 400) - Delay in ms before showing tooltip; zero shows immediately\n- **offset**: number - Distance from the trigger in pixels\n- **class**: string - Additional CSS classes\n- **transition**: FSOProps - Custom transition configuration\n- **theme**: TooltipThemeProps - Per-instance theme overrides\n- **onAfterOpen**: () => void - Callback after the opening transition completes\n- **onAfterClose**: () => void - Callback after the closing transition completes\n\nEvery prop except `trigger`, `open`, `defaultOpen` and `onOpenChange` is also an option of the\n`tooltip()` attachment.\n\n## Examples\n\n### Button Trigger\n```svelte\n<script lang=\"ts\">\n\timport { Tooltip } from 'entasis/tooltip';\n</script>\n\n<Tooltip\n\tcontent=\"This is helpful information\"\n\ttrigger={{ content: 'Hover me', variant: 'outline', color: 'neutral' }}\n/>\n```\n\n### Snippet Trigger\n```svelte\n<script lang=\"ts\">\n\timport type { Attachment } from 'svelte/attachments';\n\timport { Tooltip } from 'entasis/tooltip';\n</script>\n\n{#snippet helpTrigger(attach: Attachment<HTMLElement>)}\n\t<span class=\"underline\" {@attach attach}>What is this?</span>\n{/snippet}\n\n<Tooltip content=\"Anchored to any element you like\" trigger={helpTrigger} />\n```\n\n### Forced Open\n```svelte\n<script lang=\"ts\">\n\timport { Tooltip } from 'entasis/tooltip';\n</script>\n\n<!-- Useful for docs, screenshots and visual tests -->\n<Tooltip open content=\"Always visible\" trigger={{ content: 'Anchor', variant: 'outline' }} />\n```\n\n### Different Positions\n```svelte\n<script lang=\"ts\">\n\timport { Tooltip } from 'entasis/tooltip';\n</script>\n\n<Tooltip content=\"Top tooltip\" position=\"top\" trigger={{ content: 'Top' }} />\n<Tooltip content=\"Bottom tooltip\" position=\"bottom\" trigger={{ content: 'Bottom' }} />\n<Tooltip content=\"Left tooltip\" position=\"left\" trigger={{ content: 'Left' }} />\n<Tooltip content=\"Right tooltip\" position=\"right\" trigger={{ content: 'Right' }} />\n```\n\n### Different Colors\n```svelte\n<script lang=\"ts\">\n\timport { Tooltip } from 'entasis/tooltip';\n</script>\n\n<Tooltip content=\"Success!\" color=\"success\" trigger={{ content: 'Success' }} />\n<Tooltip content=\"Warning!\" color=\"warning\" trigger={{ content: 'Warning' }} />\n<Tooltip content=\"Error!\" color=\"danger\" trigger={{ content: 'Error' }} />\n```\n\n### Custom Delay\n```svelte\n<script lang=\"ts\">\n\timport { Tooltip } from 'entasis/tooltip';\n</script>\n\n<Tooltip content=\"Quick tooltip\" delay={100} trigger={{ content: 'Quick (100ms)' }} />\n<Tooltip content=\"Slow tooltip\" delay={1000} trigger={{ content: 'Slow (1000ms)' }} />\n```\n\n### Different Sizes and Variants\n```svelte\n<script lang=\"ts\">\n\timport { Tooltip } from 'entasis/tooltip';\n</script>\n\n<Tooltip content=\"Small tooltip\" size=\"small\" trigger={{ content: 'Small' }} />\n<Tooltip content=\"Large tooltip\" size=\"large\" trigger={{ content: 'Large' }} />\n<Tooltip content=\"Outlined tooltip\" variant=\"outline\" trigger={{ content: 'Outline' }} />\n<Tooltip content=\"Soft tooltip\" variant=\"soft\" trigger={{ content: 'Soft' }} />\n```\n\n### With Snippet Content\n```svelte\n<script lang=\"ts\">\n\timport { Tooltip } from 'entasis/tooltip';\n</script>\n\n{#snippet richContent()}\n\t<div class=\"p-2\">\n\t\t<strong>Pro Tip</strong>\n\t\t<p class=\"text-sm\">Use Ctrl+S to save</p>\n\t</div>\n{/snippet}\n\n<Tooltip content={richContent} trigger={{ content: 'Keyboard Shortcuts' }} />\n```\n\n### With Callbacks\n```svelte\n<script lang=\"ts\">\n\timport { Tooltip } from 'entasis/tooltip';\n</script>\n\n<Tooltip\n\tcontent=\"Tracked tooltip\"\n\tonAfterOpen={() => console.log('Tooltip opened')}\n\tonAfterClose={() => console.log('Tooltip closed')}\n\ttrigger={{ content: 'Track me' }}\n/>\n```\n\n## The tooltip() attachment\n\nUse the attachment when the element already exists in your markup \u2014 icons, table cells, list rows,\ndisabled wrappers \u2014 or inside another component's internals.\n\n```svelte\n<script lang=\"ts\">\n\timport { tooltip } from 'entasis/tooltip';\n</script>\n\n<button {@attach tooltip({ content: 'Click to submit' })}> Submit </button>\n\n<!-- On icons or any non-interactive element -->\n<span {@attach tooltip({ content: 'More information', position: 'right' })}> \u24D8 </span>\n\n<!-- Disabled elements do not fire events, so wrap them -->\n<span {@attach tooltip({ content: 'Feature coming soon' })}>\n\t<button disabled>Disabled Button</button>\n</span>\n```\n\nThe `<Tooltip>` component hands this same attachment to a snippet trigger, so the two forms are\ninterchangeable.\n\n## Accessibility\n\n- Shows on pointer hover and on keyboard focus (`focusin` / `focusout` on the trigger element), so\n attach it to focusable elements for keyboard users\n- Dismissed on mouse leave or blur\n- Non-interactive (cannot be clicked)\n- Renders with `role=\"tooltip\"` and sets `aria-describedby` on the trigger while visible (any previous value is restored on hide)\n- Does not block content behind it\n\n## Notes\n\n- Only one tooltip shows at a time; an `open` tooltip hands the surface over when another tooltip is\n hovered and reports that through `onOpenChange`\n- Automatically positions to stay in viewport using Floating UI\n- Uses smart delay: subsequent tooltips show instantly if within 400ms of previous\n- Brief content only (use Popover for interactive content)\n- The surface is a singleton (`TooltipHost`) rendered by `<Theme>`\n- Does not lock scroll or trap focus\n\n## Theme Customization\n\nThe Tooltip uses a theme object that can be customized using the `theme` prop or by setting a global theme.\n\n### Theme Structure\n\nThe theme object contains the following parts:\n- **root**: Main tooltip container styles\n\n### Theme Type Definition\n\n```typescript\nimport type { TooltipThemeProps } from 'entasis/tooltip';\n\n// Example theme customization\nconst customTheme: TooltipThemeProps = {\n root: {\n base: 'inline-flex w-fit items-center rounded-full border font-medium',\n size: {\n small: 'h-5 px-2 text-xs',\n normal: 'h-6 px-2.5 text-xs',\n large: 'h-7 px-3 text-sm'\n },\n\tcolor: {\n\t neutral: 'bg-neutral text-neutral-contrast',\n primary: 'bg-primary text-primary-contrast',\n danger: 'bg-danger text-danger-contrast',\n success: 'bg-success text-success-contrast',\n warning: 'bg-warning text-warning-contrast',\n\t info: 'bg-info text-info-contrast'\n\t},\n\tvariant: {\n\t solid: 'bg-color text-color-contrast',\n\t outline: 'border-color bg-transparent text-color-readable',\n\t soft: 'bg-color-muted text-color-muted-readable'\n }\n }\n};\n```\n\n### Available Variants\n\n**root**:\n- base: Base classes applied to all tooltips\n- Variants:\n - size: 'small' | 'normal' | 'large' - Controls text size and padding\n - color: 'primary' | 'secondary' | 'neutral' | 'danger' | 'success' | 'warning' | 'info' - Color scheme\n - variant: 'solid' | 'outline' | 'soft' - Matches Chip's visual variants\n\n### Usage Examples\n\n**Basic Theme Override**:\n```svelte\n<script lang=\"ts\">\n\timport { Tooltip } from 'entasis/tooltip';\n</script>\n\n<Tooltip\n\tcontent=\"Custom tooltip\"\n\ttheme={{ root: { base: 'rounded-lg lift-4 border-2', size: { normal: 'px-3 py-2 text-sm' } } }}\n\ttrigger={{ content: 'Hover me' }}\n/>\n```\n\n**Color Customization**:\n```svelte\n<script lang=\"ts\">\n\timport { tooltip } from 'entasis/tooltip';\n</script>\n\n<button\n\t{@attach tooltip({\n\t\tcontent: 'Success!',\n\t\tcolor: 'success',\n\t\ttheme: { root: { color: { success: 'bg-green-500 text-white lift-3' } } }\n\t})}\n>\n\tSuccess Tooltip\n</button>\n```\n\n**Global Theme Setting**:\n```svelte\n<script lang=\"ts\">\n\timport { setTooltipTheme } from 'entasis/tooltip';\n\n\tsetTooltipTheme({\n\t\troot: {\n\t\t\tbase: 'rounded-md lift-4 backdrop-blur-sm',\n\t\t\tsize: { normal: 'px-3 py-1.5 text-sm' },\n\t\t\tcolor: { neutral: 'bg-gray-900 text-white', primary: 'bg-blue-500 text-white' }\n\t\t}\n\t});\n</script>\n```\n\n## Motion\n\n- **motion** theme slot: one preset (no variants) \u2014 a short fade-and-rise, `duration: 'fast'`.\n- Resolved by the tooltip surface and handed to the underlying Popover, so it replaces the\n popover preset.\n- Ladder: `<Theme components={{ tooltip: { motion } }}>` \u2192 `setTooltipTheme({ motion })` \u2192\n `theme.motion` \u2192 the tooltip's `transition` option. Reduced motion collapses it to 0.\n- The tooltip surface is a singleton rendered by `<Theme>`, so a `setTooltipTheme` call\n made *below* `<Theme>` never reaches it. Call it at or above the `<Theme>` boundary, or\n use the `<Theme components={{ tooltip }}>` registry, which always applies.\n";
1
+ export declare const tooltipDescription = "\n# Tooltip\n\nContextual information shown on hover or keyboard focus. Two forms share one surface:\n\n- `<Tooltip>` \u2014 a component with a `trigger` prop, like every other overlay.\n- `tooltip()` \u2014 the underlying attachment, for elements you already render yourself.\n\nBoth are rendered by the single tooltip surface that `<Theme>` mounts (`TooltipHost`).\n\n## Basic Usage\n\n```svelte\n<script lang=\"ts\">\n\timport { Tooltip } from 'entasis/tooltip';\n</script>\n\n<Tooltip content=\"Click to submit\" trigger={{ content: 'Submit', variant: 'outline' }} />\n```\n\n## Props\n\n- **content**: string | Snippet (required) - Tooltip body\n- **trigger**: Snippet<[Attachment<HTMLElement>]> | ButtonProps & { content?: string } (required) -\n A snippet receives the tooltip attachment and spreads it on its own element; an element the\n browser cannot focus (a span, an icon) gets `tabindex=\"0\"` so keyboard focus shows the tooltip.\n Button props render a Button carrying it; `children` (a string or a snippet) replaces\n `content` for a richer body\n- **open**: boolean (default: false) - Shows the tooltip without hover or focus; bindable\n- **defaultOpen**: boolean (default: false) - Initial open state when `open` is not provided\n- **onOpenChange**: (open: boolean) => void - Called whenever the tooltip becomes visible or hidden,\n hover and focus included\n- **position**: Placement (default: 'top') - Tooltip position relative to the trigger\n - Options: 'top' | 'bottom' | 'left' | 'right' | 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end' | 'left-start' | 'left-end' | 'right-start' | 'right-end'\n- **size**: 'small' | 'normal' | 'large' (default: 'normal') - Visual size\n- **color**: Colors (default: 'neutral') - Color theme\n- **variant**: 'solid' | 'outline' | 'soft' (default: 'solid') - Visual style matching Chip\n- **delay**: number (default: 400) - Delay in ms before showing tooltip; zero shows immediately\n- **offset**: number - Distance from the trigger in pixels\n- **class**: string - Additional CSS classes\n- **transition**: FSOProps - Custom transition configuration\n- **theme**: TooltipThemeProps - Per-instance theme overrides\n- **onAfterOpen**: () => void - Callback after the opening transition completes\n- **onAfterClose**: () => void - Callback after the closing transition completes\n\nEvery prop except `trigger`, `open`, `defaultOpen` and `onOpenChange` is also an option of the\n`tooltip()` attachment.\n\n## Examples\n\n### Button Trigger\n```svelte\n<script lang=\"ts\">\n\timport { Tooltip } from 'entasis/tooltip';\n</script>\n\n<Tooltip\n\tcontent=\"This is helpful information\"\n\ttrigger={{ content: 'Hover me', variant: 'outline', color: 'neutral' }}\n/>\n```\n\n### Snippet Trigger\n```svelte\n<script lang=\"ts\">\n\timport type { Attachment } from 'svelte/attachments';\n\timport { Tooltip } from 'entasis/tooltip';\n</script>\n\n{#snippet helpTrigger(attach: Attachment<HTMLElement>)}\n\t<span class=\"underline\" {@attach attach}>What is this?</span>\n{/snippet}\n\n<Tooltip content=\"Anchored to any element you like\" trigger={helpTrigger} />\n```\n\n### Forced Open\n```svelte\n<script lang=\"ts\">\n\timport { Tooltip } from 'entasis/tooltip';\n</script>\n\n<!-- Useful for docs, screenshots and visual tests -->\n<Tooltip open content=\"Always visible\" trigger={{ content: 'Anchor', variant: 'outline' }} />\n```\n\n### Different Positions\n```svelte\n<script lang=\"ts\">\n\timport { Tooltip } from 'entasis/tooltip';\n</script>\n\n<Tooltip content=\"Top tooltip\" position=\"top\" trigger={{ content: 'Top' }} />\n<Tooltip content=\"Bottom tooltip\" position=\"bottom\" trigger={{ content: 'Bottom' }} />\n<Tooltip content=\"Left tooltip\" position=\"left\" trigger={{ content: 'Left' }} />\n<Tooltip content=\"Right tooltip\" position=\"right\" trigger={{ content: 'Right' }} />\n```\n\n### Different Colors\n```svelte\n<script lang=\"ts\">\n\timport { Tooltip } from 'entasis/tooltip';\n</script>\n\n<Tooltip content=\"Success!\" color=\"success\" trigger={{ content: 'Success' }} />\n<Tooltip content=\"Warning!\" color=\"warning\" trigger={{ content: 'Warning' }} />\n<Tooltip content=\"Error!\" color=\"danger\" trigger={{ content: 'Error' }} />\n```\n\n### Custom Delay\n```svelte\n<script lang=\"ts\">\n\timport { Tooltip } from 'entasis/tooltip';\n</script>\n\n<Tooltip content=\"Quick tooltip\" delay={100} trigger={{ content: 'Quick (100ms)' }} />\n<Tooltip content=\"Slow tooltip\" delay={1000} trigger={{ content: 'Slow (1000ms)' }} />\n```\n\n### Different Sizes and Variants\n```svelte\n<script lang=\"ts\">\n\timport { Tooltip } from 'entasis/tooltip';\n</script>\n\n<Tooltip content=\"Small tooltip\" size=\"small\" trigger={{ content: 'Small' }} />\n<Tooltip content=\"Large tooltip\" size=\"large\" trigger={{ content: 'Large' }} />\n<Tooltip content=\"Outlined tooltip\" variant=\"outline\" trigger={{ content: 'Outline' }} />\n<Tooltip content=\"Soft tooltip\" variant=\"soft\" trigger={{ content: 'Soft' }} />\n```\n\n### With Snippet Content\n```svelte\n<script lang=\"ts\">\n\timport { Tooltip } from 'entasis/tooltip';\n</script>\n\n{#snippet richContent()}\n\t<div class=\"p-2\">\n\t\t<strong>Pro Tip</strong>\n\t\t<p class=\"text-sm\">Use Ctrl+S to save</p>\n\t</div>\n{/snippet}\n\n<Tooltip content={richContent} trigger={{ content: 'Keyboard Shortcuts' }} />\n```\n\n### With Callbacks\n```svelte\n<script lang=\"ts\">\n\timport { Tooltip } from 'entasis/tooltip';\n</script>\n\n<Tooltip\n\tcontent=\"Tracked tooltip\"\n\tonAfterOpen={() => console.log('Tooltip opened')}\n\tonAfterClose={() => console.log('Tooltip closed')}\n\ttrigger={{ content: 'Track me' }}\n/>\n```\n\n## The tooltip() attachment\n\nUse the attachment when the element already exists in your markup \u2014 icons, table cells, list rows,\ndisabled wrappers \u2014 or inside another component's internals.\n\n```svelte\n<script lang=\"ts\">\n\timport { tooltip } from 'entasis/tooltip';\n</script>\n\n<button {@attach tooltip({ content: 'Click to submit' })}> Submit </button>\n\n<!-- On icons or any non-interactive element -->\n<span {@attach tooltip({ content: 'More information', position: 'right' })}> \u24D8 </span>\n\n<!-- Disabled elements do not fire events, so wrap them -->\n<span {@attach tooltip({ content: 'Feature coming soon' })}>\n\t<button disabled>Disabled Button</button>\n</span>\n```\n\nThe `<Tooltip>` component hands this same attachment to a snippet trigger, so the two forms are\ninterchangeable.\n\n## Accessibility\n\n- Shows on pointer hover and on keyboard focus (`focusin` / `focusout` on the trigger element), so\n attach it to focusable elements for keyboard users\n- Dismissed on mouse leave or blur\n- Non-interactive (cannot be clicked)\n- Renders with `role=\"tooltip\"` and sets `aria-describedby` on the trigger while visible (any previous value is restored on hide)\n- Does not block content behind it\n\n## Notes\n\n- Only one tooltip shows at a time; an `open` tooltip hands the surface over when another tooltip is\n hovered and reports that through `onOpenChange`\n- Automatically positions to stay in viewport using Floating UI\n- Uses smart delay: subsequent tooltips show instantly if within 400ms of previous\n- Brief content only (use Popover for interactive content)\n- The surface is a singleton (`TooltipHost`) rendered by `<Theme>`\n- Does not lock scroll or trap focus\n\n## Theme Customization\n\nThe Tooltip uses a theme object that can be customized using the `theme` prop or by setting a global theme.\n\n### Theme Structure\n\nThe theme object contains the following parts:\n- **root**: Main tooltip container styles\n\n### Theme Type Definition\n\n```typescript\nimport type { TooltipThemeProps } from 'entasis/tooltip';\n\n// Example theme customization\nconst customTheme: TooltipThemeProps = {\n root: {\n base: 'inline-flex w-fit items-center rounded-full border font-medium',\n size: {\n small: 'h-5 px-2 text-xs',\n normal: 'h-6 px-2.5 text-xs',\n large: 'h-7 px-3 text-sm'\n },\n\tcolor: {\n\t neutral: 'bg-neutral text-neutral-contrast',\n primary: 'bg-primary text-primary-contrast',\n danger: 'bg-danger text-danger-contrast',\n success: 'bg-success text-success-contrast',\n warning: 'bg-warning text-warning-contrast',\n\t info: 'bg-info text-info-contrast'\n\t},\n\tvariant: {\n\t solid: 'bg-color text-color-contrast',\n\t outline: 'border-color bg-transparent text-color-readable',\n\t soft: 'bg-color-muted text-color-muted-readable'\n }\n }\n};\n```\n\n### Available Variants\n\n**root**:\n- base: Base classes applied to all tooltips\n- Variants:\n - size: 'small' | 'normal' | 'large' - Controls text size and padding\n - color: 'primary' | 'secondary' | 'neutral' | 'danger' | 'success' | 'warning' | 'info' - Color scheme\n - variant: 'solid' | 'outline' | 'soft' - Matches Chip's visual variants\n\n### Usage Examples\n\n**Basic Theme Override**:\n```svelte\n<script lang=\"ts\">\n\timport { Tooltip } from 'entasis/tooltip';\n</script>\n\n<Tooltip\n\tcontent=\"Custom tooltip\"\n\ttheme={{ root: { base: 'rounded-lg lift-4 border-2', size: { normal: 'px-3 py-2 text-sm' } } }}\n\ttrigger={{ content: 'Hover me' }}\n/>\n```\n\n**Color Customization**:\n```svelte\n<script lang=\"ts\">\n\timport { tooltip } from 'entasis/tooltip';\n</script>\n\n<button\n\t{@attach tooltip({\n\t\tcontent: 'Success!',\n\t\tcolor: 'success',\n\t\ttheme: { root: { color: { success: 'bg-green-500 text-white lift-3' } } }\n\t})}\n>\n\tSuccess Tooltip\n</button>\n```\n\n**Global Theme Setting**:\n```svelte\n<script lang=\"ts\">\n\timport { setTooltipTheme } from 'entasis/tooltip';\n\n\tsetTooltipTheme({\n\t\troot: {\n\t\t\tbase: 'rounded-md lift-4 backdrop-blur-sm',\n\t\t\tsize: { normal: 'px-3 py-1.5 text-sm' },\n\t\t\tcolor: { neutral: 'bg-gray-900 text-white', primary: 'bg-blue-500 text-white' }\n\t\t}\n\t});\n</script>\n```\n\n## Motion\n\n- **motion** theme slot: one preset (no variants) \u2014 a short fade-and-rise, `duration: 'fast'`.\n- Resolved by the tooltip surface and handed to the underlying Popover, so it replaces the\n popover preset.\n- Ladder: `<Theme components={{ tooltip: { motion } }}>` \u2192 `setTooltipTheme({ motion })` \u2192\n `theme.motion` \u2192 the tooltip's `transition` option. Reduced motion collapses it to 0.\n- The tooltip surface is a singleton rendered by `<Theme>`, so a `setTooltipTheme` call\n made *below* `<Theme>` never reaches it. Call it at or above the `<Theme>` boundary, or\n use the `<Theme components={{ tooltip }}>` registry, which always applies.\n";
@@ -22,8 +22,10 @@ Both are rendered by the single tooltip surface that \`<Theme>\` mounts (\`Toolt
22
22
 
23
23
  - **content**: string | Snippet (required) - Tooltip body
24
24
  - **trigger**: Snippet<[Attachment<HTMLElement>]> | ButtonProps & { content?: string } (required) -
25
- A snippet receives the tooltip attachment and spreads it on its own element; Button props render
26
- a Button carrying it
25
+ A snippet receives the tooltip attachment and spreads it on its own element; an element the
26
+ browser cannot focus (a span, an icon) gets \`tabindex="0"\` so keyboard focus shows the tooltip.
27
+ Button props render a Button carrying it; \`children\` (a string or a snippet) replaces
28
+ \`content\` for a richer body
27
29
  - **open**: boolean (default: false) - Shows the tooltip without hover or focus; bindable
28
30
  - **defaultOpen**: boolean (default: false) - Initial open state when \`open\` is not provided
29
31
  - **onOpenChange**: (open: boolean) => void - Called whenever the tooltip becomes visible or hidden,
@@ -131,6 +131,7 @@ export const componentAliases: {
131
131
  'entasis/theme': string;
132
132
  'entasis/i18n': string;
133
133
  'entasis/tailwind-plugin': string;
134
+ 'entasis/color-palette': string;
134
135
  'entasis/types': string;
135
136
  'entasis/cva': string;
136
137
  'entasis/motion': string;
@@ -132,6 +132,7 @@ export const componentAliases = {
132
132
  'entasis/theme': './src/lib/components/Theme/index.ts',
133
133
  'entasis/i18n': './src/lib/i18n/index.ts',
134
134
  'entasis/tailwind-plugin': './src/lib/tailwind/index.ts',
135
+ 'entasis/color-palette': './src/lib/tailwind/palette.ts',
135
136
  'entasis/types': './src/lib/types/index.ts',
136
137
  'entasis/cva': './src/lib/utils/cva/index.ts',
137
138
  'entasis/motion': './src/lib/utils/motion/index.ts',