entasis 0.7.1 → 0.9.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 (62) hide show
  1. package/README.md +7 -1
  2. package/dist/components/AppShell/appShell.theme.js +2 -2
  3. package/dist/components/FloatingWindow/FloatingWindow.svelte +26 -2
  4. package/dist/components/FloatingWindow/floatingWindow.dock.svelte.js +11 -2
  5. package/dist/components/FloatingWindow/floatingWindow.mcp.d.ts +1 -1
  6. package/dist/components/FloatingWindow/floatingWindow.mcp.js +7 -4
  7. package/dist/components/FloatingWindow/floatingWindow.props.d.ts +6 -0
  8. package/dist/components/FloatingWindow/floatingWindow.state.svelte.d.ts +3 -0
  9. package/dist/components/FloatingWindow/floatingWindow.state.svelte.js +18 -4
  10. package/dist/components/FloatingWindow/floatingWindow.theme.d.ts +3 -0
  11. package/dist/components/FloatingWindow/floatingWindow.theme.js +20 -7
  12. package/dist/components/Form/File/FileInput.svelte +6 -42
  13. package/dist/components/Form/Form/form.state.svelte.d.ts +4 -0
  14. package/dist/components/Form/Form/visibility.d.ts +2 -0
  15. package/dist/components/Form/MultiStepForm/multiStepForm.state.svelte.d.ts +4 -0
  16. package/dist/components/Form/Select/Select.svelte +28 -2
  17. package/dist/components/Form/Select/select.align.d.ts +55 -0
  18. package/dist/components/Form/Select/select.align.js +41 -0
  19. package/dist/components/Form/Select/select.mcp.d.ts +1 -1
  20. package/dist/components/Form/Select/select.mcp.js +7 -3
  21. package/dist/components/Form/Select/select.props.d.ts +8 -0
  22. package/dist/components/Form/Select/select.state.svelte.d.ts +24 -0
  23. package/dist/components/Form/Select/select.state.svelte.js +111 -1
  24. package/dist/components/Popover/Popover.svelte +4 -0
  25. package/dist/components/Popover/popover.mcp.d.ts +1 -1
  26. package/dist/components/Popover/popover.mcp.js +1 -0
  27. package/dist/components/Popover/popover.props.d.ts +16 -0
  28. package/dist/components/Popover/popover.state.svelte.d.ts +1 -1
  29. package/dist/components/Popover/popover.state.svelte.js +13 -0
  30. package/dist/components/Sidebar/Sidebar.svelte +58 -10
  31. package/dist/components/Sidebar/Sidebar.svelte.d.ts +1 -1
  32. package/dist/components/Sidebar/SidebarMenuItem.svelte +19 -2
  33. package/dist/components/Sidebar/SidebarMenuItem.svelte.d.ts +4 -0
  34. package/dist/components/Sidebar/SidebarPanel.svelte +199 -88
  35. package/dist/components/Sidebar/SidebarPanel.svelte.d.ts +3 -0
  36. package/dist/components/Sidebar/SidebarViewStage.svelte +103 -0
  37. package/dist/components/Sidebar/SidebarViewStage.svelte.d.ts +18 -0
  38. package/dist/components/Sidebar/index.d.ts +1 -1
  39. package/dist/components/Sidebar/sidebar.mcp.d.ts +1 -1
  40. package/dist/components/Sidebar/sidebar.mcp.js +19 -2
  41. package/dist/components/Sidebar/sidebar.props.d.ts +56 -1
  42. package/dist/components/Sidebar/sidebar.state.svelte.d.ts +4 -0
  43. package/dist/components/Sidebar/sidebar.state.svelte.js +9 -1
  44. package/dist/components/Sidebar/sidebar.theme.d.ts +128 -5
  45. package/dist/components/Sidebar/sidebar.theme.js +69 -3
  46. package/dist/components/Sidebar/sidebar.views.svelte.d.ts +138 -0
  47. package/dist/components/Sidebar/sidebar.views.svelte.js +303 -0
  48. package/dist/components/Theme/theme.floatingWindows.d.ts +6 -1
  49. package/dist/components/Theme/theme.floatingWindows.js +7 -2
  50. package/dist/components/Theme/theme.mcp.d.ts +1 -1
  51. package/dist/components/Theme/theme.mcp.js +1 -0
  52. package/dist/generated/componentContract.d.ts +1 -1
  53. package/dist/generated/componentContract.js +1 -0
  54. package/dist/generated/componentMcpRegistry.d.ts +6 -6
  55. package/dist/tailwind/index.mcp.d.ts +1 -1
  56. package/dist/tailwind/index.mcp.js +4 -0
  57. package/dist/tailwind/scales.js +11 -3
  58. package/dist/tailwind/spacing.js +17 -0
  59. package/dist/utils/cva/merge.d.ts +7 -0
  60. package/dist/utils/cva/merge.js +6 -1
  61. package/dist/utils/pointerDrag.js +7 -1
  62. package/package.json +1 -1
