@kinetixui/cli 0.23.2 → 0.24.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 (3) hide show
  1. package/README.md +89 -0
  2. package/dist/index.js +410 -251
  3. package/package.json +9 -5
package/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # @kinetixui/cli
2
+
3
+ Adds KinetixUI components and tokens to your project by writing source into your repo — the
4
+ copy-the-code model, not a dependency you import from.
5
+
6
+ **Beta.** Ships alongside `@kinetixui/ui` and `@kinetixui/tokens` on one version line.
7
+
8
+ ```bash
9
+ npx @kinetixui/cli init
10
+ npx @kinetixui/cli add button card dialog
11
+ ```
12
+
13
+ No install step is needed; `npx` is the intended way to run it. The binary is `kinetixui` if you do
14
+ install it.
15
+
16
+ ## What it is
17
+
18
+ A registry client and a code writer. `init` sets up `kinetixui.json` and writes the token contract into
19
+ your global stylesheet. `add` resolves a component's registry dependencies, installs the npm packages it
20
+ needs with *your* package manager, and writes the component source into your tree. From that point the
21
+ code is yours to edit — nothing rewrites it unless you ask.
22
+
23
+ ## Commands
24
+
25
+ | Command | Does |
26
+ | --- | --- |
27
+ | `init` | Create `kinetixui.json`, write `:root` / `.dark` token blocks into your stylesheet |
28
+ | `add <name…>` | Resolve and write component source, plus its registry and npm dependencies |
29
+ | `list` | List what the registry offers |
30
+ | `inspect <name>` | Show a component's files, dependencies and registry metadata |
31
+ | `parity <name>` | Show which platforms carry a component, and why one does not |
32
+ | `lint` | Check your usage against the token contract |
33
+ | `doctor` | Check that your project is set up correctly |
34
+ | `theme` | Work with themes |
35
+ | `preset` | Read a Kinetix Create preset code (`KX1_…`) and emit CSS or native token files |
36
+ | `create` | Scaffolding helpers |
37
+
38
+ Run any command with `--help` for its flags. Full reference: <https://kinetixui.com/docs/cli>.
39
+
40
+ ## Where components come from
41
+
42
+ `add` fetches from a registry over HTTPS. The default is `https://kinetixui.com/r`, which serves a
43
+ shadcn-compatible JSON descriptor per component. You can point it elsewhere:
44
+
45
+ ```bash
46
+ npx @kinetixui/cli add button --registry https://your-mirror.example.com/r
47
+ ```
48
+
49
+ `-r, --registry <url>` is accepted by `init`, `add`, `list` and `inspect`. To set it once for a project
50
+ instead of on every command, put it in `kinetixui.json`:
51
+
52
+ ```json
53
+ {
54
+ "registry": "https://your-mirror.example.com/r"
55
+ }
56
+ ```
57
+
58
+ The flag wins when both are present. The URL is validated before use, and component names are validated
59
+ against a strict character set before they are used to build any path — a registry response cannot direct
60
+ the CLI to write outside your project.
61
+
62
+ **One consequence to be aware of:** `add` needs that origin to be reachable, and there is no offline
63
+ mode. If your builds cannot depend on `kinetixui.com`, mirror the registry JSON and set `registry` in
64
+ `kinetixui.json`. When the registry is unreachable the CLI says so and names both ways to point
65
+ elsewhere, rather than failing with a bare network error.
66
+
67
+ ## Requirements
68
+
69
+ - **Node 18 or newer** (declared in `engines`)
70
+ - A JavaScript package manager on `PATH` — npm, pnpm, yarn or bun; the CLI detects which you use and
71
+ calls it rather than assuming npm
72
+ - A project with a global stylesheet for `init` to write into
73
+
74
+ ## What it does not do
75
+
76
+ It does not publish, does not phone home, and does not write outside the directory you run it in. It
77
+ installs dependencies by invoking your package manager, so anything it installs is visible in your
78
+ lockfile like any other dependency.
79
+
80
+ ## Links
81
+
82
+ - CLI reference — <https://kinetixui.com/docs/cli>
83
+ - Installation — <https://kinetixui.com/docs/installation>
84
+ - Components — <https://kinetixui.com/components>
85
+ - Source — <https://github.com/zedalleys/kinetixui>
86
+
87
+ ## License
88
+
89
+ MIT
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/index.ts
3
+ // src/program.ts
4
4
  import { Command } from "commander";
5
5
  import pc10 from "picocolors";
6
6
 
