@kinetixui/cli 0.13.0 → 0.15.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 +316 -11
- 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 pc8 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;
|
|
@@ -428,10 +428,290 @@ async function list(options) {
|
|
|
428
428
|
console.log(`Add everything: ${pc5.cyan("npx @kinetixui/cli add --all")}`);
|
|
429
429
|
}
|
|
430
430
|
|
|
431
|
+
// src/commands/parity.ts
|
|
432
|
+
import pc6 from "picocolors";
|
|
433
|
+
var PLATFORMS = ["React", "SwiftUI", "Compose", "Flutter"];
|
|
434
|
+
var PLATFORM_ABBR = {
|
|
435
|
+
React: "RE",
|
|
436
|
+
SwiftUI: "SW",
|
|
437
|
+
Compose: "JC",
|
|
438
|
+
Flutter: "FL"
|
|
439
|
+
};
|
|
440
|
+
async function parity(names, options) {
|
|
441
|
+
const items = await fetchRegistryIndex(options.registry);
|
|
442
|
+
const components = items.filter((item) => item.type === "registry:ui").sort((a, b) => a.name.localeCompare(b.name));
|
|
443
|
+
const rows = names.length ? names.map((name) => {
|
|
444
|
+
const item = components.find((c) => c.name === name);
|
|
445
|
+
if (!item) throw new Error(`"${name}" isn't in the registry. Run "kinetixui list" to see what's available.`);
|
|
446
|
+
return item;
|
|
447
|
+
}) : components;
|
|
448
|
+
const nameWidth = Math.max(9, ...rows.map((r) => r.name.length));
|
|
449
|
+
const header = `${"COMPONENT".padEnd(nameWidth)} ${PLATFORMS.map((p) => PLATFORM_ABBR[p]).join(" ")} STATUS`;
|
|
450
|
+
console.log(pc6.dim(header));
|
|
451
|
+
for (const item of rows) {
|
|
452
|
+
const on = new Set(item.platforms ?? PLATFORMS);
|
|
453
|
+
const marks = PLATFORMS.map((p) => on.has(p) ? pc6.green("\u2713 ") : pc6.dim("\u2014 "));
|
|
454
|
+
const status = item.status ? pc6.yellow(item.status) : "";
|
|
455
|
+
console.log(`${item.name.padEnd(nameWidth)} ${marks.join(" ")} ${status}`);
|
|
456
|
+
}
|
|
457
|
+
console.log();
|
|
458
|
+
console.log(`${rows.length} of ${components.length} component(s) shown.`);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// src/commands/theme.ts
|
|
462
|
+
import { existsSync as existsSync8 } from "fs";
|
|
463
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
|
|
464
|
+
import path8 from "path";
|
|
465
|
+
import pc7 from "picocolors";
|
|
466
|
+
|
|
467
|
+
// src/lib/color-math.ts
|
|
468
|
+
function hexToRgb(hex) {
|
|
469
|
+
const h = hex.replace("#", "");
|
|
470
|
+
const n = parseInt(h.length === 3 ? h.replace(/(.)/g, "$1$1") : h, 16);
|
|
471
|
+
return [n >> 16 & 255, n >> 8 & 255, n & 255];
|
|
472
|
+
}
|
|
473
|
+
function rgbToHsl(r, g, b) {
|
|
474
|
+
r /= 255, g /= 255, b /= 255;
|
|
475
|
+
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
|
476
|
+
let h = 0, s = 0;
|
|
477
|
+
const l = (max + min) / 2;
|
|
478
|
+
const d = max - min;
|
|
479
|
+
if (d !== 0) {
|
|
480
|
+
s = d / (1 - Math.abs(2 * l - 1));
|
|
481
|
+
switch (max) {
|
|
482
|
+
case r:
|
|
483
|
+
h = (g - b) / d % 6;
|
|
484
|
+
break;
|
|
485
|
+
case g:
|
|
486
|
+
h = (b - r) / d + 2;
|
|
487
|
+
break;
|
|
488
|
+
default:
|
|
489
|
+
h = (r - g) / d + 4;
|
|
490
|
+
}
|
|
491
|
+
h *= 60;
|
|
492
|
+
if (h < 0) h += 360;
|
|
493
|
+
}
|
|
494
|
+
return [Math.round(h), Math.round(s * 100), Math.round(l * 100)];
|
|
495
|
+
}
|
|
496
|
+
function hexToHslChannels(hex) {
|
|
497
|
+
const [h, s, l] = rgbToHsl(...hexToRgb(hex));
|
|
498
|
+
return `${h} ${s}% ${l}%`;
|
|
499
|
+
}
|
|
500
|
+
function relLuminance([r, g, b]) {
|
|
501
|
+
const [cr, cg, cb] = [r, g, b].map((v) => {
|
|
502
|
+
const s = v / 255;
|
|
503
|
+
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
504
|
+
});
|
|
505
|
+
return 0.2126 * cr + 0.7152 * cg + 0.0722 * cb;
|
|
506
|
+
}
|
|
507
|
+
function contrastRatio(hexA, hexB) {
|
|
508
|
+
const la = relLuminance(hexToRgb(hexA)) + 0.05;
|
|
509
|
+
const lb = relLuminance(hexToRgb(hexB)) + 0.05;
|
|
510
|
+
return la > lb ? la / lb : lb / la;
|
|
511
|
+
}
|
|
512
|
+
function bestTextHex(bgHex) {
|
|
513
|
+
return contrastRatio(bgHex, "#ffffff") >= contrastRatio(bgHex, "#000000") ? "#ffffff" : "#000000";
|
|
514
|
+
}
|
|
515
|
+
function isHex(v) {
|
|
516
|
+
return /^#?[0-9a-fA-F]{6}$/.test(v.trim());
|
|
517
|
+
}
|
|
518
|
+
function normalizeHex(v) {
|
|
519
|
+
const t = v.trim().replace(/^#/, "");
|
|
520
|
+
return `#${t.toLowerCase()}`;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// src/lib/theme.ts
|
|
524
|
+
var ACCEPTED_TOKENS = [
|
|
525
|
+
"background",
|
|
526
|
+
"foreground",
|
|
527
|
+
"card",
|
|
528
|
+
"card-foreground",
|
|
529
|
+
"popover",
|
|
530
|
+
"popover-foreground",
|
|
531
|
+
"primary",
|
|
532
|
+
"primary-foreground",
|
|
533
|
+
"secondary",
|
|
534
|
+
"secondary-foreground",
|
|
535
|
+
"muted",
|
|
536
|
+
"muted-foreground",
|
|
537
|
+
"accent",
|
|
538
|
+
"accent-foreground",
|
|
539
|
+
"destructive",
|
|
540
|
+
"destructive-foreground",
|
|
541
|
+
"border",
|
|
542
|
+
"input",
|
|
543
|
+
"ring"
|
|
544
|
+
];
|
|
545
|
+
var EXAMPLE_VALUES = {
|
|
546
|
+
background: "#ffffff",
|
|
547
|
+
foreground: "#0f172a",
|
|
548
|
+
card: "#ffffff",
|
|
549
|
+
"card-foreground": "#0f172a",
|
|
550
|
+
popover: "#ffffff",
|
|
551
|
+
"popover-foreground": "#0f172a",
|
|
552
|
+
primary: "#2563eb",
|
|
553
|
+
"primary-foreground": "#ffffff",
|
|
554
|
+
secondary: "#f1f5f9",
|
|
555
|
+
"secondary-foreground": "#0f172a",
|
|
556
|
+
muted: "#f1f5f9",
|
|
557
|
+
"muted-foreground": "#64748b",
|
|
558
|
+
accent: "#f1f5f9",
|
|
559
|
+
"accent-foreground": "#0f172a",
|
|
560
|
+
destructive: "#dc2626",
|
|
561
|
+
"destructive-foreground": "#ffffff",
|
|
562
|
+
border: "#e2e8f0",
|
|
563
|
+
input: "#e2e8f0",
|
|
564
|
+
ring: "#2563eb"
|
|
565
|
+
};
|
|
566
|
+
var FOREGROUND_OF = {
|
|
567
|
+
background: "foreground",
|
|
568
|
+
card: "card-foreground",
|
|
569
|
+
popover: "popover-foreground",
|
|
570
|
+
primary: "primary-foreground",
|
|
571
|
+
secondary: "secondary-foreground",
|
|
572
|
+
muted: "muted-foreground",
|
|
573
|
+
accent: "accent-foreground",
|
|
574
|
+
destructive: "destructive-foreground"
|
|
575
|
+
};
|
|
576
|
+
var CONTRAST_PAIRS = [
|
|
577
|
+
["background", "foreground"],
|
|
578
|
+
["card", "card-foreground"],
|
|
579
|
+
["popover", "popover-foreground"],
|
|
580
|
+
["primary", "primary-foreground"],
|
|
581
|
+
["secondary", "secondary-foreground"],
|
|
582
|
+
["muted", "muted-foreground"],
|
|
583
|
+
["accent", "accent-foreground"],
|
|
584
|
+
["destructive", "destructive-foreground"]
|
|
585
|
+
];
|
|
586
|
+
var isAccepted = (t) => ACCEPTED_TOKENS.includes(t);
|
|
587
|
+
function parseThemeFile(text) {
|
|
588
|
+
const values = {};
|
|
589
|
+
const errors = [];
|
|
590
|
+
const unknownTokens = [];
|
|
591
|
+
const lines = text.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
|
|
592
|
+
lines.forEach((line, i) => {
|
|
593
|
+
const cells = line.split(/[,\t]/).map((c) => c.trim().replace(/^"|"$/g, ""));
|
|
594
|
+
if (cells.length < 2) {
|
|
595
|
+
errors.push(`Line ${i + 1}: expected "token,hex" \u2014 got "${line}"`);
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
const [rawToken, rawHex] = cells;
|
|
599
|
+
const token = rawToken.toLowerCase().replace(/^--/, "").replace(/\s+/g, "-");
|
|
600
|
+
if (!isHex(rawHex)) {
|
|
601
|
+
errors.push(`Line ${i + 1}: "${rawHex}" isn't a hex color`);
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
if (!isAccepted(token)) {
|
|
605
|
+
unknownTokens.push(rawToken);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
values[token] = normalizeHex(rawHex);
|
|
609
|
+
});
|
|
610
|
+
return { values, errors, unknownTokens };
|
|
611
|
+
}
|
|
612
|
+
function deriveForegrounds(values) {
|
|
613
|
+
const out = { ...values };
|
|
614
|
+
for (const [base, fg] of Object.entries(FOREGROUND_OF)) {
|
|
615
|
+
if (out[base] && !out[fg]) out[fg] = bestTextHex(out[base]);
|
|
616
|
+
}
|
|
617
|
+
return out;
|
|
618
|
+
}
|
|
619
|
+
function toCssBlock(values) {
|
|
620
|
+
const lines = ACCEPTED_TOKENS.filter((t) => values[t]).map((t) => ` --${t}: ${hexToHslChannels(values[t])};`);
|
|
621
|
+
return `:root {
|
|
622
|
+
${lines.join("\n")}
|
|
623
|
+
}
|
|
624
|
+
`;
|
|
625
|
+
}
|
|
626
|
+
function checkContrast(values) {
|
|
627
|
+
return CONTRAST_PAIRS.filter(([bg, fg]) => values[bg] && values[fg]).map(([bg, fg]) => {
|
|
628
|
+
const ratio = contrastRatio(values[bg], values[fg]);
|
|
629
|
+
return { pair: [bg, fg], ratio, pass: ratio >= 4.5 };
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
function scaffoldThemeFile(name) {
|
|
633
|
+
const lines = [
|
|
634
|
+
`# ${name} \u2014 KinetixUI theme override`,
|
|
635
|
+
"#",
|
|
636
|
+
"# One `token,hex` pair per line. Uncomment (remove the leading `# `) and",
|
|
637
|
+
"# edit any token you want to override; leave the rest commented out to",
|
|
638
|
+
"# keep KinetixUI's default. Run `kinetixui theme build " + name + "` to",
|
|
639
|
+
`# compile this into ${name}.css and get a WCAG AA contrast report.`,
|
|
640
|
+
"#",
|
|
641
|
+
"# Setting a base color (e.g. `primary`) without its `-foreground` pair",
|
|
642
|
+
"# auto-derives the foreground by contrast (best of white/black) \u2014 you",
|
|
643
|
+
"# don't have to set both unless you want a specific foreground.",
|
|
644
|
+
"#"
|
|
645
|
+
];
|
|
646
|
+
for (const token of ACCEPTED_TOKENS) {
|
|
647
|
+
lines.push(`# ${token},${EXAMPLE_VALUES[token]}`);
|
|
648
|
+
}
|
|
649
|
+
return lines.join("\n") + "\n";
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// src/commands/theme.ts
|
|
653
|
+
var THEME_DIR = "kinetixui-themes";
|
|
654
|
+
function csvPath(cwd, name) {
|
|
655
|
+
return path8.join(cwd, THEME_DIR, `${name}.csv`);
|
|
656
|
+
}
|
|
657
|
+
function cssPath(cwd, name) {
|
|
658
|
+
return path8.join(cwd, THEME_DIR, `${name}.css`);
|
|
659
|
+
}
|
|
660
|
+
async function themeCreate(name, options) {
|
|
661
|
+
const cwd = process.cwd();
|
|
662
|
+
const file = csvPath(cwd, name);
|
|
663
|
+
if (existsSync8(file) && !options.force) {
|
|
664
|
+
throw new Error(`${path8.relative(cwd, file)} already exists. Pass --force to overwrite it.`);
|
|
665
|
+
}
|
|
666
|
+
await mkdir2(path8.dirname(file), { recursive: true });
|
|
667
|
+
await writeFile3(file, scaffoldThemeFile(name), "utf8");
|
|
668
|
+
console.log(pc7.green("\u2714"), `Wrote ${path8.relative(cwd, file)}`);
|
|
669
|
+
console.log(`Uncomment and edit the tokens you want to override, then run:`);
|
|
670
|
+
console.log(pc7.dim(` kinetixui theme build ${name}`));
|
|
671
|
+
}
|
|
672
|
+
async function themeBuild(name, options) {
|
|
673
|
+
const cwd = process.cwd();
|
|
674
|
+
const src = csvPath(cwd, name);
|
|
675
|
+
if (!existsSync8(src)) {
|
|
676
|
+
throw new Error(`${path8.relative(cwd, src)} doesn't exist. Run "kinetixui theme create ${name}" first.`);
|
|
677
|
+
}
|
|
678
|
+
const text = await readFile2(src, "utf8");
|
|
679
|
+
const { values: parsed, errors, unknownTokens } = parseThemeFile(text);
|
|
680
|
+
for (const err of errors) console.log(pc7.yellow("!"), err);
|
|
681
|
+
for (const unknown of unknownTokens) console.log(pc7.yellow("!"), `"${unknown}" isn't a known token \u2014 skipped.`);
|
|
682
|
+
const values = deriveForegrounds(parsed);
|
|
683
|
+
const setCount = Object.keys(values).length;
|
|
684
|
+
if (setCount === 0) {
|
|
685
|
+
throw new Error(`No tokens set in ${path8.relative(cwd, src)} \u2014 nothing to build. Uncomment at least one line.`);
|
|
686
|
+
}
|
|
687
|
+
const dest = cssPath(cwd, name);
|
|
688
|
+
await writeFile3(dest, toCssBlock(values), "utf8");
|
|
689
|
+
console.log(pc7.green("\u2714"), `Wrote ${path8.relative(cwd, dest)} (${setCount} token${setCount === 1 ? "" : "s"})`);
|
|
690
|
+
console.log(`Paste it after ${pc7.dim('@import "@kinetixui/tokens/css";')} in your global CSS.`);
|
|
691
|
+
console.log();
|
|
692
|
+
console.log(pc7.dim("WCAG AA contrast (4.5:1)"));
|
|
693
|
+
const results = checkContrast(values);
|
|
694
|
+
let failed = 0;
|
|
695
|
+
for (const { pair, ratio, pass } of results) {
|
|
696
|
+
const mark = pass ? pc7.green("\u2714") : pc7.red("\u2716");
|
|
697
|
+
if (!pass) failed++;
|
|
698
|
+
console.log(`${mark} ${pair[0]} / ${pair[1]}`, pc7.dim(`${ratio.toFixed(2)}:1`));
|
|
699
|
+
}
|
|
700
|
+
if (results.length === 0) {
|
|
701
|
+
console.log(pc7.dim("(no checkable pairs \u2014 set at least one base + foreground, or just a base to auto-derive one)"));
|
|
702
|
+
}
|
|
703
|
+
console.log();
|
|
704
|
+
console.log("Native (SwiftUI/Compose/Flutter) theme output isn't built yet \u2014 this compiles to CSS only.");
|
|
705
|
+
console.log(pc7.dim("See https://kinetixui.com/docs/cli#theme for the current scope."));
|
|
706
|
+
if (failed > 0 && options.failOnContrast) {
|
|
707
|
+
process.exitCode = 1;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
431
711
|
// package.json
|
|
432
712
|
var package_default = {
|
|
433
713
|
name: "@kinetixui/cli",
|
|
434
|
-
version: "0.
|
|
714
|
+
version: "0.15.0",
|
|
435
715
|
description: "Add KinetixUI components and tokens to your project.",
|
|
436
716
|
type: "module",
|
|
437
717
|
license: "MIT",
|
|
@@ -475,7 +755,7 @@ program.command("init").description("Set up kinetixui.json and pull in the token
|
|
|
475
755
|
try {
|
|
476
756
|
await init({ registry: opts.registry, yes: opts.yes });
|
|
477
757
|
} catch (err) {
|
|
478
|
-
console.error(
|
|
758
|
+
console.error(pc8.red("\u2716"), err.message);
|
|
479
759
|
process.exitCode = 1;
|
|
480
760
|
}
|
|
481
761
|
});
|
|
@@ -483,7 +763,7 @@ program.command("add").description("Add one or more components to your project."
|
|
|
483
763
|
try {
|
|
484
764
|
await add(components, { registry: opts.registry, overwrite: opts.overwrite, all: opts.all });
|
|
485
765
|
} catch (err) {
|
|
486
|
-
console.error(
|
|
766
|
+
console.error(pc8.red("\u2716"), err.message);
|
|
487
767
|
process.exitCode = 1;
|
|
488
768
|
}
|
|
489
769
|
});
|
|
@@ -491,7 +771,7 @@ program.command("list").description("List every component available in the regis
|
|
|
491
771
|
try {
|
|
492
772
|
await list({ registry: opts.registry });
|
|
493
773
|
} catch (err) {
|
|
494
|
-
console.error(
|
|
774
|
+
console.error(pc8.red("\u2716"), err.message);
|
|
495
775
|
process.exitCode = 1;
|
|
496
776
|
}
|
|
497
777
|
});
|
|
@@ -499,7 +779,15 @@ program.command("inspect").description("Show everything the registry knows about
|
|
|
499
779
|
try {
|
|
500
780
|
await inspect(name, { registry: opts.registry });
|
|
501
781
|
} catch (err) {
|
|
502
|
-
console.error(
|
|
782
|
+
console.error(pc8.red("\u2716"), err.message);
|
|
783
|
+
process.exitCode = 1;
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
program.command("parity").description("Show which platforms carry each component (or one, by name).").argument("[components...]", "component names, e.g. button card \u2014 omit for the full matrix").option("-r, --registry <url>", "registry base URL", DEFAULT_REGISTRY).action(async (components, opts) => {
|
|
787
|
+
try {
|
|
788
|
+
await parity(components, { registry: opts.registry });
|
|
789
|
+
} catch (err) {
|
|
790
|
+
console.error(pc8.red("\u2716"), err.message);
|
|
503
791
|
process.exitCode = 1;
|
|
504
792
|
}
|
|
505
793
|
});
|
|
@@ -507,7 +795,24 @@ program.command("doctor").description("Check kinetixui.json, its aliases, and th
|
|
|
507
795
|
try {
|
|
508
796
|
await doctor();
|
|
509
797
|
} catch (err) {
|
|
510
|
-
console.error(
|
|
798
|
+
console.error(pc8.red("\u2716"), err.message);
|
|
799
|
+
process.exitCode = 1;
|
|
800
|
+
}
|
|
801
|
+
});
|
|
802
|
+
var theme = program.command("theme").description("Scaffold and compile a local token override (CSS only \u2014 see 'theme build --help').");
|
|
803
|
+
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) => {
|
|
804
|
+
try {
|
|
805
|
+
await themeCreate(name, { force: opts.force });
|
|
806
|
+
} catch (err) {
|
|
807
|
+
console.error(pc8.red("\u2716"), err.message);
|
|
808
|
+
process.exitCode = 1;
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
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) => {
|
|
812
|
+
try {
|
|
813
|
+
await themeBuild(name, { failOnContrast: opts.failOnContrast });
|
|
814
|
+
} catch (err) {
|
|
815
|
+
console.error(pc8.red("\u2716"), err.message);
|
|
511
816
|
process.exitCode = 1;
|
|
512
817
|
}
|
|
513
818
|
});
|