@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.
Files changed (85) hide show
  1. package/dist-plugin/index.cjs +707 -90
  2. package/dist-plugin/index.d.cts +1 -0
  3. package/dist-plugin/index.d.ts +1 -0
  4. package/dist-plugin/index.js +707 -90
  5. package/package.json +2 -1
  6. package/src/app/site.css +1 -1
  7. package/src/editor/component-editor/CollapsibleSectionEditor.svelte +34 -27
  8. package/src/editor/component-editor/DialogEditor.svelte +4 -4
  9. package/src/editor/component-editor/NotificationEditor.svelte +3 -1
  10. package/src/editor/component-editor/SectionDividerEditor.svelte +439 -112
  11. package/src/editor/component-editor/StandardButtonsEditor.svelte +13 -1
  12. package/src/editor/component-editor/editors.d.ts +10 -0
  13. package/src/editor/component-editor/scaffolding/AngleDial.svelte +52 -13
  14. package/src/editor/component-editor/scaffolding/ComponentFileManager.svelte +10 -11
  15. package/src/editor/component-editor/scaffolding/LinkedBlock.svelte +0 -1
  16. package/src/editor/component-editor/scaffolding/RadialShapePad.svelte +483 -0
  17. package/src/editor/component-editor/scaffolding/ShadowBackdrop.svelte +15 -2
  18. package/src/editor/component-editor/scaffolding/StateBlock.svelte +103 -15
  19. package/src/editor/component-editor/scaffolding/TokenLayout.svelte +9 -6
  20. package/src/editor/component-editor/scaffolding/TypeEditor.svelte +13 -1
  21. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +239 -25
  22. package/src/editor/component-editor/scaffolding/buildTypeGroupTokens.ts +1 -0
  23. package/src/editor/component-editor/scaffolding/types.ts +11 -0
  24. package/src/editor/core/components/componentConfigKeys.ts +8 -0
  25. package/src/editor/core/components/componentConfigService.ts +2 -2
  26. package/src/editor/core/components/componentPersist.ts +7 -5
  27. package/src/editor/core/manifests/manifestService.ts +58 -3
  28. package/src/editor/core/palettes/familySwap.ts +99 -0
  29. package/src/editor/core/palettes/paletteDerivation.ts +69 -0
  30. package/src/editor/core/palettes/tokenRegistry.ts +4 -1
  31. package/src/editor/core/store/editorStore.ts +206 -12
  32. package/src/editor/core/store/editorTypes.ts +55 -12
  33. package/src/editor/core/store/gradientSource.ts +192 -0
  34. package/src/editor/core/themes/migrations/2026-05-19-collapsiblesection-drop-frame-surface.ts +28 -0
  35. package/src/editor/core/themes/migrations/2026-05-19-sectiondivider-rich-gradient.ts +35 -0
  36. package/src/editor/core/themes/migrations/2026-05-20-sectiondivider-slim-variants.ts +82 -0
  37. package/src/editor/core/themes/migrations/2026-05-21-sectiondivider-spacing-to-padding.ts +24 -0
  38. package/src/editor/core/themes/migrations/2026-05-22-sectiondivider-intrinsics-to-css.ts +81 -0
  39. package/src/editor/core/themes/migrations/index.ts +10 -0
  40. package/src/editor/core/themes/slices/components.ts +18 -4
  41. package/src/editor/core/themes/slices/gradients.ts +88 -13
  42. package/src/editor/core/themes/themeInit.ts +2 -2
  43. package/src/editor/core/themes/themeTypes.ts +56 -1
  44. package/src/editor/overlay/ColumnsOverlay.svelte +0 -1
  45. package/src/editor/overlay/LiveEditorOverlay.svelte +1 -4
  46. package/src/editor/styles/ui-editor.css +1 -0
  47. package/src/editor/styles/ui-form-controls.css +19 -20
  48. package/src/editor/ui/BezierCurveEditor.svelte +114 -63
  49. package/src/editor/ui/EditorViewSwitcher.svelte +0 -1
  50. package/src/editor/ui/FileLoadList.svelte +22 -5
  51. package/src/editor/ui/FontStackEditor.svelte +214 -76
  52. package/src/editor/ui/GradientEditor.svelte +435 -215
  53. package/src/editor/ui/GradientStopPicker.svelte +11 -3
  54. package/src/editor/ui/ManifestFileManager.svelte +71 -4
  55. package/src/editor/ui/PaletteEditor.svelte +52 -79
  56. package/src/editor/ui/ProjectFontsSection.svelte +328 -293
  57. package/src/editor/ui/ThemeFileManager.svelte +0 -4
  58. package/src/editor/ui/UIFontFamilySelector.svelte +0 -1
  59. package/src/editor/ui/UIFontSizeSelector.svelte +3 -0
  60. package/src/editor/ui/UIInfoPopover.svelte +0 -1
  61. package/src/editor/ui/UILetterSpacingSelector.svelte +65 -0
  62. package/src/editor/ui/UIPaletteSelector.svelte +31 -4
  63. package/src/editor/ui/UIPillButton.svelte +33 -3
  64. package/src/editor/ui/UISegmentedControl.svelte +114 -0
  65. package/src/editor/ui/UITokenSelector.svelte +4 -1
  66. package/src/editor/ui/VariablesTab.svelte +41 -35
  67. package/src/editor/ui/palette/OverridesPanel.svelte +14 -37
  68. package/src/editor/ui/palette/PaletteBase.svelte +3 -3
  69. package/src/editor/ui/sections/ColumnsSection.svelte +1 -2
  70. package/src/editor/ui/sections/GradientsSection.svelte +1 -1
  71. package/src/editor/ui/sections/OverlaysSection.svelte +1 -1
  72. package/src/editor/ui/sections/ShadowsSection.svelte +1 -1
  73. package/src/system/components/Button.svelte +2 -2
  74. package/src/system/components/Card.svelte +29 -1
  75. package/src/system/components/CollapsibleSection.svelte +25 -2
  76. package/src/system/components/FloatingTokenTags.css +43 -24
  77. package/src/system/components/FloatingTokenTags.svelte +88 -137
  78. package/src/system/components/Notification.svelte +8 -1
  79. package/src/system/components/SectionDivider.svelte +456 -379
  80. package/src/system/styles/CONVENTIONS.md +1 -1
  81. package/src/system/styles/fonts.css +3 -16
  82. package/src/system/styles/tokens.css +356 -1199
  83. package/src/system/styles/tokens.generated.css +544 -0
  84. package/src/editor/component-editor/scaffolding/DividerEditor.svelte +0 -94
  85. 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
