@tensor_1/react 2.16.0 → 2.18.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 (75) hide show
  1. package/README.md +29 -0
  2. package/dist/chart-preset.cjs +15 -3
  3. package/dist/chart.cjs +15 -3
  4. package/dist/components/Alert.js +15 -7
  5. package/dist/components/AppShell.d.ts +3 -1
  6. package/dist/components/AppShell.js +3 -1
  7. package/dist/components/Avatar.js +2 -1
  8. package/dist/components/Button.js +32 -16
  9. package/dist/components/Card.d.ts +3 -1
  10. package/dist/components/Card.js +2 -2
  11. package/dist/components/ChartCore.js +15 -3
  12. package/dist/components/CommandPalette.d.ts +4 -2
  13. package/dist/components/CommandPalette.js +66 -13
  14. package/dist/components/Comments.d.ts +5 -1
  15. package/dist/components/Comments.js +1 -1
  16. package/dist/components/Composer.d.ts +8 -1
  17. package/dist/components/Composer.js +7 -2
  18. package/dist/components/DetailLayout.js +1 -1
  19. package/dist/components/Drawer.d.ts +3 -4
  20. package/dist/components/Drawer.js +6 -4
  21. package/dist/components/Field.js +6 -3
  22. package/dist/components/Inbox.d.ts +13 -3
  23. package/dist/components/Inbox.js +62 -3
  24. package/dist/components/Input.d.ts +13 -2
  25. package/dist/components/Input.js +20 -5
  26. package/dist/components/Kanban.d.ts +15 -3
  27. package/dist/components/Kanban.js +42 -10
  28. package/dist/components/Meter.d.ts +3 -1
  29. package/dist/components/Meter.js +9 -1
  30. package/dist/components/Modal.d.ts +4 -5
  31. package/dist/components/Modal.js +4 -4
  32. package/dist/components/NavFavourites.js +11 -0
  33. package/dist/components/PageHeader.d.ts +3 -1
  34. package/dist/components/PageHeader.js +2 -2
  35. package/dist/components/Peek.d.ts +3 -4
  36. package/dist/components/Peek.js +9 -6
  37. package/dist/components/Progress.d.ts +3 -1
  38. package/dist/components/Progress.js +5 -3
  39. package/dist/components/Radio.d.ts +3 -1
  40. package/dist/components/Radio.js +6 -2
  41. package/dist/components/RelationshipGraph.d.ts +81 -3
  42. package/dist/components/RelationshipGraph.js +81 -57
  43. package/dist/components/RelationshipGraphLayered.d.ts +8 -0
  44. package/dist/components/RelationshipGraphLayered.js +420 -0
  45. package/dist/components/RelationshipGraphList.d.ts +16 -0
  46. package/dist/components/RelationshipGraphList.js +33 -0
  47. package/dist/components/Spinner.d.ts +3 -1
  48. package/dist/components/Spinner.js +3 -3
  49. package/dist/components/Stat.js +5 -1
  50. package/dist/components/States.d.ts +1 -0
  51. package/dist/components/States.js +7 -1
  52. package/dist/components/Table.d.ts +8 -1
  53. package/dist/components/Table.js +10 -6
  54. package/dist/components/Tabs.d.ts +13 -1
  55. package/dist/components/Tabs.js +79 -4
  56. package/dist/components/Timeline.d.ts +3 -3
  57. package/dist/components/Timeline.js +3 -3
  58. package/dist/components/Toast.js +42 -5
  59. package/dist/components/Tooltip.d.ts +3 -1
  60. package/dist/components/Tooltip.js +44 -5
  61. package/dist/components/Tree.d.ts +2 -0
  62. package/dist/components/Tree.js +26 -4
  63. package/dist/hooks/useDialog.d.ts +1 -1
  64. package/dist/hooks/useDialog.js +17 -2
  65. package/dist/hooks/useTable.d.ts +5 -0
  66. package/dist/hooks/useTable.js +20 -1
  67. package/dist/index.cjs +1791 -446
  68. package/dist/index.d.cts +311 -36
  69. package/dist/index.d.ts +4 -4
  70. package/dist/index.js +10 -3
  71. package/dist/relationship-graph-model.d.ts +112 -1
  72. package/dist/relationship-graph-model.js +420 -1
  73. package/dist/tree-model.d.ts +4 -0
  74. package/dist/tree-model.js +8 -0
  75. package/package.json +1 -1
@@ -2,14 +2,24 @@ import * as react from 'react';
2
2
  import { HTMLAttributes, ReactNode } from 'react';
3
3
 
