react-os-shell 4.31.0 → 4.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/{Browser-63NYU74S.js → Browser-5NHZNED2.js} +6 -6
  2. package/dist/{Browser-63NYU74S.js.map → Browser-5NHZNED2.js.map} +1 -1
  3. package/dist/{Calculator-XERXEV35.js → Calculator-PHKNMRLX.js} +5 -5
  4. package/dist/{Calculator-XERXEV35.js.map → Calculator-PHKNMRLX.js.map} +1 -1
  5. package/dist/{CurrencyConverter-OSZISLHT.js → CurrencyConverter-MEG7RHEK.js} +5 -5
  6. package/dist/{CurrencyConverter-OSZISLHT.js.map → CurrencyConverter-MEG7RHEK.js.map} +1 -1
  7. package/dist/{Documents-YP4GBBRF.js → Documents-YOUMLERR.js} +7 -7
  8. package/dist/{Documents-YP4GBBRF.js.map → Documents-YOUMLERR.js.map} +1 -1
  9. package/dist/{Files-6JWIE3VH.js → Files-4NQ5IFUE.js} +8 -8
  10. package/dist/{Files-6JWIE3VH.js.map → Files-4NQ5IFUE.js.map} +1 -1
  11. package/dist/{Notepad-EHO56UNK.js → Notepad-UHTEKEEM.js} +7 -7
  12. package/dist/{Notepad-EHO56UNK.js.map → Notepad-UHTEKEEM.js.map} +1 -1
  13. package/dist/{PomodoroTimer-5ZSIG7SQ.js → PomodoroTimer-5B5AFXQH.js} +7 -7
  14. package/dist/{PomodoroTimer-5ZSIG7SQ.js.map → PomodoroTimer-5B5AFXQH.js.map} +1 -1
  15. package/dist/{Preview-67IPCMSN.js → Preview-2CCBN25S.js} +8 -8
  16. package/dist/{Preview-67IPCMSN.js.map → Preview-2CCBN25S.js.map} +1 -1
  17. package/dist/{Spreadsheet-HVXBAFR7.js → Spreadsheet-2N6TMVQU.js} +6 -6
  18. package/dist/{Spreadsheet-HVXBAFR7.js.map → Spreadsheet-2N6TMVQU.js.map} +1 -1
  19. package/dist/{Stock-4BORXILI.js → Stock-KTWRNBI6.js} +5 -5
  20. package/dist/{Stock-4BORXILI.js.map → Stock-KTWRNBI6.js.map} +1 -1
  21. package/dist/{Weather-ZPPSEUIY.js → Weather-B7MO7N4N.js} +5 -5
  22. package/dist/{Weather-ZPPSEUIY.js.map → Weather-B7MO7N4N.js.map} +1 -1
  23. package/dist/{WorldClock-LBKYWDWE.js → WorldClock-M5MEV74F.js} +5 -5
  24. package/dist/{WorldClock-LBKYWDWE.js.map → WorldClock-M5MEV74F.js.map} +1 -1
  25. package/dist/apps/index.js +16 -16
  26. package/dist/{chunk-22BNPLM4.js → chunk-3OCL3DB4.js} +3 -3
  27. package/dist/{chunk-22BNPLM4.js.map → chunk-3OCL3DB4.js.map} +1 -1
  28. package/dist/{chunk-7NIFBQUX.js → chunk-3SUSQHOW.js} +492 -118
  29. package/dist/chunk-3SUSQHOW.js.map +1 -0
  30. package/dist/{chunk-GUWAKATU.js → chunk-5S65ZC5W.js} +3 -3
  31. package/dist/{chunk-GUWAKATU.js.map → chunk-5S65ZC5W.js.map} +1 -1
  32. package/dist/{chunk-BT3EQNH3.js → chunk-BIZJ7RLC.js} +4 -4
  33. package/dist/{chunk-BT3EQNH3.js.map → chunk-BIZJ7RLC.js.map} +1 -1
  34. package/dist/{chunk-X5PZYTAK.js → chunk-ECE3B3FZ.js} +3 -3
  35. package/dist/{chunk-X5PZYTAK.js.map → chunk-ECE3B3FZ.js.map} +1 -1
  36. package/dist/{chunk-X6DOWIY2.js → chunk-FHPPPDHL.js} +3 -3
  37. package/dist/{chunk-X6DOWIY2.js.map → chunk-FHPPPDHL.js.map} +1 -1
  38. package/dist/{chunk-OI253GOA.js → chunk-IOCOH2QQ.js} +4 -4
  39. package/dist/{chunk-OI253GOA.js.map → chunk-IOCOH2QQ.js.map} +1 -1
  40. package/dist/{chunk-4AKVHY4B.js → chunk-TK6HI2U4.js} +23 -3
  41. package/dist/chunk-TK6HI2U4.js.map +1 -0
  42. package/dist/chunk-XVXEYKMT.js +7 -0
  43. package/dist/{chunk-SGS3IQEL.js.map → chunk-XVXEYKMT.js.map} +1 -1
  44. package/dist/{chunk-C22EFSND.js → chunk-ZCDFNZQS.js} +21 -6
  45. package/dist/chunk-ZCDFNZQS.js.map +1 -0
  46. package/dist/{index-CdxMm6Dk.d.ts → index-B6QGa8SJ.d.ts} +293 -38
  47. package/dist/index.d.ts +2 -2
  48. package/dist/index.js +14 -14
  49. package/dist/ui/index.d.ts +1 -1
  50. package/dist/ui/index.js +4 -4
  51. package/dist/ui.css +117 -43
  52. package/package.json +1 -1
  53. package/dist/chunk-4AKVHY4B.js.map +0 -1
  54. package/dist/chunk-7NIFBQUX.js.map +0 -1
  55. package/dist/chunk-C22EFSND.js.map +0 -1
  56. package/dist/chunk-SGS3IQEL.js +0 -7
@@ -1,6 +1,6 @@
1
1
  import { Breadcrumbs } from './chunk-HNH7FOYB.js';
2
2
  import { SidebarLayout } from './chunk-YG45DAUT.js';
3
- import { useIsMobile, useShellPrefs } from './chunk-C22EFSND.js';
3
+ import { useIsMobile, PopupMenu, PopupMenuItem, useShellPrefs } from './chunk-ZCDFNZQS.js';
4
4
  import { registerModalEscapeInterceptor, useFocusTrap, useScrollLock } from './chunk-OXLLRKBC.js';
5
5
  import { glassStyle } from './chunk-3CUSBZWG.js';
6
6
  import { forwardRef, useId, useCallback, useState, useRef, useEffect, createContext, useLayoutEffect, useMemo, Children, isValidElement, cloneElement, useContext, Fragment as Fragment$1, useSyncExternalStore } from 'react';
