@motion-proto/live-tokens 0.79.0 → 0.80.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/.claude/skills/live-tokens-check-compliance/SKILL.md +12 -30
- package/CHANGELOG.md +40 -0
- package/README.md +4 -4
- package/bin/check-component.mjs +2 -0
- package/bin/check-page.mjs +2 -0
- package/bin/cli.mjs +38 -7
- package/bin/lib/buildChecks.mjs +32 -0
- package/bin/migrate-build-script.mjs +66 -0
- package/bin/migrate.mjs +5 -0
- package/bin/rules/tokens.mjs +64 -1
- package/dist-plugin/index.cjs +38 -4
- package/dist-plugin/index.d.cts +1 -0
- package/dist-plugin/index.d.ts +1 -0
- package/dist-plugin/index.js +79 -46
- package/package.json +1 -1
- package/src/editor/skill-atlas/skillSources.generated.ts +1 -1
- package/src/editor/skill-atlas/trees/check-compliance.ts +25 -302
- package/src/editor/skill-atlas/trees/create-component.ts +4 -17
- package/src/system/components/CollapsibleSection.svelte +15 -15
- package/template/package.json +1 -2
- package/template/vite.config.ts +3 -2
package/dist-plugin/index.js
CHANGED
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
|
|
38
38
|
// vite-plugin/themeFileApi.ts
|
|
39
39
|
import fs2 from "fs";
|
|
40
|
-
import
|
|
40
|
+
import path3 from "path";
|
|
41
41
|
|
|
42
42
|
// src/editor/core/themes/migrations/2026-07-21-palette-oklch-basis.ts
|
|
43
43
|
var round = (v, dp) => {
|
|
@@ -229,18 +229,40 @@ function dataTreeWatch(opts) {
|
|
|
229
229
|
return { noteOwnWrite, subscribe };
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
+
// vite-plugin/buildChecks.ts
|
|
233
|
+
import path2 from "path";
|
|
234
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
235
|
+
var runPackageChecks = async (root) => {
|
|
236
|
+
const runner = path2.resolve(path2.dirname(fileURLToPath(import.meta.url)), "..", "bin", "lib", "buildChecks.mjs");
|
|
237
|
+
const { runBuildChecks } = await import(pathToFileURL(runner).href);
|
|
238
|
+
return runBuildChecks({ root });
|
|
239
|
+
};
|
|
240
|
+
async function checkBeforeBuild({
|
|
241
|
+
root,
|
|
242
|
+
logger,
|
|
243
|
+
run = runPackageChecks
|
|
244
|
+
}) {
|
|
245
|
+
const result = await run(root);
|
|
246
|
+
if (result.errors > 0) {
|
|
247
|
+
logger.error(result.report);
|
|
248
|
+
return `live-tokens: the design checks found ${result.errors} error(s). Run the live-tokens-check-compliance skill, or \`npx live-tokens check-page\` and \`npx live-tokens check-component\`, to repair them.`;
|
|
249
|
+
}
|
|
250
|
+
(result.warnings > 0 ? logger.warn : logger.info).call(logger, result.report);
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
|
|
232
254
|
// vite-plugin/themeFileApi.ts
|
|
233
|
-
import { fileURLToPath } from "url";
|
|
255
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
234
256
|
var PKG_VERSION = (() => {
|
|
235
257
|
try {
|
|
236
|
-
let dir =
|
|
258
|
+
let dir = path3.dirname(fileURLToPath2(import.meta.url));
|
|
237
259
|
for (let i = 0; i < 4; i++) {
|
|
238
|
-
const p =
|
|
260
|
+
const p = path3.join(dir, "package.json");
|
|
239
261
|
if (fs2.existsSync(p)) {
|
|
240
262
|
const json = JSON.parse(fs2.readFileSync(p, "utf-8"));
|
|
241
263
|
if (json?.name === "@motion-proto/live-tokens") return json.version ?? "";
|
|
242
264
|
}
|
|
243
|
-
const up =
|
|
265
|
+
const up = path3.dirname(dir);
|
|
244
266
|
if (up === dir) break;
|
|
245
267
|
dir = up;
|
|
246
268
|
}
|
|
@@ -284,37 +306,37 @@ function themeFileApi(opts) {
|
|
|
284
306
|
const COMPONENT_CONFIGS_DIR = dataDirs.componentConfigsDir;
|
|
285
307
|
const THEMES_DIR = dataDirs.themesDir;
|
|
286
308
|
const SKETCH_STYLES_DIR = dataDirs.sketchStylesDir;
|
|
287
|
-
const CSS_PATH =
|
|
309
|
+
const CSS_PATH = path3.resolve(opts.tokensCssPath);
|
|
288
310
|
const ISOLATED_DATA_DIR = testDataDir();
|
|
289
|
-
const GENERATED_CSS_PATH = ISOLATED_DATA_DIR ?
|
|
290
|
-
const FONTS_CSS_PATH = ISOLATED_DATA_DIR ?
|
|
311
|
+
const GENERATED_CSS_PATH = ISOLATED_DATA_DIR ? path3.join(ISOLATED_DATA_DIR, "tokens.generated.css") : opts.tokensGeneratedCssPath ? path3.resolve(opts.tokensGeneratedCssPath) : path3.join(dataDirs.dataDir, "tokens.generated.css");
|
|
312
|
+
const FONTS_CSS_PATH = ISOLATED_DATA_DIR ? path3.join(ISOLATED_DATA_DIR, "fonts.css") : opts.fontsCssPath ? path3.resolve(opts.fontsCssPath) : path3.join(path3.dirname(CSS_PATH), "fonts.css");
|
|
291
313
|
const API_BASE = opts.apiBase ?? "/api/live-tokens";
|
|
292
|
-
const consumerComponentDirs = opts.componentsSrcDir ? [
|
|
293
|
-
const packageComponentsDir =
|
|
294
|
-
|
|
314
|
+
const consumerComponentDirs = opts.componentsSrcDir ? [path3.resolve(opts.componentsSrcDir)] : [path3.resolve("src/components"), path3.resolve("src/system/components")];
|
|
315
|
+
const packageComponentsDir = path3.resolve(
|
|
316
|
+
path3.dirname(fileURLToPath2(import.meta.url)),
|
|
295
317
|
"..",
|
|
296
318
|
"src",
|
|
297
319
|
"system",
|
|
298
320
|
"components"
|
|
299
321
|
);
|
|
300
|
-
const packageDataDir = testPackageDataDir() ??
|
|
301
|
-
|
|
322
|
+
const packageDataDir = testPackageDataDir() ?? path3.resolve(
|
|
323
|
+
path3.dirname(fileURLToPath2(import.meta.url)),
|
|
302
324
|
"..",
|
|
303
325
|
"src",
|
|
304
326
|
"live-tokens",
|
|
305
327
|
"data"
|
|
306
328
|
);
|
|
307
|
-
const packageColorsAndTypeDir =
|
|
308
|
-
const packageThemesDir =
|
|
309
|
-
const packageComponentConfigsDir =
|
|
310
|
-
const packageSketchStylesDir =
|
|
329
|
+
const packageColorsAndTypeDir = path3.join(packageDataDir, "colors-and-type");
|
|
330
|
+
const packageThemesDir = path3.join(packageDataDir, "themes");
|
|
331
|
+
const packageComponentConfigsDir = path3.join(packageDataDir, "component-configs");
|
|
332
|
+
const packageSketchStylesDir = path3.join(packageDataDir, "sketch-styles");
|
|
311
333
|
function shippedNames(dataDir, subdir) {
|
|
312
334
|
try {
|
|
313
|
-
const pkgRoot =
|
|
314
|
-
const pkg = JSON.parse(fs2.readFileSync(
|
|
335
|
+
const pkgRoot = path3.resolve(dataDir, "..", "..", "..");
|
|
336
|
+
const pkg = JSON.parse(fs2.readFileSync(path3.join(pkgRoot, "package.json"), "utf-8"));
|
|
315
337
|
const files = Array.isArray(pkg.files) ? pkg.files : [];
|
|
316
338
|
const owned = new RegExp(`^src/live-tokens/data/${subdir}/[^/]+\\.json$`);
|
|
317
|
-
return files.filter((f) => owned.test(f)).map((f) =>
|
|
339
|
+
return files.filter((f) => owned.test(f)).map((f) => path3.basename(f, ".json"));
|
|
318
340
|
} catch {
|
|
319
341
|
return [];
|
|
320
342
|
}
|
|
@@ -323,7 +345,7 @@ function themeFileApi(opts) {
|
|
|
323
345
|
if (!COMPONENTS_SCAN_DIRS.includes(packageComponentsDir) && fs2.existsSync(packageComponentsDir)) {
|
|
324
346
|
COMPONENTS_SCAN_DIRS.push(packageComponentsDir);
|
|
325
347
|
}
|
|
326
|
-
const isLiveStateFile = (file) => file ===
|
|
348
|
+
const isLiveStateFile = (file) => file === path3.join(THEMES_DIR, "_active.json") || path3.basename(file) === "_working.json" && (file.startsWith(COLORS_AND_TYPE_DIR + path3.sep) || file.startsWith(COMPONENT_CONFIGS_DIR + path3.sep));
|
|
327
349
|
const liveWatch = dataTreeWatch({
|
|
328
350
|
dirs: [COLORS_AND_TYPE_DIR, COMPONENT_CONFIGS_DIR, THEMES_DIR],
|
|
329
351
|
isLive: isLiveStateFile
|
|
@@ -339,8 +361,8 @@ function themeFileApi(opts) {
|
|
|
339
361
|
let r = componentResourceCache.get(comp);
|
|
340
362
|
if (!r) {
|
|
341
363
|
r = versionedFileResourceServer({
|
|
342
|
-
dir:
|
|
343
|
-
packageDir:
|
|
364
|
+
dir: path3.join(COMPONENT_CONFIGS_DIR, comp),
|
|
365
|
+
packageDir: path3.join(packageComponentConfigsDir, comp),
|
|
344
366
|
onWrite: liveWatch.noteOwnWrite
|
|
345
367
|
});
|
|
346
368
|
componentResourceCache.set(comp, r);
|
|
@@ -431,7 +453,7 @@ function themeFileApi(opts) {
|
|
|
431
453
|
const productionThemeName = themesResource.getProductionName();
|
|
432
454
|
const productionTheme = readTheme(productionThemeName)?.theme ?? null;
|
|
433
455
|
if (themesResource.existingPath(productionThemeName) !== null && !productionTheme?.colorsAndType) {
|
|
434
|
-
const tail = `${
|
|
456
|
+
const tail = `${path3.basename(GENERATED_CSS_PATH)} was left as it was.`;
|
|
435
457
|
throw new Error(
|
|
436
458
|
unreadableThemeReason(productionThemeName) === "colors-and-type" ? `[live-tokens] ${notAThemeError(productionThemeName)} ${tail}` : `[live-tokens] Production theme "${productionThemeName}" is unreadable, so ${tail} Repair the theme file or adopt another theme.`
|
|
437
459
|
);
|
|
@@ -502,12 +524,12 @@ function themeFileApi(opts) {
|
|
|
502
524
|
lines.push("");
|
|
503
525
|
}
|
|
504
526
|
}
|
|
505
|
-
if (!fs2.existsSync(
|
|
506
|
-
fs2.mkdirSync(
|
|
527
|
+
if (!fs2.existsSync(path3.dirname(GENERATED_CSS_PATH))) {
|
|
528
|
+
fs2.mkdirSync(path3.dirname(GENERATED_CSS_PATH), { recursive: true });
|
|
507
529
|
}
|
|
508
530
|
fs2.writeFileSync(GENERATED_CSS_PATH, lines.join("\n"));
|
|
509
531
|
console.log(
|
|
510
|
-
`[regenerateTokensCss] Wrote ${
|
|
532
|
+
`[regenerateTokensCss] Wrote ${path3.basename(GENERATED_CSS_PATH)} (${colorsAndTypeVarCount} theme vars, ${componentOverrideCount} component overrides)`
|
|
511
533
|
);
|
|
512
534
|
}
|
|
513
535
|
function syncFontsToCss(colorsAndTypeData) {
|
|
@@ -535,14 +557,14 @@ function themeFileApi(opts) {
|
|
|
535
557
|
lines.push("");
|
|
536
558
|
}
|
|
537
559
|
const content = lines.join("\n");
|
|
538
|
-
if (!fs2.existsSync(
|
|
539
|
-
fs2.mkdirSync(
|
|
560
|
+
if (!fs2.existsSync(path3.dirname(FONTS_CSS_PATH))) {
|
|
561
|
+
fs2.mkdirSync(path3.dirname(FONTS_CSS_PATH), { recursive: true });
|
|
540
562
|
}
|
|
541
563
|
fs2.writeFileSync(FONTS_CSS_PATH, content);
|
|
542
|
-
console.log(`[syncFontsToCss] Wrote ${sources.length} source(s) into ${
|
|
564
|
+
console.log(`[syncFontsToCss] Wrote ${sources.length} source(s) into ${path3.basename(FONTS_CSS_PATH)}`);
|
|
543
565
|
}
|
|
544
566
|
function componentNameFromFile(filePath) {
|
|
545
|
-
return
|
|
567
|
+
return path3.basename(filePath, ".svelte").toLowerCase();
|
|
546
568
|
}
|
|
547
569
|
function isThemeAwareComponent(filePath) {
|
|
548
570
|
try {
|
|
@@ -558,7 +580,7 @@ function themeFileApi(opts) {
|
|
|
558
580
|
if (!fs2.existsSync(dir)) continue;
|
|
559
581
|
for (const f of fs2.readdirSync(dir)) {
|
|
560
582
|
if (!f.endsWith(".svelte")) continue;
|
|
561
|
-
const full =
|
|
583
|
+
const full = path3.join(dir, f);
|
|
562
584
|
if (!isThemeAwareComponent(full)) continue;
|
|
563
585
|
const name = componentNameFromFile(f);
|
|
564
586
|
if (!byName.has(name)) byName.set(name, full);
|
|
@@ -591,7 +613,7 @@ function themeFileApi(opts) {
|
|
|
591
613
|
(m) => ` ${m.token} (referenced by ${m.referencedBy.join(", ")})`
|
|
592
614
|
);
|
|
593
615
|
log(
|
|
594
|
-
`[live-tokens] ${missing.length} token(s) referenced by components are not defined in ${
|
|
616
|
+
`[live-tokens] ${missing.length} token(s) referenced by components are not defined in ${path3.relative(process.cwd(), CSS_PATH)}:
|
|
595
617
|
${lines.join("\n")}
|
|
596
618
|
These render as blank/empty editor slots. Run \`npx live-tokens migrate\` to reconcile.`
|
|
597
619
|
);
|
|
@@ -607,7 +629,7 @@ ${lines.join("\n")}
|
|
|
607
629
|
if (!changed) return;
|
|
608
630
|
fs2.writeFileSync(CSS_PATH, css);
|
|
609
631
|
log(
|
|
610
|
-
`[live-tokens] autoMigrate applied ${applied.length} additive migration(s) to ${
|
|
632
|
+
`[live-tokens] autoMigrate applied ${applied.length} additive migration(s) to ${path3.relative(process.cwd(), CSS_PATH)}: ${applied.join(", ")}. Review the diff in git.`
|
|
611
633
|
);
|
|
612
634
|
}
|
|
613
635
|
function generateDefaultConfig(comp, sourcePath) {
|
|
@@ -672,14 +694,14 @@ ${lines.join("\n")}
|
|
|
672
694
|
const legacy = [];
|
|
673
695
|
const check = (dir) => {
|
|
674
696
|
for (const name of ["_active.json", "_production.json"]) {
|
|
675
|
-
const p =
|
|
676
|
-
if (fs2.existsSync(p)) legacy.push(
|
|
697
|
+
const p = path3.join(dir, name);
|
|
698
|
+
if (fs2.existsSync(p)) legacy.push(path3.relative(process.cwd(), p));
|
|
677
699
|
}
|
|
678
700
|
};
|
|
679
701
|
check(COLORS_AND_TYPE_DIR);
|
|
680
702
|
if (fs2.existsSync(COMPONENT_CONFIGS_DIR)) {
|
|
681
703
|
for (const entry of fs2.readdirSync(COMPONENT_CONFIGS_DIR, { withFileTypes: true })) {
|
|
682
|
-
if (entry.isDirectory()) check(
|
|
704
|
+
if (entry.isDirectory()) check(path3.join(COMPONENT_CONFIGS_DIR, entry.name));
|
|
683
705
|
}
|
|
684
706
|
}
|
|
685
707
|
return legacy;
|
|
@@ -687,19 +709,19 @@ ${lines.join("\n")}
|
|
|
687
709
|
function warnOnLegacyPointers(legacy, bakeHeld, warn) {
|
|
688
710
|
if (legacy.length === 0) return;
|
|
689
711
|
warn(
|
|
690
|
-
`[live-tokens] ${legacy.length} legacy pointer file(s) left from the pre-working-set data model, starting with ${legacy[0]}. They are no longer read. Run \`npx live-tokens migrate\` to heal the tree.` + (bakeHeld ? ` Until then this tree has named no production theme, so ${
|
|
712
|
+
`[live-tokens] ${legacy.length} legacy pointer file(s) left from the pre-working-set data model, starting with ${legacy[0]}. They are no longer read. Run \`npx live-tokens migrate\` to heal the tree.` + (bakeHeld ? ` Until then this tree has named no production theme, so ${path3.basename(GENERATED_CSS_PATH)} is left exactly as it is rather than rebuilt from the default.` : "")
|
|
691
713
|
);
|
|
692
714
|
}
|
|
693
715
|
function warnOnLegacyLayout(evidence, warn) {
|
|
694
716
|
warn(
|
|
695
|
-
`[live-tokens] This project still uses the data layout from 0.47 and earlier: ${evidence}. Since 0.48 the whole-theme theme files live in \`themes/\` and the colors and type live in \`colors-and-type/\`. Nothing in the data directory was written, the editor cannot save, and ${
|
|
717
|
+
`[live-tokens] This project still uses the data layout from 0.47 and earlier: ${evidence}. Since 0.48 the whole-theme theme files live in \`themes/\` and the colors and type live in \`colors-and-type/\`. Nothing in the data directory was written, the editor cannot save, and ${path3.basename(GENERATED_CSS_PATH)} is left exactly as it is. Run \`npx live-tokens migrate\` to move the directories and heal the tree, then restart the dev server.`
|
|
696
718
|
);
|
|
697
719
|
}
|
|
698
720
|
function warnOnUnmigratedSketchStyles(warn) {
|
|
699
|
-
const legacyDir =
|
|
721
|
+
const legacyDir = path3.join(dataDirs.dataDir, "sketch-presets");
|
|
700
722
|
if (!fs2.existsSync(legacyDir)) return;
|
|
701
723
|
warn(
|
|
702
|
-
`[live-tokens] Saved sketchstyles still live under the retired ${
|
|
724
|
+
`[live-tokens] Saved sketchstyles still live under the retired ${path3.basename(legacyDir)}/ directory name. Run \`npx live-tokens migrate\` to rename it to ${path3.basename(SKETCH_STYLES_DIR)}/; until then they will not show up in the Sketchstyle view.`
|
|
703
725
|
);
|
|
704
726
|
}
|
|
705
727
|
const diskThemeResolvers = {
|
|
@@ -731,7 +753,7 @@ ${lines.join("\n")}
|
|
|
731
753
|
return "corrupt";
|
|
732
754
|
}
|
|
733
755
|
function notAThemeError(fileName) {
|
|
734
|
-
return `"${fileName}" is a colors-and-type file, not a theme. Before 0.48 those lived in themes/; move it to ${
|
|
756
|
+
return `"${fileName}" is a colors-and-type file, not a theme. Before 0.48 those lived in themes/; move it to ${path3.basename(COLORS_AND_TYPE_DIR)}/ or delete it. To build a theme, use the theme skill "create-theme".`;
|
|
735
757
|
}
|
|
736
758
|
function respondUnreadableTheme(res, fileName, missingError) {
|
|
737
759
|
switch (unreadableThemeReason(fileName)) {
|
|
@@ -753,7 +775,7 @@ ${lines.join("\n")}
|
|
|
753
775
|
fs2.writeFileSync(themesResource.filePath(fileName), JSON.stringify(theme, null, 2));
|
|
754
776
|
}
|
|
755
777
|
function migrateLocalThemes(warn) {
|
|
756
|
-
if (
|
|
778
|
+
if (path3.resolve(THEMES_DIR) === path3.resolve(packageThemesDir)) return;
|
|
757
779
|
if (!fs2.existsSync(THEMES_DIR)) return;
|
|
758
780
|
for (const file of fs2.readdirSync(THEMES_DIR)) {
|
|
759
781
|
if (!file.endsWith(".json") || file.startsWith("_")) continue;
|
|
@@ -761,7 +783,7 @@ ${lines.join("\n")}
|
|
|
761
783
|
if (fileName === "default") continue;
|
|
762
784
|
let raw;
|
|
763
785
|
try {
|
|
764
|
-
raw = JSON.parse(fs2.readFileSync(
|
|
786
|
+
raw = JSON.parse(fs2.readFileSync(path3.join(THEMES_DIR, file), "utf-8"));
|
|
765
787
|
} catch {
|
|
766
788
|
continue;
|
|
767
789
|
}
|
|
@@ -1649,7 +1671,9 @@ data: ${JSON.stringify(state)}
|
|
|
1649
1671
|
{ method: "POST", pattern: PRODUCTION_ROUTE, handler: methodNotAllowed },
|
|
1650
1672
|
{ method: "DELETE", pattern: PRODUCTION_ROUTE, handler: methodNotAllowed }
|
|
1651
1673
|
];
|
|
1652
|
-
const isEditorOwnedJson = (file) => file.endsWith(".json") &&
|
|
1674
|
+
const isEditorOwnedJson = (file) => file.endsWith(".json") && path3.resolve(file).startsWith(dataDirs.dataDir + path3.sep);
|
|
1675
|
+
let buildLogger = null;
|
|
1676
|
+
let buildChecked = false;
|
|
1653
1677
|
return {
|
|
1654
1678
|
name: "theme-file-api",
|
|
1655
1679
|
config() {
|
|
@@ -1662,6 +1686,15 @@ data: ${JSON.stringify(state)}
|
|
|
1662
1686
|
server: { watch: { ignored: [isEditorOwnedJson] } }
|
|
1663
1687
|
};
|
|
1664
1688
|
},
|
|
1689
|
+
configResolved(config) {
|
|
1690
|
+
buildLogger = config.command === "build" && opts.checks !== false ? config.logger : null;
|
|
1691
|
+
},
|
|
1692
|
+
async buildStart() {
|
|
1693
|
+
if (!buildLogger || buildChecked) return;
|
|
1694
|
+
buildChecked = true;
|
|
1695
|
+
const failure = await checkBeforeBuild({ root: process.cwd(), logger: buildLogger });
|
|
1696
|
+
if (failure) this.error(failure);
|
|
1697
|
+
},
|
|
1665
1698
|
configureServer(server) {
|
|
1666
1699
|
legacyLayout = detectLegacyLayout({
|
|
1667
1700
|
dataDir: dataDirs.dataDir,
|
|
@@ -1703,7 +1736,7 @@ data: ${JSON.stringify(state)}
|
|
|
1703
1736
|
},
|
|
1704
1737
|
handleHotUpdate(ctx) {
|
|
1705
1738
|
if (legacyLayout) return;
|
|
1706
|
-
const normalized =
|
|
1739
|
+
const normalized = path3.resolve(ctx.file);
|
|
1707
1740
|
if (!COMPONENTS_SCAN_DIRS.some((d) => normalized.startsWith(d))) return;
|
|
1708
1741
|
if (!normalized.endsWith(".svelte")) return;
|
|
1709
1742
|
if (!isThemeAwareComponent(normalized)) return;
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@ export const SKILL_DOC = 'SKILL.md' as const;
|
|
|
5
5
|
|
|
6
6
|
export const skillDocs: Record<string, Record<string, string[]>> = {
|
|
7
7
|
"check-compliance": {
|
|
8
|
-
"SKILL.md": ["---","name: live-tokens-check-compliance","description: Check an existing @motion-proto/live-tokens project against the design system and fix it until check-page and check-component both exit 0. Checks for correct use of components, properties, and tokens.
|
|
8
|
+
"SKILL.md": ["---","name: live-tokens-check-compliance","description: Check an existing @motion-proto/live-tokens project against the design system and fix it until check-page and check-component both exit 0. Checks for correct use of components, properties, and tokens. Runs both checkers, which bring tokens.css up to the installed package, apply every auto repair, and return each remaining finding with its own guidance, repair level, and details. Use when the user asks to check, audit, or review the project. Use when the user asks to fix the project or to make the build's design checks pass. Edits the files the checkers name. Changes tokens.css only through its migrations.","---","","# Checking and fixing a project's adherence to live-tokens","","Check the project, then fix every finding of `check-page` and `check-component` until both exit 0. `check-page` checks pages. Every component comes from the catalogue, every prop is declared, and every value in page CSS is a design token. `check-component` checks the project's own components. Every token names a semantic property, and its default is the design token that property reads. Every finding carries its own `guidance`, and the repair follows it.","","Both checkers are static. They read the project's source, never open a browser, and never mount a runtime. `npx live-tokens check-page <file> --tests` proves a page's rendered paint, and `npx live-tokens check-component <id> --tests` proves a component's declared behavior. **live-tokens-create-page** and **live-tokens-create-component** run those, one file or one id at a time, and their findings carry guidance the same way.","","## Workflow","","1. Run both checkers with `--json`. Each first brings `tokens.css` up to the installed package by applying every additive migration, then applies every finding whose `repair` is `auto`, checks again, and returns the fixes it applied in `fix.applied` beside the findings that remain. A pending breaking migration returns as a `tokens-breaking-migration` finding, since it renames design tokens the project may read. A fix in `fix.skipped` found its text moved, and the next run applies it. `--no-fix` reports without editing, for a build or CI."," ```sh"," npx live-tokens check-page --json"," npx live-tokens check-component --json"," ```","2. Read what remains. Each finding carries the fields under Finding fields, with a `repair` of `choice` or `authored`.","3. Group the findings by rule. Take the largest error group first, then the remaining errors. Take warnings only when the repair scope includes warnings.","4. Make every repair in the group from its `guidance`, within Scope below.","5. Run both checkers again. When repairable findings remain in scope, return to step 3. When no token fits a remaining finding, leave it and continue to the reply with its reason.","6. When the errors are clear, run both checkers with `--strict`. Report what `--strict` adds. Clear warnings within the existing request. Otherwise ask whether to clear the warnings now.","7. When the repair scope includes warnings, return to step 3 with `--strict`. When strict checks pass or the user defers warnings, continue to the reply.","8. Reply with:"," - the fixes the checkers applied, each with its count and any visible shift"," - the remaining changes by rule, each with its count and any visible shift"," - the findings left, each with its reason and any config entry the user chose"," - both checker commands with their exit codes","","`check-page <path>` scopes a run to one page. `check-component <id>` scopes a run to one component: its runtime, its editor, and its registration. The checkers read tokens.css from its default location.","","## Finding fields","","Every finding from both checkers, under each checker's `--json`:","","| Field | Value |","| --- | --- |","| `rule`, `severity`, `file`, `line`, `message` | What the finding is and where. |","| `guidance` | How to make the repair: the token for the role or the scale step, the command that prints the candidates, or the section of a create skill that owns the fix. |","| `repair` | `auto`, `choice`, or `authored`. See Repair levels. |","| `exception` | The narrower config entry that records a decision to leave the finding as it is: `{ \"checks\": { \"exclude\": [\"<file>\"] } }` for a page or CSS file, otherwise `{ \"checks\": { \"rules\": { \"<rule>\": \"warn\" } } }`. Applying it steps the rule down one level, error to warn and warn to off. |","| `details` | Per-rule data the message already states in prose, such as the accepted values behind `unknown-prop-value` or the nearest design-token candidates behind `dimension-literal`. Absent when a rule has nothing to add. |","","## Repair levels","","Every finding's `repair` says what moving it costs.","","- **`auto`.** The value determines the fix, such as a spacing literal with one nearest design-token step. The checkers apply it and list it in `fix.applied`.","- **`choice`.** A role or an ambiguous value determines the fix, such as a color literal and the role it plays, or a spacing literal tied between two steps. `details` lists the candidates, and `guidance` says how to pick one.","- **`authored`.** New code is the fix: a runtime that has to start behaving, an editor schema that has to name a token, a route that has to move. `guidance` names what to write. No candidate list applies.","","## Scope","","- Add no token to `tokens.css`. Map a literal with no matching token to the nearest existing token by role. When no token fits, leave the finding and say so.","- When the nearest token differs from the literal, use the token and name the shift in the reply, such as `14px` to `--space-16`.","- Apply a `tokens-breaking-migration` finding with `npx live-tokens migrate`, after `npx live-tokens migrate --check` prints the plan. `--tokens <path>` names a tokens.css in an unusual place, and `--write` also rewrites the route references the plan lists. Name each renamed design token in the reply.","- Any finding, at any repair level, can stay as a deliberate exception when the user chooses to keep it. Record that decision in the config entry its `exception` field names, and prefer the narrower entry. When the user has chosen to lower a rule's severity, record it in `live-tokens.config.json` under `\"checks\": { \"rules\": { \"<rule>\": \"warn\" } }`. `--off=<rule>` silences a rule for one run only."],
|
|
9
9
|
},
|
|
10
10
|
"create-component": {
|
|
11
11
|
"SKILL.md": ["---","name: live-tokens-create-component","description: Create an editable component for a @motion-proto/live-tokens project. A component is a runtime Svelte file, an editor Svelte file, and one registration. The runtime file declares one semantic property per editable CSS property and assigns each an existing design token. The property names are semantic, based on function, and reuse the names of the existing components. Use when live-tokens-pick-component finds no suitable component, or the user asks for a new component. Use when the user asks to make an existing Svelte component editable in the live-tokens editor. For page integration, read live-tokens-create-page.","---","","# Creating a component for a live-tokens project","","Create a component whose structure and behavior serve the user's purpose. Give each editable visual property a semantic name. Assign its default from the existing design tokens. Deliver the runtime file, the editor file, and the registration together.","","## Workflow","","1. Read the project: `package.json`, `live-tokens.config.json`, `src/main.ts`, the catalogue, the token scales the component will use, a shipped runtime and editor pair, and the property suffixes.","2. Design the properties: separate the component's parts, variants, and states, then write one row per editable role with its token and the CSS it controls, named the way the shipped components name the same role.","3. Write the runtime file: the catalogue export and the `:global(:root)` block. A structural choice is an intrinsic. Every component joins the sketch layer, and a fixed overlay portals to `<body>`.","4. Write the editor file: the schema, the preview props, and the markup. Variants that share a value are linked.","5. Register the component in the module `src/main.ts` and `live-tokens.testing.ts` both name, and write its contract in the module `contractsModule` names.","6. Run `npx live-tokens check-component <id> --tests --strict --json` until it exits 0 with complete applicable coverage, then the Svelte check and the build.","7. Reply with the files, the id, the props, and each check's result. Then place the component on a page with **live-tokens-create-page**.","","## Design model","","A live-tokens project has two layers.","","| Layer | Responsibility | Example |","|---|---|---|","| Design tokens | Name the available colors, typography, geometry, and motion values. A theme sets the values. | `--space-16`, `--radius-md`, `--surface-neutral` |","| Semantic properties | Name the visual roles within a component and reference tokens. A component config records these assignments. | `--statcard-padding: var(--space-16)` |","","A token is assigned to a property, and a CSS declaration reads the property. The editor changes the assignment; the runtime reads the property. Keep the assignment a token reference, so a theme change reaches the component.","","A property describes its purpose: `--statcard-value`, `--statcard-radius`, `--statcard-label-font-size`. Its name stays stable when its assigned color or size changes. Use role names such as `surface` and `text`. Use full words for component ids and parts.","","A component is distinct in its anatomy, its proportions, its content hierarchy, and its behavior. Its appearance comes from the existing tokens. Create only the variants and states the task requires. The tokens stay as they are; a new token is a separate change to the design system.","","Props carry content and behavior: a value, a label, a callback. Properties carry the editable appearance. When a variant is a choice the page makes, expose it as a prop.","","Prop names follow the shipped components. `label` names a control, `title` heads content, `text` is body copy. `open` is the one prop for an open state. `value` holds a selection's id. A size prop's values are `default` and `small`. A callback prop is `on` followed by the event name, all lowercase, as in `onchange`, `onclose`, and `onsave`.","","## Source inspection","","Before writing a file:","","1. Read the project's `package.json`, `live-tokens.config.json`, and `src/main.ts`.","2. Run `npx live-tokens components`. The list holds every component the project has, with its variants and catalogue entry. `npx live-tokens components <id>` prints one component's props.","3. Run `npx live-tokens tokens --scale <name>` for each token scale the component will use. Those names are the tokens a property can reference.","4. Read a shipped runtime and editor pair: `Toggle` for interaction states, `Badge` for variants and linked values, `Card` for text and container parts.","5. Read `references/token-naming.md` for the suffixes that select editor controls.","","The shipped sources are in `node_modules/@motion-proto/live-tokens/src/`: the runtime at `system/components/<Name>.svelte`, the editor at `editor/component-editor/<Name>Editor.svelte`. Inside the live-tokens repository, read them from the repository root. The source is the contract.","","## Variants and states","","A component has three kinds of division. Keep them apart in the props, the names, and the editor.","","| Kind | Meaning | Example |","|---|---|---|","| Part | Regions present at once | Dialog's overlay, header, body, footer |","| Variant | Alternative presentations the page chooses | Badge's brand, danger |","| State | A runtime condition | Toggle's on, hover, disabled |","","States have two axes. A component state is one of a set that excludes the others: default, selected (or on), disabled. An interaction state layers on a component state: default, hover, and later focus or active. `disabled` is terminal: no other state layers on it, in the names or in the editor.","","The default state carries the shared geometry and typography. A state adds properties only for the values that change: `--toggle-on-track-surface`, `--toggle-on-hover-track-surface`.","","The preview renders the state being edited. Pair each `:hover` selector with a `.force-hover` selector and expose a `class` prop, so the editor shows hover without a pointer. Keep native disabled behavior, keyboard operation, and visible focus on an interactive control.","","A component supplies its variants. The page chooses the one primary action.","","## Property design","","Before writing a file, identify the component's parts, text roles, variants, and states. Then write a property map: one row per editable role, with the token it is assigned and the CSS property it controls. Keep separate roles independent even when they start with the same value.","","| Property | Assigned token | CSS use |","|---|---|---|","| `--statcard-surface` | `--surface-neutral` | `background` |","| `--statcard-border` | `--border-neutral` | `border-color` |","| `--statcard-border-width` | `--border-width-1` | `border-width` |","| `--statcard-radius` | `--radius-md` | `border-radius` |","| `--statcard-padding` | `--space-16` | `padding` |","| `--statcard-value` | `--text-primary` | `color` of the value |","| `--statcard-value-font-size` | `--font-size-2xl` | `font-size` of the value |","| `--statcard-label` | `--text-secondary` | `color` of the label |","","Assign from the tokens the project has. Match the token scale to the role: `--surface-*` for a fill, `--border-*` for an outline, `--text-*` for text, and the space, radius, border-width, and icon-size scales for geometry. Give each text role five properties: `-font-family`, `-font-size`, `-font-weight`, `-line-height`, and `-letter-spacing`.","","A property name starts with the component id and ends with the property suffix. Use this shape for part-specific states:","","```text","--<componentId>[-<variant>][-<part>][-<state>]-<property>","```","","- `componentId` is the runtime file name in lowercase with no dashes: `StatCard.svelte` is `statcard`.","- `variant` is present when the component has more than one: `--card-default-surface`, `--card-bare-surface`. A component with one variant has no variant segment: `--toggle-track-surface`.","- `part` names a region inside the component: `header`, `body`, `track`, `thumb`. The editor's `element` tag groups rows in the panel and is never a name segment.","- `state` comes before the property: `--card-hover-border`. `disabled` is terminal, so no name pairs `disabled` with another state.","- `property` is the suffix, and the suffix selects the editor control: `-surface` for a fill, `-border` for a border color, `-border-width` for a stroke, `-radius` for corners, `-padding` and `-gap` for spacing, and the five typography suffixes. `references/token-naming.md` lists every suffix.","","For a state that affects several parts, follow Toggle: `--toggle-on-hover-track-surface`. State segments precede the affected part.","","Name a role as the shipped component that paints the same thing names it. A fill is `-surface` in every shipped component. A knob is `-thumb`. A text role's color sits on the role's own name, `-title`, `-body`, `-label`, `-value`, and its typography hangs off that name: `--card-default-title-font-size`. A component with one text role uses `-text`: `--badge-brand-text`.","","## Runtime component","","Create `src/system/components/StatCard.svelte`. `check-component` finds a runtime there only. A component in another directory is listed by `components` and `report` when that directory is named in `\"componentDirs\"` in `live-tokens.config.json`, and `check-component` does not check it. Use Svelte 5 props and snippets, semantic HTML, and the behavior the task requires.","","Open the file with a `<script module lang=\"ts\">` block that exports a `catalogue` entry in the shape every shipped component carries. `npx live-tokens components` prints it beside the id, and `components <id>` prints it with the props. Each field is `key: <string literal>`, in single, double, or backtick quotes; no `${}` interpolation, no concatenation, no identifier reference. An optional `props` map adds one line per prop whose values carry a choice, such as `variant`; each key names a prop the file declares, and the text says what the values mean.","","```svelte","<script module lang=\"ts\">"," import type { CatalogueEntry } from '@motion-proto/live-tokens';",""," export const catalogue = {"," description: 'A figure with its label.',"," useFor: 'one number the reader takes in at a glance.',"," notFor: 'a set of records (Table); a titled block of content (Card).',"," } satisfies CatalogueEntry;","</script>","```","","Declare every editable property in a literal `:global(:root)` block, each assigned a token. The plugin parses the Svelte source to seed `component-configs/<id>/default.json`, so the block holds plain declarations with no SCSS loop or interpolation.","","```svelte","<style>"," :global(:root) {"," --statcard-surface: var(--surface-neutral);"," --statcard-border: var(--border-neutral);"," --statcard-border-width: var(--border-width-1);"," --statcard-radius: var(--radius-md);"," --statcard-padding: var(--space-16);"," --statcard-value: var(--text-primary);"," --statcard-value-font-size: var(--font-size-2xl);"," --statcard-label: var(--text-secondary);"," }",""," .statcard {"," display: grid;"," background: var(--statcard-surface);"," border: var(--statcard-border-width) solid var(--statcard-border);"," border-radius: var(--statcard-radius);"," padding: var(--statcard-padding);"," }",""," .value { color: var(--statcard-value); font-size: var(--statcard-value-font-size); }"," .label { color: var(--statcard-label); }","</style>","```","","The excerpt shows the chain for part of the property map. Every editable value reads a property. Structural CSS (`display: grid`, `width: 100%`, `align-items: center`) stays in the layout rules. A value beyond a scale is a token expression: `calc(var(--space-64) * 4)`. A property that carries a structural choice, an alignment or a visibility, is an intrinsic: read `references/intrinsics.md`.","","## Component editor","","Create `src/system/components/StatCardEditor.svelte` beside the runtime file. The editor has three parts.","","1. A `<script module>` block exports `component`, the id, and `allTokens`, one row per property in the map. A row is `{ label, variable, element? }`; `element` groups rows in the panel by part, and `label` names the property in the row.","2. The instance script imports the runtime component and the editor primitives from the package's public paths, and maps the state being edited to preview props.","3. The markup mounts `ComponentEditorBase` with one `VariantGroup` per variant, each rendering a preview.","","```svelte","<script module lang=\"ts\">"," import type { Token } from '@motion-proto/live-tokens/component-editor';",""," export const component = 'statcard';"," const states: Record<string, Token[]> = {"," default: ["," { label: 'surface', element: 'frame', variable: '--statcard-surface' },"," { label: 'border', element: 'frame', variable: '--statcard-border' },"," { label: 'border width', element: 'frame', variable: '--statcard-border-width' },"," { label: 'radius', element: 'frame', variable: '--statcard-radius' },"," { label: 'padding', element: 'frame', variable: '--statcard-padding' },"," { label: 'text', element: 'value', variable: '--statcard-value' },"," { label: 'font size', element: 'value', variable: '--statcard-value-font-size' },"," { label: 'text', element: 'label', variable: '--statcard-label' },"," ],"," };"," export const allTokens: Token[] = Object.values(states).flat();","</script>","","<script lang=\"ts\">"," import { ComponentEditorBase, VariantGroup } from '@motion-proto/live-tokens/component-editor';"," import StatCard from './StatCard.svelte';","</script>","","<ComponentEditorBase {component} title=\"Stat Card\" tokens={allTokens}>"," <VariantGroup name=\"statcard\" title=\"Stat Card\" {states} {component}>"," <StatCard value=\"1,204\" label=\"Sessions\" />"," </VariantGroup>","</ComponentEditorBase>","```","","The shipped editor for the closest component gives the preview snippet for a component with states. Custom chrome in an editor takes `--ui-*` tokens and no accent color; its copy uses periods and commas, never em-dashes.","","When variants share a value, read `references/linked-siblings.md`. A `groupKey` is scoped to the text role: `value-font-size` and `label-font-size` stay separate keys. A `buildTypeGroup*` helper takes `{ component, variants }` so it derives one key per role.","","## Registration","","Register the component in `src/registerComponents.ts`, a registration-only module, beside any registration already there. The id is unique; a registration that repeats a shipped id replaces that component.","","```ts","// src/registerComponents.ts","import { registerComponent } from '@motion-proto/live-tokens';","import { catalogue } from './system/components/StatCard.svelte';","import StatCardEditor, { allTokens as statCardTokens } from './system/components/StatCardEditor.svelte';","","registerComponent({"," id: 'statcard',"," label: 'Stat Card',"," icon: 'fas fa-chart-simple',"," sourceFile: 'src/system/components/StatCard.svelte',"," editorComponent: StatCardEditor,"," schema: statCardTokens,"," catalogue,","});","```","","Import the module from `src/main.ts`, before `bootLiveTokens` or `mount`, and name it as `registrySetup` in `live-tokens.testing.ts`. One module then serves the running app and `check-component --tests`, which imports it to see the registration without mounting the app. The component's contract, one `ComponentContract` in the module `contractsModule` names, is what the eight component suites run; without it `--tests` reports `contract-missing`. Read `references/contract-tests.md` for both. A component that declares intrinsics adds `intrinsics` to the entry. `check-component` finds the registration by the id literal inside the call.","","At boot the plugin reads the `:global(:root)` block and writes `component-configs/<id>/default.json`, one token per property. An edit in the editor writes `_working.json`; Save As writes a named config. The assignments stay token references through that flow.","","Inside the live-tokens repository, a first-party component keeps its editor in `src/editor/component-editor/` and takes an entry in `builtInRegistry` in `src/editor/component-editor/registry.ts`.","","## Sketch mode and overlays","","Every component joins the sketch layer: read `references/sketch-mode.md`. A suitable root or inner wrapper carries a reserved class and names five `--sketch-*` values from its own properties. Preserve positioning, clipping, and pseudo-elements as the reference specifies. A first-party component adds a `PartSpec` row instead.","","A fixed overlay portals to `<body>`: read `references/fixed-overlays.md`. A container that owns the typography of its content follows `Card` and its `prose` prop.","","## Verification","","1. Run `npx live-tokens check-component <id> --tests --strict --json`. Inside the live-tokens repository, run `node bin/cli.mjs check-component <id> --tests --strict --json`. It applies every `auto` repair, runs the registry contract and, for a component with a contract in the module `contractsModule` names, the component contract suites, and returns the fixes it applied, the findings that remain, and coverage by rule. `--off=<rule>` silences a rule for one run, and `--no-fix` reports without editing.","2. Each remaining finding carries a rule id, a line, and its `guidance`. Make each repair from its guidance, and run the command again until it exits 0 with complete applicable coverage and no disabled checks.","3. Run the project's Svelte check and its build.","4. Reply with the files, the component id, the props, and the result of each check, naming any check the environment prevented.","","`--tests` covers every line a reviewer once checked by eye: the component's listing, its controls and preview, persistence and reset, theme projection, linked properties, and Sketch mode.","","Then place the component on a page with **live-tokens-create-page**."],
|