@godxjp/ui 33.15.0 → 33.16.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 (55) hide show
  1. package/README.md +4 -4
  2. package/agent/START-HERE.md +5 -5
  3. package/agent/components/AreaChart.json +6 -0
  4. package/agent/components/BarChart.json +6 -0
  5. package/agent/components/DashboardGrid.json +76 -0
  6. package/agent/components/DataTable.json +1 -1
  7. package/agent/components/HeatmapChart.json +6 -0
  8. package/agent/components/LineChart.json +6 -0
  9. package/agent/components/Pagination.json +1 -1
  10. package/agent/components/PieChart.json +6 -0
  11. package/agent/components/Table.json +1 -1
  12. package/agent/components-index.json +7 -1
  13. package/agent/components.json +109 -3
  14. package/agent/index.json +6 -6
  15. package/agent/llms.txt +6 -6
  16. package/agent/tokens.json +38 -2
  17. package/dist/components/charts/chart-cartesian.d.ts +3 -2
  18. package/dist/components/charts/chart-cartesian.js +146 -46
  19. package/dist/components/charts/chart-datum-selection.d.ts +15 -0
  20. package/dist/components/charts/chart-datum-selection.js +97 -0
  21. package/dist/components/charts/chart-frame.d.ts +2 -1
  22. package/dist/components/charts/chart-frame.js +2 -1
  23. package/dist/components/charts/heatmap-chart.d.ts +1 -1
  24. package/dist/components/charts/heatmap-chart.js +20 -0
  25. package/dist/components/charts/index.d.ts +1 -1
  26. package/dist/components/charts/pie-chart.d.ts +2 -1
  27. package/dist/components/charts/pie-chart.js +36 -1
  28. package/dist/components/charts/recharts-peer.d.ts +1 -1
  29. package/dist/components/charts/recharts-peer.js +6 -0
  30. package/dist/contracts/measurement.json +1 -1
  31. package/dist/i18n/messages/en.json +10 -0
  32. package/dist/i18n/messages/ja.json +10 -0
  33. package/dist/i18n/messages/vi.json +10 -0
  34. package/dist/lab/dashboard-grid-layout.d.ts +4 -0
  35. package/dist/lab/dashboard-grid-layout.js +36 -0
  36. package/dist/lab/dashboard-grid.d.ts +5 -0
  37. package/dist/lab/dashboard-grid.js +258 -0
  38. package/dist/lab/index.d.ts +2 -0
  39. package/dist/lab/index.js +2 -0
  40. package/dist/props/components/charts.prop.d.ts +11 -0
  41. package/dist/props/components/dashboard-grid.prop.d.ts +27 -0
  42. package/dist/props/components/dashboard-grid.prop.js +0 -0
  43. package/dist/props/components/index.d.ts +1 -1
  44. package/dist/props/registry.d.ts +83 -5
  45. package/dist/props/registry.js +79 -0
  46. package/dist/styles/chart-layout.css +28 -0
  47. package/dist/styles/control.css +82 -0
  48. package/dist/styles/layers.json +10 -1
  49. package/dist/styles/navigation-layout.css +2 -0
  50. package/dist/styles.css +1 -1
  51. package/dist/tokens/base.css +2 -0
  52. package/dist/tokens/components/dashboard-grid.css +14 -0
  53. package/dist/tokens/components/table.css +1 -1
  54. package/docs/COMPOSITION-VS-COMPONENT.md +15 -14
  55. package/package.json +2 -2
package/agent/llms.txt CHANGED
@@ -1,10 +1,10 @@
1
1
  # @godxjp/ui
2
2
 
3
- > A Japanese-enterprise React design system: 170 components, 2251 design tokens,
4
- > 44 cardinal rules. This file is the entry point for AI agents. Catalog version 33.15.0.
3
+ > A Japanese-enterprise React design system: 171 components, 2257 design tokens,
4
+ > 44 cardinal rules. This file is the entry point for AI agents. Catalog version 33.16.0.
5
5
 
6
6
  If your client can run a process, do not read these files — run the MCP server instead
7
- (`npx @godxjp/ui-mcp@33.15.0`). It is searchable and version-locked. These files exist for agents
7
+ (`npx @godxjp/ui-mcp@33.16.0`). It is searchable and version-locked. These files exist for agents
8
8
  that can only fetch URLs.
9
9
 
10
10
  ## Start
@@ -16,10 +16,10 @@ that can only fetch URLs.
16
16
  ## Catalog
17
17
 
18
18
  - [patterns-index.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/patterns-index.json): 28 whole-task patterns (name, tagline, tags). Start here when the task is a TASK — "build a settings page" — then fetch `patterns/<name>.json` for complete code.
