@motion-proto/live-tokens 0.7.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/live-tokens-add-component/SKILL.md +488 -0
- package/README.md +34 -0
- package/dist-plugin/index.cjs +707 -90
- package/dist-plugin/index.d.cts +1 -0
- package/dist-plugin/index.d.ts +1 -0
- package/dist-plugin/index.js +707 -90
- package/package.json +6 -2
- package/src/app/site.css +1 -1
- package/src/editor/component-editor/CollapsibleSectionEditor.svelte +34 -27
- package/src/editor/component-editor/DialogEditor.svelte +4 -4
- package/src/editor/component-editor/NotificationEditor.svelte +3 -1
- package/src/editor/component-editor/SectionDividerEditor.svelte +439 -112
- package/src/editor/component-editor/StandardButtonsEditor.svelte +13 -1
- package/src/editor/component-editor/editors.d.ts +10 -0
- package/src/editor/component-editor/index.ts +16 -1
- package/src/editor/component-editor/registry.ts +103 -26
- package/src/editor/component-editor/scaffolding/AngleDial.svelte +52 -13
- package/src/editor/component-editor/scaffolding/ComponentFileManager.svelte +10 -11
- package/src/editor/component-editor/scaffolding/ComponentsTab.svelte +2 -2
- package/src/editor/component-editor/scaffolding/LinkedBlock.svelte +0 -1
- package/src/editor/component-editor/scaffolding/RadialShapePad.svelte +483 -0
- package/src/editor/component-editor/scaffolding/ShadowBackdrop.svelte +15 -2
- package/src/editor/component-editor/scaffolding/StateBlock.svelte +103 -15
- package/src/editor/component-editor/scaffolding/TokenLayout.svelte +9 -6
- package/src/editor/component-editor/scaffolding/TypeEditor.svelte +13 -1
- package/src/editor/component-editor/scaffolding/VariantGroup.svelte +239 -25
- package/src/editor/component-editor/scaffolding/buildTypeGroupTokens.ts +1 -0
- 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/types.ts +11 -0
- package/src/editor/core/components/componentConfigKeys.ts +22 -3
- package/src/editor/core/components/componentConfigService.ts +2 -2
- package/src/editor/core/components/componentPersist.ts +7 -5
- package/src/editor/core/manifests/manifestService.ts +58 -3
- package/src/editor/core/palettes/familySwap.ts +99 -0
- package/src/editor/core/palettes/paletteDerivation.ts +69 -0
- package/src/editor/core/palettes/tokenRegistry.ts +4 -1
- package/src/editor/core/store/editorStore.ts +206 -12
- package/src/editor/core/store/editorTypes.ts +55 -12
- package/src/editor/core/store/gradientSource.ts +192 -0
- package/src/editor/core/themes/migrations/2026-05-19-collapsiblesection-drop-frame-surface.ts +28 -0
- package/src/editor/core/themes/migrations/2026-05-19-sectiondivider-rich-gradient.ts +35 -0
- package/src/editor/core/themes/migrations/2026-05-20-sectiondivider-slim-variants.ts +82 -0
- package/src/editor/core/themes/migrations/2026-05-21-sectiondivider-spacing-to-padding.ts +24 -0
- package/src/editor/core/themes/migrations/2026-05-22-sectiondivider-intrinsics-to-css.ts +81 -0
- package/src/editor/core/themes/migrations/index.ts +10 -0
- package/src/editor/core/themes/slices/components.ts +27 -4
- package/src/editor/core/themes/slices/gradients.ts +88 -13
- package/src/editor/core/themes/themeInit.ts +2 -2
- package/src/editor/core/themes/themeTypes.ts +56 -1
- package/src/editor/index.ts +10 -1
- package/src/editor/overlay/ColumnsOverlay.svelte +0 -1
- package/src/editor/overlay/LiveEditorOverlay.svelte +1 -4
- package/src/editor/pages/ComponentEditorPage.svelte +53 -3
- package/src/editor/pages/EditorShell.svelte +53 -3
- package/src/editor/styles/ui-editor.css +1 -0
- package/src/editor/styles/ui-form-controls.css +19 -20
- package/src/editor/ui/BezierCurveEditor.svelte +114 -63
- package/src/editor/ui/EditorViewSwitcher.svelte +0 -1
- package/src/editor/ui/FileLoadList.svelte +22 -5
- package/src/editor/ui/FontStackEditor.svelte +214 -76
- package/src/editor/ui/GradientEditor.svelte +435 -215
- package/src/editor/ui/GradientStopPicker.svelte +11 -3
- package/src/editor/ui/ManifestFileManager.svelte +71 -4
- package/src/editor/ui/PaletteEditor.svelte +52 -79
- package/src/editor/ui/ProjectFontsSection.svelte +328 -293
- package/src/editor/ui/ThemeFileManager.svelte +0 -4
- package/src/editor/ui/UIFontFamilySelector.svelte +0 -1
- package/src/editor/ui/UIFontSizeSelector.svelte +3 -0
- package/src/editor/ui/UIInfoPopover.svelte +0 -1
- package/src/editor/ui/UILetterSpacingSelector.svelte +65 -0
- package/src/editor/ui/UIPaletteSelector.svelte +31 -4
- package/src/editor/ui/UIPillButton.svelte +33 -3
- package/src/editor/ui/UISegmentedControl.svelte +114 -0
- package/src/editor/ui/UITokenSelector.svelte +4 -1
- package/src/editor/ui/VariablesTab.svelte +41 -35
- package/src/editor/ui/palette/OverridesPanel.svelte +14 -37
- package/src/editor/ui/palette/PaletteBase.svelte +3 -3
- package/src/editor/ui/sections/ColumnsSection.svelte +1 -2
- package/src/editor/ui/sections/GradientsSection.svelte +1 -1
- package/src/editor/ui/sections/OverlaysSection.svelte +1 -1
- package/src/editor/ui/sections/ShadowsSection.svelte +1 -1
- package/src/system/components/Button.svelte +2 -2
- package/src/system/components/Card.svelte +29 -1
- package/src/system/components/CollapsibleSection.svelte +25 -2
- package/src/system/components/Dialog.svelte +24 -4
- package/src/system/components/FloatingTokenTags.css +43 -24
- package/src/system/components/FloatingTokenTags.svelte +88 -137
- package/src/system/components/Notification.svelte +8 -1
- package/src/system/components/SectionDivider.svelte +532 -381
- package/src/system/styles/CONVENTIONS.md +1 -1
- package/src/system/styles/fonts.css +3 -16
- package/src/system/styles/tokens.css +356 -1199
- package/src/system/styles/tokens.generated.css +544 -0
- package/src/editor/component-editor/scaffolding/DividerEditor.svelte +0 -94
- package/src/editor/component-editor/scaffolding/GradientCard.svelte +0 -296
|
@@ -30,6 +30,10 @@
|
|
|
30
30
|
stateActions?: Snippet<[string]>;
|
|
31
31
|
previewActions?: Snippet;
|
|
32
32
|
compositeControls?: Snippet<[string]>;
|
|
33
|
+
/** Each child should be a `.property-row`. Rendered above the token grid
|
|
34
|
+
so per-variant display knobs (alignment, hairline, etc.) lead the list
|
|
35
|
+
before the typography and token rows. */
|
|
36
|
+
extraPropertyRowsTop?: Snippet<[string]>;
|
|
33
37
|
/** Each child should be a `.property-row` to match the token grid above. */
|
|
34
38
|
extraPropertyRows?: Snippet<[string]>;
|
|
35
39
|
/** Extra sections appended below the Background controls inside the canvas
|
|
@@ -37,6 +41,13 @@
|
|
|
37
41
|
Background. Lets per-instance display knobs (anchor, alignment, etc.)
|
|
38
42
|
live with the canvas rather than in a separate config block above. */
|
|
39
43
|
canvasToolbarExtras?: Snippet;
|
|
44
|
+
/** Per-element Show toggles, forwarded to StateBlock. Keyed by element name. */
|
|
45
|
+
elementToggles?: Record<string, { checked: boolean; label?: string; onchange: (checked: boolean) => void }>;
|
|
46
|
+
/** Explicit order for element-grouped sections, forwarded to StateBlock. */
|
|
47
|
+
elementOrder?: string[];
|
|
48
|
+
/** Per-element extras snippet, forwarded to StateBlock. Receives the
|
|
49
|
+
element name and renders between the section heading and its content. */
|
|
50
|
+
elementExtras?: Snippet<[string]>;
|
|
40
51
|
/** Skip the default centered, padded stage when the editor brings its own backdrop. */
|
|
41
52
|
unboxedPreview?: boolean;
|
|
42
53
|
backdropPadding?: string;
|
|
@@ -58,8 +69,12 @@
|
|
|
58
69
|
stateActions,
|
|
59
70
|
previewActions,
|
|
60
71
|
compositeControls,
|
|
72
|
+
extraPropertyRowsTop,
|
|
61
73
|
extraPropertyRows,
|
|
62
74
|
canvasToolbarExtras,
|
|
75
|
+
elementToggles,
|
|
76
|
+
elementOrder,
|
|
77
|
+
elementExtras,
|
|
63
78
|
unboxedPreview = false,
|
|
64
79
|
backdropPadding,
|
|
65
80
|
backdropModes,
|
|
@@ -78,7 +93,7 @@
|
|
|
78
93
|
|
|
79
94
|
let activeTab: string = $state('');
|
|
80
95
|
|
|
81
|
-
const TYPE_PROPS = ['colorVariable', 'familyVariable', 'sizeVariable', 'weightVariable', 'lineHeightVariable', 'outlineWidthVariable', 'outlineColorVariable'] as const;
|
|
96
|
+
const TYPE_PROPS = ['colorVariable', 'familyVariable', 'sizeVariable', 'weightVariable', 'lineHeightVariable', 'letterSpacingVariable', 'outlineWidthVariable', 'outlineColorVariable'] as const;
|
|
82
97
|
// Carry per-side derived vars so split padding fully transfers; no-op when absent.
|
|
83
98
|
const PADDING_SIDES = ['top', 'right', 'bottom', 'left'] as const;
|
|
84
99
|
|
|
@@ -124,6 +139,25 @@
|
|
|
124
139
|
skips these in the typeGroups copy loop. */
|
|
125
140
|
const COLOR_TYPE_PROPS = new Set(['colorVariable', 'outlineColorVariable']);
|
|
126
141
|
|
|
142
|
+
/** True iff `colorRef` is a CSS-var token whose slug contains `family`
|
|
143
|
+
as a hyphen-delimited segment (e.g. `--surface-canvas-low` ∋ `canvas`). */
|
|
144
|
+
function stopMatchesFamily(colorRef: string, family: string): boolean {
|
|
145
|
+
if (!colorRef.startsWith('--')) return false;
|
|
146
|
+
return colorRef.slice(2).split('-').includes(family);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Replace the first `from` segment in a CSS-var token slug with `to`.
|
|
150
|
+
Used to swap stop colors across variant families on gradient copy
|
|
151
|
+
(e.g. `--surface-success-high` + (success → accent) → `--surface-accent-high`). */
|
|
152
|
+
function swapFamilyInToken(colorRef: string, from: string, to: string): string {
|
|
153
|
+
if (!colorRef.startsWith('--')) return colorRef;
|
|
154
|
+
const parts = colorRef.slice(2).split('-');
|
|
155
|
+
const idx = parts.indexOf(from);
|
|
156
|
+
if (idx < 0) return colorRef;
|
|
157
|
+
parts[idx] = to;
|
|
158
|
+
return '--' + parts.join('-');
|
|
159
|
+
}
|
|
160
|
+
|
|
127
161
|
function pickCopySource(toState: string, fromVariant: string, fromState: string) {
|
|
128
162
|
const preserveColorFamily = $preserveColorFamilyStore;
|
|
129
163
|
if (!component || !states) return;
|
|
@@ -139,11 +173,15 @@
|
|
|
139
173
|
const slice = s.components[component!] ?? (s.components[component!] = { activeFile: 'default', aliases: {}, config: {} });
|
|
140
174
|
const dstVarsTouched: string[] = [];
|
|
141
175
|
/** Resolve a variable's effective value as a CSS string: the override if
|
|
142
|
-
set, otherwise its declared default. Returns null if neither exists.
|
|
176
|
+
set, otherwise its declared default. Returns null if neither exists.
|
|
177
|
+
Gradient refs short-circuit to null — the copy path for gradients
|
|
178
|
+
uses `applyGradient`, not the alpha-extract pipeline. */
|
|
143
179
|
const effectiveValue = (varName: string): string | null => {
|
|
144
180
|
const ref = slice.aliases[varName];
|
|
145
|
-
if (ref) return
|
|
146
|
-
return
|
|
181
|
+
if (!ref) return getDeclaredValue(varName);
|
|
182
|
+
if (ref.kind === 'token') return `var(${ref.name})`;
|
|
183
|
+
if (ref.kind === 'literal') return ref.value;
|
|
184
|
+
return null;
|
|
147
185
|
};
|
|
148
186
|
|
|
149
187
|
const apply = (srcVar: string, dstVar: string) => {
|
|
@@ -179,10 +217,49 @@
|
|
|
179
217
|
};
|
|
180
218
|
dstVarsTouched.push(dstVar);
|
|
181
219
|
};
|
|
220
|
+
/** Copy a structured gradient ref. Stops carry source's positions +
|
|
221
|
+
opacities verbatim; out-of-family stop colors carry verbatim too.
|
|
222
|
+
With preserveColorFamily on, in-family stop colors swap to the
|
|
223
|
+
destination family — see plan §5 for the worked example. */
|
|
224
|
+
const applyGradient = (
|
|
225
|
+
srcVar: string,
|
|
226
|
+
dstVar: string,
|
|
227
|
+
srcFamily: string | undefined,
|
|
228
|
+
dstFamily: string | undefined,
|
|
229
|
+
) => {
|
|
230
|
+
const srcRef = slice.aliases[srcVar];
|
|
231
|
+
if (!srcRef || srcRef.kind !== 'gradient') {
|
|
232
|
+
// Source has no override — clearing dst returns it to its own CSS
|
|
233
|
+
// default (which is dst's family by design), preserving the
|
|
234
|
+
// "destination keeps its family palette" invariant.
|
|
235
|
+
delete slice.aliases[dstVar];
|
|
236
|
+
dstVarsTouched.push(dstVar);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const swapFamilies = preserveColorFamily
|
|
240
|
+
&& !!srcFamily && !!dstFamily
|
|
241
|
+
&& srcFamily !== dstFamily;
|
|
242
|
+
const newStops = srcRef.value.stops.map((s) => {
|
|
243
|
+
if (swapFamilies && stopMatchesFamily(s.color, srcFamily!)) {
|
|
244
|
+
return { ...s, color: swapFamilyInToken(s.color, srcFamily!, dstFamily!) };
|
|
245
|
+
}
|
|
246
|
+
return { ...s };
|
|
247
|
+
});
|
|
248
|
+
slice.aliases[dstVar] = {
|
|
249
|
+
kind: 'gradient',
|
|
250
|
+
value: { type: srcRef.value.type, angle: srcRef.value.angle, stops: newStops },
|
|
251
|
+
};
|
|
252
|
+
dstVarsTouched.push(dstVar);
|
|
253
|
+
};
|
|
254
|
+
|
|
182
255
|
const minLen = Math.min(srcTokens.length, dstTokens.length);
|
|
183
256
|
for (let i = 0; i < minLen; i++) {
|
|
184
257
|
const srcVar = srcTokens[i].variable;
|
|
185
258
|
const dstVar = dstTokens[i].variable;
|
|
259
|
+
if (srcTokens[i].kind === 'gradient' || dstTokens[i].kind === 'gradient') {
|
|
260
|
+
applyGradient(srcVar, dstVar, srcTokens[i].family, dstTokens[i].family);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
186
263
|
if (preserveColorFamily && isColorToken(srcTokens[i])) {
|
|
187
264
|
applyColorPreserve(srcVar, dstVar);
|
|
188
265
|
continue;
|
|
@@ -231,7 +308,45 @@
|
|
|
231
308
|
});
|
|
232
309
|
|
|
233
310
|
let stateNames = $derived(states ? Object.keys(states) : []);
|
|
311
|
+
/** Key-name convention: when ANY state name contains " / ", the strip switches
|
|
312
|
+
to two-tier rendering — top row is parts (unique left-hand sides), bottom
|
|
313
|
+
row is the active part's states (right-hand sides). Parts with no slash
|
|
314
|
+
have no sub-states and skip the bottom row when active. */
|
|
315
|
+
const PART_SEP = ' / ';
|
|
316
|
+
let isHierarchical = $derived(stateNames.some((n) => n.includes(PART_SEP)));
|
|
317
|
+
/** Ordered list of unique parts, preserving the order they first appear in `states`. */
|
|
318
|
+
let parts = $derived.by(() => {
|
|
319
|
+
const seen: string[] = [];
|
|
320
|
+
for (const n of stateNames) {
|
|
321
|
+
const part = n.includes(PART_SEP) ? n.split(PART_SEP)[0] : n;
|
|
322
|
+
if (!seen.includes(part)) seen.push(part);
|
|
323
|
+
}
|
|
324
|
+
return seen;
|
|
325
|
+
});
|
|
326
|
+
/** Sub-states of the currently active part (only meaningful in hierarchical mode). */
|
|
327
|
+
let activePart = $derived(activeTab.includes(PART_SEP) ? activeTab.split(PART_SEP)[0] : activeTab);
|
|
328
|
+
let activeSubState = $derived(activeTab.includes(PART_SEP) ? activeTab.split(PART_SEP)[1] : '');
|
|
329
|
+
let partSubStates = $derived(
|
|
330
|
+
isHierarchical
|
|
331
|
+
? stateNames.filter((n) => n.startsWith(activePart + PART_SEP)).map((n) => n.split(PART_SEP)[1])
|
|
332
|
+
: [],
|
|
333
|
+
);
|
|
234
334
|
let tabsStripVisible = $derived(stateNames.length >= 2);
|
|
335
|
+
let subStripVisible = $derived(isHierarchical && partSubStates.length >= 2);
|
|
336
|
+
|
|
337
|
+
/** Switch parts. If the new part has sub-states, jump to its first one; otherwise
|
|
338
|
+
activate the part itself. Keeps activeTab as a single canonical key so the
|
|
339
|
+
downstream property lookup, copy-from menus, and focusedStateStore stay simple. */
|
|
340
|
+
function selectPart(part: string) {
|
|
341
|
+
const firstSub = stateNames.find((n) => n.startsWith(part + PART_SEP));
|
|
342
|
+
activeTab = firstSub ?? part;
|
|
343
|
+
focusedStateStore.set(activeTab);
|
|
344
|
+
}
|
|
345
|
+
function selectSubState(sub: string) {
|
|
346
|
+
activeTab = `${activePart}${PART_SEP}${sub}`;
|
|
347
|
+
focusedStateStore.set(activeTab);
|
|
348
|
+
}
|
|
349
|
+
|
|
235
350
|
$effect(() => {
|
|
236
351
|
if (stateNames.length > 0 && !stateNames.includes(activeTab)) {
|
|
237
352
|
activeTab = stateNames[0];
|
|
@@ -297,30 +412,62 @@
|
|
|
297
412
|
{@render children?.({ activeState: activeTab })}
|
|
298
413
|
</ShadowBackdrop>
|
|
299
414
|
{/if}
|
|
300
|
-
</div>
|
|
301
415
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
416
|
+
{#if tabsStripVisible}
|
|
417
|
+
<div class="tabs-states-block">
|
|
418
|
+
<span class="editor-subsection-title">{selectorLabel}</span>
|
|
419
|
+
<div class="tabs-selectors">
|
|
420
|
+
{#if isHierarchical}
|
|
421
|
+
<div class="state-tabs" role="tablist">
|
|
422
|
+
{#each parts as p}
|
|
423
|
+
<button
|
|
424
|
+
type="button"
|
|
425
|
+
class="state-tab-btn"
|
|
426
|
+
class:active={activePart === p}
|
|
427
|
+
role="tab"
|
|
428
|
+
aria-selected={activePart === p}
|
|
429
|
+
onclick={() => selectPart(p)}
|
|
430
|
+
>{p}</button>
|
|
431
|
+
{/each}
|
|
432
|
+
</div>
|
|
433
|
+
{:else}
|
|
434
|
+
<div class="state-tabs" role="tablist">
|
|
435
|
+
{#each stateNames as s}
|
|
436
|
+
<button
|
|
437
|
+
type="button"
|
|
438
|
+
class="state-tab-btn"
|
|
439
|
+
class:active={activeTab === s}
|
|
440
|
+
role="tab"
|
|
441
|
+
aria-selected={activeTab === s}
|
|
442
|
+
onclick={() => { activeTab = s; focusedStateStore.set(s); }}
|
|
443
|
+
>{s}</button>
|
|
444
|
+
{/each}
|
|
445
|
+
</div>
|
|
446
|
+
{/if}
|
|
447
|
+
{#if activeTab}
|
|
448
|
+
{@render stateActions?.(activeTab)}
|
|
449
|
+
{/if}
|
|
317
450
|
</div>
|
|
318
|
-
{#if
|
|
319
|
-
|
|
451
|
+
{#if subStripVisible}
|
|
452
|
+
<div class="tabs-selectors substrip">
|
|
453
|
+
<span class="editor-subsection-title state-eyebrow">State</span>
|
|
454
|
+
<div class="state-tabs" role="tablist">
|
|
455
|
+
{#each partSubStates as s}
|
|
456
|
+
<button
|
|
457
|
+
type="button"
|
|
458
|
+
class="state-tab-btn"
|
|
459
|
+
class:active={activeSubState === s}
|
|
460
|
+
role="tab"
|
|
461
|
+
aria-selected={activeSubState === s}
|
|
462
|
+
onclick={() => selectSubState(s)}
|
|
463
|
+
>{s}</button>
|
|
464
|
+
{/each}
|
|
465
|
+
</div>
|
|
466
|
+
</div>
|
|
320
467
|
{/if}
|
|
321
468
|
</div>
|
|
322
|
-
|
|
323
|
-
|
|
469
|
+
{/if}
|
|
470
|
+
</div>
|
|
324
471
|
|
|
325
472
|
{#if activeTab && states[activeTab]}
|
|
326
473
|
{@const stateName = activeTab}
|
|
@@ -339,12 +486,20 @@
|
|
|
339
486
|
/>
|
|
340
487
|
{/if}
|
|
341
488
|
</div>
|
|
489
|
+
{#if extraPropertyRowsTop}
|
|
490
|
+
<div class="extra-property-rows extra-property-rows--top">
|
|
491
|
+
{@render extraPropertyRowsTop(stateName)}
|
|
492
|
+
</div>
|
|
493
|
+
{/if}
|
|
342
494
|
<StateBlock
|
|
343
495
|
tokens={states[stateName]}
|
|
344
496
|
typeGroups={typeGroups[stateName] ?? []}
|
|
345
497
|
{component}
|
|
346
498
|
{linkedOrder}
|
|
347
499
|
{columns}
|
|
500
|
+
{elementToggles}
|
|
501
|
+
{elementOrder}
|
|
502
|
+
{elementExtras}
|
|
348
503
|
{onchange}
|
|
349
504
|
/>
|
|
350
505
|
{#if extraPropertyRows}
|
|
@@ -377,12 +532,46 @@
|
|
|
377
532
|
/* Card chrome lives on .editor-section-card in ui-editor.css. */
|
|
378
533
|
.variant-group {
|
|
379
534
|
gap: var(--ui-space-12);
|
|
535
|
+
container-type: inline-size;
|
|
536
|
+
container-name: variant-group;
|
|
380
537
|
}
|
|
381
538
|
|
|
539
|
+
/* Pin the preview + state-tab strip to the top of the page scroll so
|
|
540
|
+
property edits stay visually connected to the preview without scrolling.
|
|
541
|
+
The card background extends through the sticky band so the property grid
|
|
542
|
+
scrolls cleanly behind it. The element-grouped property layout (see
|
|
543
|
+
StateBlock) fans out horizontally, which keeps the property section
|
|
544
|
+
short enough that the sticky preview rarely steals usable space. */
|
|
382
545
|
.tabs-preview {
|
|
546
|
+
position: sticky;
|
|
547
|
+
top: 0;
|
|
548
|
+
z-index: 2;
|
|
383
549
|
display: flex;
|
|
384
550
|
flex-direction: column;
|
|
385
551
|
gap: var(--ui-space-20);
|
|
552
|
+
background: var(--ui-surface-low);
|
|
553
|
+
/* Bleed the background up through the card's top padding so content
|
|
554
|
+
scrolling behind doesn't peek between the viewport edge and the
|
|
555
|
+
pinned preview. The matching negative margin restores flow position.
|
|
556
|
+
Border-radius hugs the card's rounded top corners so the unpinned
|
|
557
|
+
state still reads as one continuous panel. */
|
|
558
|
+
margin: calc(-1 * var(--ui-space-20)) calc(-1 * var(--ui-space-20)) 0;
|
|
559
|
+
padding: var(--ui-space-20) var(--ui-space-20) 0;
|
|
560
|
+
border-radius: var(--ui-radius-md) var(--ui-radius-md) 0 0;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/* Soft fade at the bottom of the sticky band so property rows scrolling
|
|
564
|
+
up don't sharply cut against the pinned preview. Greyscale (no accent).
|
|
565
|
+
Sits below the preview body, above scrolling content. */
|
|
566
|
+
.tabs-preview::after {
|
|
567
|
+
content: '';
|
|
568
|
+
position: absolute;
|
|
569
|
+
left: 0;
|
|
570
|
+
right: 0;
|
|
571
|
+
bottom: calc(-1 * var(--ui-space-12));
|
|
572
|
+
height: var(--ui-space-12);
|
|
573
|
+
background: linear-gradient(to bottom, var(--ui-surface-low), transparent);
|
|
574
|
+
pointer-events: none;
|
|
386
575
|
}
|
|
387
576
|
|
|
388
577
|
.preview-header {
|
|
@@ -416,6 +605,13 @@
|
|
|
416
605
|
box-sizing: border-box;
|
|
417
606
|
}
|
|
418
607
|
|
|
608
|
+
@container variant-group (max-width: 32rem) {
|
|
609
|
+
.canvas-toolbar {
|
|
610
|
+
width: 100%;
|
|
611
|
+
height: auto;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
419
615
|
.canvas-toolbar :global(.canvas-toolbar-eyebrow) {
|
|
420
616
|
font-size: var(--ui-font-size-xs);
|
|
421
617
|
font-weight: var(--ui-font-weight-medium);
|
|
@@ -441,6 +637,7 @@
|
|
|
441
637
|
vocabulary from the rest of the editor. */
|
|
442
638
|
.canvas-toolbar :global(.canvas-toolbar-select) {
|
|
443
639
|
width: 100%;
|
|
640
|
+
box-sizing: border-box;
|
|
444
641
|
appearance: none;
|
|
445
642
|
-webkit-appearance: none;
|
|
446
643
|
padding: 0 var(--ui-space-24) 0 var(--ui-space-8);
|
|
@@ -466,9 +663,14 @@
|
|
|
466
663
|
outline-offset: 2px;
|
|
467
664
|
}
|
|
468
665
|
|
|
469
|
-
/* Native <input> styled to match the toolbar's select chrome.
|
|
666
|
+
/* Native <input> styled to match the toolbar's select chrome. Long values
|
|
667
|
+
ellipsize when blurred so the input doesn't grow or scroll-bleed past
|
|
668
|
+
the toolbar's 11rem column; focus restores native caret-driven scroll. */
|
|
470
669
|
.canvas-toolbar :global(.canvas-toolbar-input) {
|
|
471
670
|
width: 100%;
|
|
671
|
+
min-width: 0;
|
|
672
|
+
max-width: 100%;
|
|
673
|
+
box-sizing: border-box;
|
|
472
674
|
padding: 0 var(--ui-space-8);
|
|
473
675
|
min-height: 1.75rem;
|
|
474
676
|
background: var(--ui-surface-low);
|
|
@@ -477,6 +679,7 @@
|
|
|
477
679
|
color: var(--ui-text-primary);
|
|
478
680
|
font-family: var(--ui-font-sans);
|
|
479
681
|
font-size: var(--ui-font-size-sm);
|
|
682
|
+
text-overflow: ellipsis;
|
|
480
683
|
transition: background-color var(--ui-transition-fast), border-color var(--ui-transition-fast);
|
|
481
684
|
}
|
|
482
685
|
.canvas-toolbar :global(.canvas-toolbar-input:hover) {
|
|
@@ -572,6 +775,17 @@
|
|
|
572
775
|
gap: var(--ui-space-12);
|
|
573
776
|
}
|
|
574
777
|
|
|
778
|
+
/* Sub-strip sits flush under the parts strip when a part has interaction states.
|
|
779
|
+
The eyebrow label distinguishes it from the parts row above. */
|
|
780
|
+
.tabs-selectors.substrip {
|
|
781
|
+
margin-top: var(--ui-space-6);
|
|
782
|
+
}
|
|
783
|
+
.tabs-selectors.substrip .state-eyebrow {
|
|
784
|
+
color: var(--ui-text-tertiary);
|
|
785
|
+
font-size: var(--ui-font-size-xs);
|
|
786
|
+
min-width: 2.5rem;
|
|
787
|
+
}
|
|
788
|
+
|
|
575
789
|
.state-tabs {
|
|
576
790
|
display: inline-flex;
|
|
577
791
|
flex-wrap: wrap;
|
|
@@ -7,6 +7,7 @@ export const TYPE_FONT_PROPS = [
|
|
|
7
7
|
{ key: 'sizeVariable', label: 'font size', defaultGroupKey: 'font-size' },
|
|
8
8
|
{ key: 'weightVariable', label: 'font weight', defaultGroupKey: 'font-weight' },
|
|
9
9
|
{ key: 'lineHeightVariable', label: 'line height', defaultGroupKey: 'line-height' },
|
|
10
|
+
{ key: 'letterSpacingVariable', label: 'letter spacing', defaultGroupKey: 'letter-spacing' },
|
|
10
11
|
] as const satisfies ReadonlyArray<{
|
|
11
12
|
key: keyof TypeGroupConfig;
|
|
12
13
|
label: string;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getComponentRegistry } from '../registry';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Resolve a component id to its runtime source file path. Reads from the
|
|
5
|
-
*
|
|
5
|
+
* merged component registry (built-ins + runtime registrations).
|
|
6
6
|
*/
|
|
7
7
|
export function componentSourceFile(component: string): string {
|
|
8
|
-
return
|
|
8
|
+
return getComponentRegistry()[component]?.sourceFile ?? '';
|
|
9
9
|
}
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import type { ComponentSection } from './componentSectionType';
|
|
2
|
-
import {
|
|
2
|
+
import { getComponentRegistryEntries } from '../registry';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Default editor sections — derived from the
|
|
5
|
+
* Default editor sections — derived from the merged component registry. Each
|
|
6
6
|
* section's `id` is the canonical lowercase component id (matches the runtime
|
|
7
|
-
* filename, server scan, and `setComponentAlias` key); `label` is the
|
|
8
|
-
*
|
|
7
|
+
* filename, server scan, and `setComponentAlias` key); `label` is the display
|
|
8
|
+
* string; `component` is the editor Svelte component.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
10
|
+
* Recomputed on each call so consumer-registered components (added via
|
|
11
|
+
* `registerComponent()`) appear after the first-party set in iteration order.
|
|
12
|
+
*
|
|
13
|
+
* To add or reorder first-party sections, edit `src/editor/component-editor/registry.ts`.
|
|
11
14
|
*/
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
export function getDefaultSections(): ComponentSection[] {
|
|
16
|
+
return getComponentRegistryEntries().map((entry) => ({
|
|
17
|
+
id: entry.id,
|
|
18
|
+
label: entry.label,
|
|
19
|
+
component: entry.editorComponent,
|
|
20
|
+
}));
|
|
21
|
+
}
|
|
@@ -24,6 +24,15 @@ export type Token = {
|
|
|
24
24
|
StateBlock partitions the panel into labeled subsections — typography
|
|
25
25
|
and properties for each element render together. */
|
|
26
26
|
element?: string;
|
|
27
|
+
/** Hint to the editor that this token's alias is a structured payload
|
|
28
|
+
(currently only `kind: 'gradient'`). Drives Copy-from's per-kind
|
|
29
|
+
branch — gradient aliases need family-swap of in-family stop colors
|
|
30
|
+
rather than a verbatim ref copy. */
|
|
31
|
+
kind?: 'gradient';
|
|
32
|
+
/** Color-family slug for this token's owning variant (e.g. `brand`,
|
|
33
|
+
`accent`). Set on gradient-kind tokens so Copy-from's family-swap
|
|
34
|
+
can compute the src→dst family substitution. */
|
|
35
|
+
family?: string;
|
|
27
36
|
};
|
|
28
37
|
|
|
29
38
|
/** Editor type-group: a fieldset containing a coordinated set of typography tokens
|
|
@@ -43,6 +52,8 @@ export type TypeGroupConfig = {
|
|
|
43
52
|
weightLabel?: string;
|
|
44
53
|
lineHeightVariable?: string;
|
|
45
54
|
lineHeightLabel?: string;
|
|
55
|
+
letterSpacingVariable?: string;
|
|
56
|
+
letterSpacingLabel?: string;
|
|
46
57
|
outlineWidthVariable?: string;
|
|
47
58
|
outlineWidthLabel?: string;
|
|
48
59
|
outlineColorVariable?: string;
|
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
// migration that splits legacy single-bucket aliases into the new
|
|
5
5
|
// {aliases, config} shape.
|
|
6
6
|
//
|
|
7
|
-
// What goes here: literal-valued knobs that
|
|
8
|
-
//
|
|
9
|
-
// via
|
|
7
|
+
// What goes here: literal-valued knobs that live in the config bucket rather
|
|
8
|
+
// than the alias bucket. Some are runtime CSS values consumed by live
|
|
9
|
+
// components via the cascade (see CASCADING_COMPONENT_CONFIG_KEYS below);
|
|
10
|
+
// others are editor-only metadata that drive alias rewrites without ever
|
|
11
|
+
// reaching :root.
|
|
10
12
|
//
|
|
11
13
|
// What does NOT go here: aliases whose values are themselves CSS-var refs
|
|
12
14
|
// — even if the value space is constrained (e.g. `--button-shimmer` →
|
|
@@ -16,4 +18,21 @@
|
|
|
16
18
|
export const KNOWN_COMPONENT_CONFIG_KEYS: ReadonlySet<string> = new Set([
|
|
17
19
|
'--dialog-confirm-variant',
|
|
18
20
|
'--dialog-cancel-variant',
|
|
21
|
+
// SectionDivider per-variant `color-family` is editor metadata that drives
|
|
22
|
+
// the family-swap rewrite on aliases. It is not a runtime CSS value, so it
|
|
23
|
+
// stays in the config bucket. The other intrinsics (align, hairline,
|
|
24
|
+
// eyebrow/description visibility, eyebrow text-transform) now flow through
|
|
25
|
+
// the alias bucket as cascading CSS vars — see the 2026-05-22 migration.
|
|
26
|
+
'--sectiondivider-lg-color-family',
|
|
27
|
+
'--sectiondivider-md-color-family',
|
|
28
|
+
'--sectiondivider-sm-color-family',
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
// Subset of KNOWN_COMPONENT_CONFIG_KEYS that the renderer emits to :root as
|
|
32
|
+
// CSS vars so live components can read them via the cascade. Editor-only
|
|
33
|
+
// metadata (e.g. `--sectiondivider-*-color-family`, which drives an alias
|
|
34
|
+
// rewrite rather than a runtime value) is intentionally excluded.
|
|
35
|
+
export const CASCADING_COMPONENT_CONFIG_KEYS: ReadonlySet<string> = new Set([
|
|
36
|
+
'--dialog-confirm-variant',
|
|
37
|
+
'--dialog-cancel-variant',
|
|
19
38
|
]);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ComponentConfig, ComponentConfigMeta } from '../themes/themeTypes';
|
|
1
|
+
import type { AliasDiskValue, ComponentConfig, ComponentConfigMeta } from '../themes/themeTypes';
|
|
2
2
|
import { versionedFileResource } from '../storage/files/versionedFileResourceClient';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -23,7 +23,7 @@ export interface ComponentSummary {
|
|
|
23
23
|
export interface ComponentProductionInfo {
|
|
24
24
|
fileName: string;
|
|
25
25
|
name: string;
|
|
26
|
-
aliases: Record<string,
|
|
26
|
+
aliases: Record<string, AliasDiskValue>;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export interface ComponentConfigList {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { get } from 'svelte/store';
|
|
2
|
-
import type { ComponentConfig } from '../themes/themeTypes';
|
|
2
|
+
import type { AliasDiskValue, ComponentConfig } from '../themes/themeTypes';
|
|
3
3
|
import { editorState, markComponentSaved } from '../store/editorStore';
|
|
4
4
|
import type { CssVarRef } from '../store/editorTypes';
|
|
5
5
|
import { CURRENT_COMPONENT_SCHEMA_VERSION } from '../themes/migrations';
|
|
@@ -24,8 +24,10 @@ export type SaveActiveComponentResult =
|
|
|
24
24
|
| { ok: true; fileName: string; displayName: string }
|
|
25
25
|
| { ok: false; reason: 'default' | 'no-state' | 'error'; error?: unknown };
|
|
26
26
|
|
|
27
|
-
function
|
|
28
|
-
|
|
27
|
+
function refToDiskValue(ref: CssVarRef): AliasDiskValue {
|
|
28
|
+
if (ref.kind === 'token') return ref.name;
|
|
29
|
+
if (ref.kind === 'literal') return ref.value;
|
|
30
|
+
return { kind: 'gradient', value: ref.value };
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
export async function saveActiveComponentConfig(
|
|
@@ -43,8 +45,8 @@ export async function saveActiveComponentConfig(
|
|
|
43
45
|
const displayName = active?.name ?? fileName;
|
|
44
46
|
|
|
45
47
|
const now = new Date().toISOString();
|
|
46
|
-
const aliases: Record<string,
|
|
47
|
-
for (const [k, ref] of Object.entries(slice.aliases)) aliases[k] =
|
|
48
|
+
const aliases: Record<string, AliasDiskValue> = {};
|
|
49
|
+
for (const [k, ref] of Object.entries(slice.aliases)) aliases[k] = refToDiskValue(ref);
|
|
48
50
|
|
|
49
51
|
const data: ComponentConfig = {
|
|
50
52
|
name: displayName,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Manifest, ManifestMeta, Theme, ComponentConfig } from '../themes/themeTypes';
|
|
1
|
+
import type { Manifest, ManifestMeta, ManifestBundle, Theme, ComponentConfig } from '../themes/themeTypes';
|
|
2
2
|
import { versionedFileResource } from '../storage/files/versionedFileResourceClient';
|
|
3
3
|
import { listComponents } from '../components/componentConfigService';
|
|
4
4
|
import { getActiveTheme } from '../themes/themeService';
|
|
@@ -40,8 +40,9 @@ export interface ApplyManifestResult {
|
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* Server-side atomic apply: validate every referenced file exists, flip the
|
|
43
|
-
* theme + each component's `_active.json`
|
|
44
|
-
*
|
|
43
|
+
* theme + each component's `_active.json` and `_production.json` pointers,
|
|
44
|
+
* sync tokens.css/fonts.css from the new theme, mark the manifest active, and
|
|
45
|
+
* return the resolved theme + component configs in one payload. Clients
|
|
45
46
|
* usually follow with a full page reload — manifest load is a "blow up the
|
|
46
47
|
* world" action.
|
|
47
48
|
*/
|
|
@@ -114,3 +115,57 @@ export async function saveActiveManifest(displayName?: string): Promise<void> {
|
|
|
114
115
|
};
|
|
115
116
|
await saveManifest(active._fileName, manifest);
|
|
116
117
|
}
|
|
118
|
+
|
|
119
|
+
export interface ImportManifestResult {
|
|
120
|
+
ok: boolean;
|
|
121
|
+
/** Final manifest filename (may be renamed if it collided with an existing one). */
|
|
122
|
+
manifest: string;
|
|
123
|
+
/** Keyed `theme:<orig>` / `componentConfig:<comp>/<orig>` / `manifest:<orig>` → final name. */
|
|
124
|
+
renames: Record<string, string>;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Fetch the manifest as a self-contained `ManifestBundle` and trigger a
|
|
129
|
+
* browser download. Hidden-anchor trick — no infrastructure beyond the
|
|
130
|
+
* existing GET `/api/manifests/:name/export` endpoint.
|
|
131
|
+
*
|
|
132
|
+
* See temp/manifest-robustness-plan.md §11.
|
|
133
|
+
*/
|
|
134
|
+
export async function exportManifest(fileName: string): Promise<void> {
|
|
135
|
+
const res = await fetch(`/api/manifests/${encodeURIComponent(fileName)}/export`);
|
|
136
|
+
if (!res.ok) {
|
|
137
|
+
const err = await res.json().catch(() => ({ error: 'Export failed' }));
|
|
138
|
+
throw new Error(err.error || 'Export failed');
|
|
139
|
+
}
|
|
140
|
+
const blob = await res.blob();
|
|
141
|
+
const url = URL.createObjectURL(blob);
|
|
142
|
+
try {
|
|
143
|
+
const a = document.createElement('a');
|
|
144
|
+
a.href = url;
|
|
145
|
+
a.download = `${fileName}.bundle.json`;
|
|
146
|
+
document.body.appendChild(a);
|
|
147
|
+
a.click();
|
|
148
|
+
a.remove();
|
|
149
|
+
} finally {
|
|
150
|
+
URL.revokeObjectURL(url);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* POST a `ManifestBundle` to the import endpoint. Server materialises the
|
|
156
|
+
* inlined theme + component configs as fresh files (renaming on collision),
|
|
157
|
+
* rewrites the manifest's pointers, and returns the final manifest name plus
|
|
158
|
+
* the rename map so the UI can surface what got renamed.
|
|
159
|
+
*/
|
|
160
|
+
export async function importManifest(bundle: ManifestBundle): Promise<ImportManifestResult> {
|
|
161
|
+
const res = await fetch('/api/manifests/import', {
|
|
162
|
+
method: 'POST',
|
|
163
|
+
headers: { 'Content-Type': 'application/json' },
|
|
164
|
+
body: JSON.stringify(bundle),
|
|
165
|
+
});
|
|
166
|
+
if (!res.ok) {
|
|
167
|
+
const err = await res.json().catch(() => ({ error: 'Import failed' }));
|
|
168
|
+
throw new Error(err.error || 'Import failed');
|
|
169
|
+
}
|
|
170
|
+
return res.json();
|
|
171
|
+
}
|