@jirawatpyk/aura-react 5.3.0 → 5.4.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/README.md CHANGED
@@ -41,7 +41,7 @@ npm i @jirawatpyk/aura-react @jirawatpyk/aura-tokens # React 18 or 19; import
41
41
  npm i @aura/react@npm:@jirawatpyk/aura-react @aura/tokens@npm:@jirawatpyk/aura-tokens
42
42
  ```
43
43
 
44
- React 18.3 and 19 are both tested: the dev dependencies pin 18, and CI switches the whole workspace to 19 (`node scripts/use-react.mjs 19 && npm install`) and runs every suite again — contrast, token lint, SSR, types, build, hydration with 0 warnings, layout before hydration, the three pilot pages and Storybook (axe + behaviour).
44
+ React 18.3 and 19 are both tested: the dev dependencies pin 18, and CI switches the whole workspace to 19 (`node scripts/use-react.mts 19 && npm install`) and runs every suite again — contrast, token lint, SSR, types, build, hydration with 0 warnings, layout before hydration, the three pilot pages and Storybook (axe + behaviour).
45
45
 
46
46
  Public on npmjs; internal projects can use GitHub Packages instead ([repository README](https://github.com/Jirawatpyk/Aura-design#use-it-in-a-project)).
47
47
 
@@ -186,6 +186,13 @@ DataTable with sort and page in the URL (4.17): `onStateChange={({ sort, page })
186
186
  - **Time zone**: `timeZone` on AuraProvider, DatePicker, DateRangePicker and Calendar decides "today" (the marker, `min`/`max="today"`, the first month shown); or pass `today` as an ISO date. `todayIn('Asia/Bangkok')` is exported from the root and `/server`.
187
187
  - **Toasts** queue past three instead of dropping: six in a row all show, in order, three at a time.
188
188
 
189
+ ## 5.4 — types, and TypeScript across the repo
190
+
191
+ - **DataTable** takes rows of your own interface (`rows={orders}` with `interface Order {…}`; readonly arrays too), and `render`, `sortValue`, `getRowHref` and `onRowActivate` can be written with it: `render: (r: Order) => baht(r.amount)` — no cast from `Record<string, any>`.
192
+ - **FormErrorSummary** `errors` accepts a nested object written by hand (`{ address: { street: { message } } }`, arrays for field arrays), not only react-hook-form's `formState.errors`. New type `FormErrorTree`.
193
+ - **Select** `label` is optional when `aria-label` or `aria-labelledby` names the field (a toolbar, a table cell); development builds warn when it has no name at all. Every other field still requires `label`.
194
+ - The example pages (`examples/*`) are TypeScript and type-checked in CI, as are the stories and the repo's scripts; the pilot checks run on Playwright Test instead of Python.
195
+
189
196
  ## 5.3 — Select opens AURA's own list
190
197
 
191
198
  - **Select** no longer hands its list to the operating system (a white list in dark mode on Windows, a different look on every OS). The field is a button that opens an AURA list like Combobox: tokens in light and dark, a check on the chosen item, `optgroup` headings, disabled options skipped, type to jump (Thai too), Home/End/PageUp/PageDown, Escape closes the list before a dialog. The same list is used on phones (as in shadcn), not the native picker.
@@ -311,11 +318,13 @@ Every optional prop takes `undefined` (4.16), so apps on `exactOptionalPropertyT
311
318
  ```bash
312
319
  npm run build # dist/ (ESM, CJS, IIFE, CSS, types)
313
320
  npm run test:ssr # server-render every component
314
- npm run typecheck # tsc --strict over src/, then types-test/usage.tsx against the public API
321
+ npm run typecheck # tsc --strict over src/, types-test/ against the published API, and the example pages
315
322
  npm run size # gzip size of what projects import, against size-budgets.json
316
- npm run test:pilots # builds the pilots + 4 brand themes, then 37 behaviour/axe checks at 1440/820/390px (Playwright for Python, axe-core)
323
+ npm run test:pilots # builds the pilots + 4 brand themes, then 44 behaviour/axe checks at 1440/820/390px (Playwright, axe-core)
317
324
  ```
318
325
 
326
+ From the repository root, `npm run typecheck` also checks the Storybook stories (strict) and every build, test and release script. The scripts are TypeScript that Node runs as is (type stripping), so working on the repo needs **Node 22.18 or later**; the published packages still run on Node 18+ (their CLI and checkers stay JavaScript, type-checked through `// @ts-check`).
327
+
319
328
  ## Source
320
329
 
321
330
  `src/` is TypeScript (strict) and JSX, formatted with Prettier (`npm run format`), **one file per component** (`Badge.tsx`, `Tooltip.tsx`, …) so a project that imports one component gets only that component's code. Public prop types live in `src/types.ts` with their docs; each component imports its props from there and `forwardRef`s with them, so the published `dist/index.d.ts` is generated from the code and can't drift from it. The build compiles JSX to `React.createElement` (classic runtime), so the `window.Aura` script needs only `window.React`.
@@ -1286,6 +1286,14 @@ window.Aura = (() => {
1286
1286
  const mounted = useMounted();
1287
1287
  const live = !native && mounted;
1288
1288
  const density = useDensity();
1289
+ React13.useEffect(function() {
1290
+ if (props.label || props["aria-label"] || props["aria-labelledby"] || props.title) return;
1291
+ if (document.querySelector('label[for="' + id.replace(/["\\]/g, "\\$&") + '"]')) return;
1292
+ devWarnOnce(
1293
+ "select-name",
1294
+ "Select needs a label, aria-label or aria-labelledby: screen readers announce it with no name."
1295
+ );
1296
+ }, []);
1289
1297
  const selRef = React13.useRef(null), trigRef = React13.useRef(null), listRef = React13.useRef(null);
1290
1298
  const shown = React13.useState(function() {
1291
1299
  return initialLabel(props);
@@ -1268,6 +1268,14 @@ var Select = React13.forwardRef(function Select2(props, ref) {
1268
1268
  const mounted = useMounted();
1269
1269
  const live = !native && mounted;
1270
1270
  const density = useDensity();
1271
+ React13.useEffect(function() {
1272
+ if (props.label || props["aria-label"] || props["aria-labelledby"] || props.title) return;
1273
+ if (document.querySelector('label[for="' + id.replace(/["\\]/g, "\\$&") + '"]')) return;
1274
+ devWarnOnce(
1275
+ "select-name",
1276
+ "Select needs a label, aria-label or aria-labelledby: screen readers announce it with no name."
1277
+ );
1278
+ }, []);
1271
1279
  const selRef = React13.useRef(null), trigRef = React13.useRef(null), listRef = React13.useRef(null);
1272
1280
  const shown = React13.useState(function() {
1273
1281
  return initialLabel(props);
@@ -4,7 +4,7 @@ import { createPortal } from "react-dom";
4
4
  import { FIELD_KEYS, Field, describedBy } from "./Field.js";
5
5
  import { Icon } from "./Icon.js";
6
6
  import { useDensity } from "./locale.js";
7
- import { cx, omit, uid, useIsoLayoutEffect, useMergedRef, useMounted } from "./internal.js";
7
+ import { cx, devWarnOnce, omit, uid, useIsoLayoutEffect, useMergedRef, useMounted } from "./internal.js";
8
8
  const h = React.createElement;
9
9
  const FORM_KEYS = ["name", "form", "value", "defaultValue", "disabled", "autoComplete", "onInput", "onInvalid"];
10
10
  const HANDLED_KEYS = FORM_KEYS.concat([
@@ -66,6 +66,14 @@ const Select = React.forwardRef(function Select2(props, ref) {
66
66
  const mounted = useMounted();
67
67
  const live = !native && mounted;
68
68
  const density = useDensity();
69
+ React.useEffect(function() {
70
+ if (props.label || props["aria-label"] || props["aria-labelledby"] || props.title) return;
71
+ if (document.querySelector('label[for="' + id.replace(/["\\]/g, "\\$&") + '"]')) return;
72
+ devWarnOnce(
73
+ "select-name",
74
+ "Select needs a label, aria-label or aria-labelledby: screen readers announce it with no name."
75
+ );
76
+ }, []);
69
77
  const selRef = React.useRef(null), trigRef = React.useRef(null), listRef = React.useRef(null);
70
78
  const shown = React.useState(function() {
71
79
  return initialLabel(props);
package/dist/index.d.cts CHANGED
@@ -46,6 +46,11 @@ export interface DataTableState {
46
46
  sort: DataTableSort | null;
47
47
  page: number;
48
48
  }
49
+ /** A callback that gets one row. Written with your own row type (`(r: Order) => …`) it still fits (5.4):
50
+ * the row is checked the way method parameters are, so no cast from `Record<string, any>` is needed. */
51
+ export type RowCallback<R> = {
52
+ bivarianceHack(row: Record<string, any>): R;
53
+ }["bivarianceHack"];
49
54
  export interface DataTableColumn {
50
55
  /** Key into each row object. */
51
56
  key: string;
@@ -62,9 +67,9 @@ export interface DataTableColumn {
62
67
  /** Header becomes a button cycling asc → desc → unsorted. */
63
68
  sortable?: boolean | undefined;
64
69
  /** Value to sort by, when not the cell value (dates, amounts). */
65
- sortValue?: ((row: Record<string, any>) => string | number | null) | undefined;
70
+ sortValue?: RowCallback<string | number | null> | undefined;
66
71
  /** Custom cell content. */
67
- render?: ((row: Record<string, any>) => React$1.ReactNode) | undefined;
72
+ render?: RowCallback<React$1.ReactNode> | undefined;
68
73
  /** Set false to keep a sized column fixed when the table is `resizable`. */
69
74
  resizable?: boolean | undefined;
70
75
  /** Resize limits in px. Defaults 64 / 480. For the flexible column, minWidth defaults to 160. */
@@ -98,7 +103,9 @@ export interface DataTableEmpty {
98
103
  export interface DataTableProps {
99
104
  /** Defaults to ID · NAME · STATUS · OWNER (96 / 160 / 112 / auto px). */
100
105
  columns?: DataTableColumn[] | undefined;
101
- rows: Array<Record<string, React$1.ReactNode>>;
106
+ /** Row objects. Any typed interface works (5.4: before, `interface Order {…}` rows needed a cast); cells show
107
+ * the value at each column's key unless the column has `render`. */
108
+ rows: ReadonlyArray<Record<string, any>>;
102
109
  /** Column key giving each row a unique React key. Default: the first column's key. */
103
110
  rowKey?: string | undefined;
104
111
  /** Accessible name for the table. */
@@ -145,7 +152,7 @@ export interface DataTableProps {
145
152
  totalRows?: number | undefined;
146
153
  /** Makes each row a link: the first column's content renders as the provider's linkComponent (or `<a>`), and a click
147
154
  * or Enter anywhere on the row follows it. Ctrl/⌘-click opens a new tab as usual. */
148
- getRowHref?: ((row: Record<string, any>) => string) | undefined;
155
+ getRowHref?: RowCallback<string> | undefined;
149
156
  /** Pager arrows become links to these URLs (search-param paging). Without onPageChange the link navigates. */
150
157
  getPageHref?: ((page: number) => string) | undefined;
151
158
  /** Router link for getRowHref / getPageHref. Default: AuraProvider's linkComponent, else `<a>`. */
@@ -153,7 +160,7 @@ export interface DataTableProps {
153
160
  /** Skeleton row count when there is no pageSize. Default 5. */
154
161
  skeletonRows?: number | undefined;
155
162
  /** Called on row click or Enter. */
156
- onRowActivate?: ((row: Record<string, any>) => void) | undefined;
163
+ onRowActivate?: RowCallback<void> | undefined;
157
164
  /** Fixed height in px: sticky header, and only the rows in view are rendered. */
158
165
  height?: number | undefined;
159
166
  /** Column menu on every header, drag-to-reorder, and the show/hide columns button. */
@@ -321,11 +328,14 @@ export interface FormErrorItem {
321
328
  message: React$1.ReactNode;
322
329
  }
323
330
  /** The list of problems at the top of a form after a failed submit; each links to its field (GOV.UK pattern). */
331
+ /** react-hook-form's `formState.errors` (FieldErrors) or any object shaped like it: a `message` at any depth. */
332
+ export type FormErrorTree = {
333
+ readonly [field: string]: object | undefined;
334
+ };
324
335
  export interface FormErrorSummaryProps {
325
- /** A list, or react-hook-form's `formState.errors` as is (`{ name: { message } }`). Empty → renders nothing. */
326
- errors: FormErrorItem[] | Record<string, {
327
- message?: React$1.ReactNode | undefined;
328
- } | undefined>;
336
+ /** A list, or react-hook-form's `formState.errors` as is: `{ name: { message } }`, nested for `address.street`
337
+ * and field arrays (`items.0.name`). Empty → renders nothing. */
338
+ errors: FormErrorItem[] | FormErrorTree;
329
339
  /** Default "Fix N fields to continue" in the provider's language. */
330
340
  title?: React$1.ReactNode | undefined;
331
341
  /** Called with the field when a link is followed, e.g. react-hook-form's `setFocus`. Default: focus the element whose id (or name) is the field. */
@@ -419,7 +429,10 @@ export type SelectOption = string | {
419
429
  };
420
430
  /** 5.3: opens AURA's own list (light and dark alike) over a real `<select>`, which keeps `name`, `required`, the ref,
421
431
  * `onChange`, react-hook-form `register` and form posts. `multiple` or `size > 1` keeps the native list box. */
422
- export interface SelectProps extends FieldProps, Omit<React$1.SelectHTMLAttributes<HTMLSelectElement>, "required"> {
432
+ export interface SelectProps extends Omit<FieldProps, "label">, Omit<React$1.SelectHTMLAttributes<HTMLSelectElement>, "required"> {
433
+ /** Visible label; also the accessible name. Omit only with `aria-label` or `aria-labelledby` (a toolbar, a table
434
+ * cell); development builds warn when the field has no name at all. */
435
+ label?: string | undefined;
423
436
  /** The choices; or pass `<option>` / `<optgroup>` children instead (5.3: optional). */
424
437
  options?: SelectOption[] | undefined;
425
438
  /** Shown in fg-tertiary on the closed field until something is chosen; not listed as a choice. */
package/dist/index.d.ts CHANGED
@@ -46,6 +46,11 @@ export interface DataTableState {
46
46
  sort: DataTableSort | null;
47
47
  page: number;
48
48
  }
49
+ /** A callback that gets one row. Written with your own row type (`(r: Order) => …`) it still fits (5.4):
50
+ * the row is checked the way method parameters are, so no cast from `Record<string, any>` is needed. */
51
+ export type RowCallback<R> = {
52
+ bivarianceHack(row: Record<string, any>): R;
53
+ }["bivarianceHack"];
49
54
  export interface DataTableColumn {
50
55
  /** Key into each row object. */
51
56
  key: string;
@@ -62,9 +67,9 @@ export interface DataTableColumn {
62
67
  /** Header becomes a button cycling asc → desc → unsorted. */
63
68
  sortable?: boolean | undefined;
64
69
  /** Value to sort by, when not the cell value (dates, amounts). */
65
- sortValue?: ((row: Record<string, any>) => string | number | null) | undefined;
70
+ sortValue?: RowCallback<string | number | null> | undefined;
66
71
  /** Custom cell content. */
67
- render?: ((row: Record<string, any>) => React$1.ReactNode) | undefined;
72
+ render?: RowCallback<React$1.ReactNode> | undefined;
68
73
  /** Set false to keep a sized column fixed when the table is `resizable`. */
69
74
  resizable?: boolean | undefined;
70
75
  /** Resize limits in px. Defaults 64 / 480. For the flexible column, minWidth defaults to 160. */
@@ -98,7 +103,9 @@ export interface DataTableEmpty {
98
103
  export interface DataTableProps {
99
104
  /** Defaults to ID · NAME · STATUS · OWNER (96 / 160 / 112 / auto px). */
100
105
  columns?: DataTableColumn[] | undefined;
101
- rows: Array<Record<string, React$1.ReactNode>>;
106
+ /** Row objects. Any typed interface works (5.4: before, `interface Order {…}` rows needed a cast); cells show
107
+ * the value at each column's key unless the column has `render`. */
108
+ rows: ReadonlyArray<Record<string, any>>;
102
109
  /** Column key giving each row a unique React key. Default: the first column's key. */
103
110
  rowKey?: string | undefined;
104
111
  /** Accessible name for the table. */
@@ -145,7 +152,7 @@ export interface DataTableProps {
145
152
  totalRows?: number | undefined;
146
153
  /** Makes each row a link: the first column's content renders as the provider's linkComponent (or `<a>`), and a click
147
154
  * or Enter anywhere on the row follows it. Ctrl/⌘-click opens a new tab as usual. */
148
- getRowHref?: ((row: Record<string, any>) => string) | undefined;
155
+ getRowHref?: RowCallback<string> | undefined;
149
156
  /** Pager arrows become links to these URLs (search-param paging). Without onPageChange the link navigates. */
150
157
  getPageHref?: ((page: number) => string) | undefined;
151
158
  /** Router link for getRowHref / getPageHref. Default: AuraProvider's linkComponent, else `<a>`. */
@@ -153,7 +160,7 @@ export interface DataTableProps {
153
160
  /** Skeleton row count when there is no pageSize. Default 5. */
154
161
  skeletonRows?: number | undefined;
155
162
  /** Called on row click or Enter. */
156
- onRowActivate?: ((row: Record<string, any>) => void) | undefined;
163
+ onRowActivate?: RowCallback<void> | undefined;
157
164
  /** Fixed height in px: sticky header, and only the rows in view are rendered. */
158
165
  height?: number | undefined;
159
166
  /** Column menu on every header, drag-to-reorder, and the show/hide columns button. */
@@ -321,11 +328,14 @@ export interface FormErrorItem {
321
328
  message: React$1.ReactNode;
322
329
  }
323
330
  /** The list of problems at the top of a form after a failed submit; each links to its field (GOV.UK pattern). */
331
+ /** react-hook-form's `formState.errors` (FieldErrors) or any object shaped like it: a `message` at any depth. */
332
+ export type FormErrorTree = {
333
+ readonly [field: string]: object | undefined;
334
+ };
324
335
  export interface FormErrorSummaryProps {
325
- /** A list, or react-hook-form's `formState.errors` as is (`{ name: { message } }`). Empty → renders nothing. */
326
- errors: FormErrorItem[] | Record<string, {
327
- message?: React$1.ReactNode | undefined;
328
- } | undefined>;
336
+ /** A list, or react-hook-form's `formState.errors` as is: `{ name: { message } }`, nested for `address.street`
337
+ * and field arrays (`items.0.name`). Empty → renders nothing. */
338
+ errors: FormErrorItem[] | FormErrorTree;
329
339
  /** Default "Fix N fields to continue" in the provider's language. */
330
340
  title?: React$1.ReactNode | undefined;
331
341
  /** Called with the field when a link is followed, e.g. react-hook-form's `setFocus`. Default: focus the element whose id (or name) is the field. */
@@ -419,7 +429,10 @@ export type SelectOption = string | {
419
429
  };
420
430
  /** 5.3: opens AURA's own list (light and dark alike) over a real `<select>`, which keeps `name`, `required`, the ref,
421
431
  * `onChange`, react-hook-form `register` and form posts. `multiple` or `size > 1` keeps the native list box. */
422
- export interface SelectProps extends FieldProps, Omit<React$1.SelectHTMLAttributes<HTMLSelectElement>, "required"> {
432
+ export interface SelectProps extends Omit<FieldProps, "label">, Omit<React$1.SelectHTMLAttributes<HTMLSelectElement>, "required"> {
433
+ /** Visible label; also the accessible name. Omit only with `aria-label` or `aria-labelledby` (a toolbar, a table
434
+ * cell); development builds warn when the field has no name at all. */
435
+ label?: string | undefined;
423
436
  /** The choices; or pass `<option>` / `<optgroup>` children instead (5.3: optional). */
424
437
  options?: SelectOption[] | undefined;
425
438
  /** Shown in fg-tertiary on the closed field until something is chosen; not listed as a choice. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jirawatpyk/aura-react",
3
- "version": "5.3.0",
3
+ "version": "5.4.0",
4
4
  "description": "AURA Design System — React components (Enterprise Standard, Human Creative). Thai-first: Buddhist-era dates, Thai collation.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -44,20 +44,20 @@
44
44
  "./dist/aura.bundle.js"
45
45
  ],
46
46
  "scripts": {
47
- "build": "node scripts/build.mjs && node scripts/sync-bundle.mjs",
48
- "test:ssr": "node scripts/ssr-test.mjs && node --conditions=react-server scripts/server-entry-test.mjs",
49
- "typecheck": "tsc -p tsconfig.src.json && tsc -p tsconfig.json && tsc -p tsconfig.exact.json",
50
- "examples": "node examples/build.mjs",
51
- "themes": "node scripts/test-themes.mjs",
52
- "test:pilots": "npm run examples && npm run themes && python3 tests/pilot_test.py && python3 tests/pilots_test.py",
47
+ "build": "node scripts/build.ts && node scripts/sync-bundle.ts",
48
+ "test:ssr": "node scripts/ssr-test.ts && node --conditions=react-server scripts/server-entry-test.ts",
49
+ "typecheck": "tsc -p tsconfig.src.json && tsc -p tsconfig.json && tsc -p tsconfig.exact.json && tsc -p examples/tsconfig.json",
50
+ "examples": "node examples/build.ts",
51
+ "themes": "node scripts/test-themes.ts",
52
+ "test:pilots": "npm run examples && npm run themes && playwright test -c tests/playwright.config.ts",
53
53
  "theme": "node scripts/aura-theme.mjs",
54
54
  "prepack": "npm run build",
55
- "pretest:ssr": "node scripts/build.mjs",
56
- "prethemes": "node scripts/build.mjs",
57
- "test:hydrate": "node scripts/hydrate-test.mjs",
58
- "size": "node scripts/size-check.mjs",
59
- "preexamples": "node scripts/build.mjs",
60
- "test:layout": "node scripts/layout-test.mjs"
55
+ "pretest:ssr": "node scripts/build.ts",
56
+ "prethemes": "node scripts/build.ts",
57
+ "test:hydrate": "node scripts/hydrate-test.ts",
58
+ "size": "node scripts/size-check.ts",
59
+ "preexamples": "node scripts/build.ts",
60
+ "test:layout": "node scripts/layout-test.ts"
61
61
  },
62
62
  "peerDependencies": {
63
63
  "react": ">=18",
@@ -72,7 +72,7 @@
72
72
  "react": "^18.3.1",
73
73
  "react-dom": "^18.3.1",
74
74
  "react-hook-form": "^7.53.0",
75
- "typescript": "^5.6.0",
75
+ "typescript": "^5.8.0",
76
76
  "@playwright/test": "^1.45.0"
77
77
  },
78
78
  "bin": {
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ // @ts-check
2
3
  /* aura-theme — write a project theme CSS file from a brand colour.
3
4
  * npx aura-theme --brand "#0ea5e9" [--signal "#facc15"] [--primary brand] [--name acme] [--selector .tenant-acme] [--out src/aura-theme.css]
4
5
  * Prints every contrast check; exits 1 if any fails. */
@@ -7,39 +8,50 @@ import { createTheme } from '../dist/esm/theme.js';
7
8
 
8
9
  const USAGE = 'Usage: aura-theme --brand "#0ea5e9" [--signal "#facc15"] [--primary ink|brand] [--name acme] [--selector .tenant] [--out theme.css]';
9
10
  const KNOWN = ['brand', 'signal', 'primary', 'name', 'selector', 'out', 'help'];
11
+ /**
12
+ * @param {string} msg
13
+ * @returns {never}
14
+ */
10
15
  function fail(msg) {
11
16
  console.error('aura-theme: ' + msg + '\n' + USAGE);
12
17
  process.exit(2);
13
18
  }
14
19
  /* --key value and --key=value (5.2); a value-taking option with no value is an error, not `true`. */
20
+ /** @type {Record<string, string>} */
15
21
  const args = {};
22
+ let help = false;
16
23
  for (let i = 2; i < process.argv.length; i++) {
17
- const a = process.argv[i];
24
+ const a = process.argv[i] || '';
18
25
  if (!a.startsWith('--')) fail('unexpected "' + a + '"');
19
26
  const eq = a.indexOf('=');
20
27
  const k = eq > 0 ? a.slice(2, eq) : a.slice(2);
21
28
  if (KNOWN.indexOf(k) < 0) fail('unknown option --' + k);
22
- if (k === 'help') { args.help = true; continue; }
23
- const v = eq > 0 ? a.slice(eq + 1) : process.argv[i + 1] !== undefined && !process.argv[i + 1].startsWith('--') ? process.argv[++i] : undefined;
29
+ if (k === 'help') { help = true; continue; }
30
+ const next = process.argv[i + 1];
31
+ const v = eq > 0 ? a.slice(eq + 1) : next !== undefined && !next.startsWith('--') ? process.argv[++i] : undefined;
24
32
  if (v === undefined || v === '') fail('--' + k + ' needs a value');
25
33
  args[k] = v;
26
34
  }
27
- if (args.help) {
35
+ if (help) {
28
36
  console.log(USAGE);
29
37
  process.exit(0);
30
38
  }
31
39
  if (!args.brand) fail('--brand is required');
32
40
  for (const k of ['brand', 'signal']) {
33
- if (args[k] !== undefined && /^(?:[0-9a-f]{3}|[0-9a-f]{6})$/i.test(args[k])) args[k] = '#' + args[k]; /* a shell ate the # */
34
- if (args[k] !== undefined && !/^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i.test(args[k])) fail('--' + k + ' must be a #rgb or #rrggbb colour (got "' + args[k] + '"; quote it in the shell: --' + k + ' "#0ea5e9")');
41
+ const c = args[k];
42
+ if (c !== undefined && /^(?:[0-9a-f]{3}|[0-9a-f]{6})$/i.test(c)) args[k] = '#' + c; /* a shell ate the # */
43
+ const hex = args[k];
44
+ if (hex !== undefined && !/^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i.test(hex)) fail('--' + k + ' must be a #rgb or #rrggbb colour (got "' + args[k] + '"; quote it in the shell: --' + k + ' "#0ea5e9")');
35
45
  }
36
46
  if (args.primary !== undefined && args.primary !== 'ink' && args.primary !== 'brand') fail('--primary is ink or brand');
37
- let t, css;
47
+ /** @type {ReturnType<typeof createTheme>} */
48
+ let t;
49
+ let css = '';
38
50
  try {
39
51
  t = createTheme({ brand: args.brand, signal: args.signal, primary: args.primary, name: args.name });
40
52
  css = t.css(args.selector);
41
53
  } catch (e) {
42
- fail(e.message);
54
+ fail(e instanceof Error ? e.message : String(e));
43
55
  }
44
56
  if (args.out) fs.writeFileSync(args.out, css); else process.stdout.write(css);
45
57
  const log = args.out ? console.log : console.error;