19
- - [components-index.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components-index.json): 53 KB — all 170 components as name, group, tagline, plus `absorbed`: the names that do NOT exist and map to it (`Combobox` → `Select`).
19
+ - [components-index.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components-index.json): 54 KB — all 171 components as name, group, tagline, plus `absorbed`: the names that do NOT exist and map to it (`Combobox` → `Select`).
20
20
  - [components/&lt;Name&gt;.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components/Select.json): one file per component (1 KB–38 KB, median 6 KB). Read the index, then fetch only the ones you chose — this is the selective route, and the reason you do not need the blob.
21
21
  - [components.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components.json): 1.3 MB — every entry in one file. Most URL fetchers truncate a response this size without saying so; prefer the per-component files.
22
- - [tokens.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/tokens.json): every design token with its value, the reason it exists, and its `tier` — 218 `foundation` seeds (`--primary`, `--background`, `--radius`: set these when you are handed a brand), 113 `semantic` roles, 1920 `component` knobs.
22
+ - [tokens.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/tokens.json): every design token with its value, the reason it exists, and its `tier` — 218 `foundation` seeds (`--primary`, `--background`, `--radius`: set these when you are handed a brand), 113 `semantic` roles, 1926 `component` knobs.
23
23
  - [vocabulary.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/vocabulary.json): the controlled prop vocabulary — which prop name means what, across every component.
24
24
  - [rules.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/rules.json): 44 cardinal rules.
25
25
  - [anti-ai-tells.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/anti-ai-tells.json): 35 shapes that make generated UI look generated, each with its fix.
@@ -27,7 +27,7 @@ that can only fetch URLs.
27
27
  ## Pinning
28
28
 
29
29
  Every URL above tracks `main`. To pin to the release a project actually installed, swap `main` for
30
- the tag: `.../godx-jp/godxjp-ui/v33.15.0/agent/...`. A catalog that does not match the installed
30
+ the tag: `.../godx-jp/godxjp-ui/v33.16.0/agent/...`. A catalog that does not match the installed
31
31
  package describes props that are absent, or hides props that are present, and says nothing either way.
32
32
 
33
33
  Pinned catalogs only exist for releases whose tag actually contains `agent/`. If `…/v<version>/agent/index.json` returns 404, that release predates this catalog: read `…/main/…` instead and compare `index.json` → `version` against the package you have, so you at least know which way it drifted.
package/agent/tokens.json CHANGED
@@ -5291,6 +5291,42 @@
5291
5291
  "tier": "component",
5292
5292
  "value": "var(--space-1)"
5293
5293
  },
5294
+ {
5295
+ "description": "Gap between DashboardGrid tiles, both axes (#1397).",
5296
+ "name": "--dashboard-grid-gap",
5297
+ "tier": "component",
5298
+ "value": "var(--space-4)"
5299
+ },
5300
+ {
5301
+ "description": "Height of one grid row; a tile's `h` is a multiple of it.",
5302
+ "name": "--dashboard-grid-row-height",
5303
+ "tier": "component",
5304
+ "value": "var(--space-20)"
5305
+ },
5306
+ {
5307
+ "description": "Size of the drag and resize handles in edit mode (≥24px target).",
5308
+ "name": "--dashboard-grid-handle-size",
5309
+ "tier": "component",
5310
+ "value": "var(--control-height)"
5311
+ },
5312
+ {
5313
+ "description": "Inset of the resize handle from the tile's bottom-end corner.",
5314
+ "name": "--dashboard-grid-handle-inset",
5315
+ "tier": "component",
5316
+ "value": "var(--space-1)"
5317
+ },
5318
+ {
5319
+ "description": "Corner radius of a tile; defaults to the Card radius.",
5320
+ "name": "--dashboard-grid-item-radius",
5321
+ "tier": "component",
5322
+ "value": "var(--card-radius)"
5323
+ },
5324
+ {
5325
+ "description": "Colour of the drop-preview ring while moving or resizing (role-mirror knob; defaults to the focus ring colour).",
5326
+ "name": "--dashboard-grid-preview-ring-color",
5327
+ "tier": "component",
5328
+ "value": "initial"
5329
+ },
5294
5330
  {
5295
5331
  "description": "Accordion chevron and Carousel arrow. Both were bare 1rem literals in data-display-layout.css — glyphs with no tier-2 route, invisible to the icon ratchet because neither selector contains the word \"icon\".",
5296
5332
  "name": "--accordion-chevron-size",
@@ -12588,7 +12624,7 @@
12588
12624
  "value": "0px"
12589
12625
  },
