@godxjp/ui 33.14.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.
- package/README.md +5 -5
- package/agent/START-HERE.md +6 -6
- package/agent/components/AreaChart.json +6 -0
- package/agent/components/BarChart.json +6 -0
- package/agent/components/DashboardGrid.json +76 -0
- package/agent/components/DataTable.json +1 -1
- package/agent/components/HeatmapChart.json +6 -0
- package/agent/components/LineChart.json +6 -0
- package/agent/components/Pagination.json +1 -1
- package/agent/components/PieChart.json +6 -0
- package/agent/components/Table.json +1 -1
- package/agent/components/Upload.json +1 -1
- package/agent/components-index.json +7 -1
- package/agent/components.json +110 -4
- package/agent/index.json +7 -7
- package/agent/llms.txt +7 -7
- package/agent/tokens.json +38 -2
- package/dist/components/charts/chart-cartesian.d.ts +3 -2
- package/dist/components/charts/chart-cartesian.js +146 -46
- package/dist/components/charts/chart-datum-selection.d.ts +15 -0
- package/dist/components/charts/chart-datum-selection.js +97 -0
- package/dist/components/charts/chart-frame.d.ts +2 -1
- package/dist/components/charts/chart-frame.js +2 -1
- package/dist/components/charts/heatmap-chart.d.ts +1 -1
- package/dist/components/charts/heatmap-chart.js +20 -0
- package/dist/components/charts/index.d.ts +1 -1
- package/dist/components/charts/pie-chart.d.ts +2 -1
- package/dist/components/charts/pie-chart.js +36 -1
- package/dist/components/charts/recharts-peer.d.ts +1 -1
- package/dist/components/charts/recharts-peer.js +6 -0
- package/dist/components/data-entry/upload.js +9 -3
- package/dist/contracts/measurement.json +1 -1
- package/dist/i18n/messages/en.json +10 -1
- package/dist/i18n/messages/ja.json +10 -1
- package/dist/i18n/messages/vi.json +10 -1
- package/dist/lab/dashboard-grid-layout.d.ts +4 -0
- package/dist/lab/dashboard-grid-layout.js +36 -0
- package/dist/lab/dashboard-grid.d.ts +5 -0
- package/dist/lab/dashboard-grid.js +258 -0
- package/dist/lab/index.d.ts +2 -0
- package/dist/lab/index.js +2 -0
- package/dist/props/components/charts.prop.d.ts +11 -0
- package/dist/props/components/dashboard-grid.prop.d.ts +27 -0
- package/dist/props/components/dashboard-grid.prop.js +0 -0
- package/dist/props/components/index.d.ts +1 -1
- package/dist/props/registry.d.ts +83 -5
- package/dist/props/registry.js +79 -0
- package/dist/styles/chart-layout.css +28 -0
- package/dist/styles/control.css +82 -0
- package/dist/styles/layers.json +10 -1
- package/dist/styles/navigation-layout.css +2 -0
- package/dist/styles/table-layout.css +6 -0
- package/dist/styles.css +1 -1
- package/dist/tokens/base.css +2 -0
- package/dist/tokens/components/dashboard-grid.css +14 -0
- package/dist/tokens/components/table.css +1 -1
- package/docs/COMPOSITION-VS-COMPONENT.md +15 -14
- package/package.json +2 -2
|
@@ -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>;
|
|
@@ -8,6 +8,9 @@ function assertRechartsPeer() {
|
|
|
8
8
|
}
|
|
9
9
|
}
|
|
10
10
|
const {
|
|
11
|
+
Dot,
|
|
12
|
+
Rectangle,
|
|
13
|
+
Sector,
|
|
11
14
|
Area,
|
|
12
15
|
AreaChart,
|
|
13
16
|
Bar,
|
|
@@ -31,13 +34,16 @@ export {
|
|
|
31
34
|
BarChart,
|
|
32
35
|
CartesianGrid,
|
|
33
36
|
Cell,
|
|
37
|
+
Dot,
|
|
34
38
|
Legend,
|
|
35
39
|
Line,
|
|
36
40
|
LineChart,
|
|
37
41
|
Pie,
|
|
38
42
|
PieChart,
|
|
39
43
|
RECHARTS_PEER_MESSAGE,
|
|
44
|
+
Rectangle,
|
|
40
45
|
ResponsiveContainer,
|
|
46
|
+
Sector,
|
|
41
47
|
Tooltip,
|
|
42
48
|
XAxis,
|
|
43
49
|
YAxis,
|
|
@@ -119,6 +119,7 @@ function Upload({
|
|
|
119
119
|
const fieldName = React.useContext(FieldNameContext);
|
|
120
120
|
const triggerId = React.useId();
|
|
121
121
|
const selectionId = React.useId();
|
|
122
|
+
const dropzoneHintId = React.useId();
|
|
122
123
|
const triggerRef = React.useRef(null);
|
|
123
124
|
const ownAriaLabel = fieldA11y["aria-label"];
|
|
124
125
|
const callerAriaLabel = !fieldLabelledBy || fieldName && ownAriaLabel !== fieldName.label ? ownAriaLabel : void 0;
|
|
@@ -476,6 +477,12 @@ function Upload({
|
|
|
476
477
|
if (!disabled && openFileDialogOnClick) inputRef.current?.click();
|
|
477
478
|
};
|
|
478
479
|
if (variant === "dropzone") {
|
|
480
|
+
const hintText = t("dataEntry.upload.dropzoneHint");
|
|
481
|
+
const named = triggerA11y(hintText);
|
|
482
|
+
const dropzoneA11y = callerAriaLabel !== void 0 ? { ...named, "aria-label": callerAriaLabel } : {
|
|
483
|
+
...named,
|
|
484
|
+
"aria-labelledby": named["aria-labelledby"] ? `${fieldLabelledBy} ${dropzoneHintId}` : dropzoneHintId
|
|
485
|
+
};
|
|
479
486
|
return /* @__PURE__ */ jsxs("div", { ...pasteProps, className: cn("ui-stack-sm", className), children: [
|
|
480
487
|
hiddenInput,
|
|
481
488
|
liveRegion,
|
|
@@ -487,8 +494,7 @@ function Upload({
|
|
|
487
494
|
role: openFileDialogOnClick ? "button" : "group",
|
|
488
495
|
tabIndex: disabled ? -1 : 0,
|
|
489
496
|
"aria-disabled": disabled,
|
|
490
|
-
...
|
|
491
|
-
"aria-label": callerAriaLabel ?? t("dataEntry.upload.dropzoneLabel"),
|
|
497
|
+
...dropzoneA11y,
|
|
492
498
|
onClick: openPicker,
|
|
493
499
|
onKeyDown: (e) => {
|
|
494
500
|
if (e.key === "Enter" || e.key === " ") {
|
|
@@ -515,7 +521,7 @@ function Upload({
|
|
|
515
521
|
className: "ui-upload-dropzone",
|
|
516
522
|
children: [
|
|
517
523
|
/* @__PURE__ */ jsx(UploadIcon, { className: "ui-upload-dropzone-icon", "aria-hidden": "true" }),
|
|
518
|
-
/* @__PURE__ */ jsx("p", { className: "ui-upload-dropzone-hint", children: t("dataEntry.upload.dropzoneHint") }),
|
|
524
|
+
/* @__PURE__ */ jsx("p", { id: dropzoneHintId, className: "ui-upload-dropzone-hint", children: t("dataEntry.upload.dropzoneHint") }),
|
|
519
525
|
/* @__PURE__ */ jsx("p", { className: "ui-upload-dropzone-meta", children: t("dataEntry.upload.dropzoneMeta") })
|
|
520
526
|
]
|
|
521
527
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$comment": "AUTO-GENERATED by scripts/gen-measurement-contract.mjs — do not edit. Read this instead of guessing: docs/MEASUREMENT-CONTRACT.md.",
|
|
3
|
-
"version": "33.
|
|
3
|
+
"version": "33.16.0",
|
|
4
4
|
"targetSize": {
|
|
5
5
|
"standard": "WCAG 2.2 SC 2.5.8 Target Size (Minimum), level AA — 24×24 CSS px",
|
|
6
6
|
"min": 24,
|
|
@@ -74,7 +74,6 @@
|
|
|
74
74
|
"presetEmpty": "Empty"
|
|
75
75
|
},
|
|
76
76
|
"upload": {
|
|
77
|
-
"dropzoneLabel": "Upload files",
|
|
78
77
|
"dropzoneHint": "Drag and drop files here, or click to browse",
|
|
79
78
|
"dropzoneMeta": "Selected files appear here.",
|
|
80
79
|
"buttonLabel": "Choose file",
|
|
@@ -798,6 +797,7 @@
|
|
|
798
797
|
}
|
|
799
798
|
},
|
|
800
799
|
"chart": {
|
|
800
|
+
"datumLabel": "{label}, {series}: {value}",
|
|
801
801
|
"empty": "No data to display",
|
|
802
802
|
"summaryCartesian": "Chart with {series} series across {points} data points",
|
|
803
803
|
"summaryPie": "Pie chart with {slices} slices",
|
|
@@ -869,5 +869,14 @@
|
|
|
869
869
|
"error": "failed"
|
|
870
870
|
}
|
|
871
871
|
}
|
|
872
|
+
},
|
|
873
|
+
"dashboardGrid": {
|
|
874
|
+
"move": "Move {label}",
|
|
875
|
+
"resize": "Resize {label}",
|
|
876
|
+
"hint": "Enter or Space grabs and drops. Arrow keys move; Shift and arrows resize. Escape cancels.",
|
|
877
|
+
"grabbed": "Grabbed {label}. Column {x}, row {y}, width {w}, height {h}.",
|
|
878
|
+
"moved": "{label}. Column {x}, row {y}, width {w}, height {h}.",
|
|
879
|
+
"dropped": "Saved {label}. Column {x}, row {y}, width {w}, height {h}.",
|
|
880
|
+
"cancelled": "Cancelled changes to {label}."
|
|
872
881
|
}
|
|
873
882
|
}
|
|
@@ -73,7 +73,6 @@
|
|
|
73
73
|
"presetEmpty": "空"
|
|
74
74
|
},
|
|
75
75
|
"upload": {
|
|
76
|
-
"dropzoneLabel": "ファイルをアップロード",
|
|
77
76
|
"dropzoneHint": "ここにドラッグ&ドロップ、またはクリックして選択",
|
|
78
77
|
"dropzoneMeta": "選択したファイルはここに表示されます。",
|
|
79
78
|
"buttonLabel": "ファイルを選択",
|
|
@@ -772,6 +771,7 @@
|
|
|
772
771
|
}
|
|
773
772
|
},
|
|
774
773
|
"chart": {
|
|
774
|
+
"datumLabel": "{label}, {series}: {value}",
|
|
775
775
|
"empty": "データがありません",
|
|
776
776
|
"summaryCartesian": "{series}系列・{points}データ点のグラフ",
|
|
777
777
|
"summaryPie": "{slices}区分の円グラフ",
|
|
@@ -842,5 +842,14 @@
|
|
|
842
842
|
"error": "失敗"
|
|
843
843
|
}
|
|
844
844
|
}
|
|
845
|
+
},
|
|
846
|
+
"dashboardGrid": {
|
|
847
|
+
"move": "{label}を移動",
|
|
848
|
+
"resize": "{label}のサイズ変更",
|
|
849
|
+
"hint": "Enter またはスペースでつかむ・確定。矢印キーで移動、Shift と矢印でサイズ変更。Escape で取り消します。",
|
|
850
|
+
"grabbed": "{label}をつかみました。列{x}、行{y}、幅{w}、高さ{h}。",
|
|
851
|
+
"moved": "{label}。列{x}、行{y}、幅{w}、高さ{h}。",
|
|
852
|
+
"dropped": "{label}を保存しました。列{x}、行{y}、幅{w}、高さ{h}。",
|
|
853
|
+
"cancelled": "{label}の変更を取り消しました。"
|
|
845
854
|
}
|
|
846
855
|
}
|
|
@@ -73,7 +73,6 @@
|
|
|
73
73
|
"presetEmpty": "Trống"
|
|
74
74
|
},
|
|
75
75
|
"upload": {
|
|
76
|
-
"dropzoneLabel": "Tải file lên",
|
|
77
76
|
"dropzoneHint": "Kéo thả file vào đây, hoặc bấm để chọn",
|
|
78
77
|
"dropzoneMeta": "Tệp đã chọn sẽ hiển thị tại đây.",
|
|
79
78
|
"buttonLabel": "Chọn file",
|
|
@@ -773,6 +772,7 @@
|
|
|
773
772
|
}
|
|
774
773
|
},
|
|
775
774
|
"chart": {
|
|
775
|
+
"datumLabel": "{label}, {series}: {value}",
|
|
776
776
|
"empty": "Không có dữ liệu",
|
|
777
777
|
"summaryCartesian": "Biểu đồ gồm {series} chuỗi trên {points} điểm dữ liệu",
|
|
778
778
|
"summaryPie": "Biểu đồ tròn gồm {slices} phần",
|
|
@@ -843,5 +843,14 @@
|
|
|
843
843
|
"error": "thất bại"
|
|
844
844
|
}
|
|
845
845
|
}
|
|
846
|
+
},
|
|
847
|
+
"dashboardGrid": {
|
|
848
|
+
"move": "Di chuyển {label}",
|
|
849
|
+
"resize": "Đổi kích thước {label}",
|
|
850
|
+
"hint": "Enter hoặc Space để nắm và thả. Phím mũi tên để di chuyển; Shift và mũi tên để đổi kích thước. Escape để hủy.",
|
|
851
|
+
"grabbed": "Đã nắm {label}. Cột {x}, hàng {y}, rộng {w}, cao {h}.",
|
|
852
|
+
"moved": "{label}. Cột {x}, hàng {y}, rộng {w}, cao {h}.",
|
|
853
|
+
"dropped": "Đã lưu {label}. Cột {x}, hàng {y}, rộng {w}, cao {h}.",
|
|
854
|
+
"cancelled": "Đã hủy thay đổi {label}."
|
|
846
855
|
}
|
|
847
856
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { DashboardGridItemProp } from "../props/components/dashboard-grid.prop.js";
|
|
2
|
+
export declare const gridColumns: (value: number) => number;
|
|
3
|
+
/** Active tile wins. Others push down in stable visual order, then compact upward without overlap. */
|
|
4
|
+
export declare function resolveDashboardLayout(layout: readonly DashboardGridItemProp[], columns: number, active?: string): DashboardGridItemProp[];
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const integer = (value, fallback) => Number.isFinite(value) ? Math.trunc(value) : fallback;
|
|
2
|
+
const gridColumns = (value) => Math.max(1, integer(value, 1));
|
|
3
|
+
const overlaps = (a, b) => a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
|
|
4
|
+
function resolveDashboardLayout(layout, columns, active) {
|
|
5
|
+
const count = gridColumns(columns);
|
|
6
|
+
const normalized = layout.map((item) => {
|
|
7
|
+
const w = Math.min(count, Math.max(1, integer(item.w, 1)));
|
|
8
|
+
return {
|
|
9
|
+
...item,
|
|
10
|
+
w,
|
|
11
|
+
h: Math.max(1, integer(item.h, 1)),
|
|
12
|
+
x: Math.max(0, Math.min(count - w, integer(item.x, 0))),
|
|
13
|
+
y: Math.max(0, integer(item.y, 0))
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
const sorted = normalized.slice().sort((a, b) => a.y - b.y || a.x - b.x);
|
|
17
|
+
const held = normalized.find((item) => item.id === active);
|
|
18
|
+
const placed = held ? [held] : [];
|
|
19
|
+
for (const item of sorted) {
|
|
20
|
+
if (item === held) continue;
|
|
21
|
+
let collision;
|
|
22
|
+
while (collision = placed.find((other) => overlaps(item, other)))
|
|
23
|
+
item.y = collision.y + collision.h;
|
|
24
|
+
while (item.y > 0 && !placed.some((other) => overlaps({ ...item, y: item.y - 1 }, other)))
|
|
25
|
+
item.y--;
|
|
26
|
+
placed.push(item);
|
|
27
|
+
}
|
|
28
|
+
if (held)
|
|
29
|
+
while (held.y > 0 && !placed.some((other) => other !== held && overlaps({ ...held, y: held.y - 1 }, other)))
|
|
30
|
+
held.y--;
|
|
31
|
+
return normalized;
|
|
32
|
+
}
|
|
33
|
+
export {
|
|
34
|
+
gridColumns,
|
|
35
|
+
resolveDashboardLayout
|
|
36
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { DashboardGridProp } from "../props/components/dashboard-grid.prop.js";
|
|
3
|
+
export type { DashboardGridItemProp, DashboardGridProp, DashboardGridProp as DashboardGridProps, } from "../props/components/dashboard-grid.prop.js";
|
|
4
|
+
/** Optional BI layout editor. DOM reading order remains items order; x starts at inline-start. */
|
|
5
|
+
export declare const DashboardGrid: React.ForwardRefExoticComponent<DashboardGridProp & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { GripVertical } from "lucide-react";
|
|
5
|
+
import { Card, CardHeader, CardTitle } from "../components/data-display/card.js";
|
|
6
|
+
import { Button } from "../components/general/button.js";
|
|
7
|
+
import { useTranslation } from "../i18n/use-translation.js";
|
|
8
|
+
import { useMediaQuery } from "../lib/hooks.js";
|
|
9
|
+
import { cn } from "../lib/utils.js";
|
|
10
|
+
import { gridColumns, resolveDashboardLayout } from "./dashboard-grid-layout.js";
|
|
11
|
+
const DashboardGrid = React.forwardRef(
|
|
12
|
+
function DashboardGrid2({
|
|
13
|
+
items,
|
|
14
|
+
layout,
|
|
15
|
+
defaultLayout,
|
|
16
|
+
onLayoutChange,
|
|
17
|
+
columns = 12,
|
|
18
|
+
rowHeight,
|
|
19
|
+
editable = false,
|
|
20
|
+
className,
|
|
21
|
+
...props
|
|
22
|
+
}, ref) {
|
|
23
|
+
const { t, locale } = useTranslation();
|
|
24
|
+
const sm = useMediaQuery("(min-width: 40rem)");
|
|
25
|
+
const md = useMediaQuery("(min-width: 48rem)");
|
|
26
|
+
const lg = useMediaQuery("(min-width: 64rem)");
|
|
27
|
+
const xl = useMediaQuery("(min-width: 80rem)");
|
|
28
|
+
const count = gridColumns(
|
|
29
|
+
typeof columns === "number" ? columns : xl && columns.xl || lg && columns.lg || md && columns.md || sm && columns.sm || columns.base
|
|
30
|
+
);
|
|
31
|
+
const [inner, setInner] = React.useState(defaultLayout ?? []);
|
|
32
|
+
const source = layout ?? inner;
|
|
33
|
+
const committed = resolveDashboardLayout(
|
|
34
|
+
items.map(
|
|
35
|
+
(item, index) => source.find((entry) => entry.id === item.id) ?? {
|
|
36
|
+
id: item.id,
|
|
37
|
+
x: 0,
|
|
38
|
+
y: index,
|
|
39
|
+
w: 1,
|
|
40
|
+
h: 1
|
|
41
|
+
}
|
|
42
|
+
),
|
|
43
|
+
count
|
|
44
|
+
);
|
|
45
|
+
const root = React.useRef(null);
|
|
46
|
+
const session = React.useRef(null);
|
|
47
|
+
const [preview, setPreview] = React.useState(null);
|
|
48
|
+
const [announcement, announce] = React.useState("");
|
|
49
|
+
const hint = React.useId();
|
|
50
|
+
const format = new Intl.NumberFormat(locale);
|
|
51
|
+
const speak = (action, entries, id) => {
|
|
52
|
+
const item = entries.find((entry) => entry.id === id);
|
|
53
|
+
if (!item) return;
|
|
54
|
+
announce(
|
|
55
|
+
t(`dashboardGrid.${action}`, {
|
|
56
|
+
label: items.find((entry) => entry.id === id)?.label ?? id,
|
|
57
|
+
x: format.format(item.x + 1),
|
|
58
|
+
y: format.format(item.y + 1),
|
|
59
|
+
w: format.format(item.w),
|
|
60
|
+
h: format.format(item.h)
|
|
61
|
+
})
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
const begin = (id) => {
|
|
65
|
+
const next = committed.map((item) => ({ ...item }));
|
|
66
|
+
session.current = { id, base: next, next };
|
|
67
|
+
setPreview(next);
|
|
68
|
+
speak("grabbed", next, id);
|
|
69
|
+
};
|
|
70
|
+
const finish = (cancel) => {
|
|
71
|
+
const current = session.current;
|
|
72
|
+
if (!current) return;
|
|
73
|
+
session.current = null;
|
|
74
|
+
setPreview(null);
|
|
75
|
+
speak(cancel ? "cancelled" : "dropped", cancel ? current.base : current.next, current.id);
|
|
76
|
+
if (!cancel && JSON.stringify(current.base) !== JSON.stringify(current.next)) {
|
|
77
|
+
if (layout === void 0) setInner(current.next);
|
|
78
|
+
onLayoutChange?.(current.next);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
const update = (dx, dy, resize, fromBase = false) => {
|
|
82
|
+
const current = session.current;
|
|
83
|
+
if (!current) return;
|
|
84
|
+
current.next = resolveDashboardLayout(
|
|
85
|
+
(fromBase ? current.base : current.next).map(
|
|
86
|
+
(item) => item.id !== current.id ? item : resize ? { ...item, w: item.w + dx, h: item.h + dy } : { ...item, x: item.x + dx, y: item.y + dy }
|
|
87
|
+
),
|
|
88
|
+
count,
|
|
89
|
+
current.id
|
|
90
|
+
);
|
|
91
|
+
setPreview(current.next);
|
|
92
|
+
speak("moved", current.next, current.id);
|
|
93
|
+
};
|
|
94
|
+
React.useEffect(() => {
|
|
95
|
+
session.current = null;
|
|
96
|
+
setPreview(null);
|
|
97
|
+
}, [layout, columns, count, editable, items]);
|
|
98
|
+
const latest = React.useRef({ finish, update });
|
|
99
|
+
latest.current = { finish, update };
|
|
100
|
+
React.useEffect(() => {
|
|
101
|
+
const move = (event) => {
|
|
102
|
+
const pointer = session.current?.pointer;
|
|
103
|
+
if (!pointer || event.pointerId !== pointer.id) return;
|
|
104
|
+
event.preventDefault();
|
|
105
|
+
latest.current.update(
|
|
106
|
+
Math.round((event.clientX - pointer.x) / pointer.stepX) * (pointer.rtl ? -1 : 1),
|
|
107
|
+
Math.round((event.clientY - pointer.y) / pointer.stepY),
|
|
108
|
+
pointer.resize,
|
|
109
|
+
true
|
|
110
|
+
);
|
|
111
|
+
};
|
|
112
|
+
const end = (event) => {
|
|
113
|
+
if (session.current?.pointer?.id === event.pointerId)
|
|
114
|
+
latest.current.finish(event.type === "pointercancel");
|
|
115
|
+
};
|
|
116
|
+
const escape = (event) => {
|
|
117
|
+
if (event.key === "Escape" && session.current) {
|
|
118
|
+
event.preventDefault();
|
|
119
|
+
latest.current.finish(true);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
window.addEventListener("pointermove", move, { passive: false });
|
|
123
|
+
window.addEventListener("pointerup", end);
|
|
124
|
+
window.addEventListener("pointercancel", end);
|
|
125
|
+
window.addEventListener("keydown", escape);
|
|
126
|
+
return () => {
|
|
127
|
+
window.removeEventListener("pointermove", move);
|
|
128
|
+
window.removeEventListener("pointerup", end);
|
|
129
|
+
window.removeEventListener("pointercancel", end);
|
|
130
|
+
window.removeEventListener("keydown", escape);
|
|
131
|
+
};
|
|
132
|
+
}, []);
|
|
133
|
+
const pointerDown = (id, resize) => (event) => {
|
|
134
|
+
if (!editable || event.button !== 0 || session.current || !root.current) return;
|
|
135
|
+
event.preventDefault();
|
|
136
|
+
event.currentTarget.focus();
|
|
137
|
+
const css = getComputedStyle(root.current);
|
|
138
|
+
const gap = parseFloat(css.columnGap) || 0;
|
|
139
|
+
begin(id);
|
|
140
|
+
session.current.pointer = {
|
|
141
|
+
id: event.pointerId,
|
|
142
|
+
x: event.clientX,
|
|
143
|
+
y: event.clientY,
|
|
144
|
+
stepX: (root.current.clientWidth + gap) / count,
|
|
145
|
+
stepY: (parseFloat(css.gridAutoRows) || rowHeight || 80) + (parseFloat(css.rowGap) || 0),
|
|
146
|
+
rtl: css.direction === "rtl",
|
|
147
|
+
resize
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
const keyDown = (id) => (event) => {
|
|
151
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
152
|
+
event.preventDefault();
|
|
153
|
+
if (!session.current) begin(id);
|
|
154
|
+
else if (session.current.id === id) finish(false);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (session.current?.id !== id || !event.key.startsWith("Arrow")) return;
|
|
158
|
+
event.preventDefault();
|
|
159
|
+
const rtl = root.current && getComputedStyle(root.current).direction === "rtl";
|
|
160
|
+
update(
|
|
161
|
+
(event.key === "ArrowRight" ? 1 : event.key === "ArrowLeft" ? -1 : 0) * (rtl ? -1 : 1),
|
|
162
|
+
event.key === "ArrowDown" ? 1 : event.key === "ArrowUp" ? -1 : 0,
|
|
163
|
+
event.shiftKey
|
|
164
|
+
);
|
|
165
|
+
};
|
|
166
|
+
const shown = preview ?? committed;
|
|
167
|
+
return /* @__PURE__ */ jsxs("div", { ...props, ref, className: cn("ui-dashboard-grid", className), children: [
|
|
168
|
+
/* @__PURE__ */ jsx(
|
|
169
|
+
"div",
|
|
170
|
+
{
|
|
171
|
+
ref: root,
|
|
172
|
+
className: "ui-dashboard-grid-items",
|
|
173
|
+
"data-stacked": count === 1 || void 0,
|
|
174
|
+
style: {
|
|
175
|
+
"--dashboard-grid-columns": count,
|
|
176
|
+
...rowHeight && Number.isFinite(rowHeight) && rowHeight > 0 ? { "--dashboard-grid-row-height": `${rowHeight}px` } : {}
|
|
177
|
+
},
|
|
178
|
+
children: items.map((item) => {
|
|
179
|
+
const box = shown.find((entry) => entry.id === item.id) ?? committed.find((entry) => entry.id === item.id);
|
|
180
|
+
const held = session.current?.id === item.id;
|
|
181
|
+
return /* @__PURE__ */ jsx(
|
|
182
|
+
"section",
|
|
183
|
+
{
|
|
184
|
+
"aria-label": item.label,
|
|
185
|
+
className: "ui-dashboard-grid-item",
|
|
186
|
+
"data-dashboard-id": item.id,
|
|
187
|
+
"data-x": box.x,
|
|
188
|
+
"data-y": box.y,
|
|
189
|
+
"data-w": box.w,
|
|
190
|
+
"data-h": box.h,
|
|
191
|
+
"data-editable": editable || void 0,
|
|
192
|
+
"data-held": held || void 0,
|
|
193
|
+
style: {
|
|
194
|
+
gridColumn: `${box.x + 1} / span ${box.w}`,
|
|
195
|
+
gridRow: count === 1 ? "auto" : `${box.y + 1} / span ${box.h}`
|
|
196
|
+
},
|
|
197
|
+
children: /* @__PURE__ */ jsxs(Card, { className: "ui-dashboard-grid-surface", children: [
|
|
198
|
+
/* @__PURE__ */ jsx(CardHeader, { className: "ui-dashboard-grid-header", children: /* @__PURE__ */ jsx(CardTitle, { children: item.label }) }),
|
|
199
|
+
editable && /* @__PURE__ */ jsx(
|
|
200
|
+
Button,
|
|
201
|
+
{
|
|
202
|
+
variant: "ghost",
|
|
203
|
+
size: "icon-sm",
|
|
204
|
+
className: "ui-dashboard-grid-move",
|
|
205
|
+
"aria-label": t("dashboardGrid.move", { label: item.label }),
|
|
206
|
+
"aria-describedby": hint,
|
|
207
|
+
"aria-pressed": held,
|
|
208
|
+
onPointerDown: pointerDown(item.id, false),
|
|
209
|
+
onKeyDown: keyDown(item.id),
|
|
210
|
+
onBlur: () => {
|
|
211
|
+
if (!session.current?.pointer) finish(true);
|
|
212
|
+
},
|
|
213
|
+
children: /* @__PURE__ */ jsx(GripVertical, { "aria-hidden": "true" })
|
|
214
|
+
}
|
|
215
|
+
),
|
|
216
|
+
/* @__PURE__ */ jsx("div", { className: "ui-dashboard-grid-content", children: item.content }),
|
|
217
|
+
editable && /* @__PURE__ */ jsx(
|
|
218
|
+
Button,
|
|
219
|
+
{
|
|
220
|
+
variant: "ghost",
|
|
221
|
+
size: "icon-sm",
|
|
222
|
+
className: "ui-dashboard-grid-resize",
|
|
223
|
+
"aria-label": t("dashboardGrid.resize", { label: item.label }),
|
|
224
|
+
"aria-describedby": hint,
|
|
225
|
+
"aria-pressed": held,
|
|
226
|
+
onPointerDown: pointerDown(item.id, true),
|
|
227
|
+
onKeyDown: keyDown(item.id),
|
|
228
|
+
onBlur: () => {
|
|
229
|
+
if (!session.current?.pointer) finish(true);
|
|
230
|
+
},
|
|
231
|
+
children: /* @__PURE__ */ jsx(
|
|
232
|
+
"svg",
|
|
233
|
+
{
|
|
234
|
+
viewBox: "0 0 24 24",
|
|
235
|
+
fill: "none",
|
|
236
|
+
stroke: "currentColor",
|
|
237
|
+
strokeWidth: "1.5",
|
|
238
|
+
"aria-hidden": "true",
|
|
239
|
+
children: /* @__PURE__ */ jsx("path", { d: "M8 20 20 8M14 20 20 14" })
|
|
240
|
+
}
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
)
|
|
244
|
+
] })
|
|
245
|
+
},
|
|
246
|
+
item.id
|
|
247
|
+
);
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
),
|
|
251
|
+
/* @__PURE__ */ jsx("span", { id: hint, className: "sr-only", children: t("dashboardGrid.hint") }),
|
|
252
|
+
/* @__PURE__ */ jsx("span", { role: "status", "aria-live": "polite", "aria-atomic": "true", className: "sr-only", children: announcement })
|
|
253
|
+
] });
|
|
254
|
+
}
|
|
255
|
+
);
|
|
256
|
+
export {
|
|
257
|
+
DashboardGrid
|
|
258
|
+
};
|
package/dist/lab/index.d.ts
CHANGED
|
@@ -37,3 +37,5 @@ export { MegaMenu } from "./mega-menu.js";
|
|
|
37
37
|
export type { MegaMenuProp, MegaMenuProps, MegaMenuItemProp, MegaMenuItemProps, MegaMenuPanelProp, MegaMenuPanelProps, MegaMenuGroupProp, MegaMenuGroupProps, MegaMenuLinkProp, MegaMenuLinkProps, MegaMenuLinkComponentProp, MegaMenuLinkComponentProps, MegaMenuTriggerActionProp, MegaMenuTriggerActionProps, } from "./mega-menu.js";
|
|
38
38
|
export { Anchor } from "./anchor.js";
|
|
39
39
|
export type { AnchorContainerProp, AnchorDirectionProp, AnchorItemProp, AnchorProp, AnchorProps, } from "./anchor.js";
|
|
40
|
+
export { DashboardGrid } from "./dashboard-grid.js";
|
|
41
|
+
export type { DashboardGridProp, DashboardGridProps, DashboardGridItemProp } from "./dashboard-grid.js";
|
package/dist/lab/index.js
CHANGED
|
@@ -21,6 +21,7 @@ import { EmojiPicker } from "./emoji-picker.js";
|
|
|
21
21
|
import { SortableList } from "./sortable-list.js";
|
|
22
22
|
import { MegaMenu } from "./mega-menu.js";
|
|
23
23
|
import { Anchor } from "./anchor.js";
|
|
24
|
+
import { DashboardGrid } from "./dashboard-grid.js";
|
|
24
25
|
export {
|
|
25
26
|
Anchor,
|
|
26
27
|
Carousel,
|
|
@@ -29,6 +30,7 @@ export {
|
|
|
29
30
|
CarouselItem,
|
|
30
31
|
CarouselNext,
|
|
31
32
|
CarouselPrevious,
|
|
33
|
+
DashboardGrid,
|
|
32
34
|
DraggablePanel,
|
|
33
35
|
EmojiPicker,
|
|
34
36
|
FloatButton,
|
|
@@ -21,6 +21,11 @@ export type ChartSeriesProp = {
|
|
|
21
21
|
*/
|
|
22
22
|
fillColor?: string;
|
|
23
23
|
};
|
|
24
|
+
/** Selection is an action: the dashboard owns filter state. Index refers to the original data array. */
|
|
25
|
+
export type ChartDatumSelect<T = ChartDatum> = (datum: T, context: {
|
|
26
|
+
seriesKey: string;
|
|
27
|
+
index: number;
|
|
28
|
+
}) => void;
|
|
24
29
|
/** Shared base for the cartesian charts (Line / Bar / Area). */
|
|
25
30
|
type ChartCartesianBase = {
|
|
26
31
|
/** Row data. Each row is one category with a value per series. */
|
|
@@ -69,6 +74,8 @@ type ChartCartesianBase = {
|
|
|
69
74
|
numberFormat?: Intl.NumberFormatOptions;
|
|
70
75
|
/** Message shown when `data` is empty. Defaults to a localized "no data". */
|
|
71
76
|
emptyMessage?: EmptyMessageProp;
|
|
77
|
+
/** Select a finite datum by pointer or Enter/Space. Arrows navigate categories and series. */
|
|
78
|
+
onDatumSelect?: ChartDatumSelect;
|
|
72
79
|
/**
|
|
73
80
|
* Called with the clicked category's datum and its index — drill-down, zoom-to-bucket. The whole
|
|
74
81
|
* category column is the target, not only the mark, so a zero-height bar is still clickable.
|
|
@@ -161,6 +168,8 @@ export type HeatmapDatum = {
|
|
|
161
168
|
* steps through the `--chart-heat-1..5` ramp; an empty cell is outlined, a zero cell is filled.
|
|
162
169
|
*/
|
|
163
170
|
export type HeatmapChartProp = {
|
|
171
|
+
/** Select a present cell. seriesKey is String(column); index is the original data index. */
|
|
172
|
+
onDatumSelect?: ChartDatumSelect<HeatmapDatum>;
|
|
164
173
|
/** Row categories, top to bottom (the y axis — weekdays). */
|
|
165
174
|
rows: HeatmapCategory[];
|
|
166
175
|
/** Column categories, start to end (the x axis — hours). */
|
|
@@ -207,6 +216,8 @@ export type AreaChartProp = ChartCartesianBase & {
|
|
|
207
216
|
};
|
|
208
217
|
/** @see PieChart */
|
|
209
218
|
export type PieChartProp = {
|
|
219
|
+
/** Select a slice by pointer or Enter/Space; seriesKey is dataKey. Arrows navigate slices. */
|
|
220
|
+
onDatumSelect?: ChartDatumSelect;
|
|
210
221
|
/** Row data — one slice per row. */
|
|
211
222
|
data: ChartDatum[];
|
|
212
223
|
/** Key into each datum holding the slice's numeric value. */
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
import type { BreakpointProp } from "../vocabulary/index.js";
|
|
3
|
+
/** Integer grid coordinates, zero-based from inline-start. Stable id owns persistence. */
|
|
4
|
+
export interface DashboardGridItemProp {
|
|
5
|
+
id: string;
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
w: number;
|
|
9
|
+
h: number;
|
|
10
|
+
}
|
|
11
|
+
export interface DashboardGridProp extends Omit<React.HTMLAttributes<HTMLDivElement>, "children"> {
|
|
12
|
+
/** Each tile owns its Card and visible label header; content is the body (e.g. CardContent). */
|
|
13
|
+
items: {
|
|
14
|
+
id: string;
|
|
15
|
+
label: string;
|
|
16
|
+
content: React.ReactNode;
|
|
17
|
+
}[];
|
|
18
|
+
layout?: DashboardGridItemProp[];
|
|
19
|
+
defaultLayout?: DashboardGridItemProp[];
|
|
20
|
+
onLayoutChange?: (layout: DashboardGridItemProp[]) => void;
|
|
21
|
+
columns?: number | ({
|
|
22
|
+
base: number;
|
|
23
|
+
} & Partial<Record<BreakpointProp, number>>);
|
|
24
|
+
/** Row track size in pixels; omitted reads --dashboard-grid-row-height. */
|
|
25
|
+
rowHeight?: number;
|
|
26
|
+
editable?: boolean;
|
|
27
|
+
}
|
|
File without changes
|