@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
@@ -4,16 +4,18 @@
4
4
 
5
5
  export const component = 'segmentedcontrol';
6
6
 
7
- // Non-text tokens per state. Text/font properties live in `typeGroups` below
8
- // and are rendered via TypeEditor instead of TokenLayout.
9
- const states: Record<string, Token[]> = {
7
+ // Default-size schema. Non-text tokens per state; typography lives in
8
+ // `typeGroups` below and is rendered via TypeEditor. Per-option shape +
9
+ // icon-size are promoted into "option base" (one source of truth) since
10
+ // they don't vary per state in the runtime.
11
+ const defaultStates: Record<string, Token[]> = {
10
12
  'control bar': [
11
13
  { label: 'surface color', groupKey: 'surface', variable: '--segmentedcontrol-bar-surface' },
12
14
  { label: 'border color', groupKey: 'border', variable: '--segmentedcontrol-bar-border' },
13
15
  { label: 'border width', groupKey: 'width', variable: '--segmentedcontrol-bar-border-width' },
14
16
  { label: 'divider color', groupKey: 'color', variable: '--segmentedcontrol-divider-color' },
15
17
  { label: 'divider width', groupKey: 'thickness', variable: '--segmentedcontrol-divider-thickness' },
16
- { label: 'divider height', groupKey: 'height', variable: '--segmentedcontrol-divider-height' },
18
+ { label: 'divider inset', groupKey: 'divider-inset', variable: '--segmentedcontrol-divider-inset' },
17
19
  { label: 'corner radius', groupKey: 'radius', variable: '--segmentedcontrol-bar-radius' },
18
20
  { label: 'option gap', groupKey: 'gap', variable: '--segmentedcontrol-bar-gap' },
19
21
  { label: 'padding', variable: '--segmentedcontrol-bar-padding', groupKey: 'bar-padding' },
@@ -22,14 +24,21 @@
22
24
  { label: 'padding-bottom', variable: '--segmentedcontrol-bar-padding-bottom', groupKey: 'bar-padding-bottom', hidden: true },
23
25
  { label: 'padding-left', variable: '--segmentedcontrol-bar-padding-left', groupKey: 'bar-padding-left', hidden: true },
24
26
  ],
27
+ 'option base': [
28
+ { label: 'padding', variable: '--segmentedcontrol-option-padding', groupKey: 'option-padding' },
29
+ { label: 'padding-top', variable: '--segmentedcontrol-option-padding-top', groupKey: 'option-padding-top', hidden: true },
30
+ { label: 'padding-right', variable: '--segmentedcontrol-option-padding-right', groupKey: 'option-padding-right', hidden: true },
31
+ { label: 'padding-bottom', variable: '--segmentedcontrol-option-padding-bottom', groupKey: 'option-padding-bottom', hidden: true },
32
+ { label: 'padding-left', variable: '--segmentedcontrol-option-padding-left', groupKey: 'option-padding-left', hidden: true },
33
+ { label: 'icon gap', groupKey: 'option-gap', variable: '--segmentedcontrol-option-gap' },
34
+ { label: 'icon size', groupKey: 'icon-size', variable: '--segmentedcontrol-option-icon-size' },
35
+ ],
25
36
  'default option': [
26
37
  { label: 'icon color', groupKey: 'icon', variable: '--segmentedcontrol-option-icon' },
27
- { label: 'icon size', canBeLinked: true, groupKey: 'icon-size', variable: '--segmentedcontrol-option-icon-size' },
28
38
  ],
29
39
  'selected option': [
30
40
  { label: 'surface color', groupKey: 'surface', variable: '--segmentedcontrol-selected-surface' },
31
41
  { label: 'icon color', groupKey: 'icon', variable: '--segmentedcontrol-selected-icon' },
32
- { label: 'icon size', canBeLinked: true, groupKey: 'icon-size', variable: '--segmentedcontrol-selected-icon-size' },
33
42
  { label: 'border color', groupKey: 'border', variable: '--segmentedcontrol-selected-border' },
34
43
  { label: 'border width', groupKey: 'width', variable: '--segmentedcontrol-selected-border-width' },
35
44
  { label: 'corner radius', groupKey: 'radius', variable: '--segmentedcontrol-selected-radius' },
@@ -37,18 +46,46 @@
37
46
  'hover option': [
38
47
  { label: 'surface color', groupKey: 'surface', variable: '--segmentedcontrol-option-hover-surface' },
39
48
  { label: 'icon color', groupKey: 'icon', variable: '--segmentedcontrol-option-hover-icon' },
40
- { label: 'icon size', canBeLinked: true, groupKey: 'icon-size', variable: '--segmentedcontrol-option-hover-icon-size' },
41
49
  ],
42
50
  'disabled option': [
43
51
  { label: 'surface color', groupKey: 'surface', variable: '--segmentedcontrol-disabled-surface' },
44
52
  { label: 'icon color', groupKey: 'icon', variable: '--segmentedcontrol-disabled-icon' },
45
- { label: 'icon size', canBeLinked: true, groupKey: 'icon-size', variable: '--segmentedcontrol-disabled-icon-size' },
46
53
  ],
47
54
  };
48
55
 
49
- // Per-state typography groups for the option text element. All five
50
- // properties are exposed and individually share-able via groupKey across
51
- // the four states.
56
+ // Small-size schema. Thin delta layer only the size-driven properties.
57
+ // Per-state colors, borders, font-family/weight stay shared with default.
58
+ // States with no small overrides are omitted entirely so they don't render
59
+ // empty fieldsets. Typography is regular rows (no typeGroups at small)
60
+ // since family/weight/color don't differ.
61
+ const smallStates: Record<string, Token[]> = {
62
+ 'control bar': [
63
+ { label: 'divider inset', groupKey: 'small-divider-inset', variable: '--segmentedcontrol-divider-small-inset' },
64
+ { label: 'divider width', groupKey: 'small-thickness', variable: '--segmentedcontrol-divider-small-thickness' },
65
+ { label: 'corner radius', groupKey: 'small-radius', variable: '--segmentedcontrol-bar-small-radius' },
66
+ { label: 'padding', variable: '--segmentedcontrol-bar-small-padding', groupKey: 'bar-small-padding' },
67
+ { label: 'padding-top', variable: '--segmentedcontrol-bar-small-padding-top', groupKey: 'bar-small-padding-top', hidden: true },
68
+ { label: 'padding-right', variable: '--segmentedcontrol-bar-small-padding-right', groupKey: 'bar-small-padding-right', hidden: true },
69
+ { label: 'padding-bottom', variable: '--segmentedcontrol-bar-small-padding-bottom', groupKey: 'bar-small-padding-bottom', hidden: true },
70
+ { label: 'padding-left', variable: '--segmentedcontrol-bar-small-padding-left', groupKey: 'bar-small-padding-left', hidden: true },
71
+ ],
72
+ 'option base': [
73
+ { label: 'icon size', groupKey: 'small-icon-size', variable: '--segmentedcontrol-option-small-icon-size' },
74
+ { label: 'font size', groupKey: 'small-text-font-size', variable: '--segmentedcontrol-option-small-text-font-size' },
75
+ { label: 'line height', groupKey: 'small-text-line-height', variable: '--segmentedcontrol-option-small-text-line-height' },
76
+ { label: 'padding', variable: '--segmentedcontrol-option-small-padding', groupKey: 'option-small-padding' },
77
+ { label: 'padding-top', variable: '--segmentedcontrol-option-small-padding-top', groupKey: 'option-small-padding-top', hidden: true },
78
+ { label: 'padding-right', variable: '--segmentedcontrol-option-small-padding-right', groupKey: 'option-small-padding-right', hidden: true },
79
+ { label: 'padding-bottom', variable: '--segmentedcontrol-option-small-padding-bottom', groupKey: 'option-small-padding-bottom', hidden: true },
80
+ { label: 'padding-left', variable: '--segmentedcontrol-option-small-padding-left', groupKey: 'option-small-padding-left', hidden: true },
81
+ { label: 'icon gap', groupKey: 'option-small-gap', variable: '--segmentedcontrol-option-small-gap' },
82
+ ],
83
+ 'selected option': [
84
+ { label: 'corner radius', groupKey: 'small-radius', variable: '--segmentedcontrol-selected-small-radius' },
85
+ ],
86
+ };
87
+
88
+ // Per-state typography groups (default size only).
52
89
  const typeGroups: Record<string, TypeGroupConfig[]> = {
53
90
  'default option': [{
54
91
  legend: 'option text',
@@ -84,20 +121,23 @@
84
121
  }],
85
122
  };
86
123
 
87
- // Schema entries for the type-group variables — registered for groupKey
88
- // resolution but not rendered through TokenLayout. Derived from `typeGroups`
89
- // so the four font props × four states stay in lockstep with the per-state
90
- // TypeGroupConfig declarations above.
124
+ const emptyTypeGroups: Record<string, TypeGroupConfig[]> = {};
125
+
126
+ // allTokens unions BOTH sizes so the store registers every editable variable.
127
+ // The visibleStates filter is purely UI (which subset to render now).
91
128
  const typeGroupTokens: Token[] = buildTypeGroupTokens(typeGroups);
92
- export const allTokens: Token[] = [...Object.values(states).flat(), ...typeGroupTokens];
129
+ export const allTokens: Token[] = [
130
+ ...Object.values(defaultStates).flat(),
131
+ ...Object.values(smallStates).flat(),
132
+ ...typeGroupTokens,
133
+ ];
93
134
 
94
- const linkableContexts = new Map<string, string>([
95
- ...buildTypeGroupShareableContexts(typeGroups),
96
- ['--segmentedcontrol-option-icon-size', 'default option'],
97
- ['--segmentedcontrol-selected-icon-size', 'selected option'],
98
- ['--segmentedcontrol-option-hover-icon-size', 'hover option'],
99
- ['--segmentedcontrol-disabled-icon-size', 'disabled option'],
100
- ]);
135
+ // Cross-size linkage is intentionally not declared: small lives in its own
136
+ // namespace. The per-state icon-size links are gone now that icon-size is a
137
+ // single source-of-truth token in "option base".
138
+ const linkableContexts = new Map<string, string>(
139
+ buildTypeGroupShareableContexts(typeGroups),
140
+ );
101
141
  </script>
102
142
 
103
143
  <script lang="ts">
@@ -114,34 +154,45 @@
114
154
  { value: 'option-3', label: 'Option 3', icon: 'fas fa-heart' },
115
155
  ];
116
156
  let showIcons = $state(true);
157
+ let previewSize = $state<'default' | 'small'>('default');
117
158
  let previewSegments = $derived(showIcons ? segments : segments.map((s) => ({ ...s, icon: undefined })));
118
159
 
119
160
  let linked = $derived(computeLinkedBlock(component, linkableContexts, allTokens, $editorState));
120
161
 
162
+ let activeStates = $derived(previewSize === 'small' ? smallStates : defaultStates);
163
+ let activeTypeGroups = $derived(previewSize === 'small' ? emptyTypeGroups : typeGroups);
164
+
121
165
  let visibleStates = $derived(Object.fromEntries(
122
- Object.entries(states).map(([name, list]) => [name, withLinkedDisabled(list, linked.varSet)]),
166
+ Object.entries(activeStates).map(([name, list]) => [name, withLinkedDisabled(list, linked.varSet)]),
123
167
  ) as Record<string, Token[]>);
124
168
  </script>
125
169
 
126
170
  <ComponentEditorBase {component} title="Segmented Control" description="A connected set of buttons for toggling between mutually exclusive options." tokens={allTokens} {linked}>
127
- {#snippet config()}
128
-
129
- <label>
130
- <input type="checkbox" bind:checked={showIcons} />
131
- <span>Show icons</span>
132
- </label>
133
-
134
- {/snippet}
135
171
  <VariantGroup
136
172
  name="segmentedcontrol"
137
173
  title="Segmented Control"
138
174
  states={visibleStates}
139
- {typeGroups}
175
+ typeGroups={activeTypeGroups}
140
176
  {component}
141
-
142
177
  >
178
+ {#snippet previewActions()}
179
+ <label>
180
+ <span>Size</span>
181
+ <select bind:value={previewSize}>
182
+ <option value="default">Default</option>
183
+ <option value="small">Small</option>
184
+ </select>
185
+ </label>
186
+ {/snippet}
187
+ {#snippet canvasToolbarExtras()}
188
+ <hr class="canvas-toolbar-divider" />
189
+ <label class="sc-preview-check">
190
+ <input type="checkbox" bind:checked={showIcons} />
191
+ <span>Show icons</span>
192
+ </label>
193
+ {/snippet}
143
194
  {#snippet children({ activeState })}
144
- {@const previewValue = activeState === 'selected option' ? 'option-2' : ''}
195
+ {@const previewValue = activeState === 'selected option' ? 'option-2' : ''}
145
196
  {@const previewForceHover = activeState === 'hover option' ? 'option-1' : null}
146
197
  {@const previewDisabled = activeState === 'disabled option'}
147
198
  <div>
@@ -150,9 +201,19 @@
150
201
  value={previewValue}
151
202
  forceHoverValue={previewForceHover}
152
203
  disabled={previewDisabled}
204
+ size={previewSize}
153
205
  />
154
206
  </div>
155
- {/snippet}
156
- </VariantGroup>
207
+ {/snippet}
208
+ </VariantGroup>
157
209
  </ComponentEditorBase>
158
210
 
211
+ <style>
212
+ .sc-preview-check {
213
+ display: inline-flex;
214
+ align-items: center;
215
+ gap: var(--ui-space-6);
216
+ font-size: var(--ui-font-size-sm);
217
+ color: var(--ui-text-secondary);
218
+ }
219
+ </style>
@@ -0,0 +1,342 @@
1
+ <script module lang="ts">
2
+ import { buildTypeGroupColorTokens } from './scaffolding/buildTypeGroupTokens';
3
+ import type { Token, TypeGroupConfig } from './scaffolding/types';
4
+
5
+ export const component = 'sidenavigation';
6
+
7
+ // Single-variant component with five structural parts. Three of them (Title,
8
+ // Item, Footer) carry default/hover/active interaction sub-states; Toggle
9
+ // carries default/hover. Panel is geometry only. Keys use the " / " convention
10
+ // VariantGroup recognises for two-tier parts/state strips.
11
+ const STATEFUL_STATES = ['default', 'hover', 'active'] as const;
12
+ const TOGGLE_STATES = ['default', 'hover'] as const;
13
+ type StatefulState = typeof STATEFUL_STATES[number];
14
+ type ToggleState = typeof TOGGLE_STATES[number];
15
+
16
+ const STATE_LABELS: Record<string, string> = {
17
+ default: 'Default',
18
+ hover: 'Hover',
19
+ active: 'Active',
20
+ };
21
+
22
+ // --- Panel --------------------------------------------------------------
23
+ const panelTokens: Token[] = [
24
+ { label: 'surface color', groupKey: 'panel-surface', variable: '--sidenavigation-panel-surface' },
25
+ { label: 'border color', groupKey: 'panel-border', variable: '--sidenavigation-panel-border' },
26
+ { label: 'border width', canBeLinked: true, groupKey: 'border-width', variable: '--sidenavigation-panel-border-width' },
27
+ { label: 'padding', canBeLinked: true, groupKey: 'padding', variable: '--sidenavigation-panel-padding' },
28
+ { label: 'section gap', groupKey: 'panel-section-gap', variable: '--sidenavigation-panel-section-gap' },
29
+ { label: 'item indent', splittable: false, groupKey: 'panel-item-padding', variable: '--sidenavigation-panel-item-padding' },
30
+ { label: 'footer gap', groupKey: 'panel-footer-gap', variable: '--sidenavigation-panel-footer-gap' },
31
+ ];
32
+
33
+ // --- Animation ----------------------------------------------------------
34
+ const animationTokens: Token[] = [
35
+ { label: 'open duration', groupKey: 'open-duration', variable: '--sidenavigation-open-duration' },
36
+ { label: 'open easing', groupKey: 'open-easing', variable: '--sidenavigation-open-easing' },
37
+ { label: 'close duration', groupKey: 'close-duration', variable: '--sidenavigation-close-duration' },
38
+ { label: 'close easing', groupKey: 'close-easing', variable: '--sidenavigation-close-easing' },
39
+ ];
40
+
41
+ // --- Title --------------------------------------------------------------
42
+ function titleStateTokens(s: StatefulState): Token[] {
43
+ return [
44
+ { label: 'surface color', groupKey: 'title-surface', variable: `--sidenavigation-title-${s}-surface` },
45
+ { label: 'divider color', groupKey: 'title-border', variable: `--sidenavigation-title-${s}-border` },
46
+ { label: 'divider width', canBeLinked: true, groupKey: 'title-border-width', variable: `--sidenavigation-title-${s}-border-width` },
47
+ { label: 'padding', canBeLinked: true, groupKey: 'title-padding', variable: `--sidenavigation-title-${s}-padding` },
48
+ { label: 'indicator color', groupKey: 'title-accent', variable: `--sidenavigation-title-${s}-accent` },
49
+ { label: 'indicator width', canBeLinked: true, groupKey: 'title-accent-width', variable: `--sidenavigation-title-${s}-accent-width` },
50
+ ];
51
+ }
52
+ function titleStateTypeGroups(s: StatefulState): TypeGroupConfig[] {
53
+ return [{
54
+ legend: 'title label',
55
+ colorVariable: `--sidenavigation-title-${s}-label`,
56
+ familyVariable: `--sidenavigation-title-${s}-label-font-family`,
57
+ sizeVariable: `--sidenavigation-title-${s}-label-font-size`,
58
+ weightVariable: `--sidenavigation-title-${s}-label-font-weight`,
59
+ lineHeightVariable: `--sidenavigation-title-${s}-label-line-height`,
60
+ }];
61
+ }
62
+ const titleTypographyTokens: Token[] = STATEFUL_STATES.flatMap((s) => [
63
+ { label: 'font family', canBeLinked: true, groupKey: 'title-label-font-family', variable: `--sidenavigation-title-${s}-label-font-family` },
64
+ { label: 'font size', canBeLinked: true, groupKey: 'title-label-font-size', variable: `--sidenavigation-title-${s}-label-font-size` },
65
+ { label: 'font weight', canBeLinked: true, groupKey: 'title-label-font-weight', variable: `--sidenavigation-title-${s}-label-font-weight` },
66
+ { label: 'line height', canBeLinked: true, groupKey: 'title-label-line-height', variable: `--sidenavigation-title-${s}-label-line-height` },
67
+ ]);
68
+
69
+ // --- Toggle -------------------------------------------------------------
70
+ function toggleStateTokens(s: ToggleState): Token[] {
71
+ return [
72
+ { label: 'surface color', groupKey: 'toggle-surface', variable: `--sidenavigation-toggle-${s}-surface` },
73
+ { label: 'border color', groupKey: 'toggle-border', variable: `--sidenavigation-toggle-${s}-border` },
74
+ { label: 'border width', canBeLinked: true, groupKey: 'toggle-border-width', variable: `--sidenavigation-toggle-${s}-border-width` },
75
+ { label: 'corner radius', canBeLinked: true, groupKey: 'toggle-radius', variable: `--sidenavigation-toggle-${s}-radius` },
76
+ { label: 'padding', canBeLinked: true, groupKey: 'toggle-padding', variable: `--sidenavigation-toggle-${s}-padding` },
77
+ { label: 'icon color', groupKey: 'toggle-icon', variable: `--sidenavigation-toggle-${s}-icon` },
78
+ { label: 'icon size', canBeLinked: true, groupKey: 'toggle-icon-size', variable: `--sidenavigation-toggle-${s}-icon-size` },
79
+ ];
80
+ }
81
+
82
+ // --- Section ------------------------------------------------------------
83
+ // Section header is a CollapsibleSection wrapped in `.sn-section-header`;
84
+ // the wrapper paints surface + left indicator and forwards section text
85
+ // tokens into the inner CollapsibleSection (chromeless variant) by
86
+ // shadowing its slots, so section typography is editable per-state
87
+ // without modifying CollapsibleSection itself.
88
+ function sectionStateTokens(s: StatefulState): Token[] {
89
+ return [
90
+ { label: 'surface color', groupKey: 'section-surface', variable: `--sidenavigation-section-${s}-surface` },
91
+ { label: 'indicator color', groupKey: 'section-accent', variable: `--sidenavigation-section-${s}-accent` },
92
+ { label: 'indicator width', canBeLinked: true, groupKey: 'section-accent-width', variable: `--sidenavigation-section-${s}-accent-width` },
93
+ ];
94
+ }
95
+ function sectionStateTypeGroups(s: StatefulState): TypeGroupConfig[] {
96
+ return [{
97
+ legend: 'section text',
98
+ colorVariable: `--sidenavigation-section-${s}-text`,
99
+ familyVariable: `--sidenavigation-section-${s}-text-font-family`,
100
+ sizeVariable: `--sidenavigation-section-${s}-text-font-size`,
101
+ weightVariable: `--sidenavigation-section-${s}-text-font-weight`,
102
+ lineHeightVariable: `--sidenavigation-section-${s}-text-line-height`,
103
+ }];
104
+ }
105
+ const sectionTypographyTokens: Token[] = STATEFUL_STATES.flatMap((s) => [
106
+ { label: 'font family', canBeLinked: true, groupKey: 'section-text-font-family', variable: `--sidenavigation-section-${s}-text-font-family` },
107
+ { label: 'font size', canBeLinked: true, groupKey: 'section-text-font-size', variable: `--sidenavigation-section-${s}-text-font-size` },
108
+ { label: 'font weight', canBeLinked: true, groupKey: 'section-text-font-weight', variable: `--sidenavigation-section-${s}-text-font-weight` },
109
+ { label: 'line height', canBeLinked: true, groupKey: 'section-text-line-height', variable: `--sidenavigation-section-${s}-text-line-height` },
110
+ ]);
111
+
112
+ // --- Item ---------------------------------------------------------------
113
+ function itemStateTokens(s: StatefulState): Token[] {
114
+ return [
115
+ { label: 'surface color', groupKey: 'item-surface', variable: `--sidenavigation-item-${s}-surface` },
116
+ { label: 'padding', canBeLinked: true, groupKey: 'item-padding', variable: `--sidenavigation-item-${s}-padding` },
117
+ { label: 'indicator color', groupKey: 'item-accent', variable: `--sidenavigation-item-${s}-accent` },
118
+ { label: 'indicator width', canBeLinked: true, groupKey: 'item-accent-width', variable: `--sidenavigation-item-${s}-accent-width` },
119
+ ];
120
+ }
121
+ function itemStateTypeGroups(s: StatefulState): TypeGroupConfig[] {
122
+ return [{
123
+ legend: 'item text',
124
+ colorVariable: `--sidenavigation-item-${s}-text`,
125
+ familyVariable: `--sidenavigation-item-${s}-text-font-family`,
126
+ sizeVariable: `--sidenavigation-item-${s}-text-font-size`,
127
+ weightVariable: `--sidenavigation-item-${s}-text-font-weight`,
128
+ lineHeightVariable: `--sidenavigation-item-${s}-text-line-height`,
129
+ }];
130
+ }
131
+ const itemTypographyTokens: Token[] = STATEFUL_STATES.flatMap((s) => [
132
+ { label: 'font family', canBeLinked: true, groupKey: 'item-text-font-family', variable: `--sidenavigation-item-${s}-text-font-family` },
133
+ { label: 'font size', canBeLinked: true, groupKey: 'item-text-font-size', variable: `--sidenavigation-item-${s}-text-font-size` },
134
+ { label: 'font weight', canBeLinked: true, groupKey: 'item-text-font-weight', variable: `--sidenavigation-item-${s}-text-font-weight` },
135
+ { label: 'line height', canBeLinked: true, groupKey: 'item-text-line-height', variable: `--sidenavigation-item-${s}-text-line-height` },
136
+ ]);
137
+
138
+ // --- Footer -------------------------------------------------------------
139
+ function footerStateTokens(s: StatefulState): Token[] {
140
+ return [
141
+ { label: 'surface color', groupKey: 'footer-surface', variable: `--sidenavigation-footer-${s}-surface` },
142
+ { label: 'padding', canBeLinked: true, groupKey: 'footer-padding', variable: `--sidenavigation-footer-${s}-padding` },
143
+ { label: 'icon gap', groupKey: 'footer-gap', variable: `--sidenavigation-footer-${s}-gap` },
144
+ { label: 'indicator color', groupKey: 'footer-accent', variable: `--sidenavigation-footer-${s}-accent` },
145
+ { label: 'indicator width', canBeLinked: true, groupKey: 'footer-accent-width', variable: `--sidenavigation-footer-${s}-accent-width` },
146
+ { label: 'icon color', groupKey: 'footer-icon', variable: `--sidenavigation-footer-${s}-icon` },
147
+ { label: 'icon size', canBeLinked: true, groupKey: 'footer-icon-size', variable: `--sidenavigation-footer-${s}-icon-size` },
148
+ ];
149
+ }
150
+ function footerStateTypeGroups(s: StatefulState): TypeGroupConfig[] {
151
+ return [{
152
+ legend: 'footer text',
153
+ colorVariable: `--sidenavigation-footer-${s}-text`,
154
+ familyVariable: `--sidenavigation-footer-${s}-text-font-family`,
155
+ sizeVariable: `--sidenavigation-footer-${s}-text-font-size`,
156
+ weightVariable: `--sidenavigation-footer-${s}-text-font-weight`,
157
+ lineHeightVariable: `--sidenavigation-footer-${s}-text-line-height`,
158
+ }];
159
+ }
160
+ const footerTypographyTokens: Token[] = STATEFUL_STATES.flatMap((s) => [
161
+ { label: 'font family', canBeLinked: true, groupKey: 'footer-text-font-family', variable: `--sidenavigation-footer-${s}-text-font-family` },
162
+ { label: 'font size', canBeLinked: true, groupKey: 'footer-text-font-size', variable: `--sidenavigation-footer-${s}-text-font-size` },
163
+ { label: 'font weight', canBeLinked: true, groupKey: 'footer-text-font-weight', variable: `--sidenavigation-footer-${s}-text-font-weight` },
164
+ { label: 'line height', canBeLinked: true, groupKey: 'footer-text-line-height', variable: `--sidenavigation-footer-${s}-text-line-height` },
165
+ ]);
166
+
167
+ // Assemble part/state map. " / " separator triggers the two-tier strip.
168
+ const states: Record<string, Token[]> = {
169
+ 'Panel': panelTokens,
170
+ ...Object.fromEntries(STATEFUL_STATES.map((s) => [`Title / ${STATE_LABELS[s]}`, titleStateTokens(s)])),
171
+ ...Object.fromEntries(TOGGLE_STATES.map((s) => [`Toggle / ${STATE_LABELS[s]}`, toggleStateTokens(s)])),
172
+ ...Object.fromEntries(STATEFUL_STATES.map((s) => [`Section / ${STATE_LABELS[s]}`, sectionStateTokens(s)])),
173
+ ...Object.fromEntries(STATEFUL_STATES.map((s) => [`Item / ${STATE_LABELS[s]}`, itemStateTokens(s)])),
174
+ ...Object.fromEntries(STATEFUL_STATES.map((s) => [`Footer / ${STATE_LABELS[s]}`, footerStateTokens(s)])),
175
+ 'Animation': animationTokens,
176
+ };
177
+ const typeGroups: Record<string, TypeGroupConfig[]> = {
178
+ ...Object.fromEntries(STATEFUL_STATES.map((s) => [`Title / ${STATE_LABELS[s]}`, titleStateTypeGroups(s)])),
179
+ ...Object.fromEntries(STATEFUL_STATES.map((s) => [`Section / ${STATE_LABELS[s]}`, sectionStateTypeGroups(s)])),
180
+ ...Object.fromEntries(STATEFUL_STATES.map((s) => [`Item / ${STATE_LABELS[s]}`, itemStateTypeGroups(s)])),
181
+ ...Object.fromEntries(STATEFUL_STATES.map((s) => [`Footer / ${STATE_LABELS[s]}`, footerStateTypeGroups(s)])),
182
+ };
183
+
184
+ export const allTokens: Token[] = [
185
+ ...Object.values(states).flat(),
186
+ ...buildTypeGroupColorTokens(typeGroups),
187
+ ...titleTypographyTokens,
188
+ ...sectionTypographyTokens,
189
+ ...itemTypographyTokens,
190
+ ...footerTypographyTokens,
191
+ ];
192
+
193
+ // Link contexts: within each part, the per-state values share a link tree so
194
+ // a single edit can fan padding/border-width/typography across default/hover/active.
195
+ const linkableContexts = new Map<string, string>([
196
+ ...STATEFUL_STATES.flatMap((s): Array<[string, string]> => [
197
+ [`--sidenavigation-title-${s}-border-width`, `title ${s}`],
198
+ [`--sidenavigation-title-${s}-padding`, `title ${s}`],
199
+ [`--sidenavigation-title-${s}-accent-width`, `title ${s}`],
200
+ [`--sidenavigation-title-${s}-label-font-family`, `title ${s}`],
201
+ [`--sidenavigation-title-${s}-label-font-size`, `title ${s}`],
202
+ [`--sidenavigation-title-${s}-label-font-weight`, `title ${s}`],
203
+ [`--sidenavigation-title-${s}-label-line-height`, `title ${s}`],
204
+ ]),
205
+ ...TOGGLE_STATES.flatMap((s): Array<[string, string]> => [
206
+ [`--sidenavigation-toggle-${s}-border-width`, `toggle ${s}`],
207
+ [`--sidenavigation-toggle-${s}-radius`, `toggle ${s}`],
208
+ [`--sidenavigation-toggle-${s}-padding`, `toggle ${s}`],
209
+ [`--sidenavigation-toggle-${s}-icon-size`, `toggle ${s}`],
210
+ ]),
211
+ ...STATEFUL_STATES.flatMap((s): Array<[string, string]> => [
212
+ [`--sidenavigation-section-${s}-accent-width`, `section ${s}`],
213
+ [`--sidenavigation-section-${s}-text-font-family`, `section ${s}`],
214
+ [`--sidenavigation-section-${s}-text-font-size`, `section ${s}`],
215
+ [`--sidenavigation-section-${s}-text-font-weight`, `section ${s}`],
216
+ [`--sidenavigation-section-${s}-text-line-height`, `section ${s}`],
217
+ ]),
218
+ ...STATEFUL_STATES.flatMap((s): Array<[string, string]> => [
219
+ [`--sidenavigation-item-${s}-padding`, `item ${s}`],
220
+ [`--sidenavigation-item-${s}-accent-width`, `item ${s}`],
221
+ [`--sidenavigation-item-${s}-text-font-family`, `item ${s}`],
222
+ [`--sidenavigation-item-${s}-text-font-size`, `item ${s}`],
223
+ [`--sidenavigation-item-${s}-text-font-weight`, `item ${s}`],
224
+ [`--sidenavigation-item-${s}-text-line-height`, `item ${s}`],
225
+ ]),
226
+ ...STATEFUL_STATES.flatMap((s): Array<[string, string]> => [
227
+ [`--sidenavigation-footer-${s}-padding`, `footer ${s}`],
228
+ [`--sidenavigation-footer-${s}-accent-width`, `footer ${s}`],
229
+ [`--sidenavigation-footer-${s}-icon-size`, `footer ${s}`],
230
+ [`--sidenavigation-footer-${s}-text-font-family`, `footer ${s}`],
231
+ [`--sidenavigation-footer-${s}-text-font-size`, `footer ${s}`],
232
+ [`--sidenavigation-footer-${s}-text-font-weight`, `footer ${s}`],
233
+ [`--sidenavigation-footer-${s}-text-line-height`, `footer ${s}`],
234
+ ]),
235
+ ['--sidenavigation-panel-border-width', 'panel'],
236
+ ['--sidenavigation-panel-padding', 'panel'],
237
+ ]);
238
+ </script>
239
+
240
+ <script lang="ts">
241
+ import SideNavigation, { type SideNavSection, type SideNavFooter } from '../../system/components/SideNavigation.svelte';
242
+ import VariantGroup from './scaffolding/VariantGroup.svelte';
243
+ import ComponentEditorBase from './scaffolding/ComponentEditorBase.svelte';
244
+ import { editorState } from '../core/store/editorStore';
245
+ import { computeLinkedBlock, withLinkedDisabled } from './scaffolding/linkedBlock';
246
+
247
+ const demoSections: SideNavSection[] = [
248
+ {
249
+ path: 'section-1',
250
+ title: 'Section 1',
251
+ hasIndexPage: true,
252
+ items: [
253
+ { path: 'section-1/item-1', title: 'Item 1' },
254
+ { path: 'section-1/item-2', title: 'Item 2' },
255
+ { path: 'section-1/item-3', title: 'Item 3' },
256
+ ],
257
+ },
258
+ {
259
+ path: 'section-2',
260
+ title: 'Section 2',
261
+ hasIndexPage: true,
262
+ items: [],
263
+ },
264
+ ];
265
+ const demoFooter: SideNavFooter = {
266
+ path: 'footer',
267
+ title: 'Footer',
268
+ icon: 'fa-solid fa-file-lines',
269
+ };
270
+
271
+ let linked = $derived(computeLinkedBlock(component, linkableContexts, allTokens, $editorState));
272
+ let previewOpen = $state(true);
273
+
274
+ let visibleStates = $derived(Object.fromEntries(
275
+ Object.entries(states).map(([name, list]) => [name, withLinkedDisabled(list, linked.varSet)]),
276
+ ) as Record<string, Token[]>);
277
+
278
+ // Map the active part/state to the demo's force-hover / force-active hooks
279
+ // so token edits always have a row painted in that state for the viewer.
280
+ function deriveForce(activeState: string) {
281
+ const [part, sub] = activeState.includes(' / ') ? activeState.split(' / ') : [activeState, ''];
282
+ const partKey = part.toLowerCase();
283
+ const subKey = sub.toLowerCase();
284
+ let forceHoverPart: 'title' | 'toggle' | 'item' | 'footer' | 'section' | null = null;
285
+ let forceActivePart: 'title' | 'item' | 'footer' | 'section' | null = null;
286
+ if (subKey === 'hover' && (partKey === 'title' || partKey === 'toggle' || partKey === 'item' || partKey === 'footer' || partKey === 'section')) {
287
+ forceHoverPart = partKey;
288
+ } else if (subKey === 'active' && (partKey === 'title' || partKey === 'item' || partKey === 'footer' || partKey === 'section')) {
289
+ forceActivePart = partKey;
290
+ }
291
+ return { forceHoverPart, forceActivePart };
292
+ }
293
+ </script>
294
+
295
+ <ComponentEditorBase
296
+ {component}
297
+ title="Side Navigation"
298
+ description="Collapsible left panel with title header, sections, sub-page links, and an optional footer link."
299
+ tokens={allTokens}
300
+ {linked}
301
+ >
302
+ <VariantGroup
303
+ name="sidenavigation"
304
+ title="Side Navigation"
305
+ states={visibleStates}
306
+ {typeGroups}
307
+ {component}
308
+ selectorLabel="Part"
309
+ >
310
+ {#snippet children({ activeState })}
311
+ {@const { forceHoverPart, forceActivePart } = deriveForce(activeState)}
312
+ <div class="sn-preview-frame">
313
+ <SideNavigation
314
+ sections={demoSections}
315
+ footer={demoFooter}
316
+ titleLabel="Title"
317
+ titleHref="#"
318
+ currentPath="section-1/item-2"
319
+ open={previewOpen}
320
+ ontoggle={() => (previewOpen = !previewOpen)}
321
+ {forceHoverPart}
322
+ {forceActivePart}
323
+ />
324
+ </div>
325
+ {/snippet}
326
+ </VariantGroup>
327
+ </ComponentEditorBase>
328
+
329
+ <style>
330
+ /* Frame reserves room for the panel at its widest (so collapse animates into
331
+ empty space rather than reshaping the canvas), and lets the panel manage
332
+ its own width via the open/closed tokens. */
333
+ .sn-preview-frame {
334
+ min-width: 16rem;
335
+ min-height: 26rem;
336
+ display: flex;
337
+ align-items: stretch;
338
+ }
339
+ .sn-preview-frame :global(.sidenavigation) {
340
+ height: auto;
341
+ }
342
+ </style>
@@ -1,5 +1,20 @@
1
1
  export { default as ComponentsTab } from './scaffolding/ComponentsTab.svelte';
2
2
  export type { ComponentSection } from './scaffolding/componentSectionType';
3
- export { defaultSections } from './scaffolding/defaultSections';
3
+ export { getDefaultSections } from './scaffolding/defaultSections';
4
4
 
5
+ // Editor primitives for consumer-authored components.
6
+ export { default as ComponentEditorBase } from './scaffolding/ComponentEditorBase.svelte';
7
+ export { default as VariantGroup } from './scaffolding/VariantGroup.svelte';
8
+ export { default as LinkedBlock } from './scaffolding/LinkedBlock.svelte';
9
+ export { default as TypeEditor } from './scaffolding/TypeEditor.svelte';
5
10
  export { default as TokenLayout } from './scaffolding/TokenLayout.svelte';
11
+
12
+ // Helpers for assembling a VariantGroup's siblings and the linked-block view.
13
+ export { buildSiblings } from './scaffolding/siblings';
14
+ export type { Sibling } from './scaffolding/siblings';
15
+ export { computeLinkedBlock, withLinkedDisabled } from './scaffolding/linkedBlock';
16
+ export type { LinkedToken, LinkedGroup, LinkedBlockResult } from './scaffolding/linkedBlock';
17
+ export { buildTypeGroupTokens } from './scaffolding/buildTypeGroupTokens';
18
+
19
+ // Token schema type — the shape of an entry in an editor's `allTokens` array.
20
+ export type { Token } from './scaffolding/types';