@@ -13,11 +13,27 @@ var BUTTON_VARIANTS = {
13
13
  secondary: "border border-gray-300 bg-white text-gray-700 shadow-sm hover:bg-gray-50 focus:ring-blue-400/30",
14
14
  ghost: "text-gray-700 hover:bg-gray-100 focus:ring-blue-400/30",
15
15
  "ghost-danger": "text-red-600 hover:bg-red-50 focus:ring-red-400/30",
16
- danger: "bg-red-600 text-white shadow-sm hover:bg-red-700 focus:ring-red-400/40"
16
+ danger: "bg-red-600 text-white shadow-sm hover:bg-red-700 focus:ring-red-400/40",
17
+ /**
18
+ * A button that reads as a link: no box, no padding, underlined on hover.
19
+ * For an action that belongs in running text or beside a field — "Forgot
20
+ * your password?", "Use a different address" — where a boxed button would
21
+ * claim more of the page than the action deserves.
22
+ *
23
+ * It is still a <button>. Anything that NAVIGATES should be an anchor, so it
24
+ * can be middle-clicked and read off the status bar; this is for actions
25
+ * that merely look quiet.
26
+ */
27
+ link: "text-blue-600 underline-offset-4 hover:underline focus:ring-blue-400/30"
17
28
  };
