@kinetixui/cli 0.14.0 → 0.16.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/dist/index.js +455 -21
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import
|
|
5
|
+
import pc9 from "picocolors";
|
|
6
6
|
|
|
7
7
|
// src/commands/add.ts
|
|
8
8
|
import { existsSync as existsSync4 } from "fs";
|
|
@@ -248,11 +248,11 @@ async function doctor() {
|
|
|
248
248
|
levels.push(report("warn", `"${alias}" alias points to ${path5.relative(cwd, dir) || "."}, which doesn't exist yet.`));
|
|
249
249
|
}
|
|
250
250
|
}
|
|
251
|
-
const
|
|
252
|
-
if (existsSync5(
|
|
253
|
-
levels.push(report("pass", `Tailwind CSS file found at ${path5.relative(cwd,
|
|
251
|
+
const cssPath2 = path5.join(cwd, config.srcDir ? "src" : "", config.tailwind.css);
|
|
252
|
+
if (existsSync5(cssPath2)) {
|
|
253
|
+
levels.push(report("pass", `Tailwind CSS file found at ${path5.relative(cwd, cssPath2)}`));
|
|
254
254
|
} else {
|
|
255
|
-
levels.push(report("warn", `Tailwind CSS file not found at ${path5.relative(cwd,
|
|
255
|
+
levels.push(report("warn", `Tailwind CSS file not found at ${path5.relative(cwd, cssPath2)} \u2014 the token contract has nowhere to land.`));
|
|
256
256
|
}
|
|
257
257
|
}
|
|
258
258
|
let registryNames = null;
|
|
@@ -412,24 +412,183 @@ async function inspect(name, options) {
|
|
|
412
412
|
console.log(`Add it: ${pc4.cyan(`npx @kinetixui/cli add ${item.name}`)}`);
|
|
413
413
|
}
|
|
414
414
|
|
|
415
|
-
// src/commands/
|
|
415
|
+
// src/commands/lint.ts
|
|
416
|
+
import { existsSync as existsSync8 } from "fs";
|
|
417
|
+
import { readdir as readdir2, readFile as readFile2 } from "fs/promises";
|
|
418
|
+
import path8 from "path";
|
|
416
419
|
import pc5 from "picocolors";
|
|
420
|
+
|
|
421
|
+
// src/lib/lint.ts
|
|
422
|
+
function stripComments(text) {
|
|
423
|
+
const noBlocks = text.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " "));
|
|
424
|
+
return noBlocks.replace(/(^|[^:])\/\/.*$/gm, (_m, pre) => pre);
|
|
425
|
+
}
|
|
426
|
+
var HEX = "(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{4}|[0-9a-fA-F]{3})";
|
|
427
|
+
var COLOR_UTILITY_PREFIXES = [
|
|
428
|
+
"bg",
|
|
429
|
+
"text",
|
|
430
|
+
"border",
|
|
431
|
+
"fill",
|
|
432
|
+
"stroke",
|
|
433
|
+
"ring",
|
|
434
|
+
"from",
|
|
435
|
+
"via",
|
|
436
|
+
"to",
|
|
437
|
+
"outline",
|
|
438
|
+
"decoration",
|
|
439
|
+
"caret",
|
|
440
|
+
"accent",
|
|
441
|
+
"shadow",
|
|
442
|
+
"divide"
|
|
443
|
+
];
|
|
444
|
+
var COLOR_PATTERNS = [
|
|
445
|
+
// bg-[#1a2b3c], text-[#fff], ring-[#1a2b3c80] …
|
|
446
|
+
new RegExp(`\\b(?:${COLOR_UTILITY_PREFIXES.join("|")})-\\[#${HEX}\\]`, "g"),
|
|
447
|
+
// style={{ color: "#1a2b3c" }} / a plain CSS declaration `color: #1a2b3c;`
|
|
448
|
+
new RegExp(`:\\s*["']?#${HEX}["']?`, "g")
|
|
449
|
+
];
|
|
450
|
+
var SPACING_UTILITY_PATTERN = "-?(?:p|m)[trblxy]?|gap(?:-[xy])?|space-[xy]|inset(?:-[xy])?|top|right|bottom|left";
|
|
451
|
+
var SPACING_VALUE = "-?[\\d.]+(?:px|rem|em|%)?";
|
|
452
|
+
var SPACING_PATTERNS = [
|
|
453
|
+
new RegExp(`\\b(?:${SPACING_UTILITY_PATTERN})-\\[${SPACING_VALUE}\\]`, "g")
|
|
454
|
+
];
|
|
455
|
+
function lineOf(text, index) {
|
|
456
|
+
let line = 1;
|
|
457
|
+
for (let i = 0; i < index; i++) if (text.charCodeAt(i) === 10) line++;
|
|
458
|
+
return line;
|
|
459
|
+
}
|
|
460
|
+
function snippetAround(text, index, length) {
|
|
461
|
+
const lineStart = text.lastIndexOf("\n", index) + 1;
|
|
462
|
+
let lineEnd = text.indexOf("\n", index);
|
|
463
|
+
if (lineEnd === -1) lineEnd = text.length;
|
|
464
|
+
return text.slice(lineStart, lineEnd).trim().slice(0, 120) || text.slice(index, index + length);
|
|
465
|
+
}
|
|
466
|
+
function lintText(file, rawText) {
|
|
467
|
+
const text = stripComments(rawText);
|
|
468
|
+
const violations = [];
|
|
469
|
+
const seen = /* @__PURE__ */ new Set();
|
|
470
|
+
const scan = (patterns, kind) => {
|
|
471
|
+
for (const pattern of patterns) {
|
|
472
|
+
pattern.lastIndex = 0;
|
|
473
|
+
let match;
|
|
474
|
+
while (match = pattern.exec(text)) {
|
|
475
|
+
const line = lineOf(text, match.index);
|
|
476
|
+
const key = `${kind}:${line}:${match.index}`;
|
|
477
|
+
if (!seen.has(key)) {
|
|
478
|
+
seen.add(key);
|
|
479
|
+
violations.push({ file, line, kind, snippet: snippetAround(text, match.index, match[0].length) });
|
|
480
|
+
}
|
|
481
|
+
if (match[0].length === 0) pattern.lastIndex++;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
scan(COLOR_PATTERNS, "color");
|
|
486
|
+
scan(SPACING_PATTERNS, "spacing");
|
|
487
|
+
violations.sort((a, b) => a.line - b.line);
|
|
488
|
+
return violations;
|
|
489
|
+
}
|
|
490
|
+
var LINT_EXTENSIONS = [".tsx", ".jsx", ".ts", ".js", ".css"];
|
|
491
|
+
|
|
492
|
+
// src/commands/lint.ts
|
|
493
|
+
var SUGGESTION = {
|
|
494
|
+
color: "consider a semantic token utility instead (e.g. bg-primary, text-foreground)",
|
|
495
|
+
spacing: "consider a token-backed spacing utility instead (e.g. p-4, gap-2)"
|
|
496
|
+
};
|
|
497
|
+
async function collectFiles(dir) {
|
|
498
|
+
if (!existsSync8(dir)) return [];
|
|
499
|
+
const files = [];
|
|
500
|
+
const walk = async (current) => {
|
|
501
|
+
const entries = await readdir2(current, { withFileTypes: true });
|
|
502
|
+
for (const entry of entries) {
|
|
503
|
+
const full = path8.join(current, entry.name);
|
|
504
|
+
if (entry.isDirectory()) {
|
|
505
|
+
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
|
|
506
|
+
await walk(full);
|
|
507
|
+
} else if (entry.isFile() && LINT_EXTENSIONS.includes(path8.extname(entry.name))) {
|
|
508
|
+
files.push(full);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
await walk(dir);
|
|
513
|
+
return files;
|
|
514
|
+
}
|
|
515
|
+
async function lint(targetArg, options) {
|
|
516
|
+
const cwd = process.cwd();
|
|
517
|
+
let dirs;
|
|
518
|
+
if (targetArg) {
|
|
519
|
+
dirs = [path8.resolve(cwd, targetArg)];
|
|
520
|
+
} else {
|
|
521
|
+
if (!existsSync8(path8.join(cwd, CONFIG_FILE))) {
|
|
522
|
+
throw new Error(`No ${CONFIG_FILE} found \u2014 run "kinetixui init" first, or pass a path: kinetixui lint <path>`);
|
|
523
|
+
}
|
|
524
|
+
const config = await readConfig(cwd);
|
|
525
|
+
if (!config) throw new Error(`${CONFIG_FILE} exists but isn't valid JSON.`);
|
|
526
|
+
dirs = [resolveAliasDir(cwd, config, "components"), resolveAliasDir(cwd, config, "ui")];
|
|
527
|
+
}
|
|
528
|
+
const seenFiles = /* @__PURE__ */ new Set();
|
|
529
|
+
const files = [];
|
|
530
|
+
for (const dir of dirs) {
|
|
531
|
+
for (const file of await collectFiles(dir)) {
|
|
532
|
+
if (!seenFiles.has(file)) {
|
|
533
|
+
seenFiles.add(file);
|
|
534
|
+
files.push(file);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
if (files.length === 0) {
|
|
539
|
+
console.log(pc5.dim(`No ${LINT_EXTENSIONS.join("/")} files found in ${dirs.map((d) => path8.relative(cwd, d) || ".").join(", ")}.`));
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
let totalViolations = 0;
|
|
543
|
+
let filesWithViolations = 0;
|
|
544
|
+
for (const file of files) {
|
|
545
|
+
const text = await readFile2(file, "utf8");
|
|
546
|
+
const violations = lintText(file, text);
|
|
547
|
+
if (violations.length === 0) continue;
|
|
548
|
+
filesWithViolations++;
|
|
549
|
+
const rel = path8.relative(cwd, file);
|
|
550
|
+
for (const v of violations) {
|
|
551
|
+
totalViolations++;
|
|
552
|
+
console.log(
|
|
553
|
+
`${pc5.yellow("!")} ${pc5.bold(`${rel}:${v.line}`)} \u2014 hardcoded ${v.kind}: ${pc5.dim(v.snippet)}`
|
|
554
|
+
);
|
|
555
|
+
console.log(` ${pc5.dim(SUGGESTION[v.kind])}`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
console.log();
|
|
559
|
+
if (totalViolations === 0) {
|
|
560
|
+
console.log(pc5.green("\u2714"), `${files.length} file${files.length === 1 ? "" : "s"} scanned, no hardcoded colors or spacing found.`);
|
|
561
|
+
} else {
|
|
562
|
+
console.log(
|
|
563
|
+
`${totalViolations} violation${totalViolations === 1 ? "" : "s"} in ${filesWithViolations} of ${files.length} file${files.length === 1 ? "" : "s"} scanned.`
|
|
564
|
+
);
|
|
565
|
+
console.log(
|
|
566
|
+
pc5.dim(
|
|
567
|
+
"Checks unknown/deprecated tokens, accessibility violations and cross-platform inconsistencies aren't covered yet \u2014 see https://kinetixui.com/docs/cli#lint."
|
|
568
|
+
)
|
|
569
|
+
);
|
|
570
|
+
if (options.fail) process.exitCode = 1;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// src/commands/list.ts
|
|
575
|
+
import pc6 from "picocolors";
|
|
417
576
|
async function list(options) {
|
|
418
577
|
const items = await fetchRegistryIndex(options.registry);
|
|
419
578
|
const components = items.filter((item) => item.type === "registry:ui").sort((a, b) => a.name.localeCompare(b.name));
|
|
420
579
|
console.log(`${components.length} component(s):`);
|
|
421
580
|
console.log();
|
|
422
581
|
for (const item of components) {
|
|
423
|
-
const description = item.description ?
|
|
424
|
-
console.log(` ${
|
|
582
|
+
const description = item.description ? pc6.dim(` \u2014 ${item.description}`) : "";
|
|
583
|
+
console.log(` ${pc6.cyan(item.name)}${description}`);
|
|
425
584
|
}
|
|
426
585
|
console.log();
|
|
427
|
-
console.log(`Add one: ${
|
|
428
|
-
console.log(`Add everything: ${
|
|
586
|
+
console.log(`Add one: ${pc6.cyan("npx @kinetixui/cli add <name>")}`);
|
|
587
|
+
console.log(`Add everything: ${pc6.cyan("npx @kinetixui/cli add --all")}`);
|
|
429
588
|
}
|
|
430
589
|
|
|
431
590
|
// src/commands/parity.ts
|
|
432
|
-
import
|
|
591
|
+
import pc7 from "picocolors";
|
|
433
592
|
var PLATFORMS = ["React", "SwiftUI", "Compose", "Flutter"];
|
|
434
593
|
var PLATFORM_ABBR = {
|
|
435
594
|
React: "RE",
|
|
@@ -447,21 +606,271 @@ async function parity(names, options) {
|
|
|
447
606
|
}) : components;
|
|
448
607
|
const nameWidth = Math.max(9, ...rows.map((r) => r.name.length));
|
|
449
608
|
const header = `${"COMPONENT".padEnd(nameWidth)} ${PLATFORMS.map((p) => PLATFORM_ABBR[p]).join(" ")} STATUS`;
|
|
450
|
-
console.log(
|
|
609
|
+
console.log(pc7.dim(header));
|
|
451
610
|
for (const item of rows) {
|
|
452
611
|
const on = new Set(item.platforms ?? PLATFORMS);
|
|
453
|
-
const marks = PLATFORMS.map((p) => on.has(p) ?
|
|
454
|
-
const status = item.status ?
|
|
612
|
+
const marks = PLATFORMS.map((p) => on.has(p) ? pc7.green("\u2713 ") : pc7.dim("\u2014 "));
|
|
613
|
+
const status = item.status ? pc7.yellow(item.status) : "";
|
|
455
614
|
console.log(`${item.name.padEnd(nameWidth)} ${marks.join(" ")} ${status}`);
|
|
456
615
|
}
|
|
457
616
|
console.log();
|
|
458
617
|
console.log(`${rows.length} of ${components.length} component(s) shown.`);
|
|
459
618
|
}
|
|
460
619
|
|
|
620
|
+
// src/commands/theme.ts
|
|
621
|
+
import { existsSync as existsSync9 } from "fs";
|
|
622
|
+
import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
623
|
+
import path9 from "path";
|
|
624
|
+
import pc8 from "picocolors";
|
|
625
|
+
|
|
626
|
+
// src/lib/color-math.ts
|
|
627
|
+
function hexToRgb(hex) {
|
|
628
|
+
const h = hex.replace("#", "");
|
|
629
|
+
const n = parseInt(h.length === 3 ? h.replace(/(.)/g, "$1$1") : h, 16);
|
|
630
|
+
return [n >> 16 & 255, n >> 8 & 255, n & 255];
|
|
631
|
+
}
|
|
632
|
+
function rgbToHsl(r, g, b) {
|
|
633
|
+
r /= 255, g /= 255, b /= 255;
|
|
634
|
+
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
|
635
|
+
let h = 0, s = 0;
|
|
636
|
+
const l = (max + min) / 2;
|
|
637
|
+
const d = max - min;
|
|
638
|
+
if (d !== 0) {
|
|
639
|
+
s = d / (1 - Math.abs(2 * l - 1));
|
|
640
|
+
switch (max) {
|
|
641
|
+
case r:
|
|
642
|
+
h = (g - b) / d % 6;
|
|
643
|
+
break;
|
|
644
|
+
case g:
|
|
645
|
+
h = (b - r) / d + 2;
|
|
646
|
+
break;
|
|
647
|
+
default:
|
|
648
|
+
h = (r - g) / d + 4;
|
|
649
|
+
}
|
|
650
|
+
h *= 60;
|
|
651
|
+
if (h < 0) h += 360;
|
|
652
|
+
}
|
|
653
|
+
return [Math.round(h), Math.round(s * 100), Math.round(l * 100)];
|
|
654
|
+
}
|
|
655
|
+
function hexToHslChannels(hex) {
|
|
656
|
+
const [h, s, l] = rgbToHsl(...hexToRgb(hex));
|
|
657
|
+
return `${h} ${s}% ${l}%`;
|
|
658
|
+
}
|
|
659
|
+
function relLuminance([r, g, b]) {
|
|
660
|
+
const [cr, cg, cb] = [r, g, b].map((v) => {
|
|
661
|
+
const s = v / 255;
|
|
662
|
+
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
663
|
+
});
|
|
664
|
+
return 0.2126 * cr + 0.7152 * cg + 0.0722 * cb;
|
|
665
|
+
}
|
|
666
|
+
function contrastRatio(hexA, hexB) {
|
|
667
|
+
const la = relLuminance(hexToRgb(hexA)) + 0.05;
|
|
668
|
+
const lb = relLuminance(hexToRgb(hexB)) + 0.05;
|
|
669
|
+
return la > lb ? la / lb : lb / la;
|
|
670
|
+
}
|
|
671
|
+
function bestTextHex(bgHex) {
|
|
672
|
+
return contrastRatio(bgHex, "#ffffff") >= contrastRatio(bgHex, "#000000") ? "#ffffff" : "#000000";
|
|
673
|
+
}
|
|
674
|
+
function isHex(v) {
|
|
675
|
+
return /^#?[0-9a-fA-F]{6}$/.test(v.trim());
|
|
676
|
+
}
|
|
677
|
+
function normalizeHex(v) {
|
|
678
|
+
const t = v.trim().replace(/^#/, "");
|
|
679
|
+
return `#${t.toLowerCase()}`;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// src/lib/theme.ts
|
|
683
|
+
var ACCEPTED_TOKENS = [
|
|
684
|
+
"background",
|
|
685
|
+
"foreground",
|
|
686
|
+
"card",
|
|
687
|
+
"card-foreground",
|
|
688
|
+
"popover",
|
|
689
|
+
"popover-foreground",
|
|
690
|
+
"primary",
|
|
691
|
+
"primary-foreground",
|
|
692
|
+
"secondary",
|
|
693
|
+
"secondary-foreground",
|
|
694
|
+
"muted",
|
|
695
|
+
"muted-foreground",
|
|
696
|
+
"accent",
|
|
697
|
+
"accent-foreground",
|
|
698
|
+
"destructive",
|
|
699
|
+
"destructive-foreground",
|
|
700
|
+
"border",
|
|
701
|
+
"input",
|
|
702
|
+
"ring"
|
|
703
|
+
];
|
|
704
|
+
var EXAMPLE_VALUES = {
|
|
705
|
+
background: "#ffffff",
|
|
706
|
+
foreground: "#0f172a",
|
|
707
|
+
card: "#ffffff",
|
|
708
|
+
"card-foreground": "#0f172a",
|
|
709
|
+
popover: "#ffffff",
|
|
710
|
+
"popover-foreground": "#0f172a",
|
|
711
|
+
primary: "#2563eb",
|
|
712
|
+
"primary-foreground": "#ffffff",
|
|
713
|
+
secondary: "#f1f5f9",
|
|
714
|
+
"secondary-foreground": "#0f172a",
|
|
715
|
+
muted: "#f1f5f9",
|
|
716
|
+
"muted-foreground": "#64748b",
|
|
717
|
+
accent: "#f1f5f9",
|
|
718
|
+
"accent-foreground": "#0f172a",
|
|
719
|
+
destructive: "#dc2626",
|
|
720
|
+
"destructive-foreground": "#ffffff",
|
|
721
|
+
border: "#e2e8f0",
|
|
722
|
+
input: "#e2e8f0",
|
|
723
|
+
ring: "#2563eb"
|
|
724
|
+
};
|
|
725
|
+
var FOREGROUND_OF = {
|
|
726
|
+
background: "foreground",
|
|
727
|
+
card: "card-foreground",
|
|
728
|
+
popover: "popover-foreground",
|
|
729
|
+
primary: "primary-foreground",
|
|
730
|
+
secondary: "secondary-foreground",
|
|
731
|
+
muted: "muted-foreground",
|
|
732
|
+
accent: "accent-foreground",
|
|
733
|
+
destructive: "destructive-foreground"
|
|
734
|
+
};
|
|
735
|
+
var CONTRAST_PAIRS = [
|
|
736
|
+
["background", "foreground"],
|
|
737
|
+
["card", "card-foreground"],
|
|
738
|
+
["popover", "popover-foreground"],
|
|
739
|
+
["primary", "primary-foreground"],
|
|
740
|
+
["secondary", "secondary-foreground"],
|
|
741
|
+
["muted", "muted-foreground"],
|
|
742
|
+
["accent", "accent-foreground"],
|
|
743
|
+
["destructive", "destructive-foreground"]
|
|
744
|
+
];
|
|
745
|
+
var isAccepted = (t) => ACCEPTED_TOKENS.includes(t);
|
|
746
|
+
function parseThemeFile(text) {
|
|
747
|
+
const values = {};
|
|
748
|
+
const errors = [];
|
|
749
|
+
const unknownTokens = [];
|
|
750
|
+
const lines = text.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
|
|
751
|
+
lines.forEach((line, i) => {
|
|
752
|
+
const cells = line.split(/[,\t]/).map((c) => c.trim().replace(/^"|"$/g, ""));
|
|
753
|
+
if (cells.length < 2) {
|
|
754
|
+
errors.push(`Line ${i + 1}: expected "token,hex" \u2014 got "${line}"`);
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
const [rawToken, rawHex] = cells;
|
|
758
|
+
const token = rawToken.toLowerCase().replace(/^--/, "").replace(/\s+/g, "-");
|
|
759
|
+
if (!isHex(rawHex)) {
|
|
760
|
+
errors.push(`Line ${i + 1}: "${rawHex}" isn't a hex color`);
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
if (!isAccepted(token)) {
|
|
764
|
+
unknownTokens.push(rawToken);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
values[token] = normalizeHex(rawHex);
|
|
768
|
+
});
|
|
769
|
+
return { values, errors, unknownTokens };
|
|
770
|
+
}
|
|
771
|
+
function deriveForegrounds(values) {
|
|
772
|
+
const out = { ...values };
|
|
773
|
+
for (const [base, fg] of Object.entries(FOREGROUND_OF)) {
|
|
774
|
+
if (out[base] && !out[fg]) out[fg] = bestTextHex(out[base]);
|
|
775
|
+
}
|
|
776
|
+
return out;
|
|
777
|
+
}
|
|
778
|
+
function toCssBlock(values) {
|
|
779
|
+
const lines = ACCEPTED_TOKENS.filter((t) => values[t]).map((t) => ` --${t}: ${hexToHslChannels(values[t])};`);
|
|
780
|
+
return `:root {
|
|
781
|
+
${lines.join("\n")}
|
|
782
|
+
}
|
|
783
|
+
`;
|
|
784
|
+
}
|
|
785
|
+
function checkContrast(values) {
|
|
786
|
+
return CONTRAST_PAIRS.filter(([bg, fg]) => values[bg] && values[fg]).map(([bg, fg]) => {
|
|
787
|
+
const ratio = contrastRatio(values[bg], values[fg]);
|
|
788
|
+
return { pair: [bg, fg], ratio, pass: ratio >= 4.5 };
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
function scaffoldThemeFile(name) {
|
|
792
|
+
const lines = [
|
|
793
|
+
`# ${name} \u2014 KinetixUI theme override`,
|
|
794
|
+
"#",
|
|
795
|
+
"# One `token,hex` pair per line. Uncomment (remove the leading `# `) and",
|
|
796
|
+
"# edit any token you want to override; leave the rest commented out to",
|
|
797
|
+
"# keep KinetixUI's default. Run `kinetixui theme build " + name + "` to",
|
|
798
|
+
`# compile this into ${name}.css and get a WCAG AA contrast report.`,
|
|
799
|
+
"#",
|
|
800
|
+
"# Setting a base color (e.g. `primary`) without its `-foreground` pair",
|
|
801
|
+
"# auto-derives the foreground by contrast (best of white/black) \u2014 you",
|
|
802
|
+
"# don't have to set both unless you want a specific foreground.",
|
|
803
|
+
"#"
|
|
804
|
+
];
|
|
805
|
+
for (const token of ACCEPTED_TOKENS) {
|
|
806
|
+
lines.push(`# ${token},${EXAMPLE_VALUES[token]}`);
|
|
807
|
+
}
|
|
808
|
+
return lines.join("\n") + "\n";
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// src/commands/theme.ts
|
|
812
|
+
var THEME_DIR = "kinetixui-themes";
|
|
813
|
+
function csvPath(cwd, name) {
|
|
814
|
+
return path9.join(cwd, THEME_DIR, `${name}.csv`);
|
|
815
|
+
}
|
|
816
|
+
function cssPath(cwd, name) {
|
|
817
|
+
return path9.join(cwd, THEME_DIR, `${name}.css`);
|
|
818
|
+
}
|
|
819
|
+
async function themeCreate(name, options) {
|
|
820
|
+
const cwd = process.cwd();
|
|
821
|
+
const file = csvPath(cwd, name);
|
|
822
|
+
if (existsSync9(file) && !options.force) {
|
|
823
|
+
throw new Error(`${path9.relative(cwd, file)} already exists. Pass --force to overwrite it.`);
|
|
824
|
+
}
|
|
825
|
+
await mkdir2(path9.dirname(file), { recursive: true });
|
|
826
|
+
await writeFile3(file, scaffoldThemeFile(name), "utf8");
|
|
827
|
+
console.log(pc8.green("\u2714"), `Wrote ${path9.relative(cwd, file)}`);
|
|
828
|
+
console.log(`Uncomment and edit the tokens you want to override, then run:`);
|
|
829
|
+
console.log(pc8.dim(` kinetixui theme build ${name}`));
|
|
830
|
+
}
|
|
831
|
+
async function themeBuild(name, options) {
|
|
832
|
+
const cwd = process.cwd();
|
|
833
|
+
const src = csvPath(cwd, name);
|
|
834
|
+
if (!existsSync9(src)) {
|
|
835
|
+
throw new Error(`${path9.relative(cwd, src)} doesn't exist. Run "kinetixui theme create ${name}" first.`);
|
|
836
|
+
}
|
|
837
|
+
const text = await readFile3(src, "utf8");
|
|
838
|
+
const { values: parsed, errors, unknownTokens } = parseThemeFile(text);
|
|
839
|
+
for (const err of errors) console.log(pc8.yellow("!"), err);
|
|
840
|
+
for (const unknown of unknownTokens) console.log(pc8.yellow("!"), `"${unknown}" isn't a known token \u2014 skipped.`);
|
|
841
|
+
const values = deriveForegrounds(parsed);
|
|
842
|
+
const setCount = Object.keys(values).length;
|
|
843
|
+
if (setCount === 0) {
|
|
844
|
+
throw new Error(`No tokens set in ${path9.relative(cwd, src)} \u2014 nothing to build. Uncomment at least one line.`);
|
|
845
|
+
}
|
|
846
|
+
const dest = cssPath(cwd, name);
|
|
847
|
+
await writeFile3(dest, toCssBlock(values), "utf8");
|
|
848
|
+
console.log(pc8.green("\u2714"), `Wrote ${path9.relative(cwd, dest)} (${setCount} token${setCount === 1 ? "" : "s"})`);
|
|
849
|
+
console.log(`Paste it after ${pc8.dim('@import "@kinetixui/tokens/css";')} in your global CSS.`);
|
|
850
|
+
console.log();
|
|
851
|
+
console.log(pc8.dim("WCAG AA contrast (4.5:1)"));
|
|
852
|
+
const results = checkContrast(values);
|
|
853
|
+
let failed = 0;
|
|
854
|
+
for (const { pair, ratio, pass } of results) {
|
|
855
|
+
const mark = pass ? pc8.green("\u2714") : pc8.red("\u2716");
|
|
856
|
+
if (!pass) failed++;
|
|
857
|
+
console.log(`${mark} ${pair[0]} / ${pair[1]}`, pc8.dim(`${ratio.toFixed(2)}:1`));
|
|
858
|
+
}
|
|
859
|
+
if (results.length === 0) {
|
|
860
|
+
console.log(pc8.dim("(no checkable pairs \u2014 set at least one base + foreground, or just a base to auto-derive one)"));
|
|
861
|
+
}
|
|
862
|
+
console.log();
|
|
863
|
+
console.log("Native (SwiftUI/Compose/Flutter) theme output isn't built yet \u2014 this compiles to CSS only.");
|
|
864
|
+
console.log(pc8.dim("See https://kinetixui.com/docs/cli#theme for the current scope."));
|
|
865
|
+
if (failed > 0 && options.failOnContrast) {
|
|
866
|
+
process.exitCode = 1;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
461
870
|
// package.json
|
|
462
871
|
var package_default = {
|
|
463
872
|
name: "@kinetixui/cli",
|
|
464
|
-
version: "0.
|
|
873
|
+
version: "0.16.0",
|
|
465
874
|
description: "Add KinetixUI components and tokens to your project.",
|
|
466
875
|
type: "module",
|
|
467
876
|
license: "MIT",
|
|
@@ -505,7 +914,7 @@ program.command("init").description("Set up kinetixui.json and pull in the token
|
|
|
505
914
|
try {
|
|
506
915
|
await init({ registry: opts.registry, yes: opts.yes });
|
|
507
916
|
} catch (err) {
|
|
508
|
-
console.error(
|
|
917
|
+
console.error(pc9.red("\u2716"), err.message);
|
|
509
918
|
process.exitCode = 1;
|
|
510
919
|
}
|
|
511
920
|
});
|
|
@@ -513,7 +922,7 @@ program.command("add").description("Add one or more components to your project."
|
|
|
513
922
|
try {
|
|
514
923
|
await add(components, { registry: opts.registry, overwrite: opts.overwrite, all: opts.all });
|
|
515
924
|
} catch (err) {
|
|
516
|
-
console.error(
|
|
925
|
+
console.error(pc9.red("\u2716"), err.message);
|
|
517
926
|
process.exitCode = 1;
|
|
518
927
|
}
|
|
519
928
|
});
|
|
@@ -521,7 +930,7 @@ program.command("list").description("List every component available in the regis
|
|
|
521
930
|
try {
|
|
522
931
|
await list({ registry: opts.registry });
|
|
523
932
|
} catch (err) {
|
|
524
|
-
console.error(
|
|
933
|
+
console.error(pc9.red("\u2716"), err.message);
|
|
525
934
|
process.exitCode = 1;
|
|
526
935
|
}
|
|
527
936
|
});
|
|
@@ -529,7 +938,7 @@ program.command("inspect").description("Show everything the registry knows about
|
|
|
529
938
|
try {
|
|
530
939
|
await inspect(name, { registry: opts.registry });
|
|
531
940
|
} catch (err) {
|
|
532
|
-
console.error(
|
|
941
|
+
console.error(pc9.red("\u2716"), err.message);
|
|
533
942
|
process.exitCode = 1;
|
|
534
943
|
}
|
|
535
944
|
});
|
|
@@ -537,7 +946,15 @@ program.command("parity").description("Show which platforms carry each component
|
|
|
537
946
|
try {
|
|
538
947
|
await parity(components, { registry: opts.registry });
|
|
539
948
|
} catch (err) {
|
|
540
|
-
console.error(
|
|
949
|
+
console.error(pc9.red("\u2716"), err.message);
|
|
950
|
+
process.exitCode = 1;
|
|
951
|
+
}
|
|
952
|
+
});
|
|
953
|
+
program.command("lint").description("Scan for hardcoded colors and spacing that should be semantic tokens.").argument("[path]", "directory to scan \u2014 defaults to the components/ui aliases from kinetixui.json").option("--no-fail", "exit 0 even if violations are found").action(async (target, opts) => {
|
|
954
|
+
try {
|
|
955
|
+
await lint(target, { fail: opts.fail });
|
|
956
|
+
} catch (err) {
|
|
957
|
+
console.error(pc9.red("\u2716"), err.message);
|
|
541
958
|
process.exitCode = 1;
|
|
542
959
|
}
|
|
543
960
|
});
|
|
@@ -545,7 +962,24 @@ program.command("doctor").description("Check kinetixui.json, its aliases, and th
|
|
|
545
962
|
try {
|
|
546
963
|
await doctor();
|
|
547
964
|
} catch (err) {
|
|
548
|
-
console.error(
|
|
965
|
+
console.error(pc9.red("\u2716"), err.message);
|
|
966
|
+
process.exitCode = 1;
|
|
967
|
+
}
|
|
968
|
+
});
|
|
969
|
+
var theme = program.command("theme").description("Scaffold and compile a local token override (CSS only \u2014 see 'theme build --help').");
|
|
970
|
+
theme.command("create").description("Scaffold kinetixui-themes/<name>.csv with every overridable token, commented out.").argument("<name>", "theme name, e.g. acme").option("-f, --force", "overwrite an existing theme file", false).action(async (name, opts) => {
|
|
971
|
+
try {
|
|
972
|
+
await themeCreate(name, { force: opts.force });
|
|
973
|
+
} catch (err) {
|
|
974
|
+
console.error(pc9.red("\u2716"), err.message);
|
|
975
|
+
process.exitCode = 1;
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
theme.command("build").description("Compile kinetixui-themes/<name>.csv to CSS and print a WCAG AA contrast report. CSS only \u2014 no native (SwiftUI/Compose/Flutter) output yet.").argument("<name>", "theme name, e.g. acme").option("--no-fail-on-contrast", "exit 0 even if a pair fails WCAG AA").action(async (name, opts) => {
|
|
979
|
+
try {
|
|
980
|
+
await themeBuild(name, { failOnContrast: opts.failOnContrast });
|
|
981
|
+
} catch (err) {
|
|
982
|
+
console.error(pc9.red("\u2716"), err.message);
|
|
549
983
|
process.exitCode = 1;
|
|
550
984
|
}
|
|
551
985
|
});
|