entasis 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/components/FloatingWindow/FloatingWindow.svelte +26 -2
  2. package/dist/components/FloatingWindow/floatingWindow.dock.svelte.js +11 -2
  3. package/dist/components/FloatingWindow/floatingWindow.mcp.d.ts +1 -1
  4. package/dist/components/FloatingWindow/floatingWindow.mcp.js +7 -4
  5. package/dist/components/FloatingWindow/floatingWindow.props.d.ts +6 -0
  6. package/dist/components/FloatingWindow/floatingWindow.state.svelte.d.ts +3 -0
  7. package/dist/components/FloatingWindow/floatingWindow.state.svelte.js +18 -4
  8. package/dist/components/FloatingWindow/floatingWindow.theme.d.ts +3 -0
  9. package/dist/components/FloatingWindow/floatingWindow.theme.js +20 -7
  10. package/dist/components/Form/File/FileInput.svelte +6 -42
  11. package/dist/components/Sidebar/Sidebar.svelte +52 -9
  12. package/dist/components/Sidebar/Sidebar.svelte.d.ts +1 -1
  13. package/dist/components/Sidebar/SidebarMenuItem.svelte +19 -2
  14. package/dist/components/Sidebar/SidebarMenuItem.svelte.d.ts +4 -0
  15. package/dist/components/Sidebar/SidebarPanel.svelte +199 -88
  16. package/dist/components/Sidebar/SidebarPanel.svelte.d.ts +3 -0
  17. package/dist/components/Sidebar/SidebarViewStage.svelte +103 -0
  18. package/dist/components/Sidebar/SidebarViewStage.svelte.d.ts +18 -0
  19. package/dist/components/Sidebar/index.d.ts +1 -1
  20. package/dist/components/Sidebar/sidebar.mcp.d.ts +1 -1
  21. package/dist/components/Sidebar/sidebar.mcp.js +18 -1
  22. package/dist/components/Sidebar/sidebar.props.d.ts +56 -1
  23. package/dist/components/Sidebar/sidebar.state.svelte.d.ts +4 -0
  24. package/dist/components/Sidebar/sidebar.state.svelte.js +9 -1
  25. package/dist/components/Sidebar/sidebar.theme.d.ts +116 -5
  26. package/dist/components/Sidebar/sidebar.theme.js +42 -0
  27. package/dist/components/Sidebar/sidebar.views.svelte.d.ts +138 -0
  28. package/dist/components/Sidebar/sidebar.views.svelte.js +303 -0
  29. package/dist/components/Theme/theme.floatingWindows.d.ts +6 -1
  30. package/dist/components/Theme/theme.floatingWindows.js +7 -2
  31. package/dist/components/Theme/theme.mcp.d.ts +1 -1
  32. package/dist/components/Theme/theme.mcp.js +1 -0
  33. package/dist/generated/componentContract.d.ts +1 -1
  34. package/dist/generated/componentContract.js +1 -0
  35. package/dist/generated/componentMcpRegistry.d.ts +4 -4
  36. package/dist/tailwind/index.mcp.d.ts +1 -1
  37. package/dist/tailwind/index.mcp.js +4 -0
  38. package/dist/tailwind/scales.js +11 -3
  39. package/dist/tailwind/spacing.js +17 -0
  40. package/dist/utils/cva/merge.d.ts +7 -0
  41. package/dist/utils/cva/merge.js +6 -1
  42. package/dist/utils/pointerDrag.js +7 -1
  43. package/package.json +1 -1