18
29
  var SIZES = {
19
30
  sm: "gap-1 px-2.5 py-1 text-xs",
20
31
  md: "gap-1.5 px-3 py-1.5 text-sm",
32
+ // `py-2.5`, not `py-2`: this rung has to come out 40px to match IconButton's
33
+ // `h-10`, which is the promise IconButton's docblock makes for every rung.
34
+ // `py-2` gives 36px (20px line box + 8px twice) and puts a 4px step between a
35
+ // Button and an IconButton sitting in the same row.
36
+ lg: "gap-2 px-4 py-2.5 text-sm",
21
37
  // 44px is the WCAG 2.5.5 floor, and a floor is not a comfortable size. Use it
22
38
  // ONLY for chrome that sits outside the task — switching screens, ending a
23
39
  // shift — and never on the path someone is actually working through. It
@@ -31,6 +47,16 @@ var SIZES = {
31
47
  "touch-xl": "gap-3 h-20 px-8 text-2xl"
32
48
  // 80px — keypad keys, tender
33
49
  };
50
+ var LINK_BASE = "inline-flex items-center font-medium transition-colors focus:outline-none focus:ring-2 disabled:cursor-not-allowed disabled:opacity-60";
51
+ var LINK_SIZES = {
52
+ sm: "gap-1 text-xs",
53
+ md: "gap-1.5 text-sm",
54
+ lg: "gap-2 text-sm",
55
+ "touch-sm": "gap-1.5 text-base",
56
+ "touch": "gap-2 text-base",
57
+ "touch-lg": "gap-2 text-lg",
58
+ "touch-xl": "gap-3 text-2xl"
59
+ };
34
60
  function Spinner() {
35
61
  return /* @__PURE__ */ jsxs("svg", { className: "h-3.5 w-3.5 animate-spin", viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: [
36
62
  /* @__PURE__ */ jsx("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }),
@@ -63,7 +89,18 @@ var Button = forwardRef(function Button2({
63
89
  disabled: isDisabled,
64
90
  "aria-busy": loading || void 0,
65
91
  "aria-describedby": reasonId,
66
- className: [BUTTON_BASE, BUTTON_VARIANTS[variant], SIZES[size], block ? "w-full" : "", className].filter(Boolean).join(" "),
92
+ className: [
93
+ // `link` takes the base WITHOUT the box: no rounding, no padding from
94
+ // the size table. It cannot cancel those by overriding — two padding
95
+ // utilities in one class attribute resolve by compiled-stylesheet
96
+ // order, not by the order they were written — so it never receives
97
+ // them, and keeps only the size's gap and text size.
98
+ variant === "link" ? LINK_BASE : BUTTON_BASE,
99
+ BUTTON_VARIANTS[variant],
100
+ variant === "link" ? LINK_SIZES[size] : SIZES[size],
101
+ block ? "w-full" : "",
102
+ className
103
+ ].filter(Boolean).join(" "),
67
104
  ...rest,
68
105
  children: [
69
106
  loading ? /* @__PURE__ */ jsx(Spinner, {}) : leftIcon,
@@ -82,6 +119,7 @@ var Button_default = Button;
82
119
  var SQUARE = {
83
120
  sm: "h-6 w-6",
84
121
  md: "h-8 w-8",
122
+ lg: "h-10 w-10",
85
123
  "touch-sm": "h-11 w-11",
86
124
  "touch": "h-14 w-14",
87
125
  "touch-lg": "h-16 w-16",
@@ -105,7 +143,12 @@ var IconButton_default = IconButton;
105
143
  var INPUT_SHAPE_PRE = "block w-full rounded-md border border-gray-300 bg-white";
106
144
  var INPUT_SHAPE_POST = "text-gray-800 placeholder:text-gray-400 shadow-sm focus:border-blue-400 focus:outline-none focus:ring-2 focus:ring-blue-400/30";
107
145
  var INPUT_SIZES = {
146
+ // The desktop rungs carry no explicit height: a text control's height should
147
+ // follow its own font, or a long label wraps out of a box that was pinned.
148
+ // The touch rung does carry one, for the same reason Button's does.
149
+ sm: "px-2.5 py-1 text-xs",
108
150
  md: "px-3 py-1.5 text-sm",
151
+ lg: "px-3.5 py-2 text-base",
109
152
  // 56px, matching Button's `touch`. `text-base` is 16px BY AGREEMENT with the
110
153
  // rule in ui.css that forces `font-size: 16px !important` on every form
111
154
  // control under `(pointer: coarse)` — the rule that stops iOS zooming the
@@ -130,6 +173,7 @@ var Input = forwardRef(function Input2({ invalid, leftIcon, rightAdornment, size
130
173
  "input",
131
174
  {
132
175
  ref,
176
+ "aria-invalid": invalid || void 0,
133
177
  className: inputClasses({ invalid, size, className: [pad2, className].filter(Boolean).join(" ") }),
134
178
  ...rest
135
179
  }
@@ -143,7 +187,7 @@ var Input = forwardRef(function Input2({ invalid, leftIcon, rightAdornment, size
143
187
  ] });
144
188
  });
145
189
  var Input_default = Input;
146
- var Textarea = forwardRef(function Textarea2({ invalid, autoGrow, className = "", onInput, rows = 3, ...rest }, ref) {
190
+ var Textarea = forwardRef(function Textarea2({ invalid, autoGrow, className = "", onInput, rows = 3, size, ...rest }, ref) {
147
191
  const handleInput = useCallback((e) => {
148
192
  if (autoGrow) {
149
193
  const el = e.currentTarget;
@@ -156,9 +200,10 @@ var Textarea = forwardRef(function Textarea2({ invalid, autoGrow, className = ""
156
200
  "textarea",
157
201
  {
158
202
  ref,
203
+ "aria-invalid": invalid || void 0,
159
204
  rows,
160
205
  onInput: handleInput,
161
- className: inputClasses({ invalid, className: `${autoGrow ? "resize-none overflow-hidden" : ""} ${className}`.trim() }),
206
+ className: inputClasses({ invalid, size, className: `${autoGrow ? "resize-none overflow-hidden" : ""} ${className}`.trim() }),
162
207
  ...rest
163
208
  }
164
209
  );
@@ -201,14 +246,15 @@ function matchTypeahead(options, buffer, fromIndex) {
201
246
  var MENU_GAP = 4;
202
247
  var VIEWPORT_MARGIN = 8;
203
248
  var MENU_MAX_HEIGHT = 240;
204
- var NativeSelect = forwardRef(function NativeSelect2({ value, onChange, options, placeholder, invalid, className = "", ...rest }, ref) {
249
+ var NativeSelect = forwardRef(function NativeSelect2({ value, onChange, options, placeholder, invalid, className = "", size, ...rest }, ref) {
205
250
  return /* @__PURE__ */ jsxs(
206
251
  "select",
207
252
  {
208
253
  ref,
209
254
  value,
210
255
  onChange: (e) => onChange(e.target.value),
211
- className: inputClasses({ invalid, className: `pr-8 ${className}`.trim() }),
256
+ "aria-invalid": invalid || void 0,
257
+ className: inputClasses({ invalid, size, className: `pr-8 ${className}`.trim() }),
212
258
  ...rest,
213
259
  children: [
214
260
  placeholder !== void 0 && /* @__PURE__ */ jsx("option", { value: "", disabled: true, children: placeholder }),
@@ -257,7 +303,21 @@ function useAnchoredPosition(triggerRef, open) {
257
303
  }, [open, triggerRef]);
258
304
  return pos;
259
305
  }
260
- var ListboxSelect = forwardRef(function ListboxSelect2({ value, onChange, options, placeholder, invalid, className = "", id, disabled, ...rest }, ref) {
306
+ var ListboxSelect = forwardRef(function ListboxSelect2({
307
+ value,
308
+ onChange,
309
+ options,
310
+ placeholder,
311
+ invalid,
312
+ className = "",
313
+ id,
314
+ disabled,
315
+ size,
316
+ "aria-describedby": describedBy,
317
+ "aria-label": ariaLabel,
318
+ "aria-labelledby": labelledBy,
319
+ ...rest
320
+ }, ref) {
261
321
  const [open, setOpen] = useState(false);
262
322
  const [active, setActive] = useState(-1);
263
323
  const wrapRef = useRef(null);
@@ -391,10 +451,13 @@ var ListboxSelect = forwardRef(function ListboxSelect2({ value, onChange, option
391
451
  "aria-controls": open ? listboxId : void 0,
392
452
  "aria-activedescendant": open && active >= 0 ? optionId(active) : void 0,
393
453
  "aria-invalid": invalid || void 0,
454
+ "aria-describedby": describedBy,
455
+ "aria-label": ariaLabel,
456
+ "aria-labelledby": labelledBy,
394
457
  disabled,
395
458
  onClick: () => open ? close() : openList(),
396
459
  onKeyDown,
397
- className: inputClasses({ invalid, className: `pr-8 text-left ${className}`.trim() }),
460
+ className: inputClasses({ invalid, size, className: `pr-8 text-left ${className}`.trim() }),
398
461
  children: /* @__PURE__ */ jsx("span", { className: `block truncate ${selectedLabel === void 0 ? "text-gray-400" : ""}`, children: selectedLabel ?? placeholder ?? "\xA0" })
399
462
  }
400
463
  ),
@@ -556,10 +619,23 @@ function FormField({
556
619
  return /* @__PURE__ */ jsxs("div", { className, children: [
557
620
  label && /* @__PURE__ */ jsxs("label", { id: labelId, htmlFor, className: "mb-1 block text-xs font-medium text-gray-600", children: [
558
621
  label,
559
- required && /* @__PURE__ */ jsx("span", { className: "ml-0.5 text-red-500", children: "*" })
622
+ required && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "ml-0.5 text-red-500", children: "*" })
560
623
  ] }),
561
624
  children,
562
- error ? /* @__PURE__ */ jsx("p", { id: htmlFor ? `${htmlFor}-error` : void 0, className: "mt-1 text-[11px] text-red-600", children: error }) : hint ? /* @__PURE__ */ jsx("p", { id: htmlFor ? `${htmlFor}-hint` : void 0, className: "mt-1 text-[11px] text-gray-400", children: hint }) : null
625
+ error ? (
626
+ // A validation message appears after the user submits, without focus
627
+ // moving to it. Without a live region it reaches nobody — and this is
628
+ // the one status message a form cannot afford to lose.
629
+ /* @__PURE__ */ jsx(
630
+ "p",
631
+ {
632
+ id: htmlFor ? `${htmlFor}-error` : void 0,
633
+ role: "alert",
634
+ className: "mt-1 text-[11px] text-red-600",
635
+ children: error
636
+ }
637
+ )
638
+ ) : hint ? /* @__PURE__ */ jsx("p", { id: htmlFor ? `${htmlFor}-hint` : void 0, className: "mt-1 text-[11px] text-gray-400", children: hint }) : null
563
639
  ] });
564
640
  }
565
641
  function Label({ required, children, className = "", ...rest }) {
@@ -1413,6 +1489,7 @@ var DatePicker = forwardRef(function DatePicker2({ value, onChange, min, max, in
1413
1489
  {
1414
1490
  ref,
1415
1491
  type: "date",
1492
+ "aria-invalid": invalid || void 0,
1416
1493
  value: toInputValue(value),
1417
1494
  min: toInputValue(min) || void 0,
1418
1495
  max: toInputValue(max) || void 0,
@@ -1701,6 +1778,23 @@ var InputNumber = forwardRef(function InputNumber2({
1701
1778
  setText(next);
1702
1779
  onChange(parse(next));
1703
1780
  };
1781
+ const bump = (by) => {
1782
+ const from = parse(textRef.current) ?? 0;
1783
+ let next = from + by;
1784
+ if (min != null && next < min) next = min;
1785
+ if (max != null && next > max) next = max;
1786
+ const places = precision ?? String(step ?? 1).split(".")[1]?.length ?? 0;
1787
+ next = Number(next.toFixed(places));
1788
+ setText(format(next, precision));
1789
+ onChange(next);
1790
+ };
1791
+ const handleKeyDown = (event) => {
1792
+ const unit = step ?? 1;
1793
+ const by = event.key === "ArrowUp" ? unit : event.key === "ArrowDown" ? -unit : event.key === "PageUp" ? unit * 10 : event.key === "PageDown" ? -unit * 10 : null;
1794
+ if (by === null || disabled) return;
1795
+ event.preventDefault();
1796
+ bump(by);
1797
+ };
1704
1798
  const handleBlur = () => {
1705
1799
  const parsed = parse(text);
1706
1800
  if (parsed == null) {
@@ -1710,6 +1804,7 @@ var InputNumber = forwardRef(function InputNumber2({
1710
1804
  let v = parsed;
1711
1805
  if (min != null && v < min) v = min;
1712
1806
  if (max != null && v > max) v = max;
1807
+ if (precision != null) v = Number(v.toFixed(precision));
1713
1808
  setText(format(v, precision));
1714
1809
  if (v !== parsed) onChange(v);
1715
1810
  }
@@ -1727,8 +1822,14 @@ var InputNumber = forwardRef(function InputNumber2({
1727
1822
  disabled,
1728
1823
  placeholder,
1729
1824
  step,
1825
+ role: "spinbutton",
1826
+ "aria-valuenow": value ?? void 0,
1827
+ "aria-valuemin": min,
1828
+ "aria-valuemax": max,
1829
+ "aria-valuetext": value == null ? void 0 : format(value, precision),
1730
1830
  "aria-invalid": invalid || void 0,
1731
1831
  onChange: (e) => handleChange(e.target.value),
1832
+ onKeyDown: handleKeyDown,
1732
1833
  onBlur: handleBlur,
1733
1834
  className: inputClasses({
1734
1835
  invalid,
@@ -2104,13 +2205,52 @@ var PADDING = {
2104
2205
  md: { body: "p-4", edge: "px-4 py-3" },
2105
2206
  lg: { body: "p-6", edge: "px-6 py-4" }
2106
2207
  };
2107
- function Card({ children, header, footer, padded = true, padding, className = "" }) {
2208
+ function Card({
2209
+ children,
2210
+ header,
2211
+ footer,
2212
+ padded = true,
2213
+ padding,
2214
+ headingLevel,
2215
+ headerActions,
2216
+ "aria-label": ariaLabel,
2217
+ className = "",
2218
+ bodyClassName = "",
2219
+ style
2220
+ }) {
2108
2221
  const p = PADDING[padding ?? (padded ? "md" : "none")];
2109
- return /* @__PURE__ */ jsxs("div", { className: `rounded-lg border border-gray-200 bg-white shadow-sm ${className}`.trim(), children: [
2110
- header && /* @__PURE__ */ jsx("div", { className: `border-b border-gray-100 ${p.edge} text-sm font-semibold text-gray-900`, children: header }),
2111
- /* @__PURE__ */ jsx("div", { className: p.body, children }),
2112
- footer && /* @__PURE__ */ jsx("div", { className: `border-t border-gray-100 ${p.edge}`, children: footer })
2113
- ] });
2222
+ const headingId = useId();
2223
+ const titled = headingLevel != null && header != null;
2224
+ const Heading = `h${headingLevel}`;
2225
+ const named = titled || ariaLabel != null && ariaLabel !== "";
2226
+ const Root = named ? "section" : "div";
2227
+ const surface = `rounded-lg border border-gray-200 bg-white shadow-sm ${className}`.trim();
2228
+ const headerCls = `border-b border-gray-100 ${p.edge} text-sm font-semibold text-gray-900`;
2229
+ return /* @__PURE__ */ jsxs(
2230
+ Root,
2231
+ {
2232
+ className: surface,
2233
+ style,
2234
+ ...titled ? { "aria-labelledby": headingId } : named ? { "aria-label": ariaLabel } : {},
2235
+ children: [
2236
+ header != null && (headerActions != null ? (
2237
+ // With actions the row has to be a flex container, so the heading is
2238
+ // nested rather than being the row itself — and only the title is
2239
+ // inside it.
2240
+ /* @__PURE__ */ jsxs("div", { className: `${headerCls} flex items-center justify-between gap-3`, children: [
2241
+ titled ? /* @__PURE__ */ jsx(Heading, { id: headingId, children: header }) : /* @__PURE__ */ jsx("span", { children: header }),
2242
+ headerActions
2243
+ ] })
2244
+ ) : titled ? (
2245
+ // Without them the heading carries the row's own styling rather than
2246
+ // nesting a second element, so the title is not two boxes deep.
2247
+ /* @__PURE__ */ jsx(Heading, { id: headingId, className: headerCls, children: header })
2248
+ ) : /* @__PURE__ */ jsx("div", { className: headerCls, children: header })),
2249
+ /* @__PURE__ */ jsx("div", { className: [p.body, bodyClassName].filter(Boolean).join(" "), children }),
2250
+ footer && /* @__PURE__ */ jsx("div", { className: `border-t border-gray-100 ${p.edge}`, children: footer })
2251
+ ]
2252
+ }
2253
+ );
2114
2254
  }
2115
2255
  function StatCard({ label, value, icon, delta, className = "" }) {
2116
2256
  const deltaColor = delta?.direction === "up" ? "text-green-600" : delta?.direction === "down" ? "text-red-600" : "text-gray-500";
@@ -2336,7 +2476,8 @@ function DescriptionList({
2336
2476
  bordered = false,
2337
2477
  size = "md",
2338
2478
  title,
2339
- className = ""
2479
+ className = "",
2480
+ emptyText: empty = "\u2014"
2340
2481
  }) {
2341
2482
  const resolved = typeof columns === "number" ? { base: columns } : columns;
2342
2483
  const base = resolved.base ?? 1;
@@ -2361,7 +2502,7 @@ function DescriptionList({
2361
2502
  ].filter(Boolean).join(" "),
2362
2503
  children: [
2363
2504
  /* @__PURE__ */ jsx("dt", { className: `${text} text-gray-500`, children: item.label }),
2364
- /* @__PURE__ */ jsx("dd", { className: `mt-0.5 min-w-0 break-words ${text} text-gray-900`, children: item.value })
2505
+ /* @__PURE__ */ jsx("dd", { className: `mt-0.5 min-w-0 break-words ${text} text-gray-900`, children: item.value === null || item.value === void 0 || item.value === "" ? empty : item.value })
2365
2506
  ]
2366
2507
  },
2367
2508
  item.key ?? i
@@ -2642,49 +2783,89 @@ function Banner({
2642
2783
  }
2643
2784
  );
2644
2785
  }
2645
- function Tabs({ items, value, onChange, variant = "underline", className = "" }) {
2786
+ var tabButtonId = (prefix, id) => `${prefix}-tab-${id}`;
2787
+ var tabPanelId = (prefix, id) => `${prefix}-panel-${id}`;
2788
+ var LIST_CLASS = {
2789
+ underline: "flex items-center gap-1 border-b border-gray-200",
2790
+ pill: "inline-flex items-center gap-1 rounded-lg bg-gray-100 p-1"
2791
+ };
2792
+ function tabClass(variant, active) {
2646
2793
  if (variant === "pill") {
2647
- return /* @__PURE__ */ jsx("div", { role: "tablist", className: `inline-flex items-center gap-1 rounded-lg bg-gray-100 p-1 ${className}`.trim(), children: items.map((t) => {
2648
- const active = t.id === value;
2649
- return /* @__PURE__ */ jsxs(
2650
- "button",
2651
- {
2652
- type: "button",
2653
- role: "tab",
2654
- "aria-selected": active,
2655
- tabIndex: active ? 0 : -1,
2656
- disabled: t.disabled,
2657
- onClick: () => onChange(t.id),
2658
- className: `inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50 ${active ? "bg-blue-600 text-white shadow-sm" : "text-gray-600 hover:text-gray-900"}`,
2659
- children: [
2660
- t.icon,
2661
- t.label
2662
- ]
2663
- },
2664
- t.id
2665
- );
2666
- }) });
2794
+ return `inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50 ${active ? "bg-blue-600 text-white shadow-sm" : "text-gray-600 hover:text-gray-900"}`;
2667
2795
  }
2668
- return /* @__PURE__ */ jsx("div", { role: "tablist", className: `flex items-center gap-1 border-b border-gray-200 ${className}`.trim(), children: items.map((t) => {
2669
- const active = t.id === value;
2670
- return /* @__PURE__ */ jsxs(
2671
- "button",
2672
- {
2673
- type: "button",
2674
- role: "tab",
2675
- "aria-selected": active,
2676
- tabIndex: active ? 0 : -1,
2677
- disabled: t.disabled,
2678
- onClick: () => onChange(t.id),
2679
- className: `-mb-px inline-flex items-center gap-1.5 border-b-2 px-3 py-2 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50 ${active ? "border-blue-600 text-blue-600" : "border-transparent text-gray-600 hover:border-gray-300 hover:text-gray-900"}`,
2680
- children: [
2681
- t.icon,
2682
- t.label
2683
- ]
2684
- },
2685
- t.id
2686
- );
2687
- }) });
2796
+ return `-mb-px inline-flex items-center gap-1.5 border-b-2 px-3 py-2 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50 ${active ? "border-blue-600 text-blue-600" : "border-transparent text-gray-600 hover:border-gray-300 hover:text-gray-900"}`;
2797
+ }
2798
+ function Tabs({
2799
+ items,
2800
+ value,
2801
+ onChange,
2802
+ variant = "underline",
2803
+ idPrefix,
2804
+ className = "",
2805
+ "aria-label": ariaLabel,
2806
+ "aria-labelledby": ariaLabelledBy
2807
+ }) {
2808
+ const buttons = useRef([]);
2809
+ const step = (from, direction) => {
2810
+ for (let i = 1; i <= items.length; i += 1) {
2811
+ const at = ((from + direction * i) % items.length + items.length) % items.length;
2812
+ if (!items[at]?.disabled) return at;
2813
+ }
2814
+ return null;
2815
+ };
2816
+ const edge = (direction) => {
2817
+ const order = direction === 1 ? items.map((_, i) => i) : items.map((_, i) => items.length - 1 - i);
2818
+ for (const i of order) if (!items[i]?.disabled) return i;
2819
+ return null;
2820
+ };
2821
+ const onKeyDown = (event) => {
2822
+ const current = items.findIndex((t) => t.id === value);
2823
+ let next = null;
2824
+ if (event.key === "ArrowRight" || event.key === "ArrowDown") next = step(current, 1);
2825
+ else if (event.key === "ArrowLeft" || event.key === "ArrowUp") next = step(current, -1);
2826
+ else if (event.key === "Home") next = edge(1);
2827
+ else if (event.key === "End") next = edge(-1);
2828
+ else return;
2829
+ event.preventDefault();
2830
+ if (next === null || next === current) return;
2831
+ onChange(items[next].id);
2832
+ buttons.current[next]?.focus();
2833
+ };
2834
+ return /* @__PURE__ */ jsx(
2835
+ "div",
2836
+ {
2837
+ role: "tablist",
2838
+ "aria-label": ariaLabel,
2839
+ "aria-labelledby": ariaLabelledBy,
2840
+ onKeyDown,
2841
+ className: `${LIST_CLASS[variant]} ${className}`.trim(),
2842
+ children: items.map((t, i) => {
2843
+ const active = t.id === value;
2844
+ return /* @__PURE__ */ jsxs(
2845
+ "button",
2846
+ {
2847
+ ref: (el) => {
2848
+ buttons.current[i] = el;
2849
+ },
2850
+ type: "button",
2851
+ role: "tab",
2852
+ id: idPrefix ? tabButtonId(idPrefix, t.id) : void 0,
2853
+ "aria-controls": idPrefix ? tabPanelId(idPrefix, t.id) : void 0,
2854
+ "aria-selected": active,
2855
+ tabIndex: active ? 0 : -1,
2856
+ disabled: t.disabled,
2857
+ onClick: () => onChange(t.id),
2858
+ className: tabClass(variant, active),
2859
+ children: [
2860
+ t.icon && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "inline-flex shrink-0", children: t.icon }),
2861
+ t.label
2862
+ ]
2863
+ },
2864
+ t.id
2865
+ );
2866
+ })
2867
+ }
2868
+ );
2688
2869
  }
2689
2870
  function Accordion({
2690
2871
  items,
@@ -2789,11 +2970,6 @@ function Tooltip({ content, side = "top", delay = 200, children }) {
2789
2970
  }
2790
2971
  );
2791
2972
  }
2792
- function ColoredBadge({ colorClass, children, size = "sm", capitalize = false }) {
2793
- const sizeCls = size === "xs" ? "px-2 py-0.5 text-[10px]" : size === "md" ? "px-2.5 py-0.5 text-xs" : "px-2 py-0.5 text-xs";
2794
- const capCls = capitalize ? "capitalize" : "";
2795
- return /* @__PURE__ */ jsx("span", { className: `inline-flex items-center rounded-full font-medium ${sizeCls} ${capCls} ${colorClass}`, children });
2796
- }
2797
2973
  var GROUP_COLORS = {
2798
2974
  success: "bg-green-100 text-green-800",
2799
2975
  active: "bg-blue-100 text-blue-800",
@@ -2819,6 +2995,36 @@ function StatusBadge({ status }) {
2819
2995
  const label = status.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
2820
2996
  return /* @__PURE__ */ jsx("span", { className: `inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${color}`, children: label });
2821
2997
  }
2998
+ function ColoredBadge({
2999
+ colorClass,
3000
+ tone,
3001
+ children,
3002
+ size = "sm",
3003
+ capitalize = false,
3004
+ closable = false,
3005
+ onClose,
3006
+ closeLabel,
3007
+ className = ""
3008
+ }) {
3009
+ const colour = colorClass ?? GROUP_COLORS[tone ?? "neutral"];
3010
+ const sizeCls = size === "xs" ? "px-2 py-0.5 text-[10px]" : size === "md" ? "px-2.5 py-0.5 text-xs" : "px-2 py-0.5 text-xs";
3011
+ const capCls = capitalize ? "capitalize" : "";
3012
+ const closeName = closeLabel ?? (typeof children === "string" ? `Remove ${children}` : "Remove");
3013
+ const closeSize = size === "xs" ? "h-2.5 w-2.5" : "h-3 w-3";
3014
+ return /* @__PURE__ */ jsxs("span", { className: ["inline-flex items-center rounded-full font-medium", sizeCls, capCls, colour, className].filter(Boolean).join(" "), children: [
3015
+ children,
3016
+ closable && /* @__PURE__ */ jsx(
3017
+ "button",
3018
+ {
3019
+ type: "button",
3020
+ onClick: onClose,
3021
+ "aria-label": closeName,
3022
+ className: "-mr-0.5 ml-1 inline-flex shrink-0 items-center justify-center rounded-full opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-blue-400/40",
3023
+ children: /* @__PURE__ */ jsx("svg", { className: closeSize, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" }) })
3024
+ }
3025
+ )
3026
+ ] });
3027
+ }
2822
3028
  function EmptyState({
2823
3029
  title,
2824
3030
  message,
@@ -2830,12 +3036,14 @@ function EmptyState({
2830
3036
  children
2831
3037
  }) {
2832
3038
  const v = frameless ? "none" : variant ?? "dashed";
2833
- const showIcon = icon ?? v !== "card";
3039
+ const custom = typeof icon !== "boolean" && icon != null ? icon : null;
3040
+ const showIcon = custom ? false : icon ?? v !== "card";
2834
3041
  const primary = message ?? (title ? void 0 : "Nothing here yet.");
2835
3042
  const secondary = description ?? hint;
2836
3043
  const frame = v === "card" ? "rounded-md border border-gray-200 bg-white px-6 py-12" : v === "none" ? "px-6 py-12" : "rounded-lg border-2 border-dashed border-gray-300 px-6 py-12";
2837
3044
  return /* @__PURE__ */ jsxs("div", { className: `flex flex-col items-center justify-center text-center ${frame}`, children: [
2838
- showIcon && /* @__PURE__ */ jsx("svg", { className: "h-10 w-10 text-gray-300 mb-3", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" }) }),
3045
+ custom && /* @__PURE__ */ jsx("div", { className: "mb-3 flex h-10 w-10 items-center justify-center text-gray-300", "aria-hidden": "true", children: custom }),
3046
+ showIcon && /* @__PURE__ */ jsx("svg", { className: "h-10 w-10 text-gray-300 mb-3", "aria-hidden": "true", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" }) }),
2839
3047
  title && /* @__PURE__ */ jsx("h3", { className: "text-sm font-medium text-gray-900", children: title }),
2840
3048
  primary && /* @__PURE__ */ jsx("p", { className: `text-sm text-gray-500 ${title ? "mt-2" : ""}`, children: primary }),
2841
3049
  secondary && /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-400 mt-1", children: secondary }),
@@ -3854,6 +4062,133 @@ function TopNav({ items, activeKey, onSelect, brand, actions, className }) {
3854
4062
  }
3855
4063
  );
3856
4064
  }
4065
+ function DropdownMenu({
4066
+ trigger,
4067
+ items,
4068
+ align = "end",
4069
+ "aria-label": ariaLabel,
4070
+ className = "",
4071
+ disabled
4072
+ }) {
4073
+ const [open, setOpen] = useState(false);
4074
+ const [active, setActive] = useState(0);
4075
+ const wrapRef = useRef(null);
4076
+ const triggerRef = useRef(null);
4077
+ const itemRefs = useRef([]);
4078
+ const menuId = useId();
4079
+ const close = useCallback((restoreFocus) => {
4080
+ setOpen(false);
4081
+ if (restoreFocus) triggerRef.current?.focus();
4082
+ }, []);
4083
+ const enabledFrom = (from, step) => {
4084
+ for (let i = 0; i < items.length; i += 1) {
4085
+ const at = ((from + step * i) % items.length + items.length) % items.length;
4086
+ if (!items[at]?.disabled) return at;
4087
+ }
4088
+ return from;
4089
+ };
4090
+ useEffect(() => {
4091
+ if (!open) return;
4092
+ const onPointerDown = (e) => {
4093
+ if (!wrapRef.current?.contains(e.target)) close(false);
4094
+ };
4095
+ document.addEventListener("pointerdown", onPointerDown, true);
4096
+ return () => document.removeEventListener("pointerdown", onPointerDown, true);
4097
+ }, [open, close]);
4098
+ useEffect(() => {
4099
+ if (open) itemRefs.current[active]?.focus();
4100
+ }, [open, active]);
4101
+ useEffect(() => {
4102
+ if (!open) return;
4103
+ return registerModalEscapeInterceptor((event) => {
4104
+ if (event.key !== "Escape") return false;
4105
+ close(true);
4106
+ return true;
4107
+ });
4108
+ }, [open, close]);
4109
+ const openAt = (index) => {
4110
+ setActive(index);
4111
+ setOpen(true);
4112
+ };
4113
+ const onTriggerKeyDown = (e) => {
4114
+ if (e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") {
4115
+ e.preventDefault();
4116
+ openAt(enabledFrom(0, 1));
4117
+ } else if (e.key === "ArrowUp") {
4118
+ e.preventDefault();
4119
+ openAt(enabledFrom(items.length - 1, -1));
4120
+ }
4121
+ };
4122
+ const onMenuKeyDown = (e) => {
4123
+ if (e.key === "ArrowDown") {
4124
+ e.preventDefault();
4125
+ setActive(enabledFrom(active + 1, 1));
4126
+ } else if (e.key === "ArrowUp") {
4127
+ e.preventDefault();
4128
+ setActive(enabledFrom(active - 1, -1));
4129
+ } else if (e.key === "Home") {
4130
+ e.preventDefault();
4131
+ setActive(enabledFrom(0, 1));
4132
+ } else if (e.key === "End") {
4133
+ e.preventDefault();
4134
+ setActive(enabledFrom(items.length - 1, -1));
4135
+ } else if (e.key === "Tab") {
4136
+ e.preventDefault();
4137
+ close(true);
4138
+ }
4139
+ };
4140
+ const select = (item) => {
4141
+ if (item.disabled) return;
4142
+ close(true);
4143
+ item.onSelect();
4144
+ };
4145
+ return /* @__PURE__ */ jsxs("div", { ref: wrapRef, className: `relative inline-flex ${className}`.trim(), children: [
4146
+ /* @__PURE__ */ jsx(
4147
+ "button",
4148
+ {
4149
+ ref: triggerRef,
4150
+ type: "button",
4151
+ "aria-haspopup": "menu",
4152
+ "aria-expanded": open,
4153
+ "aria-controls": open ? menuId : void 0,
4154
+ "aria-label": ariaLabel,
4155
+ disabled,
4156
+ onClick: () => open ? close(false) : openAt(enabledFrom(0, 1)),
4157
+ onKeyDown: onTriggerKeyDown,
4158
+ className: "inline-flex items-center justify-center rounded-md text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-400 disabled:cursor-not-allowed disabled:opacity-60",
4159
+ children: trigger
4160
+ }
4161
+ ),
4162
+ open && /* @__PURE__ */ jsx(
4163
+ "div",
4164
+ {
4165
+ className: `absolute top-full z-50 mt-1 ${align === "end" ? "right-0" : "left-0"}`,
4166
+ onKeyDown: onMenuKeyDown,
4167
+ children: /* @__PURE__ */ jsx(PopupMenu, { portal: false, minWidth: 180, children: /* @__PURE__ */ jsx("div", { id: menuId, role: "menu", "aria-orientation": "vertical", "aria-label": ariaLabel, children: items.map((item, i) => /* @__PURE__ */ jsxs(
4168
+ PopupMenuItem,
4169
+ {
4170
+ ref: (el) => {
4171
+ itemRefs.current[i] = el;
4172
+ },
4173
+ role: "menuitem",
4174
+ tabIndex: i === active ? 0 : -1,
4175
+ danger: item.danger,
4176
+ disabled: item.disabled,
4177
+ onMouseEnter: () => {
4178
+ if (!item.disabled) setActive(i);
4179
+ },
4180
+ onClick: () => select(item),
4181
+ children: [
4182
+ item.icon,
4183
+ item.label
4184
+ ]
4185
+ },
4186
+ item.key
4187
+ )) }) })
4188
+ }
4189
+ )
4190
+ ] });
4191
+ }
3857
4192
  var SIZE_PX2 = { sm: 320, md: 448, lg: 640 };
3858
4193
  var SIDE_POSITION = {
3859
4194
  right: "inset-y-0 right-0",
@@ -4297,6 +4632,7 @@ function Pagination({
4297
4632
  showEdges && /* @__PURE__ */ jsx("button", { type: "button", "aria-label": "Last page", disabled: page === pageCount, onClick: () => go(pageCount), className: `${BTN} text-gray-600 hover:bg-gray-100`, children: "\xBB" })
4298
4633
  ] });
4299
4634
  }
4635
+ var isGroup = (h) => Array.isArray(h.columns);
4300
4636
  var PAD = {
4301
4637
  sm: "px-2 py-1.5",
4302
4638
  md: "px-3 py-2"
@@ -4319,6 +4655,7 @@ function DataTable({
4319
4655
  sort = null,
4320
4656
  onSortChange,
4321
4657
  pagination,
4658
+ caption,
4322
4659
  loading = false,
4323
4660
  bordered = false,
4324
4661
  size = "md",
@@ -4336,13 +4673,52 @@ function DataTable({
4336
4673
  if (!onSortChange) return;
4337
4674
  const field = fieldOf(col);
4338
4675
  const current = directionFor(col);
4339
- if (current === null) onSortChange({ field, direction: "asc" });
4340
- else if (current === "asc") onSortChange({ field, direction: "desc" });
4676
+ const first = col.sortFirst ?? "asc";
4677
+ const second = first === "asc" ? "desc" : "asc";
4678
+ if (current === null) onSortChange({ field, direction: first });
4679
+ else if (current === first) onSortChange({ field, direction: second });
4341
4680
  else onSortChange(null);
4342
4681
  };
4682
+ const leaves = columns.flatMap((h) => isGroup(h) ? h.columns : [h]);
4683
+ const grouped = columns.some(isGroup);
4343
4684
  const cellBase = `${PAD[size]} ${size === "sm" ? "text-xs" : "text-sm"}`;
4685
+ const headerCell = (col, ci, rowSpan) => {
4686
+ const dir = directionFor(col);
4687
+ const align = ALIGN2[col.align ?? (col.numeric ? "right" : "left")];
4688
+ const pinned = col.fixed === "left";
4689
+ return /* @__PURE__ */ jsx(
4690
+ "th",
4691
+ {
4692
+ scope: "col",
4693
+ rowSpan: rowSpan > 1 ? rowSpan : void 0,
4694
+ "aria-sort": dir === "asc" ? "ascending" : dir === "desc" ? "descending" : col.sortable && onSortChange ? "none" : void 0,
4695
+ style: { width: col.width, ...pinned ? { left: offsets[ci] ?? 0 } : {} },
4696
+ className: [
4697
+ cellBase,
4698
+ align,
4699
+ "font-semibold uppercase tracking-wide text-gray-500",
4700
+ size === "sm" ? "text-[10px]" : "text-xs",
4701
+ pinned ? "sticky z-10 bg-gray-50" : "",
4702
+ col.headerClassName ?? ""
4703
+ ].filter(Boolean).join(" "),
4704
+ children: col.sortable && onSortChange ? /* @__PURE__ */ jsxs(
4705
+ "button",
4706
+ {
4707
+ type: "button",
4708
+ onClick: () => toggleSort(col),
4709
+ className: `inline-flex items-center font-semibold uppercase tracking-wide hover:text-gray-700 ${align}`,
4710
+ children: [
4711
+ col.title,
4712
+ /* @__PURE__ */ jsx(SortIcon, { direction: dir })
4713
+ ]
4714
+ }
4715
+ ) : col.title
4716
+ },
4717
+ col.key
4718
+ );
4719
+ };
4344
4720
  let fixedOffset = 0;
4345
- const offsets = columns.map((col) => {
4721
+ const offsets = leaves.map((col) => {
4346
4722
  if (col.fixed !== "left") return null;
4347
4723
  const at = fixedOffset;
4348
4724
  fixedOffset += col.width ?? 0;
@@ -4358,56 +4734,56 @@ function DataTable({
4358
4734
  /* @__PURE__ */ jsx("span", { className: "sr-only", children: "Loading" })
4359
4735
  ] }),
4360
4736
  /* @__PURE__ */ jsxs("table", { className: "w-full border-collapse tabular-nums", style: minWidth ? { minWidth } : void 0, children: [
4361
- /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { className: "border-b border-gray-200 bg-gray-50", children: columns.map((col, ci) => {
4362
- const dir = directionFor(col);
4363
- const align = ALIGN2[col.align ?? "left"];
4364
- const pinned = col.fixed === "left";
4365
- return /* @__PURE__ */ jsx(
4366
- "th",
4367
- {
4368
- scope: "col",
4369
- "aria-sort": dir === "asc" ? "ascending" : dir === "desc" ? "descending" : void 0,
4370
- style: {
4371
- width: col.width,
4372
- ...pinned ? { left: offsets[ci] ?? 0 } : {}
4373
- },
4374
- className: [
4375
- cellBase,
4376
- align,
4377
- "font-semibold uppercase tracking-wide text-gray-500",
4378
- size === "sm" ? "text-[10px]" : "text-xs",
4379
- pinned ? "sticky z-10 bg-gray-50" : "",
4380
- col.headerClassName ?? ""
4381
- ].filter(Boolean).join(" "),
4382
- children: col.sortable && onSortChange ? /* @__PURE__ */ jsxs(
4383
- "button",
4737
+ caption && /* @__PURE__ */ jsx("caption", { className: "sr-only", children: caption }),
4738
+ /* @__PURE__ */ jsxs("thead", { children: [
4739
+ /* @__PURE__ */ jsx("tr", { className: "border-b border-gray-200 bg-gray-50", children: columns.map((h, hi) => {
4740
+ if (isGroup(h)) {
4741
+ return /* @__PURE__ */ jsx(
4742
+ "th",
4384
4743
  {
4385
- type: "button",
4386
- onClick: () => toggleSort(col),
4387
- className: `inline-flex items-center font-semibold uppercase tracking-wide hover:text-gray-700 ${align}`,
4388
- children: [
4389
- col.title,
4390
- /* @__PURE__ */ jsx(SortIcon, { direction: dir })
4391
- ]
4392
- }
4393
- ) : col.title
4394
- },
4395
- col.key
4396
- );
4397
- }) }) }),
4398
- /* @__PURE__ */ jsx("tbody", { children: data.length === 0 && !loading ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx("td", { colSpan: columns.length, className: `${cellBase} py-10 text-center text-gray-500`, children: emptyText }) }) : data.map((row, i) => {
4744
+ scope: "colgroup",
4745
+ colSpan: h.columns.length,
4746
+ className: [
4747
+ cellBase,
4748
+ "text-center",
4749
+ "font-semibold uppercase tracking-wide text-gray-500",
4750
+ size === "sm" ? "text-[10px]" : "text-xs",
4751
+ "border-b border-gray-200",
4752
+ h.headerClassName ?? ""
4753
+ ].filter(Boolean).join(" "),
4754
+ children: h.title
4755
+ },
4756
+ h.key ?? `group-${hi}`
4757
+ );
4758
+ }
4759
+ return headerCell(h, leaves.indexOf(h), grouped ? 2 : 1);
4760
+ }) }),
4761
+ grouped && /* @__PURE__ */ jsx("tr", { className: "border-b border-gray-200 bg-gray-50", children: columns.flatMap((h) => isGroup(h) ? h.columns : []).map(
4762
+ (col) => headerCell(col, leaves.indexOf(col), 1)
4763
+ ) })
4764
+ ] }),
4765
+ /* @__PURE__ */ jsx("tbody", { children: data.length === 0 && !loading ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx("td", { colSpan: leaves.length, className: `${cellBase} py-10 text-center text-gray-500`, children: emptyText }) }) : data.map((row, i) => {
4399
4766
  const rowProps = onRow?.(row, i);
4400
4767
  return /* @__PURE__ */ jsx(
4401
4768
  "tr",
4402
4769
  {
4403
4770
  onClick: rowProps?.onClick,
4771
+ ...rowProps?.onClick ? {
4772
+ tabIndex: 0,
4773
+ onKeyDown: (e) => {
4774
+ if (e.target !== e.currentTarget) return;
4775
+ if (e.key !== "Enter" && e.key !== " ") return;
4776
+ e.preventDefault();
4777
+ rowProps.onClick?.();
4778
+ }
4779
+ } : {},
4404
4780
  className: [
4405
4781
  "border-b border-gray-100 last:border-b-0",
4406
- rowProps?.onClick ? "cursor-pointer hover:bg-gray-50" : "",
4782
+ rowProps?.onClick ? "cursor-pointer hover:bg-gray-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-blue-400" : "",
4407
4783
  rowClassName?.(row, i) ?? "",
4408
4784
  rowProps?.className ?? ""
4409
4785
  ].filter(Boolean).join(" "),
4410
- children: columns.map((col, ci) => {
4786
+ children: leaves.map((col, ci) => {
4411
4787
  const pinned = col.fixed === "left";
4412
4788
  return /* @__PURE__ */ jsx(
4413
4789
  "td",
@@ -4415,7 +4791,8 @@ function DataTable({
4415
4791
  style: pinned ? { left: offsets[ci] ?? 0 } : void 0,
4416
4792
  className: [
4417
4793
  cellBase,
4418
- ALIGN2[col.align ?? "left"],
4794
+ ALIGN2[col.align ?? (col.numeric ? "right" : "left")],
4795
+ col.numeric ? "font-mono" : "",
4419
4796
  "text-gray-900",
4420
4797
  col.ellipsis ? "max-w-0 truncate" : "",
4421
4798
  // A pinned cell needs its own background or the
@@ -5137,16 +5514,13 @@ var COPY2 = {
5137
5514
  404: { title: "Page not found", body: "The page you're looking for doesn't exist or was moved." },
5138
5515
  500: { title: "Something went wrong", body: "An unexpected error occurred. Please try again." }
5139
5516
  };
5140
- function ErrorPage({ code = 404 }) {
5517
+ function ErrorPage({ code = 404, actions }) {
5141
5518
  const c = COPY2[code];
5142
5519
  return /* @__PURE__ */ jsxs("div", { className: "flex h-full flex-col items-center justify-center bg-gray-50 p-6 text-center", children: [
5143
5520
  /* @__PURE__ */ jsx("div", { className: "text-7xl font-bold tracking-tight text-gray-400", children: code }),
5144
5521
  /* @__PURE__ */ jsx("h1", { className: "mt-2 text-xl font-semibold text-gray-900", children: c.title }),
5145
5522
  /* @__PURE__ */ jsx("p", { className: "mt-1 max-w-sm text-sm text-gray-500", children: c.body }),
5146
- /* @__PURE__ */ jsxs("div", { className: "mt-6 flex items-center gap-2", children: [
5147
- /* @__PURE__ */ jsx(Button_default, { variant: "secondary", children: "Go back" }),
5148
- /* @__PURE__ */ jsx(Button_default, { children: "Take me home" })
5149
- ] })
5523
+ actions && /* @__PURE__ */ jsx("div", { className: "mt-6 flex items-center gap-2", children: actions })
5150
5524
  ] });
5151
5525
  }
5152
5526
  function subscribeMediaQuery(cb) {
@@ -5282,6 +5656,6 @@ var ALT_SHIFT_D = isMac ? "\u2325\u21E7D" : "Alt+Shift+D";
5282
5656
  var ALT_SHIFT_E = isMac ? "\u2325\u21E7E" : "Alt+Shift+E";
5283
5657
  var ALT_SHIFT_N = isMac ? "\u2325\u21E7N" : "Alt+Shift+N";
5284
5658
 
5285
- export { ALT, ALT_SHIFT_D, ALT_SHIFT_E, ALT_SHIFT_N, Accordion, AuthScreen, Avatar, AvatarGroup, Banner, BarChart, Button_default, CMD_A, CMD_DOT, CMD_ENTER, CMD_K, CMD_S, CMD_SHIFT_Z, CMD_Z, Card, ChatTemplate, Checkbox_default, CheckoutTemplate, ColoredBadge, ContainerFillChart, CountBadge, DashboardTemplate, DataTable, DataTablePage, DatePicker_default, DateRangePicker, DescriptionList, Divider, DonutChart, Drawer, ENTER, EmailTemplate, EmptyState, ErrorPage, FilePicker, FormField, FormLayoutPage, GalleryTemplate, GlobalSearch, Grid, HelpCenter, INPUT_BASE, INPUT_SIZES, IconButton_default, Inline, InputNumber_default, Input_default, Kanban, Label, ListFooter, ListLoadError, LoadingSpinner, MAX_FRACTION_DIGITS, MOD, Markdown, MediaUploadField, MediaUploadGrid, MetricBar, MilestoneTimeline, NativeSelect, NumericKeypad, PageHeader, Pagination, Paragraph, Radio_default, Result, SHIFT, SearchableSelect, Segmented, Select_default, SidebarActionButton, SidebarGroupLabel, SidebarNavItem, Skeleton, Sparkline, Stack, StatCard, Statistic, StatusBadge, StatusBadgeProvider, Switch_default, Tabs, Text, Textarea_default, TileButton_default, Title, Tooltip, TopNav, appendKey, applyThemePrefs, backspace, firstEnabledIndex, formatDate, inputClasses, isMac, isSeverityTone, lastEnabledIndex, matchTypeahead, mediaFileName, nextEnabledIndex, resolveTheme, severityOf, toISODate, useClickOutside, useTheme };
5286
- //# sourceMappingURL=chunk-7NIFBQUX.js.map
5287
- //# sourceMappingURL=chunk-7NIFBQUX.js.map
5659
+ export { ALT, ALT_SHIFT_D, ALT_SHIFT_E, ALT_SHIFT_N, Accordion, AuthScreen, Avatar, AvatarGroup, Banner, BarChart, Button_default, CMD_A, CMD_DOT, CMD_ENTER, CMD_K, CMD_S, CMD_SHIFT_Z, CMD_Z, Card, ChatTemplate, Checkbox_default, CheckoutTemplate, ColoredBadge, ContainerFillChart, CountBadge, DashboardTemplate, DataTable, DataTablePage, DatePicker_default, DateRangePicker, DescriptionList, Divider, DonutChart, Drawer, DropdownMenu, ENTER, EmailTemplate, EmptyState, ErrorPage, FilePicker, FormField, FormLayoutPage, GROUP_COLORS, GalleryTemplate, GlobalSearch, Grid, HelpCenter, INPUT_BASE, INPUT_SIZES, IconButton_default, Inline, InputNumber_default, Input_default, Kanban, Label, ListFooter, ListLoadError, LoadingSpinner, MAX_FRACTION_DIGITS, MOD, Markdown, MediaUploadField, MediaUploadGrid, MetricBar, MilestoneTimeline, NativeSelect, NumericKeypad, PageHeader, Pagination, Paragraph, Radio_default, Result, SHIFT, SearchableSelect, Segmented, Select_default, SidebarActionButton, SidebarGroupLabel, SidebarNavItem, Skeleton, Sparkline, Stack, StatCard, Statistic, StatusBadge, StatusBadgeProvider, Switch_default, Tabs, Text, Textarea_default, TileButton_default, Title, Tooltip, TopNav, appendKey, applyThemePrefs, backspace, firstEnabledIndex, formatDate, inputClasses, isMac, isSeverityTone, lastEnabledIndex, matchTypeahead, mediaFileName, nextEnabledIndex, resolveTheme, severityOf, tabButtonId, tabPanelId, toISODate, useClickOutside, useTheme };
5660
+ //# sourceMappingURL=chunk-3SUSQHOW.js.map
5661
+ //# sourceMappingURL=chunk-3SUSQHOW.js.map