entasis 0.10.0 → 0.10.2

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.
@@ -52,12 +52,14 @@ const defaultAvatarSuffix = cva({
52
52
  size: 'normal'
53
53
  }
54
54
  });
55
+ // Small initials sit on the type scale's smallest step, so they tighten their tracking instead:
56
+ // two bold capitals then fill the 24px circle in the same proportion the larger sizes do.
55
57
  const defaultAvatarInitials = cva({
56
58
  base: 'absolute bg-surface-floating bottom-0 w-full h-full text-center left-0 rounded-full flex items-center justify-center uppercase font-bold text-sm',
57
59
  variants: {
58
60
  size: {
59
61
  normal: 'text-sm',
60
- small: 'text-xs',
62
+ small: 'text-xs tracking-tight',
61
63
  large: 'text-base'
62
64
  }
63
65
  },
@@ -1,11 +1,14 @@
1
1
  import { cva } from '../../utils/cva/index.js';
2
2
  import { setComponentTheme, useComponentTheme } from '../../utils/cva/index.js';
3
+ // Each size overlaps its neighbour by about a fifth of an avatar (7.2/32, 4.8/24, 8/40px), so a
4
+ // stack reads the same at every size. Small used to overlap 0.4rem — over a quarter of its 24px —
5
+ // which slid each circle across the end of the previous avatar's two-letter initials.
3
6
  const defaultAvatarGroup = cva({
4
7
  base: 'flex gap-[-0.25rem] items-center isolate ml-[0.3rem]',
5
8
  variants: {
6
9
  size: {
7
10
  normal: '[&>[data-avatar]]:ml-[-0.45rem]',
8
- small: '[&>[data-avatar]]:ml-[-0.4rem]',
11
+ small: '[&>[data-avatar]]:ml-[-0.3rem]',
9
12
  large: '[&>[data-avatar]]:ml-[-0.5rem]'
10
13
  }
11
14
  },
@@ -49,7 +49,8 @@ export type SelectAlignment = {
49
49
  * Item-aligned placement, the native select's and Radix's: the panel covers the trigger with the
50
50
  * option's middle on the trigger's middle and its text on the value text. A panel that would cross
51
51
  * the top edge is pinned there and its list scrolled by the overflow, so the option stays on the
52
- * trigger; one that would cross the bottom edge is cut short and scrolls. Only when the trigger
53
- * sits so low that fewer than four rows would fit does the panel rise and give up the alignment.
52
+ * trigger; one that would cross the bottom edge is cut short and scrolls. When either leaves fewer
53
+ * than four rows, the panel gives up the alignment and grows to four: up from the bottom edge for a
54
+ * trigger sitting low, down from the top edge for a short list pinned there.
54
55
  */
55
56
  export declare function alignItemWithTrigger(m: SelectAlignMetrics): SelectAlignment;
@@ -5,8 +5,9 @@ const clamp = (value, min, max) => Math.min(Math.max(value, min), Math.max(min,
5
5
  * Item-aligned placement, the native select's and Radix's: the panel covers the trigger with the
6
6
  * option's middle on the trigger's middle and its text on the value text. A panel that would cross
7
7
  * the top edge is pinned there and its list scrolled by the overflow, so the option stays on the
8
- * trigger; one that would cross the bottom edge is cut short and scrolls. Only when the trigger
9
- * sits so low that fewer than four rows would fit does the panel rise and give up the alignment.
8
+ * trigger; one that would cross the bottom edge is cut short and scrolls. When either leaves fewer
9
+ * than four rows, the panel gives up the alignment and grows to four: up from the bottom edge for a
10
+ * trigger sitting low, down from the top edge for a short list pinned there.
10
11
  */
11
12
  export function alignItemWithTrigger(m) {
12
13
  const top = SELECT_ALIGN_MARGIN;
@@ -22,10 +23,16 @@ export function alignItemWithTrigger(m) {
22
23
  y = top;
23
24
  }
24
25
  let height = Math.min(m.panel.height - scrollTop, bottom - y);
25
- const minHeight = Math.min(m.panel.height, chromeAbove + chromeBelow + m.item.height * 4);
26
+ const fourRows = chromeAbove + chromeBelow + m.item.height * 4;
27
+ // A list less than a row taller than four rows (its own padding) opens whole: cutting it would
28
+ // scroll it a few pixels and show a scroll arrow for nothing.
29
+ const minHeight = m.panel.height - fourRows < m.item.height ? m.panel.height : fourRows;
26
30
  if (height < minHeight) {
31
+ // Grow away from the edge that is not in the way. A panel cut by the bottom edge rises; one
32
+ // pinned at the top (the last options of a short list under a high trigger, where the pin's
33
+ // scroll leaves too few rows) grows down and scrolls back instead of dropping to the bottom.
27
34
  height = minHeight;
28
- y = Math.max(top, bottom - height);
35
+ y = Math.max(top, Math.min(y, bottom - height));
29
36
  }
30
37
  const listHeight = Math.max(0, height - chromeAbove - chromeBelow);
31
38
  scrollTop = clamp(scrollTop, 0, m.list.height - listHeight);
@@ -33,11 +33,6 @@
33
33
  return `${Math.max(0.1, speed)}s`;
34
34
  });
35
35
 
36
- const gap = $derived.by(() => {
37
- if (size === 'small') return '0.5rem';
38
- if (size === 'large') return '1.5rem';
39
- return '1rem';
40
- });
41
36
  const configuredCopyCount = $derived.by(() => {
42
37
  if (!Number.isFinite(numberOfCopies)) return 2;
43
38
  return Math.min(MAX_COPY_COUNT, Math.max(2, Math.floor(numberOfCopies)));
@@ -80,7 +75,7 @@
80
75
  data-reverse={reverse ? 'true' : undefined}
81
76
  data-fade={fade ? 'true' : undefined}
82
77
  class={classes.root({ direction, size, fade, class: className })}
83
- style="--animation-duration: {animationDuration}; --gap: {gap};"
78
+ style="--animation-duration: {animationDuration};"
84
79
  {@attach observeRoot}
85
80
  {...attachments}
86
81
  >
@@ -144,4 +139,11 @@
144
139
  :global(html[data-entasis-reduce-motion] .animate-marquee-up) {
145
140
  animation: none;
146
141
  }
142
+
143
+ /* A still marquee starts at its first item, so only its trailing edge, where the row is cut,
144
+ keeps the fade; a faded leading edge would half-hide the first item. */
145
+ :global(html[data-entasis-reduce-motion]) div[data-slot='marquee'][data-fade] {
146
+ --scroll-fade-s: 0px;
147
+ --scroll-fade-t: 0px;
148
+ }
147
149
  </style>
@@ -1 +1 @@
1
- export declare const marqueeDescription = "\n# Marquee Component\n\nThe Marquee component creates an infinite scrolling animation for displaying content. It automatically duplicates the slot content to create a seamless infinite loop effect and supports customizable speed, direction, fade effects, and pause-on-hover functionality.\n\n## Basic Usage\n\n```svelte\n<script>\n\timport { Marquee } from 'entasis/marquee';\n\t\n\tconst items = [\n\t\t{ id: 1, text: 'Item 1' },\n\t\t{ id: 2, text: 'Item 2' },\n\t\t{ id: 3, text: 'Item 3' }\n\t];\n</script>\n\n<Marquee>\n\t{#each items as item}\n\t\t<div class=\"px-4 py-2 bg-surface rounded\">\n\t\t\t{item.text}\n\t\t</div>\n\t{/each}\n</Marquee>\n```\n\n## Props\n\n### Core Props\n- **children**: Snippet (required)\n - Content to display in the marquee\n - Parent component handles iteration over items\n - Content is automatically duplicated based on `numberOfCopies`\n\n- **direction**: 'left' | 'up' (default: 'left')\n - `'left'`: Horizontal scrolling from right to left\n - `'up'`: Vertical scrolling from bottom to top\n\n- **numberOfCopies**: number (default: 2)\n - Minimum number of times to duplicate the content for a seamless loop\n - The component measures the viewport and first copy, then adds copies when the content is too short to cover the viewport continuously\n - The effective count is bounded to 100 copies\n\n### Animation Props\n- **speed**: 'fast' | 'normal' | 'slow' | number (default: 'fast')\n - Animation speed control\n - `'fast'`: 20 seconds per cycle\n - `'normal'`: 40 seconds per cycle\n - `'slow'`: 80 seconds per cycle\n - `number`: Custom duration in seconds (e.g., `30` for 30 seconds)\n\n- **reverse**: boolean (default: false)\n - If true, animation runs in reverse direction\n - For 'left': scrolls left to right\n - For 'up': scrolls top to bottom\n\n- **pauseOnHover**: boolean (default: true)\n - If true, animation pauses when mouse hovers over the marquee\n - If false, animation continues regardless of hover state\n\n### Visual Props\n- **fade**: boolean (default: true)\n - If true, applies the shared scroll-fade utility at the marquee edges\n - Uses a static scroll-fade mode because the marquee is clipped animation, not a native scroll container\n\n- **size**: 'small' | 'normal' | 'large' (default: 'normal')\n - Controls spacing between duplicated content copies\n - `small`: Compact spacing (gap-2)\n - `normal`: Standard spacing (gap-4)\n - `large`: Expanded spacing (gap-6)\n\n### Styling Props\n- **ref**: HTMLDivElement | null (bindable)\n - Reference to the marquee container\n\n- **class**: string\n - Additional CSS classes for the marquee container\n\n- **innerClass**: string\n - Additional CSS classes for each duplicated copy of the content\n\n- **theme**: MarqueeThemeProps\n - Custom theme overrides for advanced styling\n\n## Examples\n\n### Basic Horizontal Marquee\n\n```svelte\n<script>\n\timport { Marquee } from 'entasis/marquee';\n\t\n\tconst testimonials = [\n\t\t{ author: 'John Doe', quote: 'Amazing product!' },\n\t\t{ author: 'Jane Smith', quote: 'Highly recommended!' },\n\t\t{ author: 'Bob Johnson', quote: 'Best purchase ever!' }\n\t];\n</script>\n\n<Marquee>\n\t{#each testimonials as item}\n\t\t<div class=\"px-8 py-4 bg-surface rounded-lg\">\n\t\t\t<p class=\"text-lg font-semibold\">\"{item.quote}\"</p>\n\t\t\t<p class=\"text-sm text-muted\">\u2014 {item.author}</p>\n\t\t</div>\n\t{/each}\n</Marquee>\n```\n\n### Logo Carousel\n\n```svelte\n<script>\n\timport { Marquee } from 'entasis/marquee';\n\t\n\tconst logos = [\n\t\t{ name: 'Company 1', logo: '/logo1.png' },\n\t\t{ name: 'Company 2', logo: '/logo2.png' },\n\t\t{ name: 'Company 3', logo: '/logo3.png' }\n\t];\n</script>\n\n<Marquee speed=\"normal\" size=\"large\">\n\t{#each logos as logo}\n\t\t<img src={logo.logo} alt={logo.name} class=\"h-12 w-auto\" />\n\t{/each}\n</Marquee>\n```\n\n### Vertical Marquee\n\n```svelte\n<Marquee direction=\"up\" class=\"h-[400px]\">\n\t{#each items as item}\n\t\t<div class=\"px-6 py-4 bg-surface rounded\">\n\t\t\t{item.content}\n\t\t</div>\n\t{/each}\n</Marquee>\n```\n\n### With Fade Effect\n\n```svelte\n<Marquee fade={true}>\n\t{#each items as item}\n\t\t<div>{item.content}</div>\n\t{/each}\n</Marquee>\n```\n\n### Reverse Direction\n\n```svelte\n<Marquee reverse={true}>\n\t{#each items as item}\n\t\t<div>{item.content}</div>\n\t{/each}\n</Marquee>\n```\n\n### Custom Speed\n\n```svelte\n<Marquee speed={30}>\n\t{#each items as item}\n\t\t<div>{item.content}</div>\n\t{/each}\n</Marquee>\n```\n\n### Different Sizes\n\n```svelte\n<!-- Small -->\n<Marquee size=\"small\">\n\t{#each items as item}\n\t\t<div>{item.content}</div>\n\t{/each}\n</Marquee>\n\n<!-- Normal (default) -->\n<Marquee size=\"normal\">\n\t{#each items as item}\n\t\t<div>{item.content}</div>\n\t{/each}\n</Marquee>\n\n<!-- Large -->\n<Marquee size=\"large\">\n\t{#each items as item}\n\t\t<div>{item.content}</div>\n\t{/each}\n</Marquee>\n```\n\n### Custom Number of Copies\n\n```svelte\n<!-- More copies for smoother appearance -->\n<Marquee numberOfCopies={4} speed={30}>\n\t{#each items as item}\n\t\t<div>{item.content}</div>\n\t{/each}\n</Marquee>\n```\n\n### Disable Pause on Hover\n\n```svelte\n<Marquee pauseOnHover={false}>\n\t{#each items as item}\n\t\t<div>{item.content}</div>\n\t{/each}\n</Marquee>\n```\n\n### Combined Effects\n\n```svelte\n<Marquee \n\tfade={true}\n\treverse={true}\n\tspeed=\"slow\"\n\tsize=\"large\"\n>\n\t{#each items as item}\n\t\t<div class=\"px-8 py-6 bg-surface rounded-lg\">\n\t\t\t{item.content}\n\t\t</div>\n\t{/each}\n</Marquee>\n```\n\n## Structure\n\nThe Marquee component renders:\n- A container `<div>` with overflow hidden and optional scroll-fade utility classes\n- Multiple copies of the content (default 2) based on `numberOfCopies`\n- Each copy has the animation applied independently\n- Parent component iterates over items in the slot\n\n## Animation Details\n\n- Uses CSS keyframe animations for smooth GPU-accelerated performance\n- Content is duplicated `numberOfCopies` times to create seamless infinite scroll\n- Animation translates each copy by -100% of its width/height plus gap\n- Animation duration is controlled via CSS custom property `--animation-duration`\n- Pause on hover uses CSS `animation-play-state` for instant response\n- Reverse mode uses CSS `direction: reverse` for the animation\n\n## Accessibility\n\n- Uses `role=\"presentation\"` on the container as the marquee is decorative\n- Consider providing alternative static content for screen readers\n- Ensure sufficient color foreground for text content\n- Pause on hover helps users interact with and read content\n- Animation is disabled when the user requests reduced motion\n\n## Notes\n\n- Parent component is responsible for iterating over items (similar to Carousel pattern)\n- Content is automatically duplicated internally based on `numberOfCopies`\n- The component uses CSS animations for optimal performance\n- Custom speed values are specified in seconds (e.g., `30` = 30 seconds per cycle)\n- For vertical marquees, ensure the container has a defined height\n- Increasing `numberOfCopies` creates smoother appearance but uses more DOM elements\n- The fade effect uses the shared scroll-fade utility classes\n\n## Theme Customization\n\nThe Marquee component 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 marquee container styles\n- **inner**: Inner scrolling content container styles\n\n### Available Variants\n\n**root**:\n- base: Base classes for main container\n- Variants:\n - direction: 'left' | 'up' - Scroll direction\n - size: 'small' | 'normal' | 'large' - Gap between duplicated content\n - fade: boolean - Applies shared scroll-fade utility classes\n\n**inner**:\n- base: Base classes for scrolling content\n- Variants:\n - direction: 'left' | 'up' - Animation direction\n - size: 'small' | 'normal' | 'large' - Gap size\n - pauseOnHover: boolean - Pause animation on hover\n - reverse: boolean - Reverse animation direction\n\n### Usage Examples\n\n**Basic Theme Override**:\n```svelte\n<Marquee \n theme={{\n root: {\n base: 'group flex overflow-hidden relative',\n direction: {\n left: 'flex-row'\n },\n size: {\n normal: 'gap-4'\n }\n }\n }}\n>\n {#each items as item}\n <div>{item.content}</div>\n {/each}\n</Marquee>\n```\n\n**Custom Animation Styling**:\n```svelte\n<Marquee \n theme={{\n inner: {\n base: 'flex shrink-0 justify-around whitespace-nowrap',\n direction: {\n left: 'flex-row animate-marquee-left'\n },\n pauseOnHover: {\n true: 'group-hover:[animation-play-state:paused]'\n },\n size: {\n large: 'gap-6'\n }\n }\n }}\n>\n {#each items as item}\n <div>{item.content}</div>\n {/each}\n</Marquee>\n```\n\n**Global Theme Setting**:\n```svelte\n<script>\n import { setMarqueeTheme } from './index.ts';\n \n setMarqueeTheme({\n root: {\n base: 'group flex overflow-hidden relative',\n direction: {\n left: 'flex-row'\n },\n size: {\n normal: 'gap-4'\n }\n },\n inner: {\n base: 'flex shrink-0 justify-around whitespace-nowrap',\n direction: {\n left: 'flex-row animate-marquee-left'\n },\n pauseOnHover: {\n true: 'group-hover:[animation-play-state:paused]'\n }\n }\n });\n</script>\n```\n";
1
+ export declare const marqueeDescription = "\n# Marquee Component\n\nThe Marquee component creates an infinite scrolling animation for displaying content. It automatically duplicates the slot content to create a seamless infinite loop effect and supports customizable speed, direction, fade effects, and pause-on-hover functionality.\n\n## Basic Usage\n\n```svelte\n<script>\n\timport { Marquee } from 'entasis/marquee';\n\t\n\tconst items = [\n\t\t{ id: 1, text: 'Item 1' },\n\t\t{ id: 2, text: 'Item 2' },\n\t\t{ id: 3, text: 'Item 3' }\n\t];\n</script>\n\n<Marquee>\n\t{#each items as item}\n\t\t<div class=\"px-4 py-2 bg-surface rounded\">\n\t\t\t{item.text}\n\t\t</div>\n\t{/each}\n</Marquee>\n```\n\n## Props\n\n### Core Props\n- **children**: Snippet (required)\n - Content to display in the marquee\n - Parent component handles iteration over items\n - Content is automatically duplicated based on `numberOfCopies`\n\n- **direction**: 'left' | 'up' (default: 'left')\n - `'left'`: Horizontal scrolling from right to left\n - `'up'`: Vertical scrolling from bottom to top\n\n- **numberOfCopies**: number (default: 2)\n - Minimum number of times to duplicate the content for a seamless loop\n - The component measures the viewport and first copy, then adds copies when the content is too short to cover the viewport continuously\n - The effective count is bounded to 100 copies\n\n### Animation Props\n- **speed**: 'fast' | 'normal' | 'slow' | number (default: 'fast')\n - Animation speed control\n - `'fast'`: 20 seconds per cycle\n - `'normal'`: 40 seconds per cycle\n - `'slow'`: 80 seconds per cycle\n - `number`: Custom duration in seconds (e.g., `30` for 30 seconds)\n\n- **reverse**: boolean (default: false)\n - If true, animation runs in reverse direction\n - For 'left': scrolls left to right\n - For 'up': scrolls top to bottom\n\n- **pauseOnHover**: boolean (default: true)\n - If true, animation pauses when mouse hovers over the marquee\n - If false, animation continues regardless of hover state\n\n### Visual Props\n- **fade**: boolean (default: true)\n - If true, applies the shared scroll-fade utility at the marquee edges\n - Uses a static scroll-fade mode because the marquee is clipped animation, not a native scroll container\n\n- **size**: 'small' | 'normal' | 'large' (default: 'normal')\n - Sets the marquee's one spacing value, `--gap`, from the theme spacing scale\n - `small`: `--space-md`\n - `normal`: `--space-xl`\n - `large`: `--layout-space-md`\n - The copies, the items inside each copy and the loop step all use `--gap`, so the loop stays\n seamless at every size and theme spacing. To respace a marquee, set `--gap` once on the root\n (`class=\"[--gap:var(--space-lg)]\"`); never redeclare it on the copies.\n\n### Styling Props\n- **ref**: HTMLDivElement | null (bindable)\n - Reference to the marquee container\n\n- **class**: string\n - Additional CSS classes for the marquee container\n\n- **innerClass**: string\n - Additional CSS classes for each duplicated copy of the content\n\n- **theme**: MarqueeThemeProps\n - Custom theme overrides for advanced styling\n\n## Examples\n\n### Basic Horizontal Marquee\n\n```svelte\n<script>\n\timport { Marquee } from 'entasis/marquee';\n\t\n\tconst testimonials = [\n\t\t{ author: 'John Doe', quote: 'Amazing product!' },\n\t\t{ author: 'Jane Smith', quote: 'Highly recommended!' },\n\t\t{ author: 'Bob Johnson', quote: 'Best purchase ever!' }\n\t];\n</script>\n\n<Marquee>\n\t{#each testimonials as item}\n\t\t<div class=\"px-8 py-4 bg-surface rounded-lg\">\n\t\t\t<p class=\"text-lg font-semibold\">\"{item.quote}\"</p>\n\t\t\t<p class=\"text-sm text-muted\">\u2014 {item.author}</p>\n\t\t</div>\n\t{/each}\n</Marquee>\n```\n\n### Logo Carousel\n\n```svelte\n<script>\n\timport { Marquee } from 'entasis/marquee';\n\t\n\tconst logos = [\n\t\t{ name: 'Company 1', logo: '/logo1.png' },\n\t\t{ name: 'Company 2', logo: '/logo2.png' },\n\t\t{ name: 'Company 3', logo: '/logo3.png' }\n\t];\n</script>\n\n<Marquee speed=\"normal\" size=\"large\">\n\t{#each logos as logo}\n\t\t<img src={logo.logo} alt={logo.name} class=\"h-12 w-auto\" />\n\t{/each}\n</Marquee>\n```\n\n### Vertical Marquee\n\n```svelte\n<Marquee direction=\"up\" class=\"h-[400px]\">\n\t{#each items as item}\n\t\t<div class=\"px-6 py-4 bg-surface rounded\">\n\t\t\t{item.content}\n\t\t</div>\n\t{/each}\n</Marquee>\n```\n\n### With Fade Effect\n\n```svelte\n<Marquee fade={true}>\n\t{#each items as item}\n\t\t<div>{item.content}</div>\n\t{/each}\n</Marquee>\n```\n\n### Reverse Direction\n\n```svelte\n<Marquee reverse={true}>\n\t{#each items as item}\n\t\t<div>{item.content}</div>\n\t{/each}\n</Marquee>\n```\n\n### Custom Speed\n\n```svelte\n<Marquee speed={30}>\n\t{#each items as item}\n\t\t<div>{item.content}</div>\n\t{/each}\n</Marquee>\n```\n\n### Different Sizes\n\n```svelte\n<!-- Small -->\n<Marquee size=\"small\">\n\t{#each items as item}\n\t\t<div>{item.content}</div>\n\t{/each}\n</Marquee>\n\n<!-- Normal (default) -->\n<Marquee size=\"normal\">\n\t{#each items as item}\n\t\t<div>{item.content}</div>\n\t{/each}\n</Marquee>\n\n<!-- Large -->\n<Marquee size=\"large\">\n\t{#each items as item}\n\t\t<div>{item.content}</div>\n\t{/each}\n</Marquee>\n```\n\n### Custom Number of Copies\n\n```svelte\n<!-- More copies for smoother appearance -->\n<Marquee numberOfCopies={4} speed={30}>\n\t{#each items as item}\n\t\t<div>{item.content}</div>\n\t{/each}\n</Marquee>\n```\n\n### Disable Pause on Hover\n\n```svelte\n<Marquee pauseOnHover={false}>\n\t{#each items as item}\n\t\t<div>{item.content}</div>\n\t{/each}\n</Marquee>\n```\n\n### Combined Effects\n\n```svelte\n<Marquee \n\tfade={true}\n\treverse={true}\n\tspeed=\"slow\"\n\tsize=\"large\"\n>\n\t{#each items as item}\n\t\t<div class=\"px-8 py-6 bg-surface rounded-lg\">\n\t\t\t{item.content}\n\t\t</div>\n\t{/each}\n</Marquee>\n```\n\n## Structure\n\nThe Marquee component renders:\n- A container `<div>` with overflow hidden and optional scroll-fade utility classes\n- Multiple copies of the content (default 2) based on `numberOfCopies`\n- Each copy has the animation applied independently\n- Parent component iterates over items in the slot\n\n## Animation Details\n\n- Uses CSS keyframe animations for smooth GPU-accelerated performance\n- Content is duplicated `numberOfCopies` times to create seamless infinite scroll\n- Animation translates each copy by -100% of its width/height plus `--gap`, the same value the copies are spaced with\n- Animation duration is controlled via CSS custom property `--animation-duration`\n- Pause on hover uses CSS `animation-play-state` for instant response\n- Reverse mode uses CSS `direction: reverse` for the animation\n\n## Accessibility\n\n- Uses `role=\"presentation\"` on the container as the marquee is decorative\n- Consider providing alternative static content for screen readers\n- Ensure sufficient color foreground for text content\n- Pause on hover helps users interact with and read content\n- Animation is disabled when the user requests reduced motion; the still row then fades only its trailing edge\n\n## Notes\n\n- Parent component is responsible for iterating over items (similar to Carousel pattern)\n- Content is automatically duplicated internally based on `numberOfCopies`\n- The component uses CSS animations for optimal performance\n- Custom speed values are specified in seconds (e.g., `30` = 30 seconds per cycle)\n- For vertical marquees, ensure the container has a defined height\n- Increasing `numberOfCopies` creates smoother appearance but uses more DOM elements\n- The fade effect uses the shared scroll-fade utility classes\n\n## Theme Customization\n\nThe Marquee component 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 marquee container styles\n- **inner**: Inner scrolling content container styles\n\n### Available Variants\n\n**root**:\n- base: Base classes for main container\n- Variants:\n - direction: 'left' | 'up' - Scroll direction\n - size: 'small' | 'normal' | 'large' - Gap between duplicated content\n - fade: boolean - Applies shared scroll-fade utility classes\n\n**inner**:\n- base: Base classes for scrolling content\n- Variants:\n - direction: 'left' | 'up' - Animation direction\n - size: 'small' | 'normal' | 'large' - Gap size\n - pauseOnHover: boolean - Pause animation on hover\n - reverse: boolean - Reverse animation direction\n\n### Usage Examples\n\n**Basic Theme Override**:\n```svelte\n<Marquee \n theme={{\n root: {\n base: 'group flex overflow-hidden relative',\n direction: {\n left: 'flex-row'\n },\n size: {\n normal: '[--gap:var(--space-lg)]'\n }\n }\n }}\n>\n {#each items as item}\n <div>{item.content}</div>\n {/each}\n</Marquee>\n```\n\n**Custom Animation Styling**:\n```svelte\n<Marquee \n theme={{\n inner: {\n base: 'flex shrink-0 justify-around whitespace-nowrap',\n direction: {\n left: 'flex-row animate-marquee-left'\n },\n pauseOnHover: {\n true: 'group-hover:[animation-play-state:paused]'\n }\n }\n }}\n>\n {#each items as item}\n <div>{item.content}</div>\n {/each}\n</Marquee>\n```\n\n**Global Theme Setting**:\n```svelte\n<script>\n import { setMarqueeTheme } from './index.ts';\n \n setMarqueeTheme({\n root: {\n base: 'group flex overflow-hidden relative',\n direction: {\n left: 'flex-row'\n },\n size: {\n normal: '[--gap:var(--space-lg)]'\n }\n },\n inner: {\n base: 'flex shrink-0 justify-around whitespace-nowrap',\n direction: {\n left: 'flex-row animate-marquee-left'\n },\n pauseOnHover: {\n true: 'group-hover:[animation-play-state:paused]'\n }\n }\n });\n</script>\n```\n";
@@ -65,10 +65,13 @@ The Marquee component creates an infinite scrolling animation for displaying con
65
65
  - Uses a static scroll-fade mode because the marquee is clipped animation, not a native scroll container
66
66
 
67
67
  - **size**: 'small' | 'normal' | 'large' (default: 'normal')
68
- - Controls spacing between duplicated content copies
69
- - \`small\`: Compact spacing (gap-2)
70
- - \`normal\`: Standard spacing (gap-4)
71
- - \`large\`: Expanded spacing (gap-6)
68
+ - Sets the marquee's one spacing value, \`--gap\`, from the theme spacing scale
69
+ - \`small\`: \`--space-md\`
70
+ - \`normal\`: \`--space-xl\`
71
+ - \`large\`: \`--layout-space-md\`
72
+ - The copies, the items inside each copy and the loop step all use \`--gap\`, so the loop stays
73
+ seamless at every size and theme spacing. To respace a marquee, set \`--gap\` once on the root
74
+ (\`class="[--gap:var(--space-lg)]"\`); never redeclare it on the copies.
72
75
 
73
76
  ### Styling Props
74
77
  - **ref**: HTMLDivElement | null (bindable)
@@ -245,7 +248,7 @@ The Marquee component renders:
245
248
 
246
249
  - Uses CSS keyframe animations for smooth GPU-accelerated performance
247
250
  - Content is duplicated \`numberOfCopies\` times to create seamless infinite scroll
248
- - Animation translates each copy by -100% of its width/height plus gap
251
+ - Animation translates each copy by -100% of its width/height plus \`--gap\`, the same value the copies are spaced with
249
252
  - Animation duration is controlled via CSS custom property \`--animation-duration\`
250
253
  - Pause on hover uses CSS \`animation-play-state\` for instant response
251
254
  - Reverse mode uses CSS \`direction: reverse\` for the animation
@@ -256,7 +259,7 @@ The Marquee component renders:
256
259
  - Consider providing alternative static content for screen readers
257
260
  - Ensure sufficient color foreground for text content
258
261
  - Pause on hover helps users interact with and read content
259
- - Animation is disabled when the user requests reduced motion
262
+ - Animation is disabled when the user requests reduced motion; the still row then fades only its trailing edge
260
263
 
261
264
  ## Notes
262
265
 
@@ -307,7 +310,7 @@ The theme object contains the following parts:
307
310
  left: 'flex-row'
308
311
  },
309
312
  size: {
310
- normal: 'gap-4'
313
+ normal: '[--gap:var(--space-lg)]'
311
314
  }
312
315
  }
313
316
  }}
@@ -329,9 +332,6 @@ The theme object contains the following parts:
329
332
  },
330
333
  pauseOnHover: {
331
334
  true: 'group-hover:[animation-play-state:paused]'
332
- },
333
- size: {
334
- large: 'gap-6'
335
335
  }
336
336
  }
337
337
  }}
@@ -354,7 +354,7 @@ The theme object contains the following parts:
354
354
  left: 'flex-row'
355
355
  },
356
356
  size: {
357
- normal: 'gap-4'
357
+ normal: '[--gap:var(--space-lg)]'
358
358
  }
359
359
  },
360
360
  inner: {
@@ -2,17 +2,24 @@ import { setComponentTheme, useComponentTheme } from '../../utils/cva/index.js';
2
2
  import { cva } from '../../utils/cva/index.js';
3
3
  // `--marquee-animation-*` is INTERNAL: the `pauseOnHover` and `reverse` variants use it to
4
4
  // reach the track's animation. Consumers use the props, not the properties.
5
+ //
6
+ // `--gap` is the marquee's one spacing value. The size variant sets it from the spacing scale on
7
+ // the root; the track spaces its copies with it, every copy spaces its items with it, and the loop
8
+ // steps by one copy plus that same gap. Declaring it once is what keeps the loop seamless at every
9
+ // size and theme spacing factor — a copy that redeclared it would step by a different distance
10
+ // than the gap it is drawn with, and jump once per cycle. Respace a marquee by setting `--gap` on
11
+ // the root (`class="[--gap:…]"`), never on the copies.
5
12
  const defaultMarquee = cva({
6
- base: 'group flex overflow-hidden relative',
13
+ base: 'group flex overflow-hidden relative gap-(--gap)',
7
14
  variants: {
8
15
  direction: {
9
16
  left: 'flex-row',
10
17
  up: 'flex-col'
11
18
  },
12
19
  size: {
13
- small: 'gap-md',
14
- normal: 'gap-xl',
15
- large: 'gap-layout-md'
20
+ small: '[--gap:var(--space-md)]',
21
+ normal: '[--gap:var(--space-xl)]',
22
+ large: '[--gap:var(--layout-space-md)]'
16
23
  },
17
24
  fade: {
18
25
  true: 'scroll-fade-static',
@@ -30,16 +37,18 @@ const defaultMarquee = cva({
30
37
  }
31
38
  });
32
39
  const defaultInner = cva({
33
- base: 'flex shrink-0 justify-around [--gap:1rem] whitespace-nowrap',
40
+ base: 'flex shrink-0 justify-around gap-(--gap) whitespace-nowrap',
34
41
  variants: {
35
42
  direction: {
36
43
  left: 'flex-row animate-marquee-left',
37
44
  up: 'flex-col animate-marquee-up'
38
45
  },
46
+ // The copy inherits the root's `--gap`; the variant stays so per-size theme overrides of
47
+ // the copy keep a slot to land in.
39
48
  size: {
40
- small: 'gap-md',
41
- normal: 'gap-xl',
42
- large: 'gap-layout-md'
49
+ small: '',
50
+ normal: '',
51
+ large: ''
43
52
  },
44
53
  pauseOnHover: {
45
54
  true: 'group-hover:[--marquee-animation-play-state:paused]',
@@ -255,7 +255,15 @@
255
255
  out:slideTransition={collapseMotion.out}
256
256
  >
257
257
  {#each item.items ?? [] as sub, index (sub.label + index)}
258
- <SidebarMenuSubItem {sub} {size} {activeVariant} {density} {theme} />
258
+ <SidebarMenuSubItem
259
+ {sub}
260
+ {api}
261
+ {size}
262
+ {activeVariant}
263
+ {density}
264
+ iconCollapsed={isIconCollapsed}
265
+ {theme}
266
+ />
259
267
  {/each}
260
268
  </ul>
261
269
  {/snippet}
@@ -1,29 +1,54 @@
1
1
  <script lang="ts">
2
+ import { tooltip } from '../Tooltip/tooltip.attachment.svelte.js';
2
3
  import type {
3
4
  SidebarActiveVariant,
5
+ SidebarApi,
4
6
  SidebarDensity,
5
7
  SidebarMenuSubEntry,
6
8
  SidebarSize
7
9
  } from './sidebar.props.js';
10
+ import SidebarAction from './SidebarAction.svelte';
8
11
  import SidebarIcon from './SidebarIcon.svelte';
9
12
  import { useSidebarTheme, type SidebarThemeProps } from './sidebar.theme.js';
10
13
 
11
14
  let {
12
15
  sub,
16
+ api,
13
17
  size,
14
18
  activeVariant,
15
19
  density,
20
+ iconCollapsed = false,
16
21
  theme
17
22
  }: {
18
23
  sub: SidebarMenuSubEntry;
24
+ api: SidebarApi;
19
25
  size: SidebarSize;
20
26
  activeVariant: SidebarActiveVariant;
21
27
  density: SidebarDensity;
28
+ /** The Sidebar is collapsed to icons: a pinned action must not be reachable then. */
29
+ iconCollapsed?: boolean;
22
30
  theme?: SidebarThemeProps;
23
31
  } = $props();
24
32
 
25
33
  const classes = $derived(useSidebarTheme(theme));
26
34
  const subSize = $derived(sub.size ?? size);
35
+ // A bare glyph stays bare: the wrapper only appears when the entry asks for a role tint or a
36
+ // tile, so an untinted row never inherits the ambient `data-color`.
37
+ const hasIconSurface = $derived(!!sub.icon && (sub.iconVariant === 'tile' || !!sub.iconColor));
38
+ const hasBadge = $derived(sub.badge != null);
39
+ // The label is always visible on a submenu row, so the tooltip is extra information: shown
40
+ // whenever one is set, never on mobile, where hover does not exist.
41
+ const tooltipContent = $derived(sub.tooltip && !api.isMobile ? sub.tooltip : undefined);
42
+ const buttonClass = $derived(
43
+ classes.subButton({
44
+ size: subSize,
45
+ itemSize: sub.size,
46
+ activeVariant,
47
+ density,
48
+ reserveEnd: !!sub.action || hasBadge,
49
+ className: sub.class
50
+ })
51
+ );
27
52
 
28
53
  function handleClick(event: MouseEvent) {
29
54
  if (sub.disabled) {
@@ -35,6 +60,25 @@
35
60
  }
36
61
  </script>
37
62
 
63
+ {#snippet entryContent()}
64
+ {#if hasIconSurface}
65
+ <span
66
+ data-slot="sidebar-menu-icon"
67
+ data-color={sub.iconColor}
68
+ class={classes.menuIcon({
69
+ variant: sub.iconVariant ?? 'bare',
70
+ size: subSize,
71
+ placement: 'sub'
72
+ })}
73
+ >
74
+ <SidebarIcon icon={sub.icon} />
75
+ </span>
76
+ {:else}
77
+ <SidebarIcon icon={sub.icon} />
78
+ {/if}
79
+ <span>{sub.label}</span>
80
+ {/snippet}
81
+
38
82
  <li
39
83
  data-slot="sidebar-menu-sub-item"
40
84
  data-sidebar="menu-sub-item"
@@ -53,11 +97,11 @@
53
97
  aria-current={sub.isActive ? 'page' : undefined}
54
98
  aria-disabled={sub.disabled || undefined}
55
99
  tabindex={sub.disabled ? -1 : undefined}
56
- class={classes.subButton({ size: subSize, itemSize: sub.size, activeVariant, density })}
100
+ class={buttonClass}
101
+ {@attach tooltipContent ? tooltip({ content: tooltipContent, position: 'right' }) : undefined}
57
102
  onclick={handleClick}
58
103
  >
59
- <SidebarIcon icon={sub.icon} />
60
- <span>{sub.label}</span>
104
+ {@render entryContent()}
61
105
  </a>
62
106
  <!-- eslint-enable svelte/no-navigation-without-resolve -->
63
107
  {:else}
@@ -69,11 +113,32 @@
69
113
  data-active={sub.isActive ? 'true' : undefined}
70
114
  data-active-variant={activeVariant}
71
115
  disabled={sub.disabled || undefined}
72
- class={classes.subButton({ size: subSize, itemSize: sub.size, activeVariant, density })}
116
+ class={buttonClass}
117
+ {@attach tooltipContent ? tooltip({ content: tooltipContent, position: 'right' }) : undefined}
73
118
  onclick={handleClick}
74
119
  >
75
- <SidebarIcon icon={sub.icon} />
76
- <span>{sub.label}</span>
120
+ {@render entryContent()}
77
121
  </button>
78
122
  {/if}
123
+
124
+ {#if hasBadge}
125
+ <div
126
+ data-slot="sidebar-menu-sub-badge"
127
+ data-sidebar="menu-sub-badge"
128
+ class={classes.badge({ size: subSize, density })}
129
+ >
130
+ {sub.badge}
131
+ </div>
132
+ {/if}
133
+ {#if sub.action}
134
+ <div
135
+ data-slot="sidebar-menu-sub-action"
136
+ data-sidebar="menu-sub-action"
137
+ inert={iconCollapsed ? true : undefined}
138
+ aria-hidden={iconCollapsed ? 'true' : undefined}
139
+ class={classes.subAction({ size: subSize, density })}
140
+ >
141
+ <SidebarAction action={sub.action} {api} size={subSize} {theme} />
142
+ </div>
143
+ {/if}
79
144
  </li>
@@ -1,10 +1,13 @@
1
- import type { SidebarActiveVariant, SidebarDensity, SidebarMenuSubEntry, SidebarSize } from './sidebar.props.js';
1
+ import type { SidebarActiveVariant, SidebarApi, SidebarDensity, SidebarMenuSubEntry, SidebarSize } from './sidebar.props.js';
2
2
  import { type SidebarThemeProps } from './sidebar.theme.js';
3
3
  type $$ComponentProps = {
4
4
  sub: SidebarMenuSubEntry;
5
+ api: SidebarApi;
5
6
  size: SidebarSize;
6
7
  activeVariant: SidebarActiveVariant;
7
8
  density: SidebarDensity;
9
+ /** The Sidebar is collapsed to icons: a pinned action must not be reachable then. */
10
+ iconCollapsed?: boolean;
8
11
  theme?: SidebarThemeProps;
9
12
  };
10
13
  declare const SidebarMenuSubItem: import("svelte").Component<$$ComponentProps, {}, "">;
@@ -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`. 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";
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### SidebarMenuSubEntry\nA row of an inline submenu (`items` on a menu entry). It carries the same row features as a\ntop-level entry, so nested navigation (pages filed in folders) keeps its menus, pins and tints\nwithout flattening the tree. Every field below `size` is optional, and a sub entry that sets none\nof them renders exactly as a plain one.\n- **label**: string - Visible row label.\n- **href** / **onclick** - Render as an anchor, or a button with a native click handler.\n- **icon**: SidebarIcon - Leading icon.\n- **isActive** / **disabled** / **size** - As on a top-level entry; `size` defaults to the Sidebar size.\n- **iconColor**: Colors - Role tint for the leading icon, applied through `data-color`.\n- **iconVariant**: 'bare' | 'tile' - `tile` paints the same tinted rounded square as a top-level row.\n- **badge**: string | number - Trailing count or status; the label truncates before it.\n- **tooltip**: string - Tooltip text. Submenus never show in icon-collapsed mode, so a sub row shows its tooltip whenever one is set (never on mobile).\n- **class**: string - Classes applied to the row button.\n- **action**: SidebarMenuActionDescriptor | Snippet<[SidebarApi]> - Trailing action pinned to the row's end (`data-sidebar=\"menu-sub-action\"`), drawn by the `subAction` theme part: hidden until the row is hovered or focused from `md` up, kept visible while its menu is open, always visible below `md`. The label truncates before it.\n\n```svelte\n<script lang=\"ts\">\n\timport { Sidebar, type SidebarGroup } from 'entasis/sidebar';\n\timport { folderIcon } from 'entasis/icons/folder';\n\timport { fileTextIcon } from 'entasis/icons/fileText';\n\timport { pencilSimpleIcon } from 'entasis/icons/pencilSimple';\n\timport { pushPinIcon } from 'entasis/icons/pushPin';\n\timport { trashIcon } from 'entasis/icons/trash';\n\n\tconst items: SidebarGroup[] = [\n\t\t{\n\t\t\tlabel: 'Pages',\n\t\t\titems: [\n\t\t\t\t{\n\t\t\t\t\tlabel: 'Planning',\n\t\t\t\t\ticon: folderIcon,\n\t\t\t\t\tdefaultOpen: true,\n\t\t\t\t\titems: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tlabel: 'Q3 roadmap',\n\t\t\t\t\t\t\thref: '/pages/q3-roadmap',\n\t\t\t\t\t\t\ticon: fileTextIcon,\n\t\t\t\t\t\t\ticonColor: 'success',\n\t\t\t\t\t\t\ticonVariant: 'tile',\n\t\t\t\t\t\t\tbadge: 2,\n\t\t\t\t\t\t\taction: {\n\t\t\t\t\t\t\t\tlabel: 'More options for Q3 roadmap',\n\t\t\t\t\t\t\t\tmenu: [\n\t\t\t\t\t\t\t\t\t{ type: 'option', title: 'Rename', prefix: pencilSimpleIcon },\n\t\t\t\t\t\t\t\t\t{ type: 'option', title: 'Pin', prefix: pushPinIcon },\n\t\t\t\t\t\t\t\t\t{ type: 'option', title: 'Delete', prefix: trashIcon, color: 'danger' }\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{ label: 'Retro notes', href: '/pages/retro-notes', icon: fileTextIcon }\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t];\n</script>\n\n<Sidebar {items} />\n```\n\n### SidebarView\nOne named panel content in `views`.\n- **label**: string - The view's name, shown on the back row of the views nested under it.\n- **parent**: string - Key of the view this one is nested under. A nested view opens with a back row to its parent (named \"Back, <parent label>\"), and on mobile a swipe toward the inline end goes back. A view without `parent` is a top-level section.\n- **items**: SidebarGroup[] / **content**: Snippet<[SidebarApi]> - The view's body.\n- **headerButton**, **search**, **headerMenu**, **header**, **footerButton**, **footerMenu**, **footer** - Header and footer props for this view. Each one left undefined comes from the parent view, then from the Sidebar's own prop; `null` removes an inherited one.\n\nChanging the view slides the two views side by side, like pages: a deeper view comes in from the inline end while the old one leaves to the start, a shallower one slides back the other way, and between views at the same depth (sections) the later one in `views` counts as forward. When both views get every header and footer prop from the same place, the header and footer stay still and only the menu slides; when a view changes any of them, the whole panel slides as one page. `api.view` reads the current view and `api.setView(key)` changes it from a snippet.\n\n### SidebarActivityBar\nIcon rail pinned to the outer edge of the sidebar, visible in every display state.\n- **items**: SidebarActivityBarItem[] - Items rendered from the top.\n- **footerItems**: SidebarActivityBarItem[] - Items pinned to the end of the column.\n- **header** / **footer**: Snippet - Custom content before the first item and after the pinned ones.\n- **width**: string (default '3rem') - Column thickness, published as `--sidebar-width-activity`.\n- **label**: string - Accessible name for the column landmark. Set it whenever the panel also renders navigation.\n- **onSelect**: ({ item, index }) => void - Fires after an item is activated. `index` counts `items` then `footerItems`.\n\n### SidebarActivityBarItem\n- **icon**: SidebarIcon (required) - Icon rendered in the square.\n- **label**: string (required) - Accessible name and default tooltip; the square shows no text.\n- **id**: string - Stable render key.\n- **href** / **target** / **rel** - Render an anchor instead of a button.\n- **onclick**: (event: MouseEvent) => void - Native click handler.\n- **isActive**: boolean - Adds active styling and `aria-current=\"page\"`.\n- **badge**: string | number | Snippet - Corner badge pinned to the outer top corner. An empty string renders a bare dot. A string or number badge joins the accessible name (`\"Alerts, 3\"`); a dot and a Snippet badge are decorative, so put their meaning in `label`.\n- **disabled**: boolean - Blocks activation and skips the item during keyboard navigation.\n- **tooltip**: string | false - Tooltip override; `false` suppresses it.\n\n### SidebarMenuButtonItem\nUse for `headerButton`, `footerButton`, or direct `<SidebarMenuButton />` rows.\n- **icon**: SidebarIcon - Leading logo/icon.\n- **avatar**: { src?: string; alt?: string; fallback?: string } - Leading avatar.\n- **variant**: 'default' | 'brand' | 'compact'.\n- **title**: string - Primary text.\n- **subtitle**: string - Secondary text.\n- **trailing**: SidebarIcon | false | SidebarMenuActionDescriptor - Trailing content. An icon is decorative; an action descriptor (`{ icon, label, onclick }`, the same shape as a group action) renders its own icon-only ghost button beside the row, so a workspace card can carry its own collapse control without a custom `header` snippet. Descriptor handlers are `onclick(event, api)`, so `api.toggle()` is reachable.\n- **href** / **onclick** / **menu** - Choose link, button, or popup behavior. `onclick(event, api)` receives the SidebarApi beside the event, so `api.toggle()` is reachable from the row itself.\n- **menuIconClass**: string - Class override for option icons inside the popup menu.\n\n## Props\n\n### State\n- **open**: boolean (bindable, default true) - Desktop expanded state.\n- **defaultOpen**: boolean (default true) - Initial desktop state when `open` is omitted.\n- **onOpenChange**: (open: boolean) => void - Called once for a library-originated desktop state change. Repeated requests and parent prop updates stay silent.\n- **onDisplayStateChange**: (state: SidebarDisplayState) => void - Called once for a library-originated semantic display-state change.\n- **view**: string (bindable) - Key of the view on screen when `views` is set. Defaults to `defaultView`, then the first view.\n- **defaultView**: string - Initial view when `view` is omitted.\n- **onViewChange**: (view: string) => void - Called once for a library-originated view change: a view row, a back row, a swipe. Parent prop updates stay silent; with a route-driven `view`, navigate here.\n- **api.displayState**: 'expanded' | 'collapsed' | 'hidden' - Semantic desktop state; hidden means closed offcanvas. A hover peek does not change it.\n- **api.isPeeking**: boolean - True while a hover peek renders the collapsed panel at full width. Read it alongside `displayState` when a snippet hides content in icon mode.\n- **keyboardShortcut**: string | false (default 'b') - Ctrl/Cmd shortcut key.\n\n### Layout\n- **side**: 'left' | 'right' - Desktop and mobile side.\n- **variant**: 'admin' | 'floating' | 'inset' | 'split' | 'framed' - Sidebar geometry. `framed` is the admin geometry for a sidebar hosted inside a raised card (AppShell variant framed), with a `surface-recessed` well. `admin` renders the conventional full-height navigation column; `inset` integrates navigation into the lower wall with an inset content surface; `split` renders detached sidebar and content surfaces.\n- **size**: 'small' | 'normal' | 'large' (default 'normal') - Typography, icon, avatar, badge, leading-media, item-height, and search-height scale.\n- **iconSize**: 'small' | 'normal' | 'large' - Icon and leading-media scale inside the panel on its own; defaults to `size`. Menu rows, sub rows, group labels and the header button follow it; the activity bar keeps `size`.\n- **activeVariant**: 'soft' | 'outline' | 'solid' (default 'soft') - How active rows are painted. `soft` is the shared selected recipe (`selectedSoft`: `bg-selected-muted text-selected-muted-readable`), `solid` its loud counterpart (`selectedSolid`: `bg-selected text-selected-contrast`), `outline` a bordered surface card (`bg-surface border border-neutral-muted text-neutral`) that reads as a raised card on a tinted well. Each row carries the choice as `data-active-variant`, so no descendant selector is needed to restyle selection.\n- **density**: 'compact' | 'normal' | 'comfortable' (default 'normal') - Section padding, group padding, gaps, horizontal inset, and submenu spacing.\n- **collapsible**: 'offcanvas' | 'icon' | 'none' - Collapse behavior. Icon mode requires icons on every data-driven row and otherwise resolves to offcanvas.\n- **collapseIcon**: DisclosureIndicator \u2014 'chevron' | 'plus-minus' | 'none' (default 'chevron') - Disclosure indicator drawn on collapsible menu rows. 'none' renders no indicator.\n- **mode**: 'layout' | 'panel' - Full resizing layout or only the visible navigation panel.\n- **frame**: 'viewport' | 'contained' - Standalone Sidebar positioning. Viewport mode uses Theme's dynamic window-height token; contained mode fills a positioned parent.\n- **width**: string - Expanded width.\n- **widthIcon**: string - Icon-collapsed width.\n- **widthMobile**: string - Mobile drawer width.\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- A submenu row's `action` is a sibling of its link, so Tab reaches the action button right after the row; its trigger takes `label` as its accessible name, and Escape closes its menu and returns focus to the trigger. It is `inert` and `aria-hidden` whenever the Sidebar is collapsed to icons.\n- 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";
@@ -86,6 +86,67 @@ recursive tree groups, header/footer rows, search, actions, and snippet escape h
86
86
  - **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.
87
87
  - **action**: SidebarMenuActionDescriptor | Snippet<[SidebarApi]> - Hover/focus trailing action.
88
88
 
89
+ ### SidebarMenuSubEntry
90
+ A row of an inline submenu (\`items\` on a menu entry). It carries the same row features as a
91
+ top-level entry, so nested navigation (pages filed in folders) keeps its menus, pins and tints
92
+ without flattening the tree. Every field below \`size\` is optional, and a sub entry that sets none
93
+ of them renders exactly as a plain one.
94
+ - **label**: string - Visible row label.
95
+ - **href** / **onclick** - Render as an anchor, or a button with a native click handler.
96
+ - **icon**: SidebarIcon - Leading icon.
97
+ - **isActive** / **disabled** / **size** - As on a top-level entry; \`size\` defaults to the Sidebar size.
98
+ - **iconColor**: Colors - Role tint for the leading icon, applied through \`data-color\`.
99
+ - **iconVariant**: 'bare' | 'tile' - \`tile\` paints the same tinted rounded square as a top-level row.
100
+ - **badge**: string | number - Trailing count or status; the label truncates before it.
101
+ - **tooltip**: string - Tooltip text. Submenus never show in icon-collapsed mode, so a sub row shows its tooltip whenever one is set (never on mobile).
102
+ - **class**: string - Classes applied to the row button.
103
+ - **action**: SidebarMenuActionDescriptor | Snippet<[SidebarApi]> - Trailing action pinned to the row's end (\`data-sidebar="menu-sub-action"\`), drawn by the \`subAction\` theme part: hidden until the row is hovered or focused from \`md\` up, kept visible while its menu is open, always visible below \`md\`. The label truncates before it.
104
+
105
+ \`\`\`svelte
106
+ <script lang="ts">
107
+ import { Sidebar, type SidebarGroup } from './index.ts';
108
+ import { folderIcon } from 'entasis/icons/folder';
109
+ import { fileTextIcon } from 'entasis/icons/fileText';
110
+ import { pencilSimpleIcon } from 'entasis/icons/pencilSimple';
111
+ import { pushPinIcon } from 'entasis/icons/pushPin';
112
+ import { trashIcon } from 'entasis/icons/trash';
113
+
114
+ const items: SidebarGroup[] = [
115
+ {
116
+ label: 'Pages',
117
+ items: [
118
+ {
119
+ label: 'Planning',
120
+ icon: folderIcon,
121
+ defaultOpen: true,
122
+ items: [
123
+ {
124
+ label: 'Q3 roadmap',
125
+ href: '/pages/q3-roadmap',
126
+ icon: fileTextIcon,
127
+ iconColor: 'success',
128
+ iconVariant: 'tile',
129
+ badge: 2,
130
+ action: {
131
+ label: 'More options for Q3 roadmap',
132
+ menu: [
133
+ { type: 'option', title: 'Rename', prefix: pencilSimpleIcon },
134
+ { type: 'option', title: 'Pin', prefix: pushPinIcon },
135
+ { type: 'option', title: 'Delete', prefix: trashIcon, color: 'danger' }
136
+ ]
137
+ }
138
+ },
139
+ { label: 'Retro notes', href: '/pages/retro-notes', icon: fileTextIcon }
140
+ ]
141
+ }
142
+ ]
143
+ }
144
+ ];
145
+ </script>
146
+
147
+ <Sidebar {items} />
148
+ \`\`\`
149
+
89
150
  ### SidebarView
90
151
  One named panel content in \`views\`.
91
152
  - **label**: string - The view's name, shown on the back row of the views nested under it.
@@ -213,6 +274,7 @@ default verbatim, prefixes included.
213
274
  - Mobile drawer includes a backdrop button labelled "Close Sidebar".
214
275
  - Icon-collapsed rows keep their labels mounted and visually fade them, preserving accessible names and stable icon geometry.
215
276
  - Search, group controls, actions, and nested rows become inert before collapse can remove or hide them; focus returns to the owning visible row.
277
+ - A submenu row's \`action\` is a sibling of its link, so Tab reaches the action button right after the row; its trigger takes \`label\` as its accessible name, and Escape closes its menu and returns focus to the trigger. It is \`inert\` and \`aria-hidden\` whenever the Sidebar is collapsed to icons.
216
278
  - Nested groups, tree branches, and inline submenus use reversible height transitions for open, close, and sidebar-collapse changes.
217
279
  - 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.
218
280
  - When \`rail\` and \`resizable\` are both enabled, one edge control owns click-to-toggle, drag resize, and keyboard resize without overlapping hitboxes.
@@ -168,6 +168,24 @@ export type SidebarMenuSubEntry = {
168
168
  disabled?: boolean;
169
169
  /** Submenu row geometry scale. Defaults to the Sidebar size. */
170
170
  size?: Sizes;
171
+ /** Role tint applied to the leading icon, through `data-color`. */
172
+ iconColor?: Colors;
173
+ /** Leading icon treatment. 'tile' paints a tinted rounded square around the glyph. */
174
+ iconVariant?: SidebarIconVariant;
175
+ /** Trailing badge. */
176
+ badge?: string | number;
177
+ /**
178
+ * Tooltip text. Submenus never show in icon-collapsed mode, where the label is hidden, so a
179
+ * submenu row shows its tooltip whenever one is set (never on mobile).
180
+ */
181
+ tooltip?: string;
182
+ /** Classes applied to the row button. */
183
+ class?: string;
184
+ /**
185
+ * Secondary action pinned to the row's end, like a top-level entry's: shown on hover or focus
186
+ * from `md` up and while its menu is open, always on touch-sized screens.
187
+ */
188
+ action?: Snippet<[SidebarApi]> | SidebarMenuActionDescriptor;
171
189
  };
172
190
  type SidebarMenuEntryBase = {
173
191
  /** Visible label. */
@@ -234,6 +234,10 @@ export declare const sidebarTheme: {
234
234
  normal: string;
235
235
  large: string;
236
236
  };
237
+ reserveEnd: {
238
+ true: string;
239
+ false: string;
240
+ };
237
241
  }> & import("../../utils/cva/types.js").CVAClassProp) | undefined) => string;
238
242
  menuAction: (props?: (import("../../utils/cva/types.js").CVAVariantSchema<{
239
243
  size: {
@@ -247,6 +251,18 @@ export declare const sidebarTheme: {
247
251
  comfortable: string;
248
252
  };
249
253
  }> & import("../../utils/cva/types.js").CVAClassProp) | undefined) => string;
254
+ subAction: (props?: (import("../../utils/cva/types.js").CVAVariantSchema<{
255
+ size: {
256
+ small: string;
257
+ normal: string;
258
+ large: string;
259
+ };
260
+ density: {
261
+ compact: string;
262
+ normal: string;
263
+ comfortable: string;
264
+ };
265
+ }> & import("../../utils/cva/types.js").CVAClassProp) | undefined) => string;
250
266
  actionSlot: (props?: (import("../../utils/cva/types.js").CVAVariantSchema<{
251
267
  size: {
252
268
  small: string;
@@ -271,6 +287,10 @@ export declare const sidebarTheme: {
271
287
  normal: string;
272
288
  large: string;
273
289
  };
290
+ placement: {
291
+ row: string;
292
+ sub: string;
293
+ };
274
294
  }> & import("../../utils/cva/types.js").CVAClassProp) | undefined) => string;
275
295
  buttonRow: (props?: import("../../utils/cva/types.js").CVAClassProp | undefined) => string;
276
296
  badge: (props?: (import("../../utils/cva/types.js").CVAVariantSchema<{
@@ -745,6 +765,10 @@ export declare const setSidebarTheme: (theme: InferComponentTheme<{
745
765
  normal: string;
746
766
  large: string;
747
767
  };
768
+ reserveEnd: {
769
+ true: string;
770
+ false: string;
771
+ };
748
772
  }> & import("../../utils/cva/types.js").CVAClassProp) | undefined) => string;
749
773
  menuAction: (props?: (import("../../utils/cva/types.js").CVAVariantSchema<{
750
774
  size: {
@@ -758,6 +782,18 @@ export declare const setSidebarTheme: (theme: InferComponentTheme<{
758
782
  comfortable: string;
759
783
  };
760
784
  }> & import("../../utils/cva/types.js").CVAClassProp) | undefined) => string;
785
+ subAction: (props?: (import("../../utils/cva/types.js").CVAVariantSchema<{
786
+ size: {
787
+ small: string;
788
+ normal: string;
789
+ large: string;
790
+ };
791
+ density: {
792
+ compact: string;
793
+ normal: string;
794
+ comfortable: string;
795
+ };
796
+ }> & import("../../utils/cva/types.js").CVAClassProp) | undefined) => string;
761
797
  actionSlot: (props?: (import("../../utils/cva/types.js").CVAVariantSchema<{
762
798
  size: {
763
799
  small: string;
@@ -782,6 +818,10 @@ export declare const setSidebarTheme: (theme: InferComponentTheme<{
782
818
  normal: string;
783
819
  large: string;
784
820
  };
821
+ placement: {
822
+ row: string;
823
+ sub: string;
824
+ };
785
825
  }> & import("../../utils/cva/types.js").CVAClassProp) | undefined) => string;
786
826
  buttonRow: (props?: import("../../utils/cva/types.js").CVAClassProp | undefined) => string;
787
827
  badge: (props?: (import("../../utils/cva/types.js").CVAVariantSchema<{
@@ -1254,6 +1294,10 @@ export declare const useSidebarTheme: import("../../utils/cva/theme.js").UseComp
1254
1294
  normal: string;
1255
1295
  large: string;
1256
1296
  };
1297
+ reserveEnd: {
1298
+ true: string;
1299
+ false: string;
1300
+ };
1257
1301
  }> & import("../../utils/cva/types.js").CVAClassProp) | undefined) => string;
1258
1302
  menuAction: (props?: (import("../../utils/cva/types.js").CVAVariantSchema<{
1259
1303
  size: {
@@ -1267,6 +1311,18 @@ export declare const useSidebarTheme: import("../../utils/cva/theme.js").UseComp
1267
1311
  comfortable: string;
1268
1312
  };
1269
1313
  }> & import("../../utils/cva/types.js").CVAClassProp) | undefined) => string;
1314
+ subAction: (props?: (import("../../utils/cva/types.js").CVAVariantSchema<{
1315
+ size: {
1316
+ small: string;
1317
+ normal: string;
1318
+ large: string;
1319
+ };
1320
+ density: {
1321
+ compact: string;
1322
+ normal: string;
1323
+ comfortable: string;
1324
+ };
1325
+ }> & import("../../utils/cva/types.js").CVAClassProp) | undefined) => string;
1270
1326
  actionSlot: (props?: (import("../../utils/cva/types.js").CVAVariantSchema<{
1271
1327
  size: {
1272
1328
  small: string;
@@ -1291,6 +1347,10 @@ export declare const useSidebarTheme: import("../../utils/cva/theme.js").UseComp
1291
1347
  normal: string;
1292
1348
  large: string;
1293
1349
  };
1350
+ placement: {
1351
+ row: string;
1352
+ sub: string;
1353
+ };
1294
1354
  }> & import("../../utils/cva/types.js").CVAClassProp) | undefined) => string;
1295
1355
  buttonRow: (props?: import("../../utils/cva/types.js").CVAClassProp | undefined) => string;
1296
1356
  badge: (props?: (import("../../utils/cva/types.js").CVAVariantSchema<{
@@ -333,13 +333,20 @@ const defaultSubButton = cva({
333
333
  small: 'text-xs',
334
334
  normal: '',
335
335
  large: ''
336
+ },
337
+ // Room at the row's end for a pinned action or badge, so the label truncates before it
338
+ // instead of running under it. Off adds nothing: a plain row keeps its exact classes.
339
+ reserveEnd: {
340
+ true: 'pr-layout-lg',
341
+ false: ''
336
342
  }
337
343
  },
338
344
  defaultVariants: {
339
345
  activeVariant: 'soft',
340
346
  size: 'normal',
341
347
  density: 'normal',
342
- itemSize: 'normal'
348
+ itemSize: 'normal',
349
+ reserveEnd: false
343
350
  }
344
351
  });
345
352
  const defaultMenuAction = cva({
@@ -361,6 +368,28 @@ const defaultMenuAction = cva({
361
368
  density: 'normal'
362
369
  }
363
370
  });
371
+ // A submenu row's pinned action: the `menuAction` recipe keyed to the sub row's own
372
+ // `group/menu-sub-item`, one step smaller to sit in the shorter row. Hidden until the row is
373
+ // hovered or focused from `md` up, kept while its menu is open, always shown below `md`.
374
+ const defaultSubAction = cva({
375
+ base: 'state-layer text-neutral hover:text-neutral absolute top-1/2 flex aspect-square -translate-y-1/2 items-center justify-center rounded-sm p-0 opacity-100 outline-none transition group-data-[collapsible=icon]:hidden focus-visible:ring-2 focus-visible:ring-focus/50 md:opacity-0 group-focus-within/menu-sub-item:opacity-100 group-hover/menu-sub-item:opacity-100 has-[[aria-expanded=true]]:opacity-100 [&>svg]:shrink-0 [&>svg]:size-[var(--sidebar-icon-size)]',
376
+ variants: {
377
+ size: {
378
+ small: 'size-4',
379
+ normal: 'size-4.5',
380
+ large: 'size-5'
381
+ },
382
+ density: {
383
+ compact: 'right-0.5',
384
+ normal: 'right-1',
385
+ comfortable: 'right-1.5'
386
+ }
387
+ },
388
+ defaultVariants: {
389
+ size: 'normal',
390
+ density: 'normal'
391
+ }
392
+ });
364
393
  // One icon-only ghost button box. `size` follows the descriptor, defaulting to the Sidebar size,
365
394
  // so a group's `+` lands at row scale instead of at the group label's scale.
366
395
  const defaultActionSlot = cva({
@@ -389,16 +418,24 @@ const defaultMenuIcon = cva({
389
418
  small: '',
390
419
  normal: '',
391
420
  large: ''
421
+ },
422
+ // Where the icon sits. A top-level row sizes every nested glyph itself; a submenu row only
423
+ // sizes its direct glyph, so a bare wrapped icon there carries the row's icon size.
424
+ placement: {
425
+ row: '',
426
+ sub: ''
392
427
  }
393
428
  },
394
429
  compoundVariants: [
430
+ { variant: 'bare', placement: 'sub', class: '[&_svg]:size-[var(--sidebar-icon-size)]' },
395
431
  { variant: 'tile', size: 'small', class: 'size-icon-lg [&_svg]:size-icon-xs' },
396
432
  { variant: 'tile', size: 'normal', class: 'size-icon-xl [&_svg]:size-icon-sm' },
397
433
  { variant: 'tile', size: 'large', class: 'size-icon-xl [&_svg]:size-icon-md' }
398
434
  ],
399
435
  defaultVariants: {
400
436
  variant: 'bare',
401
- size: 'normal'
437
+ size: 'normal',
438
+ placement: 'row'
402
439
  }
403
440
  });
404
441
  // A large menu row that carries its own trailing action: the row and the action are siblings, so
@@ -1008,6 +1045,7 @@ export const sidebarTheme = {
1008
1045
  treeSubMenu: defaultTreeSubMenu,
1009
1046
  subButton: defaultSubButton,
1010
1047
  menuAction: defaultMenuAction,
1048
+ subAction: defaultSubAction,
1011
1049
  actionSlot: defaultActionSlot,
1012
1050
  actionTrigger: defaultActionTrigger,
1013
1051
  menuIcon: defaultMenuIcon,
@@ -25,7 +25,7 @@ export declare const componentMcpRegistry: {
25
25
  readonly stack: "\n# Stack\n\nStack arranges arbitrary content along one flex axis. Use the `orientation` prop to switch\nbetween horizontal and vertical layout, and `align` / `justify` for cross-axis and main-axis\nalignment.\n\n## Import\n\n```svelte\n<script lang=\"ts\">\n import { Stack } from '../components/Stack/index.ts';\n</script>\n```\n\n## Usage\n\n```svelte\n<Stack gap=\"xl\" padding=\"xl\">\n <h2>Account</h2>\n <Stack orientation=\"horizontal\" align=\"center\" gap=\"md\" wrap=\"wrap\">\n <span>Profile</span>\n <span>Security</span>\n </Stack>\n</Stack>\n```\n\n## Responsive props\n\n`orientation`, `gap`, `align`, `justify` and `wrap` each take a plain value or a\nper-breakpoint record:\n\n```svelte\n<Stack orientation={{ md: 'horizontal' }} gap={{ xs: 'sm', lg: 'xl' }} align=\"center\">\n <span>Filters</span>\n <span>Results</span>\n</Stack>\n```\n\nThe breakpoints measure the stack's OWN width — `xs` base, `sm` 36rem, `md` 42rem,\n`lg` 56rem, `xl` 72rem — not the viewport's, so the same stack is `xs` in a narrow sidebar\nand `lg` full-bleed on the same page. The nearest defined key at or below the stack's width\nwins, and below the narrowest key the prop's default applies: `{ md: 'horizontal' }` is\nvertical at `xs` and `sm`. A prop falls back to its default only when it is `undefined`\nor `null`. A function form must be deterministic in its argument — it is called once per\nbreakpoint. There is no measurement and no JS: the five values ship as custom\nproperties and container queries pick one, so server-rendered markup is already laid out.\n\n## Props\n\n- `orientation`: `'horizontal' | 'vertical'` — flex direction (default: `'vertical'`).\n- `align`: cross-axis alignment — `start | center | end | stretch` (default: `'stretch'`).\n- `justify`: main-axis alignment — `start | center | end | between | around | evenly` (default: `'start'`).\n- `gap`, `padding`, `paddingInline`, and `paddingBlock` accept\n `none | xs | sm | md | lg | xl`.\n- `paddingInline` and `paddingBlock` override `padding` on their axis.\n- `width`, `height`, `maxWidth`, and `minHeight` accept CSS strings or pixel numbers.\n- `wrap` accepts `nowrap | wrap | wrap-reverse`.\n- `scrollable` enables native `overflow: auto`.\n- `as` changes the semantic HTML element without changing layout behavior:\n `div | span | section | article | aside | main | nav | header | footer | form | fieldset`.\n Lists are not among them — the root always wraps its children in one layout `<div>`, which\n `<ul>` and `<ol>` do not admit. Write the list yourself and put a stack inside an `<li>`.\n\n## Structure\n\nStack renders two elements: the root (`data-slot=\"stack\"`) is the box the host sizes and the\ncontainer the breakpoints are measured against — it takes `as`, `class`, `style`, the size\nprops, the padding and `scrollable` — and a single layout child (`data-slot=\"stack-layout\"`)\ncarries the flex line. The `inner` theme slot styles that child.\n\nStack forwards common semantic HTML attributes and Svelte attachments to the root element.\nPrefer parent-owned `gap` over child margins. The internal `micro` and `layout-*` tokens are\nreserved for component recipes and must not be used in generated interfaces.\n";
26
26
  readonly 'app-shell': "\n# AppShell Component\n\nConvenience wrapper for the common application layout: Sidebar owns navigation,\nresponsive drawer behavior, the application wall, and variant surfaces, while PageShell\nowns the page header, document-flow content, footer, and route-level injection. AppShell\nforwards one shared variant to Sidebar and composes PageShell inside it.\nAppShell establishes a dynamic viewport-height minimum while letting the document own\nvertical scrolling. The desktop sidebar remains sticky independently of page content.\n\nUse AppShell when every route follows the same sidebar + page shell structure. Use\nSidebar and PageShell directly when the frame needs custom composition.\n\n## Basic Usage\n\n```svelte\n<script lang=\"ts\">\n\timport { AppShell, type AppShellSidebarProps } from 'entasis/app-shell';\n\timport { houseIcon } from 'entasis/icons/house';\n\n\tconst sidebar: AppShellSidebarProps = {\n\t\tcollapsible: 'icon',\n\t\trail: true,\n\t\titems: [\n\t\t\t{\n\t\t\t\tlabel: 'Workspace',\n\t\t\t\titems: [{ label: 'Home', href: '/', icon: houseIcon, isActive: true }]\n\t\t\t}\n\t\t]\n\t};\n</script>\n\n<AppShell variant=\"framed\" {sidebar} title=\"Dashboard\" subtitle=\"Operational overview\">\n\t{#snippet children({ sidebar })}\n\t\t<button type=\"button\" onclick={sidebar.toggle}>Toggle sidebar</button>\n\t{/snippet}\n</AppShell>\n```\n\n## Route-Level Injection\n\nAppShell renders PageShell internally, so child pages can use the PageShell context API:\n\n```svelte\n<script lang=\"ts\">\n\timport { setPageShell } from 'entasis/page-shell';\n\n\tsetPageShell({\n\t\ttitle: 'Insights',\n\t\tsubtitle: 'Revenue and retention',\n\t\tfooter: pageFooter\n\t});\n</script>\n\n{#snippet pageFooter()}\n\t<span>Synced just now</span>\n{/snippet}\n```\n\n## Props\n\n- **variant**: 'admin' | 'floating' | 'inset' | 'split' | 'framed' - Shared shell treatment forwarded to Sidebar and PageShell chrome. Admin chrome uses the Sidebar canvas surface; floating chrome uses detached raised, rounded surfaces. `framed` draws one rounded card (`rounded-xl` + `raised-1`, so the border and elevation come from the elevation engine) around both the sidebar and the page; the Sidebar's own `framed` variant paints its navigation well as `surface-recessed`, an inset of that card, and the page header sits on the page surface.\n- **sidebar**: AppShellSidebarProps - Sidebar props except `children`, `mode`, `frame`, and `variant`.\n Configure Sidebar `size` and `density` independently inside this object.\n- **eyebrow**: string | Snippet<[PageShellApi]> - Small metadata above the PageShell title.\n- **breadcrumbs**: BreadcrumbItem[] | Snippet<[AppShellApi]> - PageShell breadcrumbs.\n- **breadcrumbsMaxItems**: number - Maximum visible breadcrumb items before ellipsis. Defaults to 4.\n- **back**: PageShellAction | Snippet<[AppShellApi]> - Back affordance before breadcrumbs or eyebrow.\n- **title**: string | Snippet<[PageShellApi]> - Default PageShell title.\n- **subtitle**: string | Snippet<[PageShellApi]> - Default PageShell subtitle.\n- **header**: Snippet<[PageShellApi]> - Custom PageShell header.\n- **headerActions**: Snippet<[AppShellApi]> | PageShellAction[] - Actions in the default PageShell header. Use an array for standard Button props, or a snippet when the action needs sidebar/page-shell API access.\n- **footer**: Snippet<[PageShellApi]> - PageShell footer.\n- **footerActions**: Snippet<[AppShellApi]> | PageShellAction[] - PageShell footer actions.\n- **children**: Snippet<[AppShellApi]> - Main content, with `pageShell` and `sidebar` APIs.\n- **contentPadding**: 'none' | 'small' | 'normal' | 'large' - PageShell content padding preset.\n- **contentWidth**: 'full' | 'narrow' | 'normal' | 'wide' | 'prose' - PageShell content width preset.\n- **pageShellTheme**: PageShellThemeProps - PageShell theme overrides.\n- **theme**: AppShellThemeProps - AppShell `root`, `frame` and `page` surface-token overrides. Sidebar owns the wall and shell geometry.\n\n## Accessibility\n\nAppShell delegates navigation semantics to Sidebar and page landmarks to PageShell.\nUse string `title` for the default `h1`, or preserve heading semantics when replacing\nthe PageShell header with a custom snippet.\n";
27
27
  readonly 'page-shell': "\n# PageShell Component\n\nContent shell for pages rendered inside an application frame. PageShell provides a\nsticky header and footer, document-flow content, title/subtitle props, and a context\nAPI for child routes to inject shell content. Scrolling stays on the document by default,\nso browser navigation and scroll restoration keep their native behavior.\n\nUse PageShell inside `Sidebar.children` when Sidebar owns navigation and responsive\ndrawer behavior.\n\n## Basic Usage\n\n```svelte\n<script lang=\"ts\">\n\timport { PageShell, type PageShellAction } from 'entasis/page-shell';\n\timport { downloadSimpleIcon } from 'entasis/icons/downloadSimple';\n\n\tconst headerActions = [\n\t\t{\n\t\t\tcontent: 'Export',\n\t\t\tcolor: 'primary',\n\t\t\tprefix: downloadSimpleIcon\n\t\t}\n\t] satisfies PageShellAction[];\n</script>\n\n<PageShell title=\"Insights\" subtitle=\"Live account health\" {headerActions}>\n\t{#snippet footer()}\n\t\t<span>Updated just now</span>\n\t{/snippet}\n\n\t{#snippet children()}\n\t\t<section class=\"p-6\">Page content</section>\n\t{/snippet}\n</PageShell>\n```\n\n## Route-Level Injection\n\nChild pages can set header and footer content through context. Use `setPageShell`\nduring component initialization for automatic cleanup.\n\n```svelte\n<script lang=\"ts\">\n\timport { setPageShell } from 'entasis/page-shell';\n\n\tsetPageShell({\n\t\ttitle: 'Revenue',\n\t\tsubtitle: 'Segment breakdown',\n\t\theaderActions: revenueActions,\n\t\tfooter: revenueFooter\n\t});\n</script>\n\n{#snippet revenueActions()}\n\t<button type=\"button\">Refresh</button>\n{/snippet}\n\n{#snippet revenueFooter()}\n\t<span>Synced 2 minutes ago</span>\n{/snippet}\n```\n\n## Props\n\n- **eyebrow**: string | Snippet<[PageShellApi]> - Small metadata above the title. Ignored when breadcrumbs are set.\n- **breadcrumbs**: BreadcrumbItem[] | Snippet<[PageShellApi]> - Default-header breadcrumbs.\n- **breadcrumbsMaxItems**: number - Maximum visible breadcrumb items before ellipsis. Defaults to 4.\n- **back**: PageShellAction | Snippet<[PageShellApi]> - Back affordance before breadcrumbs or eyebrow.\n- **title**: string | Snippet<[PageShellApi]> - Default header title.\n- **subtitle**: string | Snippet<[PageShellApi]> - Default header subtitle.\n- **header**: Snippet<[PageShellApi]> - Custom sticky header content.\n- **headerActions**: Snippet<[PageShellApi]> | PageShellAction[] - Actions on the right side of the default header. Use an array for standard Button props, or a snippet when the action needs shell API access.\n- **footer**: Snippet<[PageShellApi]> - Custom sticky footer content.\n- **footerActions**: Snippet<[PageShellApi]> | PageShellAction[] - Actions on the right side of the sticky footer.\n\t- **children**: Snippet<[PageShellApi]> - Page content rendered in normal document flow.\n\t- **contentPadding**: 'none' | 'small' | 'normal' | 'large' - Padding applied to the content inner wrapper.\n\t- **contentWidth**: 'full' | 'narrow' | 'normal' | 'wide' | 'prose' - Max-width preset for the content inner wrapper.\n\t- **actionOverflow**: 'auto' | 'never' - Mobile overflow behavior for action arrays.\n\t- **mobileActionCount**: 0 | 1 | 2 - Number of action-array buttons kept inline on mobile.\n\t- **label**: string - Accessible name for the page's `main` landmark, applied as aria-label.\n\t- **theme**: PageShellThemeProps - Per-instance theme overrides.\n\n## API\n\n- **usePageShell()** returns the current PageShell API and throws when no PageShell exists.\n- **setPageShell(config)** registers a scoped config override and removes it on component destroy.\n- **api.set(config)** pushes a manual override and returns a cleanup function.\n- **api.setFooterActions(actions)** pushes scoped page footer actions.\n- **api.reset()** clears all scoped overrides.\n\n## Header Action Arrays\n\n```svelte\n<script lang=\"ts\">\n\timport { PageShell, type PageShellAction } from 'entasis/page-shell';\n\timport { arrowClockwiseIcon } from 'entasis/icons/arrowClockwise';\n\n\tconst headerActions = [\n\t\t{\n\t\t\tlabel: 'Refresh',\n\t\t\tsquared: true,\n\t\t\tvariant: 'outline',\n\t\t\tprefix: arrowClockwiseIcon\n\t\t},\n\t\t{\n\t\t\tcontent: 'Create report',\n\t\t\tcolor: 'primary'\n\t\t}\n\t] satisfies PageShellAction[];\n</script>\n\n<PageShell title=\"Reports\" {headerActions}>\n\t{#snippet children()}\n\t\tPage content\n\t{/snippet}\n</PageShell>\n```\n\n## Content Presets And Footer Actions\n\n```svelte\n<PageShell\n\teyebrow=\"Settings\"\n\ttitle=\"Billing profile\"\n\tcontentPadding=\"normal\"\n\tcontentWidth=\"narrow\"\n\tfooterActions={[\n\t\t{ content: 'Cancel', variant: 'outline' },\n\t\t{ content: 'Save changes', color: 'primary' }\n\t]}\n>\n\t{#snippet footer()}\n\t\t<span>2 unsaved changes</span>\n\t{/snippet}\n\n\t{#snippet children()}\n\t\t<form>...</form>\n\t{/snippet}\n</PageShell>\n```\n\n## Accessibility\n\nPageShell renders semantic `header`, `main`, and `footer` regions. The title is an\n`h1` when provided as a string. Custom snippets are responsible for preserving\nequivalent semantics when replacing the default header.\n";
28
- readonly sidebar: "\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 — '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 } }}>` → `setSidebarTheme({ motion })` →\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";
28
+ readonly sidebar: "\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### SidebarMenuSubEntry\nA row of an inline submenu (`items` on a menu entry). It carries the same row features as a\ntop-level entry, so nested navigation (pages filed in folders) keeps its menus, pins and tints\nwithout flattening the tree. Every field below `size` is optional, and a sub entry that sets none\nof them renders exactly as a plain one.\n- **label**: string - Visible row label.\n- **href** / **onclick** - Render as an anchor, or a button with a native click handler.\n- **icon**: SidebarIcon - Leading icon.\n- **isActive** / **disabled** / **size** - As on a top-level entry; `size` defaults to the Sidebar size.\n- **iconColor**: Colors - Role tint for the leading icon, applied through `data-color`.\n- **iconVariant**: 'bare' | 'tile' - `tile` paints the same tinted rounded square as a top-level row.\n- **badge**: string | number - Trailing count or status; the label truncates before it.\n- **tooltip**: string - Tooltip text. Submenus never show in icon-collapsed mode, so a sub row shows its tooltip whenever one is set (never on mobile).\n- **class**: string - Classes applied to the row button.\n- **action**: SidebarMenuActionDescriptor | Snippet<[SidebarApi]> - Trailing action pinned to the row's end (`data-sidebar=\"menu-sub-action\"`), drawn by the `subAction` theme part: hidden until the row is hovered or focused from `md` up, kept visible while its menu is open, always visible below `md`. The label truncates before it.\n\n```svelte\n<script lang=\"ts\">\n\timport { Sidebar, type SidebarGroup } from 'entasis/sidebar';\n\timport { folderIcon } from 'entasis/icons/folder';\n\timport { fileTextIcon } from 'entasis/icons/fileText';\n\timport { pencilSimpleIcon } from 'entasis/icons/pencilSimple';\n\timport { pushPinIcon } from 'entasis/icons/pushPin';\n\timport { trashIcon } from 'entasis/icons/trash';\n\n\tconst items: SidebarGroup[] = [\n\t\t{\n\t\t\tlabel: 'Pages',\n\t\t\titems: [\n\t\t\t\t{\n\t\t\t\t\tlabel: 'Planning',\n\t\t\t\t\ticon: folderIcon,\n\t\t\t\t\tdefaultOpen: true,\n\t\t\t\t\titems: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tlabel: 'Q3 roadmap',\n\t\t\t\t\t\t\thref: '/pages/q3-roadmap',\n\t\t\t\t\t\t\ticon: fileTextIcon,\n\t\t\t\t\t\t\ticonColor: 'success',\n\t\t\t\t\t\t\ticonVariant: 'tile',\n\t\t\t\t\t\t\tbadge: 2,\n\t\t\t\t\t\t\taction: {\n\t\t\t\t\t\t\t\tlabel: 'More options for Q3 roadmap',\n\t\t\t\t\t\t\t\tmenu: [\n\t\t\t\t\t\t\t\t\t{ type: 'option', title: 'Rename', prefix: pencilSimpleIcon },\n\t\t\t\t\t\t\t\t\t{ type: 'option', title: 'Pin', prefix: pushPinIcon },\n\t\t\t\t\t\t\t\t\t{ type: 'option', title: 'Delete', prefix: trashIcon, color: 'danger' }\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{ label: 'Retro notes', href: '/pages/retro-notes', icon: fileTextIcon }\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t];\n</script>\n\n<Sidebar {items} />\n```\n\n### SidebarView\nOne named panel content in `views`.\n- **label**: string - The view's name, shown on the back row of the views nested under it.\n- **parent**: string - Key of the view this one is nested under. A nested view opens with a back row to its parent (named \"Back, <parent label>\"), and on mobile a swipe toward the inline end goes back. A view without `parent` is a top-level section.\n- **items**: SidebarGroup[] / **content**: Snippet<[SidebarApi]> - The view's body.\n- **headerButton**, **search**, **headerMenu**, **header**, **footerButton**, **footerMenu**, **footer** - Header and footer props for this view. Each one left undefined comes from the parent view, then from the Sidebar's own prop; `null` removes an inherited one.\n\nChanging the view slides the two views side by side, like pages: a deeper view comes in from the inline end while the old one leaves to the start, a shallower one slides back the other way, and between views at the same depth (sections) the later one in `views` counts as forward. When both views get every header and footer prop from the same place, the header and footer stay still and only the menu slides; when a view changes any of them, the whole panel slides as one page. `api.view` reads the current view and `api.setView(key)` changes it from a snippet.\n\n### SidebarActivityBar\nIcon rail pinned to the outer edge of the sidebar, visible in every display state.\n- **items**: SidebarActivityBarItem[] - Items rendered from the top.\n- **footerItems**: SidebarActivityBarItem[] - Items pinned to the end of the column.\n- **header** / **footer**: Snippet - Custom content before the first item and after the pinned ones.\n- **width**: string (default '3rem') - Column thickness, published as `--sidebar-width-activity`.\n- **label**: string - Accessible name for the column landmark. Set it whenever the panel also renders navigation.\n- **onSelect**: ({ item, index }) => void - Fires after an item is activated. `index` counts `items` then `footerItems`.\n\n### SidebarActivityBarItem\n- **icon**: SidebarIcon (required) - Icon rendered in the square.\n- **label**: string (required) - Accessible name and default tooltip; the square shows no text.\n- **id**: string - Stable render key.\n- **href** / **target** / **rel** - Render an anchor instead of a button.\n- **onclick**: (event: MouseEvent) => void - Native click handler.\n- **isActive**: boolean - Adds active styling and `aria-current=\"page\"`.\n- **badge**: string | number | Snippet - Corner badge pinned to the outer top corner. An empty string renders a bare dot. A string or number badge joins the accessible name (`\"Alerts, 3\"`); a dot and a Snippet badge are decorative, so put their meaning in `label`.\n- **disabled**: boolean - Blocks activation and skips the item during keyboard navigation.\n- **tooltip**: string | false - Tooltip override; `false` suppresses it.\n\n### SidebarMenuButtonItem\nUse for `headerButton`, `footerButton`, or direct `<SidebarMenuButton />` rows.\n- **icon**: SidebarIcon - Leading logo/icon.\n- **avatar**: { src?: string; alt?: string; fallback?: string } - Leading avatar.\n- **variant**: 'default' | 'brand' | 'compact'.\n- **title**: string - Primary text.\n- **subtitle**: string - Secondary text.\n- **trailing**: SidebarIcon | false | SidebarMenuActionDescriptor - Trailing content. An icon is decorative; an action descriptor (`{ icon, label, onclick }`, the same shape as a group action) renders its own icon-only ghost button beside the row, so a workspace card can carry its own collapse control without a custom `header` snippet. Descriptor handlers are `onclick(event, api)`, so `api.toggle()` is reachable.\n- **href** / **onclick** / **menu** - Choose link, button, or popup behavior. `onclick(event, api)` receives the SidebarApi beside the event, so `api.toggle()` is reachable from the row itself.\n- **menuIconClass**: string - Class override for option icons inside the popup menu.\n\n## Props\n\n### State\n- **open**: boolean (bindable, default true) - Desktop expanded state.\n- **defaultOpen**: boolean (default true) - Initial desktop state when `open` is omitted.\n- **onOpenChange**: (open: boolean) => void - Called once for a library-originated desktop state change. Repeated requests and parent prop updates stay silent.\n- **onDisplayStateChange**: (state: SidebarDisplayState) => void - Called once for a library-originated semantic display-state change.\n- **view**: string (bindable) - Key of the view on screen when `views` is set. Defaults to `defaultView`, then the first view.\n- **defaultView**: string - Initial view when `view` is omitted.\n- **onViewChange**: (view: string) => void - Called once for a library-originated view change: a view row, a back row, a swipe. Parent prop updates stay silent; with a route-driven `view`, navigate here.\n- **api.displayState**: 'expanded' | 'collapsed' | 'hidden' - Semantic desktop state; hidden means closed offcanvas. A hover peek does not change it.\n- **api.isPeeking**: boolean - True while a hover peek renders the collapsed panel at full width. Read it alongside `displayState` when a snippet hides content in icon mode.\n- **keyboardShortcut**: string | false (default 'b') - Ctrl/Cmd shortcut key.\n\n### Layout\n- **side**: 'left' | 'right' - Desktop and mobile side.\n- **variant**: 'admin' | 'floating' | 'inset' | 'split' | 'framed' - Sidebar geometry. `framed` is the admin geometry for a sidebar hosted inside a raised card (AppShell variant framed), with a `surface-recessed` well. `admin` renders the conventional full-height navigation column; `inset` integrates navigation into the lower wall with an inset content surface; `split` renders detached sidebar and content surfaces.\n- **size**: 'small' | 'normal' | 'large' (default 'normal') - Typography, icon, avatar, badge, leading-media, item-height, and search-height scale.\n- **iconSize**: 'small' | 'normal' | 'large' - Icon and leading-media scale inside the panel on its own; defaults to `size`. Menu rows, sub rows, group labels and the header button follow it; the activity bar keeps `size`.\n- **activeVariant**: 'soft' | 'outline' | 'solid' (default 'soft') - How active rows are painted. `soft` is the shared selected recipe (`selectedSoft`: `bg-selected-muted text-selected-muted-readable`), `solid` its loud counterpart (`selectedSolid`: `bg-selected text-selected-contrast`), `outline` a bordered surface card (`bg-surface border border-neutral-muted text-neutral`) that reads as a raised card on a tinted well. Each row carries the choice as `data-active-variant`, so no descendant selector is needed to restyle selection.\n- **density**: 'compact' | 'normal' | 'comfortable' (default 'normal') - Section padding, group padding, gaps, horizontal inset, and submenu spacing.\n- **collapsible**: 'offcanvas' | 'icon' | 'none' - Collapse behavior. Icon mode requires icons on every data-driven row and otherwise resolves to offcanvas.\n- **collapseIcon**: DisclosureIndicator — '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 } }}>` → `setSidebarTheme({ motion })` →\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- A submenu row's `action` is a sibling of its link, so Tab reaches the action button right after the row; its trigger takes `label` as its accessible name, and Escape closes its menu and returns focus to the trigger. It is `inert` and `aria-hidden` whenever the Sidebar is collapsed to icons.\n- 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";
29
29
  readonly button: "\n# Button Component\n\nThe Button component is a flexible and customizable button element that supports various variants, sizes, colors, and interactive states.\n\n## Basic Usage\n\n```svelte\n<Button>Click me</Button>\n<Button variant=\"outline\">Outline Button</Button>\n<Button color=\"primary\" size=\"large\">Large Primary Button</Button>\n```\n\n## Props\n\n### Core Props\n- **variant**: 'solid' | 'outline' | 'soft' | 'ghost' | 'link' (default: 'solid')\n - solid: Filled background with color\n - outline: Transparent background with colored border\n - soft: Muted color background\n - ghost: Transparent background with a transient state layer on hover and press\n - link: Text-only styling with underline on hover\n\n- **color**: 'primary' | 'secondary' | 'neutral' | 'danger' | 'success' | 'warning' | 'info' (default: 'neutral')\n - Determines the color scheme of the button\n\n- **size**: 'small' | 'normal' | 'large' (default: 'normal')\n - small: 28px height, smaller padding and text\n - normal: 32px height, standard padding\n - large: 36px height, larger padding and text\n\n### Layout Props\n- **fullWidth**: boolean (default: false) - Makes button take full width of container\n- **squared**: boolean - Makes button square (aspect-ratio 1:1), auto-determined if only prefix/suffix is provided\n- **disabled**: boolean (default: false) - Disables button interaction\n- **loading**: boolean (default: false) - Shows the Theme-configured loading spinner and disables interaction\n\n### State Props\nDescribe the meaning; the Button writes the ARIA. Never pass an aria-* attribute to a Button.\n- **pressed**: boolean - Toggle state of a button that stays on or off (a bold button in a toolbar, a \"show password\" eye). Rendered as aria-pressed\n- **selected**: boolean - Chosen state of a button acting as one option among several (a tab, a listbox option). Rendered as aria-selected\n- **expanded**: boolean - Whether the surface this button opens is showing. Rendered as aria-expanded. A entasis surface (Popover, PopupMenu, Select, Combobox) sets this on its own trigger, so pass it only for a surface you open yourself\n- **haspopup**: 'dialog' | 'menu' | 'listbox' | 'tree' | 'grid' | true - What the surface this button opens contains. Rendered as aria-haspopup, and likewise set by a entasis surface on its own trigger\n\n### Link Props\n- **href**: string - Makes button render as anchor tag\n- **target**: string - Link target (e.g., \"_blank\")\n- **rel**: string - Link relationship\n\n### Event Props\n- **onclick**: (event: MouseEvent) => void - Native click event handler\n- **onpointerenter**: (event: PointerEvent) => void - Native pointer enter event handler\n- **onpointerleave**: (event: PointerEvent) => void - Native pointer leave event handler\n\n### Content Props (Slots)\n- **children**: Snippet - Main button content\n- **prefix**: Snippet - Content before main text (typically icons)\n- **suffix**: Snippet - Content after main text (typically icons)\n\n### Advanced Props\n- **label**: string - Accessible label applied as aria-label on the root element (required for icon-only buttons)\n- **ref**: HTMLElement - Reference to the button element\n- **class**: string - Additional CSS classes\n- **theme**: ComponentTheme - Custom theme overrides\n\n## Structure\n\nThe button follows this DOM structure:\n```\n<Button>\n\t<Prefix /> <!-- Optional prefix content -->\n\t<Children /> <!-- Main button content -->\n\t<Suffix /> <!-- Optional suffix content -->\n</Button>\n```\n\n## Examples\n\n### Basic Buttons\n```svelte\n<Button>Default Button</Button>\n<Button variant=\"outline\" color=\"primary\">Primary Outline</Button>\n<Button variant=\"soft\" color=\"danger\">Soft Danger</Button>\n<Button variant=\"ghost\">Ghost Button</Button>\n<Button variant=\"link\">Link Button</Button>\n```\n\n### With Icons\n```svelte\n<Button>\n\t{#snippet prefix()}\n\t\t{@render icon()}\n\t{/snippet}\n\tAdd Item\n</Button>\n\n<Button squared>\n\t{#snippet prefix()}\n\t\t{@render icon()}\n\t{/snippet}\n</Button>\n```\n\n### Interactive States\n```svelte\n<Button loading>Loading...</Button>\n<Button disabled>Disabled</Button>\n<Button fullWidth>Full Width Button</Button>\n```\n\n### As Link\n```svelte\n<Button href=\"/dashboard\" target=\"_blank\">Go to Dashboard</Button>\n```\n\n### With Event Handlers\n```svelte\n<script lang=\"ts\">\n\tfunction handleClick(event: MouseEvent) {\n\t\tconsole.log('Clicked:', event.currentTarget);\n\t}\n</script>\n\n<Button onclick={handleClick}>\n\tClick\n</Button>\n```\n\n### Custom Styling\n```svelte\n<Button class=\"lift-4 border-2\" color=\"primary\" variant=\"outline\">\n\tCustom Styled\n</Button>\n```\n\n## Accessibility\n\n- Automatically sets appropriate ARIA roles (button/link)\n- Supports keyboard navigation\n- Disabled state prevents interaction\n- Loading state provides visual feedback\n\n## Notes\n\n- When `href` is provided, renders as `<a>` tag, otherwise `<button>`\n- `squared` is automatically determined when only prefix or suffix is provided without children\n- All event handlers respect disabled state\n- Icon sizing is automatically adjusted based on button size\n\n## Theme Customization\n\nThe Button component 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 button container styles\n- **prefix**: Styles for prefix content (icons before text)\n- **suffix**: Styles for suffix content (icons after text)\n\n### Theme Type Definition\n\n```typescript\nimport type { ButtonThemeProps } from 'entasis/button';\n\n// Example theme customization\nconst customTheme: ButtonThemeProps = {\n root: {\n base: 'custom-base-classes',\n size: {\n small: 'custom-small-classes',\n normal: 'custom-normal-classes',\n large: 'custom-large-classes'\n },\n color: {\n primary: 'bg-blue-500 text-white',\n danger: 'bg-red-500 text-white'\n },\n variant: {\n solid: 'bg-color text-color-contrast',\n outline: 'border-2 border-color'\n }\n },\n prefix: {\n size: {\n small: 'w-3 h-3',\n normal: 'w-4 h-4',\n large: 'w-5 h-5'\n }\n }\n};\n```\n\n### Available Variants\n\n**root**:\n- base: Base classes applied to all buttons\n- Variants:\n - size: 'small' | 'normal' | 'large' - Controls padding, text size, and height\n - color: 'primary' | 'secondary' | 'neutral' | 'danger' | 'success' | 'warning' | 'info' - Color scheme\n - variant: 'solid' | 'outline' | 'soft' | 'ghost' | 'link' - Visual style variant\n - loading: boolean - Loading state styling\n - disabled: boolean - Disabled state styling\n - squared: boolean - Square button styling\n - fullWidth: boolean - Full width styling\n\n**prefix**:\n- base: Base classes for prefix content\n- Variants:\n - size: 'small' | 'normal' | 'large' - Icon size based on button size\n\n**suffix**:\n- base: Base classes for suffix content\n- Variants:\n - size: 'small' | 'normal' | 'large' - Icon size based on button size\n\n### Usage Examples\n\n**Basic Theme Override**:\n```svelte\n<Button \n theme={{\n root: {\n base: 'rounded-full lift-4',\n size: {\n large: 'px-8 py-4 text-xl'\n }\n }\n }}\n>\n Custom Styled Button\n</Button>\n```\n\n**Color Variant Customization**:\n```svelte\n<Button \n color=\"primary\"\n theme={{\n root: {\n color: {\n primary: 'state-layer bg-linear-to-r from-blue-500 to-purple-500'\n }\n }\n }}\n>\n Gradient Button\n</Button>\n```\n\n**Global Theme Setting**:\n```svelte\n<script>\n import { setButtonTheme } from '../components/Button/index.ts';\n \n setButtonTheme({\n root: {\n variant: {\n solid: 'state-layer bg-color text-color-contrast lift-3 hover:lift-4 transition-shadow',\n outline: 'state-layer border-2 border-color'\n }\n },\n prefix: {\n size: {\n normal: 'w-5 h-5'\n }\n }\n });\n</script>\n```\n";
30
30
  readonly 'button-group': "\n# ButtonGroup Component\n\nThe ButtonGroup component displays a collection of related buttons as a cohesive group with shared styling properties.\n\n## Basic Usage\n\n```svelte\n<ButtonGroup \n\titems={[\n\t\t{ children: 'First' },\n\t\t{ children: 'Second' },\n\t\t{ children: 'Third' }\n\t]}\n/>\n```\n\n## Props\n\n### Core Props\nPass either `items` or `children`.\n- **items**: Array<ButtonProps> - Array of button configurations\n - Each button can have all standard Button component props\n- **children**: Snippet - Buttons composed directly, for content an item object cannot describe\n (a Tooltip or Popover trigger, a Button with a custom body). Each direct child is joined to its\n neighbours; set size, color and variant on each Button, since the shared props below apply to\n `items` only.\n- **label**: string - Accessible name of the group (the root has `role=\"group\"`)\n\n### Shared Button Props (items)\n- **size**: 'small' | 'normal' | 'large' - Applied to all buttons in the group\n- **color**: 'primary' | 'secondary' | 'neutral' | 'danger' | 'success' | 'warning' | 'info' - Shared color for all buttons\n- **variant**: 'solid' | 'outline' | 'soft' | 'ghost' | 'link' - Shared variant for all buttons\n- **disabled**: boolean - Disables all buttons in the group, even one whose item sets `disabled: false`\n\n### Styling Props\n- **class**: string - Additional CSS classes for the group container\n- **theme**: ComponentTheme - Custom theme overrides\n\n## Structure\n\n```\n<ButtonGroup>\n\t<Button />\n\t<Button />\n\t<Button />\n</ButtonGroup>\n```\n\n## Examples\n\n### Composed Children\n```svelte\n<script lang=\"ts\">\n\timport { Button } from 'entasis/button';\n\timport { ButtonGroup } from 'entasis/button-group';\n\timport { Tooltip } from 'entasis/tooltip';\n</script>\n\n<ButtonGroup label=\"History\">\n\t<Button variant=\"outline\">Undo</Button>\n\t<Tooltip content=\"Redo the last change\" trigger={{ content: 'Redo', variant: 'outline' }} />\n</ButtonGroup>\n```\n\n### Basic Button Group\n```svelte\n<ButtonGroup \n\titems={[\n\t\t{ children: 'Left' },\n\t\t{ children: 'Center' },\n\t\t{ children: 'Right' }\n\t]}\n/>\n```\n\n### With Shared Styling\n```svelte\n<ButtonGroup \n\tsize=\"large\"\n\tcolor=\"primary\"\n\tvariant=\"outline\"\n\titems={[\n\t\t{ children: 'Option 1' },\n\t\t{ children: 'Option 2' },\n\t\t{ children: 'Option 3' }\n\t]}\n/>\n```\n\n### With Icons\n```svelte\n<script lang=\"ts\">\n\timport { ButtonGroup } from 'entasis/button-group';\n\timport { textAlignLeftIcon } from 'entasis/icons/textAlignLeft';\n\timport { textAlignCenterIcon } from 'entasis/icons/textAlignCenter';\n\timport { textAlignRightIcon } from 'entasis/icons/textAlignRight';\n</script>\n\n<ButtonGroup \n\titems={[\n\t\t{ \n\t\t\tprefix: textAlignLeftIcon,\n\t\t\tchildren: 'Left' \n\t\t},\n\t\t{ \n\t\t\tprefix: textAlignCenterIcon,\n\t\t\tchildren: 'Center' \n\t\t},\n\t\t{ \n\t\t\tprefix: textAlignRightIcon,\n\t\t\tchildren: 'Right' \n\t\t}\n\t]}\n/>\n```\n\n### With Individual Click Handlers\n```svelte\n<script>\n\tfunction handleOption(option) {\n\t\tconsole.log(`Selected: ${option}`);\n\t}\n</script>\n\n<ButtonGroup \n\titems={[\n\t\t{ \n\t\t\tchildren: 'Save',\n\t\t\tonclick: () => handleOption('save')\n\t\t},\n\t\t{ \n\t\t\tchildren: 'Cancel',\n\t\t\tonclick: () => handleOption('cancel')\n\t\t}\n\t]}\n/>\n```\n\n### Disabled Group\n```svelte\n<ButtonGroup \n\tdisabled\n\titems={[\n\t\t{ children: 'Option 1' },\n\t\t{ children: 'Option 2' }\n\t]}\n/>\n```\n\n### Icon Only Buttons\n```svelte\n<script lang=\"ts\">\n\timport { ButtonGroup } from 'entasis/button-group';\n\timport { textBIcon } from 'entasis/icons/textB';\n\timport { textItalicIcon } from 'entasis/icons/textItalic';\n\timport { textUnderlineIcon } from 'entasis/icons/textUnderline';\n</script>\n\n<ButtonGroup \n\titems={[\n\t\t{ \n\t\t\tsquared: true,\n\t\t\tprefix: textBIcon\n\t\t},\n\t\t{ \n\t\t\tsquared: true,\n\t\t\tprefix: textItalicIcon\n\t\t},\n\t\t{ \n\t\t\tsquared: true,\n\t\t\tprefix: textUnderlineIcon\n\t\t}\n\t]}\n/>\n```\n\n### Mixed Button States\n```svelte\n<ButtonGroup \n\tvariant=\"outline\"\n\titems={[\n\t\t{ children: 'Active', color: 'primary' },\n\t\t{ children: 'Default', color: 'neutral' },\n\t\t{ children: 'Disabled', disabled: true }\n\t]}\n/>\n```\n\n### Segmented Control\n```svelte\n<script>\n\tlet selected = $state('week');\n</script>\n\n<ButtonGroup \n\titems={[\n\t\t{ \n\t\t\tchildren: 'Day',\n\t\t\tvariant: selected === 'day' ? 'solid' : 'ghost',\n\t\t\tonclick: () => selected = 'day'\n\t\t},\n\t\t{ \n\t\t\tchildren: 'Week',\n\t\t\tvariant: selected === 'week' ? 'solid' : 'ghost',\n\t\t\tonclick: () => selected = 'week'\n\t\t},\n\t\t{ \n\t\t\tchildren: 'Month',\n\t\t\tvariant: selected === 'month' ? 'solid' : 'ghost',\n\t\t\tonclick: () => selected = 'month'\n\t\t}\n\t]}\n/>\n```\n\n## Styling\n\nButtonGroup automatically:\n- Removes border-radius from middle buttons\n- Adjusts borders to prevent double borders\n- Creates a cohesive, connected appearance\n- Maintains consistent spacing\n\n## Accessibility\n\n- Each button maintains full keyboard accessibility\n- Focus styles are preserved\n- Disabled state cascades properly\n- Screen readers announce each button individually\n\n## Notes\n\n- Individual button props override shared props\n- Buttons are rendered in the order provided\n- The group container can be styled with the `class` prop\n- All Button component features are supported for individual buttons\n\n## Theme Customization\n\nThe ButtonGroup component 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 button group container styles\n\n### Available Variants\n\n**root**:\n- base: Base classes for button group container (handles border radius and border connections between buttons)\n\n### Usage Examples\n\n**Basic Theme Override**:\n```svelte\n<ButtonGroup \n items={buttons}\n theme={{\n root: {\n base: 'flex items-center rounded-lg overflow-hidden'\n }\n }}\n/>\n```\n\n**Custom Group Styling**:\n```svelte\n<ButtonGroup \n items={buttons}\n theme={{\n root: {\n base: 'flex items-center gap-0 border-2 border-primary rounded-lg overflow-hidden'\n }\n }}\n/>\n```\n\n**Global Theme Setting**:\n```svelte\n<script>\n import { setButtonGroupTheme } from '../components/ButtonGroup/index.ts';\n \n setButtonGroupTheme({\n root: {\n base: 'flex items-center first-child:rounded-r-none last-child:rounded-l-none'\n }\n });\n</script>\n```\n";
31
31
  readonly 'segmented-control': "\n# SegmentedControl\n\nA compact single-selection input for switching between a small set of mutually exclusive modes. It uses radiogroup/radio semantics and an animated indicator that slides and resizes to the selected item.\n\n## Basic usage\n\n```svelte\n<script lang=\"ts\">\n import { SegmentedControl } from '../components/SegmentedControl/index.ts';\n import { gridFourIcon } from 'entasis/icons/gridFour';\n import { listIcon } from 'entasis/icons/list';\n\n const items = [\n { value: 'grid', label: 'Grid', icon: gridFourIcon },\n { value: 'list', label: 'List', icon: listIcon }\n ] as const;\n\n let value = $state<(typeof items)[number]['value']>('grid');\n</script>\n\n<SegmentedControl {items} bind:value />\n```\n\n## Props\n\n- **items**: `readonly SegmentedControlItem[]` — required options with unique `value` fields.\n- **value**: `string` — bindable selected value; defaults to the first enabled item.\n- **defaultValue**: `string` — initial selected value when value is omitted.\n- **onValueChange**: `(value: string) => void` — called once after user interaction changes the value.\n- **item**: `Snippet<[SegmentedControlItem]>` — replaces the default item renderer.\n- **size**: `'small' | 'normal' | 'large'` — defaults to `'normal'`.\n- **color**: semantic color — controls the selected pill and focus ring; defaults to `'neutral'`.\n- **variant**: `'normal' | 'pill'` — controls corner radius; defaults to the moderately rounded `'normal'` shape.\n- **disabled**: `boolean` — disables the full control.\n- **label**: `string` — accessible name for the radiogroup; defaults to the catalog's \"Segmented control\".\n- **class**: additional root classes.\n- **theme**: component theme overrides.\n\n## Item shape\n\n```ts\ntype SegmentedControlItem<Value extends string = string> = {\n value: Value;\n label?: string | Snippet;\n icon?: string | Snippet;\n disabled?: boolean;\n};\n```\n\nThe default renderer displays `icon`, then `label`. `label` is also the segment's accessible name: a string is painted and spoken, a snippet names the segment through its own content, and an icon-only segment (no `label`) falls back to its `value`.\n\n## Custom item renderer\n\n```svelte\n<SegmentedControl {items} bind:value>\n {#snippet item(option)}\n <span>{option.label}</span>\n <span>{option.count}</span>\n {/snippet}\n</SegmentedControl>\n```\n\nExtra fields on item objects remain available to the snippet through generic inference.\n\n## Shapes\n\n`variant=\"normal\"` uses a moderately rounded track and segments. Use `variant=\"pill\"` for the fully rounded stadium shape.\n\n```svelte\n<SegmentedControl {items} bind:value />\n<SegmentedControl {items} bind:value variant=\"pill\" />\n```\n\n## Keyboard behavior\n\n- Tab enters on the selected item, or the first enabled item when no value matches.\n- Arrow Left/Right moves and selects, wrapping at the ends.\n- Home/End selects the first/last enabled item.\n- Disabled items are skipped.\n\nEach item has an invisible, size-aware pointer target that extends beyond the visual track: 36px for small, 44px for normal, and 48px for large. Adjacent targets meet at the midpoint of the configured gap instead of overlapping.\n\nUse SegmentedControl for mode or value selection. Use Tabbar when changing navigational views with tab semantics.\n";
@@ -123,7 +123,7 @@ export declare const componentMcpRegistry: {
123
123
  readonly globe: "\n# Globe Component\n\nAn interactive, auto-rotating WebGL globe (powered by cobe). Draggable, theme-aware, and animated\nin via a fade. Colors accept theme tokens and recolor automatically when the app theme changes.\n\n## Requires\n\nGlobe renders through cobe (WebGL). It is an optional peer dependency of entasis, so install it alongside entasis:\n\n`pnpm add cobe`\n\n## Basic Usage\n\n```svelte\n<script>\n import { Globe } from '../components/Globe/index.ts';\n</script>\n\n<Globe />\n<Globe baseColor=\"neutral\" glowColor=\"primary\" markerColor=\"primary\" />\n```\n\n## Props\n\n### Appearance\n- **baseColor**: Colors | [number, number, number] (default: soft white) - Landmass dot color. Token or normalized RGB (0–1). A light base gives bright continents that pop on the shaded sphere.\n- **markerColor**: Colors | [number, number, number] (default: 'primary') - Marker dot color.\n- **glowColor**: Colors | [number, number, number] (default: soft white) - Atmospheric glow color.\n- **dark**: number (default: 1) - Shading darkness 0–1. Pass a negative value to follow the app theme (dark -> 1, light -> 0).\n- **opacity**: number (default: 1) - Globe texture opacity.\n- **scale**: number (default: 1) - Globe scale within the canvas.\n- **diffuse**: number (default: 1.2) - Diffuse lighting intensity.\n- **mapSamples**: number (default: 16000) - Number of map dots (0–100000).\n- **mapBrightness**: number (default: 6) - Brightness of the map dots.\n- **offset**: [number, number] | ['x%', 'y%'] - Globe offset in pixels or viewport-relative percentages.\n\n### Rotation & interaction\n- **autoRotate**: number (default: 0.3) - Idle rotation speed. 0 disables; negative reverses.\n- **draggable**: boolean (default: true) - Drag to rotate.\n- **dragAxis**: 'x' | 'y' | 'xy' (default: 'xy') - Axis dragging affects.\n- **dragSpeed**: number (default: 2) - Drag rotation speed.\n- **dragStiffness / dragDamping**: number - Spring momentum tuning.\n- **phi / theta**: number - Initial angles.\n\n### Bindable / callbacks\n- **scrollTo**: bind:scrollTo -> (lat, lng) => void - Imperatively rotate to a coordinate. Pins there until the next drag.\n- **onReady**: () => void - Fires once the first frame renders.\n\n### Styling\n- **class**: string - Applied to the <canvas>. Default theme sizes it 400px square; override for other sizes.\n- **theme**: GlobeThemeProps - Theme overrides (globe part).\n\n## Examples\n\n### Rotate to a city on demand\n```svelte\n<script>\n let goTo;\n</script>\n\n<Globe bind:scrollTo={goTo} />\n<button onclick={() => goTo(48.85, 2.35)}>Paris</button>\n```\n\n### Custom markers and size\n```svelte\n<Globe\n class=\"size-[600px]\"\n markers={[{ location: [48.85, 2.35], size: 0.1 }]}\n/>\n```\n\n## Accessibility\n\nThe globe is a decorative <canvas> with no text content. When it conveys meaning, provide an\naccessible label on a wrapping element or nearby text; keyboard users cannot rotate it.\n\n## Notes\n\n- Renders nothing meaningful without WebGL support.\n- Colors resolve at runtime from CSS variables, so theme tokens recolor the globe on theme toggle.\n- The render loop runs while mounted and stops (and disposes the WebGL context) on unmount.\n";
124
124
  readonly map: "\n# Map Component\n\nAn interactive vector map (pan, zoom, markers, clustering, shapes, geolocation) built on [MapLibre GL](https://maplibre.org/). MapLibre is loaded from a CDN at runtime and never bundled. The base map style is a tokenized Positron style whose colours are derived from your entasis design tokens, so the map adapts to light and dark automatically. Ported from svelte-pro's Map component.\n\n## Basic Usage\n\n```svelte\n<script>\n import { Map } from '../components/Map/index.ts';\n const markers = [\n { id: 'paris', lng: 2.3522, lat: 48.8566, label: 'Paris' },\n { id: 'lyon', lng: 4.8357, lat: 45.7640, label: 'Lyon' }\n ];\n</script>\n\n<Map {markers} controls cluster popup tooltip />\n```\n\n## Props\n\n### Core Props\n- **markers**: MapMarker[] (required) - The markers to place. `MapMarker = { id, lng, lat, label?, description?, popup?, tooltip?, color?, data? }`. Ids must be unique; lng/lat are validated.\n- **center**: [number, number] (default: [2.3522, 48.8566]) - Initial map center (lng, lat).\n- **zoom**: number (default: 5) - Initial zoom level.\n- **bounds**: [w,s,e,n] | [[lng,lat],[lng,lat]] - Fit the view to these bounds (overrides center/zoom on mount).\n- **minZoom** / **maxZoom**: number - Zoom constraints.\n- **size**: 'small' | 'normal' | 'large' (default: 'normal') - Viewport height variant.\n\n### Style Props\n- **styleUrl**: string - Use a specific MapLibre style URL instead of the tokenized default (mutually exclusive with `styles`).\n- **styles**: { positron?: string; dark?: string } - Per-scheme style URL overrides (mutually exclusive with `styleUrl`).\n\n### Interaction Props\n- **interactive**: boolean - Enable/disable pan/zoom interactions.\n- **showAttribution**: boolean (default: true) - Show the attribution control.\n- **fitMarkersOnMount**: boolean - Fit the view to all markers on mount.\n- **fitMarkersPadding**: number - Padding (px) used when fitting markers.\n- **controls**: boolean | ('zoom-in'|'zoom-out'|'fit-markers'|'geolocate'|'reset')[] - Show the control cluster; pass an array to pick which buttons.\n- **controlPosition**: 'top-left'|'top-right'|'bottom-left'|'bottom-right' - Where to dock the controls.\n- **cluster**: boolean | MapClusterConfig - Cluster nearby markers.\n- **geolocation**: boolean | { enabled?, watch?, zoom?, showMarker? } - Locate the user.\n- **popup**: boolean | Snippet - Show a popover on marker click. `true` uses the marker's `popup`/`label`; a snippet receives `{ marker, data, map, lngLat, open, close }`.\n- **tooltip**: boolean | Snippet - Show a hover tooltip. Snippet receives `{ marker, data, map, lngLat, open }`.\n- **shapes**: MapShape[] - Draw circles, polylines, polygons or rectangles.\n\n### Content Props (Snippets)\n- **marker**: Snippet - Custom marker rendering; receives `{ marker, data, map, lngLat }`.\n- **clusterMarker**: Snippet - Custom cluster rendering; receives `{ id, count, coordinates, map, expand, markers, data, feature }`.\n- **userLocationMarker**: Snippet - Custom user-location marker.\n- **controlButton**: Snippet - Custom control button; receives `{ action, label, disabled, active, onclick }`.\n\n### Event Props\n- **onMarkerClick**: (payload) => void\n- **onClusterClick**: (payload) => void\n- **onReady**: (payload) => void - The MapLibre map instance is ready.\n- **onViewChange** / **onMoveEnd** / **onZoomEnd**: (payload) => void\n- **onError**: (error: Error) => void - Loading, style, or runtime errors (otherwise logged to console).\n\n### Advanced Props\n- **class**: string - Additional classes on the map root.\n- **theme**: MapTheme - Theme overrides for the root container.\n\n## Theming\n\nThe base style is generated at runtime from the current entasis `--color-*` tokens and re-generated whenever they change (theme flip, palette swap, runtime token edits): `primary` tints land, buildings and water like a monochrome basemap, `success` paints parks and points of interest, `danger` draws boundaries and `neutral` carries roads and labels on `surface`. To fully override the cartography, pass `styleUrl` or `styles`.\n\n## Accessibility\n\n- Markers are focusable buttons with descriptive labels; popups are keyboard-toggleable and close on Escape/outside-click.\n- The control cluster is a labelled button group.\n- A loading skeleton with `role=\"status\"` is shown until the map's first render; if the map fails to load (e.g. the CDN is unreachable) a `role=\"alert\"` message replaces it instead of hanging on the skeleton.\n\n## Notes\n\n- MapLibre GL JS + its stylesheet load from a CDN on first mount; nothing is bundled. A network connection is required for the library and the default Positron tiles.\n- `styleUrl` and `styles` cannot both be set.\n";
125
125
  readonly markdown: "\n# Markdown Component\n\nRenders Markdown as themed HTML, built for streaming LLM output. Wraps the user's own [svelte-streamdown](https://github.com/) renderer, restyled with entasis design tokens so it matches the rest of the app and adapts to light and dark automatically. Incomplete/streaming Markdown (an unterminated code fence, a half-written table) is handled gracefully — nothing crashes or flashes as chunks arrive.\n\nFenced code blocks render through entasis's own Code component (syntax highlighting + copy button), and \\`mermaid\\` fences render through entasis's Mermaid component (pan/zoom, brand theming, and errorForgiving so a diagram being streamed in doesn't flash errors between chunks).\n\nCard, Stat, Stack, Grid, and GridSpan are also available as built-in MDX components. They can be used directly inside the Markdown source without passing an MDX component map.\n\n## Basic Usage\n\n\\`\\`\\`svelte\n<Markdown content={\"# Hello\\n\\nSome **bold** and _italic_ text.\"} />\n<Markdown content={message} size=\"small\" />\n<Markdown content={doc} size=\"large\" />\n\\`\\`\\`\n\n## Props\n\n### Core Props\n- **content**: string (required) - The Markdown source. Re-renders reactively as it changes; incomplete/streaming Markdown is handled gracefully (safe to bind to a growing string from a token stream).\n- **size**: 'small' | 'normal' | 'large' (default: 'normal') - Type and spacing scale.\n - **small**: compact — smaller headings, tighter block margins, \\`text-sm\\` body. Best for chat messages and dense UI.\n - **normal**: the default reading scale (\\`text-base\\` body, shadcn-equivalent heading sizes).\n - **large**: long-form — larger headings, roomier margins, \\`text-lg\\` body. Best for documents and articles.\n\n### Advanced Props\n- **class**: string - Additional CSS classes on the root wrapper.\n- **theme**: MarkdownThemeProps - Overrides for the root wrapper part (size variants live here).\n- **mdxComponents**: StreamdownProps['mdxComponents'] - Custom MDX components. Matching names override the built-in entasis components.\n- **...streamdown passthroughs**: every other svelte-streamdown prop is forwarded except the ones entasis controls (\\`content\\`, \\`theme\\`, \\`baseTheme\\`, \\`mergeTheme\\`, \\`class\\`, \\`code\\`, \\`mermaid\\`, \\`children\\`, \\`streamdown\\`, \\`element\\`). Use these for streamdown features such as controlling which block types are parsed, link/image handling, and other renderer options — see the svelte-streamdown docs.\n\n## Rendering\n\n- **Block elements**: headings (h1–h6), paragraphs, lists (ordered, unordered, task lists), blockquotes, horizontal rules, tables, images and links are all styled from entasis tokens.\n- **GitHub alerts**: \\`> [!NOTE]\\`, \\`> [!TIP]\\`, \\`> [!IMPORTANT]\\`, \\`> [!WARNING]\\` and \\`> [!CAUTION]\\` render as colored callouts mapped to entasis info/success/primary/warning/danger muted pairings.\n- **Inline code** (\\`codespan\\`): styled as a subtle chip; its text size scales with \\`size\\`.\n- **Code fences**: rendered by entasis's Code component — syntax highlighting and a copy button. The code block sizing follows \\`size\\`.\n- **Mermaid fences**: a fence tagged \\`mermaid\\` renders through entasis's Mermaid component with \\`errorForgiving\\` enabled, so a diagram that is still streaming in holds its last valid frame instead of flashing parse errors. Diagram size maps from the Markdown \\`size\\`.\n\n## Built-in MDX components\n\nThe built-in tags are **Card**, **Stat**, **Stack**, **Grid**, and **GridSpan**:\n\n~~~svelte\nconst content = [\n '<Grid columns={2} gap=\"lg\">',\n '<GridSpan columns=\"full\">',\n '<Card title=\"Release readiness\" variant=\"outline\">',\n '<Stack gap=\"md\">Everything is **ready**.</Stack>',\n '</Card>',\n '</GridSpan>',\n '<Stat label=\"Coverage\" value=\"94%\" trend=\"+3.2%\" trendDirection=\"up\" />',\n '<Stat label=\"Issues\" value=\"3\" trend=\"-8\" trendDirection=\"down\" />',\n '</Grid>'\n].join('\\n');\n\n<Markdown {content} />\n~~~\n\nMDX attributes support string, number, and boolean values. Imperative event handlers and root styling or link props are not forwarded from Markdown content. Caller-provided mdxComponents take priority, and the custom mdx snippet remains the fallback for unknown tags.\n\n## Theming\n\nThe full theme is derived from entasis design tokens (translated from the shadcn preset): borders use \\`border-neutral-muted\\`, muted surfaces use \\`bg-neutral-muted\\`, secondary text uses \\`text-neutral/70\\`, and so on. Because it is token-based, the rendered Markdown adapts to light and dark automatically and stays visually consistent with the rest of the app. Per-instance tweaks to the root wrapper go through the \\`theme\\` prop; deeper element overrides fall back safely to the built-in theme.\n\n## Examples\n\n### Chat message (small)\n\\`\\`\\`svelte\n<Markdown content={assistantMessage} size=\"small\" />\n\\`\\`\\`\n\n### Long-form document (large)\n\\`\\`\\`svelte\n<Markdown content={articleBody} size=\"large\" />\n\\`\\`\\`\n\n### Streaming from an LLM\n\\`\\`\\`svelte\n<script>\n\tlet content = $state('');\n\t// append tokens as they arrive\n\tfor await (const chunk of stream) content += chunk;\n</script>\n<Markdown {content} size=\"small\" />\n\\`\\`\\`\n\n## Accessibility\n\n- Output is semantic HTML (real headings, lists, tables, \\`<a>\\`, \\`<code>\\`), so the document outline and screen-reader navigation work as expected.\n- Code blocks inherit the Code component's accessible copy button (aria-labelled, keyboard operable) and Mermaid inherits its \\`role=\"img\"\\` + aria-label and keyboard-accessible controls.\n- Color is never the only signal for GitHub alerts — each keeps its label/heading text.\n- Task-list checkboxes are disabled and have no text of their own, so each carries an accessible name from the catalog (`taskComplete` / `taskIncomplete`).\n\n## Notes\n\n- Renders reactively: it is safe to bind \\`content\\` to a string that grows over time (token streaming). Partial Markdown between chunks won't throw.\n- The theme deep-merges over svelte-streamdown's built-in base theme, so any element entasis doesn't explicitly restyle still renders sensibly.\n- Code and Mermaid rendering delegate to entasis's own components rather than streamdown's, keeping copy/pan/zoom behavior consistent across the app.\n";
126
- readonly marquee: "\n# Marquee Component\n\nThe Marquee component creates an infinite scrolling animation for displaying content. It automatically duplicates the slot content to create a seamless infinite loop effect and supports customizable speed, direction, fade effects, and pause-on-hover functionality.\n\n## Basic Usage\n\n```svelte\n<script>\n\timport { Marquee } from 'entasis/marquee';\n\t\n\tconst items = [\n\t\t{ id: 1, text: 'Item 1' },\n\t\t{ id: 2, text: 'Item 2' },\n\t\t{ id: 3, text: 'Item 3' }\n\t];\n</script>\n\n<Marquee>\n\t{#each items as item}\n\t\t<div class=\"px-4 py-2 bg-surface rounded\">\n\t\t\t{item.text}\n\t\t</div>\n\t{/each}\n</Marquee>\n```\n\n## Props\n\n### Core Props\n- **children**: Snippet (required)\n - Content to display in the marquee\n - Parent component handles iteration over items\n - Content is automatically duplicated based on `numberOfCopies`\n\n- **direction**: 'left' | 'up' (default: 'left')\n - `'left'`: Horizontal scrolling from right to left\n - `'up'`: Vertical scrolling from bottom to top\n\n- **numberOfCopies**: number (default: 2)\n - Minimum number of times to duplicate the content for a seamless loop\n - The component measures the viewport and first copy, then adds copies when the content is too short to cover the viewport continuously\n - The effective count is bounded to 100 copies\n\n### Animation Props\n- **speed**: 'fast' | 'normal' | 'slow' | number (default: 'fast')\n - Animation speed control\n - `'fast'`: 20 seconds per cycle\n - `'normal'`: 40 seconds per cycle\n - `'slow'`: 80 seconds per cycle\n - `number`: Custom duration in seconds (e.g., `30` for 30 seconds)\n\n- **reverse**: boolean (default: false)\n - If true, animation runs in reverse direction\n - For 'left': scrolls left to right\n - For 'up': scrolls top to bottom\n\n- **pauseOnHover**: boolean (default: true)\n - If true, animation pauses when mouse hovers over the marquee\n - If false, animation continues regardless of hover state\n\n### Visual Props\n- **fade**: boolean (default: true)\n - If true, applies the shared scroll-fade utility at the marquee edges\n - Uses a static scroll-fade mode because the marquee is clipped animation, not a native scroll container\n\n- **size**: 'small' | 'normal' | 'large' (default: 'normal')\n - Controls spacing between duplicated content copies\n - `small`: Compact spacing (gap-2)\n - `normal`: Standard spacing (gap-4)\n - `large`: Expanded spacing (gap-6)\n\n### Styling Props\n- **ref**: HTMLDivElement | null (bindable)\n - Reference to the marquee container\n\n- **class**: string\n - Additional CSS classes for the marquee container\n\n- **innerClass**: string\n - Additional CSS classes for each duplicated copy of the content\n\n- **theme**: MarqueeThemeProps\n - Custom theme overrides for advanced styling\n\n## Examples\n\n### Basic Horizontal Marquee\n\n```svelte\n<script>\n\timport { Marquee } from 'entasis/marquee';\n\t\n\tconst testimonials = [\n\t\t{ author: 'John Doe', quote: 'Amazing product!' },\n\t\t{ author: 'Jane Smith', quote: 'Highly recommended!' },\n\t\t{ author: 'Bob Johnson', quote: 'Best purchase ever!' }\n\t];\n</script>\n\n<Marquee>\n\t{#each testimonials as item}\n\t\t<div class=\"px-8 py-4 bg-surface rounded-lg\">\n\t\t\t<p class=\"text-lg font-semibold\">\"{item.quote}\"</p>\n\t\t\t<p class=\"text-sm text-muted\">— {item.author}</p>\n\t\t</div>\n\t{/each}\n</Marquee>\n```\n\n### Logo Carousel\n\n```svelte\n<script>\n\timport { Marquee } from 'entasis/marquee';\n\t\n\tconst logos = [\n\t\t{ name: 'Company 1', logo: '/logo1.png' },\n\t\t{ name: 'Company 2', logo: '/logo2.png' },\n\t\t{ name: 'Company 3', logo: '/logo3.png' }\n\t];\n</script>\n\n<Marquee speed=\"normal\" size=\"large\">\n\t{#each logos as logo}\n\t\t<img src={logo.logo} alt={logo.name} class=\"h-12 w-auto\" />\n\t{/each}\n</Marquee>\n```\n\n### Vertical Marquee\n\n```svelte\n<Marquee direction=\"up\" class=\"h-[400px]\">\n\t{#each items as item}\n\t\t<div class=\"px-6 py-4 bg-surface rounded\">\n\t\t\t{item.content}\n\t\t</div>\n\t{/each}\n</Marquee>\n```\n\n### With Fade Effect\n\n```svelte\n<Marquee fade={true}>\n\t{#each items as item}\n\t\t<div>{item.content}</div>\n\t{/each}\n</Marquee>\n```\n\n### Reverse Direction\n\n```svelte\n<Marquee reverse={true}>\n\t{#each items as item}\n\t\t<div>{item.content}</div>\n\t{/each}\n</Marquee>\n```\n\n### Custom Speed\n\n```svelte\n<Marquee speed={30}>\n\t{#each items as item}\n\t\t<div>{item.content}</div>\n\t{/each}\n</Marquee>\n```\n\n### Different Sizes\n\n```svelte\n<!-- Small -->\n<Marquee size=\"small\">\n\t{#each items as item}\n\t\t<div>{item.content}</div>\n\t{/each}\n</Marquee>\n\n<!-- Normal (default) -->\n<Marquee size=\"normal\">\n\t{#each items as item}\n\t\t<div>{item.content}</div>\n\t{/each}\n</Marquee>\n\n<!-- Large -->\n<Marquee size=\"large\">\n\t{#each items as item}\n\t\t<div>{item.content}</div>\n\t{/each}\n</Marquee>\n```\n\n### Custom Number of Copies\n\n```svelte\n<!-- More copies for smoother appearance -->\n<Marquee numberOfCopies={4} speed={30}>\n\t{#each items as item}\n\t\t<div>{item.content}</div>\n\t{/each}\n</Marquee>\n```\n\n### Disable Pause on Hover\n\n```svelte\n<Marquee pauseOnHover={false}>\n\t{#each items as item}\n\t\t<div>{item.content}</div>\n\t{/each}\n</Marquee>\n```\n\n### Combined Effects\n\n```svelte\n<Marquee \n\tfade={true}\n\treverse={true}\n\tspeed=\"slow\"\n\tsize=\"large\"\n>\n\t{#each items as item}\n\t\t<div class=\"px-8 py-6 bg-surface rounded-lg\">\n\t\t\t{item.content}\n\t\t</div>\n\t{/each}\n</Marquee>\n```\n\n## Structure\n\nThe Marquee component renders:\n- A container `<div>` with overflow hidden and optional scroll-fade utility classes\n- Multiple copies of the content (default 2) based on `numberOfCopies`\n- Each copy has the animation applied independently\n- Parent component iterates over items in the slot\n\n## Animation Details\n\n- Uses CSS keyframe animations for smooth GPU-accelerated performance\n- Content is duplicated `numberOfCopies` times to create seamless infinite scroll\n- Animation translates each copy by -100% of its width/height plus gap\n- Animation duration is controlled via CSS custom property `--animation-duration`\n- Pause on hover uses CSS `animation-play-state` for instant response\n- Reverse mode uses CSS `direction: reverse` for the animation\n\n## Accessibility\n\n- Uses `role=\"presentation\"` on the container as the marquee is decorative\n- Consider providing alternative static content for screen readers\n- Ensure sufficient color foreground for text content\n- Pause on hover helps users interact with and read content\n- Animation is disabled when the user requests reduced motion\n\n## Notes\n\n- Parent component is responsible for iterating over items (similar to Carousel pattern)\n- Content is automatically duplicated internally based on `numberOfCopies`\n- The component uses CSS animations for optimal performance\n- Custom speed values are specified in seconds (e.g., `30` = 30 seconds per cycle)\n- For vertical marquees, ensure the container has a defined height\n- Increasing `numberOfCopies` creates smoother appearance but uses more DOM elements\n- The fade effect uses the shared scroll-fade utility classes\n\n## Theme Customization\n\nThe Marquee component 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 marquee container styles\n- **inner**: Inner scrolling content container styles\n\n### Available Variants\n\n**root**:\n- base: Base classes for main container\n- Variants:\n - direction: 'left' | 'up' - Scroll direction\n - size: 'small' | 'normal' | 'large' - Gap between duplicated content\n - fade: boolean - Applies shared scroll-fade utility classes\n\n**inner**:\n- base: Base classes for scrolling content\n- Variants:\n - direction: 'left' | 'up' - Animation direction\n - size: 'small' | 'normal' | 'large' - Gap size\n - pauseOnHover: boolean - Pause animation on hover\n - reverse: boolean - Reverse animation direction\n\n### Usage Examples\n\n**Basic Theme Override**:\n```svelte\n<Marquee \n theme={{\n root: {\n base: 'group flex overflow-hidden relative',\n direction: {\n left: 'flex-row'\n },\n size: {\n normal: 'gap-4'\n }\n }\n }}\n>\n {#each items as item}\n <div>{item.content}</div>\n {/each}\n</Marquee>\n```\n\n**Custom Animation Styling**:\n```svelte\n<Marquee \n theme={{\n inner: {\n base: 'flex shrink-0 justify-around whitespace-nowrap',\n direction: {\n left: 'flex-row animate-marquee-left'\n },\n pauseOnHover: {\n true: 'group-hover:[animation-play-state:paused]'\n },\n size: {\n large: 'gap-6'\n }\n }\n }}\n>\n {#each items as item}\n <div>{item.content}</div>\n {/each}\n</Marquee>\n```\n\n**Global Theme Setting**:\n```svelte\n<script>\n import { setMarqueeTheme } from '../components/Marquee/index.ts';\n \n setMarqueeTheme({\n root: {\n base: 'group flex overflow-hidden relative',\n direction: {\n left: 'flex-row'\n },\n size: {\n normal: 'gap-4'\n }\n },\n inner: {\n base: 'flex shrink-0 justify-around whitespace-nowrap',\n direction: {\n left: 'flex-row animate-marquee-left'\n },\n pauseOnHover: {\n true: 'group-hover:[animation-play-state:paused]'\n }\n }\n });\n</script>\n```\n";
126
+ readonly marquee: "\n# Marquee Component\n\nThe Marquee component creates an infinite scrolling animation for displaying content. It automatically duplicates the slot content to create a seamless infinite loop effect and supports customizable speed, direction, fade effects, and pause-on-hover functionality.\n\n## Basic Usage\n\n```svelte\n<script>\n\timport { Marquee } from 'entasis/marquee';\n\t\n\tconst items = [\n\t\t{ id: 1, text: 'Item 1' },\n\t\t{ id: 2, text: 'Item 2' },\n\t\t{ id: 3, text: 'Item 3' }\n\t];\n</script>\n\n<Marquee>\n\t{#each items as item}\n\t\t<div class=\"px-4 py-2 bg-surface rounded\">\n\t\t\t{item.text}\n\t\t</div>\n\t{/each}\n</Marquee>\n```\n\n## Props\n\n### Core Props\n- **children**: Snippet (required)\n - Content to display in the marquee\n - Parent component handles iteration over items\n - Content is automatically duplicated based on `numberOfCopies`\n\n- **direction**: 'left' | 'up' (default: 'left')\n - `'left'`: Horizontal scrolling from right to left\n - `'up'`: Vertical scrolling from bottom to top\n\n- **numberOfCopies**: number (default: 2)\n - Minimum number of times to duplicate the content for a seamless loop\n - The component measures the viewport and first copy, then adds copies when the content is too short to cover the viewport continuously\n - The effective count is bounded to 100 copies\n\n### Animation Props\n- **speed**: 'fast' | 'normal' | 'slow' | number (default: 'fast')\n - Animation speed control\n - `'fast'`: 20 seconds per cycle\n - `'normal'`: 40 seconds per cycle\n - `'slow'`: 80 seconds per cycle\n - `number`: Custom duration in seconds (e.g., `30` for 30 seconds)\n\n- **reverse**: boolean (default: false)\n - If true, animation runs in reverse direction\n - For 'left': scrolls left to right\n - For 'up': scrolls top to bottom\n\n- **pauseOnHover**: boolean (default: true)\n - If true, animation pauses when mouse hovers over the marquee\n - If false, animation continues regardless of hover state\n\n### Visual Props\n- **fade**: boolean (default: true)\n - If true, applies the shared scroll-fade utility at the marquee edges\n - Uses a static scroll-fade mode because the marquee is clipped animation, not a native scroll container\n\n- **size**: 'small' | 'normal' | 'large' (default: 'normal')\n - Sets the marquee's one spacing value, `--gap`, from the theme spacing scale\n - `small`: `--space-md`\n - `normal`: `--space-xl`\n - `large`: `--layout-space-md`\n - The copies, the items inside each copy and the loop step all use `--gap`, so the loop stays\n seamless at every size and theme spacing. To respace a marquee, set `--gap` once on the root\n (`class=\"[--gap:var(--space-lg)]\"`); never redeclare it on the copies.\n\n### Styling Props\n- **ref**: HTMLDivElement | null (bindable)\n - Reference to the marquee container\n\n- **class**: string\n - Additional CSS classes for the marquee container\n\n- **innerClass**: string\n - Additional CSS classes for each duplicated copy of the content\n\n- **theme**: MarqueeThemeProps\n - Custom theme overrides for advanced styling\n\n## Examples\n\n### Basic Horizontal Marquee\n\n```svelte\n<script>\n\timport { Marquee } from 'entasis/marquee';\n\t\n\tconst testimonials = [\n\t\t{ author: 'John Doe', quote: 'Amazing product!' },\n\t\t{ author: 'Jane Smith', quote: 'Highly recommended!' },\n\t\t{ author: 'Bob Johnson', quote: 'Best purchase ever!' }\n\t];\n</script>\n\n<Marquee>\n\t{#each testimonials as item}\n\t\t<div class=\"px-8 py-4 bg-surface rounded-lg\">\n\t\t\t<p class=\"text-lg font-semibold\">\"{item.quote}\"</p>\n\t\t\t<p class=\"text-sm text-muted\">— {item.author}</p>\n\t\t</div>\n\t{/each}\n</Marquee>\n```\n\n### Logo Carousel\n\n```svelte\n<script>\n\timport { Marquee } from 'entasis/marquee';\n\t\n\tconst logos = [\n\t\t{ name: 'Company 1', logo: '/logo1.png' },\n\t\t{ name: 'Company 2', logo: '/logo2.png' },\n\t\t{ name: 'Company 3', logo: '/logo3.png' }\n\t];\n</script>\n\n<Marquee speed=\"normal\" size=\"large\">\n\t{#each logos as logo}\n\t\t<img src={logo.logo} alt={logo.name} class=\"h-12 w-auto\" />\n\t{/each}\n</Marquee>\n```\n\n### Vertical Marquee\n\n```svelte\n<Marquee direction=\"up\" class=\"h-[400px]\">\n\t{#each items as item}\n\t\t<div class=\"px-6 py-4 bg-surface rounded\">\n\t\t\t{item.content}\n\t\t</div>\n\t{/each}\n</Marquee>\n```\n\n### With Fade Effect\n\n```svelte\n<Marquee fade={true}>\n\t{#each items as item}\n\t\t<div>{item.content}</div>\n\t{/each}\n</Marquee>\n```\n\n### Reverse Direction\n\n```svelte\n<Marquee reverse={true}>\n\t{#each items as item}\n\t\t<div>{item.content}</div>\n\t{/each}\n</Marquee>\n```\n\n### Custom Speed\n\n```svelte\n<Marquee speed={30}>\n\t{#each items as item}\n\t\t<div>{item.content}</div>\n\t{/each}\n</Marquee>\n```\n\n### Different Sizes\n\n```svelte\n<!-- Small -->\n<Marquee size=\"small\">\n\t{#each items as item}\n\t\t<div>{item.content}</div>\n\t{/each}\n</Marquee>\n\n<!-- Normal (default) -->\n<Marquee size=\"normal\">\n\t{#each items as item}\n\t\t<div>{item.content}</div>\n\t{/each}\n</Marquee>\n\n<!-- Large -->\n<Marquee size=\"large\">\n\t{#each items as item}\n\t\t<div>{item.content}</div>\n\t{/each}\n</Marquee>\n```\n\n### Custom Number of Copies\n\n```svelte\n<!-- More copies for smoother appearance -->\n<Marquee numberOfCopies={4} speed={30}>\n\t{#each items as item}\n\t\t<div>{item.content}</div>\n\t{/each}\n</Marquee>\n```\n\n### Disable Pause on Hover\n\n```svelte\n<Marquee pauseOnHover={false}>\n\t{#each items as item}\n\t\t<div>{item.content}</div>\n\t{/each}\n</Marquee>\n```\n\n### Combined Effects\n\n```svelte\n<Marquee \n\tfade={true}\n\treverse={true}\n\tspeed=\"slow\"\n\tsize=\"large\"\n>\n\t{#each items as item}\n\t\t<div class=\"px-8 py-6 bg-surface rounded-lg\">\n\t\t\t{item.content}\n\t\t</div>\n\t{/each}\n</Marquee>\n```\n\n## Structure\n\nThe Marquee component renders:\n- A container `<div>` with overflow hidden and optional scroll-fade utility classes\n- Multiple copies of the content (default 2) based on `numberOfCopies`\n- Each copy has the animation applied independently\n- Parent component iterates over items in the slot\n\n## Animation Details\n\n- Uses CSS keyframe animations for smooth GPU-accelerated performance\n- Content is duplicated `numberOfCopies` times to create seamless infinite scroll\n- Animation translates each copy by -100% of its width/height plus `--gap`, the same value the copies are spaced with\n- Animation duration is controlled via CSS custom property `--animation-duration`\n- Pause on hover uses CSS `animation-play-state` for instant response\n- Reverse mode uses CSS `direction: reverse` for the animation\n\n## Accessibility\n\n- Uses `role=\"presentation\"` on the container as the marquee is decorative\n- Consider providing alternative static content for screen readers\n- Ensure sufficient color foreground for text content\n- Pause on hover helps users interact with and read content\n- Animation is disabled when the user requests reduced motion; the still row then fades only its trailing edge\n\n## Notes\n\n- Parent component is responsible for iterating over items (similar to Carousel pattern)\n- Content is automatically duplicated internally based on `numberOfCopies`\n- The component uses CSS animations for optimal performance\n- Custom speed values are specified in seconds (e.g., `30` = 30 seconds per cycle)\n- For vertical marquees, ensure the container has a defined height\n- Increasing `numberOfCopies` creates smoother appearance but uses more DOM elements\n- The fade effect uses the shared scroll-fade utility classes\n\n## Theme Customization\n\nThe Marquee component 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 marquee container styles\n- **inner**: Inner scrolling content container styles\n\n### Available Variants\n\n**root**:\n- base: Base classes for main container\n- Variants:\n - direction: 'left' | 'up' - Scroll direction\n - size: 'small' | 'normal' | 'large' - Gap between duplicated content\n - fade: boolean - Applies shared scroll-fade utility classes\n\n**inner**:\n- base: Base classes for scrolling content\n- Variants:\n - direction: 'left' | 'up' - Animation direction\n - size: 'small' | 'normal' | 'large' - Gap size\n - pauseOnHover: boolean - Pause animation on hover\n - reverse: boolean - Reverse animation direction\n\n### Usage Examples\n\n**Basic Theme Override**:\n```svelte\n<Marquee \n theme={{\n root: {\n base: 'group flex overflow-hidden relative',\n direction: {\n left: 'flex-row'\n },\n size: {\n normal: '[--gap:var(--space-lg)]'\n }\n }\n }}\n>\n {#each items as item}\n <div>{item.content}</div>\n {/each}\n</Marquee>\n```\n\n**Custom Animation Styling**:\n```svelte\n<Marquee \n theme={{\n inner: {\n base: 'flex shrink-0 justify-around whitespace-nowrap',\n direction: {\n left: 'flex-row animate-marquee-left'\n },\n pauseOnHover: {\n true: 'group-hover:[animation-play-state:paused]'\n }\n }\n }}\n>\n {#each items as item}\n <div>{item.content}</div>\n {/each}\n</Marquee>\n```\n\n**Global Theme Setting**:\n```svelte\n<script>\n import { setMarqueeTheme } from '../components/Marquee/index.ts';\n \n setMarqueeTheme({\n root: {\n base: 'group flex overflow-hidden relative',\n direction: {\n left: 'flex-row'\n },\n size: {\n normal: '[--gap:var(--space-lg)]'\n }\n },\n inner: {\n base: 'flex shrink-0 justify-around whitespace-nowrap',\n direction: {\n left: 'flex-row animate-marquee-left'\n },\n pauseOnHover: {\n true: 'group-hover:[animation-play-state:paused]'\n }\n }\n });\n</script>\n```\n";
127
127
  readonly mermaid: "\n# Mermaid Component\n\nRenders a [Mermaid](https://mermaid.js.org/) diagram (flowchart, sequence, gantt, pie, class, state, ER, etc.) from a code string, with pan, zoom, fullscreen and download controls. The diagram is themed with our design tokens so it adapts to light and dark automatically. Ported from svelte-streamdown's Mermaid element, rebuilt on the entasis state/theme system.\n\n## Basic Usage\n\n```svelte\n<Mermaid chart={`graph TD; A[Start] --> B{Choice}; B -->|Yes| C[OK]; B -->|No| D[Stop];`} />\n<Mermaid chart={sequenceCode} size=\"large\" />\n```\n\n## Props\n\n### Core Props\n- **chart**: string (required) - The mermaid diagram source. Sanitized before rendering (BOM, zero-width/control chars, smart quotes, CRLF, HTML entities, flowchart arrow spacing, trailing semicolons). Re-renders reactively when it changes.\n- **config**: MermaidConfig - Extra mermaid config merged over the defaults (`theme: 'base'`, `securityLevel: 'strict'`, brand `themeVariables`, flowchart `curve: 'basis'`). Pass `themeVariables` to override individual mapped colors, or `fontFamily` to change the diagram font.\n- **size**: 'small' | 'normal' | 'large' (default: 'normal') - The height of the diagram viewport.\n\n### Interaction Props\n- **controls**: boolean | { fit?, zoomIn?, zoomOut?, expand?, download? } (default: true) - The floating controls to show. `true` shows all, `false` hides the bar, an object toggles individual controls.\n- **mouseWheelZoom**: boolean (default: true) - Enable mouse-wheel zoom over the diagram (activated after a short hover so page scroll isn't hijacked). Pan-drag and the zoom buttons work regardless.\n- **touchPan**: boolean (default: false) - Capture single-finger touch as a pan. Off inline so the diagram never traps page scroll; the fullscreen dialog turns it on. Pinch-zoom always works.\n- **errorForgiving**: boolean (default: false) - Swallow transient parse/render errors and keep the last valid diagram on screen instead of showing the error overlay. For token-streaming (LLM output), where the source is incomplete/invalid between chunks. Errors are still reported via `onError`.\n\n### Event Props\n- **onRender**: (state: MermaidState) => void - Called after each successful render.\n- **onError**: (error: Error) => void - Called when loading mermaid, parsing, or rendering fails.\n\n### Advanced Props\n- **class**: string - Additional CSS classes on the root element (the panzoom viewport).\n- **theme**: MermaidTheme - Theme overrides (root, container, svg, buttons, error, skeleton).\n\n## Interactions\n\n- **Pan**: drag the diagram.\n- **Zoom**: the +/− buttons, mouse-wheel (after a short hover), pinch, or double-click.\n- **Fit**: the fit-to-view button re-centers and scales the diagram to the viewport.\n- **Fullscreen**: the expand button opens the same diagram in a `fullScreen` Dialog (focus trap, Escape and the close button exit, single-finger touch pans inside it).\n- **Download**: serializes the rendered diagram to a standalone .svg file.\n\n## Brand theming\n\nMermaid is initialized with `theme: 'base'` and a `themeVariables` object whose values are read from our `--color-*` design tokens at render time (mermaid reads them as concrete colors, not live CSS vars). The diagram re-renders when the active theme changes, so it stays in sync with light/dark. Mapping: `primaryColor -> --color-primary-muted`, `primaryTextColor/textColor -> --color-neutral`, `primaryBorderColor/nodeBorder -> --color-primary`, `lineColor -> --color-neutral-muted`, `background -> --color-surface`, `mainBkg -> --color-surface-raised`, `secondaryColor -> --color-secondary`, `tertiaryColor -> --color-neutral-muted`, plus note/cluster/title colors and `fontFamily`.\n\n## Examples\n\n### Live editable diagram\n```svelte\n<script>\n\tlet chart = $state('graph LR; A-->B; B-->C;');\n</script>\n<textarea bind:value={chart}></textarea>\n<Mermaid {chart} />\n```\n\n### Only some controls\n```svelte\n<Mermaid {chart} controls={{ zoomIn: true, zoomOut: true, fit: true, download: false, expand: false }} />\n```\n\n## Accessibility\n\n- The controls are Buttons with aria-labels and are keyboard accessible; the download uses safe blob/data URLs.\n- The render host has `role=\"img\"` with an aria-label; the error surface uses `role=\"alert\"`.\n\n## Notes\n\n- **mermaid is not bundled**: it is loaded from cdnjs at runtime, on the client only (mermaid **11.12.0**, the self-contained UMD build). The component is SSR-safe and shows a skeleton while loading. A strict Content-Security-Policy must allow `cdnjs.cloudflare.com` in `script-src`.\n- On a load failure or a mermaid parse/render error, an error surface is shown (danger tokens) with the message — `securityLevel: 'strict'` + `suppressErrorRendering` keep mermaid from injecting its own error DOM.\n- mermaid leaves temporary measuring containers in the body; they are removed after each render and on destroy.\n";
128
128
  readonly 'qr-code': "\n# QRCode Component\n\nThe QRCode component renders a customizable QR code as an SVG. It supports theme sizes and colors, gradients, custom shapes for data modules and finder patterns, an embedded center image, and downloading as SVG, PNG or JPEG. Ported from react-qr-code (https://github.com/LGLabGreg/react-qr-code).\n\n## Basic Usage\n\n```svelte\n<QRCode value=\"https://example.com\" />\n<QRCode value=\"https://example.com\" size=\"large\" color=\"primary\" />\n```\n\n## Props\n\n### Core Props\n- **value**: string | string[] (required) - The value to encode. An array of strings represents multiple segments to further optimize the QR Code.\n- **size**: 'small' | 'normal' | 'large' (default: 'normal')\n - small: 96px (size-24)\n - normal: 128px (size-32)\n - large: 192px (size-48)\n- **color**: Colors (default: 'neutral') - Theme color of the modules and finder patterns. Applied through `currentColor`, so it adapts to the active theme.\n- **level**: 'L' | 'M' | 'Q' | 'H' (default: 'M') - The Error Correction Level.\n- **minVersion**: number (default: 1) - Minimum QR version (1-40) used as the lower bound when encoding.\n- **boostLevel**: boolean (default: true) - Allow raising the Error Correction Level when it does not increase the version.\n- **marginSize**: number (default: 4) - Number of modules used as margin (quiet zone). The QR specification requires 4.\n\n### Styling Props\n- **background**: string | GradientSettings - Background color or gradient. Transparent when not provided.\n- **gradient**: GradientSettings - Gradient applied to data modules and finder patterns. Overrides `color` and the settings colors.\n- **dataModulesSettings**: { color?, style?, randomSize?, scale?, lineWidth? } - Data module rendering.\n - style: 'square' | 'square-sm' | 'pinched-square' | 'rounded' | 'leaf' | 'vertical-line' | 'horizontal-line' | 'circuit-board' | 'circle' | 'diamond' | 'star' | 'heart' | 'hashtag'\n- **finderPatternOuterSettings**: { color?, style? } - Outer finder pattern rendering.\n - style: 'square' | 'pinched-square' | 'rounded-sm' | 'rounded' | 'rounded-lg' | 'circle' | 'inpoint-sm' | 'inpoint' | 'inpoint-lg' | 'outpoint-sm' | 'outpoint' | 'outpoint-lg' | 'leaf-sm' | 'leaf' | 'leaf-lg'\n- **finderPatternInnerSettings**: { color?, style? } - Inner finder pattern rendering.\n - style: same as outer, plus 'diamond' | 'star' | 'heart' | 'hashtag' | 'microchip'\n- **imageSettings**: { src, width, height, excavate?, x?, y?, opacity?, crossOrigin? } - Embedded center image. `excavate` clears the modules behind the image. Pixel values are relative to the nominal size of the QR code.\n- **class**: string - Additional CSS classes on the SVG element.\n- **theme**: QRCodeTheme - Theme overrides.\n\n### Accessibility Props\n- **label**: string (default: 'QR Code') - Accessible label of the SVG.\n\n### Advanced Props\n- **ref**: SVGSVGElement | null (bindable) - The rendered SVG element.\n\n## Methods\n\nBind the component instance to access:\n\n- **download(options?)**: Downloads the QR code.\n - options.name: string (default: 'qr-code') - File name without extension.\n - options.format: 'svg' | 'png' | 'jpeg' (default: 'svg')\n - options.dimension: number (default: 500) - Exported file width and height in pixels.\n\n```svelte\n<script>\n\tlet qr;\n</script>\n\n<QRCode bind:this={qr} value=\"https://example.com\" />\n<Button onclick={() => qr.download({ format: 'png' })}>Download</Button>\n```\n\n## Examples\n\n### Gradient with custom shapes\n```svelte\n<QRCode\n\tvalue=\"https://example.com\"\n\tgradient={{\n\t\ttype: 'linear',\n\t\trotation: 45,\n\t\tstops: [\n\t\t\t{ offset: '0%', color: '#6d78d5' },\n\t\t\t{ offset: '100%', color: '#d56d6d' }\n\t\t]\n\t}}\n\tdataModulesSettings={{ style: 'circle' }}\n\tfinderPatternOuterSettings={{ style: 'rounded' }}\n\tfinderPatternInnerSettings={{ style: 'circle' }}\n/>\n```\n\n### Embedded image\n```svelte\n<QRCode\n\tvalue=\"https://example.com\"\n\tlevel=\"H\"\n\timageSettings={{ src: '/logo.png', width: 24, height: 24, excavate: true }}\n/>\n```\n\n## Accessibility\n\n- The SVG has `role=\"img\"` and an `aria-label` (customizable via the `label` prop).\n\n## Notes\n\n- Colors default to `currentColor`, driven by the `color` prop theme classes; downloads resolve the computed color so exports match the on-screen theme.\n- Keep enough contrast between the modules and the surface behind the QR code, and prefer `level=\"H\"` when embedding an image, otherwise the code may not scan.\n- `randomSize` and low `scale`/`lineWidth` values in `dataModulesSettings` may degrade scannability.\n";
129
129
  readonly hitbox: "\n# Hitbox Component\n\nHitbox enlarges the pointer target of an existing interactive element without changing its visible dimensions or semantics. It renders one transparent, aria-hidden span centered over its positioned parent; pointer events bubble to the parent button or link.\n\n## Usage\n\n```svelte\n<script>\n import { Hitbox } from '../components/Hitbox/index.ts';\n</script>\n\n<button type=\"button\" aria-label=\"Select page\" class=\"relative size-2 rounded-full bg-primary\">\n <Hitbox size=\"normal\" />\n</button>\n```\n\nThe interactive parent must establish a positioning context and allow overflow. Adjacent controls should reserve enough layout space for their hitboxes so targets do not overlap.\n\n## Props\n\n- **size**: 'small' | 'normal' | 'large' (default: 'normal') - Target dimensions: 20px, 24px, or 28px.\n- **ref**: HTMLSpanElement | null - Bindable reference to the transparent span.\n- **class**: string - Classes applied to the span.\n- **theme**: HitboxThemeProps - Theme overrides for the root part.\n\n## Accessibility\n\nHitbox is aria-hidden and does not create another focusable element. The parent remains responsible for its accessible name, keyboard behavior, disabled state, and focus indication.\n\n## Theme\n\n- **root**: Transparent centered target surface and size variants.\n";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "entasis",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
4
4
  "license": "MIT",
5
5
  "scripts": {
6
6
  "dev": "vite dev",