@@ -0,0 +1,303 @@
1
+ import { untrack } from 'svelte';
2
+ import { easingFunctions } from '../../transitions/easingFunctions.js';
3
+ import { createPointerDrag } from '../../utils/pointerDrag.js';
4
+ import { createBindableValue } from '../../utils/state.svelte.js';
5
+ /** The header and footer props a view can set; together they are the panel's chrome. */
6
+ const chromeProps = [
7
+ 'header',
8
+ 'headerButton',
9
+ 'search',
10
+ 'headerMenu',
11
+ 'footerButton',
12
+ 'footerMenu',
13
+ 'footer'
14
+ ];
15
+ /** The view and its ancestors, nearest first. A missing parent or a parent cycle ends the chain. */
16
+ export const viewChain = (views, id) => {
17
+ const chain = [];
18
+ for (let key = id; key !== undefined && key in views;) {
19
+ if (chain.includes(key))
20
+ break;
21
+ chain.push(key);
22
+ key = views[key].parent;
23
+ }
24
+ return chain;
25
+ };
26
+ /**
27
+ * Resolves one header or footer prop for a view: the nearest view in the chain that sets it
28
+ * (`null` counts, and clears it), else the Sidebar. `owner` names where it came from, so two
29
+ * views share the slot exactly when it resolves to the same owner.
30
+ */
31
+ export const resolveSlot = (views, id, slot, root) => {
32
+ const owner = viewChain(views, id).find((key) => views[key][slot] !== undefined);
33
+ const value = owner === undefined ? root[slot] : views[owner][slot];
34
+ return { owner: owner ?? null, value: (value ?? undefined) };
35
+ };
36
+ const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
37
+ const cssLength = (value) => typeof value === 'number' ? `${value}px` : (value ?? '0px');
38
+ export class SidebarViewsState {
39
+ options;
40
+ swipe = $state(null);
41
+ rtl = $state(false);
42
+ #selected;
43
+ #shown;
44
+ #lastChange = {
45
+ direction: 'none'
46
+ };
47
+ constructor(options) {
48
+ this.options = options;
49
+ this.#selected = createBindableValue(() => options.view, (view) => options.setViewProp(view), () => options.defaultView);
50
+ // A committed swipe lets go once the view it asked for is on screen; any other view change
51
+ // (a route, a parent update) ends the swipe on the spot.
52
+ $effect(() => {
53
+ const swipe = this.swipe;
54
+ if (!swipe)
55
+ return;
56
+ const current = this.current;
57
+ if ((swipe.settled && current === swipe.to) ||
58
+ (current !== swipe.from && current !== swipe.to))
59
+ this.swipe = null;
60
+ });
61
+ this.#shown = untrack(() => this.current);
62
+ }
63
+ get enabled() {
64
+ return !!this.options.views && Object.keys(this.options.views).length > 0;
65
+ }
66
+ /** The view on screen: the selected one when it exists, else `defaultView`, else the first. */
67
+ get current() {
68
+ const views = this.options.views;
69
+ if (!views)
70
+ return undefined;
71
+ const selected = this.#selected.value;
72
+ if (selected !== undefined && selected in views)
73
+ return selected;
74
+ const fallback = this.options.defaultView;
75
+ return fallback !== undefined && fallback in views ? fallback : Object.keys(views)[0];
76
+ }
77
+ depth(id) {
78
+ return this.options.views ? viewChain(this.options.views, id).length - 1 : 0;
79
+ }
80
+ parentOf(id) {
81
+ const parent = this.options.views?.[id]?.parent;
82
+ return parent !== undefined && parent in (this.options.views ?? {}) ? parent : undefined;
83
+ }
84
+ labelOf(id) {
85
+ return this.options.views?.[id]?.label;
86
+ }
87
+ /**
88
+ * The last change of the current view and its direction, read by the layer transitions when
89
+ * they start. Deeper slides forward and shallower slides back; at one depth (sections), the
90
+ * later view in `views` is forward, like pages in order.
91
+ */
92
+ get lastChange() {
93
+ const to = this.current;
94
+ if (to === this.#shown)
95
+ return this.#lastChange;
96
+ const from = this.#shown;
97
+ this.#shown = to;
98
+ if (from === undefined || to === undefined) {
99
+ this.#lastChange = { from, to, direction: 'none' };
100
+ return this.#lastChange;
101
+ }
102
+ const order = Object.keys(this.options.views ?? {});
103
+ const delta = this.depth(to) - this.depth(from) || order.indexOf(to) - order.indexOf(from);
104
+ this.#lastChange = { from, to, direction: delta < 0 ? 'back' : 'forward' };
105
+ return this.#lastChange;
106
+ }
107
+ setView = (view) => {
108
+ if (!this.options.views || !(view in this.options.views) || view === this.current)
109
+ return;
110
+ this.#selected.value = view;
111
+ this.options.onViewChange?.(view);
112
+ };
113
+ /** A view's body. */
114
+ body(view) {
115
+ const entry = this.options.views?.[view];
116
+ return { items: entry?.items, content: entry?.content };
117
+ }
118
+ /** A view's header and footer props, each from the nearest view that sets it, else the Sidebar. */
119
+ chrome(view) {
120
+ const chrome = {};
121
+ for (const prop of chromeProps) {
122
+ const { value } = resolveSlot(this.options.views ?? {}, view, prop, this.options.root);
123
+ if (value != null)
124
+ Object.assign(chrome, { [prop]: value });
125
+ }
126
+ return chrome;
127
+ }
128
+ /**
129
+ * The layers a stage renders, bottom first. Outside a swipe that is the current view alone;
130
+ * during one it is the parent under the view being swiped away. Two views that share a layer
131
+ * key collapse to one layer that never moves: the panel's key is where its chrome comes from,
132
+ * so views with the same header and footer keep one panel and only their bodies slide.
133
+ */
134
+ layers(kind) {
135
+ const current = this.current;
136
+ if (current === undefined)
137
+ return [];
138
+ const swipe = this.swipe;
139
+ const entries = swipe
140
+ ? [
141
+ { view: swipe.to, role: 'under' },
142
+ { view: swipe.from, role: 'over' }
143
+ ]
144
+ : [{ view: current }];
145
+ const layers = [];
146
+ for (const entry of entries) {
147
+ const key = kind === 'body' ? entry.view : this.#chromeKey(entry.view);
148
+ const shared = layers.find((layer) => layer.key === key);
149
+ if (shared)
150
+ shared.role = undefined;
151
+ else
152
+ layers.push({ key, ...entry });
153
+ }
154
+ return layers;
155
+ }
156
+ #chromeKey(view) {
157
+ const views = this.options.views ?? {};
158
+ return chromeProps
159
+ .map((prop) => resolveSlot(views, view, prop, this.options.root).owner ?? '')
160
+ .join('|');
161
+ }
162
+ /**
163
+ * A swipe layer's drag position, as `translate` / `opacity` styles; empty at rest. The view
164
+ * under the finger stays opaque and follows it; its parent comes from `travel` toward the
165
+ * start and from `opacity`, the same way the view motion brings a view in.
166
+ */
167
+ swipeStyle(role, travel, opacity) {
168
+ const swipe = this.swipe;
169
+ if (!swipe || !role)
170
+ return {};
171
+ const sign = this.rtl ? -1 : 1;
172
+ const rest = `calc(${cssLength(travel)} * ${-sign})`;
173
+ const progress = swipe.width ? swipe.offset / swipe.width : 0;
174
+ if (swipe.phase === 'drag') {
175
+ if (role === 'over')
176
+ return {
177
+ translate: `${swipe.offset * sign}px`,
178
+ opacity: 1 - (1 - opacity) * progress
179
+ };
180
+ return {
181
+ translate: `calc(${cssLength(travel)} * ${-sign * (1 - progress)})`,
182
+ opacity: opacity + (1 - opacity) * progress
183
+ };
184
+ }
185
+ // Settling: a committed view leaves to the inline end; a cancelled parent returns under it.
186
+ if (role === 'over')
187
+ return swipe.phase === 'commit' ? { translate: `${100 * sign}%`, opacity } : {};
188
+ return swipe.phase === 'cancel' ? { translate: rest, opacity } : {};
189
+ }
190
+ get canSwipeBack() {
191
+ const current = this.current;
192
+ return this.options.isMobile && current !== undefined && !!this.parentOf(current);
193
+ }
194
+ /**
195
+ * The mobile drawer dismisses on a swipe toward its own edge. When going back points the same
196
+ * way (a right drawer in LTR, a left one in RTL), the view body keeps the gesture.
197
+ */
198
+ get ownsDrawerSwipe() {
199
+ return this.canSwipeBack && (this.rtl ? -1 : 1) === (this.options.side === 'right' ? 1 : -1);
200
+ }
201
+ /** Called by the body stage once a settling swipe has had its motion duration. */
202
+ settleSwipe() {
203
+ const swipe = this.swipe;
204
+ if (!swipe || swipe.phase === 'drag')
205
+ return;
206
+ if (swipe.phase === 'cancel' || this.current === swipe.to)
207
+ this.swipe = null;
208
+ else
209
+ swipe.settled = true;
210
+ // ponytail: a controlled `view` that ignores `onViewChange` leaves the swipe parked off
211
+ // screen until some view change lands; fine for a consumer bug, revisit if one shows up.
212
+ }
213
+ /** Back swipe on the view body: toward the inline end, anywhere on the body, mobile only. */
214
+ swipeAttachment = (node) => {
215
+ this.rtl = getComputedStyle(node).direction === 'rtl';
216
+ let candidate = false;
217
+ let sign = 1;
218
+ let lastX = 0;
219
+ let lastTime = 0;
220
+ let velocity = 0;
221
+ const release = (cancelled, timeStamp) => {
222
+ candidate = false;
223
+ const swipe = this.swipe;
224
+ if (swipe?.phase !== 'drag')
225
+ return;
226
+ // A long stationary hold is not a flick: the last velocity sample may be stale.
227
+ if (timeStamp - lastTime > 100)
228
+ velocity = 0;
229
+ const commit = !cancelled && (velocity > 0.4 || (swipe.offset > swipe.width * 0.35 && velocity > -0.2));
230
+ swipe.phase = commit ? 'commit' : 'cancel';
231
+ if (commit)
232
+ this.setView(swipe.to);
233
+ };
234
+ return createPointerDrag({
235
+ disabled: () => !this.canSwipeBack || this.swipe !== null,
236
+ // The body is a region full of rows, not a handle: a press that never travels keeps its click.
237
+ capture: 'on-activate',
238
+ onDown: ({ event }) => {
239
+ candidate = true;
240
+ this.rtl = getComputedStyle(node).direction === 'rtl';
241
+ sign = this.rtl ? -1 : 1;
242
+ lastX = event.clientX;
243
+ lastTime = event.timeStamp;
244
+ velocity = 0;
245
+ },
246
+ // Horizontal toward the inline end, clearly more than vertical: anything else is a scroll.
247
+ shouldActivate: ({ deltaX, deltaY }) => {
248
+ if (!candidate)
249
+ return false;
250
+ const forward = deltaX * sign;
251
+ if (forward > 8 && forward > Math.abs(deltaY) * 1.5)
252
+ return true;
253
+ if (Math.abs(deltaX) > 8 || Math.abs(deltaY) > 8)
254
+ candidate = false;
255
+ return false;
256
+ },
257
+ onStart: () => {
258
+ const from = this.current;
259
+ const to = from === undefined ? undefined : this.parentOf(from);
260
+ if (from === undefined || to === undefined)
261
+ return false;
262
+ this.swipe = { from, to, phase: 'drag', offset: 0, width: node.offsetWidth };
263
+ },
264
+ onMove: ({ event, deltaX }) => {
265
+ const swipe = this.swipe;
266
+ if (swipe?.phase !== 'drag')
267
+ return;
268
+ swipe.offset = clamp(deltaX * sign, 0, swipe.width);
269
+ const elapsed = event.timeStamp - lastTime;
270
+ if (elapsed >= 8) {
271
+ velocity = ((event.clientX - lastX) * sign) / elapsed;
272
+ lastX = event.clientX;
273
+ lastTime = event.timeStamp;
274
+ }
275
+ },
276
+ onEnd: ({ event }) => release(false, event.timeStamp),
277
+ onCancel: ({ event }) => release(true, event.timeStamp)
278
+ })(node);
279
+ };
280
+ }
281
+ /**
282
+ * Pager motion between views. Forward, the new view comes from the inline end and the old one
283
+ * leaves to the start, side by side, each travelling the motion's `x` and fading along the whole
284
+ * way; back mirrors it. A layer a swipe put in place is already where it belongs, so it enters
285
+ * and leaves without motion.
286
+ */
287
+ export const sidebarViewTransition = (node, { phase, direction, motion }) => {
288
+ if (phase === 'out')
289
+ node.style.pointerEvents = 'none';
290
+ const side = phase === 'in' ? motion.in : motion.out;
291
+ const duration = side.duration ?? 0;
292
+ if (node.dataset.swipe || direction === 'none' || duration === 0)
293
+ return { duration: 0 };
294
+ const travel = cssLength(side.x);
295
+ const rest = side.opacity ?? 0;
296
+ const rtl = getComputedStyle(node).direction === 'rtl' ? -1 : 1;
297
+ const sign = rtl * (direction === 'back' ? -1 : 1) * (phase === 'in' ? 1 : -1);
298
+ return {
299
+ duration,
300
+ easing: easingFunctions[side.easing ?? 'cubicOut'],
301
+ css: (t, u) => `transform: translateX(calc(${travel} * ${u * sign})); opacity: ${rest + (1 - rest) * t}`
302
+ };
303
+ };
@@ -14,7 +14,12 @@ export declare class ThemeFloatingWindows {
14
14
  private surfaces;
15
15
  readonly layer: Attachment<HTMLElement>;
16
16
  readonly portal: Attachment<HTMLElement>;
17
- activate(id: string, type: FloatingWindowSurface): number;
17
+ /**
18
+ * Brings a surface to the top. `reserveBelow` also claims the index just under it, for a
19
+ * window's backdrop: the counter never hands an index out twice, so no other surface can land
20
+ * between a window and its backdrop.
21
+ */
22
+ activate(id: string, type: FloatingWindowSurface, reserveBelow?: boolean): number;
18
23
  unregisterSurface(id: string, type: FloatingWindowSurface): void;
19
24
  isTopWindow(id: string): boolean;
20
25
  registerDock(entry: FloatingWindowDockEntry): () => void;
@@ -20,8 +20,13 @@ export class ThemeFloatingWindows {
20
20
  node.remove();
21
21
  };
22
22
  };
