@motion-proto/live-tokens 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/.claude/skills/live-tokens-add-component/SKILL.md +488 -0
  2. package/README.md +84 -29
  3. package/dist-plugin/index.cjs +177 -125
  4. package/dist-plugin/index.d.cts +3 -2
  5. package/dist-plugin/index.d.ts +3 -2
  6. package/dist-plugin/index.js +177 -125
  7. package/package.json +8 -2
  8. package/src/editor/component-editor/BadgeEditor.svelte +44 -42
  9. package/src/editor/component-editor/ButtonEditor.svelte +224 -0
  10. package/src/editor/component-editor/CollapsibleSectionEditor.svelte +1 -7
  11. package/src/editor/component-editor/CornerBadgeEditor.svelte +44 -34
  12. package/src/editor/component-editor/ImageLightboxEditor.svelte +58 -0
  13. package/src/editor/component-editor/InputEditor.svelte +272 -0
  14. package/src/editor/component-editor/NotificationEditor.svelte +44 -65
  15. package/src/editor/component-editor/ProgressBarEditor.svelte +71 -87
  16. package/src/editor/component-editor/SegmentedControlEditor.svelte +98 -37
  17. package/src/editor/component-editor/SideNavigationEditor.svelte +342 -0
  18. package/src/editor/component-editor/index.ts +16 -1
  19. package/src/editor/component-editor/registry.ts +138 -28
  20. package/src/editor/component-editor/scaffolding/ComponentFileManager.svelte +3 -2
  21. package/src/editor/component-editor/scaffolding/ComponentsTab.svelte +2 -2
  22. package/src/editor/component-editor/scaffolding/StateBlock.svelte +9 -10
  23. package/src/editor/component-editor/scaffolding/TokenLayout.svelte +60 -36
  24. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +38 -1
  25. package/src/editor/component-editor/scaffolding/buildTypeGroupTokens.ts +1 -1
  26. package/src/editor/component-editor/scaffolding/componentSources.ts +3 -3
  27. package/src/editor/component-editor/scaffolding/defaultSections.ts +15 -10
  28. package/src/editor/component-editor/scaffolding/siblings.ts +2 -2
  29. package/src/editor/component-editor/scaffolding/types.ts +2 -1
  30. package/src/editor/core/components/componentConfigKeys.ts +14 -3
  31. package/src/editor/core/components/componentConfigService.ts +7 -6
  32. package/src/editor/core/manifests/manifestService.ts +5 -4
  33. package/src/editor/core/storage/apiBase.ts +15 -0
  34. package/src/editor/core/storage/files/versionedFileResourceClient.ts +1 -1
  35. package/src/editor/core/themes/migrations/2026-05-24-collapsiblesection-drop-active-state.ts +28 -0
  36. package/src/editor/core/themes/migrations/2026-05-24-progressbar-collapse-variants.ts +41 -0
  37. package/src/editor/core/themes/migrations/2026-05-24-promote-state-shared-tokens.ts +59 -0
  38. package/src/editor/core/themes/migrations/2026-05-24-segmentedcontrol-divider-inset.ts +29 -0
  39. package/src/editor/core/themes/migrations/2026-05-25-cornerbadge-flatten-variants.ts +46 -0
  40. package/src/editor/core/themes/migrations/index.ts +10 -0
  41. package/src/editor/core/themes/slices/components.ts +9 -0
  42. package/src/editor/core/themes/themeInit.ts +3 -2
  43. package/src/editor/core/themes/themeService.ts +3 -2
  44. package/src/editor/index.ts +10 -1
  45. package/src/editor/pages/ComponentEditorPage.svelte +53 -3
  46. package/src/editor/pages/EditorShell.svelte +53 -3
  47. package/src/editor/ui/UIEasingSelector.svelte +240 -0
  48. package/src/editor/ui/variantScales.ts +34 -0
  49. package/src/system/components/Button.svelte +34 -85
  50. package/src/system/components/CollapsibleSection.svelte +1 -48
  51. package/src/system/components/CornerBadge.svelte +72 -138
  52. package/src/system/components/Dialog.svelte +24 -4
  53. package/src/system/components/ImageLightbox.svelte +578 -0
  54. package/src/system/components/Input.svelte +387 -0
  55. package/src/system/components/ProgressBar.svelte +62 -258
  56. package/src/system/components/SectionDivider.svelte +117 -43
  57. package/src/system/components/SegmentedControl.svelte +81 -15
  58. package/src/system/components/SideNavigation.svelte +777 -0
  59. package/src/system/styles/tokens.css +43 -0
  60. package/src/system/styles/tokens.generated.css +4 -183
  61. package/src/editor/component-editor/StandardButtonsEditor.svelte +0 -190
