@motion-proto/live-tokens 0.49.1 → 0.52.1
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 +72 -0
- package/dist-plugin/adjust/index.d.cts +1 -1
- package/dist-plugin/adjust/index.d.ts +1 -1
- package/dist-plugin/{chunk-76TFDTJO.js → chunk-JIGIIE5P.js} +60 -132
- package/dist-plugin/{chunk-44RSTAII.js → chunk-TKZBVIW5.js} +33 -5
- package/dist-plugin/chunk-YR6GPXW2.js +129 -0
- package/dist-plugin/generateColorsAndType/index.cjs +100 -33
- package/dist-plugin/generateColorsAndType/index.d.cts +1 -1
- package/dist-plugin/generateColorsAndType/index.d.ts +1 -1
- package/dist-plugin/generateColorsAndType/index.js +35 -14
- package/dist-plugin/index.cjs +95 -55
- package/dist-plugin/index.js +5 -3
- package/dist-plugin/{themeTypes-DSwKq-bj.d.cts → themeTypes-Dky03HL9.d.cts} +18 -5
- package/dist-plugin/{themeTypes-DSwKq-bj.d.ts → themeTypes-Dky03HL9.d.ts} +18 -5
- package/dist-plugin/tokensCssMigrations/index.cjs +71 -5
- package/dist-plugin/tokensCssMigrations/index.d.cts +20 -9
- package/dist-plugin/tokensCssMigrations/index.d.ts +20 -9
- package/dist-plugin/tokensCssMigrations/index.js +4 -1
- package/package.json +8 -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/NotificationEditor.svelte +6 -0
- 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/palettes/oklch.ts +3 -137
- package/src/editor/core/palettes/paletteDerivation.ts +79 -37
- 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/generateColorsAndType.ts +18 -8
- 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/core/themes/themeTypes.ts +18 -6
- package/src/editor/docs/content/editing-tokens.md +6 -2
- 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 +3 -3
- package/src/editor/ui/BezierCurveEditor.svelte +90 -67
- 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/PaletteEditor.svelte +150 -44
- package/src/editor/ui/ProjectFontsSection.svelte +0 -5
- package/src/editor/ui/ThemePanel.svelte +206 -42
- package/src/editor/ui/Toggle.svelte +14 -2
- 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/editor/ui/colors/ColorStory.svelte +12 -8
- package/src/editor/ui/curveEngine.ts +101 -21
- package/src/editor/ui/palette/OverridesPanel.svelte +19 -5
- package/src/editor/ui/palette/PaletteBase.svelte +17 -78
- package/src/editor/ui/palette/ScaleCurveEditor.svelte +109 -20
- package/src/editor/ui/palette/curveSummary.ts +20 -0
- package/src/editor/ui/palette/dockMagnify.ts +5 -13
- package/src/editor/ui/palette/paletteMath.ts +7 -4
- package/src/live-tokens/data/colors-and-type/autumn.json +528 -528
- package/src/live-tokens/data/colors-and-type/default.json +247 -247
- package/src/live-tokens/data/colors-and-type/halloween.json +532 -532
- package/src/live-tokens/data/colors-and-type/midnight-study.json +538 -538
- package/src/live-tokens/data/colors-and-type/ocean.json +521 -521
- package/src/live-tokens/data/colors-and-type/royal-velvet.json +532 -532
- package/src/live-tokens/data/colors-and-type/spring-meadow.json +504 -504
- package/src/live-tokens/data/colors-and-type/sunset.json +531 -531
- package/src/live-tokens/data/themes/autumn.json +527 -527
- package/src/live-tokens/data/themes/halloween.json +531 -531
- package/src/live-tokens/data/themes/midnight-study.json +537 -537
- package/src/live-tokens/data/themes/ocean.json +520 -520
- package/src/live-tokens/data/themes/royal-velvet.json +531 -531
- package/src/live-tokens/data/themes/spring-meadow.json +516 -514
- package/src/live-tokens/data/themes/sunset.json +531 -531
- package/src/live-tokens/data/tokens.generated.css +282 -282
- package/src/system/components/Button.svelte +2 -1
- package/src/system/components/FloatingTokenTags.css +2 -2
- 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/internal/backgroundContrast.ts +7 -0
- package/src/system/internal/oklch.ts +189 -0
- package/src/system/styles/fonts.css +6 -6
- package/src/system/styles/tokens.css +283 -283
|
@@ -52,6 +52,8 @@ interface TokensCssMigration {
|
|
|
52
52
|
*/
|
|
53
53
|
/** Names declared as custom properties (`--x: …;`) anywhere in the source. */
|
|
54
54
|
declare function collectDefinedTokens(css: string): Set<string>;
|
|
55
|
+
/** Declared name → value, last declaration winning (as the cascade would). */
|
|
56
|
+
declare function collectTokenValues(css: string): Map<string, string>;
|
|
55
57
|
/** Names referenced via `var(--x)` anywhere in the source. */
|
|
56
58
|
declare function collectReferencedTokens(css: string): Set<string>;
|
|
57
59
|
interface ScaleEntry {
|
|
@@ -123,16 +125,25 @@ interface ContractViolation {
|
|
|
123
125
|
id: string;
|
|
124
126
|
/** Token names the migration removed or renamed away despite declaring `additive`. */
|
|
125
127
|
removed: string[];
|
|
128
|
+
/** Token names whose value the migration rewrote despite declaring `additive`. */
|
|
129
|
+
changed: string[];
|
|
126
130
|
}
|
|
127
131
|
/**
|
|
128
|
-
* Guardrail for the token-as-API contract: an `additive` migration must
|
|
129
|
-
* remove or rename a token
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
132
|
+
* Guardrail for the token-as-API contract: an `additive` migration must only
|
|
133
|
+
* ever insert new names — it must never remove or rename a token, and never
|
|
134
|
+
* rewrite the value of one a consumer already references. We verify
|
|
135
|
+
* behaviorally rather than trusting the label: apply each additive migration to
|
|
136
|
+
* the package's own canonical `tokens.css` (which defines the full current
|
|
137
|
+
* vocabulary) and flag any token that disappears or changes value. A rename
|
|
138
|
+
* surfaces as the removal of its old name, so it is caught too.
|
|
139
|
+
*
|
|
140
|
+
* The value half is what makes a value-rewriting migration (hex → `oklch()`)
|
|
141
|
+
* impossible to mislabel: it renames nothing, so a name-only guard would wave
|
|
142
|
+
* it through as additive and the dev plugin would auto-apply a breaking change
|
|
143
|
+
* to a consumer's vendored file.
|
|
144
|
+
*
|
|
145
|
+
* This catches the dangerous direction only. (Over-labeling a no-op as
|
|
146
|
+
* `breaking` is harmless and not checked.)
|
|
136
147
|
*/
|
|
137
148
|
declare function findContractViolations(canonicalCss: string): ContractViolation[];
|
|
138
149
|
type SemverBump = 'major' | 'minor' | 'patch' | 'none';
|
|
@@ -183,4 +194,4 @@ interface ValidateInput {
|
|
|
183
194
|
*/
|
|
184
195
|
declare function validateTokensCss(input: ValidateInput): MissingToken[];
|
|
185
196
|
|
|
186
|
-
export { type BreakingGateResult, type ComponentSource, type ContractViolation, type MissingToken, type RunResult, type SemverBump, TOKENS_CSS_MIGRATIONS, type TokensCssMigration, type ValidateInput, collectDefinedTokens, collectReferencedTokens, enforceBreakingRequiresMajor, ensureScale, findContractViolations, removeToken, removeTokensMatching, renameToken, runAdditiveTokensCssMigrations, runTokensCssMigrations, semverBumpType, validateTokensCss };
|
|
197
|
+
export { type BreakingGateResult, type ComponentSource, type ContractViolation, type MissingToken, type RunResult, type SemverBump, TOKENS_CSS_MIGRATIONS, type TokensCssMigration, type ValidateInput, collectDefinedTokens, collectReferencedTokens, collectTokenValues, enforceBreakingRequiresMajor, ensureScale, findContractViolations, removeToken, removeTokensMatching, renameToken, runAdditiveTokensCssMigrations, runTokensCssMigrations, semverBumpType, validateTokensCss };
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
TOKENS_CSS_MIGRATIONS,
|
|
3
3
|
collectDefinedTokens,
|
|
4
4
|
collectReferencedTokens,
|
|
5
|
+
collectTokenValues,
|
|
5
6
|
enforceBreakingRequiresMajor,
|
|
6
7
|
ensureScale,
|
|
7
8
|
findContractViolations,
|
|
@@ -12,7 +13,8 @@ import {
|
|
|
12
13
|
runTokensCssMigrations,
|
|
13
14
|
semverBumpType,
|
|
14
15
|
validateTokensCss
|
|
15
|
-
} from "../chunk-
|
|
16
|
+
} from "../chunk-TKZBVIW5.js";
|
|
17
|
+
import "../chunk-YR6GPXW2.js";
|
|
16
18
|
import {
|
|
17
19
|
readLiveTokensConfig
|
|
18
20
|
} from "../chunk-D3ZVKOR4.js";
|
|
@@ -20,6 +22,7 @@ export {
|
|
|
20
22
|
TOKENS_CSS_MIGRATIONS,
|
|
21
23
|
collectDefinedTokens,
|
|
22
24
|
collectReferencedTokens,
|
|
25
|
+
collectTokenValues,
|
|
23
26
|
enforceBreakingRequiresMajor,
|
|
24
27
|
ensureScale,
|
|
25
28
|
findContractViolations,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@motion-proto/live-tokens",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.52.1",
|
|
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,12 @@
|
|
|
110
110
|
"check": "svelte-check --tsconfig ./tsconfig.json",
|
|
111
111
|
"test": "vitest run",
|
|
112
112
|
"test:watch": "vitest",
|
|
113
|
+
"test:e2e": "npm run test:e2e:contract && npm run test:e2e:stateful",
|
|
114
|
+
"test:e2e:contract": "playwright test tests/e2e/component-render-contract.spec.ts tests/e2e/component-alias-contract.spec.ts --workers=4",
|
|
115
|
+
"test:e2e:stateful": "playwright test tests/e2e/live-editing.spec.ts tests/e2e/theme-workflow.spec.ts --workers=1",
|
|
116
|
+
"test:e2e:components": "playwright test tests/e2e/component-render-contract.spec.ts",
|
|
117
|
+
"test:e2e:ui": "playwright test --ui",
|
|
118
|
+
"prepare:e2e": "node scripts/prepare-playwright-data.mjs",
|
|
113
119
|
"build:plugin": "tsup",
|
|
114
120
|
"build:lib": "npm run build:plugin",
|
|
115
121
|
"deploy:local": "bash scripts/deploy-local.sh",
|
|
@@ -136,6 +142,7 @@
|
|
|
136
142
|
"vite": "^8"
|
|
137
143
|
},
|
|
138
144
|
"devDependencies": {
|
|
145
|
+
"@playwright/test": "^1.62.1",
|
|
139
146
|
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
|
140
147
|
"@types/node": "^25.9.1",
|
|
141
148
|
"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,
|
|
@@ -87,12 +87,14 @@
|
|
|
87
87
|
let dismissible = $state(false);
|
|
88
88
|
let rightOption: ButtonVariantOption = $state('none');
|
|
89
89
|
let leftOption: ButtonVariantOption = $state('none');
|
|
90
|
+
let headerAction = $state(false);
|
|
90
91
|
let actions = $derived(((): NotificationActions => {
|
|
91
92
|
const a: NotificationActions = {};
|
|
92
93
|
const right = toVariant(rightOption);
|
|
93
94
|
const left = toVariant(leftOption);
|
|
94
95
|
if (right) a.right = { label: 'Confirm', variant: right, onClick: () => {} };
|
|
95
96
|
if (left) a.left = { label: 'Cancel', variant: left, onClick: () => {} };
|
|
97
|
+
if (headerAction) a.header = { label: 'Action', onClick: () => {} };
|
|
96
98
|
return a;
|
|
97
99
|
})());
|
|
98
100
|
</script>
|
|
@@ -112,6 +114,10 @@
|
|
|
112
114
|
<input type="checkbox" bind:checked={dismissible} />
|
|
113
115
|
<span>Dismissible</span>
|
|
114
116
|
</label>
|
|
117
|
+
<label class="toolbar-check">
|
|
118
|
+
<input type="checkbox" bind:checked={headerAction} />
|
|
119
|
+
<span>Header button</span>
|
|
120
|
+
</label>
|
|
115
121
|
<label class="toolbar-field">
|
|
116
122
|
<span>Right button</span>
|
|
117
123
|
<select class="canvas-toolbar-select" bind:value={rightOption}>
|
|
@@ -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
|
}
|