@motion-proto/live-tokens 0.16.2 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cli.mjs CHANGED
@@ -9,13 +9,18 @@ import { dirname, join, resolve } from 'node:path';
9
9
  import { fileURLToPath } from 'node:url';
10
10
  import process from 'node:process';
11
11
  import { checkComponent, formatReport } from './check-component.mjs';
12
+ import { runMigrate, formatMigrateResult } from './migrate.mjs';
12
13
 
13
14
  const USAGE = `Usage: npx @motion-proto/live-tokens <command> [options]
14
15
 
15
16
  Commands:
16
- setup-claude [--force] Install bundled Claude Code skills into ./.claude/skills/
17
- check-component <id> Validate <id>'s runtime, editor, and registration
18
- against the live-tokens-create-component contract
17
+ setup-claude [--force] Install bundled Claude Code skills into ./.claude/skills/
18
+ check-component <id> Validate <id>'s runtime, editor, and registration
19
+ against the live-tokens-create-component contract
20
+ migrate [--check] [--tokens <path>]
21
+ Reconcile your tokens.css with the installed
22
+ package's Layer-1 token vocabulary. --check reports
23
+ without writing (exit 1 if changes are needed).
19
24
  `;
20
25
 
21
26
  function fail(message, code = 1) {
@@ -38,6 +43,23 @@ if (command === 'check-component') {
38
43
  process.exit(result.errors.length === 0 ? 0 : 1);
39
44
  }
40
45
 
46
+ if (command === 'migrate') {
47
+ const check = rest.includes('--check');
48
+ const tokensIdx = rest.indexOf('--tokens');
49
+ const tokensArg = tokensIdx !== -1 ? rest[tokensIdx + 1] : undefined;
50
+ if (tokensIdx !== -1 && !tokensArg) fail(`--tokens requires a path`);
51
+ try {
52
+ const result = await runMigrate({ tokensArg, check });
53
+ console.log(formatMigrateResult(result, { check }));
54
+ // Exit 1 when --check finds pending changes (CI-friendly) or on no-path.
55
+ if (result.status === 'no-path') process.exit(1);
56
+ if (check && result.status === 'would-change') process.exit(1);
57
+ process.exit(0);
58
+ } catch (err) {
59
+ fail(`migrate failed: ${err instanceof Error ? err.message : String(err)}`);
60
+ }
61
+ }
62
+
41
63
  if (command !== 'setup-claude') {
42
64
  fail(`Unknown command: ${command}\n\n${USAGE}`);
43
65
  }
@@ -0,0 +1,113 @@
1
+ // `live-tokens migrate` worker.
2
+ //
3
+ // Applies the registered tokens-css migrations to a consumer's developer-authored
4
+ // tokens.css, reconciling Layer-1 drift after a package upgrade. The migration
5
+ // engine itself lives in the built plugin (dist-plugin/tokensCssMigrations) so
6
+ // the CLI and the dev-plugin guardrail share one source of truth; this module
7
+ // only handles path resolution, file IO, and reporting.
8
+
9
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
10
+ import { dirname, relative, resolve } from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+
13
+ const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
14
+ const ENGINE = resolve(pkgRoot, 'dist-plugin/tokensCssMigrations/index.js');
15
+
16
+ // Locations to probe when neither --tokens nor config.tokensCssPath is given.
17
+ const DEFAULT_CANDIDATES = [
18
+ 'src/system/styles/tokens.css',
19
+ 'src/styles/tokens.css',
20
+ 'src/tokens.css',
21
+ 'tokens.css',
22
+ ];
23
+
24
+ async function loadEngine() {
25
+ if (!existsSync(ENGINE)) {
26
+ throw new Error(
27
+ `migration engine not found at ${relative(process.cwd(), ENGINE)}. ` +
28
+ `Build the plugin first (npm run build:plugin).`,
29
+ );
30
+ }
31
+ return import(ENGINE);
32
+ }
33
+
34
+ /** --tokens <path> > live-tokens.config.json tokensCssPath > default scan. */
35
+ export function resolveTokensCssPath(explicit, configPath, root = process.cwd()) {
36
+ if (explicit) return resolve(root, explicit);
37
+ if (configPath) return resolve(root, configPath);
38
+ for (const c of DEFAULT_CANDIDATES) {
39
+ const full = resolve(root, c);
40
+ if (existsSync(full)) return full;
41
+ }
42
+ return null;
43
+ }
44
+
45
+ /**
46
+ * Run migrations. Returns a result object; never writes when `check` is true.
47
+ * `{ status: 'no-path' | 'unchanged' | 'changed' | 'would-change', ... }`
48
+ */
49
+ export async function runMigrate({ tokensArg, check = false, root = process.cwd() } = {}) {
50
+ const { runTokensCssMigrations, readLiveTokensConfig } = await loadEngine();
51
+
52
+ const config = readLiveTokensConfig();
53
+ const tokensPath = resolveTokensCssPath(tokensArg, config.tokensCssPath, root);
54
+ if (!tokensPath) {
55
+ return {
56
+ status: 'no-path',
57
+ message:
58
+ `Could not locate tokens.css. Pass --tokens <path>, set "tokensCssPath" in ` +
59
+ `live-tokens.config.json, or place it at one of: ${DEFAULT_CANDIDATES.join(', ')}.`,
60
+ };
61
+ }
62
+ if (!existsSync(tokensPath)) {
63
+ return { status: 'no-path', message: `tokens.css not found at ${relative(root, tokensPath)}.` };
64
+ }
65
+
66
+ const before = readFileSync(tokensPath, 'utf8');
67
+ const { css, applied, changed } = runTokensCssMigrations(before);
68
+ const addedLines = diffAddedLines(before, css);
69
+
70
+ if (!changed) {
71
+ return { status: 'unchanged', tokensPath };
72
+ }
73
+ if (check) {
74
+ return { status: 'would-change', tokensPath, applied, addedLines };
75
+ }
76
+ writeFileSync(tokensPath, css);
77
+ return { status: 'changed', tokensPath, applied, addedLines };
78
+ }
79
+
80
+ function diffAddedLines(before, after) {
81
+ const beforeSet = new Set(before.split('\n'));
82
+ return after.split('\n').filter((l) => l.trim() && !beforeSet.has(l));
83
+ }
84
+
85
+ export function formatMigrateResult(result, { check }) {
86
+ const root = process.cwd();
87
+ switch (result.status) {
88
+ case 'no-path':
89
+ return result.message;
90
+ case 'unchanged':
91
+ return `✓ ${relative(root, result.tokensPath)} is already up to date.`;
92
+ case 'would-change':
93
+ case 'changed': {
94
+ const verb = result.status === 'changed' ? 'Applied' : 'Would apply';
95
+ const lines = [
96
+ `${verb} ${result.applied.length} migration(s) to ${relative(root, result.tokensPath)}:`,
97
+ ...result.applied.map((id) => ` • ${id}`),
98
+ ];
99
+ if (result.addedLines.length) {
100
+ lines.push(`Added ${result.addedLines.length} line(s):`);
101
+ for (const l of result.addedLines) lines.push(` + ${l.trim()}`);
102
+ }
103
+ if (check) {
104
+ lines.push(`\nRun without --check to write these changes, then review the diff in git.`);
105
+ } else {
106
+ lines.push(`\nReview the diff in git before committing.`);
107
+ }
108
+ return lines.join('\n');
109
+ }
110
+ default:
111
+ return `Unknown result: ${JSON.stringify(result)}`;
112
+ }
113
+ }
@@ -0,0 +1,249 @@
1
+ // src/editor/core/themes/parsers/globalRootBlock.ts
2
+ function extractGlobalRootBody(source) {
3
+ const re = /:global\(:root\)\s*\{([^}]*)\}/g;
4
+ const bodies = [];
5
+ let m;
6
+ while ((m = re.exec(source)) !== null) {
7
+ bodies.push(m[1]);
8
+ }
9
+ return bodies.join("\n");
10
+ }
11
+
12
+ // vite-plugin/tokensCssMigrations/cssTokenOps.ts
13
+ var DECL_RE = /(^|[\s;{])(--[a-z0-9-]+)\s*:/gi;
14
+ var REF_RE = /var\(\s*(--[a-z0-9-]+)/gi;
15
+ function collectDefinedTokens(css) {
16
+ const out = /* @__PURE__ */ new Set();
17
+ for (const m of css.matchAll(DECL_RE)) out.add(m[2]);
18
+ return out;
19
+ }
20
+ function collectReferencedTokens(css) {
21
+ const out = /* @__PURE__ */ new Set();
22
+ for (const m of css.matchAll(REF_RE)) out.add(m[1]);
23
+ return out;
24
+ }
25
+ function ensureScale(css, opts) {
26
+ const defined = collectDefinedTokens(css);
27
+ const missing = opts.entries.filter((e) => !defined.has(e.name));
28
+ if (missing.length === 0) return css;
29
+ const lines = css.split("\n");
30
+ const { indent, insertAfter } = findInsertionPoint(lines, opts.anchorPrefixes ?? []);
31
+ const block = [];
32
+ if (opts.sectionComment) block.push(`${indent}/* ${opts.sectionComment} */`);
33
+ for (const e of missing) block.push(`${indent}${e.name}: ${e.value};`);
34
+ lines.splice(insertAfter + 1, 0, ...block);
35
+ return lines.join("\n");
36
+ }
37
+ function renameToken(css, oldName, newName) {
38
+ const defined = collectDefinedTokens(css);
39
+ if (!defined.has(oldName) || defined.has(newName)) return css;
40
+ const re = new RegExp(escapeRe(oldName) + "(?![a-z0-9-])", "gi");
41
+ return css.replace(re, newName);
42
+ }
43
+ function removeToken(css, name) {
44
+ const lines = css.split("\n");
45
+ const declRe = new RegExp("(^|[\\s;{])" + escapeRe(name) + "\\s*:");
46
+ const kept = lines.filter((line) => !declRe.test(line));
47
+ if (kept.length === lines.length) return css;
48
+ return kept.join("\n");
49
+ }
50
+ function removeTokensMatching(css, predicate) {
51
+ const lines = css.split("\n");
52
+ const declRe = /^\s*(--[a-z0-9-]+)\s*:/;
53
+ const kept = lines.filter((line) => {
54
+ const m = line.match(declRe);
55
+ return !(m && predicate(m[1]));
56
+ });
57
+ if (kept.length === lines.length) return css;
58
+ return kept.join("\n");
59
+ }
60
+ function findInsertionPoint(lines, anchorPrefixes) {
61
+ for (const prefix of anchorPrefixes) {
62
+ let last = -1;
63
+ let indent = " ";
64
+ for (let i = 0; i < lines.length; i++) {
65
+ const m = lines[i].match(/^(\s*)(--[a-z0-9-]+)\s*:/);
66
+ if (m && m[2].startsWith(prefix)) {
67
+ last = i;
68
+ indent = m[1] || " ";
69
+ }
70
+ }
71
+ if (last !== -1) return { indent, insertAfter: last };
72
+ }
73
+ let depth = 0;
74
+ let inRoot = false;
75
+ let lastDeclIndent = " ";
76
+ for (let i = 0; i < lines.length; i++) {
77
+ const line = lines[i];
78
+ if (!inRoot && /(^|\s):root[^{]*\{/.test(line)) {
79
+ inRoot = true;
80
+ depth = 1;
81
+ continue;
82
+ }
83
+ if (!inRoot) continue;
84
+ const declIndent = line.match(/^(\s*)--[a-z0-9-]+\s*:/);
85
+ if (declIndent) lastDeclIndent = declIndent[1] || " ";
86
+ depth += (line.match(/\{/g)?.length ?? 0) - (line.match(/\}/g)?.length ?? 0);
87
+ if (depth === 0) {
88
+ return { indent: lastDeclIndent, insertAfter: i - 1 };
89
+ }
90
+ }
91
+ return { indent: " ", insertAfter: lines.length - 1 };
92
+ }
93
+ function escapeRe(s) {
94
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
95
+ }
96
+
97
+ // vite-plugin/tokensCssMigrations/migrations/2026-05-29-typography-scale-additions.ts
98
+ var tokensCssMigration_2026_05_29_typographyScaleAdditions = {
99
+ id: "2026-05-29-typography-scale-additions",
100
+ description: "Add --line-height-{xs..xl}, --letter-spacing-* and --ease-out-quart scales",
101
+ apply(css) {
102
+ let out = css;
103
+ out = ensureScale(out, {
104
+ sectionComment: "Line height (t-shirt scale)",
105
+ anchorPrefixes: ["--line-height-", "--font-size-", "--font-weight-", "--font-"],
106
+ entries: [
107
+ { name: "--line-height-xs", value: "1" },
108
+ { name: "--line-height-sm", value: "1.25" },
109
+ { name: "--line-height-md", value: "1.5" },
110
+ { name: "--line-height-lg", value: "1.75" },
111
+ { name: "--line-height-xl", value: "2" }
112
+ ]
113
+ });
114
+ out = ensureScale(out, {
115
+ sectionComment: "Letter spacing",
116
+ anchorPrefixes: ["--letter-spacing-", "--line-height-", "--font-size-", "--font-"],
117
+ entries: [
118
+ { name: "--letter-spacing-tighter", value: "-0.04em" },
119
+ { name: "--letter-spacing-tight", value: "-0.02em" },
120
+ { name: "--letter-spacing-normal", value: "0" },
121
+ { name: "--letter-spacing-wide", value: "0.04em" },
122
+ { name: "--letter-spacing-wider", value: "0.08em" }
123
+ ]
124
+ });
125
+ out = ensureScale(out, {
126
+ sectionComment: "Easing",
127
+ anchorPrefixes: ["--ease-", "--transition-", "--duration-"],
128
+ entries: [{ name: "--ease-out-quart", value: "cubic-bezier(0.25, 1, 0.5, 1)" }]
129
+ });
130
+ return out;
131
+ }
132
+ };
133
+
134
+ // vite-plugin/tokensCssMigrations/migrations/2026-05-29-sectiondivider-legacy-axis-cleanup.ts
135
+ var KEEP_SEGMENTS = /* @__PURE__ */ new Set(["lg", "md", "sm"]);
136
+ var tokensCssMigration_2026_05_29_sectiondividerLegacyAxisCleanup = {
137
+ id: "2026-05-29-sectiondivider-legacy-axis-cleanup",
138
+ description: "Remove legacy --sectiondivider-* tokens not on the lg/md/sm axis",
139
+ apply(css) {
140
+ return removeTokensMatching(css, (name) => {
141
+ if (!name.startsWith("--sectiondivider-")) return false;
142
+ const segment = name.slice("--sectiondivider-".length).split("-")[0];
143
+ return !KEEP_SEGMENTS.has(segment);
144
+ });
145
+ }
146
+ };
147
+
148
+ // vite-plugin/files/dataPaths.ts
149
+ import fs from "fs";
150
+ import path from "path";
151
+ var DEFAULT_DATA_DIR = "src/live-tokens/data";
152
+ var KNOWN_CONFIG_KEYS = /* @__PURE__ */ new Set([
153
+ "dataDir",
154
+ "themesDir",
155
+ "componentConfigsDir",
156
+ "manifestsDir",
157
+ "tokensCssPath"
158
+ ]);
159
+ var cached = null;
160
+ function readLiveTokensConfig() {
161
+ if (cached) return cached;
162
+ try {
163
+ const configPath = path.resolve("live-tokens.config.json");
164
+ if (!fs.existsSync(configPath)) return cached = {};
165
+ const parsed = JSON.parse(fs.readFileSync(configPath, "utf-8"));
166
+ if (!parsed || typeof parsed !== "object") return cached = {};
167
+ const unknown = Object.keys(parsed).filter(
168
+ (k) => k !== "$schema" && !KNOWN_CONFIG_KEYS.has(k)
169
+ );
170
+ if (unknown.length > 0) {
171
+ console.warn(
172
+ `[live-tokens] Unknown key(s) in live-tokens.config.json: ${unknown.join(", ")}. Known keys: ${Array.from(KNOWN_CONFIG_KEYS).join(", ")}.`
173
+ );
174
+ }
175
+ cached = parsed;
176
+ return cached;
177
+ } catch {
178
+ return cached = {};
179
+ }
180
+ }
181
+ function resolveDataDirs(opts = {}) {
182
+ const fileConfig = readLiveTokensConfig();
183
+ const dataDirRaw = opts.dataDir ?? fileConfig.dataDir ?? DEFAULT_DATA_DIR;
184
+ const dataDir = path.resolve(dataDirRaw);
185
+ const sub = (name) => path.resolve(dataDir, name);
186
+ return {
187
+ dataDir,
188
+ themesDir: opts.themesDir ? path.resolve(opts.themesDir) : fileConfig.themesDir ? path.resolve(fileConfig.themesDir) : sub("themes"),
189
+ componentConfigsDir: opts.componentConfigsDir ? path.resolve(opts.componentConfigsDir) : fileConfig.componentConfigsDir ? path.resolve(fileConfig.componentConfigsDir) : sub("component-configs"),
190
+ manifestsDir: opts.manifestsDir ? path.resolve(opts.manifestsDir) : fileConfig.manifestsDir ? path.resolve(fileConfig.manifestsDir) : sub("manifests")
191
+ };
192
+ }
193
+
194
+ // vite-plugin/tokensCssMigrations/index.ts
195
+ var TOKENS_CSS_MIGRATIONS = [
196
+ tokensCssMigration_2026_05_29_typographyScaleAdditions,
197
+ tokensCssMigration_2026_05_29_sectiondividerLegacyAxisCleanup
198
+ ];
199
+ function runTokensCssMigrations(css) {
200
+ let out = css;
201
+ const applied = [];
202
+ for (const m of TOKENS_CSS_MIGRATIONS) {
203
+ const next = m.apply(out);
204
+ if (next !== out) {
205
+ applied.push(m.id);
206
+ out = next;
207
+ }
208
+ }
209
+ return { css: out, applied, changed: out !== css };
210
+ }
211
+ function validateTokensCss(input) {
212
+ const defined = /* @__PURE__ */ new Set([
213
+ ...collectDefinedTokens(input.tokensCss),
214
+ ...collectDefinedTokens(input.generatedCss ?? "")
215
+ ]);
216
+ const referencedBy = /* @__PURE__ */ new Map();
217
+ for (const { name, source } of input.componentSources) {
218
+ const body = extractGlobalRootBody(source);
219
+ if (!body) continue;
220
+ for (const t of collectDefinedTokens(body)) defined.add(t);
221
+ for (const ref of collectReferencedTokens(body)) {
222
+ let set = referencedBy.get(ref);
223
+ if (!set) referencedBy.set(ref, set = /* @__PURE__ */ new Set());
224
+ set.add(name);
225
+ }
226
+ }
227
+ const missing = [];
228
+ for (const [token, names] of referencedBy) {
229
+ if (!defined.has(token)) {
230
+ missing.push({ token, referencedBy: [...names].sort() });
231
+ }
232
+ }
233
+ return missing.sort((a, b) => a.token.localeCompare(b.token));
234
+ }
235
+
236
+ export {
237
+ extractGlobalRootBody,
238
+ readLiveTokensConfig,
239
+ resolveDataDirs,
240
+ collectDefinedTokens,
241
+ collectReferencedTokens,
242
+ ensureScale,
243
+ renameToken,
244
+ removeToken,
245
+ removeTokensMatching,
246
+ TOKENS_CSS_MIGRATIONS,
247
+ runTokensCssMigrations,
248
+ validateTokensCss
249
+ };
@@ -30,7 +30,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // vite-plugin/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
- themeFileApi: () => themeFileApi
33
+ TOKENS_CSS_MIGRATIONS: () => TOKENS_CSS_MIGRATIONS,
34
+ runTokensCssMigrations: () => runTokensCssMigrations,
35
+ themeFileApi: () => themeFileApi,
36
+ validateTokensCss: () => validateTokensCss
34
37
  });
35
38
  module.exports = __toCommonJS(index_exports);
36
39
 
@@ -564,7 +567,8 @@ var KNOWN_CONFIG_KEYS = /* @__PURE__ */ new Set([
564
567
  "dataDir",
565
568
  "themesDir",
566
569
  "componentConfigsDir",
567
- "manifestsDir"
570
+ "manifestsDir",
571
+ "tokensCssPath"
568
572
  ]);
569
573
  var cached = null;
570
574
  function readLiveTokensConfig() {
@@ -601,6 +605,168 @@ function resolveDataDirs(opts = {}) {
601
605
  };
602
606
  }
603
607
 
608
+ // vite-plugin/tokensCssMigrations/cssTokenOps.ts
609
+ var DECL_RE = /(^|[\s;{])(--[a-z0-9-]+)\s*:/gi;
610
+ var REF_RE = /var\(\s*(--[a-z0-9-]+)/gi;
611
+ function collectDefinedTokens(css) {
612
+ const out = /* @__PURE__ */ new Set();
613
+ for (const m of css.matchAll(DECL_RE)) out.add(m[2]);
614
+ return out;
615
+ }
616
+ function collectReferencedTokens(css) {
617
+ const out = /* @__PURE__ */ new Set();
618
+ for (const m of css.matchAll(REF_RE)) out.add(m[1]);
619
+ return out;
620
+ }
621
+ function ensureScale(css, opts) {
622
+ const defined = collectDefinedTokens(css);
623
+ const missing = opts.entries.filter((e) => !defined.has(e.name));
624
+ if (missing.length === 0) return css;
625
+ const lines = css.split("\n");
626
+ const { indent, insertAfter } = findInsertionPoint(lines, opts.anchorPrefixes ?? []);
627
+ const block = [];
628
+ if (opts.sectionComment) block.push(`${indent}/* ${opts.sectionComment} */`);
629
+ for (const e of missing) block.push(`${indent}${e.name}: ${e.value};`);
630
+ lines.splice(insertAfter + 1, 0, ...block);
631
+ return lines.join("\n");
632
+ }
633
+ function removeTokensMatching(css, predicate) {
634
+ const lines = css.split("\n");
635
+ const declRe = /^\s*(--[a-z0-9-]+)\s*:/;
636
+ const kept = lines.filter((line) => {
637
+ const m = line.match(declRe);
638
+ return !(m && predicate(m[1]));
639
+ });
640
+ if (kept.length === lines.length) return css;
641
+ return kept.join("\n");
642
+ }
643
+ function findInsertionPoint(lines, anchorPrefixes) {
644
+ for (const prefix of anchorPrefixes) {
645
+ let last = -1;
646
+ let indent = " ";
647
+ for (let i = 0; i < lines.length; i++) {
648
+ const m = lines[i].match(/^(\s*)(--[a-z0-9-]+)\s*:/);
649
+ if (m && m[2].startsWith(prefix)) {
650
+ last = i;
651
+ indent = m[1] || " ";
652
+ }
653
+ }
654
+ if (last !== -1) return { indent, insertAfter: last };
655
+ }
656
+ let depth = 0;
657
+ let inRoot = false;
658
+ let lastDeclIndent = " ";
659
+ for (let i = 0; i < lines.length; i++) {
660
+ const line = lines[i];
661
+ if (!inRoot && /(^|\s):root[^{]*\{/.test(line)) {
662
+ inRoot = true;
663
+ depth = 1;
664
+ continue;
665
+ }
666
+ if (!inRoot) continue;
667
+ const declIndent = line.match(/^(\s*)--[a-z0-9-]+\s*:/);
668
+ if (declIndent) lastDeclIndent = declIndent[1] || " ";
669
+ depth += (line.match(/\{/g)?.length ?? 0) - (line.match(/\}/g)?.length ?? 0);
670
+ if (depth === 0) {
671
+ return { indent: lastDeclIndent, insertAfter: i - 1 };
672
+ }
673
+ }
674
+ return { indent: " ", insertAfter: lines.length - 1 };
675
+ }
676
+
677
+ // vite-plugin/tokensCssMigrations/migrations/2026-05-29-typography-scale-additions.ts
678
+ var tokensCssMigration_2026_05_29_typographyScaleAdditions = {
679
+ id: "2026-05-29-typography-scale-additions",
680
+ description: "Add --line-height-{xs..xl}, --letter-spacing-* and --ease-out-quart scales",
681
+ apply(css) {
682
+ let out = css;
683
+ out = ensureScale(out, {
684
+ sectionComment: "Line height (t-shirt scale)",
685
+ anchorPrefixes: ["--line-height-", "--font-size-", "--font-weight-", "--font-"],
686
+ entries: [
687
+ { name: "--line-height-xs", value: "1" },
688
+ { name: "--line-height-sm", value: "1.25" },
689
+ { name: "--line-height-md", value: "1.5" },
690
+ { name: "--line-height-lg", value: "1.75" },
691
+ { name: "--line-height-xl", value: "2" }
692
+ ]
693
+ });
694
+ out = ensureScale(out, {
695
+ sectionComment: "Letter spacing",
696
+ anchorPrefixes: ["--letter-spacing-", "--line-height-", "--font-size-", "--font-"],
697
+ entries: [
698
+ { name: "--letter-spacing-tighter", value: "-0.04em" },
699
+ { name: "--letter-spacing-tight", value: "-0.02em" },
700
+ { name: "--letter-spacing-normal", value: "0" },
701
+ { name: "--letter-spacing-wide", value: "0.04em" },
702
+ { name: "--letter-spacing-wider", value: "0.08em" }
703
+ ]
704
+ });
705
+ out = ensureScale(out, {
706
+ sectionComment: "Easing",
707
+ anchorPrefixes: ["--ease-", "--transition-", "--duration-"],
708
+ entries: [{ name: "--ease-out-quart", value: "cubic-bezier(0.25, 1, 0.5, 1)" }]
709
+ });
710
+ return out;
711
+ }
712
+ };
713
+
714
+ // vite-plugin/tokensCssMigrations/migrations/2026-05-29-sectiondivider-legacy-axis-cleanup.ts
715
+ var KEEP_SEGMENTS = /* @__PURE__ */ new Set(["lg", "md", "sm"]);
716
+ var tokensCssMigration_2026_05_29_sectiondividerLegacyAxisCleanup = {
717
+ id: "2026-05-29-sectiondivider-legacy-axis-cleanup",
718
+ description: "Remove legacy --sectiondivider-* tokens not on the lg/md/sm axis",
719
+ apply(css) {
720
+ return removeTokensMatching(css, (name) => {
721
+ if (!name.startsWith("--sectiondivider-")) return false;
722
+ const segment = name.slice("--sectiondivider-".length).split("-")[0];
723
+ return !KEEP_SEGMENTS.has(segment);
724
+ });
725
+ }
726
+ };
727
+
728
+ // vite-plugin/tokensCssMigrations/index.ts
729
+ var TOKENS_CSS_MIGRATIONS = [
730
+ tokensCssMigration_2026_05_29_typographyScaleAdditions,
731
+ tokensCssMigration_2026_05_29_sectiondividerLegacyAxisCleanup
732
+ ];
733
+ function runTokensCssMigrations(css) {
734
+ let out = css;
735
+ const applied = [];
736
+ for (const m of TOKENS_CSS_MIGRATIONS) {
737
+ const next = m.apply(out);
738
+ if (next !== out) {
739
+ applied.push(m.id);
740
+ out = next;
741
+ }
742
+ }
743
+ return { css: out, applied, changed: out !== css };
744
+ }
745
+ function validateTokensCss(input) {
746
+ const defined = /* @__PURE__ */ new Set([
747
+ ...collectDefinedTokens(input.tokensCss),
748
+ ...collectDefinedTokens(input.generatedCss ?? "")
749
+ ]);
750
+ const referencedBy = /* @__PURE__ */ new Map();
751
+ for (const { name, source } of input.componentSources) {
752
+ const body = extractGlobalRootBody(source);
753
+ if (!body) continue;
754
+ for (const t of collectDefinedTokens(body)) defined.add(t);
755
+ for (const ref of collectReferencedTokens(body)) {
756
+ let set = referencedBy.get(ref);
757
+ if (!set) referencedBy.set(ref, set = /* @__PURE__ */ new Set());
758
+ set.add(name);
759
+ }
760
+ }
761
+ const missing = [];
762
+ for (const [token, names] of referencedBy) {
763
+ if (!defined.has(token)) {
764
+ missing.push({ token, referencedBy: [...names].sort() });
765
+ }
766
+ }
767
+ return missing.sort((a, b) => a.token.localeCompare(b.token));
768
+ }
769
+
604
770
  // vite-plugin/themeFileApi.ts
605
771
  var import_node_url = require("url");
606
772
  var import_meta = {};
@@ -871,6 +1037,33 @@ function themeFileApi(opts) {
871
1037
  function listComponentNames() {
872
1038
  return listComponentSourcePaths().map(componentNameFromFile);
873
1039
  }
1040
+ function warnOnTokenDrift(log) {
1041
+ let tokensCss = "";
1042
+ try {
1043
+ tokensCss = import_fs3.default.readFileSync(CSS_PATH, "utf-8");
1044
+ } catch {
1045
+ return;
1046
+ }
1047
+ let generatedCss = "";
1048
+ try {
1049
+ generatedCss = import_fs3.default.readFileSync(GENERATED_CSS_PATH, "utf-8");
1050
+ } catch {
1051
+ }
1052
+ const componentSources = listComponentSourcePaths().map((p) => ({
1053
+ name: componentNameFromFile(p),
1054
+ source: import_fs3.default.readFileSync(p, "utf-8")
1055
+ }));
1056
+ const missing = validateTokensCss({ tokensCss, generatedCss, componentSources });
1057
+ if (missing.length === 0) return;
1058
+ const lines = missing.map(
1059
+ (m) => ` ${m.token} (referenced by ${m.referencedBy.join(", ")})`
1060
+ );
1061
+ log(
1062
+ `[live-tokens] ${missing.length} token(s) referenced by components are not defined in ${import_path3.default.relative(process.cwd(), CSS_PATH)}:
1063
+ ${lines.join("\n")}
1064
+ These render as blank/empty editor slots. Run \`npx live-tokens migrate\` to reconcile.`
1065
+ );
1066
+ }
874
1067
  function generateDefaultConfig(comp, sourcePath) {
875
1068
  if (!import_fs3.default.existsSync(sourcePath)) return;
876
1069
  const r = componentResource(comp);
@@ -1696,6 +1889,7 @@ function themeFileApi(opts) {
1696
1889
  ensureComponentConfigsDir();
1697
1890
  ensureManifestsDir();
1698
1891
  regenerateTokensCss();
1892
+ warnOnTokenDrift((msg) => server.config.logger.warn(msg));
1699
1893
  server.middlewares.use(async (req, res, next) => {
1700
1894
  const handled = await dispatch(req, res, routes);
1701
1895
  if (!handled) next();
@@ -1713,5 +1907,8 @@ function themeFileApi(opts) {
1713
1907
  }
1714
1908
  // Annotate the CommonJS export names for ESM import in node:
1715
1909
  0 && (module.exports = {
1716
- themeFileApi
1910
+ TOKENS_CSS_MIGRATIONS,
1911
+ runTokensCssMigrations,
1912
+ themeFileApi,
1913
+ validateTokensCss
1717
1914
  });
@@ -1,4 +1,5 @@
1
1
  import { Plugin } from 'vite';
2
+ export { ComponentSource, MissingToken, RunResult, TOKENS_CSS_MIGRATIONS, TokensCssMigration, ValidateInput, runTokensCssMigrations, validateTokensCss } from './tokensCssMigrations/index.cjs';
2
3
 
3
4
  interface ThemeFileApiOptions {
4
5
  tokensCssPath: string;
@@ -1,4 +1,5 @@
1
1
  import { Plugin } from 'vite';
2
+ export { ComponentSource, MissingToken, RunResult, TOKENS_CSS_MIGRATIONS, TokensCssMigration, ValidateInput, runTokensCssMigrations, validateTokensCss } from './tokensCssMigrations/index.js';
2
3
 
3
4
  interface ThemeFileApiOptions {
4
5
  tokensCssPath: string;