@jirawatpyk/aura-tokens 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/README.md CHANGED
@@ -15,7 +15,7 @@ Enterprise Standard, Human Creative. Tokens, Tailwind preset, Figma variables a
15
15
  | `figma-variables.csv` | 204 variables: all colours in both modes (aliases kept), spacing, radius and sizes. **Generated.** |
16
16
  | `components/` | `aura.bundle.js` (sets `window.Aura`), `aura.components.css`, `index.d.ts`. |
17
17
  | `eslint-plugin-aura.js` | `aura/no-hardcoded-color`: hex, colour functions (rgb, hsl, oklch, color-mix…), named colours in style values and Tailwind palette / `bg-[#…]` classes. ESLint 9: `import aura from '@jirawatpyk/aura-tokens/eslint-plugin'; export default [aura.configs.recommended];` (5.2). |
18
- | `scripts/build-tokens.js` | Rebuilds the generated files from `tokens.json`. |
18
+ | `scripts/build-tokens.mts` | Rebuilds the generated files from `tokens.json`. |
19
19
  | `scripts/a11y-check.js` | Checks 210 pairs in both themes: text on its grounds (WCAG AA), control edges and state marks at 3:1, neighbouring chart and status fills a lightness step apart. |
20
20
  | `scripts/lint-tokens.js` | Fails on hard-coded colours (a file that must hold them says `aura-lint: allow-colours` with a reason). |
21
21
 
package/aura-tailwind.css CHANGED
@@ -1,4 +1,4 @@
1
- /* AURA v5.2.0 — Tailwind v4 theme. GENERATED by scripts/build-tokens.js from tokens.json. Do not edit by hand.
1
+ /* AURA v5.4.0 — Tailwind v4 theme. GENERATED by scripts/build-tokens.mts from tokens.json. Do not edit by hand.
2
2
  * @import "tailwindcss";
3
3
  * @import "@jirawatpyk/aura-tokens/tailwind.css";
4
4
  * Gives bg-bg-surface, text-fg-primary, border-border-strong, p-aura-6, rounded-aura-xl, shadow-aura-overlay,
@@ -1,4 +1,4 @@
1
- /* AURA v5.2.0 — Tailwind v4 theme with aura- prefixed names. GENERATED by scripts/build-tokens.js. Do not edit by hand.
1
+ /* AURA v5.4.0 — Tailwind v4 theme with aura- prefixed names. GENERATED by scripts/build-tokens.mts. Do not edit by hand.
2
2
  * For apps that already have a Tailwind theme (shadcn): nothing here redefines an existing name.
3
3
  * @layer aura-tokens, theme, base, aura, components, utilities;
4
4
  * @import "tailwindcss";
package/aura.css CHANGED
@@ -1,4 +1,4 @@
1
- /* AURA v5.2.0 — GENERATED by scripts/build-tokens.js from tokens.json. Do not edit by hand. */
1
+ /* AURA v5.4.0 — GENERATED by scripts/build-tokens.mts from tokens.json. Do not edit by hand. */
2
2
  /* Fonts: load aura-fonts.css (or the <link> tags in README) separately, so a blocked Google Fonts never breaks this file. */
3
3
 
