@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
package/README.md CHANGED
@@ -61,4 +61,33 @@ traversal, three-way JSON diffing, metric parsing/stepping, color conversion and
61
61
  contrast checks, plus brand-profile apply/restore helpers. No graph or date
62
62
  runtime dependency is required.
63
63
 
64
+ ### RelationshipGraph layouts
65
+
66
+ `RelationshipGraph` has two layouts:
67
+ - `radial` (the default): a small neighbourhood ringed around the selection.
68
+ - `layered` (ADR-0003): reads left to right by distance from `rootId`.
69
+
70
+ Layered mode draws shared nodes once, marks cycles, collapses leaf fan-outs into clusters and labels edges. It adds structural keyboard traversal (←/→ along edges, ↑/↓ in a column, Home, End), pan and Ctrl/⌘-wheel zoom.
71
+
72
+ ```tsx
73
+ <RelationshipGraph
74
+ layout="layered"
75
+ rootId={root.id}
76
+ nodes={nodes} // { id, label, type?, description?, depth? }
77
+ edges={edges} // { id, source, target, type?, label?, arrow?, emphasis? }
78
+ labels={translatedLabels} // Partial<RelationshipGraphLabels>
79
+ renderNode={(node) => <MyNodeBody node={node} />}
80
+ nodeAriaLabel={(node) => `${node.label}, ${riskText(node)}`}
81
+ onSelect={openPeek}
82
+ height="clamp(360px, 60vh, 640px)"
83
+ />
84
+ ```
85
+
86
+ Rules the component cannot enforce for you:
87
+
88
+ - **Whatever `renderNode` / `renderCluster` shows must also be in the accessible name.** Pass `nodeAriaLabel` whenever the node body carries meaning beyond `label`, `type` and `description`.
89
+ - **Keep the equivalent list.** `showList={false}` is allowed only when the same labelled region renders a complete equivalent: every node, its relations and its state.
90
+ - **Dimming is not a signal.** `dimmedNodeIds` must be accompanied by text that says what is dimmed and why.
91
+ - **Localise everything.** Every visible and announced string comes from `labels`; the English defaults live in `DEFAULT_RELATIONSHIP_GRAPH_LABELS`.
92
+
64
93
  Ships **ESM + CJS + types**, with per-file `"use client"` so it's safe under React Server Components. Part of the **[UIx v2 styleguide](https://github.com/COx-ORG-H/UIx)**.