@@ -111,6 +111,28 @@ const defaultNav = cva({
111
111
  density: 'normal'
112
112
  }
113
113
  });
114
+ // With `views`, the panel and the menu inside it are one-cell grids whose layers are the views
115
+ // they show: one at rest, two while they slide past each other. The cell clips the slide.
116
+ const defaultViewStage = cva({
117
+ base: 'grid min-h-0 flex-1 grid-cols-1 grid-rows-1 overflow-x-clip'
118
+ });
119
+ // `translate` / `opacity` carry the swipe: they follow the finger with no transition while
120
+ // dragging, then ease to rest. The view-change motion animates `transform`, so the two never fight.
121
+ const defaultViewLayer = cva({
122
+ base: 'col-start-1 row-start-1 min-w-0 transition-[translate,opacity] ease-enter data-[dragging=true]:transition-none',
123
+ variants: {
124
+ // A body layer is its own scroll container, and `touch-action` only counts up to the nearest
125
+ // one: set on the layer, it leaves vertical scrolling to the browser and horizontal moves to
126
+ // the back swipe.
127
+ swipe: {
128
+ true: 'touch-pan-y',
129
+ false: null
130
+ }
131
+ },
132
+ defaultVariants: {
133
+ swipe: false
134
+ }
135
+ });
114
136
  const defaultGroup = cva({
115
137
  base: 'relative flex w-full min-w-0 flex-col',
116
138
  variants: {
@@ -612,12 +634,19 @@ const defaultMain = cva({
612
634
  edgeRevealed: {
613
635
  true: '',
614
636
  false: ''
637
+ },
638
+ // Nothing of the sidebar is left beside the page: the panel is hidden and there is no
639
+ // activity bar. Only then does an inset or split page drop its frame and fill the edge; with
640
+ // an activity bar the rail stays on screen, so the page keeps its framed form.
641
+ flush: {
642
+ true: '',
643
+ false: ''
615
644
  }
616
645
  },
617
646
  compoundVariants: [
618
647
  {
619
648
  variant: ['inset', 'split'],
620
- displayState: ['expanded', 'collapsed'],
649
+ flush: false,
621
650
  class: 'md:[--page-shell-edge-inset:0.5rem]'
622
651
  },
623
652
  {
@@ -642,15 +671,32 @@ const defaultMain = cva({
642
671
  },
643
672
  {
644
673
  variant: ['inset', 'split'],
645
- displayState: 'hidden',
674
+ flush: true,
646
675
  class: 'md:p-0 md:rounded-none'
676
+ },
677
+ // A hidden split panel no longer separates the page from the activity bar's card: the page
678
+ // takes the gutter on that side itself, so the two cards stay apart.
679
+ {
680
+ variant: 'split',
681
+ displayState: 'hidden',
682
+ flush: false,
683
+ side: 'left',
684
+ class: 'md:pl-md'
685
+ },
686
+ {
687
+ variant: 'split',
688
+ displayState: 'hidden',
689
+ flush: false,
690
+ side: 'right',
691
+ class: 'md:pr-md'
647
692
  }
648
693
  ],
649
694
  defaultVariants: {
650
695
  variant: 'admin',
651
696
  side: 'left',
652
697
  displayState: 'expanded',
653
- edgeRevealed: false
698
+ edgeRevealed: false,
699
+ flush: false
654
700
  }
655
701
  });
656
702
  const defaultEdgeTrigger = cva({
@@ -891,6 +937,24 @@ export const defaultSidebarMotion = motion({
891
937
  out: { axis: 'y', x: 0, y: 0, scale: 1, opacity: 0.2 },
892
938
  duration: 'normal',
893
939
  easing: 'standard'
940
+ },
941
+ variants: {
942
+ part: {
943
+ collapse: {},
944
+ // The pager between views: the arriving view comes from the inline end and the leaving
945
+ // one goes to the start, side by side, each travelling `x` while it fades between
946
+ // `opacity` and 1. `enter` easing moves on the first frame; an in-out curve would sit
947
+ // still for the first and last fifth and squeeze the whole swap into a blink.
948
+ view: {
949
+ in: { axis: 'x', x: '100%', opacity: 0 },
950
+ out: { axis: 'x', x: '100%', opacity: 0 },
951
+ duration: 'slow',
952
+ easing: 'enter'
953
+ }
954
+ }
955
+ },
956
+ defaultVariants: {
957
+ part: 'collapse'
894
958
  }
895
959
  });
896
960
  export const sidebarTheme = {
@@ -899,6 +963,8 @@ export const sidebarTheme = {
899
963
  panel: defaultPanel,
900
964
  header: defaultStackSection,
901
965
  nav: defaultNav,
966
+ viewStage: defaultViewStage,
967
+ viewLayer: defaultViewLayer,
902
968
  footer: defaultStackSection,
903
969
  group: defaultGroup,
904
970
  groupLabel: defaultGroupLabel,
@@ -0,0 +1,138 @@
1
+ import { type Snippet } from 'svelte';
2
+ import type { TransitionConfig } from 'svelte/transition';
3
+ import type { ResolvedMotion } from '../../utils/motion/index.js';
4
+ import type { SidebarApi, SidebarGroup, SidebarMenuButtonItem, SidebarMenuEntry, SidebarSearch, SidebarSide, SidebarView } from './sidebar.props.js';
5
+ /** What the panel renders for a view: the Sidebar's own props, or a view's resolved over its parents. */
6
+ export type SidebarSlotValues = {
7
+ items?: SidebarGroup[];
8
+ content?: Snippet<[SidebarApi]>;
9
+ headerButton?: SidebarMenuButtonItem;
10
+ search?: SidebarSearch;
11
+ headerMenu?: SidebarMenuEntry[];
12
+ header?: Snippet<[SidebarApi]>;
13
+ footerButton?: SidebarMenuButtonItem;
14
+ footerMenu?: SidebarMenuEntry[];
15
+ footer?: Snippet<[SidebarApi]>;
16
+ };
17
+ /** The header and footer props a view can set; together they are the panel's chrome. */
18
+ declare const chromeProps: readonly ["header", "headerButton", "search", "headerMenu", "footerButton", "footerMenu", "footer"];
19
+ /**
20
+ * What slides on a view change. `body` is the menu alone; `panel` is the whole panel (header,
21
+ * menu, footer), which only swaps when the two views do not share the same chrome.
22
+ */
23
+ export type SidebarViewKind = 'panel' | 'body';
24
+ type Views = Record<string, SidebarView>;
25
+ type PropSlot = (typeof chromeProps)[number];
26
+ /** The view and its ancestors, nearest first. A missing parent or a parent cycle ends the chain. */
27
+ export declare const viewChain: (views: Views, id: string) => string[];
28
+ /**
29
+ * Resolves one header or footer prop for a view: the nearest view in the chain that sets it
30
+ * (`null` counts, and clears it), else the Sidebar. `owner` names where it came from, so two
31
+ * views share the slot exactly when it resolves to the same owner.
32
+ */
33
+ export declare const resolveSlot: <S extends PropSlot>(views: Views, id: string, slot: S, root: SidebarSlotValues) => {
34
+ owner: string | null;
35
+ value: SidebarSlotValues[S];
36
+ };
37
+ export type SidebarViewDirection = 'forward' | 'back' | 'none';
38
+ /** Which half of a back swipe a layer plays: the view leaving (`over`) or its parent (`under`). */
39
+ export type SidebarSwipeRole = 'over' | 'under';
40
+ export type SidebarViewLayer = {
41
+ key: string;
42
+ view: string;
43
+ role?: SidebarSwipeRole;
44
+ };
45
+ type Swipe = {
46
+ from: string;
47
+ to: string;
48
+ /** `drag` follows the finger; `commit` / `cancel` animate to rest, then the swipe ends. */
49
+ phase: 'drag' | 'commit' | 'cancel';
50
+ /** Distance travelled toward the inline end, in px. */
51
+ offset: number;
52
+ width: number;
53
+ /** The commit has finished moving and waits for the view change to land (a route, say). */
54
+ settled?: boolean;
55
+ };
56
+ type SidebarViewsOptions = {
57
+ readonly views: Views | undefined;
58
+ readonly view: string | undefined;
59
+ readonly defaultView: string | undefined;
60
+ readonly onViewChange: ((view: string) => void) | undefined;
61
+ readonly isMobile: boolean;
62
+ readonly side: SidebarSide;
63
+ readonly root: SidebarSlotValues;
64
+ setViewProp: (view: string | undefined) => void;
65
+ };
66
+ export declare class SidebarViewsState {
67
+ #private;
68
+ private options;
69
+ swipe: Swipe | null;
70
+ rtl: boolean;
71
+ constructor(options: SidebarViewsOptions);
72
+ get enabled(): boolean;
73
+ /** The view on screen: the selected one when it exists, else `defaultView`, else the first. */
74
+ get current(): string | undefined;
75
+ depth(id: string): number;
76
+ parentOf(id: string): string | undefined;
77
+ labelOf(id: string): string | undefined;
78
+ /**
79
+ * The last change of the current view and its direction, read by the layer transitions when
80
+ * they start. Deeper slides forward and shallower slides back; at one depth (sections), the
81
+ * later view in `views` is forward, like pages in order.
82
+ */
83
+ get lastChange(): {
84
+ from?: string;
85
+ to?: string;
86
+ direction: SidebarViewDirection;
87
+ };
88
+ setView: (view: string) => void;
89
+ /** A view's body. */
90
+ body(view: string): {
91
+ items: SidebarGroup[] | undefined;
92
+ content: Snippet<[SidebarApi]> | undefined;
93
+ };
94
+ /** A view's header and footer props, each from the nearest view that sets it, else the Sidebar. */
95
+ chrome(view: string): SidebarSlotValues;
96
+ /**
97
+ * The layers a stage renders, bottom first. Outside a swipe that is the current view alone;
98
+ * during one it is the parent under the view being swiped away. Two views that share a layer
99
+ * key collapse to one layer that never moves: the panel's key is where its chrome comes from,
100
+ * so views with the same header and footer keep one panel and only their bodies slide.
101
+ */
102
+ layers(kind: SidebarViewKind): SidebarViewLayer[];
103
+ /**
104
+ * A swipe layer's drag position, as `translate` / `opacity` styles; empty at rest. The view
105
+ * under the finger stays opaque and follows it; its parent comes from `travel` toward the
106
+ * start and from `opacity`, the same way the view motion brings a view in.
107
+ */
108
+ swipeStyle(role: SidebarSwipeRole | undefined, travel: number | string, opacity: number): {
109
+ translate?: undefined;
110
+ opacity?: undefined;
111
+ } | {
112
+ translate: string;
113
+ opacity: number;
114
+ };
115
+ get canSwipeBack(): boolean;
116
+ /**
117
+ * The mobile drawer dismisses on a swipe toward its own edge. When going back points the same
118
+ * way (a right drawer in LTR, a left one in RTL), the view body keeps the gesture.
119
+ */
120
+ get ownsDrawerSwipe(): boolean;
121
+ /** Called by the body stage once a settling swipe has had its motion duration. */
122
+ settleSwipe(): void;
123
+ /** Back swipe on the view body: toward the inline end, anywhere on the body, mobile only. */
124
+ swipeAttachment: (node: HTMLElement) => void | (() => void);
125
+ }
126
+ type ViewTransitionParams = {
127
+ phase: 'in' | 'out';
128
+ direction: SidebarViewDirection;
129
+ motion: ResolvedMotion;
130
+ };
131
+ /**
132
+ * Pager motion between views. Forward, the new view comes from the inline end and the old one
133
+ * leaves to the start, side by side, each travelling the motion's `x` and fading along the whole
134
+ * way; back mirrors it. A layer a swipe put in place is already where it belongs, so it enters
135
+ * and leaves without motion.
136
+ */
137
+ export declare const sidebarViewTransition: (node: HTMLElement, { phase, direction, motion }: ViewTransitionParams) => TransitionConfig;
138
+ export {};
@@ -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',