23
- activate(id, type) {
24
- this.zIndex += 1;
23
+ /**
24
+ * Brings a surface to the top. `reserveBelow` also claims the index just under it, for a
25
+ * window's backdrop: the counter never hands an index out twice, so no other surface can land
26
+ * between a window and its backdrop.
27
+ */
28
+ activate(id, type, reserveBelow = false) {
29
+ this.zIndex += reserveBelow ? 2 : 1;
25
30
  this.surfaces.set(id, { type, zIndex: this.zIndex });
26
31
  return this.zIndex;
27
32
  }
@@ -1 +1 @@
1
- export declare const themeDescription = "\n# Theme\n\n`Theme` owns global theme selection, runtime design tokens, shared overlay state, and theme\ntransitions. Wrap the application once and use the `ThemeState` received by the children snippet.\n\n## Runtime design tokens\n\n```svelte\n<script lang=\"ts\">\n\timport { Theme, type ThemeDesignTokenMap } from 'entasis/theme';\n\n\tlet spacing = $state<'small' | 'normal' | 'large'>('normal');\n\tconst designTokens = $derived({\n\t\tlight: {\n\t\t\tspacing,\n\t\t\tspacingScale: { xs: 1, sm: 1.5, md: 2, lg: 3, xl: 4 },\n\t\t\tradius: 'normal',\n\t\t\ttypeScale: 'default',\n\t\t\traisedWithBorder: true,\n\t\t\tdefaultColor: 'neutral'\n\t\t},\n\t\tdark: {\n\t\t\tspacing,\n\t\t\tspacingScale: { xs: 1, sm: 1.5, md: 2, lg: 3, xl: 4 },\n\t\t\tradius: 'small',\n\t\t\ttypeScale: 'compact',\n\t\t\traisedWithBorder: false,\n\t\t\tdefaultColor: 'neutral'\n\t\t}\n\t} satisfies ThemeDesignTokenMap<readonly ['light', 'dark']>);\n</script>\n\n<Theme {designTokens} transition=\"radial-top-right\">\n\t{#snippet children(theme)}\n\t\t<button onclick={() => (spacing = spacing === 'small' ? 'large' : 'small')}>\n\t\t\tChange density\n\t\t</button>\n\t\t<button onclick={() => (theme.theme = theme.resolvedTheme === 'dark' ? 'light' : 'dark')}>\n\t\t\tToggle color scheme\n\t\t</button>\n\t{/snippet}\n</Theme>\n```\n\n`designTokens` is keyed by logical theme name and respects the `attribute` and `value` props.\nEleven presets ship as `themePresets` (`dense`, `compact`, `balanced`, `comfortable`, `spacious`,\n`sharp`, `rounded`, `display`, `editorial`, `glass`, `terminal`): `designTokens={{ light: themePresets.glass.tokens, dark: themePresets.glass.tokens }}`.\nChanging the controlled object updates already-rendered Tailwind utilities without rebuilding CSS.\n\n### ThemeDesignTokens\n\n- `spacing`: `'small' | 'normal' | 'large' | number`. Globally scales density.\n- `spacingScale`: partial overrides for the strictly increasing `xs`, `sm`, `md`, `lg`,\n and `xl` spacing multipliers. Defaults to 1/1.5/2/3/4.\n- `radius`: `'none' | 'subtile' | 'small' | 'normal' | 'large' | 'round' | number`.\n- `typeScale`: `'compact' | 'default' | 'comfortable' | 'large' | TypeScaleOptions`.\n- `raisedWithBorder`: toggles the border used by `raised-*` utilities.\n- `defaultColor`: `Colors` role kit chrome inherits when a control omits `color`.\n Defaults to `neutral`. Set `primary` to restore an accent-colored kit. Compiles the\n current-color family (`--color`, `--color-readable`, muted/contrast/light/dark variants)\n and `--default-color` onto the theme selector. Do not set `data-color` on `html`.\n- `focusColor`, `selectedColor`, `hoverColor`, `pressedColor`: the four `Colors` **state\n roles**. They pin, for the whole theme, what a focus ring, a persistent selection and the\n transient hover/pressed layer look like, independently of the role of the control the state\n lands on. They compile `--color-focus`, `--color-selected` (plus its `-contrast`,\n `-readable` and `-muted-readable` companions), `--color-hover` and\n `--color-pressed` onto the theme selector. There is no `--color-selected-muted`: the soft\n fill is a translucent tint of `--color-selected` at `--state-selected-opacity`, so it reads\n on any surface. None is declared at `:root`: every use site\n falls back to the matching current role (`ring-focus` is\n `var(--color-focus, var(--color))`, `bg-selected-muted` tints\n `var(--color-selected, var(--color))`, the state layer is\n `var(--color-hover, currentColor)` and on `:active`\n `var(--color-pressed, var(--color-hover, currentColor))`), so leaving them unset changes\n nothing and `data-color` keeps moving the states with `--color`. Theme-level only: there is\n no per-component override. An unknown role throws.\n\nComponent-level density remains a local variant. It selects utility classes whose values inherit\nthe active global spacing token.\n\nGenerated interfaces should use the public `xs | sm | md | lg | xl` vocabulary through component\nprops and named gap/padding utilities. `micro` and `layout-*` are internal recipe tokens. Prefer\nparent-owned gaps over child margins; do not emit arbitrary spacing or unsupported radius values.\n\n## Theme selection\n\nThe selection props wrap `svelte-themes`: `themes`, `defaultTheme`, `forcedTheme`,\n`systemTheme`, `syncColorScheme`, `transitionOnChange`, `storageKey`, `attribute`,\n`value`, and `colorScheme`. The default themes are light and dark, with system selection\nenabled. `systemTheme`, `syncColorScheme`, and `transitionOnChange` all default to\n`true`; they map onto the library's `enableSystem`, `enableColorScheme`, and\n`disableTransitionOnChange` options.\n\n`ThemeState` exposes `theme`, `resolvedTheme`, `themes`, and `systemTheme`. Assign\n`theme.theme` to switch themes. The optional `transition` prop applies a named view transition;\nunsupported browsers and reduced-motion users switch instantly.\n\n`spinnerVariant` sets the global default spinner animation. The children snippet is required.\n\n## Motion tokens\n\nMotion is a token scale like spacing and radius: five duration steps and four easing roles.\n`motion` retunes them app-wide; an omitted token keeps its default.\n\n| Duration | Default | | Easing role | Default |\n| ---------- | ------- | --- | ------------ | ------------- |\n| `instant` | 0ms | | `standard` | `cubicInOut` |\n| `fast` | 100ms | | `enter` | `cubicOut` |\n| `normal` | 200ms | | `exit` | `cubicIn` |\n| `slow` | 300ms | | `emphasized` | `backOut` |\n| `slower` | 500ms | | | |\n\n```svelte\n<script lang=\"ts\">\n\timport { Theme } from 'entasis/theme';\n\n\tlet { children } = $props();\n</script>\n\n<Theme motion={{ duration: { normal: 150, slow: 260 }, easing: { standard: 'quintOut' } }}>\n\t{@render children()}\n</Theme>\n```\n\nThe Tailwind plugin emits the same scale as CSS variables on `html` (`--duration-normal`,\n`--ease-standard`, ...) plus the matching `duration-*` / `ease-*` utilities, so CSS transitions\nand Svelte transitions read one set of numbers. The `motion` prop rewrites those variables on\n`html` at runtime and `designTokens.motion` rewrites them again per theme, layered over the prop;\n`ThemeState.motion` resolves through the same two rungs, so the utilities and the presets never\ndisagree. `ThemeState.transition` is a deprecated alias for its `normal` duration and `standard`\neasing. Reduced motion resolves every duration to 0 and collapses the `--duration-*` variables via\nthe `data-entasis-reduce-motion` attribute on `html`.\n\nComponents keep their own transition in a reserved `motion` slot on their theme, so the `theme`\nprop covers motion as well as classes:\n\n```svelte\n<script lang=\"ts\">\n\timport { Dialog } from 'entasis/dialog';\n</script>\n\n<Dialog theme={{ motion: { duration: 'fast', easing: 'emphasized' } }} title=\"Quick\">Body</Dialog>\n```\n\n## Component theme registry\n\n`components` sets app-wide component theme defaults without a wrapper component per component:\nit is keyed by theme name (`dialog`, `button`, ...) and each entry takes the same slots as that\ncomponent's `theme` prop, the `motion` slot included. A `set<Component>Theme` call in a subtree\nbeats the registry, and an instance `theme` prop beats both \u2014 per slot: each rung layers on the one\nbelow it, so a subtree that restyles one slot keeps the registry's others.\n\n```svelte\n<script lang=\"ts\">\n\timport { Theme } from 'entasis/theme';\n\n\tlet { children } = $props();\n</script>\n\n<Theme\n\tcomponents={{\n\t\tdialog: { motion: { duration: 'fast' }, content: { base: 'rounded-2xl' } },\n\t\tbutton: { root: { base: 'tracking-wide' } }\n\t}}\n>\n\t{@render children()}\n</Theme>\n```\n\n## Reduced motion\n\n`reduceMotion` forces reduced motion on (`true`) or off (`false`) for every entasis animation,\noverriding the OS `prefers-reduced-motion` setting; omit it to follow the OS. The live result is\nexposed as `ThemeState.preferReducesMotion` (reactive, so it updates when the OS setting changes)\nand mirrored as a `data-entasis-reduce-motion` attribute on `html` for CSS-only animations.\n\n```svelte\n<script>\n\timport { Theme } from 'entasis/theme';\n\n\tlet { children } = $props();\n</script>\n\n<Theme reduceMotion>{@render children()}</Theme>\n```\n\n## Build-time boundary\n\nThe Tailwind plugin still generates color palettes and registers utility names, variants,\nkeyframes, and spinner CSS. Spacing, radius, typography scale, raised borders,\n`defaultColor` and the four state roles (`focusColor`, `selectedColor`, `hoverColor`,\n`pressedColor`) belong to `Theme.designTokens`; colors remain CSS variables and can be\noverridden directly. `ThemeState.defaultColor` exposes the active role. Kit chrome should\nresolve omitted `color` props with `useDefaultColor`.\n";
1
+ export declare const themeDescription = "\n# Theme\n\n`Theme` owns global theme selection, runtime design tokens, shared overlay state, and theme\ntransitions. Wrap the application once and use the `ThemeState` received by the children snippet.\n\n## Runtime design tokens\n\n```svelte\n<script lang=\"ts\">\n\timport { Theme, type ThemeDesignTokenMap } from 'entasis/theme';\n\n\tlet spacing = $state<'small' | 'normal' | 'large'>('normal');\n\tconst designTokens = $derived({\n\t\tlight: {\n\t\t\tspacing,\n\t\t\tspacingScale: { xs: 1, sm: 1.5, md: 2, lg: 3, xl: 4 },\n\t\t\tradius: 'normal',\n\t\t\ttypeScale: 'default',\n\t\t\traisedWithBorder: true,\n\t\t\tdefaultColor: 'neutral'\n\t\t},\n\t\tdark: {\n\t\t\tspacing,\n\t\t\tspacingScale: { xs: 1, sm: 1.5, md: 2, lg: 3, xl: 4 },\n\t\t\tradius: 'small',\n\t\t\ttypeScale: 'compact',\n\t\t\traisedWithBorder: false,\n\t\t\tdefaultColor: 'neutral'\n\t\t}\n\t} satisfies ThemeDesignTokenMap<readonly ['light', 'dark']>);\n</script>\n\n<Theme {designTokens} transition=\"radial-top-right\">\n\t{#snippet children(theme)}\n\t\t<button onclick={() => (spacing = spacing === 'small' ? 'large' : 'small')}>\n\t\t\tChange density\n\t\t</button>\n\t\t<button onclick={() => (theme.theme = theme.resolvedTheme === 'dark' ? 'light' : 'dark')}>\n\t\t\tToggle color scheme\n\t\t</button>\n\t{/snippet}\n</Theme>\n```\n\n`designTokens` is keyed by logical theme name and respects the `attribute` and `value` props.\nEleven presets ship as `themePresets` (`dense`, `compact`, `balanced`, `comfortable`, `spacious`,\n`sharp`, `rounded`, `display`, `editorial`, `glass`, `terminal`): `designTokens={{ light: themePresets.glass.tokens, dark: themePresets.glass.tokens }}`.\nChanging the controlled object updates already-rendered Tailwind utilities without rebuilding CSS.\n\n### ThemeDesignTokens\n\n- `spacing`: `'small' | 'normal' | 'large' | number`. Globally scales density.\n- `spacingScale`: partial overrides for the strictly increasing `xs`, `sm`, `md`, `lg`,\n and `xl` spacing multipliers. Defaults to 1/1.5/2/3/4.\n- `radius`: `'none' | 'subtile' | 'small' | 'normal' | 'large' | 'round' | number`.\n Controls take the full multiplier; surface steps (`lg` and up) stop at `large` (1.5\u00D7).\n- `typeScale`: `'compact' | 'default' | 'comfortable' | 'large' | TypeScaleOptions`.\n- `raisedWithBorder`: toggles the border used by `raised-*` utilities.\n- `defaultColor`: `Colors` role kit chrome inherits when a control omits `color`.\n Defaults to `neutral`. Set `primary` to restore an accent-colored kit. Compiles the\n current-color family (`--color`, `--color-readable`, muted/contrast/light/dark variants)\n and `--default-color` onto the theme selector. Do not set `data-color` on `html`.\n- `focusColor`, `selectedColor`, `hoverColor`, `pressedColor`: the four `Colors` **state\n roles**. They pin, for the whole theme, what a focus ring, a persistent selection and the\n transient hover/pressed layer look like, independently of the role of the control the state\n lands on. They compile `--color-focus`, `--color-selected` (plus its `-contrast`,\n `-readable` and `-muted-readable` companions), `--color-hover` and\n `--color-pressed` onto the theme selector. There is no `--color-selected-muted`: the soft\n fill is a translucent tint of `--color-selected` at `--state-selected-opacity`, so it reads\n on any surface. None is declared at `:root`: every use site\n falls back to the matching current role (`ring-focus` is\n `var(--color-focus, var(--color))`, `bg-selected-muted` tints\n `var(--color-selected, var(--color))`, the state layer is\n `var(--color-hover, currentColor)` and on `:active`\n `var(--color-pressed, var(--color-hover, currentColor))`), so leaving them unset changes\n nothing and `data-color` keeps moving the states with `--color`. Theme-level only: there is\n no per-component override. An unknown role throws.\n\nComponent-level density remains a local variant. It selects utility classes whose values inherit\nthe active global spacing token.\n\nGenerated interfaces should use the public `xs | sm | md | lg | xl` vocabulary through component\nprops and named gap/padding utilities. `micro` and `layout-*` are internal recipe tokens. Prefer\nparent-owned gaps over child margins; do not emit arbitrary spacing or unsupported radius values.\n\n## Theme selection\n\nThe selection props wrap `svelte-themes`: `themes`, `defaultTheme`, `forcedTheme`,\n`systemTheme`, `syncColorScheme`, `transitionOnChange`, `storageKey`, `attribute`,\n`value`, and `colorScheme`. The default themes are light and dark, with system selection\nenabled. `systemTheme`, `syncColorScheme`, and `transitionOnChange` all default to\n`true`; they map onto the library's `enableSystem`, `enableColorScheme`, and\n`disableTransitionOnChange` options.\n\n`ThemeState` exposes `theme`, `resolvedTheme`, `themes`, and `systemTheme`. Assign\n`theme.theme` to switch themes. The optional `transition` prop applies a named view transition;\nunsupported browsers and reduced-motion users switch instantly.\n\n`spinnerVariant` sets the global default spinner animation. The children snippet is required.\n\n## Motion tokens\n\nMotion is a token scale like spacing and radius: five duration steps and four easing roles.\n`motion` retunes them app-wide; an omitted token keeps its default.\n\n| Duration | Default | | Easing role | Default |\n| ---------- | ------- | --- | ------------ | ------------- |\n| `instant` | 0ms | | `standard` | `cubicInOut` |\n| `fast` | 100ms | | `enter` | `cubicOut` |\n| `normal` | 200ms | | `exit` | `cubicIn` |\n| `slow` | 300ms | | `emphasized` | `backOut` |\n| `slower` | 500ms | | | |\n\n```svelte\n<script lang=\"ts\">\n\timport { Theme } from 'entasis/theme';\n\n\tlet { children } = $props();\n</script>\n\n<Theme motion={{ duration: { normal: 150, slow: 260 }, easing: { standard: 'quintOut' } }}>\n\t{@render children()}\n</Theme>\n```\n\nThe Tailwind plugin emits the same scale as CSS variables on `html` (`--duration-normal`,\n`--ease-standard`, ...) plus the matching `duration-*` / `ease-*` utilities, so CSS transitions\nand Svelte transitions read one set of numbers. The `motion` prop rewrites those variables on\n`html` at runtime and `designTokens.motion` rewrites them again per theme, layered over the prop;\n`ThemeState.motion` resolves through the same two rungs, so the utilities and the presets never\ndisagree. `ThemeState.transition` is a deprecated alias for its `normal` duration and `standard`\neasing. Reduced motion resolves every duration to 0 and collapses the `--duration-*` variables via\nthe `data-entasis-reduce-motion` attribute on `html`.\n\nComponents keep their own transition in a reserved `motion` slot on their theme, so the `theme`\nprop covers motion as well as classes:\n\n```svelte\n<script lang=\"ts\">\n\timport { Dialog } from 'entasis/dialog';\n</script>\n\n<Dialog theme={{ motion: { duration: 'fast', easing: 'emphasized' } }} title=\"Quick\">Body</Dialog>\n```\n\n## Component theme registry\n\n`components` sets app-wide component theme defaults without a wrapper component per component:\nit is keyed by theme name (`dialog`, `button`, ...) and each entry takes the same slots as that\ncomponent's `theme` prop, the `motion` slot included. A `set<Component>Theme` call in a subtree\nbeats the registry, and an instance `theme` prop beats both \u2014 per slot: each rung layers on the one\nbelow it, so a subtree that restyles one slot keeps the registry's others.\n\n```svelte\n<script lang=\"ts\">\n\timport { Theme } from 'entasis/theme';\n\n\tlet { children } = $props();\n</script>\n\n<Theme\n\tcomponents={{\n\t\tdialog: { motion: { duration: 'fast' }, content: { base: 'rounded-2xl' } },\n\t\tbutton: { root: { base: 'tracking-wide' } }\n\t}}\n>\n\t{@render children()}\n</Theme>\n```\n\n## Reduced motion\n\n`reduceMotion` forces reduced motion on (`true`) or off (`false`) for every entasis animation,\noverriding the OS `prefers-reduced-motion` setting; omit it to follow the OS. The live result is\nexposed as `ThemeState.preferReducesMotion` (reactive, so it updates when the OS setting changes)\nand mirrored as a `data-entasis-reduce-motion` attribute on `html` for CSS-only animations.\n\n```svelte\n<script>\n\timport { Theme } from 'entasis/theme';\n\n\tlet { children } = $props();\n</script>\n\n<Theme reduceMotion>{@render children()}</Theme>\n```\n\n## Build-time boundary\n\nThe Tailwind plugin still generates color palettes and registers utility names, variants,\nkeyframes, and spinner CSS. Spacing, radius, typography scale, raised borders,\n`defaultColor` and the four state roles (`focusColor`, `selectedColor`, `hoverColor`,\n`pressedColor`) belong to `Theme.designTokens`; colors remain CSS variables and can be\noverridden directly. `ThemeState.defaultColor` exposes the active role. Kit chrome should\nresolve omitted `color` props with `useDefaultColor`.\n";
@@ -54,6 +54,7 @@ Changing the controlled object updates already-rendered Tailwind utilities witho
54
54
  - \`spacingScale\`: partial overrides for the strictly increasing \`xs\`, \`sm\`, \`md\`, \`lg\`,
55
55
  and \`xl\` spacing multipliers. Defaults to 1/1.5/2/3/4.
56
56
  - \`radius\`: \`'none' | 'subtile' | 'small' | 'normal' | 'large' | 'round' | number\`.
57
+ Controls take the full multiplier; surface steps (\`lg\` and up) stop at \`large\` (1.5×).
57
58
  - \`typeScale\`: \`'compact' | 'default' | 'comfortable' | 'large' | TypeScaleOptions\`.
58
59
  - \`raisedWithBorder\`: toggles the border used by \`raised-*\` utilities.
59
60
  - \`defaultColor\`: \`Colors\` role kit chrome inherits when a control omits \`color\`.
@@ -361,7 +361,7 @@ export declare const componentInventory: readonly [{
361
361
  readonly id: "sidebar";
362
362
  readonly subpath: "entasis/sidebar";
363
363
  readonly sourceIndex: "src/lib/components/Sidebar/index.ts";
364
- readonly exportedSymbols: readonly ["Sidebar", "SidebarActiveVariant", "SidebarActivityBar", "SidebarActivityBarItem", "SidebarActivityBarSelectPayload", "SidebarApi", "SidebarCollapsible", "SidebarDensity", "SidebarDisplayState", "SidebarFrame", "SidebarGroup", "SidebarIcon", "SidebarIconVariant", "SidebarMenuActionDescriptor", "SidebarMenuAlign", "SidebarMenuButton", "SidebarMenuButtonItem", "SidebarMenuButtonSize", "SidebarMenuButtonVariant", "SidebarMenuEntry", "SidebarMenuSide", "SidebarMenuSubEntry", "SidebarMode", "SidebarProps", "SidebarRail", "SidebarResizable", "SidebarResizableOptions", "SidebarSearch", "SidebarSide", "SidebarSize", "SidebarState", "SidebarTheme", "SidebarThemeProps", "SidebarTooltipMode", "SidebarTreeNode", "SidebarVariant", "SidebarWidthChangePayload", "setSidebarTheme", "sidebarDescription", "sidebarTheme", "useSidebarTheme"];
364
+ readonly exportedSymbols: readonly ["Sidebar", "SidebarActiveVariant", "SidebarActivityBar", "SidebarActivityBarItem", "SidebarActivityBarSelectPayload", "SidebarApi", "SidebarCollapsible", "SidebarDensity", "SidebarDisplayState", "SidebarFrame", "SidebarGroup", "SidebarIcon", "SidebarIconVariant", "SidebarMenuActionDescriptor", "SidebarMenuAlign", "SidebarMenuButton", "SidebarMenuButtonItem", "SidebarMenuButtonSize", "SidebarMenuButtonVariant", "SidebarMenuEntry", "SidebarMenuSide", "SidebarMenuSubEntry", "SidebarMode", "SidebarProps", "SidebarRail", "SidebarResizable", "SidebarResizableOptions", "SidebarSearch", "SidebarSide", "SidebarSize", "SidebarState", "SidebarTheme", "SidebarThemeProps", "SidebarTooltipMode", "SidebarTreeNode", "SidebarVariant", "SidebarView", "SidebarWidthChangePayload", "setSidebarTheme", "sidebarDescription", "sidebarTheme", "useSidebarTheme"];
365
365
  readonly docs: readonly [{
366
366
  readonly id: "sidebar";
367
367
  readonly route: "/components/sidebar";
@@ -932,6 +932,7 @@ export const componentInventory = [
932
932
  'SidebarTooltipMode',
933
933
  'SidebarTreeNode',
934
934
  'SidebarVariant',
935
+ 'SidebarView',
935
936
  'SidebarWidthChangePayload',
936
937
  'setSidebarTheme',
937
938
  'sidebarDescription',