@@ -39,6 +39,11 @@ async function readConfig(cwd) {
39
39
  async function writeConfig(cwd, config) {
40
40
  await writeFile(path.join(cwd, CONFIG_FILE), JSON.stringify(config, null, 2) + "\n", "utf8");
41
41
  }
42
+ async function resolveRegistry(cwd, fromFlag) {
43
+ if (fromFlag && fromFlag !== DEFAULT_REGISTRY) return fromFlag;
44
+ const config = await readConfig(cwd);
45
+ return config?.registry?.trim() || DEFAULT_REGISTRY;
46
+ }
42
47
  function resolveAliasDir(cwd, config, alias) {
43
48
  const raw = config.aliases[alias].replace(/^@\//, "");
44
49
  return path.join(cwd, config.srcDir ? "src" : "", raw);
@@ -147,13 +152,49 @@ function runInstall(cwd, pm, packages) {
147
152
  }
148
153
 
149
154
  // src/lib/registry.ts
155
+ async function request(url) {
156
+ try {
157
+ return await fetch(url);
158
+ } catch (cause) {
159
+ const origin = safeOrigin(url);
160
+ throw new Error(
161
+ `Could not reach the registry at ${origin} (${cause.message}). Check the network, or point somewhere else with --registry <url> or "registry" in kinetixui.json.`,
162
+ { cause }
163
+ );
164
+ }
165
+ }
166
+ function safeOrigin(url) {
167
+ try {
168
+ return new URL(url).origin;
169
+ } catch {
170
+ return url;
171
+ }
172
+ }
173
+ async function parse(res, url, describe, valid) {
174
+ let data;
175
+ try {
176
+ data = await res.json();
177
+ } catch (cause) {
178
+ throw new Error(
179
+ `The registry at ${safeOrigin(url)} did not return JSON for ${describe} (${cause.message}). If you are behind a proxy, it may be answering instead of the registry.`,
180
+ { cause }
181
+ );
182
+ }
183
+ if (!valid(data)) {
184
+ throw new Error(`The registry at ${safeOrigin(url)} returned JSON that is not a valid ${describe} (${url}).`);
185
+ }
186
+ return data;
187
+ }
188
+ var isIndex = (v) => typeof v === "object" && v !== null && Array.isArray(v.items);
189
+ var isItem = (v) => typeof v === "object" && v !== null && typeof v.name === "string" && Array.isArray(v.files);
190
+ var isSpec = (v) => typeof v === "object" && v !== null && typeof v.name === "string";
150
191
  async function fetchRegistryIndex(registryUrl) {
151
192
  const url = `${assertRegistryUrl(registryUrl).replace(/\/$/, "")}/registry.json`;
152
- const res = await fetch(url);
193
+ const res = await request(url);
153
194
  if (!res.ok) {
154
195
  throw new Error(`Could not load the registry index (${res.status} at ${url}).`);
155
196
  }
156
- const data = await res.json();
197
+ const data = await parse(res, url, "registry index", isIndex);
157
198
  return data.items;
158
199
  }
159
200
  async function fetchComponentNames(registryUrl) {
@@ -163,22 +204,22 @@ async function fetchComponentNames(registryUrl) {
163
204
  async function fetchRegistryItem(registryUrl, name) {
164
205
  const base = assertRegistryUrl(registryUrl).replace(/\/$/, "");
165
206
  const url = `${base}/${assertComponentName(name)}.json`;
166
- const res = await fetch(url);
207
+ const res = await request(url);
167
208
  if (!res.ok) {
168
209
  throw new Error(`Could not find "${name}" in the registry (${res.status} at ${url}).`);
169
210
  }
170
- return await res.json();
211
+ return await parse(res, url, `registry item for "${name}"`, isItem);
171
212
  }
172
213
  async function fetchComponentSpec(registryUrl, name) {
173
214
  const base = assertRegistryUrl(registryUrl).replace(/\/$/, "");
174
215
  const specsBase = base.endsWith("/r") ? `${base.slice(0, -2)}/specs` : `${base}/specs`;
175
216
  const url = `${specsBase}/${assertComponentName(name)}.json`;
176
- const res = await fetch(url);
217
+ const res = await request(url);
177
218
  if (res.status === 404) return null;
178
219
  if (!res.ok) {
179
220
  throw new Error(`Could not load the component spec for "${name}" (${res.status} at ${url}).`);
180
221
  }
181
- return await res.json();
222
+ return await parse(res, url, `component spec for "${name}"`, isSpec);
182
223
  }
183
224
  async function resolveTree(registryUrl, names) {
184
225
  const resolved = /* @__PURE__ */ new Map();
@@ -199,17 +240,18 @@ async function resolveTree(registryUrl, names) {
199
240
  async function add(names, options) {
200
241
  const cwd = process.cwd();
201
242
  const config = await readConfig(cwd) ?? DEFAULT_CONFIG;
243
+ const registry = await resolveRegistry(cwd, options.registry);
202
244
  if (!existsSync4(path4.join(cwd, CONFIG_FILE))) {
203
245
  console.log(pc.yellow("!"), `No ${CONFIG_FILE} found \u2014 using defaults. Run "kinetixui init" first to set your own aliases.`);
204
246
  }
205
247
  if (options.all) {
206
248
  console.log("Fetching the full component list\u2026");
207
- names = await fetchComponentNames(options.registry);
249
+ names = await fetchComponentNames(registry);
208
250
  } else if (names.length === 0) {
209
251
  throw new Error('Pass component names to add (e.g. "kinetixui add button card"), or use --all for every component.');
210
252
  }
211
253
  console.log(options.all ? `Resolving all ${names.length} components\u2026` : `Resolving ${names.join(", ")}\u2026`);
212
- const items = await resolveTree(options.registry, names);
254
+ const items = await resolveTree(registry, names);
213
255
  const { deps, written, skipped } = await writeItems(cwd, config, items.values(), options.overwrite);
214
256
  for (const file of written) console.log(pc.green("\u2714"), `Added ${file}`);
215
257
  for (const file of skipped) console.log(pc.yellow("skip"), `${file} already exists (pass --overwrite to replace it)`);
@@ -270,13 +312,15 @@ async function doctor() {
270
312
  levels.push(report("warn", `Tailwind CSS file not found at ${path5.relative(cwd, cssPath2)} \u2014 the token contract has nowhere to land.`));
271
313
  }
272
314
  }
315
+ const registry = await resolveRegistry(cwd);
316
+ const origin = registry === DEFAULT_REGISTRY ? registry : `${registry} (from ${CONFIG_FILE})`;
273
317
  let registryNames = null;
274
318
  try {
275
- const items = await fetchRegistryIndex(DEFAULT_REGISTRY);
319
+ const items = await fetchRegistryIndex(registry);
276
320
  registryNames = new Set(items.filter((item) => item.type === "registry:ui").map((item) => item.name));
277
- levels.push(report("pass", `Registry reachable (${DEFAULT_REGISTRY}) \u2014 ${registryNames.size} component(s) listed.`));
321
+ levels.push(report("pass", `Registry reachable (${origin}) \u2014 ${registryNames.size} component(s) listed.`));
278
322
  } catch (err) {
279
- levels.push(report("fail", `Registry unreachable at ${DEFAULT_REGISTRY}: ${err.message}`));
323
+ levels.push(report("fail", `Registry unreachable at ${origin}: ${err.message}`));
280
324
  }
281
325
  if (config && registryNames) {
282
326
  const uiDir = resolveAliasDir(cwd, config, "ui");
@@ -374,7 +418,7 @@ async function init(options) {
374
418
  };
375
419
  await writeConfig(cwd, config);
376
420
  console.log(pc3.green("\u2714"), `Created ${CONFIG_FILE}`);
377
- const items = await resolveTree(options.registry, ["tokens"]);
421
+ const items = await resolveTree(await resolveRegistry(cwd, options.registry), ["tokens"]);
378
422
  const { written, skipped } = await writeItems(cwd, config, items.values(), false);
379
423
  for (const file of written) console.log(pc3.green("\u2714"), `Wrote ${file}`);
380
424
  for (const file of skipped) {
@@ -390,7 +434,8 @@ import { existsSync as existsSync7 } from "fs";
390
434
  import path7 from "path";
391
435
  import pc4 from "picocolors";
392
436
  async function inspect(name, options) {
393
- const item = await fetchRegistryItem(options.registry, name);
437
+ const registry = await resolveRegistry(process.cwd(), options.registry);
438
+ const item = await fetchRegistryItem(registry, name);
394
439
  console.log(`${pc4.bold(item.title ?? item.name)} ${pc4.dim(`(${item.name})`)}`);
395
440
  if (item.description) console.log(item.description);
396
441
  console.log();
@@ -420,7 +465,7 @@ async function inspect(name, options) {
420
465
  for (const file of item.files) {
421
466
  console.log(` ${file.path} ${pc4.dim("\u2192")} ${file.target}`);
422
467
  }
423
- const spec = await fetchComponentSpec(options.registry, name);
468
+ const spec = await fetchComponentSpec(registry, name);
424
469
  if (spec) {
425
470
  if (spec.since || spec.status) {
426
471
  console.log();
@@ -617,7 +662,7 @@ async function lint(targetArg, options) {
617
662
  // src/commands/list.ts
618
663
  import pc6 from "picocolors";
619
664
  async function list(options) {
620
- const items = await fetchRegistryIndex(options.registry);
665
+ const items = await fetchRegistryIndex(await resolveRegistry(process.cwd(), options.registry));
621
666
  const components = items.filter((item) => item.type === "registry:ui").sort((a, b) => a.name.localeCompare(b.name));
622
667
  console.log(`${components.length} component(s):`);
623
668
  console.log();
@@ -1236,6 +1281,7 @@ var tokens = {
1236
1281
  "secondary-foreground": "#465245",
1237
1282
  "muted": "#f6f6f6",
1238
1283
  "muted-foreground": "#6d6d6d",
1284
+ "surface-grouped": "#f6f6f6",
1239
1285
  "accent": "#f0f7ff",
1240
1286
  "accent-foreground": "#1d4ed8",
1241
1287
  "destructive": "#c60a0a",
@@ -1258,7 +1304,8 @@ var tokens = {
1258
1304
  "on-success-container": "#465245",
1259
1305
  "warning": "#f97907",
1260
1306
  "info-container": "#f0f7ff",
1261
- "on-info-container": "#395a70"
1307
+ "on-info-container": "#395a70",
1308
+ "muted-on-container": "#616161"
1262
1309
  },
1263
1310
  "chart-1": "#1b3c53",
1264
1311
  "chart-2": "#748873",
@@ -1280,6 +1327,8 @@ var tokens = {
1280
1327
  "tertiary-foreground": "#f0f7ff",
1281
1328
  "info": "#57778d",
1282
1329
  "info-foreground": "#f0f7ff",
1330
+ "scrim": "rgba(0, 0, 0, 0.4)",
1331
+ "on-info-container": "#395a70",
1283
1332
  "brand": "#1b3c53",
1284
1333
  "brand-foreground": "#f0f7ff",
1285
1334
  "action": "#1d4ed8",
@@ -1395,40 +1444,40 @@ var tokens = {
1395
1444
  }
1396
1445
  },
1397
1446
  "fontSize": {
1398
- "display-lg": "57px",
1399
- "display-md": "45px",
1400
- "display-sm": "36px",
1401
- "headline-lg": "32px",
1402
- "headline-md": "28px",
1403
- "headline-sm": "24px",
1404
- "title-lg": "22px",
1405
- "title-md": "16px",
1406
- "title-sm": "14px",
1407
- "title-dialog": "18px",
1408
- "label-lg": "14px",
1409
- "label-md": "12px",
1410
- "label-sm": "11px",
1411
- "body-lg": "16px",
1412
- "body-md": "14px",
1413
- "body-sm": "12px"
1447
+ "display-lg": "3.5625rem",
1448
+ "display-md": "2.8125rem",
1449
+ "display-sm": "2.25rem",
1450
+ "headline-lg": "2rem",
1451
+ "headline-md": "1.75rem",
1452
+ "headline-sm": "1.5rem",
1453
+ "title-lg": "1.375rem",
1454
+ "title-md": "1rem",
1455
+ "title-sm": "0.875rem",
1456
+ "title-dialog": "1.125rem",
1457
+ "label-lg": "0.875rem",
1458
+ "label-md": "0.75rem",
1459
+ "label-sm": "0.6875rem",
1460
+ "body-lg": "1rem",
1461
+ "body-md": "0.875rem",
1462
+ "body-sm": "0.75rem"
1414
1463
  },
1415
1464
  "lineHeight": {
1416
- "display-lg": "64px",
1417
- "display-md": "52px",
1418
- "display-sm": "44px",
1419
- "headline-lg": "40px",
1420
- "headline-md": "36px",
1421
- "headline-sm": "32px",
1422
- "title-lg": "28px",
1423
- "title-md": "24px",
1424
- "title-sm": "20px",
1425
- "title-dialog": "24px",
1426
- "label-lg": "20px",
1427
- "label-md": "16px",
1428
- "label-sm": "16px",
1429
- "body-lg": "24px",
1430
- "body-md": "20px",
1431
- "body-sm": "16px"
1465
+ "display-lg": "4rem",
1466
+ "display-md": "3.25rem",
1467
+ "display-sm": "2.75rem",
1468
+ "headline-lg": "2.5rem",
1469
+ "headline-md": "2.25rem",
1470
+ "headline-sm": "2rem",
1471
+ "title-lg": "1.75rem",
1472
+ "title-md": "1.5rem",
1473
+ "title-sm": "1.25rem",
1474
+ "title-dialog": "1.5rem",
1475
+ "label-lg": "1.25rem",
1476
+ "label-md": "1rem",
1477
+ "label-sm": "1rem",
1478
+ "body-lg": "1.5rem",
1479
+ "body-md": "1.25rem",
1480
+ "body-sm": "1rem"
1432
1481
  },
1433
1482
  "letterSpacing": {
1434
1483
  "tighter": "-0.25px",
@@ -1515,14 +1564,14 @@ var tokens = {
1515
1564
  ],
1516
1565
  "focus-destructive": [
1517
1566
  {
1518
- "color": "#ec5047",
1567
+ "color": "#c60a0a",
1519
1568
  "offsetX": "0",
1520
1569
  "offsetY": "0",
1521
1570
  "blur": "0",
1522
1571
  "spread": "1px"
1523
1572
  },
1524
1573
  {
1525
- "color": "#ec504733",
1574
+ "color": "#c60a0a33",
1526
1575
  "offsetX": "0",
1527
1576
  "offsetY": "0",
1528
1577
  "blur": "0",
@@ -1566,113 +1615,113 @@ var tokens = {
1566
1615
  "display-lg": {
1567
1616
  "fontFamily": "Montserrat, ui-sans-serif, system-ui, sans-serif",
1568
1617
  "fontWeight": "500",
1569
- "fontSize": "57px",
1570
- "lineHeight": "64px",
1618
+ "fontSize": "3.5625rem",
1619
+ "lineHeight": "4rem",
1571
1620
  "letterSpacing": "-0.25px"
1572
1621
  },
1573
1622
  "display-md": {
1574
1623
  "fontFamily": "Montserrat, ui-sans-serif, system-ui, sans-serif",
1575
1624
  "fontWeight": "500",
1576
- "fontSize": "45px",
1577
- "lineHeight": "52px",
1625
+ "fontSize": "2.8125rem",
1626
+ "lineHeight": "3.25rem",
1578
1627
  "letterSpacing": "0"
1579
1628
  },
1580
1629
  "display-sm": {
1581
1630
  "fontFamily": "Montserrat, ui-sans-serif, system-ui, sans-serif",
1582
1631
  "fontWeight": "400",
1583
- "fontSize": "36px",
1584
- "lineHeight": "44px",
1632
+ "fontSize": "2.25rem",
1633
+ "lineHeight": "2.75rem",
1585
1634
  "letterSpacing": "0"
1586
1635
  },
1587
1636
  "headline-lg": {
1588
1637
  "fontFamily": "Montserrat, ui-sans-serif, system-ui, sans-serif",
1589
1638
  "fontWeight": "600",
1590
- "fontSize": "32px",
1591
- "lineHeight": "40px",
1639
+ "fontSize": "2rem",
1640
+ "lineHeight": "2.5rem",
1592
1641
  "letterSpacing": "0"
1593
1642
  },
1594
1643
  "headline-md": {
1595
1644
  "fontFamily": "Montserrat, ui-sans-serif, system-ui, sans-serif",
1596
1645
  "fontWeight": "400",
1597
- "fontSize": "28px",
1598
- "lineHeight": "36px",
1646
+ "fontSize": "1.75rem",
1647
+ "lineHeight": "2.25rem",
1599
1648
  "letterSpacing": "0"
1600
1649
  },
1601
1650
  "headline-sm": {
1602
1651
  "fontFamily": "Montserrat, ui-sans-serif, system-ui, sans-serif",
1603
1652
  "fontWeight": "400",
1604
- "fontSize": "24px",
1605
- "lineHeight": "32px",
1653
+ "fontSize": "1.5rem",
1654
+ "lineHeight": "2rem",
1606
1655
  "letterSpacing": "0"
1607
1656
  },
1608
1657
  "title-lg": {
1609
1658
  "fontFamily": "Montserrat, ui-sans-serif, system-ui, sans-serif",
1610
1659
  "fontWeight": "400",
1611
- "fontSize": "22px",
1612
- "lineHeight": "28px",
1660
+ "fontSize": "1.375rem",
1661
+ "lineHeight": "1.75rem",
1613
1662
  "letterSpacing": "0"
1614
1663
  },
1615
1664
  "title-md": {
1616
1665
  "fontFamily": "Montserrat, ui-sans-serif, system-ui, sans-serif",
1617
1666
  "fontWeight": "500",
1618
- "fontSize": "16px",
1619
- "lineHeight": "24px",
1667
+ "fontSize": "1rem",
1668
+ "lineHeight": "1.5rem",
1620
1669
  "letterSpacing": "0.15px"
1621
1670
  },
1622
1671
  "title-sm": {
1623
1672
  "fontFamily": "Montserrat, ui-sans-serif, system-ui, sans-serif",
1624
1673
  "fontWeight": "500",
1625
- "fontSize": "14px",
1626
- "lineHeight": "20px",
1674
+ "fontSize": "0.875rem",
1675
+ "lineHeight": "1.25rem",
1627
1676
  "letterSpacing": "0.1px"
1628
1677
  },
1629
1678
  "title-dialog": {
1630
1679
  "fontFamily": "Montserrat, ui-sans-serif, system-ui, sans-serif",
1631
1680
  "fontWeight": "600",
1632
- "fontSize": "18px",
1633
- "lineHeight": "24px",
1681
+ "fontSize": "1.125rem",
1682
+ "lineHeight": "1.5rem",
1634
1683
  "letterSpacing": "0"
1635
1684
  },
1636
1685
  "label-lg": {
1637
1686
  "fontFamily": "Montserrat, ui-sans-serif, system-ui, sans-serif",
1638
1687
  "fontWeight": "500",
1639
- "fontSize": "14px",
1640
- "lineHeight": "20px",
1688
+ "fontSize": "0.875rem",
1689
+ "lineHeight": "1.25rem",
1641
1690
  "letterSpacing": "0.1px"
1642
1691
  },
1643
1692
  "label-md": {
1644
1693
  "fontFamily": "Montserrat, ui-sans-serif, system-ui, sans-serif",
1645
1694
  "fontWeight": "500",
1646
- "fontSize": "12px",
1647
- "lineHeight": "16px",
1695
+ "fontSize": "0.75rem",
1696
+ "lineHeight": "1rem",
1648
1697
  "letterSpacing": "0.5px"
1649
1698
  },
1650
1699
  "label-sm": {
1651
1700
  "fontFamily": "Montserrat, ui-sans-serif, system-ui, sans-serif",
1652
1701
  "fontWeight": "500",
1653
- "fontSize": "11px",
1654
- "lineHeight": "16px",
1702
+ "fontSize": "0.6875rem",
1703
+ "lineHeight": "1rem",
1655
1704
  "letterSpacing": "0.5px"
1656
1705
  },
1657
1706
  "body-lg": {
1658
1707
  "fontFamily": "Montserrat, ui-sans-serif, system-ui, sans-serif",
1659
1708
  "fontWeight": "400",
1660
- "fontSize": "16px",
1661
- "lineHeight": "24px",
1709
+ "fontSize": "1rem",
1710
+ "lineHeight": "1.5rem",
1662
1711
  "letterSpacing": "0.5px"
1663
1712
  },
1664
1713
  "body-md": {
1665
1714
  "fontFamily": "Montserrat, ui-sans-serif, system-ui, sans-serif",
1666
1715
  "fontWeight": "400",
1667
- "fontSize": "14px",
1668
- "lineHeight": "20px",
1716
+ "fontSize": "0.875rem",
1717
+ "lineHeight": "1.25rem",
1669
1718
  "letterSpacing": "0.25px"
1670
1719
  },
1671
1720
  "body-sm": {
1672
1721
  "fontFamily": "Montserrat, ui-sans-serif, system-ui, sans-serif",
1673
1722
  "fontWeight": "400",
1674
- "fontSize": "12px",
1675
- "lineHeight": "16px",
1723
+ "fontSize": "0.75rem",
1724
+ "lineHeight": "1rem",
1676
1725
  "letterSpacing": "0"
1677
1726
  }
1678
1727
  }
@@ -1691,6 +1740,7 @@ var TOKEN_CONTRACT = [
1691
1740
  { token: "secondary-foreground", light: "#465245", dark: "#e3e7e3" },
1692
1741
  { token: "muted", light: "#f6f6f6", dark: "#102432" },
1693
1742
  { token: "muted-foreground", light: "#6d6d6d", dark: "#92b2c8" },
1743
+ { token: "surface-grouped", light: "#f6f6f6", dark: "#081219", note: "grouped section under a card" },
1694
1744
  { token: "accent", light: "#f0f7ff", dark: "#102432" },
1695
1745
  { token: "accent-foreground", light: "#1d4ed8", dark: "#f0f7ff" },
1696
1746
  { token: "destructive", light: "#c60a0a", dark: "#dd6a6a", note: "darkened for AA" },
@@ -1709,6 +1759,12 @@ var TOKEN_CONTRACT = [
1709
1759
  { token: "link", light: "#1d4ed8", dark: "#60a5fa", note: "defaults to primary" },
1710
1760
  { token: "focus", light: "#1d4ed8", dark: "#60a5fa", note: "defaults to ring" }
1711
1761
  ];
1762
+ var ROLE_DEFAULTS = {
1763
+ action: "primary",
1764
+ "action-foreground": "primary-foreground",
1765
+ link: "primary",
1766
+ focus: "ring"
1767
+ };
1712
1768
  var RADIUS_STEP_NAMES = ["sm", "md", "lg", "xl"];
1713
1769
  var ELEVATION_STEP_NAMES = ["sm", "md", "lg", "xl"];
1714
1770
  var px = (value) => Number(value.replace("px", ""));
@@ -1842,6 +1898,9 @@ var NEUTRAL_LADDER = {
1842
1898
  card: 1,
1843
1899
  popover: 1,
1844
1900
  muted: 0.975,
1901
+ // The grouped section a card sits on. Light shares `muted`'s step; dark sits between the page and
1902
+ // `card`, because dark `muted` is above `card` and a card on it would read as recessed.
1903
+ "surface-grouped": 0.975,
1845
1904
  border: 0.86,
1846
1905
  input: 0.86,
1847
1906
  "muted-foreground": 0.535,
@@ -1852,6 +1911,7 @@ var NEUTRAL_LADDER = {
1852
1911
  card: 0.22,
1853
1912
  popover: 0.22,
1854
1913
  muted: 0.27,
1914
+ "surface-grouped": 0.19,
1855
1915
  border: 0.4,
1856
1916
  input: 0.4,
1857
1917
  "muted-foreground": 0.72,
@@ -1933,6 +1993,7 @@ var ALIASED_FROM = {
1933
1993
  input: "border"
1934
1994
  };
1935
1995
  var FOREGROUND_OF = Object.fromEntries(CONTRAST_PAIRS);
1996
+ var BASE_OF = Object.fromEntries(CONTRAST_PAIRS.map(([base, fg]) => [fg, base]));
1936
1997
  function shippedColorsFor(mode) {
1937
1998
  const out = {};
1938
1999
  for (const row2 of TOKEN_CONTRACT) out[row2.token] = mode === "dark" ? row2.dark : row2.light;
@@ -1963,14 +2024,29 @@ function resolveMode(design, mode) {
1963
2024
  Object.assign(colors, deriveChart(design.chartPalette, design.brand, mode));
1964
2025
  const border = deriveSurfaceBorder(design.surface, mode, colors.border ?? "#000000");
1965
2026
  if (border) colors.border = border;
2027
+ const generatedAction = colors.action;
2028
+ const pinned = design.manualOverrides;
1966
2029
  for (const [token, hex] of Object.entries(design.manualOverrides)) {
1967
2030
  if (hex) colors[token] = hex;
1968
2031
  }
1969
2032
  for (const [base, fg] of Object.entries(FOREGROUND_OF)) {
1970
- if (design.manualOverrides[base] && !design.manualOverrides[fg]) {
2033
+ if (pinned[base] && !pinned[fg]) {
1971
2034
  colors[fg] = foregroundFor(colors[base]);
1972
2035
  }
1973
2036
  }
2037
+ for (const [role, source] of Object.entries(ROLE_DEFAULTS)) {
2038
+ if (pinned[role]) continue;
2039
+ if (BASE_OF[role] && pinned[BASE_OF[role]]) continue;
2040
+ const sourceMoved = Boolean(pinned[source] || BASE_OF[source] && pinned[BASE_OF[source]]);
2041
+ if (sourceMoved && colors[source]) colors[role] = colors[source];
2042
+ }
2043
+ const action = colors.action ? hexToOklch(colors.action) : null;
2044
+ if (action && colors.action !== generatedAction) {
2045
+ const states = interactionStates(action, mode);
2046
+ colors["action-hover"] = states.hover;
2047
+ colors["action-pressed"] = states.pressed;
2048
+ if (!pinned["action-foreground"] && !pinned["primary-foreground"]) colors["action-foreground"] = states.foreground;
2049
+ }
1974
2050
  return {
1975
2051
  colors,
1976
2052
  radius: deriveRadius(design.radius),
@@ -2027,14 +2103,14 @@ function cssVarOverrides(resolved, base = SHIPPED_THEME.light) {
2027
2103
  return out;
2028
2104
  }
2029
2105
  var varLines = (resolved, base) => Object.entries(cssVarOverrides(resolved, base)).map(([token, value]) => declaration(token, value));
2030
- function exportCss(theme2, options = {}) {
2106
+ function exportCss(theme, options = {}) {
2031
2107
  const rootSelector = options.rootSelector ?? ":root";
2032
2108
  const darkSelector = options.darkSelector ?? ".dark";
2033
- const rootVars = varLines(theme2.light, SHIPPED_THEME.light);
2034
- const root = [...colorLines(theme2.light, SHIPPED_THEME.light), ...rootVars];
2109
+ const rootVars = varLines(theme.light, SHIPPED_THEME.light);
2110
+ const root = [...colorLines(theme.light, SHIPPED_THEME.light), ...rootVars];
2035
2111
  const dark = [
2036
- ...colorLines(theme2.dark, SHIPPED_THEME.dark),
2037
- ...varLines(theme2.dark, SHIPPED_THEME.dark).filter((line) => !rootVars.includes(line))
2112
+ ...colorLines(theme.dark, SHIPPED_THEME.dark),
2113
+ ...varLines(theme.dark, SHIPPED_THEME.dark).filter((line) => !rootVars.includes(line))
2038
2114
  ];
2039
2115
  return [block(rootSelector, root), block(darkSelector, dark)].filter(Boolean).join("\n\n");
2040
2116
  }
@@ -2127,7 +2203,9 @@ var FLUTTER_COLOR_FIELDS = [
2127
2203
  ["info", "info"],
2128
2204
  ["infoForeground", "info-foreground"],
2129
2205
  ["popover", "popover"],
2130
- ["popoverForeground", "popover-foreground"]
2206
+ ["popoverForeground", "popover-foreground"],
2207
+ ["onInfoContainer", "on-info-container"],
2208
+ ["scrim", "scrim"]
2131
2209
  ];
2132
2210
  var FLUTTER_CHART_STOPS = [1, 2, 3, 4, 5];
2133
2211
  var shippedRef = (appearance, field) => `KinetixColors.${appearance}.${field}`;
@@ -2147,8 +2225,8 @@ function colorSet(mode, appearance) {
2147
2225
  );
2148
2226
  return [...fields, " chart: <Color>[", ...chart, " ],"].join("\n");
2149
2227
  }
2150
- function header(symbol, theme2) {
2151
- const design = theme2.meta.design;
2228
+ function header(symbol, theme) {
2229
+ const design = theme.meta.design;
2152
2230
  return [
2153
2231
  "//",
2154
2232
  `// ${symbol} \u2014 a KinetixUI Create theme.`,
@@ -2167,9 +2245,9 @@ function header(symbol, theme2) {
2167
2245
  "// They apply on the web. That is the only thing this file does not carry.",
2168
2246
  "//",
2169
2247
  "// A field written as `KinetixColors.light.\u2026` is one this design did not change, and it keeps",
2170
- "// following the library. Six of them \u2014 tertiary, tertiaryForeground, info, infoForeground,",
2171
- "// successForeground and warningForeground \u2014 are always references, because Create does not model",
2172
- "// those tokens.",
2248
+ "// following the library. Eight of them \u2014 tertiary, tertiaryForeground, info, infoForeground,",
2249
+ "// onInfoContainer, scrim, successForeground and warningForeground \u2014 are always references, because",
2250
+ "// Create does not model those tokens.",
2173
2251
  "//",
2174
2252
  "// These are `static final` rather than `static const`: Dart's constant expressions do not include",
2175
2253
  "// instance field access, so a `static const` could not reference the shipped palette. The theme is",
@@ -2186,12 +2264,12 @@ function header(symbol, theme2) {
2186
2264
  "//"
2187
2265
  ].join("\n");
2188
2266
  }
2189
- function exportFlutter(theme2, options = {}) {
2267
+ function exportFlutter(theme, options = {}) {
2190
2268
  const symbol = options.symbol ?? DEFAULT_FLUTTER_SYMBOL;
2191
2269
  const reason = dartSymbolError(symbol);
2192
2270
  if (reason) throw new Error(reason);
2193
2271
  return [
2194
- header(symbol, theme2),
2272
+ header(symbol, theme),
2195
2273
  "",
2196
2274
  "import 'package:flutter/widgets.dart';",
2197
2275
  "import 'package:kinetix_ui/kinetix_ui.dart';",
@@ -2201,11 +2279,11 @@ function exportFlutter(theme2, options = {}) {
2201
2279
  // `enum` and Compose's `object` express.
2202
2280
  `abstract final class ${symbol} {`,
2203
2281
  " static final KinetixColors light = KinetixColors(",
2204
- colorSet(theme2.light, "light"),
2282
+ colorSet(theme.light, "light"),
2205
2283
  " );",
2206
2284
  "",
2207
2285
  " static final KinetixColors dark = KinetixColors(",
2208
- colorSet(theme2.dark, "dark"),
2286
+ colorSet(theme.dark, "dark"),
2209
2287
  " );",
2210
2288
  "}",
2211
2289
  ""
@@ -2295,7 +2373,9 @@ var COMPOSE_COLOR_FIELDS = [
2295
2373
  ["info", "info"],
2296
2374
  ["infoForeground", "info-foreground"],
2297
2375
  ["popover", "popover"],
2298
- ["popoverForeground", "popover-foreground"]
2376
+ ["popoverForeground", "popover-foreground"],
2377
+ ["onInfoContainer", "on-info-container"],
2378
+ ["scrim", "scrim"]
2299
2379
  ];
2300
2380
  var COMPOSE_CHART_STOPS = [1, 2, 3, 4, 5];
2301
2381
  var COMPOSE_UNMAPPED_PRESET_ROLES = ["input", "ring"];
@@ -2318,8 +2398,8 @@ function colorSet2(mode, appearance) {
2318
2398
  );
2319
2399
  return [...fields, " chart = listOf(", ...chart, " ),"].join("\n");
2320
2400
  }
2321
- function header2(symbol, theme2) {
2322
- const design = theme2.meta.design;
2401
+ function header2(symbol, theme) {
2402
+ const design = theme.meta.design;
2323
2403
  return [
2324
2404
  "//",
2325
2405
  `// ${symbol} \u2014 a KinetixUI Create theme.`,
@@ -2349,15 +2429,15 @@ function header2(symbol, theme2) {
2349
2429
  "//"
2350
2430
  ].join("\n");
2351
2431
  }
2352
- function exportCompose(theme2, options = {}) {
2432
+ function exportCompose(theme, options = {}) {
2353
2433
  const symbol = options.symbol ?? DEFAULT_COMPOSE_SYMBOL;
2354
2434
  const reason = kotlinSymbolError(symbol);
2355
2435
  if (reason) throw new Error(reason);
2356
- const light = colorSet2(theme2.light, "light");
2357
- const dark = colorSet2(theme2.dark, "dark");
2436
+ const light = colorSet2(theme.light, "light");
2437
+ const dark = colorSet2(theme.dark, "dark");
2358
2438
  const usesColor = `${light}${dark}`.includes("Color(0x");
2359
2439
  return [
2360
- header2(symbol, theme2),
2440
+ header2(symbol, theme),
2361
2441
  "",
2362
2442
  "package com.kinetixui.create",
2363
2443
  "",
@@ -2468,6 +2548,59 @@ function swiftColor(hex) {
2468
2548
  const [r, g, b] = swiftUiChannels(`#${h}`);
2469
2549
  return `Color(red: ${r}, green: ${g}, blue: ${b})`;
2470
2550
  }
2551
+ function swiftShadowColor(hex) {
2552
+ const h = hex.replace("#", "").trim();
2553
+ if (!/^[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$/.test(h)) return null;
2554
+ const base = h.slice(0, 6) === "000000" ? "Color.black" : swiftColor(`#${h.slice(0, 6)}`);
2555
+ if (!base) return null;
2556
+ if (h.length === 6) return base;
2557
+ const alpha = Number((parseInt(h.slice(6, 8), 16) / 255).toFixed(3));
2558
+ return `${base}.opacity(${alpha})`;
2559
+ }
2560
+ var RADIUS_STEPS2 = ["sm", "md", "lg", "xl"];
2561
+ var ELEVATION_STEPS = ["sm", "md", "lg", "xl"];
2562
+ var sameLayers = (a, b) => a.length === b.length && a.every((layer, i) => {
2563
+ const other = b[i];
2564
+ return layer.color.toLowerCase() === other.color.toLowerCase() && layer.offsetX === other.offsetX && layer.offsetY === other.offsetY && layer.blur === other.blur && layer.spread === other.spread;
2565
+ });
2566
+ var swiftLength = (value) => Number(Number(value).toFixed(3));
2567
+ function swiftRadii(radius) {
2568
+ if (RADIUS_STEPS2.every((step) => radius[step] === SHIPPED_RADIUS[step])) return "KinetixRadii.default";
2569
+ return [
2570
+ "KinetixRadii(",
2571
+ ` field: ${swiftLength(String(radius.sm))},`,
2572
+ ` control: ${swiftLength(String(radius.md))},`,
2573
+ ` container: ${swiftLength(String(radius.lg))},`,
2574
+ ` surface: ${swiftLength(String(radius.xl))}`,
2575
+ " )"
2576
+ ].join("\n");
2577
+ }
2578
+ function swiftElevations(elevation) {
2579
+ if (ELEVATION_STEPS.every((s) => sameLayers(elevation[s], SHIPPED_ELEVATION[s]))) return "KinetixElevations.default";
2580
+ const step = (layers) => {
2581
+ if (layers.length === 0) return ".none";
2582
+ const rendered = layers.map((layer) => {
2583
+ const color = swiftShadowColor(layer.color) ?? "Color.black.opacity(0.05)";
2584
+ return [
2585
+ " KinetixShadowLayer(",
2586
+ ` color: ${color},`,
2587
+ ` radius: ${swiftLength(layer.blur)},`,
2588
+ ` x: ${swiftLength(layer.offsetX)},`,
2589
+ ` y: ${swiftLength(layer.offsetY)}`,
2590
+ " ),"
2591
+ ].join("\n");
2592
+ });
2593
+ return ["KinetixElevation([", ...rendered, " ])"].join("\n");
2594
+ };
2595
+ return [
2596
+ "KinetixElevations(",
2597
+ ` sm: ${step(elevation.sm)},`,
2598
+ ` md: ${step(elevation.md)},`,
2599
+ ` lg: ${step(elevation.lg)},`,
2600
+ ` xl: ${step(elevation.xl)}`,
2601
+ " )"
2602
+ ].join("\n");
2603
+ }
2471
2604
  var SWIFT_COLOR_FIELDS = [
2472
2605
  "primary",
2473
2606
  "primary-foreground",
@@ -2503,7 +2636,9 @@ var SWIFT_COLOR_FIELDS = [
2503
2636
  "info",
2504
2637
  "info-foreground",
2505
2638
  "popover",
2506
- "popover-foreground"
2639
+ "popover-foreground",
2640
+ "on-info-container",
2641
+ "scrim"
2507
2642
  ];
2508
2643
  var SWIFT_CHART_STOPS = [1, 2, 3, 4, 5];
2509
2644
  var SHIPPED_ENUM = { light: "KinetixColorsSwiftUI", dark: "KinetixColorsSwiftUIDark" };
@@ -2524,8 +2659,8 @@ function colorSet3(mode, appearance) {
2524
2659
  );
2525
2660
  return [...fields, " chart: [", ...chart, " ]"].join("\n");
2526
2661
  }
2527
- function header3(symbol, theme2) {
2528
- const design = theme2.meta.design;
2662
+ function header3(symbol, theme) {
2663
+ const design = theme.meta.design;
2529
2664
  return [
2530
2665
  "//",
2531
2666
  `// ${symbol} \u2014 a KinetixUI Create theme.`,
@@ -2534,38 +2669,50 @@ function header3(symbol, theme2) {
2534
2669
  "// editing this file: it is a rendering of a preset, not a source of one.",
2535
2670
  "//",
2536
2671
  `// Theme colour ${design.brand} \xB7 ${design.neutral} neutral \xB7 ${design.chartPalette} charts`,
2672
+ `// Radius ${design.radius} \xB7 surface ${design.surface}`,
2673
+ "//",
2674
+ "// Colours, corner radii and elevation \u2014 the whole design this file can express. Radii and",
2675
+ "// elevation are one value rather than a light/dark pair, because the runtime takes one and the",
2676
+ "// two appearances resolve to the same ladder.",
2537
2677
  "//",
2538
- "// COLOURS ONLY \u2014 and that is now a limit of this exporter rather than of the package. SwiftUI",
2539
- "// gained `KinetixRadii` and `KinetixElevations` alongside `KinetixColors`, so there is somewhere",
2540
- `// for a radius to land; this file does not write one yet. This design's radius (${design.radius}) and`,
2541
- `// surface treatment (${design.surface}) are therefore NOT carried here \u2014 set them by hand for now:`,
2542
- "// `KinetixTheme(light: \u2026, dark: \u2026, radii: \u2026, elevations: \u2026)`. Nothing is silently dropped.",
2678
+ "// ONE THING DOES NOT SURVIVE THE PORT. CSS shadows have `spread`; SwiftUI's `.shadow` has no",
2679
+ "// equivalent, so the spread on this design's larger steps is dropped and they render slightly",
2680
+ "// wider here than on the web. The shipped `KinetixElevations.default` is mapped the same way, so",
2681
+ "// a generated theme and the built-in one differ from the web in exactly the same manner.",
2543
2682
  "//",
2544
2683
  "// A field written as `KinetixColorsSwiftUI.\u2026` is one this design did not change, and it keeps",
2545
2684
  "// following the library.",
2546
2685
  "//"
2547
2686
  ].join("\n");
2548
2687
  }
2549
- function exportSwiftUi(theme2, options = {}) {
2688
+ function exportSwiftUi(theme, options = {}) {
2550
2689
  const symbol = options.symbol ?? DEFAULT_SWIFT_SYMBOL;
2551
2690
  const reason = swiftSymbolError(symbol);
2552
2691
  if (reason) throw new Error(reason);
2553
2692
  return [
2554
- header3(symbol, theme2),
2693
+ header3(symbol, theme),
2555
2694
  "",
2556
2695
  "import SwiftUI",
2557
2696
  "import KinetixUI",
2558
2697
  "",
2559
2698
  `public enum ${symbol} {`,
2560
- " /// Apply with `KinetixTheme(light: .light, dark: .dark) { \u2026 }`, or set",
2561
- " /// `\\.kinetixColors` directly to pin one appearance.",
2699
+ " /// Apply the whole design with",
2700
+ ` /// \`KinetixTheme(light: .light, dark: .dark, radii: .radii, elevations: .elevations) { \u2026 }\`,`,
2701
+ " /// or set `\\.kinetixColors` directly to pin one appearance.",
2562
2702
  ` public static let light = KinetixColors(`,
2563
- colorSet3(theme2.light, "light"),
2703
+ colorSet3(theme.light, "light"),
2564
2704
  " )",
2565
2705
  "",
2566
2706
  ` public static let dark = KinetixColors(`,
2567
- colorSet3(theme2.dark, "dark"),
2707
+ colorSet3(theme.dark, "dark"),
2568
2708
  " )",
2709
+ "",
2710
+ // One value, not a light/dark pair: the runtime takes one, and the resolver produces the same
2711
+ // ladder for both appearances — a card's corner does not change when the lights go out.
2712
+ // `swiftui.test.ts` asserts that equality rather than trusting it.
2713
+ ` public static let radii = ${swiftRadii(theme.light.radius)}`,
2714
+ "",
2715
+ ` public static let elevations = ${swiftElevations(theme.light.elevation)}`,
2569
2716
  "}",
2570
2717
  ""
2571
2718
  ].join("\n");
@@ -2637,7 +2784,10 @@ async function presetSwiftUi(input, opts) {
2637
2784
  const swift = exportSwiftUi(resolveCreateTheme(read(input)), { symbol });
2638
2785
  if (opts.output) {
2639
2786
  await writeFile3(opts.output, swift, "utf8");
2640
- console.log(pc8.green("\u2714"), `Wrote ${opts.output} \u2014 apply it with KinetixTheme(light: ${symbol}.light, dark: ${symbol}.dark)`);
2787
+ console.log(
2788
+ pc8.green("\u2714"),
2789
+ `Wrote ${opts.output} \u2014 apply it with KinetixTheme(light: ${symbol}.light, dark: ${symbol}.dark, radii: ${symbol}.radii, elevations: ${symbol}.elevations)`
2790
+ );
2641
2791
  return;
2642
2792
  }
2643
2793
  process.stdout.write(swift);
@@ -2886,7 +3036,7 @@ async function themeBuild(name, options) {
2886
3036
  // package.json
2887
3037
  var package_default = {
2888
3038
  name: "@kinetixui/cli",
2889
- version: "0.23.2",
3039
+ version: "0.24.0",
2890
3040
  description: "Add KinetixUI components and tokens to your project.",
2891
3041
  type: "module",
2892
3042
  license: "MIT",
@@ -2900,8 +3050,10 @@ var package_default = {
2900
3050
  ],
2901
3051
  scripts: {
2902
3052
  build: "tsup src/index.ts --format esm --clean",
2903
- typecheck: "tsc --noEmit",
2904
- dev: 'tsup src/index.ts --format esm --watch --onSuccess "node dist/index.js"'
3053
+ dev: 'tsup src/index.ts --format esm --watch --onSuccess "node dist/index.js"',
3054
+ test: "vitest run",
3055
+ "test:watch": "vitest",
3056
+ typecheck: "tsc --noEmit"
2905
3057
  },
2906
3058
  dependencies: {
2907
3059
  commander: "^15.0.0",
@@ -2911,10 +3063,12 @@ var package_default = {
2911
3063
  devDependencies: {
2912
3064
  "@kinetixui/create-preset": "workspace:*",
2913
3065
  "@kinetixui/create-theme": "workspace:*",
2914
- "@types/node": "^22.20.3",
3066
+ "@types/node": "^22.20.4",
2915
3067
  "@types/prompts": "^2.4.9",
3068
+ jsdom: "^30.1.0",
2916
3069
  tsup: "^8.5.1",
2917
- typescript: "^5.9.3"
3070
+ typescript: "^5.9.3",
3071
+ vitest: "^5.0.2"
2918
3072
  },
2919
3073
  engines: {
2920
3074
  node: ">=18"
@@ -2925,129 +3079,134 @@ var package_default = {
2925
3079
  }
2926
3080
  };
2927
3081
 
3082
+ // src/program.ts
3083
+ function createProgram() {
3084
+ const program = new Command();
3085
+ program.name("kinetixui").description("Add KinetixUI components and tokens to your project.").version(package_default.version);
3086
+ program.command("init").description("Set up kinetixui.json and pull in the token contract.").option("-y, --yes", "skip prompts and use defaults", false).option("-r, --registry <url>", "registry base URL", DEFAULT_REGISTRY).action(async (opts) => {
3087
+ try {
3088
+ await init({ registry: opts.registry, yes: opts.yes });
3089
+ } catch (err) {
3090
+ console.error(pc10.red("\u2716"), err.message);
3091
+ process.exitCode = 1;
3092
+ }
3093
+ });
3094
+ program.command("add").description("Add one or more components to your project.").argument("[components...]", "component names, e.g. button card").option("-a, --all", "add every component in the registry", false).option("-o, --overwrite", "overwrite existing files", false).option("-r, --registry <url>", "registry base URL", DEFAULT_REGISTRY).action(async (components, opts) => {
3095
+ try {
3096
+ await add(components, { registry: opts.registry, overwrite: opts.overwrite, all: opts.all });
3097
+ } catch (err) {
3098
+ console.error(pc10.red("\u2716"), err.message);
3099
+ process.exitCode = 1;
3100
+ }
3101
+ });
3102
+ program.command("list").description("List every component available in the registry.").option("-r, --registry <url>", "registry base URL", DEFAULT_REGISTRY).action(async (opts) => {
3103
+ try {
3104
+ await list({ registry: opts.registry });
3105
+ } catch (err) {
3106
+ console.error(pc10.red("\u2716"), err.message);
3107
+ process.exitCode = 1;
3108
+ }
3109
+ });
3110
+ program.command("inspect").description("Show everything the registry knows about one component.").argument("<name>", "component name, e.g. button").option("-r, --registry <url>", "registry base URL", DEFAULT_REGISTRY).action(async (name, opts) => {
3111
+ try {
3112
+ await inspect(name, { registry: opts.registry });
3113
+ } catch (err) {
3114
+ console.error(pc10.red("\u2716"), err.message);
3115
+ process.exitCode = 1;
3116
+ }
3117
+ });
3118
+ 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) => {
3119
+ try {
3120
+ await parity(components, { registry: opts.registry });
3121
+ } catch (err) {
3122
+ console.error(pc10.red("\u2716"), err.message);
3123
+ process.exitCode = 1;
3124
+ }
3125
+ });
3126
+ 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) => {
3127
+ try {
3128
+ await lint(target, { fail: opts.fail });
3129
+ } catch (err) {
3130
+ console.error(pc10.red("\u2716"), err.message);
3131
+ process.exitCode = 1;
3132
+ }
3133
+ });
3134
+ program.command("doctor").description("Check kinetixui.json, its aliases, and the registry connection.").action(async () => {
3135
+ try {
3136
+ await doctor();
3137
+ } catch (err) {
3138
+ console.error(pc10.red("\u2716"), err.message);
3139
+ process.exitCode = 1;
3140
+ }
3141
+ });
3142
+ const theme = program.command("theme").description("Scaffold and compile a local token override (CSS only \u2014 see 'theme build --help').");
3143
+ 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) => {
3144
+ try {
3145
+ await themeCreate(name, { force: opts.force });
3146
+ } catch (err) {
3147
+ console.error(pc10.red("\u2716"), err.message);
3148
+ process.exitCode = 1;
3149
+ }
3150
+ });
3151
+ theme.command("build").description("Compile kinetixui-themes/<name>.csv to CSS and print a WCAG AA contrast report. This command is CSS only; native themes come from `preset swiftui|compose|flutter`.").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) => {
3152
+ try {
3153
+ await themeBuild(name, { failOnContrast: opts.failOnContrast });
3154
+ } catch (err) {
3155
+ console.error(pc10.red("\u2716"), err.message);
3156
+ process.exitCode = 1;
3157
+ }
3158
+ });
3159
+ const preset = program.command("preset").description("Read a Kinetix Create preset code (KX1_\u2026) produced by kinetixui.com/create.");
3160
+ preset.command("decode").description("Show what a preset contains. Accepts a KX1_ code or a full /create?preset=\u2026 URL.").argument("<preset>", "a KX1_ code, or a share URL").option("--json", "print the configuration as JSON", false).action((input, opts) => {
3161
+ try {
3162
+ presetDecode(input, { json: opts.json });
3163
+ } catch (err) {
3164
+ console.error(pc10.red("\u2716"), err.message);
3165
+ process.exitCode = 1;
3166
+ }
3167
+ });
3168
+ preset.command("css").description("Resolve a preset into its web CSS override block \u2014 the same output the workspace exports for Web CSS. This command is CSS only; see `preset swiftui|compose|flutter`.").argument("<preset>", "a KX1_ code, or a share URL").option("-o, --output <file>", "write to a file instead of stdout").action(async (input, opts) => {
3169
+ try {
3170
+ await presetCss(input, { output: opts.output });
3171
+ } catch (err) {
3172
+ console.error(pc10.red("\u2716"), err.message);
3173
+ process.exitCode = 1;
3174
+ }
3175
+ });
3176
+ preset.command("swiftui").description("Resolve a preset into a SwiftUI theme file (KinetixColors, light and dark). Colours only \u2014 SwiftUI has no radius or elevation token to carry. No Compose or Flutter output.").argument("<preset>", "a KX1_ code, or a share URL").option("-o, --output <file>", "write to a file instead of stdout").option("-n, --name <symbol>", "the Swift enum to generate", DEFAULT_SWIFT_SYMBOL).action(async (input, opts) => {
3177
+ try {
3178
+ await presetSwiftUi(input, { output: opts.output, name: opts.name });
3179
+ } catch (err) {
3180
+ console.error(pc10.red("\u2716"), err.message);
3181
+ process.exitCode = 1;
3182
+ }
3183
+ });
3184
+ preset.command("compose").description("Resolve a preset into a Jetpack Compose theme file (KinetixColors, light and dark). Colours only \u2014 Compose has no runtime radius or elevation token to carry. No Flutter or Android XML output.").argument("<preset>", "a KX1_ code, or a share URL").option("-o, --output <file>", "write to a file instead of stdout").option("-n, --name <symbol>", "the Kotlin object to generate", DEFAULT_COMPOSE_SYMBOL).action(async (input, opts) => {
3185
+ try {
3186
+ await presetCompose(input, { output: opts.output, name: opts.name });
3187
+ } catch (err) {
3188
+ console.error(pc10.red("\u2716"), err.message);
3189
+ process.exitCode = 1;
3190
+ }
3191
+ });
3192
+ preset.command("flutter").description("Resolve a preset into a Flutter theme file (KinetixColors, light and dark). Colours only \u2014 Flutter's radius and elevation are not runtime-themeable. No Android XML output.").argument("<preset>", "a KX1_ code, or a share URL").option("-o, --output <file>", "write to a file instead of stdout").option("-n, --name <symbol>", "the Dart class to generate", DEFAULT_FLUTTER_SYMBOL).action(async (input, opts) => {
3193
+ try {
3194
+ await presetFlutter(input, { output: opts.output, name: opts.name });
3195
+ } catch (err) {
3196
+ console.error(pc10.red("\u2716"), err.message);
3197
+ process.exitCode = 1;
3198
+ }
3199
+ });
3200
+ preset.command("url").description("Print the canonical share URL for a preset. Prints it \u2014 piping it to your opener is up to you.").argument("<preset>", "a KX1_ code, or a share URL").option("-s, --site <origin>", "site origin", "https://kinetixui.com").action((input, opts) => {
3201
+ try {
3202
+ presetUrlCommand(input, { site: opts.site });
3203
+ } catch (err) {
3204
+ console.error(pc10.red("\u2716"), err.message);
3205
+ process.exitCode = 1;
3206
+ }
3207
+ });
3208
+ return program;
3209
+ }
3210
+
2928
3211
  // src/index.ts
2929
- var program = new Command();
2930
- program.name("kinetixui").description("Add KinetixUI components and tokens to your project.").version(package_default.version);
2931
- program.command("init").description("Set up kinetixui.json and pull in the token contract.").option("-y, --yes", "skip prompts and use defaults", false).option("-r, --registry <url>", "registry base URL", DEFAULT_REGISTRY).action(async (opts) => {
2932
- try {
2933
- await init({ registry: opts.registry, yes: opts.yes });
2934
- } catch (err) {
2935
- console.error(pc10.red("\u2716"), err.message);
2936
- process.exitCode = 1;
2937
- }
2938
- });
2939
- program.command("add").description("Add one or more components to your project.").argument("[components...]", "component names, e.g. button card").option("-a, --all", "add every component in the registry", false).option("-o, --overwrite", "overwrite existing files", false).option("-r, --registry <url>", "registry base URL", DEFAULT_REGISTRY).action(async (components, opts) => {
2940
- try {
2941
- await add(components, { registry: opts.registry, overwrite: opts.overwrite, all: opts.all });
2942
- } catch (err) {
2943
- console.error(pc10.red("\u2716"), err.message);
2944
- process.exitCode = 1;
2945
- }
2946
- });
2947
- program.command("list").description("List every component available in the registry.").option("-r, --registry <url>", "registry base URL", DEFAULT_REGISTRY).action(async (opts) => {
2948
- try {
2949
- await list({ registry: opts.registry });
2950
- } catch (err) {
2951
- console.error(pc10.red("\u2716"), err.message);
2952
- process.exitCode = 1;
2953
- }
2954
- });
2955
- program.command("inspect").description("Show everything the registry knows about one component.").argument("<name>", "component name, e.g. button").option("-r, --registry <url>", "registry base URL", DEFAULT_REGISTRY).action(async (name, opts) => {
2956
- try {
2957
- await inspect(name, { registry: opts.registry });
2958
- } catch (err) {
2959
- console.error(pc10.red("\u2716"), err.message);
2960
- process.exitCode = 1;
2961
- }
2962
- });
2963
- 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) => {
2964
- try {
2965
- await parity(components, { registry: opts.registry });
2966
- } catch (err) {
2967
- console.error(pc10.red("\u2716"), err.message);
2968
- process.exitCode = 1;
2969
- }
2970
- });
2971
- 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) => {
2972
- try {
2973
- await lint(target, { fail: opts.fail });
2974
- } catch (err) {
2975
- console.error(pc10.red("\u2716"), err.message);
2976
- process.exitCode = 1;
2977
- }
2978
- });
2979
- program.command("doctor").description("Check kinetixui.json, its aliases, and the registry connection.").action(async () => {
2980
- try {
2981
- await doctor();
2982
- } catch (err) {
2983
- console.error(pc10.red("\u2716"), err.message);
2984
- process.exitCode = 1;
2985
- }
2986
- });
2987
- var theme = program.command("theme").description("Scaffold and compile a local token override (CSS only \u2014 see 'theme build --help').");
2988
- 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) => {
2989
- try {
2990
- await themeCreate(name, { force: opts.force });
2991
- } catch (err) {
2992
- console.error(pc10.red("\u2716"), err.message);
2993
- process.exitCode = 1;
2994
- }
2995
- });
2996
- theme.command("build").description("Compile kinetixui-themes/<name>.csv to CSS and print a WCAG AA contrast report. This command is CSS only; native themes come from `preset swiftui|compose|flutter`.").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) => {
2997
- try {
2998
- await themeBuild(name, { failOnContrast: opts.failOnContrast });
2999
- } catch (err) {
3000
- console.error(pc10.red("\u2716"), err.message);
3001
- process.exitCode = 1;
3002
- }
3003
- });
3004
- var preset = program.command("preset").description("Read a Kinetix Create preset code (KX1_\u2026) produced by kinetixui.com/create.");
3005
- preset.command("decode").description("Show what a preset contains. Accepts a KX1_ code or a full /create?preset=\u2026 URL.").argument("<preset>", "a KX1_ code, or a share URL").option("--json", "print the configuration as JSON", false).action((input, opts) => {
3006
- try {
3007
- presetDecode(input, { json: opts.json });
3008
- } catch (err) {
3009
- console.error(pc10.red("\u2716"), err.message);
3010
- process.exitCode = 1;
3011
- }
3012
- });
3013
- preset.command("css").description("Resolve a preset into its web CSS override block \u2014 the same output the workspace exports for Web CSS. This command is CSS only; see `preset swiftui|compose|flutter`.").argument("<preset>", "a KX1_ code, or a share URL").option("-o, --output <file>", "write to a file instead of stdout").action(async (input, opts) => {
3014
- try {
3015
- await presetCss(input, { output: opts.output });
3016
- } catch (err) {
3017
- console.error(pc10.red("\u2716"), err.message);
3018
- process.exitCode = 1;
3019
- }
3020
- });
3021
- preset.command("swiftui").description("Resolve a preset into a SwiftUI theme file (KinetixColors, light and dark). Colours only \u2014 SwiftUI has no radius or elevation token to carry. No Compose or Flutter output.").argument("<preset>", "a KX1_ code, or a share URL").option("-o, --output <file>", "write to a file instead of stdout").option("-n, --name <symbol>", "the Swift enum to generate", DEFAULT_SWIFT_SYMBOL).action(async (input, opts) => {
3022
- try {
3023
- await presetSwiftUi(input, { output: opts.output, name: opts.name });
3024
- } catch (err) {
3025
- console.error(pc10.red("\u2716"), err.message);
3026
- process.exitCode = 1;
3027
- }
3028
- });
3029
- preset.command("compose").description("Resolve a preset into a Jetpack Compose theme file (KinetixColors, light and dark). Colours only \u2014 Compose has no runtime radius or elevation token to carry. No Flutter or Android XML output.").argument("<preset>", "a KX1_ code, or a share URL").option("-o, --output <file>", "write to a file instead of stdout").option("-n, --name <symbol>", "the Kotlin object to generate", DEFAULT_COMPOSE_SYMBOL).action(async (input, opts) => {
3030
- try {
3031
- await presetCompose(input, { output: opts.output, name: opts.name });
3032
- } catch (err) {
3033
- console.error(pc10.red("\u2716"), err.message);
3034
- process.exitCode = 1;
3035
- }
3036
- });
3037
- preset.command("flutter").description("Resolve a preset into a Flutter theme file (KinetixColors, light and dark). Colours only \u2014 Flutter's radius and elevation are not runtime-themeable. No Android XML output.").argument("<preset>", "a KX1_ code, or a share URL").option("-o, --output <file>", "write to a file instead of stdout").option("-n, --name <symbol>", "the Dart class to generate", DEFAULT_FLUTTER_SYMBOL).action(async (input, opts) => {
3038
- try {
3039
- await presetFlutter(input, { output: opts.output, name: opts.name });
3040
- } catch (err) {
3041
- console.error(pc10.red("\u2716"), err.message);
3042
- process.exitCode = 1;
3043
- }
3044
- });
3045
- preset.command("url").description("Print the canonical share URL for a preset. Prints it \u2014 piping it to your opener is up to you.").argument("<preset>", "a KX1_ code, or a share URL").option("-s, --site <origin>", "site origin", "https://kinetixui.com").action((input, opts) => {
3046
- try {
3047
- presetUrlCommand(input, { site: opts.site });
3048
- } catch (err) {
3049
- console.error(pc10.red("\u2716"), err.message);
3050
- process.exitCode = 1;
3051
- }
3052
- });
3053
- program.parseAsync();
3212
+ createProgram().parseAsync();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kinetixui/cli",
3
- "version": "0.23.2",
3
+ "version": "0.24.0",
4
4
  "description": "Add KinetixUI components and tokens to your project.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -23,10 +23,12 @@
23
23
  "devDependencies": {
24
24
  "@kinetixui/create-preset": "0.0.0",
25
25
  "@kinetixui/create-theme": "0.0.0",
26
- "@types/node": "^22.20.3",
26
+ "@types/node": "^22.20.4",
27
27
  "@types/prompts": "^2.4.9",
28
+ "jsdom": "^30.1.0",
28
29
  "tsup": "^8.5.1",
29
- "typescript": "^5.9.3"
30
+ "typescript": "^5.9.3",
31
+ "vitest": "^5.0.2"
30
32
  },
31
33
  "engines": {
32
34
  "node": ">=18"
@@ -37,7 +39,9 @@
37
39
  },
38
40
  "scripts": {
39
41
  "build": "tsup src/index.ts --format esm --clean",
40
- "typecheck": "tsc --noEmit",
41
- "dev": "tsup src/index.ts --format esm --watch --onSuccess \"node dist/index.js\""
42
+ "dev": "tsup src/index.ts --format esm --watch --onSuccess \"node dist/index.js\"",
43
+ "test": "vitest run",
44
+ "test:watch": "vitest",
45
+ "typecheck": "tsc --noEmit"
42
46
  }
43
47
  }