@motion-proto/live-tokens 0.49.1 → 0.50.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/CHANGELOG.md +27 -0
- package/dist-plugin/generateColorsAndType/index.cjs +13 -2
- package/dist-plugin/generateColorsAndType/index.js +13 -2
- package/package.json +6 -1
- package/src/editor/bootstrap.ts +2 -0
- package/src/editor/component-editor/ButtonEditor.svelte +1 -1
- package/src/editor/component-editor/CardEditor.svelte +5 -2
- package/src/editor/component-editor/IconButtonEditor.svelte +1 -1
- package/src/editor/component-editor/ImageEditor.svelte +1 -1
- package/src/editor/component-editor/SectionDividerEditor.svelte +4 -1
- package/src/editor/component-editor/SideNavigationEditor.svelte +6 -0
- package/src/editor/component-editor/TabBarEditor.svelte +1 -5
- package/src/editor/component-editor/scaffolding/ComponentEditorBase.svelte +8 -1
- package/src/editor/component-editor/scaffolding/ComponentFileManager.svelte +10 -32
- package/src/editor/component-editor/scaffolding/StateBlock.svelte +2 -0
- package/src/editor/component-editor/scaffolding/TypeEditor.svelte +3 -1
- package/src/editor/component-editor/scaffolding/types.ts +3 -0
- package/src/editor/core/components/componentConfigService.ts +25 -0
- package/src/editor/core/cssVarSync.ts +65 -7
- package/src/editor/core/fonts/fontLoader.ts +12 -3
- package/src/editor/core/fonts/fontWeightAvailability.ts +97 -0
- package/src/editor/core/preview/lookPreview.ts +23 -18
- package/src/editor/core/store/editorRenderer.ts +22 -7
- package/src/editor/core/store/editorStore.ts +31 -0
- package/src/editor/core/store/gradientSource.ts +5 -0
- package/src/editor/core/themes/colorsAndTypeService.ts +2 -12
- package/src/editor/core/themes/migrations/2026-05-20-sectiondivider-slim-variants.ts +4 -1
- package/src/editor/core/themes/migrations/2026-05-21-sectiondivider-spacing-to-padding.ts +4 -1
- package/src/editor/core/themes/migrations/2026-05-25-cornerbadge-flatten-variants.ts +8 -0
- package/src/editor/core/themes/slices/fonts.ts +4 -5
- package/src/editor/core/themes/themeDocumentSync.ts +66 -0
- package/src/editor/core/themes/themeInit.ts +7 -10
- package/src/editor/core/themes/themeService.ts +10 -3
- package/src/editor/docs/content/themes-workflow.md +14 -11
- package/src/editor/docs/content/where-themes-live.md +2 -2
- package/src/editor/docs/content.generated.ts +2 -2
- package/src/editor/ui/FileLoadList.svelte +1 -0
- package/src/editor/ui/FontStackEditor.svelte +1 -2
- package/src/editor/ui/GradientEditor.svelte +5 -1
- package/src/editor/ui/ProjectFontsSection.svelte +0 -5
- package/src/editor/ui/ThemePanel.svelte +206 -42
- package/src/editor/ui/UIDialog.svelte +7 -1
- package/src/editor/ui/UIFontFamilySelector.svelte +13 -13
- package/src/editor/ui/UIFontSizeSelector.svelte +5 -5
- package/src/editor/ui/UIFontWeightSelector.svelte +52 -2
- package/src/editor/ui/UIOptionItem.svelte +14 -1
- package/src/editor/ui/UIPaddingSelector.svelte +4 -3
- package/src/editor/ui/UIPaletteSelector.svelte +20 -13
- package/src/editor/ui/UIRelinkConfirmDialog.svelte +18 -3
- package/src/editor/ui/UITextTransformSelector.svelte +5 -5
- package/src/editor/ui/UITokenSelector.svelte +29 -7
- package/src/live-tokens/data/themes/spring-meadow.json +14 -12
- package/src/system/components/Button.svelte +2 -1
- package/src/system/components/FloatingTokenTags.css +2 -1
- package/src/system/components/FloatingTokenTags.svelte +11 -0
- package/src/system/components/IconButton.svelte +2 -1
- package/src/system/components/Image.svelte +9 -3
- package/src/system/components/ImageLightbox.svelte +2 -0
- package/src/system/components/SectionDivider.svelte +20 -2
- package/src/system/components/SideNavigation.svelte +15 -1
- package/src/system/styles/fonts.css +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.50.0 — Live editing stays in sync
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Save and Adopt can capture every unsaved component.** When component
|
|
8
|
+
editors have pending changes, the Theme panel offers to save them together
|
|
9
|
+
before saving or adopting the theme, so the look on screen can be persisted
|
|
10
|
+
without visiting each component editor first.
|
|
11
|
+
|
|
12
|
+
- **Font-weight choices follow the selected family.** The editor detects the
|
|
13
|
+
weights available for project and Google fonts, keeps the current custom
|
|
14
|
+
value visible, and avoids offering weights the font cannot render.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **Theme changes paint as one transaction.** Loading, previewing, and applying
|
|
19
|
+
a theme now update colors, type, and components together instead of exposing
|
|
20
|
+
intermediate mixed states or flashing through the previous live look.
|
|
21
|
+
|
|
22
|
+
- **Editor controls react reliably to live token changes.** Reused selectors,
|
|
23
|
+
gradient controls, linked aliases, typography, spacing, and component state
|
|
24
|
+
now refresh when their bound variable or upstream token changes.
|
|
25
|
+
|
|
26
|
+
- **Component saves preserve migrated values.** Theme-level saves serialize
|
|
27
|
+
the current component state consistently, including corrected migration
|
|
28
|
+
handling for Section Divider and Corner Badge configurations.
|
|
29
|
+
|
|
3
30
|
## 0.49.1 — Automatic working-copy cleanup
|
|
4
31
|
|
|
5
32
|
### Fixed
|
|
@@ -1034,7 +1034,10 @@ var VARIANTS2 = ["lg", "md", "sm"];
|
|
|
1034
1034
|
var SUFFIX_RENAMES = [
|
|
1035
1035
|
[/-title-border-width$/, "-title-outline-width"],
|
|
1036
1036
|
[/-title-stroke-color$/, "-title-outline-color"],
|
|
1037
|
-
[
|
|
1037
|
+
[
|
|
1038
|
+
/^--sectiondivider-(canvas|neutral|alternate|primary|accent|special)-padding$/,
|
|
1039
|
+
"--sectiondivider-$1-spacing"
|
|
1040
|
+
]
|
|
1038
1041
|
];
|
|
1039
1042
|
function renameKeySuffix(key) {
|
|
1040
1043
|
for (const [re, repl] of SUFFIX_RENAMES) {
|
|
@@ -1085,7 +1088,10 @@ var componentMigration_2026_05_21_sectiondividerSpacingToPadding = {
|
|
|
1085
1088
|
if (meta.component !== "sectiondivider") return { ...rawVars };
|
|
1086
1089
|
const out = {};
|
|
1087
1090
|
for (const [key, value] of Object.entries(rawVars)) {
|
|
1088
|
-
const renamed = key.replace(
|
|
1091
|
+
const renamed = key.replace(
|
|
1092
|
+
/^--sectiondivider-(lg|md|sm)-spacing$/,
|
|
1093
|
+
"--sectiondivider-$1-padding"
|
|
1094
|
+
);
|
|
1089
1095
|
out[renamed] = value;
|
|
1090
1096
|
}
|
|
1091
1097
|
return out;
|
|
@@ -1241,6 +1247,7 @@ var componentMigration_2026_05_24_collapsiblesectionDropActiveState = {
|
|
|
1241
1247
|
// src/editor/core/themes/migrations/2026-05-25-cornerbadge-flatten-variants.ts
|
|
1242
1248
|
var VARIANTS3 = ["primary", "accent", "neutral", "alternate", "canvas", "special", "success", "warning", "danger", "info"];
|
|
1243
1249
|
var RE = new RegExp(`^--corner-badge-(${VARIANTS3.join("|")})-(.+)$`);
|
|
1250
|
+
var CURRENT_COLOR_PROPS = /* @__PURE__ */ new Set(["surface", "border", "text"]);
|
|
1244
1251
|
function flattenVariants(rawVars, meta) {
|
|
1245
1252
|
if (meta.component !== "cornerbadge") return rawVars;
|
|
1246
1253
|
const out = {};
|
|
@@ -1249,6 +1256,10 @@ function flattenVariants(rawVars, meta) {
|
|
|
1249
1256
|
const m = key.match(RE);
|
|
1250
1257
|
if (m) {
|
|
1251
1258
|
const prop = m[2];
|
|
1259
|
+
if (CURRENT_COLOR_PROPS.has(prop)) {
|
|
1260
|
+
out[key] = value;
|
|
1261
|
+
continue;
|
|
1262
|
+
}
|
|
1252
1263
|
if (!collected.has(prop)) collected.set(prop, value);
|
|
1253
1264
|
continue;
|
|
1254
1265
|
}
|
|
@@ -512,7 +512,10 @@ var VARIANTS2 = ["lg", "md", "sm"];
|
|
|
512
512
|
var SUFFIX_RENAMES = [
|
|
513
513
|
[/-title-border-width$/, "-title-outline-width"],
|
|
514
514
|
[/-title-stroke-color$/, "-title-outline-color"],
|
|
515
|
-
[
|
|
515
|
+
[
|
|
516
|
+
/^--sectiondivider-(canvas|neutral|alternate|primary|accent|special)-padding$/,
|
|
517
|
+
"--sectiondivider-$1-spacing"
|
|
518
|
+
]
|
|
516
519
|
];
|
|
517
520
|
function renameKeySuffix(key) {
|
|
518
521
|
for (const [re, repl] of SUFFIX_RENAMES) {
|
|
@@ -563,7 +566,10 @@ var componentMigration_2026_05_21_sectiondividerSpacingToPadding = {
|
|
|
563
566
|
if (meta.component !== "sectiondivider") return { ...rawVars };
|
|
564
567
|
const out = {};
|
|
565
568
|
for (const [key, value] of Object.entries(rawVars)) {
|
|
566
|
-
const renamed = key.replace(
|
|
569
|
+
const renamed = key.replace(
|
|
570
|
+
/^--sectiondivider-(lg|md|sm)-spacing$/,
|
|
571
|
+
"--sectiondivider-$1-padding"
|
|
572
|
+
);
|
|
567
573
|
out[renamed] = value;
|
|
568
574
|
}
|
|
569
575
|
return out;
|
|
@@ -719,6 +725,7 @@ var componentMigration_2026_05_24_collapsiblesectionDropActiveState = {
|
|
|
719
725
|
// src/editor/core/themes/migrations/2026-05-25-cornerbadge-flatten-variants.ts
|
|
720
726
|
var VARIANTS3 = ["primary", "accent", "neutral", "alternate", "canvas", "special", "success", "warning", "danger", "info"];
|
|
721
727
|
var RE = new RegExp(`^--corner-badge-(${VARIANTS3.join("|")})-(.+)$`);
|
|
728
|
+
var CURRENT_COLOR_PROPS = /* @__PURE__ */ new Set(["surface", "border", "text"]);
|
|
722
729
|
function flattenVariants(rawVars, meta) {
|
|
723
730
|
if (meta.component !== "cornerbadge") return rawVars;
|
|
724
731
|
const out = {};
|
|
@@ -727,6 +734,10 @@ function flattenVariants(rawVars, meta) {
|
|
|
727
734
|
const m = key.match(RE);
|
|
728
735
|
if (m) {
|
|
729
736
|
const prop = m[2];
|
|
737
|
+
if (CURRENT_COLOR_PROPS.has(prop)) {
|
|
738
|
+
out[key] = value;
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
730
741
|
if (!collected.has(prop)) collected.set(prop, value);
|
|
731
742
|
continue;
|
|
732
743
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@motion-proto/live-tokens",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.50.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Design token editor with live CSS variable editing. Svelte 5 + Vite 8.",
|
|
6
6
|
"keywords": [
|
|
@@ -110,6 +110,10 @@
|
|
|
110
110
|
"check": "svelte-check --tsconfig ./tsconfig.json",
|
|
111
111
|
"test": "vitest run",
|
|
112
112
|
"test:watch": "vitest",
|
|
113
|
+
"test:e2e": "playwright test",
|
|
114
|
+
"test:e2e:components": "playwright test tests/e2e/component-render-contract.spec.ts",
|
|
115
|
+
"test:e2e:ui": "playwright test --ui",
|
|
116
|
+
"prepare:e2e": "node scripts/prepare-playwright-data.mjs",
|
|
113
117
|
"build:plugin": "tsup",
|
|
114
118
|
"build:lib": "npm run build:plugin",
|
|
115
119
|
"deploy:local": "bash scripts/deploy-local.sh",
|
|
@@ -136,6 +140,7 @@
|
|
|
136
140
|
"vite": "^8"
|
|
137
141
|
},
|
|
138
142
|
"devDependencies": {
|
|
143
|
+
"@playwright/test": "^1.62.1",
|
|
139
144
|
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
|
140
145
|
"@types/node": "^25.9.1",
|
|
141
146
|
"happy-dom": "^20.9.0",
|
package/src/editor/bootstrap.ts
CHANGED
|
@@ -19,6 +19,7 @@ import * as cssVarSync from './core/cssVarSync';
|
|
|
19
19
|
import * as router from './core/routing/router';
|
|
20
20
|
import * as columnsOverlay from './overlay/columnsOverlay';
|
|
21
21
|
import * as editorStore from './core/store/editorStore';
|
|
22
|
+
import * as themeDocumentSync from './core/themes/themeDocumentSync';
|
|
22
23
|
import { initializeTheme } from './core/themes/themeInit';
|
|
23
24
|
import { registerComponent, type RegisterComponentEntry } from './component-editor/registry';
|
|
24
25
|
|
|
@@ -38,6 +39,7 @@ export async function bootLiveTokens(
|
|
|
38
39
|
editorStore.init();
|
|
39
40
|
|
|
40
41
|
if (import.meta.env.DEV) {
|
|
42
|
+
themeDocumentSync.init();
|
|
41
43
|
if (opts.components) {
|
|
42
44
|
for (const entry of opts.components) {
|
|
43
45
|
registerComponent(entry);
|
|
@@ -165,7 +165,7 @@
|
|
|
165
165
|
{/if}
|
|
166
166
|
{/snippet}
|
|
167
167
|
{#snippet children({ activeState })}
|
|
168
|
-
{@const forceClass = activeState === 'hover' ? 'force-hover' : ''}
|
|
168
|
+
{@const forceClass = activeState === 'hover' ? 'force-hover' : activeState === 'active' ? 'force-active' : ''}
|
|
169
169
|
{@const isDisabled = activeState === 'disabled'}
|
|
170
170
|
<div class="button-showcase-grid">
|
|
171
171
|
<div class="button-showcase-item">
|
|
@@ -144,7 +144,10 @@
|
|
|
144
144
|
>
|
|
145
145
|
{#snippet stateActions(stateName)}
|
|
146
146
|
{#if stateName === 'hover'}
|
|
147
|
-
<div
|
|
147
|
+
<div
|
|
148
|
+
class="hover-control"
|
|
149
|
+
data-token-variables="--card-hover-border-active --card-hover-shadow-active"
|
|
150
|
+
>
|
|
148
151
|
<label class="hover-enable">
|
|
149
152
|
<input type="checkbox" checked={hoverEnabled} onchange={(e) => setHoverEnabled(e.currentTarget.checked)} />
|
|
150
153
|
<span>Use hover</span>
|
|
@@ -159,7 +162,7 @@
|
|
|
159
162
|
{#snippet children({ activeState })}
|
|
160
163
|
{@const previewClass = activeState === 'hover' ? 'force-hover' : ''}
|
|
161
164
|
<div class="card-demo">
|
|
162
|
-
<Card title="Card title" class={previewClass}>
|
|
165
|
+
<Card title="Card title" icon="fas fa-star" class={previewClass}>
|
|
163
166
|
<div class="content-placeholder">Content Placeholder</div>
|
|
164
167
|
</Card>
|
|
165
168
|
</div>
|
|
@@ -140,7 +140,7 @@
|
|
|
140
140
|
previewActions={sizeAction}
|
|
141
141
|
>
|
|
142
142
|
{#snippet children({ activeState })}
|
|
143
|
-
{@const forceClass = activeState === 'hover' ? 'force-hover' : ''}
|
|
143
|
+
{@const forceClass = activeState === 'hover' ? 'force-hover' : activeState === 'active' ? 'force-active' : ''}
|
|
144
144
|
{@const isDisabled = activeState === 'disabled'}
|
|
145
145
|
<IconButton variant={v} icon={previewIcons[v]} ariaLabel={`${v} action`} disabled={isDisabled} class={forceClass} />
|
|
146
146
|
{/snippet}
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
<span>Overflow scaling (mask zoom to frame)</span>
|
|
79
79
|
</label>
|
|
80
80
|
{/snippet}
|
|
81
|
-
<Image src={demoImageUrl} alt="Demo" variant="banner" zoom={useZoom ? undefined : false} {overflowScaling} />
|
|
81
|
+
<Image src={demoImageUrl} alt="Demo" variant="banner" zoom={useZoom ? undefined : false} {overflowScaling} forceHover={useZoom} />
|
|
82
82
|
<p class="zoom-help">
|
|
83
83
|
Use zoom on hover: every image scales on hover (a page can still force it per image with the
|
|
84
84
|
<code>zoom</code> prop). Overflow scaling on (default): the content zooms inside the fixed frame,
|
|
@@ -66,6 +66,7 @@
|
|
|
66
66
|
legend: '',
|
|
67
67
|
element: 'title',
|
|
68
68
|
colorVariable: `--sectiondivider-${v}-title`,
|
|
69
|
+
colorCanBeLinked: true,
|
|
69
70
|
familyVariable: `--sectiondivider-${v}-title-font-family`,
|
|
70
71
|
sizeVariable: `--sectiondivider-${v}-title-font-size`,
|
|
71
72
|
weightVariable: `--sectiondivider-${v}-title-font-weight`,
|
|
@@ -78,6 +79,7 @@
|
|
|
78
79
|
legend: '',
|
|
79
80
|
element: 'description',
|
|
80
81
|
colorVariable: `--sectiondivider-${v}-description`,
|
|
82
|
+
colorCanBeLinked: true,
|
|
81
83
|
familyVariable: `--sectiondivider-${v}-description-font-family`,
|
|
82
84
|
sizeVariable: `--sectiondivider-${v}-description-font-size`,
|
|
83
85
|
weightVariable: `--sectiondivider-${v}-description-font-weight`,
|
|
@@ -87,6 +89,7 @@
|
|
|
87
89
|
legend: '',
|
|
88
90
|
element: 'eyebrow',
|
|
89
91
|
colorVariable: `--sectiondivider-${v}-eyebrow`,
|
|
92
|
+
colorCanBeLinked: true,
|
|
90
93
|
familyVariable: `--sectiondivider-${v}-eyebrow-font-family`,
|
|
91
94
|
sizeVariable: `--sectiondivider-${v}-eyebrow-font-size`,
|
|
92
95
|
weightVariable: `--sectiondivider-${v}-eyebrow-font-weight`,
|
|
@@ -399,7 +402,7 @@
|
|
|
399
402
|
}
|
|
400
403
|
</script>
|
|
401
404
|
|
|
402
|
-
<ComponentEditorBase {component} title="Section Divider" description="Full-width section banner. Each variant (lg/md/sm) is a full preset: its own size, typography, colors, AND intrinsic properties (alignment, hairline, eyebrow/description visibility)." tokens={allTokens} {linked} variants={variantOptions}>
|
|
405
|
+
<ComponentEditorBase {component} title="Section Divider" description="Full-width section banner. Each variant (lg/md/sm) is a full preset: its own size, typography, colors, AND intrinsic properties (alignment, hairline, eyebrow/description visibility)." tokens={allTokens} {linked} variants={variantOptions} defaultVariant="md">
|
|
403
406
|
{#each variants as v}
|
|
404
407
|
<VariantGroup
|
|
405
408
|
name={v.key}
|
|
@@ -48,6 +48,8 @@
|
|
|
48
48
|
{ label: 'surface color', groupKey: 'title-surface', variable: `--sidenavigation-title-${s}-surface` },
|
|
49
49
|
{ label: 'border color', groupKey: 'title-border', variable: `--sidenavigation-title-${s}-border` },
|
|
50
50
|
{ label: 'border width', canBeLinked: true, groupKey: 'title-border-width', variable: `--sidenavigation-title-${s}-border-width` },
|
|
51
|
+
{ label: 'indicator color', groupKey: 'title-accent', variable: `--sidenavigation-title-${s}-accent` },
|
|
52
|
+
{ label: 'indicator width', canBeLinked: true, groupKey: 'title-accent-width', variable: `--sidenavigation-title-${s}-accent-width` },
|
|
51
53
|
{ label: 'padding', canBeLinked: true, groupKey: 'title-padding', variable: `--sidenavigation-title-${s}-padding` },
|
|
52
54
|
];
|
|
53
55
|
}
|
|
@@ -59,6 +61,8 @@
|
|
|
59
61
|
{ label: 'gap', canBeLinked: true, groupKey: 'title-gap', variable: '--sidenavigation-title-gap' },
|
|
60
62
|
{ label: 'corner radius', canBeLinked: true, groupKey: 'title-radius', variable: '--sidenavigation-title-radius' },
|
|
61
63
|
{ label: 'label surface color', groupKey: 'title-label-surface', variable: '--sidenavigation-title-label-surface' },
|
|
64
|
+
{ label: 'label border color', groupKey: 'title-label-border', variable: '--sidenavigation-title-label-border' },
|
|
65
|
+
{ label: 'label border width', canBeLinked: true, groupKey: 'title-label-border-width', variable: '--sidenavigation-title-label-border-width' },
|
|
62
66
|
{ label: 'label corner radius', canBeLinked: true, groupKey: 'title-label-radius', variable: '--sidenavigation-title-label-radius' },
|
|
63
67
|
{ label: 'label padding', canBeLinked: true, groupKey: 'title-label-padding', variable: '--sidenavigation-title-label-padding' },
|
|
64
68
|
];
|
|
@@ -214,6 +218,7 @@
|
|
|
214
218
|
const linkableContexts = new Map<string, string>([
|
|
215
219
|
...STATEFUL_STATES.flatMap((s): Array<[string, string]> => [
|
|
216
220
|
[`--sidenavigation-title-${s}-border-width`, `title ${s}`],
|
|
221
|
+
[`--sidenavigation-title-${s}-accent-width`, `title ${s}`],
|
|
217
222
|
[`--sidenavigation-title-${s}-padding`, `title ${s}`],
|
|
218
223
|
[`--sidenavigation-title-${s}-label-font-family`, `title ${s}`],
|
|
219
224
|
[`--sidenavigation-title-${s}-label-font-size`, `title ${s}`],
|
|
@@ -252,6 +257,7 @@
|
|
|
252
257
|
]),
|
|
253
258
|
['--sidenavigation-panel-border-width', 'panel'],
|
|
254
259
|
['--sidenavigation-panel-padding', 'panel'],
|
|
260
|
+
['--sidenavigation-title-label-border-width', 'title block'],
|
|
255
261
|
]);
|
|
256
262
|
</script>
|
|
257
263
|
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
{ label: 'border width', element: 'frame', canBeLinked: true, groupKey: 'tab-border-width', variable: `--tabbar-${s}-tab-border-width` },
|
|
20
20
|
{ label: 'padding', element: 'frame', canBeLinked: true, groupKey: 'padding', variable: `--tabbar-${s}-padding` },
|
|
21
21
|
{ label: 'size', element: 'icon', canBeLinked: true, groupKey: 'icon-size', variable: `--tabbar-${s}-icon-size` },
|
|
22
|
+
{ label: 'color', element: 'indicator', groupKey: 'indicator-color', variable: `--tabbar-${s}-border` },
|
|
22
23
|
{ label: 'width', element: 'indicator', canBeLinked: true, groupKey: 'indicator-border-width', variable: `--tabbar-${s}-indicator-border-width` },
|
|
23
24
|
];
|
|
24
25
|
}
|
|
@@ -50,10 +51,6 @@
|
|
|
50
51
|
],
|
|
51
52
|
...Object.fromEntries(tabStateNames.map((s) => [`${s} tab`, tabStateTokens(s)])),
|
|
52
53
|
};
|
|
53
|
-
states['active tab'] = [
|
|
54
|
-
...states['active tab'],
|
|
55
|
-
{ label: 'color', element: 'indicator', groupKey: 'indicator-color', variable: '--tabbar-active-border' },
|
|
56
|
-
];
|
|
57
54
|
const typeGroups: Record<string, TypeGroupConfig[]> = Object.fromEntries(
|
|
58
55
|
tabStateNames.map((s) => [`${s} tab`, tabStateTypeGroups(s)]),
|
|
59
56
|
);
|
|
@@ -71,7 +68,6 @@
|
|
|
71
68
|
|
|
72
69
|
// Linking: shape props across tab states (same tab object).
|
|
73
70
|
const linkableContexts = new Map<string, string>([
|
|
74
|
-
[`--tabbar-active-border`, `active tab`] as const,
|
|
75
71
|
...tabStateNames.flatMap((s) => [
|
|
76
72
|
[`--tabbar-${s}-tab-border-color`, `${s} tab`] as const,
|
|
77
73
|
[`--tabbar-${s}-tab-border-width`, `${s} tab`] as const,
|
|
@@ -25,6 +25,9 @@
|
|
|
25
25
|
with 2+ entries, a single variant tab strip is rendered that drives which
|
|
26
26
|
VariantGroup is focused. */
|
|
27
27
|
variants?: { value: string; label: string }[];
|
|
28
|
+
/** Variant selected when this component editor first opens. Falls back to
|
|
29
|
+
the first declared variant. */
|
|
30
|
+
defaultVariant?: string;
|
|
28
31
|
config?: import('svelte').Snippet;
|
|
29
32
|
children?: import('svelte').Snippet<[any]>;
|
|
30
33
|
}
|
|
@@ -36,6 +39,7 @@
|
|
|
36
39
|
tokens = [],
|
|
37
40
|
linked = null,
|
|
38
41
|
variants = [],
|
|
42
|
+
defaultVariant = '',
|
|
39
43
|
config,
|
|
40
44
|
children
|
|
41
45
|
}: Props = $props();
|
|
@@ -52,7 +56,10 @@
|
|
|
52
56
|
let showVariantTabs = $derived(variants.length >= 2);
|
|
53
57
|
run(() => {
|
|
54
58
|
if (showVariantTabs && ($focusedVariant === null || !variants.some((v) => v.value === $focusedVariant))) {
|
|
55
|
-
|
|
59
|
+
const preferred = variants.some((v) => v.value === defaultVariant)
|
|
60
|
+
? defaultVariant
|
|
61
|
+
: variants[0].value;
|
|
62
|
+
focusedVariant.set(preferred);
|
|
56
63
|
}
|
|
57
64
|
});
|
|
58
65
|
let resetVariables = $derived(tokens.map((t) => t.variable));
|
|
@@ -5,10 +5,7 @@
|
|
|
5
5
|
<script lang="ts">
|
|
6
6
|
import { onMount, onDestroy } from 'svelte';
|
|
7
7
|
import UIInfoPopover from '../../ui/UIInfoPopover.svelte';
|
|
8
|
-
import { get } from 'svelte/store';
|
|
9
8
|
import type {
|
|
10
|
-
AliasDiskValue,
|
|
11
|
-
ComponentConfig,
|
|
12
9
|
ComponentConfigMeta,
|
|
13
10
|
LiveSource,
|
|
14
11
|
} from '../../core/themes/themeTypes';
|
|
@@ -19,6 +16,7 @@
|
|
|
19
16
|
saveComponentConfig,
|
|
20
17
|
deleteComponentConfig,
|
|
21
18
|
writeWorkingComponentConfig,
|
|
19
|
+
componentConfigFromState,
|
|
22
20
|
} from '../../core/components/componentConfigService';
|
|
23
21
|
import {
|
|
24
22
|
editorState,
|
|
@@ -42,8 +40,6 @@
|
|
|
42
40
|
saveAsTheme,
|
|
43
41
|
} from '../../core/themes/themeService';
|
|
44
42
|
import type { ThemeMeta } from '../../core/themes/themeTypes';
|
|
45
|
-
import { CURRENT_COMPONENT_SCHEMA_VERSION } from '../../core/themes/migrations';
|
|
46
|
-
import { refToDiskValue } from '../../core/store/cssVarRef';
|
|
47
43
|
import { safeFetch } from '../../core/storage/storage';
|
|
48
44
|
import { API_BASE } from '../../core/storage/apiBase';
|
|
49
45
|
import { flashStatus } from '../../core/flashStatus';
|
|
@@ -182,36 +178,14 @@
|
|
|
182
178
|
}
|
|
183
179
|
}
|
|
184
180
|
|
|
185
|
-
function currentAliases(): Record<string, AliasDiskValue> {
|
|
186
|
-
const slice = get(editorState).components[component];
|
|
187
|
-
if (!slice) return {};
|
|
188
|
-
const out: Record<string, AliasDiskValue> = {};
|
|
189
|
-
for (const [k, ref] of Object.entries(slice.aliases)) out[k] = refToDiskValue(ref);
|
|
190
|
-
return out;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function currentConfig(): Record<string, unknown> {
|
|
194
|
-
return get(editorState).components[component]?.config ?? {};
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function currentConfigData(displayName: string): ComponentConfig {
|
|
198
|
-
const now = new Date().toISOString();
|
|
199
|
-
return {
|
|
200
|
-
name: displayName,
|
|
201
|
-
component,
|
|
202
|
-
createdAt: now,
|
|
203
|
-
updatedAt: now,
|
|
204
|
-
aliases: { ...currentAliases() },
|
|
205
|
-
config: { ...currentConfig() },
|
|
206
|
-
schemaVersion: CURRENT_COMPONENT_SCHEMA_VERSION,
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
|
|
210
181
|
/** Flush the editor state to the component's working buffer. A theme save
|
|
211
182
|
* captures the buffers, so this is what puts these edits in reach of Save
|
|
212
183
|
* and Adopt. */
|
|
213
184
|
async function persist(): Promise<void> {
|
|
214
|
-
await writeWorkingComponentConfig(
|
|
185
|
+
await writeWorkingComponentConfig(
|
|
186
|
+
component,
|
|
187
|
+
componentConfigFromState($editorState, component, openTheme.name),
|
|
188
|
+
);
|
|
215
189
|
liveSource = 'working';
|
|
216
190
|
markComponentSaved(component);
|
|
217
191
|
bumpComponentActiveRevision();
|
|
@@ -237,7 +211,11 @@
|
|
|
237
211
|
const { displayName, fileName } = detail;
|
|
238
212
|
saveStatus = 'saving';
|
|
239
213
|
try {
|
|
240
|
-
await saveComponentConfig(
|
|
214
|
+
await saveComponentConfig(
|
|
215
|
+
component,
|
|
216
|
+
fileName,
|
|
217
|
+
componentConfigFromState($editorState, component, displayName),
|
|
218
|
+
);
|
|
241
219
|
await persist();
|
|
242
220
|
flashStatus(setSaveStatus, 'saved');
|
|
243
221
|
await refreshFiles();
|
|
@@ -129,6 +129,7 @@
|
|
|
129
129
|
legend={tg.legend ?? ''}
|
|
130
130
|
colorVariable={tg.colorVariable}
|
|
131
131
|
colorLabel={tg.colorLabel ?? 'text color'}
|
|
132
|
+
colorCanBeLinked={tg.colorCanBeLinked ?? false}
|
|
132
133
|
familyVariable={tg.familyVariable}
|
|
133
134
|
familyLabel={tg.familyLabel ?? 'font family'}
|
|
134
135
|
sizeVariable={tg.sizeVariable}
|
|
@@ -171,6 +172,7 @@
|
|
|
171
172
|
legend={tg.legend ?? 'type'}
|
|
172
173
|
colorVariable={tg.colorVariable}
|
|
173
174
|
colorLabel={tg.colorLabel ?? 'text color'}
|
|
175
|
+
colorCanBeLinked={tg.colorCanBeLinked ?? false}
|
|
174
176
|
familyVariable={tg.familyVariable}
|
|
175
177
|
familyLabel={tg.familyLabel ?? 'font family'}
|
|
176
178
|
sizeVariable={tg.sizeVariable}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
interface Props {
|
|
16
16
|
colorVariable: string;
|
|
17
17
|
colorLabel?: string;
|
|
18
|
+
colorCanBeLinked?: boolean;
|
|
18
19
|
familyVariable?: string | undefined;
|
|
19
20
|
familyLabel?: string;
|
|
20
21
|
sizeVariable?: string | undefined;
|
|
@@ -42,6 +43,7 @@
|
|
|
42
43
|
let {
|
|
43
44
|
colorVariable,
|
|
44
45
|
colorLabel = 'color',
|
|
46
|
+
colorCanBeLinked = false,
|
|
45
47
|
familyVariable = undefined,
|
|
46
48
|
familyLabel = 'family',
|
|
47
49
|
sizeVariable = undefined,
|
|
@@ -65,7 +67,7 @@
|
|
|
65
67
|
<FieldsetWrapper {legend}>
|
|
66
68
|
<div class="type-grid">
|
|
67
69
|
<span class="row-label">{colorLabel}</span>
|
|
68
|
-
<UIPaletteSelector variable={colorVariable} {component} {onchange} />
|
|
70
|
+
<UIPaletteSelector variable={colorVariable} {component} canBeLinked={colorCanBeLinked} {onchange} />
|
|
69
71
|
|
|
70
72
|
{#if familyVariable}
|
|
71
73
|
<span class="row-label">{familyLabel}</span>
|
|
@@ -70,6 +70,9 @@ export type TypeGroupConfig = {
|
|
|
70
70
|
legend?: string;
|
|
71
71
|
colorVariable: string;
|
|
72
72
|
colorLabel?: string;
|
|
73
|
+
/** Whether this typography color participates in its declared cross-context
|
|
74
|
+
* sibling group. Kept explicit because most state colors are independent. */
|
|
75
|
+
colorCanBeLinked?: boolean;
|
|
73
76
|
familyVariable?: string;
|
|
74
77
|
familyLabel?: string;
|
|
75
78
|
sizeVariable?: string;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { ComponentConfig, ComponentConfigMeta, LiveSource } from '../themes/themeTypes';
|
|
2
|
+
import type { EditorState } from '../store/editorTypes';
|
|
2
3
|
import { versionedFileResource } from '../storage/files/versionedFileResourceClient';
|
|
3
4
|
import { API_BASE } from '../storage/apiBase';
|
|
4
5
|
import { liveMovedSinceBake } from '../productionPulse';
|
|
6
|
+
import { CURRENT_COMPONENT_SCHEMA_VERSION } from '../themes/migrations';
|
|
7
|
+
import { refToDiskValue } from '../store/cssVarRef';
|
|
5
8
|
|
|
6
9
|
/**
|
|
7
10
|
* REST client for per-component config files. Parallel to `colorsAndTypeService.ts`
|
|
@@ -59,6 +62,28 @@ export const deleteComponentConfig = (component: string, fileName: string): Prom
|
|
|
59
62
|
export const getActiveComponentConfig = (component: string): Promise<ComponentConfig | null> =>
|
|
60
63
|
resourceFor(component).getActive();
|
|
61
64
|
|
|
65
|
+
/** Serialize one live component slice into the on-disk config shape. Shared by
|
|
66
|
+
* the component editor's local checkpoint and the theme-level save-all flow. */
|
|
67
|
+
export function componentConfigFromState(
|
|
68
|
+
state: EditorState,
|
|
69
|
+
component: string,
|
|
70
|
+
displayName: string,
|
|
71
|
+
): ComponentConfig {
|
|
72
|
+
const now = new Date().toISOString();
|
|
73
|
+
const slice = state.components[component];
|
|
74
|
+
return {
|
|
75
|
+
name: displayName,
|
|
76
|
+
component,
|
|
77
|
+
createdAt: now,
|
|
78
|
+
updatedAt: now,
|
|
79
|
+
aliases: Object.fromEntries(
|
|
80
|
+
Object.entries(slice?.aliases ?? {}).map(([name, ref]) => [name, refToDiskValue(ref)]),
|
|
81
|
+
),
|
|
82
|
+
config: { ...(slice?.config ?? {}) },
|
|
83
|
+
schemaVersion: CURRENT_COMPONENT_SCHEMA_VERSION,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
62
87
|
/** Write the component's unsaved buffer. */
|
|
63
88
|
export async function writeWorkingComponentConfig(
|
|
64
89
|
component: string,
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
* Writes to document.documentElement and — when running inside a same-origin
|
|
5
5
|
* iframe (the live-preview overlay) — also writes to
|
|
6
6
|
* window.parent.document.documentElement. This lets the overlay editor at
|
|
7
|
-
* /live-tokens/editor drive the host site's :root
|
|
8
|
-
*
|
|
7
|
+
* /live-tokens/editor drive the host site's :root for ordinary live edits.
|
|
8
|
+
* Complete theme application additionally synchronizes each document's typed
|
|
9
|
+
* Svelte store through `themeDocumentSync`; copying CSS alone would leave an
|
|
10
|
+
* already-open editor disconnected when the load originates in the host.
|
|
9
11
|
*
|
|
10
12
|
* When the editor runs standalone at /live-tokens/editor (not inside the overlay iframe),
|
|
11
13
|
* parentRoot is null and every call is a plain single-root write.
|
|
@@ -64,10 +66,54 @@ export function getSyncedDocuments(): Document[] {
|
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
export const CSS_VAR_CHANGE_EVENT = 'cssvar:change';
|
|
69
|
+
/** One notification for a completed CSS-variable transaction. Internal editor
|
|
70
|
+
* controls use this instead of reacting once per variable during theme loads. */
|
|
71
|
+
export const CSS_VARS_CHANGE_EVENT = 'cssvars:change';
|
|
72
|
+
|
|
73
|
+
export interface CssVarsChangeDetail {
|
|
74
|
+
names: string[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let batchDepth = 0;
|
|
78
|
+
const pendingNames = new Set<string>();
|
|
79
|
+
|
|
80
|
+
function dispatchChanges(names: string[]): void {
|
|
81
|
+
if (typeof document === 'undefined' || names.length === 0) return;
|
|
82
|
+
document.dispatchEvent(new CustomEvent<CssVarsChangeDetail>(CSS_VARS_CHANGE_EVENT, {
|
|
83
|
+
detail: { names },
|
|
84
|
+
}));
|
|
85
|
+
// Preserve the public per-variable contract for consumers. These events are
|
|
86
|
+
// deliberately dispatched only after every write in a batch has landed, so
|
|
87
|
+
// a listener's computed-style read cannot force layout against a half-painted
|
|
88
|
+
// theme.
|
|
89
|
+
for (const name of names) {
|
|
90
|
+
document.dispatchEvent(new CustomEvent(CSS_VAR_CHANGE_EVENT, { detail: { name } }));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
67
93
|
|
|
68
94
|
function notifyChange(name: string): void {
|
|
69
|
-
if (
|
|
70
|
-
|
|
95
|
+
if (batchDepth > 0) {
|
|
96
|
+
pendingNames.add(name);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
dispatchChanges([name]);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Run a group of CSS-variable writes as one observable transaction. DOM
|
|
103
|
+
* styles are still applied synchronously; notifications wait until the outer
|
|
104
|
+
* batch completes. Nested batches coalesce into their parent. */
|
|
105
|
+
export function batchCssVarChanges<T>(fn: () => T): T {
|
|
106
|
+
batchDepth += 1;
|
|
107
|
+
try {
|
|
108
|
+
return fn();
|
|
109
|
+
} finally {
|
|
110
|
+
batchDepth -= 1;
|
|
111
|
+
if (batchDepth === 0 && pendingNames.size > 0) {
|
|
112
|
+
const names = Array.from(pendingNames);
|
|
113
|
+
pendingNames.clear();
|
|
114
|
+
dispatchChanges(names);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
71
117
|
}
|
|
72
118
|
|
|
73
119
|
export function setCssVar(name: string, value: string): void {
|
|
@@ -86,9 +132,11 @@ export function removeCssVar(name: string): void {
|
|
|
86
132
|
|
|
87
133
|
/** Apply a map of CSS variables to :root (and the parent :root when in an iframe). */
|
|
88
134
|
export function applyCssVariables(variables: Record<string, string>): void {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
135
|
+
batchCssVarChanges(() => {
|
|
136
|
+
for (const [name, value] of Object.entries(variables)) {
|
|
137
|
+
setCssVar(name, value);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
92
140
|
}
|
|
93
141
|
|
|
94
142
|
/** Remove all inline CSS custom properties from :root on both self and parent. */
|
|
@@ -122,3 +170,13 @@ export function scrapeCssVariables(): Record<string, string> {
|
|
|
122
170
|
}
|
|
123
171
|
return variables;
|
|
124
172
|
}
|
|
173
|
+
|
|
174
|
+
/** Test-only: forget cached window/document roots so a test can install a
|
|
175
|
+
* synthetic same-origin parent before the next write resolves them. */
|
|
176
|
+
export function __resetCssVarSyncForTests(): void {
|
|
177
|
+
selfRoot = null;
|
|
178
|
+
parentRoot = null;
|
|
179
|
+
resolved = false;
|
|
180
|
+
batchDepth = 0;
|
|
181
|
+
pendingNames.clear();
|
|
182
|
+
}
|
|
@@ -6,7 +6,7 @@ import type {
|
|
|
6
6
|
FontStackVariable,
|
|
7
7
|
SystemCascadePreset,
|
|
8
8
|
} from '../themes/themeTypes';
|
|
9
|
-
import { setCssVar, getSyncedDocuments } from '../cssVarSync';
|
|
9
|
+
import { setCssVar, removeCssVar, getSyncedDocuments } from '../cssVarSync';
|
|
10
10
|
|
|
11
11
|
export const SYSTEM_CASCADES: Record<SystemCascadePreset, string> = {
|
|
12
12
|
'system-ui-sans':
|
|
@@ -15,6 +15,13 @@ export const SYSTEM_CASCADES: Record<SystemCascadePreset, string> = {
|
|
|
15
15
|
'system-ui-mono': 'ui-monospace, "SF Mono", Menlo, Consolas, monospace',
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
const FONT_STACK_VARIABLES: FontStackVariable[] = [
|
|
19
|
+
'--font-display',
|
|
20
|
+
'--font-sans',
|
|
21
|
+
'--font-serif',
|
|
22
|
+
'--font-mono',
|
|
23
|
+
];
|
|
24
|
+
|
|
18
25
|
const LINK_ATTR = 'data-font-source-id';
|
|
19
26
|
const STYLE_ATTR = 'data-font-source-id';
|
|
20
27
|
|
|
@@ -124,7 +131,9 @@ export function resolveFontStackValues(
|
|
|
124
131
|
*/
|
|
125
132
|
export function applyFontStacks(stacks: FontStack[], sources: FontSource[]): void {
|
|
126
133
|
const resolved = resolveFontStackValues(stacks, sources);
|
|
127
|
-
for (const
|
|
128
|
-
|
|
134
|
+
for (const name of FONT_STACK_VARIABLES) {
|
|
135
|
+
const value = resolved[name];
|
|
136
|
+
if (value) setCssVar(name, value);
|
|
137
|
+
else removeCssVar(name);
|
|
129
138
|
}
|
|
130
139
|
}
|