@motion-proto/live-tokens 0.70.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 (66) hide show
  1. package/.claude/skills/live-tokens-check-compliance/SKILL.md +78 -0
  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/.claude/skills/live-tokens-fix-findings/SKILL.md +5 -2
  7. package/CHANGELOG.md +100 -0
  8. package/README.md +9 -2
  9. package/bin/check-component.mjs +2 -11
  10. package/bin/check-page.mjs +4 -3
  11. package/bin/cli.mjs +29 -1
  12. package/bin/lib/catalogue.mjs +6 -2
  13. package/bin/lib/findings.mjs +17 -0
  14. package/bin/lib/report.mjs +158 -0
  15. package/bin/lib/tokenVocabulary.mjs +20 -0
  16. package/dist-plugin/adjust/index.cjs +33 -6
  17. package/dist-plugin/adjust/index.d.cts +2 -2
  18. package/dist-plugin/adjust/index.d.ts +2 -2
  19. package/dist-plugin/adjust/index.js +7 -6
  20. package/dist-plugin/{chunk-2YNERPXY.js → chunk-RIXO2E55.js} +1 -1
  21. package/dist-plugin/{chunk-RVE3MNKM.js → chunk-W6Y4BWFB.js} +3 -1
  22. package/dist-plugin/{chunk-GPIBU44G.js → chunk-YLCOIGQC.js} +25 -1
  23. package/dist-plugin/{dataPaths-bJTCEO4H.d.ts → dataPaths-BhWzd5cL.d.cts} +4 -0
  24. package/dist-plugin/{dataPaths-bJTCEO4H.d.cts → dataPaths-BhWzd5cL.d.ts} +4 -0
  25. package/dist-plugin/fontPairing/index.cjs +3 -1
  26. package/dist-plugin/fontPairing/index.d.cts +1 -1
  27. package/dist-plugin/fontPairing/index.d.ts +1 -1
  28. package/dist-plugin/fontPairing/index.js +1 -1
  29. package/dist-plugin/generateColorsAndType/index.cjs +28 -2
  30. package/dist-plugin/generateColorsAndType/index.d.cts +1 -1
  31. package/dist-plugin/generateColorsAndType/index.d.ts +1 -1
  32. package/dist-plugin/generateColorsAndType/index.js +2 -2
  33. package/dist-plugin/index.cjs +28 -2
  34. package/dist-plugin/index.d.cts +1 -1
  35. package/dist-plugin/index.d.ts +1 -1
  36. package/dist-plugin/index.js +3 -3
  37. package/dist-plugin/migrateData/index.cjs +28 -2
  38. package/dist-plugin/migrateData/index.d.cts +1 -1
  39. package/dist-plugin/migrateData/index.d.ts +1 -1
  40. package/dist-plugin/migrateData/index.js +3 -3
  41. package/dist-plugin/tokensCssMigrations/index.cjs +3 -1
  42. package/dist-plugin/tokensCssMigrations/index.d.cts +1 -1
  43. package/dist-plugin/tokensCssMigrations/index.d.ts +1 -1
  44. package/dist-plugin/tokensCssMigrations/index.js +1 -1
  45. package/package.json +5 -1
  46. package/src/editor/component-editor/SectionDividerEditor.svelte +1 -11
  47. package/src/editor/component-editor/contract.ts +175 -0
  48. package/src/editor/component-editor/scaffolding/StateBlock.svelte +0 -8
  49. package/src/editor/component-editor/scaffolding/TokenLayout.svelte +5 -0
  50. package/src/editor/component-editor/scaffolding/TypeEditor.svelte +0 -21
  51. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +2 -2
  52. package/src/editor/component-editor/scaffolding/types.ts +1 -7
  53. package/src/editor/core/components/aliasKinds.ts +6 -2
  54. package/src/editor/core/themes/migrations/2026-09-02-sectiondivider-drop-title-outline.ts +39 -0
  55. package/src/editor/core/themes/migrations/index.ts +2 -0
  56. package/src/live-tokens/data/themes/autumn.json +1 -7
  57. package/src/live-tokens/data/themes/halloween.json +1 -7
  58. package/src/live-tokens/data/themes/midnight-study.json +1 -7
  59. package/src/live-tokens/data/themes/ocean.json +1 -7
  60. package/src/live-tokens/data/themes/royal-velvet.json +1 -7
  61. package/src/live-tokens/data/themes/sketchy.json +1 -7
  62. package/src/live-tokens/data/themes/spring-meadow.json +1 -7
  63. package/src/live-tokens/data/themes/sunset.json +1 -7
  64. package/src/system/components/SectionDivider.svelte +3 -143
  65. package/src/system/components/FloatingTokenTags.css +0 -284
  66. package/src/system/components/FloatingTokenTags.svelte +0 -570