12590
12626
  {
12591
- "description": "Table · stacked-record-collection preset (gh#293 restore — SCR-215) — the canonical WIDE, heterogeneous record collection. Below the collapse step every `<tr>` becomes a bordered key-value card; these tokens own the card's own geometry (unlike action-collection, there is no column-priority measure to retune — the whole row becomes one block).",
12627
+ "description": "Table · stacked-record-collection preset (gh#293 restore — SCR-215) — the canonical WIDE, heterogeneous record collection. Below the collapse step every `<tr>` becomes a continuous key-value record; these tokens own the record's geometry (unlike action-collection, there is no column-priority measure to retune — the whole row becomes one block).",
12592
12628
  "name": "--table-stacked-collection-card-padding-y",
12593
12629
  "tier": "component",
12594
12630
  "value": "var(--space-stack-sm)"
@@ -12603,7 +12639,7 @@
12603
12639
  "description": "Table component tokens: row height, cell padding.",
12604
12640
  "name": "--table-stacked-collection-card-gap",
12605
12641
  "tier": "component",
12606
- "value": "var(--space-stack-sm)"
12642
+ "value": "0"
12607
12643
  },
12608
12644
  {
12609
12645
  "description": "Table component tokens: row height, cell padding.",
@@ -1,5 +1,5 @@
1
1
  import * as React from "react";
2
- import type { ChartDatum, ChartSeriesProp } from "../../props/components/charts.prop.js";
2
+ import type { ChartDatum, ChartSeriesProp, ChartDatumSelect } from "../../props/components/charts.prop.js";
3
3
  /**
4
4
  * Shared cartesian renderer for Line / Bar / Area. Internal — the filename pascal
5
5
  * (`ChartCartesian`) is intentionally NOT exported, so this is not a catalog entry.
@@ -21,6 +21,7 @@ type CartesianChartProps = {
21
21
  valueTicks?: number[];
22
22
  numberFormat?: Intl.NumberFormatOptions;
23
23
  emptyMessage?: string;
24
+ onDatumSelect?: ChartDatumSelect;
24
25
  onCategoryClick?: (datum: ChartDatum, index: number) => void;
25
26
  className?: string;
26
27
  id?: string;
@@ -33,5 +34,5 @@ type CartesianChartProps = {
33
34
  /** bar */
34
35
  horizontal?: boolean;
35
36
  };
36
- export declare function CartesianChart({ kind, data, series, categoryKey, label, showCaption, description, size, height, showLegend, showGrid, valueDomain, valueTicks, numberFormat, emptyMessage, onCategoryClick, className, id, curved, showDots, stacked, horizontal, }: CartesianChartProps): React.JSX.Element;
37
+ export declare function CartesianChart({ kind, data, series, categoryKey, label, showCaption, description, size, height, showLegend, showGrid, valueDomain, valueTicks, numberFormat, emptyMessage, onCategoryClick, onDatumSelect, className, id, curved, showDots, stacked, horizontal, }: CartesianChartProps): React.JSX.Element;
37
38
  export {};
