@jirawatpyk/aura-react 5.2.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/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. */
@@ -417,9 +427,15 @@ export type SelectOption = string | {
417
427
  label: string;
418
428
  disabled?: boolean | undefined;
419
429
  };
420
- export interface SelectProps extends FieldProps, Omit<React$1.SelectHTMLAttributes<HTMLSelectElement>, "required"> {
421
- options: SelectOption[];
422
- /** A disabled first option shown in fg-tertiary until something is chosen. */
430
+ /** 5.3: opens AURA's own list (light and dark alike) over a real `<select>`, which keeps `name`, `required`, the ref,
431
+ * `onChange`, react-hook-form `register` and form posts. `multiple` or `size > 1` keeps the native list box. */
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;
436
+ /** The choices; or pass `<option>` / `<optgroup>` children instead (5.3: optional). */
437
+ options?: SelectOption[] | undefined;
438
+ /** Shown in fg-tertiary on the closed field until something is chosen; not listed as a choice. */
423
439
  placeholder?: string | undefined;
424
440
  icon?: IconInput | undefined;
425
441
  }
@@ -1217,6 +1233,7 @@ export interface FieldComponentProps extends Omit<FieldPropsPublic, "label"> {
1217
1233
  export declare const Field: React$1.ForwardRefExoticComponent<FieldComponentProps & React$1.RefAttributes<HTMLDivElement>>;
1218
1234
  export declare const TextField: React$1.ForwardRefExoticComponent<TextFieldProps & React$1.RefAttributes<HTMLInputElement>>;
1219
1235
  export declare const Textarea: React$1.ForwardRefExoticComponent<TextareaProps & React$1.RefAttributes<HTMLTextAreaElement>>;
1236
+ /** A select field: a button that opens an AURA list (5.3), over a real <select> that keeps forms, refs and events. */
1220
1237
  export declare const Select: React$1.ForwardRefExoticComponent<SelectProps & React$1.RefAttributes<HTMLSelectElement>>;
1221
1238
  export declare const RadioGroup: React$1.ForwardRefExoticComponent<RadioGroupProps & React$1.RefAttributes<HTMLFieldSetElement>>;
1222
1239
  export declare const Switch: React$1.ForwardRefExoticComponent<SwitchProps & React$1.RefAttributes<HTMLButtonElement>>;
package/dist/styles.css CHANGED
@@ -424,7 +424,16 @@ a.aura-icon-btn { text-decoration: none; }
424
424
  .aura-select .aura-input__control { padding-right: calc(var(--aura-space-3) + var(--aura-icon-sm) + var(--aura-space-2)); cursor: pointer; }
425
425
  .aura-select__chevron { position: absolute; right: var(--aura-space-3); color: var(--aura-fg-secondary); pointer-events: none; }
426
426
  .aura-select select:invalid, .aura-select select:has(option[value=""]:checked) { color: var(--aura-fg-tertiary); }
427
+ /* 5.3: the field is a button over an invisible <select> (forms, refs, events); the list is AURA's, like Combobox. */
427
428
  .aura-select option { color: var(--aura-fg-primary); background: var(--aura-bg-input); }
429
+ .aura-select--custom { position: relative; }
430
+ .aura-select__native { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; padding: 0; border: 0; opacity: 0; pointer-events: none; appearance: none; }
431
+ .aura-select__trigger { display: flex; align-items: center; text-align: start; background: none; border: 0; color: inherit; font: inherit; width: 100%; min-width: 0; }
432
+ .aura-select__trigger:disabled { cursor: not-allowed; }
433
+ .aura-select__value { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
434
+ .aura-select__value.is-placeholder { color: var(--aura-fg-tertiary); }
435
+ .aura-select__group { padding: var(--aura-space-2) var(--aura-space-2) var(--aura-space-1); font-family: var(--font-mono); font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--aura-fg-tertiary); }
436
+ .aura-select.is-open .aura-select__chevron { transform: rotate(180deg); }
428
437
 
429
438
  .aura-radio-group > legend { margin-bottom: 6px; }
430
439
  .aura-radio-group__list { display: flex; flex-direction: column; gap: var(--aura-space-3); }
@@ -426,7 +426,16 @@ a.aura-icon-btn { text-decoration: none; }
426
426
  .aura-select .aura-input__control { padding-right: calc(var(--aura-space-3) + var(--aura-icon-sm) + var(--aura-space-2)); cursor: pointer; }
427
427
  .aura-select__chevron { position: absolute; right: var(--aura-space-3); color: var(--aura-fg-secondary); pointer-events: none; }
428
428
  .aura-select select:invalid, .aura-select select:has(option[value=""]:checked) { color: var(--aura-fg-tertiary); }
429
+ /* 5.3: the field is a button over an invisible <select> (forms, refs, events); the list is AURA's, like Combobox. */
429
430
  .aura-select option { color: var(--aura-fg-primary); background: var(--aura-bg-input); }
431
+ .aura-select--custom { position: relative; }
432
+ .aura-select__native { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; padding: 0; border: 0; opacity: 0; pointer-events: none; appearance: none; }
433
+ .aura-select__trigger { display: flex; align-items: center; text-align: start; background: none; border: 0; color: inherit; font: inherit; width: 100%; min-width: 0; }
434
+ .aura-select__trigger:disabled { cursor: not-allowed; }
435
+ .aura-select__value { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
436
+ .aura-select__value.is-placeholder { color: var(--aura-fg-tertiary); }
437
+ .aura-select__group { padding: var(--aura-space-2) var(--aura-space-2) var(--aura-space-1); font-family: var(--font-mono); font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--aura-fg-tertiary); }
438
+ .aura-select.is-open .aura-select__chevron { transform: rotate(180deg); }
430
439
 
431
440
  .aura-radio-group > legend { margin-bottom: 6px; }
432
441
  .aura-radio-group__list { display: flex; flex-direction: column; gap: var(--aura-space-3); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jirawatpyk/aura-react",
3
- "version": "5.2.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;