@motion-proto/live-tokens 0.71.0 → 0.72.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 (65) hide show
  1. package/.claude/skills/live-tokens-check-compliance/SKILL.md +7 -1
  2. package/.claude/skills/live-tokens-create-component/SKILL.md +2 -2
  3. package/.claude/skills/live-tokens-create-component/references/contract-tests.md +99 -0
  4. package/.claude/skills/live-tokens-create-component/references/sketch-mode.md +1 -2
  5. package/.claude/skills/live-tokens-create-component/references/token-naming.md +6 -0
  6. package/CHANGELOG.md +77 -0
  7. package/README.md +1 -1
  8. package/bin/check-component.mjs +2 -11
  9. package/bin/check-page.mjs +3 -2
  10. package/bin/cli.mjs +2 -1
  11. package/bin/lib/catalogue.mjs +6 -2
  12. package/bin/lib/findings.mjs +17 -0
  13. package/bin/lib/report.mjs +1 -1
  14. package/bin/lib/tokenVocabulary.mjs +20 -0
  15. package/dist-plugin/adjust/index.cjs +33 -6
  16. package/dist-plugin/adjust/index.d.cts +2 -2
  17. package/dist-plugin/adjust/index.d.ts +2 -2
  18. package/dist-plugin/adjust/index.js +7 -6
  19. package/dist-plugin/{chunk-2YNERPXY.js → chunk-RIXO2E55.js} +1 -1
  20. package/dist-plugin/{chunk-RVE3MNKM.js → chunk-W6Y4BWFB.js} +3 -1
  21. package/dist-plugin/{chunk-GPIBU44G.js → chunk-YLCOIGQC.js} +25 -1
  22. package/dist-plugin/{dataPaths-bJTCEO4H.d.ts → dataPaths-BhWzd5cL.d.cts} +4 -0
  23. package/dist-plugin/{dataPaths-bJTCEO4H.d.cts → dataPaths-BhWzd5cL.d.ts} +4 -0
  24. package/dist-plugin/fontPairing/index.cjs +3 -1
  25. package/dist-plugin/fontPairing/index.d.cts +1 -1
  26. package/dist-plugin/fontPairing/index.d.ts +1 -1
  27. package/dist-plugin/fontPairing/index.js +1 -1
  28. package/dist-plugin/generateColorsAndType/index.cjs +28 -2
  29. package/dist-plugin/generateColorsAndType/index.d.cts +1 -1
  30. package/dist-plugin/generateColorsAndType/index.d.ts +1 -1
  31. package/dist-plugin/generateColorsAndType/index.js +2 -2
  32. package/dist-plugin/index.cjs +28 -2
  33. package/dist-plugin/index.d.cts +1 -1
  34. package/dist-plugin/index.d.ts +1 -1
  35. package/dist-plugin/index.js +3 -3
  36. package/dist-plugin/migrateData/index.cjs +28 -2
  37. package/dist-plugin/migrateData/index.d.cts +1 -1
  38. package/dist-plugin/migrateData/index.d.ts +1 -1
  39. package/dist-plugin/migrateData/index.js +3 -3
  40. package/dist-plugin/tokensCssMigrations/index.cjs +3 -1
  41. package/dist-plugin/tokensCssMigrations/index.d.cts +1 -1
  42. package/dist-plugin/tokensCssMigrations/index.d.ts +1 -1
  43. package/dist-plugin/tokensCssMigrations/index.js +1 -1
  44. package/package.json +5 -1
  45. package/src/editor/component-editor/SectionDividerEditor.svelte +1 -11
  46. package/src/editor/component-editor/contract.ts +175 -0
  47. package/src/editor/component-editor/scaffolding/StateBlock.svelte +0 -8
  48. package/src/editor/component-editor/scaffolding/TokenLayout.svelte +5 -0
  49. package/src/editor/component-editor/scaffolding/TypeEditor.svelte +0 -21
  50. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +2 -2
  51. package/src/editor/component-editor/scaffolding/types.ts +1 -7
  52. package/src/editor/core/components/aliasKinds.ts +6 -2
  53. package/src/editor/core/themes/migrations/2026-09-02-sectiondivider-drop-title-outline.ts +39 -0
  54. package/src/editor/core/themes/migrations/index.ts +2 -0
  55. package/src/live-tokens/data/themes/autumn.json +1 -7
  56. package/src/live-tokens/data/themes/halloween.json +1 -7
  57. package/src/live-tokens/data/themes/midnight-study.json +1 -7
  58. package/src/live-tokens/data/themes/ocean.json +1 -7
  59. package/src/live-tokens/data/themes/royal-velvet.json +1 -7
  60. package/src/live-tokens/data/themes/sketchy.json +1 -7
  61. package/src/live-tokens/data/themes/spring-meadow.json +1 -7
  62. package/src/live-tokens/data/themes/sunset.json +1 -7
  63. package/src/system/components/SectionDivider.svelte +3 -143
  64. package/src/system/components/FloatingTokenTags.css +0 -284
  65. package/src/system/components/FloatingTokenTags.svelte +0 -570