4
4
  interface InboxProps extends HTMLAttributes<HTMLDivElement> {
5
+ /**
6
+ * Which pane a narrow viewport (<700px) shows: 'list' (default, matches the old
7
+ * behaviour) or 'detail'. Consumers should render a Back control in the detail pane
8
+ * on narrow viewports to return to the list (UIX-A11Y-2).
9
+ */
10
+ view?: 'list' | 'detail';
5
11
  children?: ReactNode;
6
12
  }
7
13
  /** Two-pane inbox master-detail container backed by `.uix-inbox`. Place `<InboxList>` and `<InboxDetail>` as children. */
8
- declare function Inbox({ children, className, ...props }: InboxProps): react.JSX.Element;
14
+ declare function Inbox({ view, children, className, ...props }: InboxProps): react.JSX.Element;
9
15
  interface InboxListProps extends HTMLAttributes<HTMLDivElement> {
10
16
  children?: ReactNode;
11
17
  }
12
- /** Left-pane item list. Place `<InboxItem>` children here. */
18
+ /**
19
+ * Left-pane item list. Place `<InboxItem>` children here. A keyboard listbox
20
+ * (UIX-A11Y-2): the list is the single tab stop; ArrowUp/ArrowDown/Home/End move the
21
+ * active option via `aria-activedescendant`, Enter/Space click it.
22
+ */
13
23
  declare function InboxList({ children, className, ...props }: InboxListProps): react.JSX.Element;