@@ -60,6 +60,12 @@ function readToken(name) {
60
60
  function uixChartPalette() {
61
61
  return [1, 2, 3, 4, 5, 6, 7, 8].map((i) => readToken(`--uix-chart-${i}`));
62
62
  }
63
+ function motionSafe(option) {
64
+ if (typeof matchMedia !== "undefined" && matchMedia("(prefers-reduced-motion: reduce)").matches) {
65
+ return { ...option, animation: false };
66
+ }
67
+ return option;
68
+ }
63
69
  function ChartCore({
64
70
  engine,
65
71
  option,
@@ -85,16 +91,22 @@ function ChartCore({
85
91
  const onReadyRef = (0, import_react.useRef)(onReady);
86
92
  const tableId = (0, import_react.useId)();
87
93
  const hasEmpty = empty != null;
94
+ const hasTextAlternative = Boolean(ariaLabel || title || tableData?.length);
95
+ const hasTextAlternativeRef = (0, import_react.useRef)(hasTextAlternative);
88
96
  latestOptionRef.current = option;
89
97
  onReadyRef.current = onReady;
98
+ hasTextAlternativeRef.current = hasTextAlternative;
90
99
  (0, import_react.useEffect)(() => {
91
100
  const element = containerRef.current;
92
101
  if (!element) return;
93
102
  const chart = engine.init(element, null, { renderer: "svg" });
94
103
  chartRef.current = chart;
95
- chart.setOption(latestOptionRef.current);
104
+ chart.setOption(motionSafe(latestOptionRef.current));
96
105
  appliedOptionRef.current = latestOptionRef.current;
97
106
  onReadyRef.current?.(chart);
107
+ if (!hasTextAlternativeRef.current) {
108
+ console.warn("[uix] Chart has no text alternative \u2014 provide ariaLabel, title, or tableData.");
109
+ }
98
110
  let frame = 0;
99
111
  let width = element.clientWidth;
100
112
  let height2 = element.clientHeight;
@@ -122,7 +134,7 @@ function ChartCore({
122
134
  (0, import_react.useEffect)(() => {
123
135
  const chart = chartRef.current;
124
136
  if (!chart || appliedOptionRef.current === option) return;
125
- chart.setOption(option, { notMerge: false });
137
+ chart.setOption(motionSafe(option), { notMerge: false });
126
138
  appliedOptionRef.current = option;
127
139
  }, [option]);
128
140
  const heightValue = typeof height === "number" ? `${height}px` : height;
@@ -147,7 +159,7 @@ function ChartCore({
147
159
  "aria-describedby": hasTable ? tableId : void 0
148
160
  }
149
161
  ) }),
150
- hasTable ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("table", { id: tableId, className: "sr-only", "aria-label": `${effectiveAriaLabel} \u2014 data table`, children: [
162
+ hasTable ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("table", { id: tableId, className: "uix-visually-hidden", "aria-label": `${effectiveAriaLabel} \u2014 data table`, children: [
151
163
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tr", { children: tableHeaders.map((label) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { scope: "col", children: label }, label)) }) }),
152
164
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tbody", { children: tableData.map((row, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tr", { children: tableHeaders.map((label) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { children: row[label] }, label)) }, index)) })
153
165
  ] }) : null,
package/dist/chart.cjs CHANGED
@@ -58,6 +58,12 @@ function readToken(name) {
58
58
  function uixChartPalette() {
59
59
  return [1, 2, 3, 4, 5, 6, 7, 8].map((i) => readToken(`--uix-chart-${i}`));
60
60
  }
61
+ function motionSafe(option) {
62
+ if (typeof matchMedia !== "undefined" && matchMedia("(prefers-reduced-motion: reduce)").matches) {
63
+ return { ...option, animation: false };
64
+ }
65
+ return option;
66
+ }
61
67
  function ChartCore({
62
68
  engine,
63
69
  option,
@@ -83,16 +89,22 @@ function ChartCore({
83
89
  const onReadyRef = (0, import_react.useRef)(onReady);
84
90
  const tableId = (0, import_react.useId)();
85
91
  const hasEmpty = empty != null;
92
+ const hasTextAlternative = Boolean(ariaLabel || title || tableData?.length);
93
+ const hasTextAlternativeRef = (0, import_react.useRef)(hasTextAlternative);
86
94
  latestOptionRef.current = option;
87
95
  onReadyRef.current = onReady;
96
+ hasTextAlternativeRef.current = hasTextAlternative;
88
97
  (0, import_react.useEffect)(() => {
89
98
  const element = containerRef.current;
90
99
  if (!element) return;
91
100
  const chart = engine.init(element, null, { renderer: "svg" });
92
101
  chartRef.current = chart;
93
- chart.setOption(latestOptionRef.current);
102
+ chart.setOption(motionSafe(latestOptionRef.current));
94
103
  appliedOptionRef.current = latestOptionRef.current;
95
104
  onReadyRef.current?.(chart);
105
+ if (!hasTextAlternativeRef.current) {
106
+ console.warn("[uix] Chart has no text alternative \u2014 provide ariaLabel, title, or tableData.");
107
+ }
96
108
  let frame = 0;
97
109
  let width = element.clientWidth;
98
110
  let height2 = element.clientHeight;
@@ -120,7 +132,7 @@ function ChartCore({
120
132
  (0, import_react.useEffect)(() => {
121
133
  const chart = chartRef.current;
122
134
  if (!chart || appliedOptionRef.current === option) return;
123
- chart.setOption(option, { notMerge: false });
135
+ chart.setOption(motionSafe(option), { notMerge: false });
124
136
  appliedOptionRef.current = option;
125
137
  }, [option]);
126
138
  const heightValue = typeof height === "number" ? `${height}px` : height;
@@ -145,7 +157,7 @@ function ChartCore({
145
157
  "aria-describedby": hasTable ? tableId : void 0
146
158
  }
147
159
  ) }),
148
- hasTable ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("table", { id: tableId, className: "sr-only", "aria-label": `${effectiveAriaLabel} \u2014 data table`, children: [
160
+ hasTable ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("table", { id: tableId, className: "uix-visually-hidden", "aria-label": `${effectiveAriaLabel} \u2014 data table`, children: [
149
161
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tr", { children: tableHeaders.map((label) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { scope: "col", children: label }, label)) }) }),
150
162
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tbody", { children: tableData.map((row, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tr", { children: tableHeaders.map((label) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { children: row[label] }, label)) }, index)) })
151
163
  ] }) : null,
@@ -1,13 +1,21 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { cx } from "../cx.js";
3
3
  function Alert({ tone, title, icon, children, className, ...props }) {
4
- return /* @__PURE__ */ jsxs("div", { className: cx("uix-alert", tone && tone !== "neutral" && `uix-alert--${tone}`, className), ...props, children: [
5
- icon && /* @__PURE__ */ jsx("div", { className: "uix-alert__icon", children: icon }),
6
- /* @__PURE__ */ jsxs("div", { children: [
7
- title && /* @__PURE__ */ jsx("div", { className: "uix-alert__title", children: title }),
8
- children && /* @__PURE__ */ jsx("div", { className: "uix-alert__body", children })
9
- ] })
10
- ] });
4
+ return /* @__PURE__ */ jsxs(
5
+ "div",
6
+ {
7
+ className: cx("uix-alert", tone && tone !== "neutral" && `uix-alert--${tone}`, className),
8
+ role: tone === "danger" || tone === "warning" ? "alert" : "status",
9
+ ...props,
10
+ children: [
11
+ icon && /* @__PURE__ */ jsx("div", { className: "uix-alert__icon", children: icon }),
12
+ /* @__PURE__ */ jsxs("div", { children: [
13
+ title && /* @__PURE__ */ jsx("div", { className: "uix-alert__title", children: title }),
14
+ children && /* @__PURE__ */ jsx("div", { className: "uix-alert__body", children })
15
+ ] })
16
+ ]
17
+ }
18
+ );
11
19
  }
12
20
  export {
13
21
  Alert
@@ -17,10 +17,12 @@ interface AppShellProps extends HTMLAttributes<HTMLDivElement> {
17
17
  onExitFocus?: () => void;
18
18
  /** Drop the content gutter so a genuinely wide table runs edge-to-edge. */
19
19
  mainBleed?: boolean;
20
+ /** id given to `<main>` and targeted by the skip link. Override on collision (UIX-A11Y-2). */
21
+ mainId?: string;
20
22
  sidebar?: ReactNode;
21
23
  topbar?: ReactNode;
22
24
  children?: ReactNode;
23
25
  }
24
- declare function AppShell({ nav, collapsed, focus, onExitFocus, mainBleed, sidebar, topbar, children, className, ...props }: AppShellProps): react.JSX.Element;
26
+ declare function AppShell({ nav, collapsed, focus, onExitFocus, mainBleed, mainId, sidebar, topbar, children, className, ...props }: AppShellProps): react.JSX.Element;
25
27
 
26
28
  export { AppShell, type AppShellProps, type ShellNav };
@@ -8,6 +8,7 @@ function AppShell({
8
8
  focus,
9
9
  onExitFocus,
10
10
  mainBleed,
11
+ mainId = "uix-main",
11
12
  sidebar,
12
13
  topbar,
13
14
  children,
@@ -32,9 +33,10 @@ function AppShell({
32
33
  "data-focus": focus || void 0,
33
34
  ...props,
34
35
  children: [
36
+ /* @__PURE__ */ jsx("a", { className: "uix-skiplink", href: `#${mainId}`, children: "Skip to content" }),
35
37
  sidebar && /* @__PURE__ */ jsx("div", { className: "uix-shell__sidebar", children: sidebar }),
36
38
  topbar && /* @__PURE__ */ jsx("div", { className: "uix-shell__topbar", children: topbar }),
37
- /* @__PURE__ */ jsx("main", { className: cx("uix-shell__main", mainBleed && "uix-shell__main--bleed"), children }),
39
+ /* @__PURE__ */ jsx("main", { id: mainId, tabIndex: -1, className: cx("uix-shell__main", mainBleed && "uix-shell__main--bleed"), children }),
38
40
  focus && onExitFocus && /* @__PURE__ */ jsx("div", { className: "uix-shell__focus-exit", children: /* @__PURE__ */ jsx("button", { type: "button", className: "uix-btn uix-btn--secondary uix-btn--sm", onClick: onExitFocus, children: "Esc \xB7 Exit focus" }) })
39
41
  ]
40
42
  }
@@ -3,7 +3,8 @@ import { cx } from "../cx.js";
3
3
  function Avatar({ size = "md", src, alt, children, status, className, ...props }) {
4
4
  return /* @__PURE__ */ jsxs("span", { className: cx("uix-avatar", size !== "md" && `uix-avatar--${size}`, className), ...props, children: [
5
5
  src ? /* @__PURE__ */ jsx("img", { src, alt: alt ?? "" }) : children,
6
- status && /* @__PURE__ */ jsx("span", { className: "uix-avatar__status" })
6
+ status && /* @__PURE__ */ jsx("span", { className: "uix-avatar__status", "aria-hidden": "true" }),
7
+ status && /* @__PURE__ */ jsx("span", { className: "uix-visually-hidden", children: "online" })
7
8
  ] });
8
9
  }
9
10
  function AvatarGroup({ children, className, ...props }) {
@@ -1,24 +1,40 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import { forwardRef } from "react";
3
3
  import { cx } from "../cx.js";
4
+ let warnedIconButton = false;
4
5
  const Button = forwardRef(
5
- ({ variant = "secondary", size = "md", icon = false, loading = false, className, children, disabled, ...props }, ref) => /* @__PURE__ */ jsx(
6
- "button",
7
- {
8
- ref,
9
- className: cx(
10
- "uix-btn",
11
- `uix-btn--${variant}`,
12
- size !== "md" && `uix-btn--${size}`,
13
- icon && "uix-btn--icon",
14
- className
15
- ),
16
- disabled: disabled || loading,
17
- "data-loading": loading || void 0,
18
- ...props,
19
- children
6
+ ({ variant = "secondary", size = "md", icon = false, loading = false, className, children, disabled, onClick, ...props }, ref) => {
7
+ if (!warnedIconButton && icon && children == null && !props["aria-label"] && !props["aria-labelledby"] && typeof process !== "undefined" && process.env.NODE_ENV !== "production") {
8
+ warnedIconButton = true;
9
+ console.warn("uix: icon-only <Button> has no accessible name \u2014 pass aria-label or aria-labelledby.");
20
10
  }
21
- )
11
+ return /* @__PURE__ */ jsx(
12
+ "button",
13
+ {
14
+ ref,
15
+ className: cx(
16
+ "uix-btn",
17
+ `uix-btn--${variant}`,
18
+ size !== "md" && `uix-btn--${size}`,
19
+ icon && "uix-btn--icon",
20
+ className
21
+ ),
22
+ disabled,
23
+ "aria-disabled": loading || void 0,
24
+ "aria-busy": loading || void 0,
25
+ "data-loading": loading || void 0,
26
+ onClick: loading || onClick ? (e) => {
27
+ if (loading) {
28
+ e.preventDefault();
29
+ return;
30
+ }
31
+ onClick?.(e);
32
+ } : void 0,
33
+ ...props,
34
+ children
35
+ }
36
+ );
37
+ }
22
38
  );
23
39
  Button.displayName = "Button";
24
40
  function ButtonGroup({ children, className }) {
@@ -3,12 +3,14 @@ import { HTMLAttributes, ReactNode, AnchorHTMLAttributes } from 'react';
3
3
 
4
4
  interface CardProps extends Omit<HTMLAttributes<HTMLDivElement>, 'title'> {
5
5
  title?: ReactNode;
6
+ /** Element for the title — pick a real heading to slot the card into the page outline (UIX-A11Y-4). Default `div` keeps legacy rendering. */
7
+ titleAs?: 'h2' | 'h3' | 'h4' | 'div';
6
8
  subtitle?: ReactNode;
7
9
  headerAction?: ReactNode;
8
10
  footer?: ReactNode;
9
11
  children?: ReactNode;
10
12
  }
11
- declare function Card({ title, subtitle, headerAction, footer, children, className, ...props }: CardProps): react.JSX.Element;
13
+ declare function Card({ title, titleAs: TitleTag, subtitle, headerAction, footer, children, className, ...props }: CardProps): react.JSX.Element;
12
14
  interface CardLinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'title'> {
13
15
  title?: ReactNode;
14
16
  subtitle?: ReactNode;
@@ -1,11 +1,11 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { cx } from "../cx.js";
3
- function Card({ title, subtitle, headerAction, footer, children, className, ...props }) {
3
+ function Card({ title, titleAs: TitleTag = "div", subtitle, headerAction, footer, children, className, ...props }) {
4
4
  const hasHeader = title != null || subtitle != null || headerAction != null;
5
5
  return /* @__PURE__ */ jsxs("div", { className: cx("uix-card", className), ...props, children: [
6
6
  hasHeader && /* @__PURE__ */ jsxs("div", { className: "uix-card__header", children: [
7
7
  /* @__PURE__ */ jsxs("div", { style: { flex: 1 }, children: [
8
- title && /* @__PURE__ */ jsx("div", { className: "uix-card__title", children: title }),
8
+ title && /* @__PURE__ */ jsx(TitleTag, { className: "uix-card__title", children: title }),
9
9
  subtitle && /* @__PURE__ */ jsx("div", { className: "uix-card__subtitle", children: subtitle })
10
10
  ] }),
11
11
  headerAction
@@ -9,6 +9,12 @@ function readToken(name) {
9
9
  function uixChartPalette() {
10
10
  return [1, 2, 3, 4, 5, 6, 7, 8].map((i) => readToken(`--uix-chart-${i}`));
11
11
  }
12
+ function motionSafe(option) {
13
+ if (typeof matchMedia !== "undefined" && matchMedia("(prefers-reduced-motion: reduce)").matches) {
14
+ return { ...option, animation: false };
15
+ }
16
+ return option;
17
+ }
12
18
  function ChartCore({
13
19
  engine,
14
20
  option,
@@ -34,16 +40,22 @@ function ChartCore({
34
40
  const onReadyRef = useRef(onReady);
35
41
  const tableId = useId();
36
42
  const hasEmpty = empty != null;
43
+ const hasTextAlternative = Boolean(ariaLabel || title || tableData?.length);
44
+ const hasTextAlternativeRef = useRef(hasTextAlternative);
37
45
  latestOptionRef.current = option;
38
46
  onReadyRef.current = onReady;
47
+ hasTextAlternativeRef.current = hasTextAlternative;
39
48
  useEffect(() => {
40
49
  const element = containerRef.current;
41
50
  if (!element) return;
42
51
  const chart = engine.init(element, null, { renderer: "svg" });
43
52
  chartRef.current = chart;
44
- chart.setOption(latestOptionRef.current);
53
+ chart.setOption(motionSafe(latestOptionRef.current));
45
54
  appliedOptionRef.current = latestOptionRef.current;
46
55
  onReadyRef.current?.(chart);
56
+ if (!hasTextAlternativeRef.current) {
57
+ console.warn("[uix] Chart has no text alternative \u2014 provide ariaLabel, title, or tableData.");
58
+ }
47
59
  let frame = 0;
48
60
  let width = element.clientWidth;
49
61
  let height2 = element.clientHeight;
@@ -71,7 +83,7 @@ function ChartCore({
71
83
  useEffect(() => {
72
84
  const chart = chartRef.current;
73
85
  if (!chart || appliedOptionRef.current === option) return;
74
- chart.setOption(option, { notMerge: false });
86
+ chart.setOption(motionSafe(option), { notMerge: false });
75
87
  appliedOptionRef.current = option;
76
88
  }, [option]);
77
89
  const heightValue = typeof height === "number" ? `${height}px` : height;
@@ -96,7 +108,7 @@ function ChartCore({
96
108
  "aria-describedby": hasTable ? tableId : void 0
97
109
  }
98
110
  ) }),
99
- hasTable ? /* @__PURE__ */ jsxs("table", { id: tableId, className: "sr-only", "aria-label": `${effectiveAriaLabel} \u2014 data table`, children: [
111
+ hasTable ? /* @__PURE__ */ jsxs("table", { id: tableId, className: "uix-visually-hidden", "aria-label": `${effectiveAriaLabel} \u2014 data table`, children: [
100
112
  /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children: tableHeaders.map((label) => /* @__PURE__ */ jsx("th", { scope: "col", children: label }, label)) }) }),
101
113
  /* @__PURE__ */ jsx("tbody", { children: tableData.map((row, index) => /* @__PURE__ */ jsx("tr", { children: tableHeaders.map((label) => /* @__PURE__ */ jsx("td", { children: row[label] }, label)) }, index)) })
102
114
  ] }) : null,
@@ -8,7 +8,9 @@ interface CommandPaletteProps extends HTMLAttributes<HTMLDivElement> {
8
8
  }
9
9
  /**
10
10
  * ⌘K command palette surface over `.uix-cmdk`. Presentational: mount it inside a
11
- * Modal/Popover and wire search + keyboard selection in the consumer.
11
+ * Modal/Popover and wire search + keyboard selection in the consumer. The input is
12
+ * a combobox over the listbox of `CommandItem`s; spreads override every default, so
13
+ * consumers already passing their own roles/labels keep working.
12
14
  */
13
15
  declare function CommandPalette({ inputProps, children, className, ...props }: CommandPaletteProps): react.JSX.Element;
14
16
  interface CommandGroupProps {
@@ -25,6 +27,6 @@ interface CommandItemProps extends HTMLAttributes<HTMLDivElement> {
25
27
  children?: ReactNode;
26
28
  }
27
29
  /** A single command row over `.uix-cmdk__item`. */
28
- declare function CommandItem({ icon, shortcut, active, children, className, ...props }: CommandItemProps): react.JSX.Element;
30
+ declare function CommandItem({ icon, shortcut, active, children, className, id, ...props }: CommandItemProps): react.JSX.Element;
29
31
 
30
32
  export { CommandGroup, type CommandGroupProps, CommandItem, type CommandItemProps, CommandPalette, type CommandPaletteProps };
@@ -1,23 +1,76 @@
1
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { createContext, useCallback, useContext, useEffect, useId, useMemo, useState } from "react";
2
4
  import { cx } from "../cx.js";
5
+ const CmdkContext = createContext(null);
3
6
  function CommandPalette({ inputProps, children, className, ...props }) {
4
- return /* @__PURE__ */ jsxs("div", { className: cx("uix-cmdk", className), ...props, children: [
5
- /* @__PURE__ */ jsx("input", { className: "uix-cmdk__input", ...inputProps }),
6
- /* @__PURE__ */ jsx("div", { className: "uix-cmdk__list", children })
7
- ] });
7
+ const listId = useId();
8
+ const [activeId, setActiveId] = useState(null);
9
+ const [count, setCount] = useState(0);
10
+ const [announced, setAnnounced] = useState("");
11
+ const setActive = useCallback((id, active) => {
12
+ setActiveId((cur) => active ? id : cur === id ? null : cur);
13
+ }, []);
14
+ const registerItem = useCallback(() => {
15
+ setCount((c) => c + 1);
16
+ return () => setCount((c) => c - 1);
17
+ }, []);
18
+ useEffect(() => {
19
+ const t = window.setTimeout(() => setAnnounced(`${count} result${count === 1 ? "" : "s"}`), 150);
20
+ return () => clearTimeout(t);
21
+ }, [count]);
22
+ const ctx = useMemo(() => ({ listId, setActive, registerItem }), [listId, setActive, registerItem]);
23
+ return /* @__PURE__ */ jsx(CmdkContext.Provider, { value: ctx, children: /* @__PURE__ */ jsxs("div", { className: cx("uix-cmdk", className), ...props, children: [
24
+ /* @__PURE__ */ jsx(
25
+ "input",
26
+ {
27
+ className: "uix-cmdk__input",
28
+ role: "combobox",
29
+ "aria-expanded": "true",
30
+ "aria-autocomplete": "list",
31
+ "aria-controls": listId,
32
+ "aria-activedescendant": activeId ?? void 0,
33
+ "aria-label": "Command palette",
34
+ ...inputProps
35
+ }
36
+ ),
37
+ /* @__PURE__ */ jsx("div", { className: "uix-cmdk__list", role: "listbox", id: listId, children }),
38
+ /* @__PURE__ */ jsx("span", { className: "uix-visually-hidden", role: "status", children: announced })
39
+ ] }) });
8
40
  }
9
41
  function CommandGroup({ label, children }) {
10
- return /* @__PURE__ */ jsxs(Fragment, { children: [
11
- label != null && /* @__PURE__ */ jsx("div", { className: "uix-cmdk__group", children: label }),
42
+ const labelId = useId();
43
+ return /* @__PURE__ */ jsxs("div", { role: "group", "aria-labelledby": label != null ? labelId : void 0, children: [
44
+ label != null && /* @__PURE__ */ jsx("div", { className: "uix-cmdk__group", id: labelId, children: label }),
12
45
  children
13
46
  ] });
14
47
  }
15
- function CommandItem({ icon, shortcut, active, children, className, ...props }) {
16
- return /* @__PURE__ */ jsxs("div", { className: cx("uix-cmdk__item", className), "data-active": active || void 0, ...props, children: [
17
- icon,
18
- /* @__PURE__ */ jsx("span", { children }),
19
- shortcut
20
- ] });
48
+ function CommandItem({ icon, shortcut, active, children, className, id, ...props }) {
49
+ const ctx = useContext(CmdkContext);
50
+ const autoId = useId();
51
+ const itemId = id ?? autoId;
52
+ useEffect(() => ctx?.registerItem(), [ctx]);
53
+ useEffect(() => {
54
+ if (!ctx || !active) return;
55
+ ctx.setActive(itemId, true);
56
+ return () => ctx.setActive(itemId, false);
57
+ }, [ctx, itemId, active]);
58
+ return /* @__PURE__ */ jsxs(
59
+ "div",
60
+ {
61
+ className: cx("uix-cmdk__item", className),
62
+ "data-active": active || void 0,
63
+ id: itemId,
64
+ role: "option",
65
+ "aria-selected": !!active,
66
+ ...props,
67
+ children: [
68
+ icon,
69
+ /* @__PURE__ */ jsx("span", { children }),
70
+ shortcut
71
+ ]
72
+ }
73
+ );
21
74
  }
22
75
  export {
23
76
  CommandGroup,
@@ -4,7 +4,11 @@ import { HTMLAttributes, ReactNode } from 'react';
4
4
  interface CommentsProps extends HTMLAttributes<HTMLDivElement> {
5
5
  children?: ReactNode;
6
6
  }
7
- /** Comment thread container over `.uix-comments`. */
7
+ /**
8
+ * Comment thread container over `.uix-comments`. role="log" is an implicit polite
9
+ * live region: comments appended after initial render are announced, the initial
10
+ * list is not (UIX-A11Y-3). Pass `role` to override.
11
+ */
8
12
  declare function Comments({ children, className, ...props }: CommentsProps): react.JSX.Element;
9
13
  interface CommentProps extends HTMLAttributes<HTMLDivElement> {
10
14
  /** Usually an `<Avatar />` (the grid reserves the leading column). */
@@ -1,7 +1,7 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { cx } from "../cx.js";
3
3
  function Comments({ children, className, ...props }) {
4
- return /* @__PURE__ */ jsx("div", { className: cx("uix-comments", className), ...props, children });
4
+ return /* @__PURE__ */ jsx("div", { role: "log", className: cx("uix-comments", className), ...props, children });
5
5
  }
6
6
  function Comment({ avatar, author, meta, children, className, ...props }) {
7
7
  return /* @__PURE__ */ jsxs("div", { className: cx("uix-comment", className), ...props, children: [
@@ -3,6 +3,8 @@ import { HTMLAttributes, ReactNode } from 'react';
3
3
 
4
4
  interface ComposerProps extends HTMLAttributes<HTMLDivElement> {
5
5
  children?: ReactNode;
6
+ /** Accessible name for the child `<textarea>`; applied as its `aria-label`. */
7
+ label?: string;
6
8
  }
7
9
  /**
8
10
  * Composer surface over `.uix-composer` — a single box that owns the
@@ -10,8 +12,13 @@ interface ComposerProps extends HTMLAttributes<HTMLDivElement> {
10
12
  * `<ComposerBar>` inside; the descendant `textarea` picks up the composer's
11
13
  * padding/typography automatically, so the input never reads as a second
12
14
  * nested box.
15
+ *
16
+ * A placeholder is not an accessible name, so the composer labels a bare child
17
+ * `<textarea>` with `label` (default "Add a comment"); a textarea that brings its
18
+ * own `aria-label`/`aria-labelledby` — or an `id` (likely tied to an external
19
+ * `<label htmlFor>`) — is left untouched (UIX-A11Y-3).
13
20
  */
14
- declare function Composer({ children, className, ...props }: ComposerProps): react.JSX.Element;
21
+ declare function Composer({ children, className, label, ...props }: ComposerProps): react.JSX.Element;
15
22
  interface ComposerBarProps extends HTMLAttributes<HTMLDivElement> {
16
23
  children?: ReactNode;
17
24
  }
@@ -1,7 +1,12 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
+ import { Children, cloneElement, isValidElement } from "react";
2
3
  import { cx } from "../cx.js";
3
- function Composer({ children, className, ...props }) {
4
- return /* @__PURE__ */ jsx("div", { className: cx("uix-composer", className), ...props, children });
4
+ function Composer({ children, className, label = "Add a comment", ...props }) {
5
+ return /* @__PURE__ */ jsx("div", { className: cx("uix-composer", className), ...props, children: Children.map(children, (child) => {
6
+ if (!isValidElement(child) || child.type !== "textarea") return child;
7
+ const p = child.props;
8
+ return p["aria-label"] || p["aria-labelledby"] || p.id ? child : cloneElement(child, { "aria-label": label });
9
+ }) });
5
10
  }
6
11
  function ComposerBar({ children, className, ...props }) {
7
12
  return /* @__PURE__ */ jsx("div", { className: cx("uix-composer__bar", className), ...props, children });
@@ -3,7 +3,7 @@ import { cx } from "../cx.js";
3
3
  function DetailLayout({ side, children, className, ...props }) {
4
4
  return /* @__PURE__ */ jsxs("div", { className: cx("uix-detail", className), ...props, children: [
5
5
  /* @__PURE__ */ jsx("div", { className: "uix-detail__main", children }),
6
- side != null && /* @__PURE__ */ jsx("div", { className: "uix-detail__side", children: side })
6
+ side != null && /* @__PURE__ */ jsx("aside", { className: "uix-detail__side", children: side })
7
7
  ] });
8
8
  }
9
9
  export {
@@ -1,14 +1,13 @@
1
1
  import * as react from 'react';
2
- import { ReactNode } from 'react';
2
+ import { HTMLAttributes, ReactNode } from 'react';
3
3
 
4
- interface DrawerProps {
4
+ interface DrawerProps 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
10
  }
12
- declare function Drawer({ open, onClose, title, children, footer, className }: DrawerProps): react.JSX.Element;
11
+ declare function Drawer({ open, onClose, title, children, footer, className, ...rest }: DrawerProps): react.JSX.Element;
13
12
 
14
13
  export { Drawer, type DrawerProps };
@@ -1,13 +1,15 @@
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
- function Drawer({ open, onClose, title, children, footer, className }) {
7
- const ref = useDialog(open);
8
- return /* @__PURE__ */ jsxs("dialog", { ref, className: cx("uix-drawer", className), onClose, children: [
7
+ function Drawer({ open, onClose, title, children, footer, className, ...rest }) {
8
+ const ref = useDialog(open, onClose);
9
+ const titleId = useId();
10
+ return /* @__PURE__ */ jsxs("dialog", { ref, className: cx("uix-drawer", className), "aria-labelledby": title ? titleId : void 0, ...rest, children: [
9
11
  (title != null || onClose) && /* @__PURE__ */ jsxs("div", { className: "uix-drawer__header", children: [
10
- title && /* @__PURE__ */ jsx("div", { style: { fontWeight: 600, fontSize: "var(--uix-text-h3)" }, children: title }),
12
+ title && /* @__PURE__ */ jsx("h2", { id: titleId, style: { fontWeight: 600, fontSize: "var(--uix-text-h3)", fontFamily: "inherit", letterSpacing: "inherit", lineHeight: "inherit" }, children: title }),
11
13
  onClose && /* @__PURE__ */ jsx(
12
14
  "button",
13
15
  {
@@ -5,14 +5,17 @@ function Field({ label, hint, error, success, required, children, className, htm
5
5
  const uid = useId();
6
6
  const msgId = `${uid}-msg`;
7
7
  const describedBy = error || success || hint ? msgId : void 0;
8
+ const controlId = isValidElement(children) ? children.props.id ?? `${uid}-ctl` : void 0;
8
9
  const control = isValidElement(children) ? cloneElement(children, {
10
+ id: controlId,
9
11
  "aria-describedby": [children.props["aria-describedby"], describedBy].filter(Boolean).join(" ") || void 0,
10
- "aria-invalid": error ? true : children.props["aria-invalid"]
12
+ "aria-invalid": error ? true : children.props["aria-invalid"],
13
+ "aria-required": children.props["aria-required"] ?? (required || void 0)
11
14
  }) : children;
12
15
  return /* @__PURE__ */ jsxs("div", { className: cx("uix-field", className), children: [
13
- label && /* @__PURE__ */ jsx("label", { className: "uix-field__label", htmlFor, "data-required": required || void 0, children: label }),
16
+ label && /* @__PURE__ */ jsx("label", { className: "uix-field__label", htmlFor: htmlFor ?? controlId, "data-required": required || void 0, children: label }),
14
17
  control,
15
- /* @__PURE__ */ jsx("div", { className: "uix-field__msg", id: msgId, children: error ? /* @__PURE__ */ jsx("span", { className: "uix-field__error", role: "alert", children: error }) : success ? /* @__PURE__ */ jsx("span", { className: "uix-field__success", children: success }) : hint ? /* @__PURE__ */ jsx("span", { className: "uix-field__hint", children: hint }) : null })
18
+ /* @__PURE__ */ jsx("div", { className: "uix-field__msg", id: msgId, children: error ? /* @__PURE__ */ jsx("span", { className: "uix-field__error", role: "alert", children: error }) : success ? /* @__PURE__ */ jsx("span", { className: "uix-field__success", role: "status", children: success }) : hint ? /* @__PURE__ */ jsx("span", { className: "uix-field__hint", children: hint }) : null })
16
19
  ] });
17
20
  }
18
21
  export {