4
4
  :root, [data-theme="light"] {
@@ -1224,7 +1224,52 @@ window.Aura = (() => {
1224
1224
 
1225
1225
  // src/Select.tsx
1226
1226
  var React13 = __toESM(require_react(), 1);
1227
+ var import_react_dom2 = __toESM(require_react_dom(), 1);
1227
1228
  var h3 = React13.createElement;
1229
+ var FORM_KEYS = ["name", "form", "value", "defaultValue", "disabled", "autoComplete", "onInput", "onInvalid"];
1230
+ var HANDLED_KEYS = FORM_KEYS.concat([
1231
+ "onChange",
1232
+ "onBlur",
1233
+ "onFocus",
1234
+ "onKeyDown",
1235
+ "onClick",
1236
+ "autoFocus",
1237
+ "tabIndex",
1238
+ "aria-label",
1239
+ "aria-labelledby",
1240
+ "aria-describedby",
1241
+ "multiple",
1242
+ "size"
1243
+ ]);
1244
+ function pick(o, keys) {
1245
+ const r = {};
1246
+ for (const k of keys) if (k in o) r[k] = o[k];
1247
+ return r;
1248
+ }
1249
+ function readItems(el) {
1250
+ if (!el) return [];
1251
+ return Array.prototype.map.call(el.options, function(o, i) {
1252
+ const g = o.parentElement && o.parentElement.tagName === "OPTGROUP" ? o.parentElement : null;
1253
+ return {
1254
+ value: o.value,
1255
+ label: o.textContent || "",
1256
+ disabled: o.disabled || !!(g && g.disabled),
1257
+ group: g ? g.label : null,
1258
+ index: i
1259
+ };
1260
+ });
1261
+ }
1262
+ function initialLabel(props) {
1263
+ const opts = (props.options || []).map(function(o) {
1264
+ return typeof o === "object" ? o : { value: o, label: o };
1265
+ });
1266
+ const v = props.value !== void 0 && props.value !== null ? String(props.value) : props.defaultValue !== void 0 && props.defaultValue !== null ? String(props.defaultValue) : props.placeholder ? "" : opts[0] ? opts[0].value : "";
1267
+ const hit = opts.filter(function(o) {
1268
+ return o.value === v;
1269
+ })[0];
1270
+ if (hit) return { label: hit.label, empty: false };
1271
+ return { label: props.placeholder || "", empty: true };
1272
+ }
1228
1273
  var Select = React13.forwardRef(function Select2(props, ref) {
1229
1274
  const auto = uid(), id = props.id || auto;
1230
1275
  const rest = omit(props, FIELD_KEYS.concat(["children"]));
@@ -1237,10 +1282,267 @@ window.Aura = (() => {
1237
1282
  /* @__PURE__ */ React13.createElement("option", { key: "__ph", value: "", disabled: true }, props.placeholder)
1238
1283
  );
1239
1284
  const extra = props.value === void 0 && props.defaultValue === void 0 && props.placeholder ? { defaultValue: "" } : {};
1240
- return /* @__PURE__ */ React13.createElement(
1285
+ const native = !!props.multiple || props.size != null && props.size > 1;
1286
+ const mounted = useMounted();
1287
+ const live = !native && mounted;
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
+ }, []);
1297
+ const selRef = React13.useRef(null), trigRef = React13.useRef(null), listRef = React13.useRef(null);
1298
+ const shown = React13.useState(function() {
1299
+ return initialLabel(props);
1300
+ });
1301
+ const openState = React13.useState(false), open = openState[0];
1302
+ const activeState = React13.useState(-1), active = activeState[0];
1303
+ const items2 = React13.useState([]);
1304
+ const pos = React13.useState(null);
1305
+ const typed = React13.useRef({ text: "", at: 0 });
1306
+ const sync = React13.useCallback(function() {
1307
+ const el = selRef.current;
1308
+ if (!el) return;
1309
+ const o = el.selectedIndex >= 0 ? el.options[el.selectedIndex] : null;
1310
+ const next = o ? { label: o.textContent || "", empty: o.value === "" } : { label: "", empty: true };
1311
+ shown[1](function(cur) {
1312
+ return cur.label === next.label && cur.empty === next.empty ? cur : next;
1313
+ });
1314
+ }, []);
1315
+ const hook = React13.useCallback(
1316
+ function(el) {
1317
+ selRef.current = el;
1318
+ if (!el || native || el.__auraHooked) return;
1319
+ el.__auraHooked = true;
1320
+ const proto = HTMLSelectElement.prototype;
1321
+ const pv = Object.getOwnPropertyDescriptor(proto, "value"), pi = Object.getOwnPropertyDescriptor(proto, "selectedIndex");
1322
+ if (pv && pv.set && pv.get)
1323
+ Object.defineProperty(el, "value", {
1324
+ configurable: true,
1325
+ get: function() {
1326
+ return pv.get.call(el);
1327
+ },
1328
+ set: function(v) {
1329
+ pv.set.call(el, v);
1330
+ sync();
1331
+ }
1332
+ });
1333
+ if (pi && pi.set && pi.get)
1334
+ Object.defineProperty(el, "selectedIndex", {
1335
+ configurable: true,
1336
+ get: function() {
1337
+ return pi.get.call(el);
1338
+ },
1339
+ set: function(v) {
1340
+ pi.set.call(el, v);
1341
+ sync();
1342
+ }
1343
+ });
1344
+ el.focus = function(o) {
1345
+ if (trigRef.current) trigRef.current.focus(o);
1346
+ else proto.focus.call(el, o);
1347
+ };
1348
+ },
1349
+ [native, sync]
1350
+ );
1351
+ const selMerged = useMergedRef(ref, hook);
1352
+ React13.useEffect(
1353
+ function() {
1354
+ const f = selRef.current && selRef.current.form;
1355
+ if (!f || native) return;
1356
+ function onReset() {
1357
+ setTimeout(sync, 0);
1358
+ }
1359
+ f.addEventListener("reset", onReset);
1360
+ return function() {
1361
+ f.removeEventListener("reset", onReset);
1362
+ };
1363
+ },
1364
+ [native, sync]
1365
+ );
1366
+ const focusedOnce = React13.useRef(false);
1367
+ React13.useEffect(
1368
+ function() {
1369
+ if (!live || focusedOnce.current || !trigRef.current) return;
1370
+ focusedOnce.current = true;
1371
+ if (props.autoFocus || selRef.current && document.activeElement === selRef.current) trigRef.current.focus();
1372
+ },
1373
+ [live]
1374
+ );
1375
+ useIsoLayoutEffect(function() {
1376
+ if (!live) return;
1377
+ sync();
1378
+ if (!open) return;
1379
+ const now = readItems(selRef.current), was = items2[0];
1380
+ const same2 = now.length === was.length && now.every(function(it, i) {
1381
+ const w = was[i];
1382
+ return w.value === it.value && w.label === it.label && w.disabled === it.disabled && w.group === it.group;
1383
+ });
1384
+ if (same2) return;
1385
+ items2[1](now);
1386
+ if (!choices(now).length) {
1387
+ close(false);
1388
+ return;
1389
+ }
1390
+ const hv = was[active] ? was[active].value : null;
1391
+ const again = now.filter(function(it) {
1392
+ return !it.disabled && it.value === hv;
1393
+ })[0];
1394
+ const cur = selRef.current ? selRef.current.selectedIndex : -1;
1395
+ activeState[1](again ? again.index : now[cur] && !now[cur].disabled ? cur : choices(now)[0].index);
1396
+ });
1397
+ function choices(list) {
1398
+ return list.filter(function(i) {
1399
+ return !i.disabled;
1400
+ });
1401
+ }
1402
+ function choose(it) {
1403
+ const el = selRef.current;
1404
+ if (!el || it.disabled) return;
1405
+ if (el.value !== it.value) {
1406
+ el.value = it.value;
1407
+ el.dispatchEvent(new Event("input", { bubbles: true }));
1408
+ el.dispatchEvent(new Event("change", { bubbles: true }));
1409
+ }
1410
+ sync();
1411
+ close(true);
1412
+ }
1413
+ function openList(at) {
1414
+ if (props.disabled || trigRef.current && trigRef.current.disabled) return;
1415
+ const list = readItems(selRef.current);
1416
+ const ok = choices(list);
1417
+ if (!ok.length) return;
1418
+ items2[1](list);
1419
+ const cur = selRef.current ? selRef.current.selectedIndex : -1;
1420
+ let a = at === "first" ? ok[0].index : at === "last" ? ok[ok.length - 1].index : cur;
1421
+ if (typeof at === "number") a = at;
1422
+ if (a < 0 || !list[a] || list[a].disabled) a = ok[0].index;
1423
+ activeState[1](a);
1424
+ openState[1](true);
1425
+ }
1426
+ function close(focus) {
1427
+ openState[1](false);
1428
+ pos[1](null);
1429
+ if (focus && trigRef.current) trigRef.current.focus();
1430
+ }
1431
+ function step(from, dir, list) {
1432
+ for (let i = from + dir; i >= 0 && i < list.length; i += dir) if (!list[i].disabled) return i;
1433
+ return from;
1434
+ }
1435
+ function typing() {
1436
+ return !!typed.current.text && Date.now() - typed.current.at < 500;
1437
+ }
1438
+ function typeahead(ch, list, from) {
1439
+ const now = Date.now(), t = typed.current;
1440
+ t.text = now - t.at < 500 ? t.text + ch : ch;
1441
+ t.at = now;
1442
+ const q = t.text.toLocaleLowerCase();
1443
+ for (let k = 1; k <= list.length; k++) {
1444
+ const i = (from + (t.text.length > 1 ? 0 : 1) + k - 1 + list.length) % list.length;
1445
+ if (!list[i].disabled && list[i].label.toLocaleLowerCase().indexOf(q) === 0) return i;
1446
+ }
1447
+ t.text = "";
1448
+ return -1;
1449
+ }
1450
+ function onKeyDown(e) {
1451
+ const k = e.key;
1452
+ const printable = k.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey;
1453
+ const search = printable && (k !== " " || typing());
1454
+ if (!open) {
1455
+ if (search) {
1456
+ const list2 = readItems(selRef.current);
1457
+ const i = typeahead(k, list2, selRef.current ? selRef.current.selectedIndex : -1);
1458
+ e.preventDefault();
1459
+ if (i >= 0) openList(i);
1460
+ } else if (k === "ArrowDown" || k === "ArrowUp" || k === "Enter" || k === " ") {
1461
+ e.preventDefault();
1462
+ openList(k === "ArrowUp" && !(selRef.current && selRef.current.value) ? "last" : void 0);
1463
+ }
1464
+ return;
1465
+ }
1466
+ const list = items2[0];
1467
+ if (search) {
1468
+ e.preventDefault();
1469
+ const i = typeahead(k, list, active);
1470
+ if (i >= 0) activeState[1](i);
1471
+ } else if (k === "ArrowDown") {
1472
+ e.preventDefault();
1473
+ activeState[1](step(active, 1, list));
1474
+ } else if (k === "ArrowUp") {
1475
+ e.preventDefault();
1476
+ activeState[1](step(active, -1, list));
1477
+ } else if (k === "Home" || k === "PageUp") {
1478
+ e.preventDefault();
1479
+ activeState[1](k === "Home" ? step(-1, 1, list) : Math.max(step(-1, 1, list), step(active - 9, 1, list)));
1480
+ } else if (k === "End" || k === "PageDown") {
1481
+ e.preventDefault();
1482
+ activeState[1](
1483
+ k === "End" ? step(list.length, -1, list) : Math.min(step(list.length, -1, list), step(active + 9, -1, list))
1484
+ );
1485
+ } else if (k === "Enter" || k === " ") {
1486
+ e.preventDefault();
1487
+ if (list[active]) choose(list[active]);
1488
+ } else if (k === "Escape") {
1489
+ e.preventDefault();
1490
+ e.stopPropagation();
1491
+ close(true);
1492
+ } else if (k === "Tab") {
1493
+ close(false);
1494
+ }
1495
+ }
1496
+ useIsoLayoutEffect(
1497
+ function() {
1498
+ if (!open) return;
1499
+ function place() {
1500
+ const t = trigRef.current && trigRef.current.parentElement;
1501
+ if (!t) return;
1502
+ const r = t.getBoundingClientRect(), below2 = window.innerHeight - r.bottom - 8, above = r.top - 8, want = listRef.current ? listRef.current.scrollHeight : 320, up = below2 < Math.min(want, 200) && above > below2;
1503
+ pos[1]({
1504
+ left: r.left,
1505
+ width: r.width,
1506
+ top: up ? void 0 : r.bottom + 4,
1507
+ bottom: up ? window.innerHeight - r.top + 4 : void 0,
1508
+ maxHeight: Math.min(320, (up ? above : below2) - 4)
1509
+ });
1510
+ }
1511
+ place();
1512
+ function outside(e) {
1513
+ const n2 = e.target;
1514
+ if (listRef.current && listRef.current.contains(n2)) return;
1515
+ if (trigRef.current && trigRef.current.contains(n2)) return;
1516
+ close(false);
1517
+ }
1518
+ function onScroll(e) {
1519
+ if (!listRef.current || !listRef.current.contains(e.target)) place();
1520
+ }
1521
+ document.addEventListener("pointerdown", outside, true);
1522
+ window.addEventListener("scroll", onScroll, true);
1523
+ window.addEventListener("resize", place);
1524
+ return function() {
1525
+ document.removeEventListener("pointerdown", outside, true);
1526
+ window.removeEventListener("scroll", onScroll, true);
1527
+ window.removeEventListener("resize", place);
1528
+ };
1529
+ },
1530
+ [open]
1531
+ );
1532
+ React13.useEffect(
1533
+ function() {
1534
+ if (!open || !listRef.current || active < 0) return;
1535
+ const el = listRef.current.querySelector('[data-idx="' + active + '"]');
1536
+ if (el && el.scrollIntoView) el.scrollIntoView({ block: "nearest" });
1537
+ },
1538
+ [open, active, pos[0] != null]
1539
+ );
1540
+ const described = [describedBy(id, props), rest["aria-describedby"]].filter(Boolean).join(" ") || void 0;
1541
+ const field = (child) => /* @__PURE__ */ React13.createElement(
1241
1542
  Field,
1242
1543
  {
1243
1544
  id,
1545
+ labelId: native ? void 0 : id + "-label",
1244
1546
  label: props.label,
1245
1547
  hint: props.hint,
1246
1548
  error: props.error,
@@ -1249,19 +1551,180 @@ window.Aura = (() => {
1249
1551
  disabled: props.disabled,
1250
1552
  className: props.className
1251
1553
  },
1252
- /* @__PURE__ */ React13.createElement("div", { className: cx("aura-input aura-select", props.icon && "has-icon") }, props.icon ? /* @__PURE__ */ React13.createElement(Icon, { name: props.icon, className: "aura-input__icon" }) : null, h3(
1253
- "select",
1254
- Object.assign(extra, rest, {
1255
- ref,
1256
- id,
1257
- className: "aura-input__control",
1258
- required: props.required,
1259
- "aria-invalid": props.error ? true : void 0,
1260
- "aria-describedby": describedBy(id, props)
1261
- }),
1262
- opts,
1263
- props.children
1264
- ), /* @__PURE__ */ React13.createElement(Icon, { name: "chevron-down", className: "aura-select__chevron" }))
1554
+ child
1555
+ );
1556
+ if (!live)
1557
+ return field(
1558
+ /* @__PURE__ */ React13.createElement("div", { className: cx("aura-input aura-select", props.icon && "has-icon") }, props.icon ? /* @__PURE__ */ React13.createElement(Icon, { name: props.icon, className: "aura-input__icon" }) : null, h3(
1559
+ "select",
1560
+ Object.assign(extra, rest, {
1561
+ ref: native ? ref : selMerged,
1562
+ id,
1563
+ className: "aura-input__control",
1564
+ required: props.required,
1565
+ "aria-invalid": props.error ? true : void 0,
1566
+ "aria-describedby": described
1567
+ }),
1568
+ opts,
1569
+ props.children
1570
+ ), /* @__PURE__ */ React13.createElement(Icon, { name: "chevron-down", className: "aura-select__chevron" }))
1571
+ );
1572
+ const listId = id + "-list", labelId = id + "-label";
1573
+ const optId = function(i) {
1574
+ return id + "-opt-" + i;
1575
+ };
1576
+ const labelledBy = rest["aria-labelledby"] || (props.label ? labelId : void 0);
1577
+ const selIndex = selRef.current ? selRef.current.selectedIndex : -1;
1578
+ const option = function(it) {
1579
+ const sel = selIndex === it.index;
1580
+ return /* @__PURE__ */ React13.createElement(
1581
+ "div",
1582
+ {
1583
+ key: it.index,
1584
+ id: optId(it.index),
1585
+ role: "option",
1586
+ "data-idx": it.index,
1587
+ "aria-selected": sel,
1588
+ "aria-disabled": it.disabled || void 0,
1589
+ className: cx(
1590
+ "aura-combo__option",
1591
+ it.index === active && "is-active",
1592
+ sel && "is-selected",
1593
+ it.disabled && "is-disabled"
1594
+ ),
1595
+ onPointerDown: function(e) {
1596
+ e.preventDefault();
1597
+ },
1598
+ onPointerMove: function() {
1599
+ if (!it.disabled && active !== it.index) activeState[1](it.index);
1600
+ },
1601
+ onClick: function() {
1602
+ choose(it);
1603
+ }
1604
+ },
1605
+ /* @__PURE__ */ React13.createElement("span", { className: "aura-combo__text" }, /* @__PURE__ */ React13.createElement("span", { className: "aura-combo__label" }, it.label)),
1606
+ sel ? /* @__PURE__ */ React13.createElement(Icon, { name: "check", className: "aura-combo__check" }) : null
1607
+ );
1608
+ };
1609
+ const runs = [];
1610
+ items2[0].forEach(function(it) {
1611
+ if (it.value === "" && it.disabled) return;
1612
+ const last = runs[runs.length - 1];
1613
+ if (last && last.group === it.group) last.items.push(it);
1614
+ else runs.push({ group: it.group, items: [it] });
1615
+ });
1616
+ const popup = open ? (0, import_react_dom2.createPortal)(
1617
+ /* @__PURE__ */ React13.createElement(
1618
+ "div",
1619
+ {
1620
+ ref: listRef,
1621
+ "data-density": density,
1622
+ className: "aura-combo__popover aura-select__popover",
1623
+ style: pos[0] ? {
1624
+ left: pos[0].left,
1625
+ minWidth: pos[0].width,
1626
+ top: pos[0].top,
1627
+ bottom: pos[0].bottom,
1628
+ maxHeight: pos[0].maxHeight
1629
+ } : { left: -9999, top: -9999 }
1630
+ },
1631
+ /* @__PURE__ */ React13.createElement(
1632
+ "div",
1633
+ {
1634
+ id: listId,
1635
+ role: "listbox",
1636
+ "aria-labelledby": labelledBy,
1637
+ "aria-label": labelledBy ? void 0 : rest["aria-label"],
1638
+ className: "aura-combo__list"
1639
+ },
1640
+ runs.map(function(r, n2) {
1641
+ if (r.group === null) return /* @__PURE__ */ React13.createElement(React13.Fragment, { key: "r" + n2 }, r.items.map(option));
1642
+ const gid = id + "-group-" + n2;
1643
+ return /* @__PURE__ */ React13.createElement("div", { key: "r" + n2, role: "group", "aria-labelledby": gid }, /* @__PURE__ */ React13.createElement("div", { id: gid, className: "aura-select__group" }, r.group), r.items.map(option));
1644
+ })
1645
+ )
1646
+ ),
1647
+ document.body
1648
+ ) : null;
1649
+ const userFocus = rest.onFocus, userBlur = rest.onBlur, userKey = rest.onKeyDown, userClick = rest.onClick;
1650
+ return field(
1651
+ /* @__PURE__ */ React13.createElement(
1652
+ "div",
1653
+ {
1654
+ className: cx("aura-input aura-select aura-select--custom", props.icon && "has-icon", open && "is-open"),
1655
+ "data-invalid": props.error ? "" : void 0
1656
+ },
1657
+ props.icon ? /* @__PURE__ */ React13.createElement(Icon, { name: props.icon, className: "aura-input__icon" }) : null,
1658
+ h3(
1659
+ "select",
1660
+ Object.assign(extra, pick(rest, FORM_KEYS), {
1661
+ ref: selMerged,
1662
+ id: id + "-select",
1663
+ className: "aura-select__native",
1664
+ tabIndex: -1,
1665
+ "aria-hidden": true,
1666
+ required: props.required,
1667
+ onChange: function(e) {
1668
+ sync();
1669
+ if (props.onChange) props.onChange(e);
1670
+ },
1671
+ /* A browser focusing the <select> (the required bubble, a label click) hands focus to the button. */
1672
+ onFocus: function() {
1673
+ if (trigRef.current) trigRef.current.focus();
1674
+ },
1675
+ /* Only the blur the button reports (below): the <select>'s own, while it hands focus over, is not one. */
1676
+ onBlur: function(e) {
1677
+ if (!e.nativeEvent.isTrusted && userBlur) userBlur(e);
1678
+ }
1679
+ }),
1680
+ opts,
1681
+ props.children
1682
+ ),
1683
+ /* @__PURE__ */ React13.createElement(
1684
+ "button",
1685
+ {
1686
+ ...omit(rest, HANDLED_KEYS),
1687
+ ref: trigRef,
1688
+ type: "button",
1689
+ role: "combobox",
1690
+ form: id + "-no-form",
1691
+ className: "aura-input__control aura-select__trigger",
1692
+ disabled: props.disabled,
1693
+ tabIndex: rest.tabIndex,
1694
+ "aria-haspopup": "listbox",
1695
+ "aria-expanded": open,
1696
+ "aria-controls": open ? listId : void 0,
1697
+ "aria-activedescendant": open && active >= 0 ? optId(active) : void 0,
1698
+ id,
1699
+ "aria-label": rest["aria-label"],
1700
+ "aria-labelledby": rest["aria-labelledby"],
1701
+ "aria-required": props.required || void 0,
1702
+ "aria-invalid": props.error ? true : void 0,
1703
+ "aria-describedby": described,
1704
+ onFocus: function(e) {
1705
+ if (userFocus) userFocus(e);
1706
+ },
1707
+ onClick: function(e) {
1708
+ if (userClick) userClick(e);
1709
+ if (e.defaultPrevented) return;
1710
+ if (open) close(true);
1711
+ else openList();
1712
+ },
1713
+ onKeyDown: function(e) {
1714
+ if (userKey) userKey(e);
1715
+ if (!e.defaultPrevented) onKeyDown(e);
1716
+ },
1717
+ onBlur: function(e) {
1718
+ if (listRef.current && e.relatedTarget && listRef.current.contains(e.relatedTarget)) return;
1719
+ if (open) close(false);
1720
+ if (selRef.current) selRef.current.dispatchEvent(new FocusEvent("focusout", { bubbles: true }));
1721
+ }
1722
+ },
1723
+ /* @__PURE__ */ React13.createElement("span", { id: id + "-value", className: cx("aura-select__value", shown[0].empty && "is-placeholder") }, shown[0].label || " ")
1724
+ ),
1725
+ /* @__PURE__ */ React13.createElement(Icon, { name: "chevron-down", className: "aura-select__chevron" }),
1726
+ popup
1727
+ )
1265
1728
  );
1266
1729
  });
1267
1730
 
@@ -1360,7 +1823,7 @@ window.Aura = (() => {
1360
1823
 
1361
1824
  // src/Combobox.tsx
1362
1825
  var React16 = __toESM(require_react(), 1);
1363
- var import_react_dom2 = __toESM(require_react_dom(), 1);
1826
+ var import_react_dom3 = __toESM(require_react_dom(), 1);
1364
1827
  function norm(s) {
1365
1828
  return String(s == null ? "" : s).normalize("NFC").toLocaleLowerCase("th");
1366
1829
  }
@@ -1380,6 +1843,7 @@ window.Aura = (() => {
1380
1843
  const multi = all.multiple === true;
1381
1844
  const mp = all;
1382
1845
  const t = useStrings();
1846
+ const density = useDensity();
1383
1847
  const auto = uid(), id = props.id || auto, listId = id + "-list";
1384
1848
  const options = (props.options || []).map(toOpt);
1385
1849
  const st = useMaybeControlled(
@@ -1542,8 +2006,8 @@ window.Aura = (() => {
1542
2006
  const optId = function(i) {
1543
2007
  return id + "-opt-" + i;
1544
2008
  };
1545
- const list = open && mounted && posState[0] ? (0, import_react_dom2.createPortal)(
1546
- /* @__PURE__ */ React16.createElement("div", { ref: listRef, className: "aura-combo__popover", style: posState[0] }, live.length ? /* @__PURE__ */ React16.createElement(
2009
+ const list = open && mounted && posState[0] ? (0, import_react_dom3.createPortal)(
2010
+ /* @__PURE__ */ React16.createElement("div", { ref: listRef, "data-density": density, className: "aura-combo__popover", style: posState[0] }, live.length ? /* @__PURE__ */ React16.createElement(
1547
2011
  "ul",
1548
2012
  {
1549
2013
  id: listId,
@@ -1703,7 +2167,7 @@ window.Aura = (() => {
1703
2167
 
1704
2168
  // src/DatePicker.tsx
1705
2169
  var React17 = __toESM(require_react(), 1);
1706
- var import_react_dom3 = __toESM(require_react_dom(), 1);
2170
+ var import_react_dom4 = __toESM(require_react_dom(), 1);
1707
2171
 
1708
2172
  // src/dates.ts
1709
2173
  var ERA = /^พ\.ศ\.\s?|\s?(BE|พ\.ศ\.)$/g;
@@ -2308,7 +2772,7 @@ window.Aura = (() => {
2308
2772
  pop.setOpen(false);
2309
2773
  if (inputRef.current) inputRef.current.focus();
2310
2774
  }
2311
- const cal = pop.open && pop.mounted && pop.pos ? (0, import_react_dom3.createPortal)(
2775
+ const cal = pop.open && pop.mounted && pop.pos ? (0, import_react_dom4.createPortal)(
2312
2776
  /* @__PURE__ */ React17.createElement(
2313
2777
  "div",
2314
2778
  {
@@ -2433,7 +2897,7 @@ window.Aura = (() => {
2433
2897
  draft[1](null);
2434
2898
  if (inputRef.current) inputRef.current.focus();
2435
2899
  }
2436
- function pick(iso) {
2900
+ function pick2(iso) {
2437
2901
  if (!draft[0]) {
2438
2902
  draft[1](iso);
2439
2903
  return;
@@ -2443,7 +2907,7 @@ window.Aura = (() => {
2443
2907
  st[1](a <= b ? { start: a, end: b } : { start: b, end: a });
2444
2908
  close();
2445
2909
  }
2446
- const cal = pop.open && pop.mounted && pop.pos ? (0, import_react_dom3.createPortal)(
2910
+ const cal = pop.open && pop.mounted && pop.pos ? (0, import_react_dom4.createPortal)(
2447
2911
  /* @__PURE__ */ React17.createElement(
2448
2912
  "div",
2449
2913
  {
@@ -2477,7 +2941,7 @@ window.Aura = (() => {
2477
2941
  start: draft[0] || v.start,
2478
2942
  end: draft[0] ? null : v.end,
2479
2943
  focus: draft[0] || v.start,
2480
- onSelect: pick
2944
+ onSelect: pick2
2481
2945
  }
2482
2946
  )
2483
2947
  ),
@@ -2547,7 +3011,7 @@ window.Aura = (() => {
2547
3011
 
2548
3012
  // src/Toaster.tsx
2549
3013
  var React19 = __toESM(require_react(), 1);
2550
- var import_react_dom4 = __toESM(require_react_dom(), 1);
3014
+ var import_react_dom5 = __toESM(require_react_dom(), 1);
2551
3015
  var MAX_VISIBLE = 3;
2552
3016
  var toastState = {
2553
3017
  list: [],
@@ -2708,7 +3172,7 @@ window.Aura = (() => {
2708
3172
  };
2709
3173
  }, []);
2710
3174
  if (!mounted) return null;
2711
- return (0, import_react_dom4.createPortal)(
3175
+ return (0, import_react_dom5.createPortal)(
2712
3176
  /* @__PURE__ */ React19.createElement(
2713
3177
  "div",
2714
3178
  {
@@ -2989,7 +3453,7 @@ window.Aura = (() => {
2989
3453
 
2990
3454
  // src/Command.tsx
2991
3455
  var React25 = __toESM(require_react(), 1);
2992
- var import_react_dom5 = __toESM(require_react_dom(), 1);
3456
+ var import_react_dom6 = __toESM(require_react_dom(), 1);
2993
3457
 
2994
3458
  // src/useModal.tsx
2995
3459
  var React24 = __toESM(require_react(), 1);
@@ -3193,7 +3657,7 @@ window.Aura = (() => {
3193
3657
  }
3194
3658
  if (!modal.ready) return null;
3195
3659
  let i = -1;
3196
- return (0, import_react_dom5.createPortal)(
3660
+ return (0, import_react_dom6.createPortal)(
3197
3661
  /* @__PURE__ */ React25.createElement(
3198
3662
  "div",
3199
3663
  {
@@ -3302,7 +3766,7 @@ window.Aura = (() => {
3302
3766
 
3303
3767
  // src/Tooltip.tsx
3304
3768
  var React26 = __toESM(require_react(), 1);
3305
- var import_react_dom6 = __toESM(require_react_dom(), 1);
3769
+ var import_react_dom7 = __toESM(require_react_dom(), 1);
3306
3770
  var Tooltip = React26.forwardRef(function Tooltip2(props, ref) {
3307
3771
  const id = uid(), st = React26.useState(false), open = props.open !== void 0 ? props.open : st[0], set = st[1];
3308
3772
  const anchor = React26.useRef(null), anchorMerged = useMergedRef(ref, anchor), tip = React26.useRef(null), timer = React26.useRef(void 0);
@@ -3400,7 +3864,7 @@ window.Aura = (() => {
3400
3864
  },
3401
3865
  React26.cloneElement(child, { "aria-describedby": open ? id : child.props["aria-describedby"] })
3402
3866
  );
3403
- return /* @__PURE__ */ React26.createElement(React26.Fragment, null, trigger, open && mounted ? (0, import_react_dom6.createPortal)(
3867
+ return /* @__PURE__ */ React26.createElement(React26.Fragment, null, trigger, open && mounted ? (0, import_react_dom7.createPortal)(
3404
3868
  /* @__PURE__ */ React26.createElement(
3405
3869
  "div",
3406
3870
  {
@@ -3422,7 +3886,7 @@ window.Aura = (() => {
3422
3886
 
3423
3887
  // src/Dialog.tsx
3424
3888
  var React27 = __toESM(require_react(), 1);
3425
- var import_react_dom7 = __toESM(require_react_dom(), 1);
3889
+ var import_react_dom8 = __toESM(require_react_dom(), 1);
3426
3890
  var Dialog = React27.forwardRef(function Dialog2(props, ref) {
3427
3891
  const t = useStrings();
3428
3892
  const density = useDensity();
@@ -3437,7 +3901,7 @@ window.Aura = (() => {
3437
3901
  footSelector: ".aura-dialog__foot"
3438
3902
  });
3439
3903
  if (!modal.ready) return null;
3440
- return (0, import_react_dom7.createPortal)(
3904
+ return (0, import_react_dom8.createPortal)(
3441
3905
  /* @__PURE__ */ React27.createElement("div", { className: "aura-dialog-layer", "data-density": density, onKeyDown: modal.onKeyDown }, /* @__PURE__ */ React27.createElement("div", { className: "aura-scrim", onClick: close, "aria-hidden": true }), /* @__PURE__ */ React27.createElement(
3442
3906
  "div",
3443
3907
  {
@@ -3472,7 +3936,7 @@ window.Aura = (() => {
3472
3936
  });
3473
3937
  if (!modal.ready) return null;
3474
3938
  const side = props.side === "left" ? "left" : "right";
3475
- return (0, import_react_dom7.createPortal)(
3939
+ return (0, import_react_dom8.createPortal)(
3476
3940
  /* @__PURE__ */ React27.createElement("div", { className: "aura-dialog-layer aura-drawer-layer", "data-density": density, onKeyDown: modal.onKeyDown }, /* @__PURE__ */ React27.createElement("div", { className: "aura-scrim", onClick: close, "aria-hidden": true }), /* @__PURE__ */ React27.createElement(
3477
3941
  "div",
3478
3942
  {
@@ -3496,7 +3960,7 @@ window.Aura = (() => {
3496
3960
 
3497
3961
  // src/DataTable.tsx
3498
3962
  var React28 = __toESM(require_react(), 1);
3499
- var import_react_dom8 = __toESM(require_react_dom(), 1);
3963
+ var import_react_dom9 = __toESM(require_react_dom(), 1);
3500
3964
  var BP = { sm: 640, md: 768, lg: 1024, xl: 1280 };
3501
3965
  var DEFAULT_COLUMNS = [
3502
3966
  { key: "id", label: "ID", width: 96, mono: true },
@@ -3653,7 +4117,7 @@ window.Aura = (() => {
3653
4117
  function hideFull() {
3654
4118
  setTip(null);
3655
4119
  }
3656
- const tipEl = tip && typeof document !== "undefined" ? (0, import_react_dom8.createPortal)(
4120
+ const tipEl = tip && typeof document !== "undefined" ? (0, import_react_dom9.createPortal)(
3657
4121
  /* @__PURE__ */ React28.createElement(
3658
4122
  "div",
3659
4123
  {
@@ -4903,7 +5367,7 @@ window.Aura = (() => {
4903
5367
 
4904
5368
  // src/SideNav.tsx
4905
5369
  var React31 = __toESM(require_react(), 1);
4906
- var import_react_dom9 = __toESM(require_react_dom(), 1);
5370
+ var import_react_dom10 = __toESM(require_react_dom(), 1);
4907
5371
  function contains(it, id) {
4908
5372
  return !!id && !!it.children && it.children.some(function(c) {
4909
5373
  return c.id === id || contains(c, id);
@@ -5096,7 +5560,7 @@ window.Aura = (() => {
5096
5560
  }
5097
5561
  }
5098
5562
  )) : null,
5099
- tip && typeof document !== "undefined" ? (0, import_react_dom9.createPortal)(
5563
+ tip && typeof document !== "undefined" ? (0, import_react_dom10.createPortal)(
5100
5564
  /* @__PURE__ */ React31.createElement(
5101
5565
  "div",
5102
5566
  {
@@ -5540,7 +6004,7 @@ window.Aura = (() => {
5540
6004
 
5541
6005
  // src/TimePicker.tsx
5542
6006
  var React45 = __toESM(require_react(), 1);
5543
- var import_react_dom10 = __toESM(require_react_dom(), 1);
6007
+ var import_react_dom11 = __toESM(require_react_dom(), 1);
5544
6008
 
5545
6009
  // src/text.ts
5546
6010
  function formatBytes(n2) {
@@ -5756,7 +6220,7 @@ window.Aura = (() => {
5756
6220
  }
5757
6221
  }
5758
6222
  const error = errState[0] || props.error;
5759
- const list = open && mounted && pos[0] ? (0, import_react_dom10.createPortal)(
6223
+ const list = open && mounted && pos[0] ? (0, import_react_dom11.createPortal)(
5760
6224
  /* @__PURE__ */ React45.createElement("div", { ref: listRef, className: "aura-combo__popover aura-time__popover", style: pos[0] }, /* @__PURE__ */ React45.createElement("ul", { id: listId, role: "listbox", "aria-label": props.label, className: "aura-combo__list" }, slots.map(function(s, i) {
5761
6225
  const dis = blocked(s), sel = s === value;
5762
6226
  return /* @__PURE__ */ React45.createElement(
@@ -6747,7 +7211,7 @@ window.Aura = (() => {
6747
7211
 
6748
7212
  // src/Popover.tsx
6749
7213
  var React55 = __toESM(require_react(), 1);
6750
- var import_react_dom11 = __toESM(require_react_dom(), 1);
7214
+ var import_react_dom12 = __toESM(require_react_dom(), 1);
6751
7215
  function position(anchor, pop, placement) {
6752
7216
  const r = anchor.getBoundingClientRect(), pw = pop.offsetWidth, ph = pop.scrollHeight, vw = window.innerWidth, vh = window.innerHeight, gap = 6;
6753
7217
  let side = (placement || "bottom-start").split("-")[0], align = (placement || "bottom-start").split("-")[1] || "start";
@@ -6825,7 +7289,7 @@ window.Aura = (() => {
6825
7289
  [open, mounted]
6826
7290
  );
6827
7291
  const child = React55.Children.only(props.trigger);
6828
- const panel = open && mounted ? (0, import_react_dom11.createPortal)(
7292
+ const panel = open && mounted ? (0, import_react_dom12.createPortal)(
6829
7293
  /* @__PURE__ */ React55.createElement(
6830
7294
  "div",
6831
7295
  {
@@ -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); }
@@ -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>>;
@@ -1,3 +1,4 @@
1
+ // @ts-check
1
2
  // eslint-plugin-aura.js — AURA 5.2
2
3
  // Flags hard-coded colours in JS/TS: hex (#rgb, #rgba, #rrggbb, #rrggbbaa), colour functions (rgb/hsl/hwb/lab/lch/
3
4
  // oklab/oklch/color-mix), CSS named colours in a style value, Tailwind palette classes (bg-red-500, text-white) and
@@ -17,6 +18,7 @@ const HEX = /#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3,4})(?![\w-])/i;
17
18
  * call a template literal continues (`rgb(${r}, …)`). Token-based ones pass: rgb(var(--brand-rgb) / .5),
18
19
  * color-mix(in srgb, var(--aura-accent) 20%, transparent). var() and url() are removed before any check. */
19
20
  const FN_CALL = /\b(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch|color-mix)\(([^()]*)(\)|$)/gi;
21
+ /** @param {string} v */
20
22
  function fnHit(v) {
21
23
  FN_CALL.lastIndex = 0;
22
24
  let m;
@@ -26,7 +28,7 @@ function fnHit(v) {
26
28
  }
27
29
  return null;
28
30
  }
29
- const stripRefs = (v) => v.replace(/var\([^()]*(?:\([^()]*\)[^()]*)*\)/g, ' ').replace(/url\([^)]*\)/g, ' ');
31
+ const stripRefs = (/** @type {string} */ v) => v.replace(/var\([^()]*(?:\([^()]*\)[^()]*)*\)/g, ' ').replace(/url\([^)]*\)/g, ' ');
30
32
  const TW_ARB = new RegExp('(?:^|[\\s:"\'`])(?:' + UTIL + ')-\\[(?:#|rgb|hsl|oklch|color)', 'i');
31
33
  const TW_PAL = new RegExp(
32
34
  '(?:^|[\\s:"\'`!])(?:' + UTIL + ')-(?:(?:' + PALETTE + ')-\\d{2,3}|white|black)(?:\\/\\d+)?(?![\\w-])',
@@ -49,6 +51,10 @@ const COLOUR_KEY =
49
51
  const NOT_COLOUR_ATTR =
50
52
  /^(?:href|to|id|htmlFor|for|src|action|formAction|xlinkHref|xlink:href|aria-[a-z]+|data-[\w-]+|name|key)$/;
51
53
 
54
+ /**
55
+ * @param {unknown} raw
56
+ * @param {boolean} colourKey
57
+ */
52
58
  function hitIn(raw, colourKey) {
53
59
  if (typeof raw !== 'string') return null;
54
60
  const value = stripRefs(raw);
@@ -59,6 +65,8 @@ function hitIn(raw, colourKey) {
59
65
  if (colourKey && NAMED_WHOLE.test(value)) return value.trim();
60
66
  return null;
61
67
  }
68
+ /** An ESTree / JSX AST node, loosely (the plugin runs on whatever parser the project uses). @typedef {any} Node */
69
+ /** @param {Node} node */
62
70
  function skip(node) {
63
71
  let p = node.parent;
64
72
  if (!p) return false;
@@ -80,11 +88,17 @@ function skip(node) {
80
88
  return true;
81
89
  return false;
82
90
  }
91
+ /** @param {Node} node */
83
92
  function colourKeyOf(node) {
84
93
  const p = node.parent;
85
94
  if (p && p.type === 'Property' && p.value === node && p.key) return COLOUR_KEY.test(p.key.name || p.key.value || '');
86
95
  return false;
87
96
  }
97
+ /**
98
+ * @param {{ report(d: { node: Node, message: string }): void }} context
99
+ * @param {Node} node
100
+ * @param {string} hit
101
+ */
88
102
  function report(context, node, hit) {
89
103
  context.report({
90
104
  node,
@@ -101,14 +115,14 @@ const plugin = {
101
115
  docs: { description: 'ห้ามใช้สี hardcode ให้ใช้ token — use AURA semantic tokens, never raw colours' },
102
116
  schema: [],
103
117
  },
104
- create(context) {
118
+ create(/** @type {any} */ context) {
105
119
  return {
106
- Literal(node) {
120
+ Literal(/** @type {Node} */ node) {
107
121
  if (skip(node)) return;
108
122
  const hit = hitIn(node.value, colourKeyOf(node));
109
123
  if (hit) report(context, node, hit);
110
124
  },
111
- TemplateElement(node) {
125
+ TemplateElement(/** @type {Node} */ node) {
112
126
  if (skip(node)) return;
113
127
  const hit = hitIn(node.value && node.value.raw, false);
114
128
  if (hit) report(context, node, hit);
@@ -117,6 +131,7 @@ const plugin = {
117
131
  },
118
132
  },
119
133
  },
134
+ /** @type {Record<string, { plugins: unknown, rules: Record<string, string> }>} */
120
135
  configs: {},
121
136
  };
122
137
  /* Flat config (ESLint 9, the default): the plugin object itself, not a name. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jirawatpyk/aura-tokens",
3
- "version": "5.2.0",
3
+ "version": "5.4.0",
4
4
  "description": "AURA Design System — tokens (CSS variables, Tailwind preset, Figma variables), theme contrast checks and token lint.",
5
5
  "main": "tailwind.tokens.cjs",
6
6
  "types": "components/index.d.ts",
@@ -55,14 +55,14 @@
55
55
  "./package.json": "./package.json"
56
56
  },
57
57
  "scripts": {
58
- "build": "node scripts/build-tokens.js",
58
+ "build": "node scripts/build-tokens.mts",
59
59
  "a11y": "node scripts/a11y-check.js",
60
- "check:tailwind4": "node scripts/tailwind4-check.mjs",
60
+ "check:tailwind4": "node scripts/tailwind4-check.mts",
61
61
  "lint:tokens": "node scripts/lint-tokens.js ../react/src && node scripts/lint-tokens.js ../react/styles",
62
- "check:generated": "node scripts/build-tokens.js && git diff --exit-code -- aura.css aura-fonts.css aura-fonts.local.css tailwind.tokens.cjs aura-tailwind.css aura-tailwind.prefixed.css figma-variables.csv",
62
+ "check:generated": "node scripts/build-tokens.mts && git diff --exit-code -- aura.css aura-fonts.css aura-fonts.local.css tailwind.tokens.cjs aura-tailwind.css aura-tailwind.prefixed.css figma-variables.csv",
63
63
  "prepack": "npm run build",
64
- "check:fonts": "node scripts/fonts-check.mjs",
65
- "check:eslint": "node scripts/eslint-check.mjs"
64
+ "check:fonts": "node scripts/fonts-check.mts",
65
+ "check:eslint": "node scripts/eslint-check.mts"
66
66
  },
67
67
  "keywords": [
68
68
  "design-system",
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ // @ts-check
2
3
  /* AURA contrast check — resolves tokens.json and tests every documented text/ground pair in BOTH themes.
3
4
  * Text needs 4.5:1, control edges and focus rings 3:1 (WCAG 2.1 AA). Writes a11y-report.json; exits 1 on any failure. */
4
5
  const fs = require('fs');
@@ -6,6 +7,11 @@ const path = require('path');
6
7
  const TOKENS = path.join(__dirname, '..', 'tokens.json');
7
8
  const T = JSON.parse(fs.readFileSync(TOKENS, 'utf8'));
8
9
 
10
+ /**
11
+ * @param {unknown} value
12
+ * @param {string} theme
13
+ * @returns {string}
14
+ */
9
15
  function resolve(value, theme, depth = 0) {
10
16
  if (depth > 16) throw new Error('alias loop at ' + value);
11
17
  const m = /^\{(primitive|semantic|component)\.([a-z0-9-]+)(?:\.([a-z0-9]+))?\}$/.exec(String(value).trim());
@@ -16,24 +22,38 @@ function resolve(value, theme, depth = 0) {
16
22
  if (v === undefined) throw new Error(`missing ${tier}.${a} (${theme})`);
17
23
  return resolve(v, theme, depth + 1);
18
24
  }
19
- const tok = (name, theme) => resolve(T.semantic[theme][name] ?? T.component[theme][name], theme);
25
+ const tok = (/** @type {string} */ name, /** @type {string} */ theme) => resolve(T.semantic[theme][name] ?? T.component[theme][name], theme);
20
26
 
27
+ /**
28
+ * @param {string} c
29
+ * @param {string} [ground]
30
+ * @returns {number[]}
31
+ */
21
32
  function rgb(c, ground) {
22
33
  let m = /^#([0-9a-f]{6})$/i.exec(c);
23
- if (m) return [0, 2, 4].map((i) => parseInt(m[1].slice(i, i + 2), 16));
34
+ const hex = m && m[1];
35
+ if (hex) return [0, 2, 4].map((i) => parseInt(hex.slice(i, i + 2), 16));
24
36
  m = /^#([0-9a-f]{3})$/i.exec(c);
25
37
  if (m) return m[1].split('').map((x) => parseInt(x + x, 16));
26
38
  m = /^rgba?\(([^)]+)\)$/i.exec(c);
27
39
  if (m) {
28
40
  const [r, g, b, a = 1] = m[1].split(',').map(Number);
41
+ if ([r, g, b, a].some((x) => x === undefined || Number.isNaN(x))) throw new Error('unsupported colour ' + c);
29
42
  if (a >= 1 || !ground) return [r, g, b];
30
43
  const G = rgb(ground);
31
- return [r, g, b].map((v, i) => Math.round(a * v + (1 - a) * G[i]));
44
+ /** @type {(v: number, i: number) => number} */
45
+ const mix = (v, i) => Math.round(a * v + (1 - a) * (G[i] || 0));
46
+ return [r || 0, g || 0, b || 0].map(mix);
32
47
  }
33
48
  throw new Error('unsupported colour ' + c);
34
49
  }
35
- const lum = (c) => rgb(c).map((v) => v / 255).map((v) => (v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4))
36
- .reduce((s, v, i) => s + v * [0.2126, 0.7152, 0.0722][i], 0);
50
+ const W = [0.2126, 0.7152, 0.0722];
51
+ const lum = (/** @type {string} */ c) => rgb(c).map((v) => v / 255).map((v) => (v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4))
52
+ .reduce((s, v, i) => s + v * (W[i] || 0), 0);
53
+ /**
54
+ * @param {string} fg
55
+ * @param {string} bg
56
+ */
37
57
  function ratio(fg, bg) {
38
58
  const B = '#' + rgb(bg).map((v) => v.toString(16).padStart(2, '0')).join('');
39
59
  const F = '#' + rgb(fg, B).map((v) => v.toString(16).padStart(2, '0')).join('');
@@ -43,6 +63,7 @@ function ratio(fg, bg) {
43
63
 
44
64
  const TEXT = 4.5, UI = 3;
45
65
  const grounds = ['bg-surface', 'bg-canvas', 'bg-surface-hover'];
66
+ /** @type {[string, string, number][]} */
46
67
  const pairs = [];
47
68
  for (const fg of ['fg-primary', 'fg-secondary', 'fg-tertiary', 'fg-accent', 'fg-danger', 'fg-positive']) for (const bg of grounds) pairs.push([fg, bg, TEXT]);
48
69
  pairs.push(['fg-primary', 'bg-selected', TEXT], ['fg-secondary', 'bg-selected', TEXT]);
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ // @ts-check
2
3
  /* Token lint — fails when source files hard-code colours instead of AURA tokens, or (CSS) motion:
3
4
  * a transition/animation must take its duration and easing from --aura-duration-* / --aura-ease / --aura-spring
4
5
  * (`linear` is allowed: it's for spinners and progress, not a feel).
@@ -24,16 +25,19 @@ const NAMED = 'white|black|red|green|blue|yellow|orange|purple|pink|gray|grey|si
24
25
  const CSS_NAMED = new RegExp('(?:^|[;{\\s])(?:color|background(?:-color)?|border(?:-[a-z]+)?-color|fill|stroke|outline-color)\\s*:\\s*[^;]*(?<![\\w-])(?:' + NAMED + ')(?![\\w-])');
25
26
  /* Colour functions count only with a literal colour inside (a hex or three numbers), as in eslint-plugin-aura. */
26
27
  const FN_CALL = /\b(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch|color-mix)\(([^()]*)(\)|$)/gi;
28
+ /** @param {string} v */
27
29
  function fnHit(v) {
28
30
  FN_CALL.lastIndex = 0;
29
31
  let m;
30
32
  while ((m = FN_CALL.exec(v))) if (!m[2] || /#[0-9a-f]{3}/i.test(m[1]) || (m[1].match(/-?\d*\.?\d+/g) || []).length >= 3) return m[0];
31
33
  return null;
32
34
  }
33
- const stripRefs = (v) => v.replace(/var\([^()]*(?:\([^()]*\)[^()]*)*\)/g, ' ').replace(/url\([^)]*\)/g, ' ');
35
+ const stripRefs = (/** @type {string} */ v) => v.replace(/var\([^()]*(?:\([^()]*\)[^()]*)*\)/g, ' ').replace(/url\([^)]*\)/g, ' ');
34
36
  const MOTION_PROP = /\b(?:transition|animation)(?:-duration|-timing-function|-delay)?\s*:/i;
35
37
  const MOTION_RAW = /(?:^|[\s,:(])(?:\d*\.?\d+m?s)\b|cubic-bezier\(|\bease(?:-in-out|-in|-out)?\b/gi;
38
+ /** @type {string[]} */
36
39
  const violations = [];
40
+ /** @param {string} d */
37
41
  function scan(d) {
38
42
  if (!fs.existsSync(d)) return;
39
43
  for (const f of fs.readdirSync(d)) {
@@ -46,8 +50,9 @@ function scan(d) {
46
50
  if (/aura-lint:\s*allow-colours/.test(text.slice(0, 600))) continue;
47
51
  text.split('\n').forEach((line, i) => {
48
52
  const plain = stripRefs(line);
49
- const m = plain.match(COLOUR) || (fnHit(plain) ? [fnHit(plain)] : null);
50
- if (m) violations.push(`${path.relative(process.cwd(), full)}:${i + 1} ${m.map((s) => s.trim().replace(/^[:"'`!]/, '')).join(', ')}`);
53
+ const fn = fnHit(plain);
54
+ const m = plain.match(COLOUR) || (fn ? [fn] : null);
55
+ if (m) violations.push(`${path.relative(process.cwd(), full)}:${i + 1} ${m.map((/** @type {string} */ s) => s.trim().replace(/^[:"'`!]/, '')).join(', ')}`);
51
56
  if (/\.s?css$/.test(f) && !/^\s*--/.test(line)) {
52
57
  const n = CSS_NAMED.exec(stripRefs(line.replace(/\/\*.*?\*\//g, '')));
53
58
  if (n) violations.push(`${path.relative(process.cwd(), full)}:${i + 1} named colour: ${n[0].trim()}`);
@@ -1,4 +1,4 @@
1
- /* GENERATED by scripts/build-tokens.js from tokens.json. Do not edit by hand. */
1
+ /* GENERATED by scripts/build-tokens.mts from tokens.json. Do not edit by hand. */
2
2
  module.exports = {
3
3
  "colors": {
4
4
  "aura": {
package/tokens.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "5.2.0",
2
+ "version": "5.4.0",
3
3
  "meta": {
4
4
  "name": "AURA Design System",
5
5
  "tier": "3-tier",