@@ -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.70.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',
@@ -1,13 +1,11 @@
1
1
  <script lang="ts">
2
2
  import UIPaletteSelector from '../../ui/UIPaletteSelector.svelte';
3
- import UIVariantSelector from '../../ui/UIVariantSelector.svelte';
4
3
  import UIFontFamilySelector from '../../ui/UIFontFamilySelector.svelte';
5
4
  import UIFontSizeSelector from '../../ui/UIFontSizeSelector.svelte';
6
5
  import UIFontWeightSelector from '../../ui/UIFontWeightSelector.svelte';
7
6
  import UILineHeightSelector from '../../ui/UILineHeightSelector.svelte';
8
7
  import UILetterSpacingSelector from '../../ui/UILetterSpacingSelector.svelte';
9
8
  import FieldsetWrapper from './FieldsetWrapper.svelte';
10
- import { BORDER_WIDTH } from '../../ui/variantScales';
11
9
 
12
10
 
13
11
 
@@ -26,13 +24,6 @@
26
24
  lineHeightLabel?: string;
27
25
  letterSpacingVariable?: string | undefined;
28
26
  letterSpacingLabel?: string;
29
- /** Optional outline rows rendered under the typography rows so a text-with-
30
- stroke group keeps stroke controls visually nested with the type they
31
- drive (e.g. SectionDivider title outline). */
32
- outlineWidthVariable?: string | undefined;
33
- outlineWidthLabel?: string;
34
- outlineColorVariable?: string | undefined;
35
- outlineColorLabel?: string;
36
27
  /** When set, writes persist through the editor store under this component. */
37
28
  component?: string | undefined;
38
29
  /** Legend text for the fieldset. */
@@ -54,10 +45,6 @@
54
45
  lineHeightLabel = 'line-h',
55
46
  letterSpacingVariable = undefined,
56
47
  letterSpacingLabel = 'letter-sp',
57
- outlineWidthVariable = undefined,
58
- outlineWidthLabel = 'outline thickness',
59
- outlineColorVariable = undefined,
60
- outlineColorLabel = 'outline color',
61
48
  component = undefined,
62
49
  legend = 'type',
63
50
  onchange,
@@ -89,14 +76,6 @@
89
76
  <span class="row-label">{letterSpacingLabel}</span>
90
77
  <UILetterSpacingSelector variable={letterSpacingVariable} {component} canBeLinked {onchange} />
91
78
  {/if}
