@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.
Files changed (2) hide show
  1. package/dist/index.js +316 -11
  2. 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 pc6 from "picocolors";
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 cssPath = path5.join(cwd, config.srcDir ? "src" : "", config.tailwind.css);
252
- if (existsSync5(cssPath)) {
253
- levels.push(report("pass", `Tailwind CSS file found at ${path5.relative(cwd, cssPath)}`));
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, cssPath)} \u2014 the token contract has nowhere to land.`));
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.13.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(pc6.red("\u2716"), err.message);
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(pc6.red("\u2716"), err.message);
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(pc6.red("\u2716"), err.message);
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(pc6.red("\u2716"), err.message);
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(pc6.red("\u2716"), err.message);
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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kinetixui/cli",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "Add KinetixUI components and tokens to your project.",
5
5
  "type": "module",
6
6
  "license": "MIT",