14
24
  interface InboxItemProps extends HTMLAttributes<HTMLDivElement> {
15
25
  /** Primary subject / title line. */
@@ -25,7 +35,7 @@ interface InboxItemProps extends HTMLAttributes<HTMLDivElement> {
25
35
  children?: ReactNode;
26
36
  }
27
37
  /** A single inbox row with subject, preview excerpt, optional leading slot, and unread/selected states. */
28
- declare function InboxItem({ subject, preview, leading, unread, selected, children, className, ...props }: InboxItemProps): react.JSX.Element;
38
+ declare function InboxItem({ subject, preview, leading, unread, selected, children, className, id, ...props }: InboxItemProps): react.JSX.Element;
29
39
  interface InboxDetailProps extends HTMLAttributes<HTMLDivElement> {
30
40
  children?: ReactNode;
31
41
  }
@@ -1,10 +1,63 @@
1
+ "use client";
1
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { createContext, useContext, useId, useMemo, useRef, useState } from "react";
2
4
  import { cx } from "../cx.js";
3
- function Inbox({ children, className, ...props }) {
4
- return /* @__PURE__ */ jsx("div", { className: cx("uix-inbox", className), ...props, children });
5
+ function Inbox({ view, children, className, ...props }) {
6
+ return /* @__PURE__ */ jsx("div", { className: cx("uix-inbox", className), "data-view": view, ...props, children });
5
7
  }
8
+ const InboxListCtx = createContext({});
6
9
  function InboxList({ children, className, ...props }) {
7
- return /* @__PURE__ */ jsx("div", { className: cx("uix-inbox__list", className), role: "listbox", ...props, children });
10
+ const listRef = useRef(null);
11
+ const [activeId, setActiveId] = useState(void 0);
12
+ const ctx = useMemo(() => ({ activeId }), [activeId]);
13
+ const onKeyDown = (e) => {
14
+ const opts = Array.from(listRef.current?.querySelectorAll('[role="option"]') ?? []);
15
+ if (opts.length === 0) return;
16
+ const idx = opts.findIndex((el2) => el2.id === activeId);
17
+ let next;
18
+ switch (e.key) {
19
+ case "ArrowDown":
20
+ next = Math.min(idx + 1, opts.length - 1);
21
+ break;
22
+ case "ArrowUp":
23
+ next = idx < 0 ? 0 : Math.max(idx - 1, 0);
24
+ break;
25
+ case "Home":
26
+ next = 0;
27
+ break;
28
+ case "End":
29
+ next = opts.length - 1;
30
+ break;
31
+ case "Enter":
32
+ case " ":
33
+ if (idx >= 0) {
34
+ e.preventDefault();
35
+ opts[idx]?.click();
36
+ }
37
+ return;
38
+ default:
39
+ return;
40
+ }
41
+ e.preventDefault();
42
+ const el = opts[next];
43
+ if (!el || !el.id) return;
44
+ setActiveId(el.id);
45
+ el.scrollIntoView({ block: "nearest" });
46
+ };
47
+ return /* @__PURE__ */ jsx(InboxListCtx.Provider, { value: ctx, children: /* @__PURE__ */ jsx(
48
+ "div",
49
+ {
50
+ ref: listRef,
51
+ className: cx("uix-inbox__list", className),
52
+ role: "listbox",
53
+ tabIndex: 0,
54
+ "aria-label": "Inbox",
55
+ "aria-activedescendant": activeId,
56
+ onKeyDown,
57
+ ...props,
58
+ children
59
+ }
60
+ ) });
8
61
  }
9
62
  function InboxItem({
10
63
  subject,
@@ -14,13 +67,19 @@ function InboxItem({
14
67
  selected,
15
68
  children,
16
69
  className,
70
+ id,
17
71
  ...props
18
72
  }) {
73
+ const autoId = useId();
74
+ const itemId = id ?? autoId;
75
+ const { activeId } = useContext(InboxListCtx);
19
76
  return /* @__PURE__ */ jsxs(
20
77
  "div",
21
78
  {
22
79
  role: "option",
80
+ id: itemId,
23
81
  "aria-selected": selected ?? false,
82
+ "data-active": activeId === itemId ? "" : void 0,
24
83
  className: cx("uix-inbox__item", className),
25
84
  ...unread ? { "data-unread": "" } : {},
26
85
  ...props,
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { InputHTMLAttributes, ReactNode } from 'react';
2
+ import { InputHTMLAttributes, ReactNode, AriaAttributes } from 'react';
3
3
 
4
4
  interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {
5
5
  size?: 'sm' | 'md';
@@ -12,7 +12,18 @@ interface InputGroupProps {
12
12
  leadingIcon?: ReactNode;
13
13
  trailingIcon?: ReactNode;
14
14
  className?: string;
15
+ /** Injected by `<Field>` when it wraps the group; forwarded onto the child control. */
16
+ id?: string;
17
+ 'aria-describedby'?: string;
18
+ 'aria-invalid'?: AriaAttributes['aria-invalid'];
19
+ 'aria-required'?: AriaAttributes['aria-required'];
15
20
  }
16
- declare function InputGroup({ children, leadingIcon, trailingIcon, className }: InputGroupProps): react.JSX.Element;
21
+ /**
22
+ * Icon-adorned wrapper for a child `<Input>`. The control is passed as `children`, so
23
+ * `<Field>`'s injected `id`/`aria-*` land on this wrapper — they're forwarded onto the
24
+ * single child element (the child's own props win) so label/message association survives
25
+ * the extra div (UIX-A11Y-3).
26
+ */
27
+ declare function InputGroup({ children, leadingIcon, trailingIcon, className, id, 'aria-describedby': describedBy, 'aria-invalid': invalid, 'aria-required': required, }: InputGroupProps): react.JSX.Element;
17
28
 
18
29
  export { Input, InputGroup, type InputGroupProps, type InputProps };
@@ -1,5 +1,5 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
- import { forwardRef } from "react";
2
+ import { cloneElement, forwardRef, isValidElement } from "react";
3
3
  import { cx } from "../cx.js";
4
4
  const Input = forwardRef(
5
5
  ({ size = "md", invalid, valid, className, ...props }, ref) => /* @__PURE__ */ jsx(
@@ -14,7 +14,22 @@ const Input = forwardRef(
14
14
  )
15
15
  );
16
16
  Input.displayName = "Input";
17
- function InputGroup({ children, leadingIcon, trailingIcon, className }) {
17
+ function InputGroup({
18
+ children,
19
+ leadingIcon,
20
+ trailingIcon,
21
+ className,
22
+ id,
23
+ "aria-describedby": describedBy,
24
+ "aria-invalid": invalid,
25
+ "aria-required": required
26
+ }) {
27
+ const control = isValidElement(children) && (id ?? describedBy ?? invalid ?? required) != null ? cloneElement(children, {
28
+ id: children.props.id ?? id,
29
+ "aria-describedby": children.props["aria-describedby"] ?? describedBy,
30
+ "aria-invalid": children.props["aria-invalid"] ?? invalid,
31
+ "aria-required": children.props["aria-required"] ?? required
32
+ }) : children;
18
33
  return /* @__PURE__ */ jsxs(
19
34
  "div",
20
35
  {
@@ -25,9 +40,9 @@ function InputGroup({ children, leadingIcon, trailingIcon, className }) {
25
40
  className
26
41
  ),
27
42
  children: [
28
- leadingIcon && /* @__PURE__ */ jsx("span", { className: "uix-input-group__icon", children: leadingIcon }),
29
- children,
30
- trailingIcon && /* @__PURE__ */ jsx("span", { className: "uix-input-group__icon uix-input-group__icon--trailing", children: trailingIcon })
43
+ leadingIcon && /* @__PURE__ */ jsx("span", { className: "uix-input-group__icon", "aria-hidden": "true", children: leadingIcon }),
44
+ control,
45
+ trailingIcon && /* @__PURE__ */ jsx("span", { className: "uix-input-group__icon uix-input-group__icon--trailing", "aria-hidden": "true", children: trailingIcon })
31
46
  ]
32
47
  }
33
48
  );
@@ -2,20 +2,32 @@ import * as react from 'react';
2
2
  import { HTMLAttributes, ReactNode } from 'react';
3
3
 
4
4
  interface KanbanProps extends HTMLAttributes<HTMLDivElement> {
5
+ /**
6
+ * Polite live-region text for keyboard moves (UIX-A11Y-2). `onMove` implementations
7
+ * should set it, e.g. "Card X moved to Doing, position 2 of 4".
8
+ */
9
+ announcement?: string;
5
10
  children?: ReactNode;
6
11
  }
7
- declare function Kanban({ children, className, ...props }: KanbanProps): react.JSX.Element;
12
+ declare function Kanban({ announcement, children, className, ...props }: KanbanProps): react.JSX.Element;
8
13
  interface KanbanColumnProps extends Omit<HTMLAttributes<HTMLDivElement>, 'title'> {
9
14
  title?: ReactNode;
10
15
  count?: number;
11
16
  children?: ReactNode;
12
17
  }
13
18
  declare function KanbanColumn({ title, count, children, className, ...props }: KanbanColumnProps): react.JSX.Element;
19
+ type KanbanMoveDirection = 'up' | 'down' | 'left' | 'right';
14
20
  interface KanbanCardProps extends Omit<HTMLAttributes<HTMLDivElement>, 'title'> {
15
21
  title?: ReactNode;
16
22
  meta?: ReactNode;
23
+ /**
24
+ * Keyboard move contract (UIX-A11Y-2): when provided, Alt+Arrow on the focused card
25
+ * calls it — the same accelerator convention as NavFavourites row moves. Pair with
26
+ * the board-level `announcement` so SR users hear the result.
27
+ */
28
+ onMove?: (dir: KanbanMoveDirection) => void;
17
29
  children?: ReactNode;
18
30
  }
19
- declare function KanbanCard({ title, meta, children, className, ...props }: KanbanCardProps): react.JSX.Element;
31
+ declare function KanbanCard({ title, meta, children, className, onMove, onKeyDown, ...props }: KanbanCardProps): react.JSX.Element;
20
32
 
21
- export { Kanban, KanbanCard, type KanbanCardProps, KanbanColumn, type KanbanColumnProps, type KanbanProps };
33
+ export { Kanban, KanbanCard, type KanbanCardProps, KanbanColumn, type KanbanColumnProps, type KanbanMoveDirection, type KanbanProps };
@@ -1,23 +1,55 @@
1
+ "use client";
1
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useId } from "react";
2
4
  import { cx } from "../cx.js";
3
- function Kanban({ children, className, ...props }) {
4
- return /* @__PURE__ */ jsx("div", { className: cx("uix-kanban", className), ...props, children });
5
+ function Kanban({ announcement, children, className, ...props }) {
6
+ return /* @__PURE__ */ jsxs("div", { className: cx("uix-kanban", className), ...props, children: [
7
+ announcement != null && /* @__PURE__ */ jsx("div", { role: "status", className: "uix-visually-hidden", children: announcement }),
8
+ children
9
+ ] });
5
10
  }
6
11
  function KanbanColumn({ title, count, children, className, ...props }) {
12
+ const headId = useId();
13
+ const hasHead = title != null || count != null;
7
14
  return /* @__PURE__ */ jsxs("div", { className: cx("uix-kanban__col", className), ...props, children: [
8
- (title != null || count != null) && /* @__PURE__ */ jsxs("div", { className: "uix-kanban__head", children: [
15
+ hasHead && /* @__PURE__ */ jsxs("div", { className: "uix-kanban__head", id: headId, children: [
9
16
  title,
10
17
  count != null && /* @__PURE__ */ jsx("span", { className: "uix-kanban__count", children: count })
11
18
  ] }),
12
- /* @__PURE__ */ jsx("div", { className: "uix-kanban__body", children })
19
+ /* @__PURE__ */ jsx("div", { className: "uix-kanban__body", role: "list", "aria-labelledby": hasHead ? headId : void 0, children })
13
20
  ] });
14
21
  }
15
- function KanbanCard({ title, meta, children, className, ...props }) {
16
- return /* @__PURE__ */ jsxs("div", { className: cx("uix-kanban__card", className), ...props, children: [
17
- title && /* @__PURE__ */ jsx("div", { className: "uix-kanban__card-title", children: title }),
18
- meta && /* @__PURE__ */ jsx("div", { className: "uix-kanban__card-meta", children: meta }),
19
- children
20
- ] });
22
+ const MOVE_KEYS = {
23
+ ArrowUp: "up",
24
+ ArrowDown: "down",
25
+ ArrowLeft: "left",
26
+ ArrowRight: "right"
27
+ };
28
+ function KanbanCard({ title, meta, children, className, onMove, onKeyDown, ...props }) {
29
+ const handleKeyDown = (e) => {
30
+ onKeyDown?.(e);
31
+ if (!onMove || !e.altKey || e.defaultPrevented) return;
32
+ const dir = MOVE_KEYS[e.key];
33
+ if (dir) {
34
+ e.preventDefault();
35
+ onMove(dir);
36
+ }
37
+ };
38
+ return /* @__PURE__ */ jsxs(
39
+ "div",
40
+ {
41
+ role: "listitem",
42
+ tabIndex: 0,
43
+ className: cx("uix-kanban__card", className),
44
+ onKeyDown: handleKeyDown,
45
+ ...props,
46
+ children: [
47
+ title && /* @__PURE__ */ jsx("div", { className: "uix-kanban__card-title", children: title }),
48
+ meta && /* @__PURE__ */ jsx("div", { className: "uix-kanban__card-meta", children: meta }),
49
+ children
50
+ ]
51
+ }
52
+ );
21
53
  }
22
54
  export {
23
55
  Kanban,
@@ -8,8 +8,10 @@ interface MeterProps extends HTMLAttributes<HTMLDivElement> {
8
8
  value?: number;
9
9
  /** Threshold tone applied via `data-tone` on the fill element. `success` is the implicit default (no attribute). */
10
10
  tone?: MeterTone;
11
+ /** Accessible name — rendered as `aria-label` (an explicit `aria-label` prop wins). */
12
+ label?: string;
11
13
  }
12
14
  /** Horizontal utilization / threshold bar backed by `.uix-meter`. */
13
- declare function Meter({ value, tone, className, ...props }: MeterProps): react.JSX.Element;
15
+ declare function Meter({ value, tone, label, className, ...props }: MeterProps): react.JSX.Element;
14
16
 
15
17
  export { Meter, type MeterProps, type MeterTone };
@@ -1,6 +1,12 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import { cx } from "../cx.js";
3
- function Meter({ value = 0, tone, className, ...props }) {
3
+ const toneText = {
4
+ warning: "warning",
5
+ danger: "critical",
6
+ attention: "needs attention",
7
+ overdue: "overdue"
8
+ };
9
+ function Meter({ value = 0, tone, label, className, ...props }) {
4
10
  const pct = Math.max(0, Math.min(100, value));
5
11
  return /* @__PURE__ */ jsx(
6
12
  "div",
@@ -9,6 +15,8 @@ function Meter({ value = 0, tone, className, ...props }) {
9
15
  "aria-valuenow": pct,
10
16
  "aria-valuemin": 0,
11
17
  "aria-valuemax": 100,
18
+ "aria-valuetext": tone && tone !== "success" ? `${pct}%, ${toneText[tone]}` : void 0,
19
+ "aria-label": label,
12
20
  className: cx("uix-meter", className),
13
21
  ...props,
14
22
  children: /* @__PURE__ */ jsx(
@@ -1,17 +1,16 @@
1
1
  import * as react from 'react';
2
- import { ReactNode, CSSProperties } from 'react';
2
+ import { HTMLAttributes, ReactNode } from 'react';
3
3
 
4
- interface ModalProps {
4
+ interface ModalProps extends Omit<HTMLAttributes<HTMLDialogElement>, 'title'> {
5
5
  open: boolean;
6
6
  onClose?: () => void;
7
7
  title?: ReactNode;
8
8
  children?: ReactNode;
9
9
  footer?: ReactNode;
10
- className?: string;
11
- style?: CSSProperties;
10
+ /** Accessible name for the close button (default "Close dialog"). */
12
11
  closeLabel?: string;
13
12
  role?: 'dialog' | 'alertdialog';
14
13
  }
15
- declare function Modal({ open, onClose, title, children, footer, className, style, closeLabel, role }: ModalProps): react.JSX.Element;
14
+ declare function Modal({ open, onClose, title, children, footer, className, closeLabel, role, ...rest }: ModalProps): react.JSX.Element;
16
15
 
17
16
  export { Modal, type ModalProps };
@@ -4,12 +4,12 @@ import { useId } from "react";
4
4
  import { cx } from "../cx.js";
5
5
  import { useDialog } from "../hooks/useDialog.js";
6
6
  const CloseIcon = () => /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M4 4l8 8M12 4l-8 8" }) });
7
- function Modal({ open, onClose, title, children, footer, className, style, closeLabel = "Close dialog", role = "dialog" }) {
8
- const ref = useDialog(open);
7
+ function Modal({ open, onClose, title, children, footer, className, closeLabel = "Close dialog", role = "dialog", ...rest }) {
8
+ const ref = useDialog(open, onClose);
9
9
  const titleId = useId();
10
- return /* @__PURE__ */ jsxs("dialog", { ref, className: cx("uix-dialog", className), style, onClose, role, "aria-labelledby": title != null ? titleId : void 0, children: [
10
+ return /* @__PURE__ */ jsxs("dialog", { ref, className: cx("uix-dialog", className), role, "aria-labelledby": title != null ? titleId : void 0, ...rest, children: [
11
11
  (title != null || onClose) && /* @__PURE__ */ jsxs("div", { className: "uix-dialog__header", children: [
12
- title && /* @__PURE__ */ jsx("div", { id: titleId, className: "uix-dialog__title", children: title }),
12
+ title && /* @__PURE__ */ jsx("h2", { className: "uix-dialog__title", id: titleId, children: title }),
13
13
  onClose && /* @__PURE__ */ jsx("button", { type: "button", className: "uix-dialog__close", onClick: onClose, "aria-label": closeLabel, children: /* @__PURE__ */ jsx(CloseIcon, {}) })
14
14
  ] }),
15
15
  children != null && /* @__PURE__ */ jsx("div", { className: "uix-dialog__body", children }),
@@ -66,6 +66,7 @@ function NavFavourites({
66
66
  const regionId = useId();
67
67
  const headerRef = useRef(null);
68
68
  const regionRef = useRef(null);
69
+ const [announcement, setAnnouncement] = useState("");
69
70
  const focusAfterRef = useRef(null);
70
71
  const triggerRefs = useRef(/* @__PURE__ */ new Map());
71
72
  const order = useMemo(() => items.map((item) => item.id), [items]);
@@ -95,6 +96,8 @@ function NavFavourites({
95
96
  if (it === void 0) return;
96
97
  reordered.splice(next, 0, it);
97
98
  focusAfterRef.current = id;
99
+ const moved = items.find((i) => i.id === id);
100
+ if (moved) setAnnouncement(`${moved.label} moved to position ${next + 1} of ${order.length}`);
98
101
  onReorder(reordered);
99
102
  }, [onReorder, order]);
100
103
  const remove = useCallback((id) => {
@@ -108,6 +111,7 @@ function NavFavourites({
108
111
  else triggerRefs.current.delete(id);
109
112
  }, []);
110
113
  return /* @__PURE__ */ jsxs("div", { className: cx("uix-favourites", className), children: [
114
+ /* @__PURE__ */ jsx("div", { role: "status", className: "uix-visually-hidden", children: announcement }),
111
115
  /* @__PURE__ */ jsxs(
112
116
  "button",
113
117
  {
@@ -189,6 +193,12 @@ const FavouriteRow = memo(function FavouriteRow2({
189
193
  } else if (e.key === "ArrowUp") {
190
194
  e.preventDefault();
191
195
  menuItems[(currentIdx - 1 + menuItems.length) % menuItems.length]?.focus();
196
+ } else if (e.key === "Home") {
197
+ e.preventDefault();
198
+ menuItems[0]?.focus();
199
+ } else if (e.key === "End") {
200
+ e.preventDefault();
201
+ menuItems[menuItems.length - 1]?.focus();
192
202
  } else if (e.key === "Escape") {
193
203
  e.preventDefault();
194
204
  closeAndReturnFocus();
@@ -247,6 +257,7 @@ const FavouriteRow = memo(function FavouriteRow2({
247
257
  {
248
258
  id: menuId,
249
259
  role: "menu",
260
+ "aria-label": optionsTemplate.replace("{label}", item.label),
250
261
  onKeyDown: onMenuKeyDown,
251
262
  className: "uix-favourites__menu",
252
263
  children: actions.map((action, i) => /* @__PURE__ */ jsx("li", { role: "presentation", children: /* @__PURE__ */ jsx(
@@ -4,6 +4,8 @@ import { HTMLAttributes, ReactNode } from 'react';
4
4
  interface PageHeaderProps extends Omit<HTMLAttributes<HTMLElement>, 'title'> {
5
5
  eyebrow?: ReactNode;
6
6
  title: ReactNode;
7
+ /** Heading level for the title — drop to `h2` when the page already owns an `h1` (UIX-A11Y-4). */
8
+ as?: 'h1' | 'h2';
7
9
  /** Supporting copy under the title. */
8
10
  subtitle?: ReactNode;
9
11
  /** Right-aligned action area (buttons, menus). */
@@ -13,6 +15,6 @@ interface PageHeaderProps extends Omit<HTMLAttributes<HTMLElement>, 'title'> {
13
15
  * Page header — eyebrow / title / subtitle + actions row, over `.uix-page-header`.
14
16
  * The hairline + breathing room below come from the component CSS.
15
17
  */
16
- declare function PageHeader({ eyebrow, title, subtitle, actions, className, ...props }: PageHeaderProps): react.JSX.Element;
18
+ declare function PageHeader({ eyebrow, title, subtitle, actions, as: TitleTag, className, ...props }: PageHeaderProps): react.JSX.Element;
17
19
 
18
20
  export { PageHeader, type PageHeaderProps };
@@ -1,10 +1,10 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { cx } from "../cx.js";
3
- function PageHeader({ eyebrow, title, subtitle, actions, className, ...props }) {
3
+ function PageHeader({ eyebrow, title, subtitle, actions, as: TitleTag = "h1", className, ...props }) {
4
4
  return /* @__PURE__ */ jsxs("header", { className: cx("uix-page-header", className), ...props, children: [
5
5
  /* @__PURE__ */ jsxs("div", { className: "uix-page-header__titles", children: [
6
6
  eyebrow != null && /* @__PURE__ */ jsx("div", { className: "uix-page-header__eyebrow", children: eyebrow }),
7
- /* @__PURE__ */ jsx("h1", { className: "uix-page-header__title", children: title }),
7
+ /* @__PURE__ */ jsx(TitleTag, { className: "uix-page-header__title", children: title }),
8
8
  subtitle != null && /* @__PURE__ */ jsx("p", { className: "uix-page-header__subtitle", children: subtitle })
9
9
  ] }),
10
10
  actions != null && /* @__PURE__ */ jsx("div", { className: "uix-page-header__actions", children: actions })
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
- import { ReactNode } from 'react';
2
+ import { HTMLAttributes, ReactNode } from 'react';
3
3
 
4
- interface PeekProps {
4
+ interface PeekProps extends Omit<HTMLAttributes<HTMLDialogElement>, 'title'> {
5
5
  open: boolean;
6
6
  onClose?: () => void;
7
7
  title?: ReactNode;
@@ -10,8 +10,7 @@ interface PeekProps {
10
10
  onNavPrev?: () => void;
11
11
  onNavNext?: () => void;
12
12
  hint?: string;
13
- className?: string;
14
13
  }
15
- declare function Peek({ open, onClose, title, children, footer, onNavPrev, onNavNext, hint, className }: PeekProps): react.JSX.Element;
14
+ declare function Peek({ open, onClose, title, children, footer, onNavPrev, onNavNext, hint, className, onClick, ...rest }: PeekProps): react.JSX.Element;
16
15
 
17
16
  export { Peek, type PeekProps };
@@ -1,27 +1,30 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useId } from "react";
3
4
  import { cx } from "../cx.js";
4
5
  import { useDialog } from "../hooks/useDialog.js";
5
6
  const CloseIcon = () => /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M4 4l8 8M12 4l-8 8" }) });
6
7
  const ChevronUp = () => /* @__PURE__ */ jsx("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M2 8l4-4 4 4" }) });
7
8
  const ChevronDown = () => /* @__PURE__ */ jsx("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M2 4l4 4 4-4" }) });
8
- function Peek({ open, onClose, title, children, footer, onNavPrev, onNavNext, hint, className }) {
9
- const ref = useDialog(open);
9
+ function Peek({ open, onClose, title, children, footer, onNavPrev, onNavNext, hint, className, onClick, ...rest }) {
10
+ const ref = useDialog(open, onClose);
10
11
  const hasNav = onNavPrev != null || onNavNext != null;
12
+ const titleId = useId();
11
13
  const onBackdropClick = (e) => {
14
+ onClick?.(e);
12
15
  if (!onClose || e.target !== e.currentTarget) return;
13
16
  const r = e.currentTarget.getBoundingClientRect();
14
17
  if (r.width === 0) return;
15
18
  const outside = e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom;
16
19
  if (outside) onClose();
17
20
  };
18
- return /* @__PURE__ */ jsxs("dialog", { ref, className: cx("uix-peek", className), onClose, onClick: onBackdropClick, children: [
21
+ return /* @__PURE__ */ jsxs("dialog", { ref, className: cx("uix-peek", className), "aria-labelledby": title ? titleId : void 0, ...rest, onClick: onBackdropClick, children: [
19
22
  /* @__PURE__ */ jsxs("div", { className: "uix-peek__header", children: [
20
23
  hasNav && /* @__PURE__ */ jsxs("div", { className: "uix-peek__nav", children: [
21
- /* @__PURE__ */ jsx("button", { className: "uix-peek__navbtn", onClick: onNavPrev, disabled: !onNavPrev, "aria-label": "Previous record", children: /* @__PURE__ */ jsx(ChevronUp, {}) }),
22
- /* @__PURE__ */ jsx("button", { className: "uix-peek__navbtn", onClick: onNavNext, disabled: !onNavNext, "aria-label": "Next record", children: /* @__PURE__ */ jsx(ChevronDown, {}) })
24
+ /* @__PURE__ */ jsx("button", { className: "uix-peek__navbtn", onClick: () => onNavPrev?.(), "aria-disabled": onNavPrev ? void 0 : true, "aria-label": "Previous record", children: /* @__PURE__ */ jsx(ChevronUp, {}) }),
25
+ /* @__PURE__ */ jsx("button", { className: "uix-peek__navbtn", onClick: () => onNavNext?.(), "aria-disabled": onNavNext ? void 0 : true, "aria-label": "Next record", children: /* @__PURE__ */ jsx(ChevronDown, {}) })
23
26
  ] }),
24
- title && /* @__PURE__ */ jsx("div", { className: "uix-peek__title", children: title }),
27
+ title && /* @__PURE__ */ jsx("h2", { className: "uix-peek__title", id: titleId, children: title }),
25
28
  hint && /* @__PURE__ */ jsx("span", { className: "uix-peek__hint", children: hint }),
26
29
  onClose && /* @__PURE__ */ jsx("button", { className: "uix-peek__close", onClick: onClose, "aria-label": "Close preview", children: /* @__PURE__ */ jsx(CloseIcon, {}) })
27
30
  ] }),
@@ -8,7 +8,9 @@ interface ProgressProps extends HTMLAttributes<HTMLDivElement> {
8
8
  max?: number;
9
9
  /** Continuous slide animation when the amount of work is unknown. */
10
10
  indeterminate?: boolean;
11
+ /** Accessible name — rendered as `aria-label` (an explicit `aria-label` prop wins). */
12
+ label?: string;
11
13
  }
12
- declare function Progress({ value, max, indeterminate, className, ...props }: ProgressProps): react.JSX.Element;
14
+ declare function Progress({ value, max, indeterminate, label, className, ...props }: ProgressProps): react.JSX.Element;
13
15
 
14
16
  export { Progress, type ProgressProps };
@@ -1,14 +1,16 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import { cx } from "../cx.js";
3
- function Progress({ value = 0, max = 100, indeterminate, className, ...props }) {
4
- const pct = Math.max(0, Math.min(100, value / (max || 100) * 100));
3
+ function Progress({ value = 0, max = 100, indeterminate, label, className, ...props }) {
4
+ const clamped = Math.max(0, Math.min(max || 100, value));
5
+ const pct = clamped / (max || 100) * 100;
5
6
  return /* @__PURE__ */ jsx(
6
7
  "div",
7
8
  {
8
9
  role: "progressbar",
9
10
  "aria-valuemin": 0,
10
11
  "aria-valuemax": max,
11
- "aria-valuenow": indeterminate ? void 0 : value,
12
+ "aria-valuenow": indeterminate ? void 0 : clamped,
13
+ "aria-label": label,
12
14
  className: cx("uix-progress", indeterminate && "uix-progress--indeterminate", className),
13
15
  ...props,
14
16
  children: /* @__PURE__ */ jsx("div", { className: "uix-progress__bar", style: indeterminate ? void 0 : { width: `${pct}%` } })
@@ -8,7 +8,9 @@ declare const Radio: react.ForwardRefExoticComponent<RadioProps & react.RefAttri
8
8
  interface RadioGroupProps {
9
9
  children: ReactNode;
10
10
  className?: string;
11
+ /** Group name; when set the group renders as fieldset/legend so it's announced with each radio. */
12
+ label?: ReactNode;
11
13
  }
12
- declare function RadioGroup({ children, className }: RadioGroupProps): react.JSX.Element;
14
+ declare function RadioGroup({ children, className, label }: RadioGroupProps): react.JSX.Element;
13
15
 
14
16
  export { Radio, RadioGroup, type RadioGroupProps, type RadioProps };
@@ -9,8 +9,12 @@ const Radio = forwardRef(
9
9
  ] })
10
10
  );
11
11
  Radio.displayName = "Radio";
12
- function RadioGroup({ children, className }) {
13
- return /* @__PURE__ */ jsx("div", { className: cx("uix-radio-group", className), children });
12
+ function RadioGroup({ children, className, label }) {
13
+ if (label == null) return /* @__PURE__ */ jsx("div", { className: cx("uix-radio-group", className), children });
14
+ return /* @__PURE__ */ jsxs("fieldset", { className: cx("uix-radio-group", className), children: [
15
+ /* @__PURE__ */ jsx("legend", { className: "uix-radio-group__legend", children: label }),
16
+ children
17
+ ] });
14
18
  }
15
19
  export {
16
20
  Radio,