@@ -2,6 +2,8 @@
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import {
5
+ Dot,
6
+ Rectangle,
5
7
  Area,
6
8
  AreaChart as RAreaChart,
7
9
  Bar,
@@ -22,6 +24,7 @@ import { useTranslation } from "../../i18n/use-translation.js";
22
24
  import { ChartFrame, chartColor, chartHeight, useChartNumberFormat } from "./chart-frame.js";
23
25
  import { buildCartesianSummary } from "./chart-summary.js";
24
26
  import { useCategoryAxisMetrics } from "./chart-category-axis.js";
27
+ import { useChartDatumSelection } from "./chart-datum-selection.js";
25
28
  import { listFormat } from "../../lib/intl-cache.js";
26
29
  function CartesianChart({
27
30
  kind,
@@ -40,6 +43,7 @@ function CartesianChart({
40
43
  numberFormat,
41
44
  emptyMessage,
42
45
  onCategoryClick,
46
+ onDatumSelect,
43
47
  className,
44
48
  id,
45
49
  curved = false,
@@ -51,6 +55,71 @@ function CartesianChart({
51
55
  const { t, locale } = useTranslation();
52
56
  const fmt = useChartNumberFormat(numberFormat);
53
57
  const list = React.useMemo(() => listFormat(locale, { style: "narrow", type: "unit" }), [locale]);
58
+ const points = series.flatMap(
59
+ (s, seriesIndex) => data.flatMap(
60
+ (datum, index) => typeof datum[s.dataKey] === "number" && Number.isFinite(datum[s.dataKey]) ? [{ index, series: seriesIndex }] : []
61
+ )
62
+ );
63
+ const markProps = useChartDatumSelection(points);
64
+ const mark = (seriesIndex, shape, raw) => {
65
+ const s = series[seriesIndex];
66
+ const props = raw;
67
+ const { x = 0, y = 0, width = 0, height: height2 = 0 } = props;
68
+ const index = props.index;
69
+ const datum = data[index];
70
+ if (!datum || typeof datum[s.dataKey] !== "number" || !Number.isFinite(datum[s.dataKey]))
71
+ return /* @__PURE__ */ jsx("g", {});
72
+ const action = markProps(
73
+ index,
74
+ seriesIndex,
75
+ t("chart.datumLabel", {
76
+ label: String(datum[categoryKey] ?? ""),
77
+ series: s.label ?? s.dataKey,
78
+ value: fmt.format(Number(datum[s.dataKey]))
79
+ }),
80
+ () => onDatumSelect?.(datum, { seriesKey: s.dataKey, index })
81
+ );
82
+ return shape === "bar" ? /* @__PURE__ */ jsxs("g", { ...action, children: [
83
+ /* @__PURE__ */ jsx(
84
+ "rect",
85
+ {
86
+ x: Math.min(x, x + width),
87
+ y: Math.min(y, y + height2),
88
+ width: Math.abs(width),
89
+ height: Math.abs(height2),
90
+ className: "ui-chart-bar-hit",
91
+ fill: "transparent",
92
+ style: {
93
+ "--chart-mark-width": `${Math.abs(width)}px`,
94
+ "--chart-mark-height": `${Math.abs(height2)}px`
95
+ }
96
+ }
97
+ ),
98
+ /* @__PURE__ */ jsx(
99
+ Rectangle,
100
+ {
101
+ x: props.x,
102
+ y: props.y,
103
+ width: props.width,
104
+ height: props.height,
105
+ fill: props.fill,
106
+ radius: 2
107
+ }
108
+ )
109
+ ] }) : /* @__PURE__ */ jsxs("g", { ...action, children: [
110
+ /* @__PURE__ */ jsx("circle", { cx: props.cx, cy: props.cy, className: "ui-chart-point-hit", fill: "transparent" }),
111
+ /* @__PURE__ */ jsx(Dot, { cx: props.cx, cy: props.cy, fill: chartColor(seriesIndex, s.color) })
112
+ ] });
113
+ };
114
+ const markRef = React.useRef(mark);
115
+ markRef.current = mark;
116
+ const shapes = React.useMemo(
117
+ () => Array.from({ length: series.length }, (_, i) => ({
118
+ bar: (raw) => markRef.current(i, "bar", raw),
119
+ dot: (raw) => markRef.current(i, "dot", raw)
120
+ })),
121
+ [series.length]
122
+ );
54
123
  const hasData = data.length > 0 && series.length > 0;
55
124
  const summary = buildCartesianSummary(
56
125
  data,
@@ -161,59 +230,90 @@ function CartesianChart({
161
230
  const onBarClick = onCategoryClick ? (_entry, index) => pickCategory(index) : void 0;
162
231
  const chart = () => {
163
232
  if (kind === "line") {
164
- return /* @__PURE__ */ jsxs(RLineChart, { data, onClick: onChartClick, children: [
165
- axes(true),
166
- series.map((s, i) => /* @__PURE__ */ jsx(
167
- Line,
168
- {
169
- type: curve,
170
- dataKey: s.dataKey,
171
- name: s.label ?? s.dataKey,
172
- stroke: chartColor(i, s.color),
173
- dot: showDots
174
- },
175
- s.dataKey
176
- ))
177
- ] });
233
+ return /* @__PURE__ */ jsxs(
234
+ RLineChart,
235
+ {
236
+ accessibilityLayer: onDatumSelect ? false : void 0,
237
+ data,
238
+ onClick: onChartClick,
239
+ children: [
240
+ axes(true),
241
+ series.map((s, i) => /* @__PURE__ */ jsx(
242
+ Line,
243
+ {
244
+ type: curve,
245
+ dataKey: s.dataKey,
246
+ name: s.label ?? s.dataKey,
247
+ stroke: chartColor(i, s.color),
248
+ dot: onDatumSelect ? shapes[i].dot : showDots,
249
+ activeDot: onDatumSelect ? false : void 0,
250
+ isAnimationActive: onDatumSelect ? false : void 0
251
+ },
252
+ s.dataKey
253
+ ))
254
+ ]
255
+ }
256
+ );
178
257
  }
179
258
  if (kind === "area") {
180
- return /* @__PURE__ */ jsxs(RAreaChart, { data, onClick: onChartClick, children: [
181
- axes(true),
182
- series.map((s, i) => /* @__PURE__ */ jsx(
183
- Area,
184
- {
185
- type: curve,
186
- dataKey: s.dataKey,
187
- name: s.label ?? s.dataKey,
188
- stackId: stacked ? "stack" : void 0,
189
- stroke: chartColor(i, s.color),
190
- fill: s.fillColor ?? chartColor(i, s.color),
191
- dot: showDots
192
- },
193
- s.dataKey
194
- ))
195
- ] });
196
- }
197
- return /* @__PURE__ */ jsxs(RBarChart, { data, layout: horizontal ? "vertical" : "horizontal", children: [
198
- axes(!horizontal),
199
- series.map((s, i) => /* @__PURE__ */ jsx(
200
- Bar,
259
+ return /* @__PURE__ */ jsxs(
260
+ RAreaChart,
201
261
  {
202
- dataKey: s.dataKey,
203
- name: s.label ?? s.dataKey,
204
- stackId: stacked ? "stack" : void 0,
205
- fill: chartColor(i, s.color),
206
- radius: 2,
207
- onClick: onBarClick,
208
- background: onCategoryClick ? { fill: "transparent" } : void 0
209
- },
210
- s.dataKey
211
- ))
212
- ] });
262
+ accessibilityLayer: onDatumSelect ? false : void 0,
263
+ data,
264
+ onClick: onChartClick,
265
+ children: [
266
+ axes(true),
267
+ series.map((s, i) => /* @__PURE__ */ jsx(
268
+ Area,
269
+ {
270
+ type: curve,
271
+ dataKey: s.dataKey,
272
+ name: s.label ?? s.dataKey,
273
+ stackId: stacked ? "stack" : void 0,
274
+ stroke: chartColor(i, s.color),
275
+ fill: s.fillColor ?? chartColor(i, s.color),
276
+ dot: onDatumSelect ? shapes[i].dot : showDots,
277
+ activeDot: onDatumSelect ? false : void 0,
278
+ isAnimationActive: onDatumSelect ? false : void 0
279
+ },
280
+ s.dataKey
281
+ ))
282
+ ]
283
+ }
284
+ );
285
+ }
286
+ return /* @__PURE__ */ jsxs(
287
+ RBarChart,
288
+ {
289
+ accessibilityLayer: onDatumSelect ? false : void 0,
290
+ data,
291
+ layout: horizontal ? "vertical" : "horizontal",
292
+ children: [
293
+ axes(!horizontal),
294
+ series.map((s, i) => /* @__PURE__ */ jsx(
295
+ Bar,
296
+ {
297
+ dataKey: s.dataKey,
298
+ name: s.label ?? s.dataKey,
299
+ stackId: stacked ? "stack" : void 0,
300
+ fill: chartColor(i, s.color),
301
+ radius: 2,
302
+ shape: onDatumSelect ? shapes[i].bar : void 0,
303
+ isAnimationActive: onDatumSelect ? false : void 0,
304
+ onClick: onBarClick,
305
+ background: onCategoryClick ? { fill: "transparent" } : void 0
306
+ },
307
+ s.dataKey
308
+ ))
309
+ ]
310
+ }
311
+ );
213
312
  };
214
313
  return /* @__PURE__ */ jsx(
215
314
  ChartFrame,
216
315
  {
316
+ interactive: !!onDatumSelect,
217
317
  label,
218
318
  showCaption,
219
319
  description,
@@ -0,0 +1,15 @@
1
+ import * as React from "react";
2
+ /** Roving mark focus. Geometry stays in recharts; data identity stays in the original array. */
3
+ export declare function useChartDatumSelection(points: {
4
+ index: number;
5
+ series: number;
6
+ }[]): (index: number, series: number, name: string, select: () => void) => {
7
+ role: string;
8
+ "aria-label": string;
9
+ "data-chart-datum": string;
10
+ className: string;
11
+ tabIndex: number;
12
+ onFocus: (event: React.FocusEvent<SVGElement>) => void;
13
+ onClick: (event: React.MouseEvent<SVGElement>) => void;
14
+ onKeyDown: (event: React.KeyboardEvent<SVGElement>) => void;
15
+ };
@@ -0,0 +1,97 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ function useChartDatumSelection(points) {
4
+ const active = React.useRef("");
5
+ const recovery = React.useRef(null);
6
+ React.useEffect(() => () => recovery.current?.(), []);
7
+ const key = (index, series) => `${series}:${index}`;
8
+ const keys = points.map((p) => key(p.index, p.series));
9
+ const current = keys.includes(active.current) ? active.current : keys[0];
10
+ const activate = (node, select) => {
11
+ const canvas = node.closest(".ui-chart-canvas");
12
+ const identity = node.dataset.chartDatum;
13
+ recovery.current?.();
14
+ let focused = node;
15
+ const stop = () => {
16
+ observer.disconnect();
17
+ document.removeEventListener("keydown", stop, true);
18
+ document.removeEventListener("pointerdown", stop, true);
19
+ document.removeEventListener("focusin", onFocusIn, true);
20
+ };
21
+ const onFocusIn = (event) => {
22
+ if (event.target !== focused) stop();
23
+ };
24
+ const observer = new MutationObserver(() => {
25
+ if (focused.isConnected || !canvas?.isConnected || document.activeElement !== document.body)
26
+ return;
27
+ const replacement = Array.from(
28
+ canvas.querySelectorAll("[data-chart-datum]")
29
+ ).find((mark) => mark.dataset.chartDatum === identity);
30
+ if (replacement) {
31
+ focused = replacement;
32
+ replacement.focus();
33
+ }
34
+ });
35
+ recovery.current = stop;
36
+ if (canvas) observer.observe(canvas, { childList: true, subtree: true });
37
+ document.addEventListener("keydown", stop, true);
38
+ document.addEventListener("pointerdown", stop, true);
39
+ document.addEventListener("focusin", onFocusIn, true);
40
+ select();
41
+ };
42
+ return (index, series, name, select) => ({
43
+ role: "button",
44
+ "aria-label": name,
45
+ "data-chart-datum": key(index, series),
46
+ className: "ui-chart-datum ui-focus-ring",
47
+ tabIndex: current === key(index, series) ? 0 : -1,
48
+ onFocus: (event) => {
49
+ event.stopPropagation();
50
+ active.current = key(index, series);
51
+ event.currentTarget.closest(".ui-chart-canvas")?.querySelectorAll("[data-chart-datum]").forEach((node) => {
52
+ node.tabIndex = node === event.currentTarget ? 0 : -1;
53
+ });
54
+ },
55
+ onClick: (event) => {
56
+ event.stopPropagation();
57
+ event.currentTarget.focus();
58
+ activate(event.currentTarget, select);
59
+ },
60
+ onKeyDown: (event) => {
61
+ if (event.key === "Enter" || event.key === " ") {
62
+ event.preventDefault();
63
+ event.stopPropagation();
64
+ if (!event.repeat) activate(event.currentTarget, select);
65
+ return;
66
+ }
67
+ const rtl = getComputedStyle(event.currentTarget).direction === "rtl";
68
+ let target;
69
+ const along = event.key === "ArrowRight" ? rtl ? -1 : 1 : event.key === "ArrowLeft" ? rtl ? 1 : -1 : 0;
70
+ const across = event.key === "ArrowDown" ? 1 : event.key === "ArrowUp" ? -1 : 0;
71
+ if (along) {
72
+ const row = points.filter((p) => p.series === series);
73
+ const at = row.findIndex((p) => p.index === index);
74
+ target = row[Math.max(0, Math.min(row.length - 1, at + along))];
75
+ } else if (across) {
76
+ const column = points.filter((p) => p.index === index);
77
+ const at = column.findIndex((p) => p.series === series);
78
+ target = column[Math.max(0, Math.min(column.length - 1, at + across))];
79
+ } else if (event.key === "Home" || event.key === "End") {
80
+ const row = event.ctrlKey || event.metaKey ? points : points.filter((p) => p.series === series);
81
+ target = event.key === "Home" ? row[0] : row.at(-1);
82
+ } else return;
83
+ event.preventDefault();
84
+ event.stopPropagation();
85
+ if (!target) return;
86
+ const next = key(target.index, target.series);
87
+ const canvas = event.currentTarget.closest(".ui-chart-canvas");
88
+ const mark = Array.from(
89
+ canvas?.querySelectorAll("[data-chart-datum]") ?? []
90
+ ).find((node) => node.dataset.chartDatum === next);
91
+ mark?.focus();
92
+ }
93
+ });
94
+ }
95
+ export {
96
+ useChartDatumSelection
97
+ };
@@ -16,6 +16,7 @@ export declare function chartHeight(size?: "xs" | "sm" | "md" | "lg", height?: n
16
16
  /** A locale-bound number formatter for the active app locale. */
17
17
  export declare function useChartNumberFormat(options?: Intl.NumberFormatOptions): Intl.NumberFormat;
18
18
  type ChartFrameProps = {
19
+ interactive?: boolean;
19
20
  /** Visible caption; also the accessible name via `aria-labelledby` → `<figcaption>`. */
20
21
  label: React.ReactNode;
21
22
  /**
@@ -54,5 +55,5 @@ type ChartFrameProps = {
54
55
  * in a `role="img"` element with a short `aria-label`; the full data is exposed as
55
56
  * a sibling visually-hidden list referenced by the figure's `aria-describedby`.
56
57
  */
57
- export declare function ChartFrame({ label, showCaption, description, summaryRows, imgSummary, hasData, height, size, footer, className, id, children, ref, canvasRef, canvasExtra, }: ChartFrameProps): React.JSX.Element;
58
+ export declare function ChartFrame({ interactive, label, showCaption, description, summaryRows, imgSummary, hasData, height, size, footer, className, id, children, ref, canvasRef, canvasExtra, }: ChartFrameProps): React.JSX.Element;
58
59
  export {};
@@ -29,6 +29,7 @@ function useChartNumberFormat(options) {
29
29
  return React.useMemo(() => numberFormat(locale, options), [locale, options]);
30
30
  }
31
31
  function ChartFrame({
32
+ interactive = false,
32
33
  label,
33
34
  showCaption = true,
34
35
  description,
@@ -66,7 +67,7 @@ function ChartFrame({
66
67
  ref: canvasRef,
67
68
  className: "ui-chart-canvas",
68
69
  style: { height },
69
- role: "img",
70
+ role: interactive ? "group" : "img",
70
71
  "aria-label": imgSummary,
71
72
  children: [
72
73
  children,
@@ -16,4 +16,4 @@ export type { HeatmapChartProp, HeatmapChartProp as HeatmapChartProps, HeatmapCa
16
16
  * - **Narrow screens** — the matrix never shrinks a cell below its `size` tier; it scrolls inside
17
17
  * its own box with the row header pinned, so the page never scrolls sideways.
18
18
  */
19
- export declare function HeatmapChart({ rows, columns, data, label, showCaption, description, size, numberFormat, topN, showLegend, emptyMessage, className, id, ref, }: HeatmapChartProp): React.JSX.Element;
19
+ export declare function HeatmapChart({ onDatumSelect, rows, columns, data, label, showCaption, description, size, numberFormat, topN, showLegend, emptyMessage, className, id, ref, }: HeatmapChartProp): React.JSX.Element;
@@ -11,6 +11,7 @@ const OPEN_DELAY = 200;
11
11
  const CLOSE_GRACE = 100;
12
12
  const RAMP = Array.from({ length: HEAT_STEPS }, (_, index) => index + 1);
13
13
  function HeatmapChart({
14
+ onDatumSelect,
14
15
  rows,
15
16
  columns,
16
17
  data,
@@ -91,6 +92,12 @@ function HeatmapChart({
91
92
  reveal(cell);
92
93
  }
93
94
  };
95
+ const selectCell = (r, c) => {
96
+ const index = data.findIndex(
97
+ (datum) => String(datum.row) === String(rows[r]?.key) && String(datum.column) === String(columns[c]?.key) && typeof datum.value === "number" && Number.isFinite(datum.value)
98
+ );
99
+ if (index >= 0) onDatumSelect?.(data[index], { seriesKey: String(columns[c].key), index });
100
+ };
94
101
  const onGridKeyDown = (event) => {
95
102
  const rtl = getComputedStyle(event.currentTarget).direction === "rtl";
96
103
  const lastRow = rows.length - 1;
@@ -193,6 +200,19 @@ function HeatmapChart({
193
200
  },
194
201
  tabIndex: r === activeRow && c === activeColumn ? 0 : -1,
195
202
  className: "ui-chart-heatmap-cell ui-focus-ring",
203
+ "data-interactive": onDatumSelect && !empty ? "" : void 0,
204
+ "aria-label": onDatumSelect ? t("chart.heatmapCell", {
205
+ row: row.label,
206
+ column: column.label,
207
+ value: text.value
208
+ }) : void 0,
209
+ onKeyDown: onDatumSelect ? (event) => {
210
+ if (event.key !== "Enter" && event.key !== " ") return;
211
+ event.preventDefault();
212
+ event.stopPropagation();
213
+ if (!event.repeat && !empty) selectCell(r, c);
214
+ } : void 0,
215
+ onClick: onDatumSelect && !empty ? () => selectCell(r, c) : void 0,
196
216
  "data-step": empty ? void 0 : model.steps.get(key),
197
217
  "data-empty": empty ? "" : void 0,
198
218
  "data-emphasized": model.ranks.has(key) ? "" : void 0,
@@ -16,5 +16,5 @@ export { AreaChart } from "./area-chart.js";
16
16
  export type { AreaChartProp, AreaChartProps } from "./area-chart.js";
17
17
  export { PieChart } from "./pie-chart.js";
18
18
  export type { PieChartProp, PieChartProps } from "./pie-chart.js";
19
- export type { ChartSeriesProp, ChartDatum } from "../../props/components/charts.prop.js";
19
+ export type { ChartSeriesProp, ChartDatum, ChartDatumSelect } from "../../props/components/charts.prop.js";
20
20
  export { CHART_COLORS } from "./chart-frame.js";
@@ -1,3 +1,4 @@
1
+ import * as React from "react";
1
2
  import type { PieChartProp } from "../../props/components/charts.prop.js";
2
3
  export type { PieChartProp, PieChartProp as PieChartProps, } from "../../props/components/charts.prop.js";
3
4
  /**
@@ -5,4 +6,4 @@ export type { PieChartProp, PieChartProp as PieChartProps, } from "../../props/c
5
6
  * Import from the tree-shaken `@godxjp/ui/charts` entry; requires the `recharts`
6
7
  * optional peer dependency.
7
8
  */
8
- export declare function PieChart({ data, dataKey, nameKey, colors, label, showCaption, description, size, height, showLegend, numberFormat, donut, emptyMessage, className, id, }: PieChartProp): import("react").JSX.Element;
9
+ export declare function PieChart({ onDatumSelect, data, dataKey, nameKey, colors, label, showCaption, description, size, height, showLegend, numberFormat, donut, emptyMessage, className, id, }: PieChartProp): React.JSX.Element;
@@ -1,7 +1,9 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
3
4
  import {
4
5
  Cell,
6
+ Sector,
5
7
  Legend,
6
8
  Pie,
7
9
  PieChart as RPieChart,
@@ -12,8 +14,10 @@ import {
12
14
  import { EmptyState } from "../data-display/empty-state.js";
13
15
  import { useTranslation } from "../../i18n/use-translation.js";
14
16
  import { ChartFrame, chartColor, chartHeight, useChartNumberFormat } from "./chart-frame.js";
17
+ import { useChartDatumSelection } from "./chart-datum-selection.js";
15
18
  import { buildPieSummary } from "./chart-summary.js";
16
19
  function PieChart({
20
+ onDatumSelect,
17
21
  data,
18
22
  dataKey,
19
23
  nameKey,
@@ -33,6 +37,34 @@ function PieChart({
33
37
  assertRechartsPeer();
34
38
  const { t } = useTranslation();
35
39
  const fmt = useChartNumberFormat(numberFormat);
40
+ const points = data.flatMap(
41
+ (datum, index) => typeof datum[dataKey] === "number" && Number.isFinite(datum[dataKey]) && Number(datum[dataKey]) > 0 ? [{ index, series: 0 }] : []
42
+ );
43
+ const markProps = useChartDatumSelection(points);
44
+ const shape = (raw) => {
45
+ const props = raw;
46
+ const datum = data[props.index];
47
+ if (!datum || !points.some((p) => p.index === props.index)) return /* @__PURE__ */ jsx("g", {});
48
+ return /* @__PURE__ */ jsx(
49
+ Sector,
50
+ {
51
+ ...props,
52
+ ...markProps(
53
+ props.index,
54
+ 0,
55
+ t("chart.datumLabel", {
56
+ label: String(datum[nameKey] ?? ""),
57
+ series: dataKey,
58
+ value: fmt.format(Number(datum[dataKey]))
59
+ }),
60
+ () => onDatumSelect?.(datum, { seriesKey: dataKey, index: props.index })
61
+ )
62
+ }
63
+ );
64
+ };
65
+ const shapeRef = React.useRef(shape);
66
+ shapeRef.current = shape;
67
+ const stableShape = React.useCallback((raw) => shapeRef.current(raw), []);
36
68
  const hasData = data.length > 0;
37
69
  const summary = buildPieSummary(
38
70
  data,
@@ -45,6 +77,7 @@ function PieChart({
45
77
  return /* @__PURE__ */ jsx(
46
78
  ChartFrame,
47
79
  {
80
+ interactive: !!onDatumSelect,
48
81
  label,
49
82
  showCaption,
50
83
  description,
@@ -54,10 +87,12 @@ function PieChart({
54
87
  height: resolvedHeight,
55
88
  className,
56
89
  id,
57
- children: hasData ? /* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: "100%", minWidth: 0, children: /* @__PURE__ */ jsxs(RPieChart, { children: [
90
+ children: hasData ? /* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: "100%", minWidth: 0, children: /* @__PURE__ */ jsxs(RPieChart, { accessibilityLayer: onDatumSelect ? false : void 0, children: [
58
91
  /* @__PURE__ */ jsx(
59
92
  Pie,
60
93
  {
94
+ shape: onDatumSelect ? stableShape : void 0,
95
+ isAnimationActive: onDatumSelect ? false : void 0,
61
96
  data,
62
97
  dataKey,
63
98
  nameKey,
@@ -32,7 +32,7 @@ export declare const RECHARTS_PEER_MESSAGE: string;
32
32
  * recharts-backed chart before it renders anything.
33
33
  */
34
34
  export declare function assertRechartsPeer(): void;
35
- export declare const Area: <DataPointType = any, ValueAxisType = any>(props: peer.AreaProps<DataPointType, ValueAxisType>) => import("react").ReactElement, AreaChart: <DataPointType = any>(props: import("recharts/types/util/types").CartesianChartProps<DataPointType> & {
35
+ export declare const Dot: import("react").FC<peer.DotProps>, Rectangle: import("react").FC<peer.RectangleProps>, Sector: import("react").FC<peer.SectorProps>, Area: <DataPointType = any, ValueAxisType = any>(props: peer.AreaProps<DataPointType, ValueAxisType>) => import("react").ReactElement, AreaChart: <DataPointType = any>(props: import("recharts/types/util/types").CartesianChartProps<DataPointType> & {
36
36
  ref?: React.Ref<SVGSVGElement>;
37
37
  }) => React.ReactElement, Bar: <DataPointType = any, ValueAxisType = any>(props: peer.BarProps<DataPointType, ValueAxisType>) => import("react").ReactElement, BarChart: <DataPointType = any>(props: import("recharts/types/util/types").CartesianChartProps<DataPointType> & {
38
38
  ref?: React.Ref<SVGSVGElement>;