@@ -382,7 +382,9 @@ var KNOWN_CONFIG_KEYS = /* @__PURE__ */ new Set([
382
382
  "componentConfigsDir",
383
383
  "themesDir",
384
384
  "manifestsDir",
385
- "tokensCssPath"
385
+ "tokensCssPath",
386
+ "componentDirs",
387
+ "checks"
386
388
  ]);
387
389
  var cached = null;
388
390
  function readLiveTokensConfig() {
@@ -1,5 +1,5 @@
1
1
  import { c as FontStackVariable, C as ColorsAndType, b as FontStack, F as FontSource } from '../themeTypes-DSV3Zisf.cjs';
2
- export { r as readLiveTokensConfig, a as resolveDataDirs } from '../dataPaths-bJTCEO4H.cjs';
2
+ export { r as readLiveTokensConfig, a as resolveDataDirs } from '../dataPaths-BhWzd5cL.cjs';
3
3
 
4
4
  type PairingSlot = 'display' | 'body' | 'serif' | 'mono' | 'editorial';
5
5
  interface PairingFace {
@@ -1,5 +1,5 @@
1
1
  import { c as FontStackVariable, C as ColorsAndType, b as FontStack, F as FontSource } from '../themeTypes-DSV3Zisf.js';
2
- export { r as readLiveTokensConfig, a as resolveDataDirs } from '../dataPaths-bJTCEO4H.js';
2
+ export { r as readLiveTokensConfig, a as resolveDataDirs } from '../dataPaths-BhWzd5cL.js';
3
3
 
4
4
  type PairingSlot = 'display' | 'body' | 'serif' | 'mono' | 'editorial';
5
5
  interface PairingFace {
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  readLiveTokensConfig,
3
3
  resolveDataDirs
4
- } from "../chunk-RVE3MNKM.js";
4
+ } from "../chunk-W6Y4BWFB.js";
5
5
 
6
6
  // src/editor/core/fonts/fontParse.ts
7
7
  function parseGoogleFontsUrl(url2) {
@@ -1651,6 +1651,29 @@ var componentMigration_2026_09_01_gateSuffixEnabled = {
1651
1651
  }
1652
1652
  };
1653
1653
 
1654
+ // src/editor/core/themes/migrations/2026-09-02-sectiondivider-drop-title-outline.ts
1655
+ var DROPPED2 = new Set(
1656
+ ["lg", "md", "sm"].flatMap((v) => [
1657
+ `--sectiondivider-${v}-title-outline-width`,
1658
+ `--sectiondivider-${v}-title-outline-color`
1659
+ ])
1660
+ );
1661
+ var componentMigration_2026_09_02_sectiondividerDropTitleOutline = {
1662
+ id: "2026-09-02-sectiondivider-drop-title-outline",
1663
+ fromVersion: 25,
1664
+ toVersion: 26,
1665
+ appliesTo: "component-config",
1666
+ apply(rawVars, meta) {
1667
+ if (meta.component !== "sectiondivider") return { ...rawVars };
1668
+ const out = {};
1669
+ for (const [key, value] of Object.entries(rawVars)) {
1670
+ if (DROPPED2.has(key)) continue;
1671
+ out[key] = value;
1672
+ }
1673
+ return out;
1674
+ }
1675
+ };
1676
+
1654
1677
  // src/editor/core/themes/migrations/index.ts
1655
1678
  var MIGRATIONS = [
1656
1679
  colorsAndTypeMigration_2026_04_24_legacyKeysAndBgToCanvas,
@@ -1684,7 +1707,8 @@ var MIGRATIONS = [
1684
1707
  colorsAndTypeMigration_2026_09_01_tintRename,
1685
1708
  componentMigration_2026_09_01_tintRename,
1686
1709
  componentMigration_2026_09_01_tabbarActiveTint,
1687
- componentMigration_2026_09_01_gateSuffixEnabled
1710
+ componentMigration_2026_09_01_gateSuffixEnabled,
1711
+ componentMigration_2026_09_02_sectiondividerDropTitleOutline
1688
1712
  ];
1689
1713
  function countFor(kind) {
1690
1714
  return MIGRATIONS.filter((m) => m.appliesTo === kind).length;
@@ -1999,7 +2023,9 @@ var KNOWN_CONFIG_KEYS = /* @__PURE__ */ new Set([
1999
2023
  "componentConfigsDir",
2000
2024
  "themesDir",
2001
2025
  "manifestsDir",
2002
- "tokensCssPath"
2026
+ "tokensCssPath",
2027
+ "componentDirs",
2028
+ "checks"
2003
2029
  ]);
2004
2030
  var cached = null;
2005
2031
  function readLiveTokensConfig() {
@@ -1,5 +1,5 @@
1
1
  import { F as FontSource, b as FontStack, G as GradientDiskToken, O as Oklch, H as HarmonyMode, C as ColorsAndType } from '../themeTypes-DSV3Zisf.cjs';
2
- export { r as readLiveTokensConfig, a as resolveDataDirs } from '../dataPaths-bJTCEO4H.cjs';
2
+ export { r as readLiveTokensConfig, a as resolveDataDirs } from '../dataPaths-BhWzd5cL.cjs';
3
3
  export { C as CURRENT_COMPONENT_SCHEMA_VERSION } from '../index-4N-Orzzi.cjs';
4
4
 
5
5
  /**
@@ -1,5 +1,5 @@
1
1
  import { F as FontSource, b as FontStack, G as GradientDiskToken, O as Oklch, H as HarmonyMode, C as ColorsAndType } from '../themeTypes-DSV3Zisf.js';
2
- export { r as readLiveTokensConfig, a as resolveDataDirs } from '../dataPaths-bJTCEO4H.js';
2
+ export { r as readLiveTokensConfig, a as resolveDataDirs } from '../dataPaths-BhWzd5cL.js';
3
3
  export { C as CURRENT_COMPONENT_SCHEMA_VERSION } from '../index-4N-Orzzi.js';
4
4
 
5
5
  /**
@@ -19,7 +19,7 @@ import {
19
19
  import {
20
20
  CURRENT_COLORS_AND_TYPE_SCHEMA_VERSION,
21
21
  CURRENT_COMPONENT_SCHEMA_VERSION
22
- } from "../chunk-GPIBU44G.js";
22
+ } from "../chunk-YLCOIGQC.js";
23
23
  import {
24
24
  cssColorToOklch,
25
25
  gamutClamp,
@@ -30,7 +30,7 @@ import {
30
30
  import {
31
31
  readLiveTokensConfig,
32
32
  resolveDataDirs
33
- } from "../chunk-RVE3MNKM.js";
33
+ } from "../chunk-W6Y4BWFB.js";
34
34
 
35
35
  // src/editor/core/store/editorCore.ts
36
36
  import { writable, derived, get } from "svelte/store";
@@ -1587,6 +1587,29 @@ var componentMigration_2026_09_01_gateSuffixEnabled = {
1587
1587
  }
1588
1588
  };
1589
1589
 
1590
+ // src/editor/core/themes/migrations/2026-09-02-sectiondivider-drop-title-outline.ts
1591
+ var DROPPED2 = new Set(
1592
+ ["lg", "md", "sm"].flatMap((v) => [
1593
+ `--sectiondivider-${v}-title-outline-width`,
1594
+ `--sectiondivider-${v}-title-outline-color`
1595
+ ])
1596
+ );
1597
+ var componentMigration_2026_09_02_sectiondividerDropTitleOutline = {
1598
+ id: "2026-09-02-sectiondivider-drop-title-outline",
1599
+ fromVersion: 25,
1600
+ toVersion: 26,
1601
+ appliesTo: "component-config",
1602
+ apply(rawVars, meta) {
1603
+ if (meta.component !== "sectiondivider") return { ...rawVars };
1604
+ const out = {};
1605
+ for (const [key, value] of Object.entries(rawVars)) {
1606
+ if (DROPPED2.has(key)) continue;
1607
+ out[key] = value;
1608
+ }
1609
+ return out;
1610
+ }
1611
+ };
1612
+
1590
1613
  // src/editor/core/themes/migrations/index.ts
1591
1614
  var MIGRATIONS = [
1592
1615
  colorsAndTypeMigration_2026_04_24_legacyKeysAndBgToCanvas,
@@ -1620,7 +1643,8 @@ var MIGRATIONS = [
1620
1643
  colorsAndTypeMigration_2026_09_01_tintRename,
1621
1644
  componentMigration_2026_09_01_tintRename,
1622
1645
  componentMigration_2026_09_01_tabbarActiveTint,
1623
- componentMigration_2026_09_01_gateSuffixEnabled
1646
+ componentMigration_2026_09_01_gateSuffixEnabled,
1647
+ componentMigration_2026_09_02_sectiondividerDropTitleOutline
1624
1648
  ];
1625
1649
  function countFor(kind) {
1626
1650
  return MIGRATIONS.filter((m) => m.appliesTo === kind).length;
@@ -2601,7 +2625,9 @@ var KNOWN_CONFIG_KEYS = /* @__PURE__ */ new Set([
2601
2625
  "componentConfigsDir",
2602
2626
  "themesDir",
2603
2627
  "manifestsDir",
2604
- "tokensCssPath"
2628
+ "tokensCssPath",
2629
+ "componentDirs",
2630
+ "checks"
2605
2631
  ]);
2606
2632
  var cached = null;
2607
2633
  function readLiveTokensConfig() {
@@ -1,6 +1,6 @@
1
1
  import { Plugin } from 'vite';
2
2
  export { ComponentSource, MissingToken, RunResult, TOKENS_CSS_MIGRATIONS, TokensCssMigration, ValidateInput, runTokensCssMigrations, validateTokensCss } from './tokensCssMigrations/index.cjs';
3
- import './dataPaths-bJTCEO4H.cjs';
3
+ import './dataPaths-BhWzd5cL.cjs';
4
4
 
5
5
  interface ThemeFileApiOptions {
6
6
  tokensCssPath: string;
@@ -1,6 +1,6 @@
1
1
  import { Plugin } from 'vite';
2
2
  export { ComponentSource, MissingToken, RunResult, TOKENS_CSS_MIGRATIONS, TokensCssMigration, ValidateInput, runTokensCssMigrations, validateTokensCss } from './tokensCssMigrations/index.js';
3
- import './dataPaths-bJTCEO4H.js';
3
+ import './dataPaths-BhWzd5cL.js';
4
4
 
5
5
  interface ThemeFileApiOptions {
6
6
  tokensCssPath: string;
@@ -13,10 +13,10 @@ import {
13
13
  nextAvailableName,
14
14
  normalizeTheme,
15
15
  versionedFileResourceServer
16
- } from "./chunk-2YNERPXY.js";
16
+ } from "./chunk-RIXO2E55.js";
17
17
  import {
18
18
  CURRENT_COMPONENT_SCHEMA_VERSION
19
- } from "./chunk-GPIBU44G.js";
19
+ } from "./chunk-YLCOIGQC.js";
20
20
  import {
21
21
  TOKENS_CSS_MIGRATIONS,
22
22
  extractGlobalRootBody,
@@ -31,7 +31,7 @@ import {
31
31
  } from "./chunk-7TQQTHI6.js";
32
32
  import {
33
33
  resolveDataDirs
34
- } from "./chunk-RVE3MNKM.js";
34
+ } from "./chunk-W6Y4BWFB.js";
35
35
 
36
36
  // vite-plugin/themeFileApi.ts
37
37
  import fs from "fs";
@@ -976,6 +976,29 @@ var componentMigration_2026_09_01_gateSuffixEnabled = {
976
976
  }
977
977
  };
978
978
 
979
+ // src/editor/core/themes/migrations/2026-09-02-sectiondivider-drop-title-outline.ts
980
+ var DROPPED2 = new Set(
981
+ ["lg", "md", "sm"].flatMap((v) => [
982
+ `--sectiondivider-${v}-title-outline-width`,
983
+ `--sectiondivider-${v}-title-outline-color`
984
+ ])
985
+ );
986
+ var componentMigration_2026_09_02_sectiondividerDropTitleOutline = {
987
+ id: "2026-09-02-sectiondivider-drop-title-outline",
988
+ fromVersion: 25,
989
+ toVersion: 26,
990
+ appliesTo: "component-config",
991
+ apply(rawVars, meta) {
992
+ if (meta.component !== "sectiondivider") return { ...rawVars };
993
+ const out = {};
994
+ for (const [key, value] of Object.entries(rawVars)) {
995
+ if (DROPPED2.has(key)) continue;
996
+ out[key] = value;
997
+ }
998
+ return out;
999
+ }
1000
+ };
1001
+
979
1002
  // src/editor/core/themes/migrations/index.ts
980
1003
  var MIGRATIONS = [
981
1004
  colorsAndTypeMigration_2026_04_24_legacyKeysAndBgToCanvas,
@@ -1009,7 +1032,8 @@ var MIGRATIONS = [
1009
1032
  colorsAndTypeMigration_2026_09_01_tintRename,
1010
1033
  componentMigration_2026_09_01_tintRename,
1011
1034
  componentMigration_2026_09_01_tabbarActiveTint,
1012
- componentMigration_2026_09_01_gateSuffixEnabled
1035
+ componentMigration_2026_09_01_gateSuffixEnabled,
1036
+ componentMigration_2026_09_02_sectiondividerDropTitleOutline
1013
1037
  ];
1014
1038
  function countFor(kind) {
1015
1039
  return MIGRATIONS.filter((m) => m.appliesTo === kind).length;
@@ -2178,7 +2202,9 @@ var KNOWN_CONFIG_KEYS = /* @__PURE__ */ new Set([
2178
2202
  "componentConfigsDir",
2179
2203
  "themesDir",
2180
2204
  "manifestsDir",
2181
- "tokensCssPath"
2205
+ "tokensCssPath",
2206
+ "componentDirs",
2207
+ "checks"
2182
2208
  ]);
2183
2209
  var cached = null;
2184
2210
  function readLiveTokensConfig() {
@@ -1,4 +1,4 @@
1
- export { r as readLiveTokensConfig, a as resolveDataDirs } from '../dataPaths-bJTCEO4H.cjs';
1
+ export { r as readLiveTokensConfig, a as resolveDataDirs } from '../dataPaths-BhWzd5cL.cjs';
2
2
 
3
3
  interface LegacyRename {
4
4
  from: string;
@@ -1,4 +1,4 @@
1
- export { r as readLiveTokensConfig, a as resolveDataDirs } from '../dataPaths-bJTCEO4H.js';
1
+ export { r as readLiveTokensConfig, a as resolveDataDirs } from '../dataPaths-BhWzd5cL.js';
2
2
 
3
3
  interface LegacyRename {
4
4
  from: string;
@@ -7,14 +7,14 @@ import {
7
7
  normalizeTheme,
8
8
  planLegacyRenames,
9
9
  versionedFileResourceServer
10
- } from "../chunk-2YNERPXY.js";
10
+ } from "../chunk-RIXO2E55.js";
11
11
  import {
12
12
  CURRENT_COMPONENT_SCHEMA_VERSION
13
- } from "../chunk-GPIBU44G.js";
13
+ } from "../chunk-YLCOIGQC.js";
14
14
  import {
15
15
  readLiveTokensConfig,
16
16
  resolveDataDirs
17
- } from "../chunk-RVE3MNKM.js";
17
+ } from "../chunk-W6Y4BWFB.js";
18
18
 
19
19
  // vite-plugin/migrateData/migrateData.ts
20
20
  import fs from "fs";
@@ -648,7 +648,9 @@ var KNOWN_CONFIG_KEYS = /* @__PURE__ */ new Set([
648
648
  "componentConfigsDir",
649
649
  "themesDir",
650
650
  "manifestsDir",
651
- "tokensCssPath"
651
+ "tokensCssPath",
652
+ "componentDirs",
653
+ "checks"
652
654
  ]);
653
655
  var cached = null;
654
656
  function readLiveTokensConfig() {
@@ -1,4 +1,4 @@
1
- export { r as readLiveTokensConfig } from '../dataPaths-bJTCEO4H.cjs';
1
+ export { r as readLiveTokensConfig } from '../dataPaths-BhWzd5cL.cjs';
2
2
 
3
3
  /**
4
4
  * A `tokens-css` migration: a pure, idempotent transform on the text of a
@@ -1,4 +1,4 @@
1
- export { r as readLiveTokensConfig } from '../dataPaths-bJTCEO4H.js';
1
+ export { r as readLiveTokensConfig } from '../dataPaths-BhWzd5cL.js';
2
2
 
3
3
  /**
4
4
  * A `tokens-css` migration: a pure, idempotent transform on the text of a
@@ -17,7 +17,7 @@ import {
17
17
  import "../chunk-7TQQTHI6.js";
18
18
  import {
19
19
  readLiveTokensConfig
20
- } from "../chunk-RVE3MNKM.js";
20
+ } from "../chunk-W6Y4BWFB.js";
21
21
  export {
22
22
  TOKENS_CSS_MIGRATIONS,
23
23
  collectDefinedTokens,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@motion-proto/live-tokens",
3
- "version": "0.71.0",
3
+ "version": "0.72.0",
4
4
  "type": "module",
5
5
  "description": "Design token editor with live CSS variable editing. Svelte 5 + Vite 8.",
6
6
  "keywords": [
@@ -72,6 +72,10 @@
72
72
  "types": "./src/editor/component-editor/index.ts",
73
73
  "default": "./src/editor/component-editor/index.ts"
74
74
  },
75
+ "./component-editor/contract": {
76
+ "types": "./src/editor/component-editor/contract.ts",
77
+ "default": "./src/editor/component-editor/contract.ts"
78
+ },
75
79
  "./ui": {
76
80
  "svelte": "./src/editor/ui/index.ts",
77
81
  "types": "./src/editor/ui/index.ts",
@@ -36,25 +36,18 @@
36
36
  { label: 'hairline thickness', canBeLinked: true, groupKey: 'hairline-thickness', variable: `--sectiondivider-${v}-hairline-thickness`, element: 'hairline' },
37
37
  ];
38
38
  }
39
- function titleOutlineTokens(v: Variant): Token[] {
40
- return [
41
- { label: 'outline thickness', canBeLinked: true, groupKey: 'title-outline-width', variable: `--sectiondivider-${v}-title-outline-width`, element: 'title' },
42
- { label: 'outline color', canBeLinked: true, groupKey: 'title-outline-color', variable: `--sectiondivider-${v}-title-outline-color`, element: 'title' },
43
- ];
44
- }
45
39
  function backgroundTokens(v: Variant): Token[] {
46
40
  return [
47
41
  { label: 'background', groupKey: 'background', variable: `--sectiondivider-${v}-background`, kind: 'gradient', family: variants.find((x) => x.key === v)!.family },
48
42
  ];
49
43
  }
50
44
  function variantTokens(v: Variant): Token[] {
51
- return [...containerTokens(v), ...hairlineTokens(v), ...titleOutlineTokens(v), ...backgroundTokens(v), ...typePaddingTokens(v)];
45
+ return [...containerTokens(v), ...hairlineTokens(v), ...backgroundTokens(v), ...typePaddingTokens(v)];
52
46
  }
53
47
  function stateTokens(v: Variant): Token[] {
54
48
  return [
55
49
  ...containerTokens(v),
56
50
  ...hairlineTokens(v),
57
- ...titleOutlineTokens(v).map((t) => ({ ...t, hidden: true })),
58
51
  ...backgroundTokens(v).map((t) => ({ ...t, hidden: true })),
59
52
  ...typePaddingTokens(v),
60
53
  ];
@@ -72,8 +65,6 @@
72
65
  weightVariable: `--sectiondivider-${v}-title-font-weight`,
73
66
  lineHeightVariable: `--sectiondivider-${v}-title-line-height`,
74
67
  letterSpacingVariable: `--sectiondivider-${v}-title-letter-spacing`,
75
- outlineWidthVariable: `--sectiondivider-${v}-title-outline-width`,
76
- outlineColorVariable: `--sectiondivider-${v}-title-outline-color`,
77
68
  },
78
69
  {
79
70
  legend: '',
@@ -127,7 +118,6 @@
127
118
  const LINKED_GROUP_KEYS = [
128
119
  'container-padding', 'radius', 'border', 'border-width', 'shadow',
129
120
  'hairline-color', 'hairline-thickness',
130
- 'title-outline-width', 'title-outline-color',
131
121
  'title-color', 'description-color', 'eyebrow-color',
132
122
  'title-padding', 'description-padding', 'eyebrow-padding',
133
123
  'title-font-family', 'title-font-weight', 'title-line-height', 'title-letter-spacing',
@@ -0,0 +1,175 @@
1
+ /**
2
+ * The registry contract, as a function a project outside this package can run.
3
+ *
4
+ * Node-only: it reads the runtime `.svelte` file and the component's
5
+ * `default.json` off disk, so it lives behind its own subpath export
6
+ * (`@motion-proto/live-tokens/component-editor/contract`) rather than in the
7
+ * browser barrel. A consumer's whole suite is a `describe.each` over their own
8
+ * registrations and one call:
9
+ *
10
+ * ```ts
11
+ * // @vitest-environment happy-dom
12
+ * import { describe, it, expect } from 'vitest';
13
+ * import { getComponentRegistryEntries, registerComponent } from '@motion-proto/live-tokens';
14
+ * import { checkRegistryEntry } from '@motion-proto/live-tokens/component-editor/contract';
15
+ * import MyWidgetEditor, { allTokens } from '../src/system/components/MyWidgetEditor.svelte';
16
+ *
17
+ * registerComponent({ id: 'mywidget', label: 'My Widget', icon: 'fas fa-magic',
18
+ * sourceFile: 'src/system/components/MyWidget.svelte',
19
+ * editorComponent: MyWidgetEditor, schema: allTokens });
20
+ *
21
+ * const mine = getComponentRegistryEntries().filter((e) => e.origin === 'custom');
22
+ *
23
+ * describe.each(mine.map((e) => [e.id, e] as const))('%s', (_id, entry) => {
24
+ * it('meets the registry contract', () => {
25
+ * expect(checkRegistryEntry(entry)).toEqual([]);
26
+ * });
27
+ * });
28
+ * ```
29
+ *
30
+ * The `origin` filter is load-bearing outside this package: the registry always
31
+ * carries the shipped components too, and their `sourceFile` paths are relative
32
+ * to the package root, not the consumer's.
33
+ */
34
+ import { readFileSync, existsSync } from 'node:fs';
35
+ import path from 'node:path';
36
+ import { get } from 'svelte/store';
37
+ import type { RegistryEntry } from './registry';
38
+ import type { Token } from './scaffolding/types';
39
+ import { parseColorOpacity } from '../core/themes/parsers/colorOpacity';
40
+ import { editorState, setComponentAlias, clearComponentAlias } from '../core/store/editorStore';
41
+
42
+ export interface RegistryContractOptions {
43
+ /** Root that `entry.sourceFile` is relative to. Defaults to `process.cwd()`,
44
+ which is the project root under both vitest and `npm test`. */
45
+ projectRoot?: string;
46
+ /** Directory holding `<id>/default.json`. Defaults to
47
+ `<projectRoot>/src/live-tokens/data/component-configs`. Pass the
48
+ `componentConfigsDir` from `live-tokens.config.json` when a project moved it. */
49
+ componentConfigsDir?: string;
50
+ }
51
+
52
+ const PADDING_SIDE_RE = /-padding-(top|right|bottom|left)$/;
53
+ const PROBE = { kind: 'token', name: '--space-8' } as const;
54
+
55
+ /**
56
+ * Tokens excluded from the runtime-declaration and default-seed checks:
57
+ * - `hidden: true` (vestigial or hidden by design),
58
+ * - `kind: 'gradient'` (structured payloads, stored as gradient objects, not vars),
59
+ * - `-padding-(top|right|bottom|left)` (written on demand by the split-padding
60
+ * UI and consumed through the `themed-padding` mixin's fallback chain, so
61
+ * they exist as neither `:root` declarations nor default seeds).
62
+ */
63
+ function isEditableSurfaceToken(t: Token): boolean {
64
+ if (t.hidden) return false;
65
+ if (t.kind === 'gradient') return false;
66
+ if (PADDING_SIDE_RE.test(t.variable)) return false;
67
+ return true;
68
+ }
69
+
70
+ /** Every `--xxx:` declaration in every `<style>` block of a Svelte file. */
71
+ function extractRuntimeDeclarations(source: string): Set<string> {
72
+ const out = new Set<string>();
73
+ const blocks = [...source.matchAll(/<style[^>]*>([\s\S]*?)<\/style>/g)].map((m) => m[1]);
74
+ for (const block of blocks) {
75
+ for (const m of block.matchAll(/(?:^|[{;\n])\s*(--[a-z0-9-]+)\s*:/g)) {
76
+ out.add(m[1]);
77
+ }
78
+ }
79
+ return out;
80
+ }
81
+
82
+ /** Opacity a seeded alias resolves to. A plain token alias is fully opaque; a
83
+ gradient payload or `transparent` carries no guarantee, so it reads as 0. */
84
+ function seededOpacity(raw: unknown): number {
85
+ if (typeof raw !== 'string' || raw === 'transparent') return 0;
86
+ return parseColorOpacity(raw)?.opacity ?? 100;
87
+ }
88
+
89
+ /**
90
+ * Run the registry contract against one entry and return a violation line per
91
+ * failure. An empty array is the pass.
92
+ *
93
+ * The contract:
94
+ * 1. Registration — `sourceFile` resolves to a real file, the schema is non-empty.
95
+ * 2. Uniqueness — no schema variable is declared twice.
96
+ * 3. Editor ↔ runtime — every editable token's CSS var is declared in the
97
+ * runtime's `<style>` block, so an edit has something to repaint.
98
+ * 4. Editor ↔ default config — every editable token has a seed alias in
99
+ * `<id>/default.json`, so a consumer adopts with full defaults. A component
100
+ * with no `default.json` is editor-only; the seed and floor checks skip it.
101
+ * 5. Opacity floors — a token declaring `minOpacity` ships a default at or
102
+ * above it, so a floating panel starts out legible over page content.
103
+ * 6. Round-trip — `setComponentAlias` persists into the slice under the same key.
104
+ */
105
+ export function checkRegistryEntry(
106
+ entry: RegistryEntry,
107
+ options: RegistryContractOptions = {},
108
+ ): string[] {
109
+ const projectRoot = options.projectRoot ?? process.cwd();
110
+ const configsDir =
111
+ options.componentConfigsDir ??
112
+ path.resolve(projectRoot, 'src/live-tokens/data/component-configs');
113
+
114
+ const violations: string[] = [];
115
+ const schema = entry.schema ?? [];
116
+ const runtimePath = path.resolve(projectRoot, entry.sourceFile);
117
+
118
+ if (schema.length === 0) violations.push('registration: schema is empty');
119
+ if (!existsSync(runtimePath)) {
120
+ violations.push(`registration: sourceFile does not resolve to a file (${runtimePath})`);
121
+ }
122
+
123
+ const seen = new Set<string>();
124
+ for (const t of schema) {
125
+ if (seen.has(t.variable)) violations.push(`uniqueness: ${t.variable} is declared twice`);
126
+ seen.add(t.variable);
127
+ }
128
+
129
+ const editable = schema.filter(isEditableSurfaceToken);
130
+
131
+ if (existsSync(runtimePath)) {
132
+ const runtimeVars = extractRuntimeDeclarations(readFileSync(runtimePath, 'utf-8'));
133
+ for (const t of editable) {
134
+ if (!runtimeVars.has(t.variable)) {
135
+ violations.push(`runtime: ${t.variable} is not declared in ${entry.sourceFile}`);
136
+ }
137
+ }
138
+ }
139
+
140
+ const configPath = path.join(configsDir, entry.id, 'default.json');
141
+ if (existsSync(configPath)) {
142
+ const config = JSON.parse(readFileSync(configPath, 'utf-8')) as {
143
+ aliases?: Record<string, unknown>;
144
+ };
145
+ const aliases = config.aliases ?? {};
146
+ for (const t of editable) {
147
+ if (!(t.variable in aliases)) {
148
+ violations.push(`default config: ${t.variable} has no seed in ${entry.id}/default.json`);
149
+ }
150
+ }
151
+ for (const t of schema) {
152
+ if (t.minOpacity === undefined) continue;
153
+ const opacity = seededOpacity(aliases[t.variable]);
154
+ if (opacity < t.minOpacity) {
155
+ violations.push(
156
+ `opacity floor: ${t.variable} seeds at ${opacity}%, below its ${t.minOpacity}% floor`,
157
+ );
158
+ }
159
+ }
160
+ }
161
+
162
+ const sample = editable[0];
163
+ if (sample) {
164
+ setComponentAlias(entry.id, sample.variable, PROBE);
165
+ const stored = get(editorState).components[entry.id]?.aliases?.[sample.variable];
166
+ if (JSON.stringify(stored) !== JSON.stringify(PROBE)) {
167
+ violations.push(
168
+ `round-trip: setComponentAlias(${entry.id}, ${sample.variable}) stored ${JSON.stringify(stored)}`,
169
+ );
170
+ }
171
+ clearComponentAlias(entry.id, sample.variable);
172
+ }
173
+
174
+ return violations;
175
+ }
@@ -140,10 +140,6 @@
140
140
  lineHeightLabel={tg.lineHeightLabel ?? 'line height'}
141
141
  letterSpacingVariable={tg.letterSpacingVariable}
142
142
  letterSpacingLabel={tg.letterSpacingLabel ?? 'letter spacing'}
143
- outlineWidthVariable={tg.outlineWidthVariable}
144
- outlineWidthLabel={tg.outlineWidthLabel ?? 'outline thickness'}
145
- outlineColorVariable={tg.outlineColorVariable}
146
- outlineColorLabel={tg.outlineColorLabel ?? 'outline color'}
147
143
  {component}
148
144
  {onchange}
149
145
  />
@@ -183,10 +179,6 @@
183
179
  lineHeightLabel={tg.lineHeightLabel ?? 'line height'}
184
180
  letterSpacingVariable={tg.letterSpacingVariable}
185
181
  letterSpacingLabel={tg.letterSpacingLabel ?? 'letter spacing'}
186
- outlineWidthVariable={tg.outlineWidthVariable}
187
- outlineWidthLabel={tg.outlineWidthLabel ?? 'outline thickness'}
188
- outlineColorVariable={tg.outlineColorVariable}
189
- outlineColorLabel={tg.outlineColorLabel ?? 'outline color'}
190
182
  {component}
191
183
  {onchange}
192
184
  />
@@ -72,6 +72,7 @@
72
72
  'divider-height',
73
73
  'divider-inset',
74
74
  'dot-size',
75
+ 'length',
75
76
  'radius',
76
77
  'padding',
77
78
  'padding-split',
@@ -150,6 +151,9 @@
150
151
  extra: () => ({ mode: 'sides' }),
151
152
  },
152
153
  'gap': { component: UIPaddingSelector, extra: () => ({ mode: 'single', splittable: false }) },
154
+ /* A bare width/height/size is a length off the same `--space-*` scale gap
155
+ reads from, so it takes gap's picker rather than a scale of its own. */
156
+ 'length': { component: UIPaddingSelector, extra: () => ({ mode: 'single', splittable: false }) },
153
157
  'duration': { component: UIVariantSelector, extra: () => ({ ...DURATION }) },
154
158
  'easing': { component: UIEasingSelector },
155
159
  'blur': { component: UIVariantSelector, extra: () => ({ ...BLUR }) },
@@ -176,6 +180,7 @@
176
180
  'divider-height',
177
181
  'divider-inset',
178
182
  'dot-size',
183
+ 'length',
179
184
  'radius',
180
185
  'padding',
181
186
  'padding-split',