@@ -0,0 +1,777 @@
1
+ <script lang="ts">
2
+ import { createEventDispatcher } from 'svelte';
3
+ import CollapsibleSection from './CollapsibleSection.svelte';
4
+
5
+ export type SideNavItem = {
6
+ /** Full path. Matches `currentPath` exactly when this item is active. */
7
+ path: string;
8
+ title: string;
9
+ };
10
+
11
+ export type SideNavSection = {
12
+ /** Unique key for expand-state tracking. Also the section's own page path
13
+ when `hasIndexPage` is true. */
14
+ path: string;
15
+ title: string;
16
+ items: SideNavItem[];
17
+ /** When true, the section header is a link to `hrefFor(section.path)`.
18
+ Defaults to false — the header acts as a toggle only. */
19
+ hasIndexPage?: boolean;
20
+ };
21
+
22
+ export type SideNavFooter = {
23
+ path: string;
24
+ title: string;
25
+ icon?: string;
26
+ };
27
+
28
+ interface Props {
29
+ sections?: SideNavSection[];
30
+ footer?: SideNavFooter | undefined;
31
+ titleLabel?: string;
32
+ titleHref?: string;
33
+ currentPath?: string;
34
+ open?: boolean;
35
+ /** Builds the href for a section/item/footer path. Default returns `#${path}`. */
36
+ hrefFor?: (path: string) => string;
37
+ /** Force-hover one part for editor previews. */
38
+ forceHoverPart?: 'title' | 'toggle' | 'item' | 'footer' | 'section' | null;
39
+ /** Force-active one part for editor previews. */
40
+ forceActivePart?: 'title' | 'item' | 'footer' | 'section' | null;
41
+ class?: string;
42
+ /** Toggle callback. Preferred over `on:toggle` from 0.5.0 onward. */
43
+ ontoggle?: () => void;
44
+ }
45
+
46
+ let {
47
+ sections = [],
48
+ footer = undefined,
49
+ titleLabel = '',
50
+ titleHref = '#',
51
+ currentPath = '',
52
+ open = true,
53
+ hrefFor,
54
+ forceHoverPart = null,
55
+ forceActivePart = null,
56
+ class: className = '',
57
+ ontoggle,
58
+ }: Props = $props();
59
+
60
+ // Dual-fire bridge — see Button.svelte for the deprecation timeline.
61
+ const dispatch = createEventDispatcher<{ toggle: void }>();
62
+
63
+ let expandedSections: Record<string, boolean> = $state({});
64
+
65
+ // Auto-expand sections that match the current path or contain an active item.
66
+ $effect(() => {
67
+ for (const section of sections) {
68
+ const containsActive = section.items.some((i) => i.path === currentPath);
69
+ if (currentPath === section.path || containsActive) {
70
+ expandedSections[section.path] = true;
71
+ }
72
+ }
73
+ });
74
+
75
+ function toggleSection(path: string) {
76
+ expandedSections[path] = !expandedSections[path];
77
+ }
78
+
79
+ function fireToggle() {
80
+ ontoggle?.();
81
+ dispatch('toggle');
82
+ }
83
+
84
+ function buildHref(path: string): string {
85
+ return hrefFor ? hrefFor(path) : `#${path}`;
86
+ }
87
+
88
+ function sectionHref(section: SideNavSection): string | undefined {
89
+ return section.hasIndexPage ? buildHref(section.path) : undefined;
90
+ }
91
+
92
+ // The first section's first / second items are the canonical preview targets
93
+ // when the editor force-activates or force-hovers an item, so token edits
94
+ // always have a row to paint against.
95
+ let activeItemKey = $derived(() => {
96
+ if (forceActivePart !== 'item') return null;
97
+ const s = sections[0];
98
+ if (!s || s.items.length === 0) return null;
99
+ return s.items[0].path;
100
+ });
101
+ let hoverItemKey = $derived(() => {
102
+ if (forceHoverPart !== 'item') return null;
103
+ const s = sections[0];
104
+ if (!s || s.items.length === 0) return null;
105
+ return (s.items[1] ?? s.items[0]).path;
106
+ });
107
+
108
+ let titleActive = $derived(forceActivePart === 'title' || currentPath === '');
109
+
110
+ // When the editor force-activates the "section" part, pick the first section
111
+ // that's a valid route (hasIndexPage) so the active styling has somewhere
112
+ // to land in the preview.
113
+ let activeSectionKey = $derived(() => {
114
+ if (forceActivePart !== 'section') return null;
115
+ const s = sections.find((sec) => sec.hasIndexPage) ?? sections[0];
116
+ return s?.path ?? null;
117
+ });
118
+ let hoverSectionKey = $derived(() => {
119
+ if (forceHoverPart !== 'section') return null;
120
+ const s = sections.find((sec) => sec.hasIndexPage) ?? sections[0];
121
+ return s?.path ?? null;
122
+ });
123
+
124
+ function isSectionActive(section: SideNavSection): boolean {
125
+ if (!section.hasIndexPage) return false;
126
+ if (activeSectionKey() === section.path) return true;
127
+ return currentPath === section.path;
128
+ }
129
+ function isSectionHover(section: SideNavSection): boolean {
130
+ return hoverSectionKey() === section.path;
131
+ }
132
+ </script>
133
+
134
+ <aside
135
+ class="sidenavigation {className}"
136
+ class:collapsed={!open}
137
+ class:force-title-hover={forceHoverPart === 'title'}
138
+ class:force-toggle-hover={forceHoverPart === 'toggle'}
139
+ class:force-footer-hover={forceHoverPart === 'footer'}
140
+ class:force-footer-active={forceActivePart === 'footer'}
141
+ >
142
+ <!-- Toggle is a single persistent element. Its `left` position is
143
+ calc'd from the panel-width tokens so it transitions smoothly
144
+ between right-of-title (open) and centre-of-rail (closed) using
145
+ the same duration/easing tokens as the rail width. -->
146
+ <button
147
+ type="button"
148
+ class="sn-toggle"
149
+ onclick={fireToggle}
150
+ aria-label={open ? 'Collapse sidebar' : 'Expand sidebar'}
151
+ aria-expanded={open}
152
+ >
153
+ <i class="fa-solid fa-angles-right" aria-hidden="true"></i>
154
+ </button>
155
+
156
+ {#if open}
157
+ <!-- Open layout. Title bar reserves space for the persistent toggle on
158
+ the right; the menu is locked at the open-width so it can't reflow
159
+ while the rail expands around it. -->
160
+ <header class="sn-title" class:active={titleActive}>
161
+ <a href={titleHref} class="sn-title-label">{titleLabel}</a>
162
+ </header>
163
+
164
+ <div class="sn-menu">
165
+ {#each sections as section (section.path)}
166
+ <div class="sn-section">
167
+ <div
168
+ class="sn-section-header"
169
+ class:active={isSectionActive(section)}
170
+ class:force-hover={isSectionHover(section)}
171
+ >
172
+ <CollapsibleSection
173
+ variant="chromeless"
174
+ label={section.title}
175
+ href={sectionHref(section)}
176
+ expanded={expandedSections[section.path] || false}
177
+ ontoggle={() => toggleSection(section.path)}
178
+ />
179
+ </div>
180
+
181
+ {#if expandedSections[section.path]}
182
+ <div class="sn-items">
183
+ {#each section.items as item (item.path)}
184
+ <a
185
+ href={buildHref(item.path)}
186
+ class="sn-item"
187
+ class:active={item.path === currentPath || item.path === activeItemKey()}
188
+ class:force-hover={item.path === hoverItemKey()}
189
+ >
190
+ {item.title}
191
+ </a>
192
+ {/each}
193
+ </div>
194
+ {/if}
195
+ </div>
196
+ {/each}
197
+
198
+ {#if footer}
199
+ <a
200
+ href={buildHref(footer.path)}
201
+ class="sn-footer"
202
+ class:active={currentPath === footer.path}
203
+ >
204
+ {#if footer.icon}<i class={footer.icon} aria-hidden="true"></i>{/if}
205
+ <span>{footer.title}</span>
206
+ </a>
207
+ {/if}
208
+ </div>
209
+ {/if}
210
+ </aside>
211
+
212
+ <style lang="scss">
213
+ @use '../styles/padding' as *;
214
+
215
+ :global(:root) {
216
+ /* Panel — outer chrome + layout. No states. */
217
+ --sidenavigation-panel-surface: var(--surface-canvas-lower);
218
+ --sidenavigation-panel-border: var(--border-canvas-faint);
219
+ --sidenavigation-panel-border-width: var(--border-width-1);
220
+ --sidenavigation-panel-padding: var(--space-16);
221
+ --sidenavigation-panel-section-gap: var(--space-4);
222
+ --sidenavigation-panel-item-padding: var(--space-32);
223
+ --sidenavigation-panel-footer-gap: var(--space-16);
224
+ --sidenavigation-panel-open-width: 16rem;
225
+ --sidenavigation-panel-closed-width: 3rem;
226
+ --sidenavigation-open-duration: var(--duration-200);
227
+ --sidenavigation-open-easing: var(--ease-out-quart);
228
+ --sidenavigation-close-duration: var(--duration-150);
229
+ --sidenavigation-close-easing: var(--ease-out-quart);
230
+
231
+ /* Title — default */
232
+ --sidenavigation-title-default-surface: var(--color-transparent);
233
+ --sidenavigation-title-default-border: var(--border-canvas-faint);
234
+ --sidenavigation-title-default-border-width: var(--border-width-1);
235
+ --sidenavigation-title-default-padding: var(--space-12);
236
+ --sidenavigation-title-default-accent: var(--color-transparent);
237
+ --sidenavigation-title-default-accent-width: var(--border-width-3);
238
+ --sidenavigation-title-default-label: var(--text-primary);
239
+ --sidenavigation-title-default-label-font-family: var(--font-display);
240
+ --sidenavigation-title-default-label-font-size: var(--font-size-2xl);
241
+ --sidenavigation-title-default-label-font-weight: var(--font-weight-normal);
242
+ --sidenavigation-title-default-label-line-height: var(--line-height-sm);
243
+
244
+ /* Title — hover */
245
+ --sidenavigation-title-hover-surface: var(--surface-canvas);
246
+ --sidenavigation-title-hover-border: var(--border-canvas-faint);
247
+ --sidenavigation-title-hover-border-width: var(--border-width-1);
248
+ --sidenavigation-title-hover-padding: var(--space-12);
249
+ --sidenavigation-title-hover-accent: var(--color-transparent);
250
+ --sidenavigation-title-hover-accent-width: var(--border-width-3);
251
+ --sidenavigation-title-hover-label: var(--text-primary);
252
+ --sidenavigation-title-hover-label-font-family: var(--font-display);
253
+ --sidenavigation-title-hover-label-font-size: var(--font-size-2xl);
254
+ --sidenavigation-title-hover-label-font-weight: var(--font-weight-normal);
255
+ --sidenavigation-title-hover-label-line-height: var(--line-height-sm);
256
+
257
+ /* Title — active */
258
+ --sidenavigation-title-active-surface: var(--surface-canvas-low);
259
+ --sidenavigation-title-active-border: var(--border-canvas-faint);
260
+ --sidenavigation-title-active-border-width: var(--border-width-1);
261
+ --sidenavigation-title-active-padding: var(--space-12);
262
+ --sidenavigation-title-active-accent: var(--border-brand-medium);
263
+ --sidenavigation-title-active-accent-width: var(--border-width-3);
264
+ --sidenavigation-title-active-label: var(--text-primary);
265
+ --sidenavigation-title-active-label-font-family: var(--font-display);
266
+ --sidenavigation-title-active-label-font-size: var(--font-size-2xl);
267
+ --sidenavigation-title-active-label-font-weight: var(--font-weight-normal);
268
+ --sidenavigation-title-active-label-line-height: var(--line-height-sm);
269
+
270
+ /* Toggle — default */
271
+ --sidenavigation-toggle-default-surface: var(--color-transparent);
272
+ --sidenavigation-toggle-default-border: var(--border-brand-medium);
273
+ --sidenavigation-toggle-default-border-width: var(--border-width-1);
274
+ --sidenavigation-toggle-default-radius: var(--radius-md);
275
+ --sidenavigation-toggle-default-padding: var(--space-8);
276
+ --sidenavigation-toggle-default-icon: var(--text-primary);
277
+ --sidenavigation-toggle-default-icon-size: var(--icon-size-md);
278
+
279
+ /* Toggle — hover */
280
+ --sidenavigation-toggle-hover-surface: var(--surface-brand-lowest);
281
+ --sidenavigation-toggle-hover-border: var(--border-brand);
282
+ --sidenavigation-toggle-hover-border-width: var(--border-width-1);
283
+ --sidenavigation-toggle-hover-radius: var(--radius-md);
284
+ --sidenavigation-toggle-hover-padding: var(--space-8);
285
+ --sidenavigation-toggle-hover-icon: var(--text-brand);
286
+ --sidenavigation-toggle-hover-icon-size: var(--icon-size-md);
287
+
288
+ /* Section header — default. The wrapper paints surface + indicator; the
289
+ text tokens below are forwarded into the inner CollapsibleSection
290
+ (chromeless variant) by shadowing its slots in `.sn-section-header`,
291
+ so section typography can be edited per-state without touching the
292
+ generic chromeless variant used elsewhere. */
293
+ --sidenavigation-section-default-surface: var(--color-transparent);
294
+ --sidenavigation-section-default-accent: var(--color-transparent);
295
+ --sidenavigation-section-default-accent-width: var(--border-width-3);
296
+ --sidenavigation-section-default-text: var(--text-primary);
297
+ --sidenavigation-section-default-text-font-family: var(--font-sans);
298
+ --sidenavigation-section-default-text-font-size: var(--font-size-md);
299
+ --sidenavigation-section-default-text-font-weight: var(--font-weight-normal);
300
+ --sidenavigation-section-default-text-line-height: var(--line-height-md);
301
+
302
+ /* Section header — hover */
303
+ --sidenavigation-section-hover-surface: var(--surface-canvas);
304
+ --sidenavigation-section-hover-accent: var(--color-transparent);
305
+ --sidenavigation-section-hover-accent-width: var(--border-width-3);
306
+ --sidenavigation-section-hover-text: var(--text-primary);
307
+ --sidenavigation-section-hover-text-font-family: var(--font-sans);
308
+ --sidenavigation-section-hover-text-font-size: var(--font-size-md);
309
+ --sidenavigation-section-hover-text-font-weight: var(--font-weight-normal);
310
+ --sidenavigation-section-hover-text-line-height: var(--line-height-md);
311
+
312
+ /* Section header — active (this section's page is the current route) */
313
+ --sidenavigation-section-active-surface: var(--surface-canvas-low);
314
+ --sidenavigation-section-active-accent: var(--border-brand-medium);
315
+ --sidenavigation-section-active-accent-width: var(--border-width-3);
316
+ --sidenavigation-section-active-text: var(--text-primary);
317
+ --sidenavigation-section-active-text-font-family: var(--font-sans);
318
+ --sidenavigation-section-active-text-font-size: var(--font-size-md);
319
+ --sidenavigation-section-active-text-font-weight: var(--font-weight-normal);
320
+ --sidenavigation-section-active-text-line-height: var(--line-height-md);
321
+
322
+ /* Item — default */
323
+ --sidenavigation-item-default-surface: var(--color-transparent);
324
+ --sidenavigation-item-default-padding: var(--space-6);
325
+ --sidenavigation-item-default-accent: var(--color-transparent);
326
+ --sidenavigation-item-default-accent-width: var(--border-width-3);
327
+ --sidenavigation-item-default-text: var(--text-tertiary);
328
+ --sidenavigation-item-default-text-font-family: var(--font-sans);
329
+ --sidenavigation-item-default-text-font-size: var(--font-size-sm);
330
+ --sidenavigation-item-default-text-font-weight: var(--font-weight-light);
331
+ --sidenavigation-item-default-text-line-height: var(--line-height-md);
332
+
333
+ /* Item — hover */
334
+ --sidenavigation-item-hover-surface: var(--surface-canvas);
335
+ --sidenavigation-item-hover-padding: var(--space-6);
336
+ --sidenavigation-item-hover-accent: var(--color-transparent);
337
+ --sidenavigation-item-hover-accent-width: var(--border-width-3);
338
+ --sidenavigation-item-hover-text: var(--text-secondary);
339
+ --sidenavigation-item-hover-text-font-family: var(--font-sans);
340
+ --sidenavigation-item-hover-text-font-size: var(--font-size-sm);
341
+ --sidenavigation-item-hover-text-font-weight: var(--font-weight-light);
342
+ --sidenavigation-item-hover-text-line-height: var(--line-height-md);
343
+
344
+ /* Item — active */
345
+ --sidenavigation-item-active-surface: var(--surface-canvas-low);
346
+ --sidenavigation-item-active-padding: var(--space-6);
347
+ --sidenavigation-item-active-accent: var(--border-brand-medium);
348
+ --sidenavigation-item-active-accent-width: var(--border-width-3);
349
+ --sidenavigation-item-active-text: var(--text-primary);
350
+ --sidenavigation-item-active-text-font-family: var(--font-sans);
351
+ --sidenavigation-item-active-text-font-size: var(--font-size-sm);
352
+ --sidenavigation-item-active-text-font-weight: var(--font-weight-normal);
353
+ --sidenavigation-item-active-text-line-height: var(--line-height-md);
354
+
355
+ /* Footer — default */
356
+ --sidenavigation-footer-default-surface: var(--color-transparent);
357
+ --sidenavigation-footer-default-padding: var(--space-8);
358
+ --sidenavigation-footer-default-gap: var(--space-8);
359
+ --sidenavigation-footer-default-accent: var(--color-transparent);
360
+ --sidenavigation-footer-default-accent-width: var(--border-width-3);
361
+ --sidenavigation-footer-default-icon: var(--text-muted);
362
+ --sidenavigation-footer-default-icon-size: var(--icon-size-xs);
363
+ --sidenavigation-footer-default-text: var(--text-tertiary);
364
+ --sidenavigation-footer-default-text-font-family: var(--font-sans);
365
+ --sidenavigation-footer-default-text-font-size: var(--font-size-sm);
366
+ --sidenavigation-footer-default-text-font-weight: var(--font-weight-light);
367
+ --sidenavigation-footer-default-text-line-height: var(--line-height-md);
368
+
369
+ /* Footer — hover */
370
+ --sidenavigation-footer-hover-surface: var(--surface-canvas);
371
+ --sidenavigation-footer-hover-padding: var(--space-8);
372
+ --sidenavigation-footer-hover-gap: var(--space-8);
373
+ --sidenavigation-footer-hover-accent: var(--color-transparent);
374
+ --sidenavigation-footer-hover-accent-width: var(--border-width-3);
375
+ --sidenavigation-footer-hover-icon: var(--text-secondary);
376
+ --sidenavigation-footer-hover-icon-size: var(--icon-size-xs);
377
+ --sidenavigation-footer-hover-text: var(--text-secondary);
378
+ --sidenavigation-footer-hover-text-font-family: var(--font-sans);
379
+ --sidenavigation-footer-hover-text-font-size: var(--font-size-sm);
380
+ --sidenavigation-footer-hover-text-font-weight: var(--font-weight-light);
381
+ --sidenavigation-footer-hover-text-line-height: var(--line-height-md);
382
+
383
+ /* Footer — active */
384
+ --sidenavigation-footer-active-surface: var(--surface-canvas-low);
385
+ --sidenavigation-footer-active-padding: var(--space-8);
386
+ --sidenavigation-footer-active-gap: var(--space-8);
387
+ --sidenavigation-footer-active-accent: var(--border-brand-medium);
388
+ --sidenavigation-footer-active-accent-width: var(--border-width-3);
389
+ --sidenavigation-footer-active-icon: var(--text-primary);
390
+ --sidenavigation-footer-active-icon-size: var(--icon-size-xs);
391
+ --sidenavigation-footer-active-text: var(--text-primary);
392
+ --sidenavigation-footer-active-text-font-family: var(--font-sans);
393
+ --sidenavigation-footer-active-text-font-size: var(--font-size-sm);
394
+ --sidenavigation-footer-active-text-font-weight: var(--font-weight-normal);
395
+ --sidenavigation-footer-active-text-line-height: var(--line-height-md);
396
+ }
397
+
398
+ .sidenavigation {
399
+ display: flex;
400
+ flex-direction: column;
401
+ overflow: hidden;
402
+ background: var(--sidenavigation-panel-surface);
403
+ border-right: var(--sidenavigation-panel-border-width) solid var(--sidenavigation-panel-border);
404
+ @include themed-padding(--sidenavigation-panel-padding, $h: 0);
405
+ scrollbar-width: thin;
406
+ width: var(--sidenavigation-panel-open-width);
407
+ /* Opening uses the open-* timing tokens; the .collapsed rule below
408
+ overrides with the close-* tokens for the reverse direction. */
409
+ transition: width var(--sidenavigation-open-duration) var(--sidenavigation-open-easing);
410
+ }
411
+ .sidenavigation.collapsed {
412
+ width: var(--sidenavigation-panel-closed-width);
413
+ transition: width var(--sidenavigation-close-duration) var(--sidenavigation-close-easing);
414
+ }
415
+
416
+ /* The .sidenavigation needs to be the positioning context for the
417
+ absolutely-anchored toggle. Set on the outer aside so the toggle's
418
+ `left` calc is relative to the rail's left edge. */
419
+ .sidenavigation {
420
+ position: relative;
421
+ }
422
+
423
+ /* Per-state token rebinds — layout-affecting properties are written exactly
424
+ once on the element, then state rules rebind only the `--_*` values they
425
+ need. Matches the TabBar pattern: paint-only repaint, no reshape. */
426
+ .sn-title {
427
+ --_surface: var(--sidenavigation-title-default-surface);
428
+ --_border: var(--sidenavigation-title-default-border);
429
+ --_border-width: var(--sidenavigation-title-default-border-width);
430
+ --_padding: var(--sidenavigation-title-default-padding);
431
+ --_indicator: var(--sidenavigation-title-default-accent);
432
+ --_indicator-width: var(--sidenavigation-title-default-accent-width);
433
+ --_label: var(--sidenavigation-title-default-label);
434
+ --_label-family: var(--sidenavigation-title-default-label-font-family);
435
+ --_label-size: var(--sidenavigation-title-default-label-font-size);
436
+ --_label-weight: var(--sidenavigation-title-default-label-font-weight);
437
+ --_label-line-height: var(--sidenavigation-title-default-label-line-height);
438
+
439
+ /* Positioning context for the absolutely-anchored toggle button. */
440
+ position: relative;
441
+ flex: 0 0 auto;
442
+ /* Lock to the open-width so the title doesn't reflow during the rail
443
+ expansion (3rem → 16rem on open). border-box keeps the outer width
444
+ equal to the token value (otherwise padding + border would push the
445
+ toggle past the rail's right edge). */
446
+ box-sizing: border-box;
447
+ width: var(--sidenavigation-panel-open-width);
448
+ display: flex;
449
+ align-items: center;
450
+ gap: var(--space-12);
451
+ background: var(--_surface);
452
+ border-bottom: var(--_border-width) solid var(--_border);
453
+ border-left: var(--_indicator-width) solid var(--_indicator);
454
+ @include themed-padding(--_padding);
455
+ /* Right padding clears the absolutely-positioned toggle inside this
456
+ header (toggle width ~36px + 8px breathing room). */
457
+ padding-right: calc(var(--space-12) + 44px);
458
+ transition: background var(--duration-150), border-color var(--duration-150);
459
+ }
460
+
461
+ .sn-title:hover:not(.active),
462
+ .sidenavigation.force-title-hover .sn-title:not(.active) {
463
+ --_surface: var(--sidenavigation-title-hover-surface);
464
+ --_border: var(--sidenavigation-title-hover-border);
465
+ --_border-width: var(--sidenavigation-title-hover-border-width);
466
+ --_padding: var(--sidenavigation-title-hover-padding);
467
+ --_indicator: var(--sidenavigation-title-hover-accent);
468
+ --_indicator-width: var(--sidenavigation-title-hover-accent-width);
469
+ --_label: var(--sidenavigation-title-hover-label);
470
+ --_label-family: var(--sidenavigation-title-hover-label-font-family);
471
+ --_label-size: var(--sidenavigation-title-hover-label-font-size);
472
+ --_label-weight: var(--sidenavigation-title-hover-label-font-weight);
473
+ --_label-line-height: var(--sidenavigation-title-hover-label-line-height);
474
+ }
475
+
476
+ .sn-title.active {
477
+ --_surface: var(--sidenavigation-title-active-surface);
478
+ --_border: var(--sidenavigation-title-active-border);
479
+ --_border-width: var(--sidenavigation-title-active-border-width);
480
+ --_padding: var(--sidenavigation-title-active-padding);
481
+ --_indicator: var(--sidenavigation-title-active-accent);
482
+ --_indicator-width: var(--sidenavigation-title-active-accent-width);
483
+ --_label: var(--sidenavigation-title-active-label);
484
+ --_label-family: var(--sidenavigation-title-active-label-font-family);
485
+ --_label-size: var(--sidenavigation-title-active-label-font-size);
486
+ --_label-weight: var(--sidenavigation-title-active-label-font-weight);
487
+ --_label-line-height: var(--sidenavigation-title-active-label-line-height);
488
+ }
489
+
490
+ .sn-title-label {
491
+ color: var(--_label);
492
+ font-family: var(--_label-family);
493
+ font-size: var(--_label-size);
494
+ font-weight: var(--_label-weight);
495
+ line-height: var(--_label-line-height);
496
+ text-decoration: none;
497
+ flex: 1 1 auto;
498
+ min-width: 0;
499
+ white-space: nowrap;
500
+ overflow: hidden;
501
+ text-overflow: clip;
502
+ }
503
+
504
+ .sn-toggle {
505
+ --_surface: var(--sidenavigation-toggle-default-surface);
506
+ --_border: var(--sidenavigation-toggle-default-border);
507
+ --_border-width: var(--sidenavigation-toggle-default-border-width);
508
+ --_radius: var(--sidenavigation-toggle-default-radius);
509
+ --_padding: var(--sidenavigation-toggle-default-padding);
510
+ --_icon: var(--sidenavigation-toggle-default-icon);
511
+ --_icon-size: var(--sidenavigation-toggle-default-icon-size);
512
+
513
+ /* Persistent element anchored to the panel (not the title). Open-state
514
+ `left` puts it near the right edge of the open-width title; collapsed
515
+ `left` centres it in the closed-width rail. Both calc'd from the
516
+ same width tokens so they stay in sync if the consumer overrides. */
517
+ position: absolute;
518
+ /* Toggle's outer width is derived from its constituent tokens — icon
519
+ + padding (both sides) + border (both sides). Stays accurate when a
520
+ consumer customizes any of those without re-hardcoding here. */
521
+ --_toggle-width: calc(
522
+ var(--sidenavigation-toggle-default-icon-size)
523
+ + 2 * var(--sidenavigation-toggle-default-padding)
524
+ + 2 * var(--sidenavigation-toggle-default-border-width)
525
+ );
526
+ top: var(--space-12);
527
+ left: calc(var(--sidenavigation-panel-open-width) - var(--_toggle-width) - var(--space-8));
528
+ z-index: 1;
529
+ transition:
530
+ left var(--sidenavigation-open-duration) var(--sidenavigation-open-easing),
531
+ background var(--duration-150),
532
+ border-color var(--duration-150),
533
+ color var(--duration-150);
534
+
535
+ display: inline-flex;
536
+ align-items: center;
537
+ justify-content: center;
538
+ flex-shrink: 0;
539
+ background: var(--_surface);
540
+ border: var(--_border-width) solid var(--_border);
541
+ border-radius: var(--_radius);
542
+ color: var(--_icon);
543
+ @include themed-padding(--_padding);
544
+ cursor: pointer;
545
+ }
546
+
547
+ .sn-toggle i {
548
+ font-size: var(--_icon-size);
549
+ line-height: 1;
550
+ /* Icon points right by default (expand). Open state flips it to point
551
+ left (collapse). Rotation tweens with the position so the affordance
552
+ morphs smoothly between the two states. */
553
+ transition: transform var(--sidenavigation-open-duration) var(--sidenavigation-open-easing);
554
+ }
555
+ .sidenavigation:not(.collapsed) .sn-toggle i {
556
+ transform: rotate(180deg);
557
+ }
558
+
559
+ /* Collapsed: centre the toggle in the rail, and swap to close-* timing
560
+ tokens so the leftward slide matches the rail's shrink. */
561
+ .sidenavigation.collapsed .sn-toggle {
562
+ left: calc((var(--sidenavigation-panel-closed-width) - var(--_toggle-width)) / 2);
563
+ transition:
564
+ left var(--sidenavigation-close-duration) var(--sidenavigation-close-easing),
565
+ background var(--duration-150),
566
+ border-color var(--duration-150),
567
+ color var(--duration-150);
568
+ }
569
+ .sidenavigation.collapsed .sn-toggle i {
570
+ transition: transform var(--sidenavigation-close-duration) var(--sidenavigation-close-easing);
571
+ }
572
+
573
+ .sn-toggle:hover,
574
+ .sidenavigation.force-toggle-hover .sn-toggle {
575
+ --_surface: var(--sidenavigation-toggle-hover-surface);
576
+ --_border: var(--sidenavigation-toggle-hover-border);
577
+ --_border-width: var(--sidenavigation-toggle-hover-border-width);
578
+ --_radius: var(--sidenavigation-toggle-hover-radius);
579
+ --_padding: var(--sidenavigation-toggle-hover-padding);
580
+ --_icon: var(--sidenavigation-toggle-hover-icon);
581
+ --_icon-size: var(--sidenavigation-toggle-hover-icon-size);
582
+ }
583
+
584
+ /* Menu wraps everything below the title header. Scroll lives here so the
585
+ title (with its toggle) stays pinned while a tall menu list scrolls.
586
+ Locked to the open-width (matching .sn-title) so the menu's intrinsic
587
+ layout doesn't shift as the rail expands on open — items can't wrap
588
+ and re-flow if their container width never changes. The rail's
589
+ `overflow: hidden` clips them on the right until the rail catches up. */
590
+ .sn-menu {
591
+ flex: 1 1 auto;
592
+ box-sizing: border-box;
593
+ width: var(--sidenavigation-panel-open-width);
594
+ overflow-y: auto;
595
+ overflow-x: hidden;
596
+ }
597
+
598
+ .sn-section {
599
+ margin-top: var(--sidenavigation-panel-section-gap);
600
+ }
601
+
602
+ /* Section header wrapper. Paints background + left indicator around the
603
+ CollapsibleSection so a section that's also a route can show an active
604
+ state matching the panel's item/footer treatment. Section text tokens
605
+ are forwarded into the inner CollapsibleSection by shadowing its
606
+ chromeless slots — section typography can then be edited per-state
607
+ without touching the generic chromeless variant. */
608
+ .sn-section-header {
609
+ --_surface: var(--sidenavigation-section-default-surface);
610
+ --_indicator: var(--sidenavigation-section-default-accent);
611
+ --_indicator-width: var(--sidenavigation-section-default-accent-width);
612
+
613
+ --collapsiblesection-chromeless-default-label: var(--sidenavigation-section-default-text);
614
+ --collapsiblesection-chromeless-default-label-font-family: var(--sidenavigation-section-default-text-font-family);
615
+ --collapsiblesection-chromeless-default-label-font-size: var(--sidenavigation-section-default-text-font-size);
616
+ --collapsiblesection-chromeless-default-label-font-weight: var(--sidenavigation-section-default-text-font-weight);
617
+ --collapsiblesection-chromeless-default-label-line-height: var(--sidenavigation-section-default-text-line-height);
618
+ --collapsiblesection-chromeless-hover-label: var(--sidenavigation-section-hover-text);
619
+ --collapsiblesection-chromeless-hover-label-font-family: var(--sidenavigation-section-hover-text-font-family);
620
+ --collapsiblesection-chromeless-hover-label-font-size: var(--sidenavigation-section-hover-text-font-size);
621
+ --collapsiblesection-chromeless-hover-label-font-weight: var(--sidenavigation-section-hover-text-font-weight);
622
+ --collapsiblesection-chromeless-hover-label-line-height: var(--sidenavigation-section-hover-text-line-height);
623
+
624
+ background: var(--_surface);
625
+ border-left: var(--_indicator-width) solid var(--_indicator);
626
+ transition: background var(--duration-150), border-color var(--duration-150);
627
+ }
628
+ .sn-section-header:hover:not(.active),
629
+ .sn-section-header.force-hover:not(.active) {
630
+ --_surface: var(--sidenavigation-section-hover-surface);
631
+ --_indicator: var(--sidenavigation-section-hover-accent);
632
+ --_indicator-width: var(--sidenavigation-section-hover-accent-width);
633
+ }
634
+ .sn-section-header.active {
635
+ --_surface: var(--sidenavigation-section-active-surface);
636
+ --_indicator: var(--sidenavigation-section-active-accent);
637
+ --_indicator-width: var(--sidenavigation-section-active-accent-width);
638
+
639
+ /* Inner CollapsibleSection has no "active" state — shadow both the
640
+ default-slot and hover-slot with the active text values so the section
641
+ header keeps painting active typography whether or not it's hovered. */
642
+ --collapsiblesection-chromeless-default-label: var(--sidenavigation-section-active-text);
643
+ --collapsiblesection-chromeless-default-label-font-family: var(--sidenavigation-section-active-text-font-family);
644
+ --collapsiblesection-chromeless-default-label-font-size: var(--sidenavigation-section-active-text-font-size);
645
+ --collapsiblesection-chromeless-default-label-font-weight: var(--sidenavigation-section-active-text-font-weight);
646
+ --collapsiblesection-chromeless-default-label-line-height: var(--sidenavigation-section-active-text-line-height);
647
+ --collapsiblesection-chromeless-hover-label: var(--sidenavigation-section-active-text);
648
+ --collapsiblesection-chromeless-hover-label-font-family: var(--sidenavigation-section-active-text-font-family);
649
+ --collapsiblesection-chromeless-hover-label-font-size: var(--sidenavigation-section-active-text-font-size);
650
+ --collapsiblesection-chromeless-hover-label-font-weight: var(--sidenavigation-section-active-text-font-weight);
651
+ --collapsiblesection-chromeless-hover-label-line-height: var(--sidenavigation-section-active-text-line-height);
652
+ }
653
+
654
+ .sn-items {
655
+ display: flex;
656
+ flex-direction: column;
657
+ padding: var(--space-4) 0;
658
+ }
659
+
660
+ .sn-item {
661
+ --_surface: var(--sidenavigation-item-default-surface);
662
+ --_padding: var(--sidenavigation-item-default-padding);
663
+ --_indicator: var(--sidenavigation-item-default-accent);
664
+ --_indicator-width: var(--sidenavigation-item-default-accent-width);
665
+ --_text: var(--sidenavigation-item-default-text);
666
+ --_text-family: var(--sidenavigation-item-default-text-font-family);
667
+ --_text-size: var(--sidenavigation-item-default-text-font-size);
668
+ --_text-weight: var(--sidenavigation-item-default-text-font-weight);
669
+ --_text-line-height: var(--sidenavigation-item-default-text-line-height);
670
+
671
+ display: block;
672
+ background: var(--_surface);
673
+ border-left: var(--_indicator-width) solid var(--_indicator);
674
+ @include themed-padding(--_padding);
675
+ padding-left: var(--sidenavigation-panel-item-padding);
676
+ color: var(--_text);
677
+ font-family: var(--_text-family);
678
+ font-size: var(--_text-size);
679
+ font-weight: var(--_text-weight);
680
+ line-height: var(--_text-line-height);
681
+ text-decoration: none;
682
+ transition: background var(--duration-150), color var(--duration-150);
683
+ }
684
+
685
+ .sn-item:hover:not(.active),
686
+ .sn-item.force-hover:not(.active) {
687
+ --_surface: var(--sidenavigation-item-hover-surface);
688
+ --_padding: var(--sidenavigation-item-hover-padding);
689
+ --_indicator: var(--sidenavigation-item-hover-accent);
690
+ --_indicator-width: var(--sidenavigation-item-hover-accent-width);
691
+ --_text: var(--sidenavigation-item-hover-text);
692
+ --_text-family: var(--sidenavigation-item-hover-text-font-family);
693
+ --_text-size: var(--sidenavigation-item-hover-text-font-size);
694
+ --_text-weight: var(--sidenavigation-item-hover-text-font-weight);
695
+ --_text-line-height: var(--sidenavigation-item-hover-text-line-height);
696
+ }
697
+
698
+ .sn-item.active {
699
+ --_surface: var(--sidenavigation-item-active-surface);
700
+ --_padding: var(--sidenavigation-item-active-padding);
701
+ --_indicator: var(--sidenavigation-item-active-accent);
702
+ --_indicator-width: var(--sidenavigation-item-active-accent-width);
703
+ --_text: var(--sidenavigation-item-active-text);
704
+ --_text-family: var(--sidenavigation-item-active-text-font-family);
705
+ --_text-size: var(--sidenavigation-item-active-text-font-size);
706
+ --_text-weight: var(--sidenavigation-item-active-text-font-weight);
707
+ --_text-line-height: var(--sidenavigation-item-active-text-line-height);
708
+ }
709
+
710
+ .sn-footer {
711
+ --_surface: var(--sidenavigation-footer-default-surface);
712
+ --_padding: var(--sidenavigation-footer-default-padding);
713
+ --_gap: var(--sidenavigation-footer-default-gap);
714
+ --_indicator: var(--sidenavigation-footer-default-accent);
715
+ --_indicator-width: var(--sidenavigation-footer-default-accent-width);
716
+ --_icon: var(--sidenavigation-footer-default-icon);
717
+ --_icon-size: var(--sidenavigation-footer-default-icon-size);
718
+ --_text: var(--sidenavigation-footer-default-text);
719
+ --_text-family: var(--sidenavigation-footer-default-text-font-family);
720
+ --_text-size: var(--sidenavigation-footer-default-text-font-size);
721
+ --_text-weight: var(--sidenavigation-footer-default-text-font-weight);
722
+ --_text-line-height: var(--sidenavigation-footer-default-text-line-height);
723
+
724
+ display: flex;
725
+ align-items: center;
726
+ gap: var(--_gap);
727
+ margin-top: var(--sidenavigation-panel-footer-gap);
728
+ background: var(--_surface);
729
+ border-left: var(--_indicator-width) solid var(--_indicator);
730
+ @include themed-padding(--_padding);
731
+ color: var(--_text);
732
+ font-family: var(--_text-family);
733
+ font-size: var(--_text-size);
734
+ font-weight: var(--_text-weight);
735
+ line-height: var(--_text-line-height);
736
+ text-decoration: none;
737
+ transition: background var(--duration-150), color var(--duration-150);
738
+ }
739
+
740
+ .sn-footer i {
741
+ color: var(--_icon);
742
+ font-size: var(--_icon-size);
743
+ line-height: 1;
744
+ }
745
+
746
+ .sn-footer:hover:not(.active),
747
+ .sidenavigation.force-footer-hover .sn-footer:not(.active) {
748
+ --_surface: var(--sidenavigation-footer-hover-surface);
749
+ --_padding: var(--sidenavigation-footer-hover-padding);
750
+ --_gap: var(--sidenavigation-footer-hover-gap);
751
+ --_indicator: var(--sidenavigation-footer-hover-accent);
752
+ --_indicator-width: var(--sidenavigation-footer-hover-accent-width);
753
+ --_icon: var(--sidenavigation-footer-hover-icon);
754
+ --_icon-size: var(--sidenavigation-footer-hover-icon-size);
755
+ --_text: var(--sidenavigation-footer-hover-text);
756
+ --_text-family: var(--sidenavigation-footer-hover-text-font-family);
757
+ --_text-size: var(--sidenavigation-footer-hover-text-font-size);
758
+ --_text-weight: var(--sidenavigation-footer-hover-text-font-weight);
759
+ --_text-line-height: var(--sidenavigation-footer-hover-text-line-height);
760
+ }
761
+
762
+ .sn-footer.active,
763
+ .sidenavigation.force-footer-active .sn-footer {
764
+ --_surface: var(--sidenavigation-footer-active-surface);
765
+ --_padding: var(--sidenavigation-footer-active-padding);
766
+ --_gap: var(--sidenavigation-footer-active-gap);
767
+ --_indicator: var(--sidenavigation-footer-active-accent);
768
+ --_indicator-width: var(--sidenavigation-footer-active-accent-width);
769
+ --_icon: var(--sidenavigation-footer-active-icon);
770
+ --_icon-size: var(--sidenavigation-footer-active-icon-size);
771
+ --_text: var(--sidenavigation-footer-active-text);
772
+ --_text-family: var(--sidenavigation-footer-active-text-font-family);
773
+ --_text-size: var(--sidenavigation-footer-active-text-font-size);
774
+ --_text-weight: var(--sidenavigation-footer-active-text-font-weight);
775
+ --_text-line-height: var(--sidenavigation-footer-active-text-line-height);
776
+ }
777
+ </style>