92
- {#if outlineWidthVariable}
93
- <span class="row-label">{outlineWidthLabel}</span>
94
- <UIVariantSelector variable={outlineWidthVariable} {component} canBeLinked {...BORDER_WIDTH} {onchange} />
95
- {/if}
96
- {#if outlineColorVariable}
97
- <span class="row-label">{outlineColorLabel}</span>
98
- <UIPaletteSelector variable={outlineColorVariable} {component} canBeLinked {onchange} />
99
- {/if}
100
79
  </div>
101
80
  </FieldsetWrapper>
102
81
 
@@ -105,7 +105,7 @@
105
105
 
106
106
  let activeTab: string = $state('');
107
107
 
108
- const TYPE_PROPS = ['colorVariable', 'familyVariable', 'sizeVariable', 'weightVariable', 'lineHeightVariable', 'letterSpacingVariable', 'outlineWidthVariable', 'outlineColorVariable'] as const;
108
+ const TYPE_PROPS = ['colorVariable', 'familyVariable', 'sizeVariable', 'weightVariable', 'lineHeightVariable', 'letterSpacingVariable'] as const;
109
109
  // Carry per-side derived vars so split padding fully transfers; no-op when absent.
110
110
  const PADDING_SIDES = ['top', 'right', 'bottom', 'left'] as const;
111
111
 
@@ -147,7 +147,7 @@
147
147
 
148
148
  /** TypeGroup props whose values are color tokens; preserve-color-families
149
149
  skips these in the typeGroups copy loop. */
150
- const COLOR_TYPE_PROPS = new Set(['colorVariable', 'outlineColorVariable']);
150
+ const COLOR_TYPE_PROPS = new Set(['colorVariable']);
151
151
 
152
152
  /** True iff `colorRef` is a CSS-var token whose slug contains `family`
153
153
  as a hyphen-delimited segment (e.g. `--surface-canvas-low` ∋ `canvas`). */
@@ -69,9 +69,7 @@ export type IntrinsicSpec = {
69
69
 
70
70
  /** Editor type-group: a fieldset containing a coordinated set of typography tokens
71
71
  (text color + font-family/size/weight/line-height) for a piece of content
72
- (e.g. a card title, notification body). Optional outline rows let
73
- text-with-stroke groups (e.g. SectionDivider title) keep their stroke
74
- width/color visually nested with the typography that drives them. */
72
+ (e.g. a card title, notification body). */
75
73
  export type TypeGroupConfig = {
76
74
  legend?: string;
77
75
  colorVariable: string;
@@ -89,10 +87,6 @@ export type TypeGroupConfig = {
89
87
  lineHeightLabel?: string;
90
88
  letterSpacingVariable?: string;
91
89
  letterSpacingLabel?: string;
92
- outlineWidthVariable?: string;
93
- outlineWidthLabel?: string;
94
- outlineColorVariable?: string;
95
- outlineColorLabel?: string;
96
90
  /** Explicit groupKey for this group's color token. Wins over every derivation
97
91
  (structural or `groupKeyFor`) and is never recomputed — the durable, one-line
98
92
  fix when the derived key is wrong. */
@@ -7,6 +7,7 @@ export type TokenKind =
7
7
  | 'divider-height'
8
8
  | 'divider-inset'
9
9
  | 'dot-size'
10
+ | 'length'
10
11
  | 'blur'
11
12
  | 'scale'
12
13
  | 'shadow'
@@ -60,11 +61,14 @@ export const KIND_RULES: ReadonlyArray<{
60
61
  { kind: 'border-width', suffix: ['-border-width', '-accent-width', '-hairline-thickness', '-thickness'],
61
62
  prefix: ['--border-width-'] },
62
63
  { kind: 'border', suffix: ['-border'], prefix: ['--border-'] },
64
+ // A dimension with no more specific name behind it — a panel's width, an
65
+ // avatar's size. Last of the geometry rules, so every `-border-width`,
66
+ // `-divider-height`, `-icon-size` and the rest claim their token first.
67
+ { kind: 'length', suffix: ['-width', '-height', '-size'] },
63
68
  // Fills. A tint is a wash over a surface, so it takes the surface picker: the
64
69
  // full palette with an alpha, not just the tint stops it defaults to.
65
70
  { kind: 'surface', suffix: ['-surface', '-fill', '-divider', '-background', '-indicator',
66
- '-thumb', '-accent', '-color', '-tint', '-opacity',
67
- '-width', '-height', '-size'],
71
+ '-thumb', '-accent', '-color', '-tint', '-opacity'],
68
72
  prefix: ['--surface-', '--tint', '--color-'] },
69
73
  ];
70
74
 
@@ -0,0 +1,39 @@
1
+ import type { Migration } from './index';
2
+
3
+ /**
4
+ * SectionDivider title outline, removed (2026-09-02).
5
+ *
6
+ * The outline was a decorative stroke around the title's glyphs, and the only
7
+ * reason the title was an SVG `<text>` behind a `feMorphology` filter rather
8
+ * than an element. That SVG cost a `getBBox()` viewBox, a per-instance
9
+ * MutationObserver on the document's inline style (filter primitives cannot
10
+ * read a CSS var), font-load listeners to re-measure, and a title that was not
11
+ * selectable or findable. It shipped transparent in the default and in all
12
+ * eight presets, and the trap-out it was built for is done by the layout: the
13
+ * `through-label` hairlines flank the title in a flex row, so no rule ever runs
14
+ * behind the glyphs.
15
+ *
16
+ * Dropping the keys rather than rebinding them: nothing reads the values now.
17
+ */
18
+ const DROPPED = new Set(
19
+ ['lg', 'md', 'sm'].flatMap((v) => [
20
+ `--sectiondivider-${v}-title-outline-width`,
21
+ `--sectiondivider-${v}-title-outline-color`,
22
+ ]),
23
+ );
24
+
25
+ export const componentMigration_2026_09_02_sectiondividerDropTitleOutline: Migration = {
26
+ id: '2026-09-02-sectiondivider-drop-title-outline',
27
+ fromVersion: 25,
28
+ toVersion: 26,
29
+ appliesTo: 'component-config',
30
+ apply(rawVars, meta) {
31
+ if (meta.component !== 'sectiondivider') return { ...rawVars };
32
+ const out: Record<string, string> = {};
33
+ for (const [key, value] of Object.entries(rawVars)) {
34
+ if (DROPPED.has(key)) continue;
35
+ out[key] = value;
36
+ }
37
+ return out;
38
+ },
39
+ };
@@ -72,6 +72,7 @@ import {
72
72
  } from './2026-09-01-tint-rename';
73
73
  import { componentMigration_2026_09_01_tabbarActiveTint } from './2026-09-01-tabbar-active-tint';
74
74
  import { componentMigration_2026_09_01_gateSuffixEnabled } from './2026-09-01-gate-suffix-enabled';
75
+ import { componentMigration_2026_09_02_sectiondividerDropTitleOutline } from './2026-09-02-sectiondivider-drop-title-outline';
75
76
 
76
77
  /**
77
78
  * Registered migrations. Order in this array does not matter — the runner
@@ -110,6 +111,7 @@ export const MIGRATIONS: Migration[] = [
110
111
  componentMigration_2026_09_01_tintRename,
111
112
  componentMigration_2026_09_01_tabbarActiveTint,
112
113
  componentMigration_2026_09_01_gateSuffixEnabled,
114
+ componentMigration_2026_09_02_sectiondividerDropTitleOutline,
113
115
  ];
114
116
 
115
117
  function countFor(kind: 'colors-and-type' | 'component-config'): number {
@@ -3515,7 +3515,6 @@
3515
3515
  "--sectiondivider-lg-title-font-size": "--font-size-5xl",
3516
3516
  "--sectiondivider-lg-title-line-height": "--line-height-normal",
3517
3517
  "--sectiondivider-lg-title-letter-spacing": "--letter-spacing-normal",
3518
- "--sectiondivider-lg-title-outline-width": "--border-width-4",
3519
3518
  "--sectiondivider-lg-description-font-family": "--font-sans",
3520
3519
  "--sectiondivider-lg-description-font-weight": "--font-weight-medium",
3521
3520
  "--sectiondivider-lg-description-font-size": "--font-size-lg",
@@ -3537,14 +3536,12 @@
3537
3536
  "--sectiondivider-lg-description": "--text-alternate",
3538
3537
  "--sectiondivider-lg-eyebrow": "--text-brand",
3539
3538
  "--sectiondivider-lg-border": "--color-transparent",
3540
- "--sectiondivider-lg-title-outline-color": "transparent",
3541
3539
  "--sectiondivider-lg-hairline-color": "--border-brand-medium",
3542
3540
  "--sectiondivider-md-title-font-family": "--font-display",
3543
3541
  "--sectiondivider-md-title-font-weight": "--font-weight-bold",
3544
3542
  "--sectiondivider-md-title-font-size": "--font-size-4xl",
3545
3543
  "--sectiondivider-md-title-line-height": "--line-height-normal",
3546
3544
  "--sectiondivider-md-title-letter-spacing": "--letter-spacing-normal",
3547
- "--sectiondivider-md-title-outline-width": "--border-width-4",
3548
3545
  "--sectiondivider-md-description-font-family": "--font-sans",
3549
3546
  "--sectiondivider-md-description-font-weight": "--font-weight-medium",
3550
3547
  "--sectiondivider-md-description-font-size": "--font-size-lg",
@@ -3566,14 +3563,12 @@
3566
3563
  "--sectiondivider-md-description": "--text-secondary",
3567
3564
  "--sectiondivider-md-eyebrow": "--text-tertiary",
3568
3565
  "--sectiondivider-md-border": "--color-transparent",
3569
- "--sectiondivider-md-title-outline-color": "transparent",
3570
3566
  "--sectiondivider-md-hairline-color": "--border-brand-medium",
3571
3567
  "--sectiondivider-sm-title-font-family": "--font-display",
3572
3568
  "--sectiondivider-sm-title-font-weight": "--font-weight-medium",
3573
3569
  "--sectiondivider-sm-title-font-size": "--font-size-3xl",
3574
3570
  "--sectiondivider-sm-title-line-height": "--line-height-normal",
3575
3571
  "--sectiondivider-sm-title-letter-spacing": "--letter-spacing-normal",
3576
- "--sectiondivider-sm-title-outline-width": "--border-width-4",
3577
3572
  "--sectiondivider-sm-description-font-family": "--font-sans",
3578
3573
  "--sectiondivider-sm-description-font-weight": "--font-weight-medium",
3579
3574
  "--sectiondivider-sm-description-font-size": "--font-size-5xl",
@@ -3595,7 +3590,6 @@
3595
3590
  "--sectiondivider-sm-description": "--text-secondary",
3596
3591
  "--sectiondivider-sm-eyebrow": "--text-tertiary",
3597
3592
  "--sectiondivider-sm-border": "--color-transparent",
3598
- "--sectiondivider-sm-title-outline-color": "transparent",
3599
3593
  "--sectiondivider-sm-hairline-color": "color-mix(in srgb, var(--border-brand-medium) 50%, transparent)"
3600
3594
  }
3601
3595
  },
@@ -4036,5 +4030,5 @@
4036
4030
  }
4037
4031
  }
4038
4032
  },
4039
- "componentSchemaVersion": 25
4033
+ "componentSchemaVersion": 26
4040
4034
  }