@motion-proto/live-tokens 0.7.1 → 0.8.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/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 +2 -1
- 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/scaffolding/AngleDial.svelte +52 -13
- package/src/editor/component-editor/scaffolding/ComponentFileManager.svelte +10 -11
- 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/types.ts +11 -0
- package/src/editor/core/components/componentConfigKeys.ts +8 -0
- 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 +18 -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/overlay/ColumnsOverlay.svelte +0 -1
- package/src/editor/overlay/LiveEditorOverlay.svelte +1 -4
- 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/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 +456 -379
- 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
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Migration } from './index';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Component-config migration (2026-05-19, sectiondivider only):
|
|
5
|
+
* the per-variant gradient was 7 flat tokens (angle + 3× (color + position)).
|
|
6
|
+
* It now lives as a single structured alias at `--sectiondivider-{v}-gradient`,
|
|
7
|
+
* carrying `{type, angle, stops[]}` inline on the component slice.
|
|
8
|
+
*
|
|
9
|
+
* The structured value is synthesized *before* this migration runs — see
|
|
10
|
+
* `synthesizeSectionDividerGradients` in `editorStore.ts`. The synthesizer
|
|
11
|
+
* walks the legacy 7-token shape and emits a `kind: 'gradient'` alias into
|
|
12
|
+
* the load path's object subset, where it survives this stripping pass and
|
|
13
|
+
* lands in the in-memory slice.
|
|
14
|
+
*
|
|
15
|
+
* This migration's only job is to delete the now-redundant flat tokens
|
|
16
|
+
* from the disk-shape string subset so a subsequent save round-trips to
|
|
17
|
+
* the new format.
|
|
18
|
+
*/
|
|
19
|
+
const FLAT_RE = /^--sectiondivider-(canvas|neutral|alternate|primary|accent|special)-gradient-(angle|stop-[123]-(color|position))$/;
|
|
20
|
+
|
|
21
|
+
export const componentMigration_2026_05_19_sectiondividerRichGradient: Migration = {
|
|
22
|
+
id: '2026-05-19-sectiondivider-rich-gradient',
|
|
23
|
+
fromVersion: 7,
|
|
24
|
+
toVersion: 8,
|
|
25
|
+
appliesTo: 'component-config',
|
|
26
|
+
apply(rawVars, meta) {
|
|
27
|
+
if (meta.component !== 'sectiondivider') return { ...rawVars };
|
|
28
|
+
const out: Record<string, string> = {};
|
|
29
|
+
for (const [key, value] of Object.entries(rawVars)) {
|
|
30
|
+
if (FLAT_RE.test(key)) continue;
|
|
31
|
+
out[key] = value;
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
},
|
|
35
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { Migration } from './index';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Component-config migration (2026-05-20, sectiondivider only):
|
|
5
|
+
* the variant axis was reshaped from six color families →
|
|
6
|
+
* three size presets (lg/md/sm). Each new variant owns its full token set
|
|
7
|
+
* (typography, geometry, AND colors / background) so the editor can author
|
|
8
|
+
* full visual presets per-variant without a separate color axis.
|
|
9
|
+
*
|
|
10
|
+
* Migration strategy: take **canvas** as the canonical family from the v8
|
|
11
|
+
* file and seed all three new variants from its values. Customisations on
|
|
12
|
+
* other v8 families (neutral, accent, primary, etc.) are dropped — the user
|
|
13
|
+
* customises each new variant from the shared canvas baseline.
|
|
14
|
+
*
|
|
15
|
+
* Two inline suffix renames also land here:
|
|
16
|
+
* `*-title-border-width` → `*-title-outline-width`
|
|
17
|
+
* `*-title-stroke-color` → `*-title-outline-color`
|
|
18
|
+
* `*-padding` → `*-spacing`
|
|
19
|
+
*
|
|
20
|
+
* The structured background payload moves through a companion rename in
|
|
21
|
+
* `migrateComponentAliases` (editorStore.ts), since the runner only sees
|
|
22
|
+
* string keys.
|
|
23
|
+
*/
|
|
24
|
+
const FAMILIES = ['canvas', 'neutral', 'alternate', 'primary', 'accent', 'special'] as const;
|
|
25
|
+
type Family = typeof FAMILIES[number];
|
|
26
|
+
const VARIANTS = ['lg', 'md', 'sm'] as const;
|
|
27
|
+
|
|
28
|
+
const SUFFIX_RENAMES: Array<[RegExp, string]> = [
|
|
29
|
+
[/-title-border-width$/, '-title-outline-width'],
|
|
30
|
+
[/-title-stroke-color$/, '-title-outline-color'],
|
|
31
|
+
[/-padding$/, '-spacing'],
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
function renameKeySuffix(key: string): string {
|
|
35
|
+
for (const [re, repl] of SUFFIX_RENAMES) {
|
|
36
|
+
if (re.test(key)) return key.replace(re, repl);
|
|
37
|
+
}
|
|
38
|
+
return key;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function splitFamilyKey(key: string): { family: Family; suffix: string } | null {
|
|
42
|
+
const m = key.match(/^--sectiondivider-(canvas|neutral|alternate|primary|accent|special)-(.+)$/);
|
|
43
|
+
if (!m) return null;
|
|
44
|
+
return { family: m[1] as Family, suffix: m[2] };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const componentMigration_2026_05_20_sectiondividerSlimVariants: Migration = {
|
|
48
|
+
id: '2026-05-20-sectiondivider-slim-variants',
|
|
49
|
+
fromVersion: 8,
|
|
50
|
+
toVersion: 9,
|
|
51
|
+
appliesTo: 'component-config',
|
|
52
|
+
apply(rawVars, meta) {
|
|
53
|
+
if (meta.component !== 'sectiondivider') return { ...rawVars };
|
|
54
|
+
const out: Record<string, string> = {};
|
|
55
|
+
|
|
56
|
+
// Collect canvas's suffix → value mapping first; everything else gets
|
|
57
|
+
// pruned. (Canvas wins; other families' customisations are discarded
|
|
58
|
+
// because the new variants are size/preset-shaped, not color-shaped.)
|
|
59
|
+
const canvasValues: Record<string, string> = {};
|
|
60
|
+
|
|
61
|
+
for (const [rawKey, value] of Object.entries(rawVars)) {
|
|
62
|
+
const key = renameKeySuffix(rawKey);
|
|
63
|
+
const parts = splitFamilyKey(key);
|
|
64
|
+
if (!parts) {
|
|
65
|
+
// Non-family-prefixed key — pass through verbatim.
|
|
66
|
+
out[key] = value;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (parts.family !== 'canvas') continue; // Drop non-canvas tokens
|
|
70
|
+
canvasValues[parts.suffix] = value;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Seed all three size variants from canvas's values.
|
|
74
|
+
for (const suffix of Object.keys(canvasValues)) {
|
|
75
|
+
for (const v of VARIANTS) {
|
|
76
|
+
out[`--sectiondivider-${v}-${suffix}`] = canvasValues[suffix];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return out;
|
|
81
|
+
},
|
|
82
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Migration } from './index';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Component-config migration (2026-05-21, sectiondivider only):
|
|
5
|
+
* the container `*-spacing` token is renamed back to `*-padding` so it can
|
|
6
|
+
* use the editor's splittable padding selector (per-side control). The new
|
|
7
|
+
* editor also adds per-element padding tokens (title / description / eyebrow),
|
|
8
|
+
* but those have no v9 predecessor — they simply start unset.
|
|
9
|
+
*/
|
|
10
|
+
export const componentMigration_2026_05_21_sectiondividerSpacingToPadding: Migration = {
|
|
11
|
+
id: '2026-05-21-sectiondivider-spacing-to-padding',
|
|
12
|
+
fromVersion: 9,
|
|
13
|
+
toVersion: 10,
|
|
14
|
+
appliesTo: 'component-config',
|
|
15
|
+
apply(rawVars, meta) {
|
|
16
|
+
if (meta.component !== 'sectiondivider') return { ...rawVars };
|
|
17
|
+
const out: Record<string, string> = {};
|
|
18
|
+
for (const [key, value] of Object.entries(rawVars)) {
|
|
19
|
+
const renamed = key.replace(/-spacing$/, '-padding');
|
|
20
|
+
out[renamed] = value;
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
},
|
|
24
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { Migration } from './index';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Component-config migration (2026-05-22, sectiondivider only):
|
|
5
|
+
* the per-variant *intrinsics* (align, hairline visibility + position, eyebrow
|
|
6
|
+
* visibility + uppercase, description visibility) move from the `config`
|
|
7
|
+
* bucket — where the editor preview consumed them as runtime props — into
|
|
8
|
+
* the `aliases` bucket as cascading CSS variables. Live consumers can then
|
|
9
|
+
* read them off `:root` via `var(...)`, closing the editor-to-consumer sync
|
|
10
|
+
* gap the old config-bucket encoding had.
|
|
11
|
+
*
|
|
12
|
+
* Mapping per variant `v in (lg, md, sm)`:
|
|
13
|
+
* --sectiondivider-{v}-show-eyebrow ('1' | '') → --sectiondivider-{v}-eyebrow-display ('block' | 'none')
|
|
14
|
+
* --sectiondivider-{v}-show-description ('1' | '') → --sectiondivider-{v}-description-display ('flex' | 'none')
|
|
15
|
+
* --sectiondivider-{v}-show-hairline + -hairline (toggle + pos) → --sectiondivider-{v}-hairline ('none' | <position>)
|
|
16
|
+
* --sectiondivider-{v}-eyebrow-uppercase ('1' | '') → --sectiondivider-{v}-eyebrow-text-transform ('uppercase' | 'none')
|
|
17
|
+
* --sectiondivider-{v}-align → unchanged (value space is already CSS-keyword: 'start' | 'center')
|
|
18
|
+
* --sectiondivider-{v}-color-family → unchanged (still a config-bucket entry; drives editor's family swap)
|
|
19
|
+
*
|
|
20
|
+
* The hairline merge collapses two old keys (a boolean toggle + a position
|
|
21
|
+
* enum) into one. Rules (matching the legacy `getShowHairline` reader):
|
|
22
|
+
* - `show-hairline` explicitly '' → 'none'
|
|
23
|
+
* - `show-hairline` explicitly '1' → existing position, or 'above-label' fallback
|
|
24
|
+
* - both undefined → emit nothing (default 'none' applies via CSS)
|
|
25
|
+
* - `show-hairline` unset but a real position exists → preserve the position
|
|
26
|
+
*
|
|
27
|
+
* The migration touches the unified bag produced by `migrateComponentAliases`
|
|
28
|
+
* (which now pre-merges string-valued config entries). The post-migration
|
|
29
|
+
* `splitAliasesAndConfig` step routes the new keys to `aliases` (since
|
|
30
|
+
* `KNOWN_COMPONENT_CONFIG_KEYS` no longer contains them) while keeping
|
|
31
|
+
* `--sectiondivider-{v}-color-family` in `config`.
|
|
32
|
+
*/
|
|
33
|
+
export const componentMigration_2026_05_22_sectiondividerIntrinsicsToCss: Migration = {
|
|
34
|
+
id: '2026-05-22-sectiondivider-intrinsics-to-css',
|
|
35
|
+
fromVersion: 10,
|
|
36
|
+
toVersion: 11,
|
|
37
|
+
appliesTo: 'component-config',
|
|
38
|
+
apply(rawVars, meta) {
|
|
39
|
+
if (meta.component !== 'sectiondivider') return { ...rawVars };
|
|
40
|
+
const VARIANTS = ['lg', 'md', 'sm'] as const;
|
|
41
|
+
const prefix = '--sectiondivider-';
|
|
42
|
+
const dropped = new Set<string>();
|
|
43
|
+
for (const v of VARIANTS) {
|
|
44
|
+
dropped.add(`${prefix}${v}-show-eyebrow`);
|
|
45
|
+
dropped.add(`${prefix}${v}-show-description`);
|
|
46
|
+
dropped.add(`${prefix}${v}-show-hairline`);
|
|
47
|
+
dropped.add(`${prefix}${v}-hairline`);
|
|
48
|
+
dropped.add(`${prefix}${v}-eyebrow-uppercase`);
|
|
49
|
+
}
|
|
50
|
+
const out: Record<string, string> = {};
|
|
51
|
+
for (const [key, value] of Object.entries(rawVars)) {
|
|
52
|
+
if (dropped.has(key)) continue;
|
|
53
|
+
out[key] = value;
|
|
54
|
+
}
|
|
55
|
+
for (const v of VARIANTS) {
|
|
56
|
+
const showEyebrow = rawVars[`${prefix}${v}-show-eyebrow`];
|
|
57
|
+
if (showEyebrow !== undefined) {
|
|
58
|
+
out[`${prefix}${v}-eyebrow-display`] = showEyebrow === '1' ? 'block' : 'none';
|
|
59
|
+
}
|
|
60
|
+
const showDesc = rawVars[`${prefix}${v}-show-description`];
|
|
61
|
+
if (showDesc !== undefined) {
|
|
62
|
+
out[`${prefix}${v}-description-display`] = showDesc === '1' ? 'flex' : 'none';
|
|
63
|
+
}
|
|
64
|
+
const showHair = rawVars[`${prefix}${v}-show-hairline`];
|
|
65
|
+
const oldHair = rawVars[`${prefix}${v}-hairline`];
|
|
66
|
+
const oldHairIsPos = typeof oldHair === 'string' && oldHair !== '' && oldHair !== 'off';
|
|
67
|
+
let newHair: string | undefined;
|
|
68
|
+
if (showHair === '') newHair = 'none';
|
|
69
|
+
else if (showHair === '1') newHair = oldHairIsPos ? oldHair : 'above-label';
|
|
70
|
+
else if (oldHair !== undefined) newHair = oldHairIsPos ? oldHair : 'none';
|
|
71
|
+
if (newHair !== undefined) {
|
|
72
|
+
out[`${prefix}${v}-hairline`] = newHair;
|
|
73
|
+
}
|
|
74
|
+
const upper = rawVars[`${prefix}${v}-eyebrow-uppercase`];
|
|
75
|
+
if (upper !== undefined) {
|
|
76
|
+
out[`${prefix}${v}-eyebrow-text-transform`] = upper === '1' ? 'uppercase' : 'none';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
},
|
|
81
|
+
};
|
|
@@ -40,6 +40,11 @@ import {
|
|
|
40
40
|
themeMigration_2026_05_13_primaryToBrand,
|
|
41
41
|
componentMigration_2026_05_13_primaryToBrand,
|
|
42
42
|
} from './2026-05-13-primary-to-brand';
|
|
43
|
+
import { componentMigration_2026_05_19_collapsiblesectionDropFrameSurface } from './2026-05-19-collapsiblesection-drop-frame-surface';
|
|
44
|
+
import { componentMigration_2026_05_19_sectiondividerRichGradient } from './2026-05-19-sectiondivider-rich-gradient';
|
|
45
|
+
import { componentMigration_2026_05_20_sectiondividerSlimVariants } from './2026-05-20-sectiondivider-slim-variants';
|
|
46
|
+
import { componentMigration_2026_05_21_sectiondividerSpacingToPadding } from './2026-05-21-sectiondivider-spacing-to-padding';
|
|
47
|
+
import { componentMigration_2026_05_22_sectiondividerIntrinsicsToCss } from './2026-05-22-sectiondivider-intrinsics-to-css';
|
|
43
48
|
|
|
44
49
|
/**
|
|
45
50
|
* Registered migrations. Order in this array does not matter — the runner
|
|
@@ -54,6 +59,11 @@ export const MIGRATIONS: Migration[] = [
|
|
|
54
59
|
componentMigration_2026_05_08_collapsiblesectionFrameAndCleanup,
|
|
55
60
|
componentMigration_2026_05_10_sectiondividerGradientStops,
|
|
56
61
|
componentMigration_2026_05_13_primaryToBrand,
|
|
62
|
+
componentMigration_2026_05_19_collapsiblesectionDropFrameSurface,
|
|
63
|
+
componentMigration_2026_05_19_sectiondividerRichGradient,
|
|
64
|
+
componentMigration_2026_05_20_sectiondividerSlimVariants,
|
|
65
|
+
componentMigration_2026_05_21_sectiondividerSpacingToPadding,
|
|
66
|
+
componentMigration_2026_05_22_sectiondividerIntrinsicsToCss,
|
|
57
67
|
];
|
|
58
68
|
|
|
59
69
|
function countFor(kind: 'theme' | 'component-config'): number {
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
import { writable, derived, get, type Readable } from 'svelte/store';
|
|
33
33
|
import type { CssVarRef, EditorState } from '../../store/editorTypes';
|
|
34
34
|
import { store, mutate } from '../../store/editorCore';
|
|
35
|
+
import { formatGradientValue } from './gradients';
|
|
35
36
|
|
|
36
37
|
const EMPTY_COMPONENT_BASELINE = JSON.stringify({ aliases: {}, config: {} });
|
|
37
38
|
|
|
@@ -43,7 +44,9 @@ export function componentsToVars(components: EditorState['components']): Record<
|
|
|
43
44
|
const out: Record<string, string> = {};
|
|
44
45
|
for (const slice of Object.values(components)) {
|
|
45
46
|
for (const [varName, ref] of Object.entries(slice.aliases)) {
|
|
46
|
-
|
|
47
|
+
if (ref.kind === 'token') out[varName] = `var(${ref.name})`;
|
|
48
|
+
else if (ref.kind === 'literal') out[varName] = ref.value;
|
|
49
|
+
else out[varName] = formatGradientValue(ref.value);
|
|
47
50
|
}
|
|
48
51
|
}
|
|
49
52
|
return out;
|
|
@@ -239,9 +242,20 @@ export function getComponentPropertySiblings(component: string, varName: string)
|
|
|
239
242
|
function cssVarRefEqual(a: CssVarRef | undefined, b: CssVarRef | undefined): boolean {
|
|
240
243
|
if (!a || !b) return a === b;
|
|
241
244
|
if (a.kind !== b.kind) return false;
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
+
if (a.kind === 'token') return a.name === (b as { kind: 'token'; name: string }).name;
|
|
246
|
+
if (a.kind === 'literal') return a.value === (b as { kind: 'literal'; value: string }).value;
|
|
247
|
+
// gradient: structural compare on type, angle, aspect axes, and stops.
|
|
248
|
+
const av = a.value;
|
|
249
|
+
const bv = (b as { kind: 'gradient'; value: typeof a.value }).value;
|
|
250
|
+
if (av.type !== bv.type || av.angle !== bv.angle || av.stops.length !== bv.stops.length) return false;
|
|
251
|
+
if ((av.aspectX ?? 1) !== (bv.aspectX ?? 1)) return false;
|
|
252
|
+
if ((av.aspectY ?? 1) !== (bv.aspectY ?? 1)) return false;
|
|
253
|
+
for (let i = 0; i < av.stops.length; i++) {
|
|
254
|
+
const sa = av.stops[i];
|
|
255
|
+
const sb = bv.stops[i];
|
|
256
|
+
if (sa.position !== sb.position || sa.color !== sb.color || (sa.opacity ?? 100) !== (sb.opacity ?? 100)) return false;
|
|
257
|
+
}
|
|
258
|
+
return true;
|
|
245
259
|
}
|
|
246
260
|
|
|
247
261
|
/** True iff `varName` is not individually opted out, has ≥2 declared siblings,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* (`--color-brand-500`); the renderer wraps them in `var(...)` so palette
|
|
5
5
|
* edits flow through.
|
|
6
6
|
*/
|
|
7
|
-
import type { EditorState, GradientToken, GradientTokenStop, GradientType } from '../../store/editorTypes';
|
|
7
|
+
import type { EditorState, GradientToken, GradientTokenStop, GradientType, GradientAliasValue } from '../../store/editorTypes';
|
|
8
8
|
import { mutate } from '../../store/editorCore';
|
|
9
9
|
|
|
10
10
|
export function makeDefaultGradients(): GradientToken[] {
|
|
@@ -49,12 +49,7 @@ export function makeDefaultGradients(): GradientToken[] {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
function formatGradientStop(s: GradientTokenStop): string {
|
|
52
|
-
|
|
53
|
-
const opacity = s.opacity ?? 100;
|
|
54
|
-
const color = opacity >= 100
|
|
55
|
-
? base
|
|
56
|
-
: `color-mix(in srgb, ${base} ${opacity}%, transparent)`;
|
|
57
|
-
return `${color} ${s.position}%`;
|
|
52
|
+
return `${formatGradientStopColor(s)} ${s.position}%`;
|
|
58
53
|
}
|
|
59
54
|
|
|
60
55
|
/** Stops portion only — used by the palette selector to materialize a
|
|
@@ -64,10 +59,67 @@ export function formatGradientStops(t: GradientToken): string {
|
|
|
64
59
|
return t.stops.map(formatGradientStop).join(', ');
|
|
65
60
|
}
|
|
66
61
|
|
|
62
|
+
/** Serialize a structured gradient value (theme token or component-owned)
|
|
63
|
+
* into a CSS background declaration.
|
|
64
|
+
* - `none` → `transparent` (no background paint).
|
|
65
|
+
* - `solid` → the first stop's color (no gradient function).
|
|
66
|
+
* - `linear` → `linear-gradient(<angle>, <stops>)`.
|
|
67
|
+
* - `radial` → `radial-gradient(<shape> at <centerX>% 50%, <stops>)`.
|
|
68
|
+
* centerX defaults to 50. Shape is `circle [radius]` when the
|
|
69
|
+
* aspect ratio is 1 (or absent); for aspect ≠ 1 we emit
|
|
70
|
+
* `ellipse rx ry` anchored to `radius || 100px`, area-preserved
|
|
71
|
+
* so the R=1 boundary is continuous with the legacy circle. */
|
|
72
|
+
export function formatGradientValue(v: GradientAliasValue): string {
|
|
73
|
+
if (v.type === 'none') return 'transparent';
|
|
74
|
+
if (v.type === 'solid') {
|
|
75
|
+
const first = v.stops[0];
|
|
76
|
+
if (!first) return 'transparent';
|
|
77
|
+
return formatGradientStopColor(first);
|
|
78
|
+
}
|
|
79
|
+
const stops = v.stops.map(formatGradientStop).join(', ');
|
|
80
|
+
if (v.type === 'linear') return `linear-gradient(${v.angle}deg, ${stops})`;
|
|
81
|
+
const cx = v.centerX ?? 50;
|
|
82
|
+
return `radial-gradient(${formatRadialShape(v)} at ${cx}% 50%, ${stops})`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Default base radius (px) when the gradient has no explicit `radius` but
|
|
86
|
+
* needs concrete dimensions to express its aspect ratio. Chosen as a
|
|
87
|
+
* pleasant-looking mid-size that reads as a "soft glow" inside typical
|
|
88
|
+
* component containers; the user can dial `radius` to override. */
|
|
89
|
+
const DEFAULT_RADIAL_BASE_PX = 100;
|
|
90
|
+
|
|
91
|
+
function formatRadialShape(v: GradientAliasValue): string {
|
|
92
|
+
const ax = v.aspectX ?? 1;
|
|
93
|
+
const ay = v.aspectY ?? 1;
|
|
94
|
+
if (ax === 1 && ay === 1) {
|
|
95
|
+
return v.radius && v.radius > 0 ? `circle ${v.radius}px` : 'circle';
|
|
96
|
+
}
|
|
97
|
+
const base = v.radius && v.radius > 0 ? v.radius : DEFAULT_RADIAL_BASE_PX;
|
|
98
|
+
const rx = base * ax;
|
|
99
|
+
const ry = base * ay;
|
|
100
|
+
return `ellipse ${rx.toFixed(2)}px ${ry.toFixed(2)}px`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Resolve a stop's color + opacity into a CSS color value without the
|
|
104
|
+
* trailing `${position}%`. Shared by the gradient-stop formatter (which
|
|
105
|
+
* appends the position) and the solid path (which doesn't). */
|
|
106
|
+
function formatGradientStopColor(s: GradientTokenStop): string {
|
|
107
|
+
const base = s.color.startsWith('--') ? `var(${s.color})` : s.color;
|
|
108
|
+
const opacity = s.opacity ?? 100;
|
|
109
|
+
return opacity >= 100
|
|
110
|
+
? base
|
|
111
|
+
: `color-mix(in srgb, ${base} ${opacity}%, transparent)`;
|
|
112
|
+
}
|
|
113
|
+
|
|
67
114
|
function formatGradient(t: GradientToken): string {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
115
|
+
return formatGradientValue({
|
|
116
|
+
type: t.type,
|
|
117
|
+
angle: t.angle,
|
|
118
|
+
centerX: t.centerX,
|
|
119
|
+
aspectX: t.aspectX,
|
|
120
|
+
aspectY: t.aspectY,
|
|
121
|
+
stops: t.stops,
|
|
122
|
+
});
|
|
71
123
|
}
|
|
72
124
|
|
|
73
125
|
export function gradientsToVars(g: EditorState['gradients']): Record<string, string> {
|
|
@@ -80,17 +132,20 @@ function findGradient(s: EditorState, variable: string): GradientToken | undefin
|
|
|
80
132
|
return s.gradients.tokens.find((t) => t.variable === variable);
|
|
81
133
|
}
|
|
82
134
|
|
|
83
|
-
/** Replace a gradient's type, angle, and stops in one shot.
|
|
84
|
-
* to restore a pre-edit snapshot on Cancel. */
|
|
135
|
+
/** Replace a gradient's type, angle, centerX, aspect, and stops in one shot.
|
|
136
|
+
* Used by the editor to restore a pre-edit snapshot on Cancel. */
|
|
85
137
|
export function setGradient(
|
|
86
138
|
variable: string,
|
|
87
|
-
next: { type: GradientType; angle: number; stops: GradientTokenStop[] },
|
|
139
|
+
next: { type: GradientType; angle: number; centerX?: number; aspectX?: number; aspectY?: number; stops: GradientTokenStop[] },
|
|
88
140
|
): void {
|
|
89
141
|
mutate(`replace gradient ${variable}`, (s) => {
|
|
90
142
|
const t = findGradient(s, variable);
|
|
91
143
|
if (!t) return;
|
|
92
144
|
t.type = next.type;
|
|
93
145
|
t.angle = next.angle;
|
|
146
|
+
t.centerX = next.centerX;
|
|
147
|
+
t.aspectX = next.aspectX;
|
|
148
|
+
t.aspectY = next.aspectY;
|
|
94
149
|
t.stops = next.stops.map((st) => ({ ...st }));
|
|
95
150
|
});
|
|
96
151
|
}
|
|
@@ -109,6 +164,26 @@ export function setGradientAngle(variable: string, angle: number): void {
|
|
|
109
164
|
});
|
|
110
165
|
}
|
|
111
166
|
|
|
167
|
+
export function setGradientCenterX(variable: string, centerX: number): void {
|
|
168
|
+
mutate(`set gradient center ${variable}`, (s) => {
|
|
169
|
+
const t = findGradient(s, variable);
|
|
170
|
+
if (t) t.centerX = centerX;
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function setGradientAspect(variable: string, aspect: { x: number; y: number }): void {
|
|
175
|
+
mutate(`set gradient aspect ${variable}`, (s) => {
|
|
176
|
+
const t = findGradient(s, variable);
|
|
177
|
+
if (!t) return;
|
|
178
|
+
// Drop axes that equal 1 (the legacy circle baseline) so the persisted
|
|
179
|
+
// shape stays minimal and old data round-trips unchanged.
|
|
180
|
+
if (aspect.x === 1) delete t.aspectX;
|
|
181
|
+
else t.aspectX = aspect.x;
|
|
182
|
+
if (aspect.y === 1) delete t.aspectY;
|
|
183
|
+
else t.aspectY = aspect.y;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
112
187
|
export function setGradientStop(variable: string, index: number, stop: Partial<GradientTokenStop>): void {
|
|
113
188
|
mutate(`set gradient stop ${variable}[${index}]`, (s) => {
|
|
114
189
|
const t = findGradient(s, variable);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Theme } from './themeTypes';
|
|
1
|
+
import type { AliasDiskValue, Theme } from './themeTypes';
|
|
2
2
|
import { activeFileName } from '../store/editorConfigStore';
|
|
3
3
|
import { migrateThemeFonts } from '../fonts/fontMigration';
|
|
4
4
|
import { applyFontSources, applyFontStacks } from '../fonts/fontLoader';
|
|
@@ -52,7 +52,7 @@ export async function initializeTheme(): Promise<void> {
|
|
|
52
52
|
if (list && Array.isArray(list.components)) {
|
|
53
53
|
const configs: Record<
|
|
54
54
|
string,
|
|
55
|
-
{ activeFile: string; aliases: Record<string,
|
|
55
|
+
{ activeFile: string; aliases: Record<string, AliasDiskValue>; config?: Record<string, unknown>; schemaVersion?: number }
|
|
56
56
|
> = {};
|
|
57
57
|
await Promise.all(
|
|
58
58
|
list.components.map(async (c) => {
|
|
@@ -27,6 +27,19 @@ export interface PaletteConfig {
|
|
|
27
27
|
gradientStops?: GradientStop[];
|
|
28
28
|
gradientSize?: 'page' | 'window';
|
|
29
29
|
anchorToBase?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Set to true by importers when they overlay `cssVariables[--color-{ns}-*]`
|
|
32
|
+
* without owning the typed-state curves. The storage-layer reconciler uses
|
|
33
|
+
* it as an opt-in switch: snap `baseColor` (or `tintHue`+`tintChroma` for
|
|
34
|
+
* gray palettes) to the imported `--color-{ns}-500` anchor and clear the
|
|
35
|
+
* flag. Editor-authored themes never set this, so the reconciler is a
|
|
36
|
+
* strict no-op for them.
|
|
37
|
+
*
|
|
38
|
+
* Persists on disk for first-load reconciliation. After reconcile strips
|
|
39
|
+
* the palette-derived keys from `cssVariables`, subsequent reconciles find
|
|
40
|
+
* no anchor and become idempotent no-ops regardless of the flag's value.
|
|
41
|
+
*/
|
|
42
|
+
_imported?: boolean;
|
|
30
43
|
}
|
|
31
44
|
|
|
32
45
|
export type FontSourceKind = 'google' | 'typekit' | 'css-url' | 'font-face';
|
|
@@ -92,12 +105,20 @@ export interface ThemeMeta {
|
|
|
92
105
|
isActive: boolean;
|
|
93
106
|
}
|
|
94
107
|
|
|
108
|
+
/** On-disk shape of a single alias entry. Plain strings carry the bulk of
|
|
109
|
+
* aliases (token refs like `--surface-canvas-low` or literal CSS like `4px`);
|
|
110
|
+
* the gradient object shape is the structured payload for component-owned
|
|
111
|
+
* gradients that can't compress to a single string. */
|
|
112
|
+
export type AliasDiskValue =
|
|
113
|
+
| string
|
|
114
|
+
| { kind: 'gradient'; value: { type: 'linear' | 'radial' | 'solid' | 'none'; angle: number; radius?: number; centerX?: number; aspectX?: number; aspectY?: number; stops: { position: number; color: string; opacity?: number }[] } };
|
|
115
|
+
|
|
95
116
|
export interface ComponentConfig {
|
|
96
117
|
name: string;
|
|
97
118
|
component: string;
|
|
98
119
|
createdAt: string;
|
|
99
120
|
updatedAt: string;
|
|
100
|
-
aliases: Record<string,
|
|
121
|
+
aliases: Record<string, AliasDiskValue>;
|
|
101
122
|
config?: Record<string, unknown>;
|
|
102
123
|
/**
|
|
103
124
|
* Server-attached file-name marker. Same role as `Theme._fileName`. Set by
|
|
@@ -139,6 +160,40 @@ export interface Manifest {
|
|
|
139
160
|
_fileName?: string;
|
|
140
161
|
}
|
|
141
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Transport artifact for sharing a manifest with someone else. Self-contained:
|
|
165
|
+
* the bundle inlines the referenced theme and every non-default component
|
|
166
|
+
* config so the receiver doesn't need anything else on disk to apply it.
|
|
167
|
+
*
|
|
168
|
+
* Bundles are *not* stored under `manifests/` — they're transient downloads /
|
|
169
|
+
* uploads. Local manifests stay lightweight pointer files; bundles are the
|
|
170
|
+
* import/export envelope. See temp/manifest-robustness-plan.md §11.
|
|
171
|
+
*
|
|
172
|
+
* `componentConfigs` is keyed by `${component}/${configName}` so a single map
|
|
173
|
+
* carries multiple components. Entries whose manifest value is `"default"`
|
|
174
|
+
* are deliberately omitted — the receiver's local `default.json` is the
|
|
175
|
+
* live-tokens package's canonical default, and shipping the sender's default
|
|
176
|
+
* would risk version-divergence with no clean conflict story.
|
|
177
|
+
*/
|
|
178
|
+
export interface ManifestBundle {
|
|
179
|
+
/** Discriminator for safe identification of bundle JSON files. */
|
|
180
|
+
kind: 'manifest-bundle';
|
|
181
|
+
/** Bumps when the bundle envelope shape changes. Start at 1. */
|
|
182
|
+
schemaVersion: 1;
|
|
183
|
+
/** Sender's `@motion-proto/live-tokens` package version. Receiver can
|
|
184
|
+
* compare to its own to warn about compatibility drift. */
|
|
185
|
+
liveTokensVersion: string;
|
|
186
|
+
/** ISO timestamp of when the bundle was exported. */
|
|
187
|
+
exportedAt: string;
|
|
188
|
+
/** Full pointer-form manifest (same shape as on-disk manifest files). */
|
|
189
|
+
manifest: Manifest;
|
|
190
|
+
/** Full content of the theme that `manifest.theme` references. */
|
|
191
|
+
theme: Theme;
|
|
192
|
+
/** Full content of each non-default component config referenced by
|
|
193
|
+
* `manifest.componentConfigs`, keyed by `${component}/${configName}`. */
|
|
194
|
+
componentConfigs: Record<string, ComponentConfig>;
|
|
195
|
+
}
|
|
196
|
+
|
|
142
197
|
export interface ManifestMeta {
|
|
143
198
|
name: string;
|
|
144
199
|
fileName: string;
|
|
@@ -422,7 +422,7 @@
|
|
|
422
422
|
|
|
423
423
|
.lt-overlay.no-transition,
|
|
424
424
|
.lt-overlay.no-transition .frame-wrap {
|
|
425
|
-
transition: none
|
|
425
|
+
transition: none;
|
|
426
426
|
}
|
|
427
427
|
|
|
428
428
|
.header {
|
|
@@ -451,7 +451,6 @@
|
|
|
451
451
|
font-size: var(--ui-font-size-md, 16px);
|
|
452
452
|
font-weight: var(--ui-font-weight-semibold, 600);
|
|
453
453
|
color: rgba(255, 255, 255, 0.85);
|
|
454
|
-
letter-spacing: 0.02em;
|
|
455
454
|
}
|
|
456
455
|
|
|
457
456
|
.spacer { flex: 1; }
|
|
@@ -460,7 +459,6 @@
|
|
|
460
459
|
font-size: var(--ui-font-size-md, 16px);
|
|
461
460
|
font-weight: var(--ui-font-weight-medium, 500);
|
|
462
461
|
color: rgba(255, 255, 255, 0.4);
|
|
463
|
-
letter-spacing: 0.02em;
|
|
464
462
|
margin-left: var(--ui-space-2, 2px);
|
|
465
463
|
user-select: none;
|
|
466
464
|
}
|
|
@@ -523,7 +521,6 @@
|
|
|
523
521
|
.seg-label {
|
|
524
522
|
font-size: var(--ui-font-size-md, 16px);
|
|
525
523
|
font-weight: var(--ui-font-weight-semibold, 600);
|
|
526
|
-
letter-spacing: 0.02em;
|
|
527
524
|
color: var(--ui-text-primary, #fff);
|
|
528
525
|
}
|
|
529
526
|
|