@motion-proto/live-tokens 0.9.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/README.md +50 -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 +4 -1
- 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/registry.ts +35 -2
- package/src/editor/component-editor/scaffolding/ComponentFileManager.svelte +3 -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/siblings.ts +2 -2
- package/src/editor/component-editor/scaffolding/types.ts +2 -1
- 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/themeInit.ts +3 -2
- package/src/editor/core/themes/themeService.ts +3 -2
- 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/ImageLightbox.svelte +578 -0
- package/src/system/components/Input.svelte +387 -0
- package/src/system/components/ProgressBar.svelte +62 -258
- 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
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
import { CURRENT_COMPONENT_SCHEMA_VERSION } from '../../core/themes/migrations';
|
|
29
29
|
import type { CssVarRef } from '../../core/store/editorTypes';
|
|
30
30
|
import { safeFetch } from '../../core/storage/storage';
|
|
31
|
+
import { API_BASE } from '../../core/storage/apiBase';
|
|
31
32
|
import { flashStatus } from '../../core/flashStatus';
|
|
32
33
|
import ComponentFileMenu from './ComponentFileMenu.svelte';
|
|
33
34
|
import SaveAsDialog from './SaveAsDialog.svelte';
|
|
@@ -91,7 +92,7 @@
|
|
|
91
92
|
const data = await safeFetch<{
|
|
92
93
|
files: ComponentConfigMeta[];
|
|
93
94
|
activeFile: string;
|
|
94
|
-
}>(
|
|
95
|
+
}>(`${API_BASE}/component-configs/${encodeURIComponent(component)}`);
|
|
95
96
|
if (!data) return;
|
|
96
97
|
files = data.files;
|
|
97
98
|
activeFileName = data.activeFile;
|
|
@@ -103,7 +104,7 @@
|
|
|
103
104
|
// Preserve existing productionInfo on transient fetch failure rather than
|
|
104
105
|
// clobbering it to null — same behaviour as the previous empty catch.
|
|
105
106
|
const info = await safeFetch<ComponentProductionInfo>(
|
|
106
|
-
|
|
107
|
+
`${API_BASE}/component-configs/${encodeURIComponent(component)}/production`,
|
|
107
108
|
);
|
|
108
109
|
if (info) productionInfo = info;
|
|
109
110
|
}
|
|
@@ -250,19 +250,18 @@
|
|
|
250
250
|
padding-top: calc(var(--ui-font-size-xs) + var(--ui-space-4));
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
-
/* Element-grouped mode: subsections
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
to their top edge instead of stretching the shorter ones.
|
|
253
|
+
/* Element-grouped mode: subsections sit flush against their content width
|
|
254
|
+
with a 1rem gap between them, wrapping to a new row when the panel is too
|
|
255
|
+
narrow to fit them side-by-side. Flex (not grid 1fr) so wide viewports
|
|
256
|
+
don't spread the columns apart — sections cluster left and consume only
|
|
257
|
+
as much width as their controls need.
|
|
259
258
|
Within a section the two-col split (typography fieldsets + property grid)
|
|
260
259
|
still applies when the section has both. */
|
|
261
260
|
.state-controls.element-grouped {
|
|
262
|
-
display:
|
|
263
|
-
|
|
264
|
-
gap: var(--ui-space-
|
|
265
|
-
align-items: start;
|
|
261
|
+
display: flex;
|
|
262
|
+
flex-wrap: wrap;
|
|
263
|
+
gap: var(--ui-space-16);
|
|
264
|
+
align-items: flex-start;
|
|
266
265
|
}
|
|
267
266
|
|
|
268
267
|
/* Each element section stacks typography fieldset(s) above the property
|
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
import UIFontSizeSelector from '../../ui/UIFontSizeSelector.svelte';
|
|
8
8
|
import UILineHeightSelector from '../../ui/UILineHeightSelector.svelte';
|
|
9
9
|
import UIPaddingSelector from '../../ui/UIPaddingSelector.svelte';
|
|
10
|
-
import
|
|
10
|
+
import UILetterSpacingSelector from '../../ui/UILetterSpacingSelector.svelte';
|
|
11
|
+
import UIEasingSelector from '../../ui/UIEasingSelector.svelte';
|
|
12
|
+
import { BLUR, BORDER_WIDTH, DOT_SIZE, DURATION, RADIUS, SHADOW, DIVIDER_HEIGHT, DIVIDER_INSET } from '../../ui/variantScales';
|
|
11
13
|
import {
|
|
12
14
|
editorState,
|
|
13
15
|
getComponentPropertySiblings,
|
|
@@ -26,6 +28,7 @@
|
|
|
26
28
|
| 'radius'
|
|
27
29
|
| 'divider-width'
|
|
28
30
|
| 'divider-height'
|
|
31
|
+
| 'divider-inset'
|
|
29
32
|
| 'dot-size'
|
|
30
33
|
| 'blur'
|
|
31
34
|
| 'shadow'
|
|
@@ -33,10 +36,13 @@
|
|
|
33
36
|
| 'font-weight'
|
|
34
37
|
| 'font-size'
|
|
35
38
|
| 'line-height'
|
|
39
|
+
| 'letter-spacing'
|
|
36
40
|
| 'padding'
|
|
37
41
|
| 'padding-split'
|
|
38
42
|
| 'gap'
|
|
39
|
-
| '
|
|
43
|
+
| 'duration'
|
|
44
|
+
| 'easing'
|
|
45
|
+
| 'text-color';
|
|
40
46
|
|
|
41
47
|
type Entry = { kind: Kind; token: Token };
|
|
42
48
|
|
|
@@ -75,44 +81,61 @@
|
|
|
75
81
|
onchange,
|
|
76
82
|
}: Props = $props();
|
|
77
83
|
|
|
78
|
-
/** Suffix/prefix patterns mapped to kinds — single source of truth used by `
|
|
79
|
-
Order matters:
|
|
80
|
-
otherwise match `surface`
|
|
84
|
+
/** Suffix/prefix patterns mapped to kinds — single source of truth used by `rawKind`.
|
|
85
|
+
Order matters: `-text` must run before `-border`/`-surface` because `--text-*`
|
|
86
|
+
would otherwise match `surface`/`border` if any pattern overlapped. Variables
|
|
87
|
+
that don't match any pattern fall through to `text-color` (renders as a palette
|
|
88
|
+
picker). Tokens with unconventional suffixes should be renamed. */
|
|
81
89
|
const KIND_PATTERNS: Array<{ kind: Kind; matches: (v: string) => boolean }> = [
|
|
82
|
-
{ kind: 'font-family',
|
|
83
|
-
{ kind: 'font-weight',
|
|
84
|
-
{ kind: 'font-size',
|
|
85
|
-
{ kind: 'line-height',
|
|
86
|
-
{ kind: '
|
|
87
|
-
{ kind: '
|
|
88
|
-
{ kind: '
|
|
90
|
+
{ kind: 'font-family', matches: (v) => v.endsWith('-font-family') },
|
|
91
|
+
{ kind: 'font-weight', matches: (v) => v.endsWith('-font-weight') },
|
|
92
|
+
{ kind: 'font-size', matches: (v) => v.endsWith('-font-size') || v.endsWith('-icon-size') },
|
|
93
|
+
{ kind: 'line-height', matches: (v) => v.endsWith('-line-height') },
|
|
94
|
+
{ kind: 'letter-spacing', matches: (v) => v.endsWith('-letter-spacing') },
|
|
95
|
+
{ kind: 'text-color', matches: (v) => v.endsWith('-text') || v.startsWith('--text-') },
|
|
96
|
+
{ kind: 'radius', matches: (v) => v.endsWith('-radius') || v.startsWith('--radius-') },
|
|
97
|
+
{ kind: 'divider-width', matches: (v) => v.endsWith('-divider-width') || v.endsWith('-divider-thickness') },
|
|
89
98
|
{ kind: 'divider-height', matches: (v) => v.endsWith('-divider-height') || v.endsWith('-track-height') },
|
|
90
|
-
{ kind: '
|
|
91
|
-
{ kind: '
|
|
92
|
-
{ kind: '
|
|
93
|
-
{ kind: '
|
|
94
|
-
{ kind: '
|
|
95
|
-
{ kind: '
|
|
96
|
-
{ kind: '
|
|
97
|
-
{ kind: '
|
|
99
|
+
{ kind: 'divider-inset', matches: (v) => v.endsWith('-divider-inset') },
|
|
100
|
+
{ kind: 'dot-size', matches: (v) => v.endsWith('-dot-size') },
|
|
101
|
+
{ kind: 'blur', matches: (v) => v.endsWith('-blur') || v.startsWith('--blur-') },
|
|
102
|
+
{ kind: 'shadow', matches: (v) => v.endsWith('-shadow') || v.startsWith('--shadow-') },
|
|
103
|
+
{ kind: 'padding', matches: (v) => v.endsWith('-padding') || v.endsWith('-margin') },
|
|
104
|
+
{ kind: 'gap', matches: (v) => v.endsWith('-gap') },
|
|
105
|
+
{ kind: 'duration', matches: (v) => v.endsWith('-duration') || v.startsWith('--duration-') },
|
|
106
|
+
{ kind: 'easing', matches: (v) => v.endsWith('-easing') || v.startsWith('--ease-') },
|
|
107
|
+
{ kind: 'border-width', matches: (v) => v.endsWith('-border-width') || v.endsWith('-accent-width') || v.endsWith('-hairline-thickness') || v.startsWith('--border-width-') },
|
|
108
|
+
{ kind: 'border', matches: (v) => v.endsWith('-border') || v.startsWith('--border-') },
|
|
109
|
+
{ kind: 'surface', matches: (v) => v.endsWith('-surface') || v.startsWith('--surface-') },
|
|
98
110
|
];
|
|
99
111
|
|
|
112
|
+
function rawKind(variable: string): Kind {
|
|
113
|
+
for (const { kind, matches } of KIND_PATTERNS) {
|
|
114
|
+
if (matches(variable)) return kind;
|
|
115
|
+
}
|
|
116
|
+
return 'text-color';
|
|
117
|
+
}
|
|
118
|
+
|
|
100
119
|
/** Fixed internal order for tokens within a layout. `padding-split` co-orders with `padding`. */
|
|
101
120
|
const baseKindOrder: Kind[] = [
|
|
102
121
|
'font-family',
|
|
103
122
|
'font-weight',
|
|
104
123
|
'font-size',
|
|
105
124
|
'line-height',
|
|
125
|
+
'letter-spacing',
|
|
106
126
|
'divider-width',
|
|
107
127
|
'divider-height',
|
|
128
|
+
'divider-inset',
|
|
108
129
|
'dot-size',
|
|
109
130
|
'radius',
|
|
110
131
|
'padding',
|
|
111
132
|
'padding-split',
|
|
112
133
|
'gap',
|
|
134
|
+
'duration',
|
|
135
|
+
'easing',
|
|
113
136
|
'blur',
|
|
114
137
|
'shadow',
|
|
115
|
-
'
|
|
138
|
+
'text-color',
|
|
116
139
|
'surface',
|
|
117
140
|
'border-width',
|
|
118
141
|
'border',
|
|
@@ -121,13 +144,6 @@
|
|
|
121
144
|
baseKindOrder.map((k, i) => [k, i]),
|
|
122
145
|
) as Record<Kind, number>;
|
|
123
146
|
|
|
124
|
-
function rawKind(v: string): Kind {
|
|
125
|
-
for (const { kind, matches } of KIND_PATTERNS) {
|
|
126
|
-
if (matches(v)) return kind;
|
|
127
|
-
}
|
|
128
|
-
return 'extras';
|
|
129
|
-
}
|
|
130
|
-
|
|
131
147
|
/** A padding token is "split" when its per-side variables exist for this component. */
|
|
132
148
|
function paddingIsSplit(varName: string, comp: string | undefined, state: typeof $editorState): boolean {
|
|
133
149
|
const sides = ['top', 'right', 'bottom', 'left'];
|
|
@@ -148,8 +164,8 @@
|
|
|
148
164
|
}
|
|
149
165
|
|
|
150
166
|
/** For sibling/grouping checks we want the canonical kind, not the split-vs-single distinction. */
|
|
151
|
-
function groupingKind(
|
|
152
|
-
return rawKind(
|
|
167
|
+
function groupingKind(variable: string): Kind {
|
|
168
|
+
return rawKind(variable);
|
|
153
169
|
}
|
|
154
170
|
|
|
155
171
|
/** Selector registry: one entry per kind. `extra` props (e.g. UIPaddingSelector's
|
|
@@ -167,9 +183,11 @@
|
|
|
167
183
|
'font-weight': { component: UIFontWeightSelector },
|
|
168
184
|
'font-size': { component: UIFontSizeSelector },
|
|
169
185
|
'line-height': { component: UILineHeightSelector },
|
|
186
|
+
'letter-spacing': { component: UILetterSpacingSelector },
|
|
170
187
|
'border-width': { component: UIVariantSelector, extra: () => ({ ...BORDER_WIDTH }) },
|
|
171
188
|
'divider-width': { component: UIVariantSelector, extra: () => ({ ...BORDER_WIDTH }) },
|
|
172
189
|
'divider-height': { component: UIVariantSelector, extra: () => ({ ...DIVIDER_HEIGHT }) },
|
|
190
|
+
'divider-inset': { component: UIVariantSelector, extra: () => ({ ...DIVIDER_INSET }) },
|
|
173
191
|
'dot-size': { component: UIVariantSelector, extra: () => ({ ...DOT_SIZE }) },
|
|
174
192
|
'radius': { component: UIVariantSelector, extra: () => ({ ...RADIUS }) },
|
|
175
193
|
'padding': { component: UIPaddingSelector, extra: (t) => ({ mode: 'single', splittable: t.splittable !== false }) },
|
|
@@ -184,31 +202,37 @@
|
|
|
184
202
|
extra: () => ({ mode: 'sides' }),
|
|
185
203
|
},
|
|
186
204
|
'gap': { component: UIPaddingSelector, extra: () => ({ mode: 'single', splittable: false }) },
|
|
205
|
+
'duration': { component: UIVariantSelector, extra: () => ({ ...DURATION }) },
|
|
206
|
+
'easing': { component: UIEasingSelector },
|
|
187
207
|
'blur': { component: UIVariantSelector, extra: () => ({ ...BLUR }) },
|
|
188
208
|
'shadow': { component: UIVariantSelector, extra: () => ({ ...SHADOW }) },
|
|
189
209
|
'surface': { component: UIPaletteSelector },
|
|
190
210
|
'border': { component: UIPaletteSelector },
|
|
191
|
-
'
|
|
211
|
+
'text-color': { component: UIPaletteSelector },
|
|
192
212
|
};
|
|
193
213
|
|
|
194
|
-
/** Multi-col rank: same as `orderRank` but with `
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
already segregates
|
|
214
|
+
/** Multi-col rank: same as `orderRank` but with `text-color` hoisted between
|
|
215
|
+
`line-height` and `divider-width` so typography reads as one logical block
|
|
216
|
+
in column flow. Single-col mode keeps `orderRank` (linked-first sort
|
|
217
|
+
already segregates text-color to the bottom). */
|
|
198
218
|
const multiColRank: Record<Kind, number> = (() => {
|
|
199
219
|
const reordered: Kind[] = [
|
|
200
220
|
'font-family',
|
|
201
221
|
'font-weight',
|
|
202
222
|
'font-size',
|
|
203
223
|
'line-height',
|
|
204
|
-
'
|
|
224
|
+
'letter-spacing',
|
|
225
|
+
'text-color',
|
|
205
226
|
'divider-width',
|
|
206
227
|
'divider-height',
|
|
228
|
+
'divider-inset',
|
|
207
229
|
'dot-size',
|
|
208
230
|
'radius',
|
|
209
231
|
'padding',
|
|
210
232
|
'padding-split',
|
|
211
233
|
'gap',
|
|
234
|
+
'duration',
|
|
235
|
+
'easing',
|
|
212
236
|
'blur',
|
|
213
237
|
'shadow',
|
|
214
238
|
'surface',
|
|
@@ -582,10 +582,47 @@
|
|
|
582
582
|
}
|
|
583
583
|
|
|
584
584
|
.preview-actions {
|
|
585
|
-
|
|
585
|
+
display: inline-flex;
|
|
586
|
+
align-items: center;
|
|
587
|
+
gap: var(--ui-space-12);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/* Labels inside previewActions read at the UI body size, matching the
|
|
591
|
+
variant tab strip they sit beside. Greyscale only. */
|
|
592
|
+
.preview-actions :global(label) {
|
|
586
593
|
display: inline-flex;
|
|
587
594
|
align-items: center;
|
|
588
595
|
gap: var(--ui-space-8);
|
|
596
|
+
font-size: var(--ui-font-size-md);
|
|
597
|
+
color: var(--ui-text-secondary);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/* Native <select> styled to match the property-row trigger chrome so
|
|
601
|
+
toolbar selects share one visual vocabulary with property dropdowns. */
|
|
602
|
+
.preview-actions :global(select) {
|
|
603
|
+
appearance: none;
|
|
604
|
+
-webkit-appearance: none;
|
|
605
|
+
padding: 0 var(--ui-space-24) 0 var(--ui-space-10);
|
|
606
|
+
min-height: 1.75rem;
|
|
607
|
+
background-color: var(--ui-surface-low);
|
|
608
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 12 12'%3E%3Cpath fill='none' stroke='%23999' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' d='M3 5l3 3 3-3'/%3E%3C/svg%3E");
|
|
609
|
+
background-repeat: no-repeat;
|
|
610
|
+
background-position: right var(--ui-space-8) center;
|
|
611
|
+
border: 1px solid var(--ui-border);
|
|
612
|
+
border-radius: var(--ui-radius-md);
|
|
613
|
+
color: var(--ui-text-primary);
|
|
614
|
+
font-family: var(--ui-font-sans);
|
|
615
|
+
font-size: var(--ui-font-size-md);
|
|
616
|
+
cursor: pointer;
|
|
617
|
+
transition: background-color var(--ui-transition-fast), border-color var(--ui-transition-fast);
|
|
618
|
+
}
|
|
619
|
+
.preview-actions :global(select:hover) {
|
|
620
|
+
background-color: var(--ui-surface-high);
|
|
621
|
+
border-color: var(--ui-border-higher);
|
|
622
|
+
}
|
|
623
|
+
.preview-actions :global(select:focus-visible) {
|
|
624
|
+
outline: 2px solid var(--ui-highlight);
|
|
625
|
+
outline-offset: 2px;
|
|
589
626
|
}
|
|
590
627
|
|
|
591
628
|
/* Sits in the backdrop's right-rail column (ShadowBackdrop owns the two-column
|
|
@@ -31,7 +31,7 @@ export type BuildTypeGroupTokensOptions = {
|
|
|
31
31
|
out of the linked block while still appearing in the editor's full token surface
|
|
32
32
|
(used by the reset-button and the design-token resolution test).
|
|
33
33
|
|
|
34
|
-
Mirrors the `flatMap`/loop pattern in
|
|
34
|
+
Mirrors the `flatMap`/loop pattern in ButtonEditor and RadioButtonEditor so
|
|
35
35
|
editors don't have to hand-list 16+ near-identical Token entries. */
|
|
36
36
|
export function buildTypeGroupTokens(
|
|
37
37
|
typeGroups: Record<string, TypeGroupConfig[]>,
|
|
@@ -13,8 +13,8 @@ export type Sibling = {
|
|
|
13
13
|
|
|
14
14
|
`variantStates(v)` and `variantTypeGroups(v)` return the same shape the
|
|
15
15
|
parent VariantGroup gets for its own `states` / `typeGroups` props — a map
|
|
16
|
-
keyed by state name. For single-state-per-variant editors (Badge,
|
|
17
|
-
|
|
16
|
+
keyed by state name. For single-state-per-variant editors (Badge,
|
|
17
|
+
Notification), wrap the single-state builders inline:
|
|
18
18
|
`(v) => ({ [v]: variantTokens(v) })`. */
|
|
19
19
|
export function buildSiblings<V extends string>(
|
|
20
20
|
variants: readonly V[],
|
|
@@ -27,7 +27,8 @@ export type Token = {
|
|
|
27
27
|
/** Hint to the editor that this token's alias is a structured payload
|
|
28
28
|
(currently only `kind: 'gradient'`). Drives Copy-from's per-kind
|
|
29
29
|
branch — gradient aliases need family-swap of in-family stop colors
|
|
30
|
-
rather than a verbatim ref copy.
|
|
30
|
+
rather than a verbatim ref copy. Distinct from `picker`: `kind`
|
|
31
|
+
marks the value's data shape; `picker` selects the editor control. */
|
|
31
32
|
kind?: 'gradient';
|
|
32
33
|
/** Color-family slug for this token's owning variant (e.g. `brand`,
|
|
33
34
|
`accent`). Set on gradient-kind tokens so Copy-from's family-swap
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import type { AliasDiskValue, ComponentConfig, ComponentConfigMeta } from '../themes/themeTypes';
|
|
2
2
|
import { versionedFileResource } from '../storage/files/versionedFileResourceClient';
|
|
3
|
+
import { API_BASE } from '../storage/apiBase';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* REST client for per-component config files. Parallel to `themeService.ts`
|
|
6
|
-
* but scoped to
|
|
7
|
-
* has its own lifecycle: default.json (generated from the `.svelte`
|
|
8
|
-
* plus user-authored named configs, each with its own active /
|
|
9
|
-
* pointer.
|
|
7
|
+
* but scoped to `${API_BASE}/component-configs/*`. Each component (button,
|
|
8
|
+
* card, …) has its own lifecycle: default.json (generated from the `.svelte`
|
|
9
|
+
* source), plus user-authored named configs, each with its own active /
|
|
10
|
+
* production pointer.
|
|
10
11
|
*
|
|
11
12
|
* Both this and `themeService` consume `versionedFileResource(...)`. Adding a
|
|
12
13
|
* third file-managed resource — per the user's "mirror theme-file lifecycle
|
|
@@ -34,7 +35,7 @@ export interface ComponentConfigList {
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
export async function listComponents(): Promise<ComponentSummary[]> {
|
|
37
|
-
const res = await fetch(
|
|
38
|
+
const res = await fetch(`${API_BASE}/component-configs`);
|
|
38
39
|
if (!res.ok) throw new Error('Failed to list components');
|
|
39
40
|
const data = await res.json();
|
|
40
41
|
return data.components;
|
|
@@ -42,7 +43,7 @@ export async function listComponents(): Promise<ComponentSummary[]> {
|
|
|
42
43
|
|
|
43
44
|
function resourceFor(component: string) {
|
|
44
45
|
return versionedFileResource<ComponentConfig, ComponentConfigMeta, ComponentProductionInfo>({
|
|
45
|
-
baseUrl:
|
|
46
|
+
baseUrl: `${API_BASE}/component-configs/${encodeURIComponent(component)}`,
|
|
46
47
|
});
|
|
47
48
|
}
|
|
48
49
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Manifest, ManifestMeta, ManifestBundle, Theme, ComponentConfig } from '../themes/themeTypes';
|
|
2
2
|
import { versionedFileResource } from '../storage/files/versionedFileResourceClient';
|
|
3
|
+
import { API_BASE } from '../storage/apiBase';
|
|
3
4
|
import { listComponents } from '../components/componentConfigService';
|
|
4
5
|
import { getActiveTheme } from '../themes/themeService';
|
|
5
6
|
|
|
@@ -13,7 +14,7 @@ import { getActiveTheme } from '../themes/themeService';
|
|
|
13
14
|
*/
|
|
14
15
|
|
|
15
16
|
const manifestsResource = versionedFileResource<Manifest, ManifestMeta, never>({
|
|
16
|
-
baseUrl:
|
|
17
|
+
baseUrl: `${API_BASE}/manifests`,
|
|
17
18
|
});
|
|
18
19
|
|
|
19
20
|
export const listManifests = async (): Promise<ManifestMeta[]> => {
|
|
@@ -47,7 +48,7 @@ export interface ApplyManifestResult {
|
|
|
47
48
|
* world" action.
|
|
48
49
|
*/
|
|
49
50
|
export async function applyManifest(fileName: string): Promise<ApplyManifestResult> {
|
|
50
|
-
const res = await fetch(
|
|
51
|
+
const res = await fetch(`${API_BASE}/manifests/${encodeURIComponent(fileName)}/apply`, {
|
|
51
52
|
method: 'PUT',
|
|
52
53
|
});
|
|
53
54
|
if (!res.ok) {
|
|
@@ -132,7 +133,7 @@ export interface ImportManifestResult {
|
|
|
132
133
|
* See temp/manifest-robustness-plan.md §11.
|
|
133
134
|
*/
|
|
134
135
|
export async function exportManifest(fileName: string): Promise<void> {
|
|
135
|
-
const res = await fetch(
|
|
136
|
+
const res = await fetch(`${API_BASE}/manifests/${encodeURIComponent(fileName)}/export`);
|
|
136
137
|
if (!res.ok) {
|
|
137
138
|
const err = await res.json().catch(() => ({ error: 'Export failed' }));
|
|
138
139
|
throw new Error(err.error || 'Export failed');
|
|
@@ -158,7 +159,7 @@ export async function exportManifest(fileName: string): Promise<void> {
|
|
|
158
159
|
* the rename map so the UI can surface what got renamed.
|
|
159
160
|
*/
|
|
160
161
|
export async function importManifest(bundle: ManifestBundle): Promise<ImportManifestResult> {
|
|
161
|
-
const res = await fetch(
|
|
162
|
+
const res = await fetch(`${API_BASE}/manifests/import`, {
|
|
162
163
|
method: 'POST',
|
|
163
164
|
headers: { 'Content-Type': 'application/json' },
|
|
164
165
|
body: JSON.stringify(bundle),
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolved API base path for the editor's REST client. Injected by the
|
|
3
|
+
* `themeFileApi` vite plugin via `config().define` so the client and server
|
|
4
|
+
* always agree on the route prefix, even when a consumer overrides `apiBase`.
|
|
5
|
+
*
|
|
6
|
+
* Fallback (no plugin running) matches the plugin's default. The strings must
|
|
7
|
+
* stay in sync — there is no single source of truth across the build-time /
|
|
8
|
+
* runtime boundary, but the cost of one constant in two files is small.
|
|
9
|
+
*/
|
|
10
|
+
declare const __LIVE_TOKENS_API_BASE__: string | undefined;
|
|
11
|
+
|
|
12
|
+
export const API_BASE: string =
|
|
13
|
+
typeof __LIVE_TOKENS_API_BASE__ !== 'undefined' && __LIVE_TOKENS_API_BASE__
|
|
14
|
+
? __LIVE_TOKENS_API_BASE__
|
|
15
|
+
: '/api/live-tokens';
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
export interface VersionedFileResourceClientOptions {
|
|
15
|
-
/** REST endpoint root, e.g.
|
|
15
|
+
/** REST endpoint root, e.g. `${API_BASE}/themes` or `${API_BASE}/component-configs/button`. */
|
|
16
16
|
baseUrl: string;
|
|
17
17
|
}
|
|
18
18
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Migration } from './index';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 2026-05-24: drop the `active` header state from collapsiblesection.
|
|
5
|
+
*
|
|
6
|
+
* The state only paid rent when the component was used as a nav link (an
|
|
7
|
+
* externally-supplied `active={true}` paired with `href`). No consumer was
|
|
8
|
+
* using it, so the runtime now omits both the prop and the `&.active` CSS
|
|
9
|
+
* branch. Strip the matching saved aliases from every variant.
|
|
10
|
+
*/
|
|
11
|
+
function dropActiveState(rawVars: Record<string, string>, meta: { component?: string }): Record<string, string> {
|
|
12
|
+
if (meta.component !== 'collapsiblesection') return rawVars;
|
|
13
|
+
const re = /^--collapsiblesection-(chromeless|divider|container)-active-/;
|
|
14
|
+
const out: Record<string, string> = {};
|
|
15
|
+
for (const [key, value] of Object.entries(rawVars)) {
|
|
16
|
+
if (re.test(key)) continue;
|
|
17
|
+
out[key] = value;
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const componentMigration_2026_05_24_collapsiblesectionDropActiveState: Migration = {
|
|
23
|
+
id: '2026-05-24-collapsiblesection-drop-active-state',
|
|
24
|
+
fromVersion: 14,
|
|
25
|
+
toVersion: 15,
|
|
26
|
+
appliesTo: 'component-config',
|
|
27
|
+
apply: dropActiveState,
|
|
28
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Migration } from './index';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Component-config migration (2026-05-24, progressbar only): ProgressBar
|
|
5
|
+
* collapsed from a per-variant token namespace (primary/success/warning/
|
|
6
|
+
* danger/info) to a single token set. Fill color is now a runtime `fill`
|
|
7
|
+
* prop on the consumer side, not a variant axis.
|
|
8
|
+
*
|
|
9
|
+
* Strategy: keep the `primary` namespace's values as the canonical defaults
|
|
10
|
+
* (rename `--progressbar-primary-X` → `--progressbar-X`); drop the four
|
|
11
|
+
* remaining variant namespaces entirely. Consumers that previously selected
|
|
12
|
+
* a non-primary variant lose their variant-specific surface/border/fill
|
|
13
|
+
* customizations and fall back to the merged single set.
|
|
14
|
+
*/
|
|
15
|
+
const PRIMARY_PREFIX = '--progressbar-primary-';
|
|
16
|
+
const NEW_PREFIX = '--progressbar-';
|
|
17
|
+
const DROP_PREFIXES = [
|
|
18
|
+
'--progressbar-success-',
|
|
19
|
+
'--progressbar-warning-',
|
|
20
|
+
'--progressbar-danger-',
|
|
21
|
+
'--progressbar-info-',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
export const componentMigration_2026_05_24_progressbarCollapseVariants: Migration = {
|
|
25
|
+
id: '2026-05-24-progressbar-collapse-variants',
|
|
26
|
+
fromVersion: 13,
|
|
27
|
+
toVersion: 14,
|
|
28
|
+
appliesTo: 'component-config',
|
|
29
|
+
apply(rawVars, meta) {
|
|
30
|
+
if (meta.component !== 'progressbar') return { ...rawVars };
|
|
31
|
+
const out: Record<string, string> = {};
|
|
32
|
+
for (const [oldKey, value] of Object.entries(rawVars)) {
|
|
33
|
+
if (DROP_PREFIXES.some((p) => oldKey.startsWith(p))) continue;
|
|
34
|
+
const key = oldKey.startsWith(PRIMARY_PREFIX)
|
|
35
|
+
? NEW_PREFIX + oldKey.slice(PRIMARY_PREFIX.length)
|
|
36
|
+
: oldKey;
|
|
37
|
+
if (!(key in out)) out[key] = value;
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Migration } from './index';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Component-config migration (2026-05-24, button + segmentedcontrol):
|
|
5
|
+
*
|
|
6
|
+
* Promotes shape/icon-size properties that don't actually vary per state into
|
|
7
|
+
* their own "base" part. The editor's per-state shape rows were dead UI — the
|
|
8
|
+
* runtime treated them as separate tokens that were always linked anyway. The
|
|
9
|
+
* runtime CSS now reads the default-state token in hover/disabled rules, so
|
|
10
|
+
* the per-state tokens are dropped entirely.
|
|
11
|
+
*
|
|
12
|
+
* Saved per-state overrides are dropped (rare case; users re-customize once).
|
|
13
|
+
* The default-state value remains the authoritative one.
|
|
14
|
+
*
|
|
15
|
+
* button drops --button-{v}-{hover|disabled}-{padding|radius|border-width}
|
|
16
|
+
* for v in primary/secondary/outline/success/danger/warning.
|
|
17
|
+
*
|
|
18
|
+
* segmentedcontrol drops --segmentedcontrol-{selected|option-hover|disabled}-icon-size.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const BUTTON_VARIANTS = ['primary', 'secondary', 'outline', 'success', 'danger', 'warning'] as const;
|
|
22
|
+
const BUTTON_STATES = ['hover', 'disabled'] as const;
|
|
23
|
+
const BUTTON_PROPS = ['padding', 'radius', 'border-width'] as const;
|
|
24
|
+
|
|
25
|
+
const buttonDeadKeys = new Set<string>(
|
|
26
|
+
BUTTON_VARIANTS.flatMap((v) =>
|
|
27
|
+
BUTTON_STATES.flatMap((s) =>
|
|
28
|
+
BUTTON_PROPS.map((p) => `--button-${v}-${s}-${p}`),
|
|
29
|
+
),
|
|
30
|
+
),
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const segmentedcontrolDeadKeys = new Set<string>([
|
|
34
|
+
'--segmentedcontrol-selected-icon-size',
|
|
35
|
+
'--segmentedcontrol-option-hover-icon-size',
|
|
36
|
+
'--segmentedcontrol-disabled-icon-size',
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
export const componentMigration_2026_05_24_promoteStateSharedTokens: Migration = {
|
|
40
|
+
id: '2026-05-24-promote-state-shared-tokens',
|
|
41
|
+
fromVersion: 12,
|
|
42
|
+
toVersion: 13,
|
|
43
|
+
appliesTo: 'component-config',
|
|
44
|
+
apply(rawVars, meta) {
|
|
45
|
+
const dead =
|
|
46
|
+
meta.component === 'button'
|
|
47
|
+
? buttonDeadKeys
|
|
48
|
+
: meta.component === 'segmentedcontrol'
|
|
49
|
+
? segmentedcontrolDeadKeys
|
|
50
|
+
: null;
|
|
51
|
+
if (!dead) return { ...rawVars };
|
|
52
|
+
const out: Record<string, string> = {};
|
|
53
|
+
for (const [key, value] of Object.entries(rawVars)) {
|
|
54
|
+
if (dead.has(key)) continue;
|
|
55
|
+
out[key] = value;
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Migration } from './index';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Component-config migration (2026-05-24, segmentedcontrol only):
|
|
5
|
+
* `--segmentedcontrol-divider-height` was retired in favor of
|
|
6
|
+
* `--segmentedcontrol-divider-inset`. The old token set a percentage / pixel
|
|
7
|
+
* height on a centered flex item; with `Full` (100%) it collapsed to zero in
|
|
8
|
+
* auto-sized inline-flex parents. The new token is `margin-block` trimmed
|
|
9
|
+
* from a stretched divider, so Full = 0 inset = bar-height divider.
|
|
10
|
+
*
|
|
11
|
+
* Value semantic flipped (larger old value meant taller divider; larger new
|
|
12
|
+
* value means shorter divider), so the saved customization is dropped rather
|
|
13
|
+
* than copied across. Users fall back to the new default and re-pick once.
|
|
14
|
+
*/
|
|
15
|
+
export const componentMigration_2026_05_24_segmentedcontrolDividerInset: Migration = {
|
|
16
|
+
id: '2026-05-24-segmentedcontrol-divider-inset',
|
|
17
|
+
fromVersion: 11,
|
|
18
|
+
toVersion: 12,
|
|
19
|
+
appliesTo: 'component-config',
|
|
20
|
+
apply(rawVars, meta) {
|
|
21
|
+
if (meta.component !== 'segmentedcontrol') return { ...rawVars };
|
|
22
|
+
const out: Record<string, string> = {};
|
|
23
|
+
for (const [key, value] of Object.entries(rawVars)) {
|
|
24
|
+
if (key === '--segmentedcontrol-divider-height') continue;
|
|
25
|
+
out[key] = value;
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
},
|
|
29
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Migration } from './index';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 2026-05-25: drop the per-variant token axis from cornerbadge.
|
|
5
|
+
*
|
|
6
|
+
* CornerBadge's "variants" carried only shape + spacing + type aliases — never
|
|
7
|
+
* colors (those come from the composed Badge). With every variant's defaults
|
|
8
|
+
* identical, the axis was 10× duplication that the editor had to tile across
|
|
9
|
+
* a variant strip for no semantic gain. Collapsing to a single flat token set
|
|
10
|
+
* removes the strip and the duplication.
|
|
11
|
+
*
|
|
12
|
+
* For each `--corner-badge-{variant}-{prop}`, fold into `--corner-badge-{prop}`.
|
|
13
|
+
* First-occurrence wins on conflict — defaults were uniform so this is a no-op
|
|
14
|
+
* in the typical case, and any tuned divergence is rare enough that picking
|
|
15
|
+
* one is better than silently averaging.
|
|
16
|
+
*/
|
|
17
|
+
const VARIANTS = ['primary', 'accent', 'neutral', 'alternate', 'canvas', 'special', 'success', 'warning', 'danger', 'info'] as const;
|
|
18
|
+
const RE = new RegExp(`^--corner-badge-(${VARIANTS.join('|')})-(.+)$`);
|
|
19
|
+
|
|
20
|
+
function flattenVariants(rawVars: Record<string, string>, meta: { component?: string }): Record<string, string> {
|
|
21
|
+
if (meta.component !== 'cornerbadge') return rawVars;
|
|
22
|
+
const out: Record<string, string> = {};
|
|
23
|
+
const collected: Map<string, string> = new Map();
|
|
24
|
+
for (const [key, value] of Object.entries(rawVars)) {
|
|
25
|
+
const m = key.match(RE);
|
|
26
|
+
if (m) {
|
|
27
|
+
const prop = m[2];
|
|
28
|
+
if (!collected.has(prop)) collected.set(prop, value);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
out[key] = value;
|
|
32
|
+
}
|
|
33
|
+
for (const [prop, value] of collected) {
|
|
34
|
+
const flatKey = `--corner-badge-${prop}`;
|
|
35
|
+
if (!(flatKey in out)) out[flatKey] = value;
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const componentMigration_2026_05_25_cornerbadgeFlattenVariants: Migration = {
|
|
41
|
+
id: '2026-05-25-cornerbadge-flatten-variants',
|
|
42
|
+
fromVersion: 15,
|
|
43
|
+
toVersion: 16,
|
|
44
|
+
appliesTo: 'component-config',
|
|
45
|
+
apply: flattenVariants,
|
|
46
|
+
};
|