@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.
- package/.claude/skills/live-tokens-add-component/SKILL.md +488 -0
- package/README.md +84 -29
- package/dist-plugin/index.cjs +177 -125
- package/dist-plugin/index.d.cts +3 -2
- package/dist-plugin/index.d.ts +3 -2
- package/dist-plugin/index.js +177 -125
- package/package.json +8 -2
- package/src/editor/component-editor/BadgeEditor.svelte +44 -42
- package/src/editor/component-editor/ButtonEditor.svelte +224 -0
- package/src/editor/component-editor/CollapsibleSectionEditor.svelte +1 -7
- package/src/editor/component-editor/CornerBadgeEditor.svelte +44 -34
- package/src/editor/component-editor/ImageLightboxEditor.svelte +58 -0
- package/src/editor/component-editor/InputEditor.svelte +272 -0
- package/src/editor/component-editor/NotificationEditor.svelte +44 -65
- package/src/editor/component-editor/ProgressBarEditor.svelte +71 -87
- package/src/editor/component-editor/SegmentedControlEditor.svelte +98 -37
- package/src/editor/component-editor/SideNavigationEditor.svelte +342 -0
- package/src/editor/component-editor/index.ts +16 -1
- package/src/editor/component-editor/registry.ts +138 -28
- package/src/editor/component-editor/scaffolding/ComponentFileManager.svelte +3 -2
- package/src/editor/component-editor/scaffolding/ComponentsTab.svelte +2 -2
- package/src/editor/component-editor/scaffolding/StateBlock.svelte +9 -10
- package/src/editor/component-editor/scaffolding/TokenLayout.svelte +60 -36
- package/src/editor/component-editor/scaffolding/VariantGroup.svelte +38 -1
- package/src/editor/component-editor/scaffolding/buildTypeGroupTokens.ts +1 -1
- package/src/editor/component-editor/scaffolding/componentSources.ts +3 -3
- package/src/editor/component-editor/scaffolding/defaultSections.ts +15 -10
- package/src/editor/component-editor/scaffolding/siblings.ts +2 -2
- package/src/editor/component-editor/scaffolding/types.ts +2 -1
- package/src/editor/core/components/componentConfigKeys.ts +14 -3
- package/src/editor/core/components/componentConfigService.ts +7 -6
- package/src/editor/core/manifests/manifestService.ts +5 -4
- package/src/editor/core/storage/apiBase.ts +15 -0
- package/src/editor/core/storage/files/versionedFileResourceClient.ts +1 -1
- package/src/editor/core/themes/migrations/2026-05-24-collapsiblesection-drop-active-state.ts +28 -0
- package/src/editor/core/themes/migrations/2026-05-24-progressbar-collapse-variants.ts +41 -0
- package/src/editor/core/themes/migrations/2026-05-24-promote-state-shared-tokens.ts +59 -0
- package/src/editor/core/themes/migrations/2026-05-24-segmentedcontrol-divider-inset.ts +29 -0
- package/src/editor/core/themes/migrations/2026-05-25-cornerbadge-flatten-variants.ts +46 -0
- package/src/editor/core/themes/migrations/index.ts +10 -0
- package/src/editor/core/themes/slices/components.ts +9 -0
- package/src/editor/core/themes/themeInit.ts +3 -2
- package/src/editor/core/themes/themeService.ts +3 -2
- package/src/editor/index.ts +10 -1
- package/src/editor/pages/ComponentEditorPage.svelte +53 -3
- package/src/editor/pages/EditorShell.svelte +53 -3
- package/src/editor/ui/UIEasingSelector.svelte +240 -0
- package/src/editor/ui/variantScales.ts +34 -0
- package/src/system/components/Button.svelte +34 -85
- package/src/system/components/CollapsibleSection.svelte +1 -48
- package/src/system/components/CornerBadge.svelte +72 -138
- package/src/system/components/Dialog.svelte +24 -4
- package/src/system/components/ImageLightbox.svelte +578 -0
- package/src/system/components/Input.svelte +387 -0
- package/src/system/components/ProgressBar.svelte +62 -258
- package/src/system/components/SectionDivider.svelte +117 -43
- package/src/system/components/SegmentedControl.svelte +81 -15
- package/src/system/components/SideNavigation.svelte +777 -0
- package/src/system/styles/tokens.css +43 -0
- package/src/system/styles/tokens.generated.css +4 -183
- 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
|
|
8
|
-
// and
|
|
9
|
-
|
|
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
|
|
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
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
//
|
|
90
|
-
//
|
|
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[] = [
|
|
129
|
+
export const allTokens: Token[] = [
|
|
130
|
+
...Object.values(defaultStates).flat(),
|
|
131
|
+
...Object.values(smallStates).flat(),
|
|
132
|
+
...typeGroupTokens,
|
|
133
|
+
];
|
|
93
134
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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(
|
|
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
|
-
{
|
|
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
|
-
|
|
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
|
-
|
|
156
|
-
|
|
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 {
|
|
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';
|