- out[varName] = ref.kind === 'token' ? `var(${ref.name})` : ref.value;
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
- return a.kind === 'token'
243
- ? a.name === (b as { kind: 'token'; name: string }).name
244
- : a.value === (b as { kind: 'literal'; value: string }).value;
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
- const base = s.color.startsWith('--') ? `var(${s.color})` : s.color;
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
- const stops = formatGradientStops(t);
69
- if (t.type === 'linear') return `linear-gradient(${t.angle}deg, ${stops})`;
70
- return `radial-gradient(${stops})`;
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. Used by the editor
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, string>; config?: Record<string, unknown>; schemaVersion?: number }
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, 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;
@@ -118,7 +118,6 @@
118
118
  font-family: var(--ui-font-mono, ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace);
119
119
  font-size: 10px;
120
120
  color: rgba(255, 255, 255, 0.75);
121
- letter-spacing: 0.02em;
122
121
  white-space: nowrap;
123
122
  }
124
123
 
@@ -422,7 +422,7 @@
422
422
 
423
423
  .lt-overlay.no-transition,
424
424
  .lt-overlay.no-transition .frame-wrap {
425
- transition: none !important;
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
 
@@ -96,6 +96,7 @@
96
96
  --ui-font-size-3xl: 1.75rem; /* 28px */
97
97
  --ui-font-size-4xl: 2.25rem; /* 36px */
98
98
 
99
+ --ui-font-weight-light: 200;
99
100
  --ui-font-weight-normal: 400;
100
101
  --ui-font-weight-medium: 500;
101
102
  --ui-font-weight-semibold: 600;