@arqel-dev/ui 0.11.0 → 0.13.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/SKILL.md +5 -2
- package/dist/AreaChart-NMQ2RQGA.js +33 -0
- package/dist/AreaChart-NMQ2RQGA.js.map +1 -0
- package/dist/ArqelCreatePage-P3G6TKIK.js +8 -0
- package/dist/ArqelCreatePage-P3G6TKIK.js.map +1 -0
- package/dist/ArqelDashboardPage-PX3Y75AA.js +6 -0
- package/dist/ArqelDashboardPage-PX3Y75AA.js.map +1 -0
- package/dist/ArqelEditPage-HKQSE6E5.js +8 -0
- package/dist/ArqelEditPage-HKQSE6E5.js.map +1 -0
- package/dist/ArqelIndexPage-MRH3572Z.js +12 -0
- package/dist/ArqelIndexPage-MRH3572Z.js.map +1 -0
- package/dist/ArqelShowPage-4FS5MTUN.js +6 -0
- package/dist/ArqelShowPage-4FS5MTUN.js.map +1 -0
- package/dist/BarChart-KLHKTLO2.js +19 -0
- package/dist/BarChart-KLHKTLO2.js.map +1 -0
- package/dist/DonutChart-L56R4QF2.js +11 -0
- package/dist/DonutChart-L56R4QF2.js.map +1 -0
- package/dist/LineChart-GHMPHFZZ.js +30 -0
- package/dist/LineChart-GHMPHFZZ.js.map +1 -0
- package/dist/PieChart-T2VQOFPP.js +4 -0
- package/dist/PieChart-T2VQOFPP.js.map +1 -0
- package/dist/RadarChart-CSWR5AO7.js +32 -0
- package/dist/RadarChart-CSWR5AO7.js.map +1 -0
- package/dist/Topbar-DffcNdcJ.d.ts +50 -0
- package/dist/action.d.ts +12 -2
- package/dist/action.js +6 -1090
- package/dist/action.js.map +1 -1
- package/dist/auth.js +1 -10
- package/dist/auth.js.map +1 -1
- package/dist/chunk-2LTZJ4R7.js +110 -0
- package/dist/chunk-2LTZJ4R7.js.map +1 -0
- package/dist/chunk-3JJOHYD7.js +57 -0
- package/dist/chunk-3JJOHYD7.js.map +1 -0
- package/dist/chunk-47HD7ALI.js +370 -0
- package/dist/chunk-47HD7ALI.js.map +1 -0
- package/dist/chunk-6UJQP7KU.js +737 -0
- package/dist/chunk-6UJQP7KU.js.map +1 -0
- package/dist/chunk-7LSZADUC.js +50 -0
- package/dist/chunk-7LSZADUC.js.map +1 -0
- package/dist/chunk-B7ULUJVO.js +11 -0
- package/dist/chunk-B7ULUJVO.js.map +1 -0
- package/dist/chunk-C5H44NSP.js +37 -0
- package/dist/chunk-C5H44NSP.js.map +1 -0
- package/dist/chunk-DSULVRB6.js +101 -0
- package/dist/chunk-DSULVRB6.js.map +1 -0
- package/dist/chunk-ERT7LL6C.js +199 -0
- package/dist/chunk-ERT7LL6C.js.map +1 -0
- package/dist/chunk-EYAUPJ6J.js +64 -0
- package/dist/chunk-EYAUPJ6J.js.map +1 -0
- package/dist/chunk-FHRQ7PWE.js +58 -0
- package/dist/chunk-FHRQ7PWE.js.map +1 -0
- package/dist/chunk-FV7RESKL.js +285 -0
- package/dist/chunk-FV7RESKL.js.map +1 -0
- package/dist/chunk-K7QY56XN.js +610 -0
- package/dist/chunk-K7QY56XN.js.map +1 -0
- package/dist/chunk-L3JBBN4H.js +27 -0
- package/dist/chunk-L3JBBN4H.js.map +1 -0
- package/dist/chunk-MSRTGCTJ.js +597 -0
- package/dist/chunk-MSRTGCTJ.js.map +1 -0
- package/dist/chunk-MTNFFSVA.js +92 -0
- package/dist/chunk-MTNFFSVA.js.map +1 -0
- package/dist/chunk-MTZJDFQ6.js +32 -0
- package/dist/chunk-MTZJDFQ6.js.map +1 -0
- package/dist/chunk-MXUXMHOK.js +16 -0
- package/dist/chunk-MXUXMHOK.js.map +1 -0
- package/dist/chunk-PXON56HL.js +35 -0
- package/dist/chunk-PXON56HL.js.map +1 -0
- package/dist/chunk-PZEVKMZX.js +598 -0
- package/dist/chunk-PZEVKMZX.js.map +1 -0
- package/dist/chunk-R4BOUU6Y.js +55 -0
- package/dist/chunk-R4BOUU6Y.js.map +1 -0
- package/dist/chunk-RMPOR2YZ.js +12 -0
- package/dist/chunk-RMPOR2YZ.js.map +1 -0
- package/dist/chunk-RNAMHQM6.js +12 -0
- package/dist/chunk-RNAMHQM6.js.map +1 -0
- package/dist/chunk-TNJEHYRF.js +387 -0
- package/dist/chunk-TNJEHYRF.js.map +1 -0
- package/dist/chunk-VSTRVXXZ.js +49 -0
- package/dist/chunk-VSTRVXXZ.js.map +1 -0
- package/dist/chunk-WKWV36PG.js +140 -0
- package/dist/chunk-WKWV36PG.js.map +1 -0
- package/dist/cn-By8XWiSA.d.ts +12 -0
- package/dist/flash.js +2 -104
- package/dist/flash.js.map +1 -1
- package/dist/form.js +4 -673
- package/dist/form.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +18 -3706
- package/dist/index.js.map +1 -1
- package/dist/pages.js +23 -3025
- package/dist/pages.js.map +1 -1
- package/dist/palette.js +1 -283
- package/dist/palette.js.map +1 -1
- package/dist/primitives.js +3 -658
- package/dist/primitives.js.map +1 -1
- package/dist/resource.js +5 -720
- package/dist/resource.js.map +1 -1
- package/dist/shell.d.ts +26 -46
- package/dist/shell.js +46 -1039
- package/dist/shell.js.map +1 -1
- package/dist/table.js +4 -618
- package/dist/table.js.map +1 -1
- package/dist/utility.js +3 -104
- package/dist/utility.js.map +1 -1
- package/dist/utils.d.ts +12 -6
- package/dist/utils.js +2 -9
- package/dist/utils.js.map +1 -1
- package/dist/widgets.d.ts +7 -2
- package/dist/widgets.js +3 -675
- package/dist/widgets.js.map +1 -1
- package/package.json +15 -14
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import { WidgetWrapper, StatCard } from './chunk-WKWV36PG.js';
|
|
2
|
+
import { cn } from './chunk-B7ULUJVO.js';
|
|
3
|
+
import { lazy, Suspense, useState, useRef, useEffect } from 'react';
|
|
4
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
5
|
+
|
|
6
|
+
var LineChart = lazy(() => import('./LineChart-GHMPHFZZ.js').then((m) => ({ default: m.LineChart })));
|
|
7
|
+
var BarChart = lazy(() => import('./BarChart-KLHKTLO2.js').then((m) => ({ default: m.BarChart })));
|
|
8
|
+
var AreaChart = lazy(() => import('./AreaChart-NMQ2RQGA.js').then((m) => ({ default: m.AreaChart })));
|
|
9
|
+
var PieChart = lazy(() => import('./PieChart-T2VQOFPP.js').then((m) => ({ default: m.PieChart })));
|
|
10
|
+
var DonutChart = lazy(
|
|
11
|
+
() => import('./DonutChart-L56R4QF2.js').then((m) => ({ default: m.DonutChart }))
|
|
12
|
+
);
|
|
13
|
+
var RadarChart = lazy(
|
|
14
|
+
() => import('./RadarChart-CSWR5AO7.js').then((m) => ({ default: m.RadarChart }))
|
|
15
|
+
);
|
|
16
|
+
var REGISTRY = {
|
|
17
|
+
line: LineChart,
|
|
18
|
+
bar: BarChart,
|
|
19
|
+
area: AreaChart,
|
|
20
|
+
pie: PieChart,
|
|
21
|
+
donut: DonutChart,
|
|
22
|
+
radar: RadarChart
|
|
23
|
+
};
|
|
24
|
+
function ChartCard({ widget }) {
|
|
25
|
+
const Chart = REGISTRY[widget.chartType] ?? LineChart;
|
|
26
|
+
return /* @__PURE__ */ jsx(WidgetWrapper, { heading: widget.heading, description: widget.description, children: /* @__PURE__ */ jsx(
|
|
27
|
+
Suspense,
|
|
28
|
+
{
|
|
29
|
+
fallback: /* @__PURE__ */ jsx(
|
|
30
|
+
"div",
|
|
31
|
+
{
|
|
32
|
+
role: "status",
|
|
33
|
+
"aria-label": "Loading chart",
|
|
34
|
+
"data-testid": "chart-fallback",
|
|
35
|
+
className: "animate-pulse rounded bg-muted",
|
|
36
|
+
style: { height: widget.height }
|
|
37
|
+
}
|
|
38
|
+
),
|
|
39
|
+
children: /* @__PURE__ */ jsx(
|
|
40
|
+
Chart,
|
|
41
|
+
{
|
|
42
|
+
chartData: widget.chartData,
|
|
43
|
+
chartOptions: widget.chartOptions,
|
|
44
|
+
height: widget.height,
|
|
45
|
+
showLegend: widget.showLegend,
|
|
46
|
+
showGrid: widget.showGrid
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
) });
|
|
51
|
+
}
|
|
52
|
+
function DashboardFilters({ filters, values, onChange, className }) {
|
|
53
|
+
if (!filters || filters.length === 0) return null;
|
|
54
|
+
return /* @__PURE__ */ jsx("div", { className: cn("flex flex-wrap gap-2", className), "data-testid": "dashboard-filters", children: filters.map((filter) => /* @__PURE__ */ jsx(
|
|
55
|
+
FilterControl,
|
|
56
|
+
{
|
|
57
|
+
filter,
|
|
58
|
+
value: values[filter.name],
|
|
59
|
+
onChange: (v) => onChange(filter.name, v)
|
|
60
|
+
},
|
|
61
|
+
filter.name
|
|
62
|
+
)) });
|
|
63
|
+
}
|
|
64
|
+
function FilterControl({
|
|
65
|
+
filter,
|
|
66
|
+
value,
|
|
67
|
+
onChange
|
|
68
|
+
}) {
|
|
69
|
+
if (filter.type === "select") {
|
|
70
|
+
return /* @__PURE__ */ jsx(SelectControl, { filter, value, onChange });
|
|
71
|
+
}
|
|
72
|
+
if (filter.type === "date_range") {
|
|
73
|
+
return /* @__PURE__ */ jsx(
|
|
74
|
+
DateRangeControl,
|
|
75
|
+
{
|
|
76
|
+
filter,
|
|
77
|
+
value,
|
|
78
|
+
onChange
|
|
79
|
+
}
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
function controlClasses() {
|
|
85
|
+
return cn(
|
|
86
|
+
"h-9 rounded-sm border border-[var(--input)]",
|
|
87
|
+
"bg-background px-2 text-sm",
|
|
88
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
function normaliseOptions(raw) {
|
|
92
|
+
if (!raw) return [];
|
|
93
|
+
if (Array.isArray(raw)) {
|
|
94
|
+
return raw.map((o) => ({ value: String(o.value), label: String(o.label ?? o.value) }));
|
|
95
|
+
}
|
|
96
|
+
return Object.entries(raw).map(([k, v]) => ({ value: String(k), label: String(v) }));
|
|
97
|
+
}
|
|
98
|
+
function SelectControl({
|
|
99
|
+
filter,
|
|
100
|
+
value,
|
|
101
|
+
onChange
|
|
102
|
+
}) {
|
|
103
|
+
const options = normaliseOptions(filter.options);
|
|
104
|
+
const label = filter.label ?? filter.name;
|
|
105
|
+
if (filter.multiple) {
|
|
106
|
+
const selected = Array.isArray(value) ? value.map(String) : [];
|
|
107
|
+
return /* @__PURE__ */ jsxs("label", { className: "flex flex-col gap-1 text-xs text-muted-foreground", children: [
|
|
108
|
+
label,
|
|
109
|
+
/* @__PURE__ */ jsx(
|
|
110
|
+
"select",
|
|
111
|
+
{
|
|
112
|
+
multiple: true,
|
|
113
|
+
"aria-label": label,
|
|
114
|
+
className: cn(controlClasses(), "h-auto min-h-[2.25rem] py-1"),
|
|
115
|
+
value: selected,
|
|
116
|
+
onChange: (e) => {
|
|
117
|
+
const next = Array.from(e.target.selectedOptions, (o) => o.value);
|
|
118
|
+
onChange(next.length === 0 ? null : next);
|
|
119
|
+
},
|
|
120
|
+
children: options.map((opt) => /* @__PURE__ */ jsx("option", { value: opt.value, children: opt.label }, opt.value))
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
] });
|
|
124
|
+
}
|
|
125
|
+
return /* @__PURE__ */ jsxs("label", { className: "flex flex-col gap-1 text-xs text-muted-foreground", children: [
|
|
126
|
+
label,
|
|
127
|
+
/* @__PURE__ */ jsxs(
|
|
128
|
+
"select",
|
|
129
|
+
{
|
|
130
|
+
"aria-label": label,
|
|
131
|
+
className: controlClasses(),
|
|
132
|
+
value: value === void 0 || value === null ? "" : String(value),
|
|
133
|
+
onChange: (e) => onChange(e.target.value === "" ? null : e.target.value),
|
|
134
|
+
children: [
|
|
135
|
+
/* @__PURE__ */ jsx("option", { value: "", children: "All" }),
|
|
136
|
+
options.map((opt) => /* @__PURE__ */ jsx("option", { value: opt.value, children: opt.label }, opt.value))
|
|
137
|
+
]
|
|
138
|
+
}
|
|
139
|
+
)
|
|
140
|
+
] });
|
|
141
|
+
}
|
|
142
|
+
function DateRangeControl({
|
|
143
|
+
filter,
|
|
144
|
+
value,
|
|
145
|
+
onChange
|
|
146
|
+
}) {
|
|
147
|
+
const range = value && typeof value === "object" ? value : {};
|
|
148
|
+
const fromValue = typeof range.from === "string" ? range.from : "";
|
|
149
|
+
const toValue = typeof range.to === "string" ? range.to : "";
|
|
150
|
+
return /* @__PURE__ */ jsxs("fieldset", { className: "flex flex-col gap-1 text-xs text-muted-foreground", children: [
|
|
151
|
+
/* @__PURE__ */ jsx("legend", { children: filter.label ?? filter.name }),
|
|
152
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
153
|
+
/* @__PURE__ */ jsx(
|
|
154
|
+
"input",
|
|
155
|
+
{
|
|
156
|
+
type: "date",
|
|
157
|
+
"aria-label": `${filter.label ?? filter.name} from`,
|
|
158
|
+
className: controlClasses(),
|
|
159
|
+
value: fromValue,
|
|
160
|
+
onChange: (e) => onChange({ from: e.target.value || null, to: range.to ?? null })
|
|
161
|
+
}
|
|
162
|
+
),
|
|
163
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\u2013" }),
|
|
164
|
+
/* @__PURE__ */ jsx(
|
|
165
|
+
"input",
|
|
166
|
+
{
|
|
167
|
+
type: "date",
|
|
168
|
+
"aria-label": `${filter.label ?? filter.name} to`,
|
|
169
|
+
className: controlClasses(),
|
|
170
|
+
value: toValue,
|
|
171
|
+
onChange: (e) => onChange({ from: range.from ?? null, to: e.target.value || null })
|
|
172
|
+
}
|
|
173
|
+
)
|
|
174
|
+
] })
|
|
175
|
+
] });
|
|
176
|
+
}
|
|
177
|
+
function TableCard({ widget, className }) {
|
|
178
|
+
return /* @__PURE__ */ jsx(
|
|
179
|
+
WidgetWrapper,
|
|
180
|
+
{
|
|
181
|
+
heading: widget.heading ?? void 0,
|
|
182
|
+
description: widget.description ?? void 0,
|
|
183
|
+
className,
|
|
184
|
+
children: widget.loadError ? /* @__PURE__ */ jsx("div", { role: "alert", className: "text-sm text-red-600", children: widget.loadError }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
185
|
+
/* @__PURE__ */ jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxs("table", { className: cn("w-full border-collapse text-sm", "text-foreground"), children: [
|
|
186
|
+
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { className: "border-b border-border text-left", children: widget.columns.map((column) => /* @__PURE__ */ jsx(
|
|
187
|
+
"th",
|
|
188
|
+
{
|
|
189
|
+
scope: "col",
|
|
190
|
+
className: "px-2 py-1.5 font-medium text-muted-foreground",
|
|
191
|
+
children: column.label ?? column.name
|
|
192
|
+
},
|
|
193
|
+
column.name
|
|
194
|
+
)) }) }),
|
|
195
|
+
/* @__PURE__ */ jsx("tbody", { children: widget.records.map((record, rowIndex) => /* @__PURE__ */ jsx(
|
|
196
|
+
"tr",
|
|
197
|
+
{
|
|
198
|
+
className: "border-b border-border last:border-0",
|
|
199
|
+
children: widget.columns.map((column) => /* @__PURE__ */ jsx("td", { className: "px-2 py-1.5", children: formatCell(record[column.name]) }, column.name))
|
|
200
|
+
},
|
|
201
|
+
rowIndex
|
|
202
|
+
)) })
|
|
203
|
+
] }) }),
|
|
204
|
+
widget.seeAllUrl && /* @__PURE__ */ jsx("div", { className: "mt-3 text-right", children: /* @__PURE__ */ jsx("a", { href: widget.seeAllUrl, className: "text-sm text-primary", children: "See all \u2192" }) })
|
|
205
|
+
] })
|
|
206
|
+
}
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
function formatCell(value) {
|
|
210
|
+
if (value === null || value === void 0) return "";
|
|
211
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
212
|
+
return String(value);
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
return JSON.stringify(value);
|
|
216
|
+
} catch {
|
|
217
|
+
return "";
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function WidgetRenderer({ widget, filterValues, fetcher }) {
|
|
221
|
+
const [data, setData] = useState(widget.data ?? null);
|
|
222
|
+
const filterRef = useRef(filterValues);
|
|
223
|
+
filterRef.current = filterValues;
|
|
224
|
+
const dashboardId = widget.dashboardId;
|
|
225
|
+
const widgetId = widget.widgetId ?? widget.id ?? widget.name;
|
|
226
|
+
const poll = typeof widget.poll === "number" ? widget.poll : 0;
|
|
227
|
+
const deferred = widget.deferred === true;
|
|
228
|
+
const hasInlineDataRef = useRef(widget.data !== null && widget.data !== void 0);
|
|
229
|
+
useEffect(() => {
|
|
230
|
+
if (!dashboardId || !widgetId) return void 0;
|
|
231
|
+
const baseUrl = `/admin/dashboards/${dashboardId}/widgets/${widgetId}/data`;
|
|
232
|
+
const doFetch = async () => {
|
|
233
|
+
try {
|
|
234
|
+
const url = withFilters(baseUrl, filterRef.current);
|
|
235
|
+
const body = fetcher ? await fetcher(url) : await fetch(url, { headers: { Accept: "application/json" } }).then((r) => r.json());
|
|
236
|
+
const result = body && typeof body === "object" && "data" in body ? body["data"] : body;
|
|
237
|
+
setData(result);
|
|
238
|
+
} catch {
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
let interval;
|
|
242
|
+
if (deferred && !hasInlineDataRef.current) {
|
|
243
|
+
void doFetch();
|
|
244
|
+
}
|
|
245
|
+
if (poll > 0) {
|
|
246
|
+
interval = setInterval(() => {
|
|
247
|
+
void doFetch();
|
|
248
|
+
}, poll * 1e3);
|
|
249
|
+
}
|
|
250
|
+
return () => {
|
|
251
|
+
if (interval) clearInterval(interval);
|
|
252
|
+
};
|
|
253
|
+
}, [dashboardId, widgetId, poll, deferred, fetcher]);
|
|
254
|
+
const merged = mergeData(widget, data);
|
|
255
|
+
switch (widget.type) {
|
|
256
|
+
case "stat":
|
|
257
|
+
return /* @__PURE__ */ jsx(StatCard, { widget: merged });
|
|
258
|
+
case "chart":
|
|
259
|
+
return /* @__PURE__ */ jsx(ChartCard, { widget: merged });
|
|
260
|
+
case "table":
|
|
261
|
+
return /* @__PURE__ */ jsx(TableCard, { widget: merged });
|
|
262
|
+
default:
|
|
263
|
+
return /* @__PURE__ */ jsxs("div", { role: "alert", className: "text-sm text-red-600", children: [
|
|
264
|
+
"Widget type ",
|
|
265
|
+
widget.type,
|
|
266
|
+
" not registered"
|
|
267
|
+
] });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function withFilters(baseUrl, filters) {
|
|
271
|
+
if (!filters) return baseUrl;
|
|
272
|
+
const params = new URLSearchParams();
|
|
273
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
274
|
+
appendFilterValue(params, `filters[${key}]`, value);
|
|
275
|
+
}
|
|
276
|
+
const query = params.toString();
|
|
277
|
+
return query ? `${baseUrl}?${query}` : baseUrl;
|
|
278
|
+
}
|
|
279
|
+
function appendFilterValue(params, prefix, value) {
|
|
280
|
+
if (value === null || value === void 0 || value === "") return;
|
|
281
|
+
if (Array.isArray(value)) {
|
|
282
|
+
for (const el of value) {
|
|
283
|
+
appendFilterValue(params, `${prefix}[]`, el);
|
|
284
|
+
}
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (typeof value === "object") {
|
|
288
|
+
for (const [subKey, subVal] of Object.entries(value)) {
|
|
289
|
+
appendFilterValue(params, `${prefix}[${subKey}]`, subVal);
|
|
290
|
+
}
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
params.append(prefix, String(value));
|
|
294
|
+
}
|
|
295
|
+
function mergeData(widget, data) {
|
|
296
|
+
if (data && typeof data === "object" && !Array.isArray(data)) {
|
|
297
|
+
return { ...widget, ...data };
|
|
298
|
+
}
|
|
299
|
+
return { ...widget, data };
|
|
300
|
+
}
|
|
301
|
+
var BREAKPOINT_PREFIX = {
|
|
302
|
+
sm: "sm:",
|
|
303
|
+
md: "md:",
|
|
304
|
+
lg: "lg:",
|
|
305
|
+
xl: "xl:",
|
|
306
|
+
"2xl": "2xl:"
|
|
307
|
+
};
|
|
308
|
+
function DashboardGrid({
|
|
309
|
+
dashboard,
|
|
310
|
+
filterValues,
|
|
311
|
+
onFilterChange,
|
|
312
|
+
className
|
|
313
|
+
}) {
|
|
314
|
+
const [internalValues, setInternalValues] = useState(filterValues ?? {});
|
|
315
|
+
const values = filterValues ?? internalValues;
|
|
316
|
+
const handleChange = (name, value) => {
|
|
317
|
+
if (onFilterChange) {
|
|
318
|
+
onFilterChange(name, value);
|
|
319
|
+
} else {
|
|
320
|
+
setInternalValues((prev) => ({ ...prev, [name]: value }));
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
const filterPayload = normaliseFilters(dashboard.filters);
|
|
324
|
+
const heading = dashboard.heading ?? dashboard.label;
|
|
325
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("space-y-4", className), children: [
|
|
326
|
+
/* @__PURE__ */ jsxs("header", { className: "flex flex-wrap items-start justify-between gap-3", children: [
|
|
327
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
|
|
328
|
+
heading && /* @__PURE__ */ jsx("h1", { className: "text-xl font-semibold", children: heading }),
|
|
329
|
+
dashboard.description && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: dashboard.description })
|
|
330
|
+
] }),
|
|
331
|
+
filterPayload.length > 0 && /* @__PURE__ */ jsx(DashboardFilters, { filters: filterPayload, values, onChange: handleChange })
|
|
332
|
+
] }),
|
|
333
|
+
/* @__PURE__ */ jsx(
|
|
334
|
+
"div",
|
|
335
|
+
{
|
|
336
|
+
className: cn("grid gap-4", gridColsClass(dashboard.columns)),
|
|
337
|
+
"data-testid": "dashboard-grid",
|
|
338
|
+
children: dashboard.widgets.map((widget) => {
|
|
339
|
+
const rawSpan = widget.columnSpan;
|
|
340
|
+
const span = typeof rawSpan === "number" ? rawSpan : 1;
|
|
341
|
+
const style = { gridColumn: `span ${span}` };
|
|
342
|
+
return /* @__PURE__ */ jsx("div", { style, "data-widget-slot": widget.name, children: /* @__PURE__ */ jsx(WidgetRenderer, { widget, filterValues: values }) }, widget.name);
|
|
343
|
+
})
|
|
344
|
+
}
|
|
345
|
+
)
|
|
346
|
+
] });
|
|
347
|
+
}
|
|
348
|
+
function gridColsClass(columns) {
|
|
349
|
+
if (typeof columns === "number") {
|
|
350
|
+
return `grid-cols-${Math.max(1, Math.min(12, columns))}`;
|
|
351
|
+
}
|
|
352
|
+
const parts = [];
|
|
353
|
+
parts.push("grid-cols-1");
|
|
354
|
+
for (const bp of ["sm", "md", "lg", "xl", "2xl"]) {
|
|
355
|
+
const value = columns[bp];
|
|
356
|
+
if (typeof value === "number") {
|
|
357
|
+
parts.push(`${BREAKPOINT_PREFIX[bp]}grid-cols-${Math.max(1, Math.min(12, value))}`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return parts.join(" ");
|
|
361
|
+
}
|
|
362
|
+
function normaliseFilters(raw) {
|
|
363
|
+
if (!raw) return [];
|
|
364
|
+
if (Array.isArray(raw)) return raw;
|
|
365
|
+
return [];
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export { ChartCard, DashboardFilters, DashboardGrid, TableCard, WidgetRenderer };
|
|
369
|
+
//# sourceMappingURL=chunk-47HD7ALI.js.map
|
|
370
|
+
//# sourceMappingURL=chunk-47HD7ALI.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/widgets/ChartCard.tsx","../src/widgets/DashboardFilters.tsx","../src/widgets/TableCard.tsx","../src/widgets/WidgetRenderer.tsx","../src/widgets/DashboardGrid.tsx"],"names":["jsx","jsxs","useState"],"mappings":"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","file":"chunk-47HD7ALI.js","sourcesContent":["import { type ComponentType, lazy, Suspense } from 'react';\nimport type { ChartSubProps, ChartType, ChartWidgetProps } from './types';\nimport { WidgetWrapper } from './WidgetWrapper';\n\n/**\n * Lazy-loaded chart sub-components. Each `import()` becomes a separate chunk\n * in the build output so the ~60KB Recharts payload (and its sibling chart\n * primitives) stay out of the main `@arqel-dev/ui` bundle. Dashboards that only\n * use StatCard never pay for Recharts.\n */\nconst LineChart = lazy(() => import('./charts/LineChart').then((m) => ({ default: m.LineChart })));\nconst BarChart = lazy(() => import('./charts/BarChart').then((m) => ({ default: m.BarChart })));\nconst AreaChart = lazy(() => import('./charts/AreaChart').then((m) => ({ default: m.AreaChart })));\nconst PieChart = lazy(() => import('./charts/PieChart').then((m) => ({ default: m.PieChart })));\nconst DonutChart = lazy(() =>\n import('./charts/DonutChart').then((m) => ({ default: m.DonutChart })),\n);\nconst RadarChart = lazy(() =>\n import('./charts/RadarChart').then((m) => ({ default: m.RadarChart })),\n);\n\nconst REGISTRY: Record<ChartType, ComponentType<ChartSubProps>> = {\n line: LineChart,\n bar: BarChart,\n area: AreaChart,\n pie: PieChart,\n donut: DonutChart,\n radar: RadarChart,\n};\n\nexport function ChartCard({ widget }: ChartWidgetProps) {\n const Chart = REGISTRY[widget.chartType] ?? LineChart;\n\n return (\n <WidgetWrapper heading={widget.heading} description={widget.description}>\n <Suspense\n fallback={\n <div\n role=\"status\"\n aria-label=\"Loading chart\"\n data-testid=\"chart-fallback\"\n className=\"animate-pulse rounded bg-muted\"\n style={{ height: widget.height }}\n />\n }\n >\n <Chart\n chartData={widget.chartData}\n chartOptions={widget.chartOptions}\n height={widget.height}\n showLegend={widget.showLegend}\n showGrid={widget.showGrid}\n />\n </Suspense>\n </WidgetWrapper>\n );\n}\n","/**\n * `<DashboardFilters>` — renders one control per dashboard `Filter`\n * payload (`Arqel\\Widgets\\Filters\\*`). Two filter types are wired in\n * Phase 2 part-3: `select` (single dropdown) and `date_range` (from/to\n * date inputs). Unknown types are dropped silently — extension points\n * land in later slices.\n *\n * Controlled component: callers own `values` and react to `onChange`.\n */\n\nimport { cn } from '../utils/cn.js';\n\nexport interface SelectFilterPayload {\n name: string;\n type: 'select';\n label?: string | null;\n options?: Record<string, string> | Array<{ value: string; label: string }>;\n multiple?: boolean;\n}\n\nexport interface DateRangeFilterPayload {\n name: string;\n type: 'date_range';\n label?: string | null;\n}\n\nexport type DashboardFilterPayload =\n | SelectFilterPayload\n | DateRangeFilterPayload\n | { name: string; type: string; label?: string | null; [key: string]: unknown };\n\nexport interface DashboardFiltersProps {\n filters: DashboardFilterPayload[];\n values: Record<string, unknown>;\n onChange: (name: string, value: unknown) => void;\n className?: string | undefined;\n}\n\nexport function DashboardFilters({ filters, values, onChange, className }: DashboardFiltersProps) {\n if (!filters || filters.length === 0) return null;\n\n return (\n <div className={cn('flex flex-wrap gap-2', className)} data-testid=\"dashboard-filters\">\n {filters.map((filter) => (\n <FilterControl\n key={filter.name}\n filter={filter}\n value={values[filter.name]}\n onChange={(v) => onChange(filter.name, v)}\n />\n ))}\n </div>\n );\n}\n\nfunction FilterControl({\n filter,\n value,\n onChange,\n}: {\n filter: DashboardFilterPayload;\n value: unknown;\n onChange: (value: unknown) => void;\n}) {\n if (filter.type === 'select') {\n return (\n <SelectControl filter={filter as SelectFilterPayload} value={value} onChange={onChange} />\n );\n }\n if (filter.type === 'date_range') {\n return (\n <DateRangeControl\n filter={filter as DateRangeFilterPayload}\n value={value}\n onChange={onChange}\n />\n );\n }\n return null;\n}\n\nfunction controlClasses() {\n return cn(\n 'h-9 rounded-sm border border-[var(--input)]',\n 'bg-background px-2 text-sm',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',\n );\n}\n\nfunction normaliseOptions(\n raw: SelectFilterPayload['options'],\n): Array<{ value: string; label: string }> {\n if (!raw) return [];\n if (Array.isArray(raw)) {\n return raw.map((o) => ({ value: String(o.value), label: String(o.label ?? o.value) }));\n }\n return Object.entries(raw).map(([k, v]) => ({ value: String(k), label: String(v) }));\n}\n\nfunction SelectControl({\n filter,\n value,\n onChange,\n}: {\n filter: SelectFilterPayload;\n value: unknown;\n onChange: (value: unknown) => void;\n}) {\n const options = normaliseOptions(filter.options);\n const label = filter.label ?? filter.name;\n\n if (filter.multiple) {\n const selected = Array.isArray(value) ? (value as Array<string | number>).map(String) : [];\n return (\n <label className=\"flex flex-col gap-1 text-xs text-muted-foreground\">\n {label}\n <select\n multiple\n aria-label={label}\n className={cn(controlClasses(), 'h-auto min-h-[2.25rem] py-1')}\n value={selected}\n onChange={(e) => {\n const next = Array.from(e.target.selectedOptions, (o) => o.value);\n onChange(next.length === 0 ? null : next);\n }}\n >\n {options.map((opt) => (\n <option key={opt.value} value={opt.value}>\n {opt.label}\n </option>\n ))}\n </select>\n </label>\n );\n }\n\n return (\n <label className=\"flex flex-col gap-1 text-xs text-muted-foreground\">\n {label}\n <select\n aria-label={label}\n className={controlClasses()}\n value={value === undefined || value === null ? '' : String(value)}\n onChange={(e) => onChange(e.target.value === '' ? null : e.target.value)}\n >\n <option value=\"\">All</option>\n {options.map((opt) => (\n <option key={opt.value} value={opt.value}>\n {opt.label}\n </option>\n ))}\n </select>\n </label>\n );\n}\n\nfunction DateRangeControl({\n filter,\n value,\n onChange,\n}: {\n filter: DateRangeFilterPayload;\n value: unknown;\n onChange: (value: unknown) => void;\n}) {\n const range = (value && typeof value === 'object' ? (value as Record<string, unknown>) : {}) as {\n from?: unknown;\n to?: unknown;\n };\n const fromValue = typeof range.from === 'string' ? range.from : '';\n const toValue = typeof range.to === 'string' ? range.to : '';\n\n return (\n <fieldset className=\"flex flex-col gap-1 text-xs text-muted-foreground\">\n <legend>{filter.label ?? filter.name}</legend>\n <div className=\"flex items-center gap-1\">\n <input\n type=\"date\"\n aria-label={`${filter.label ?? filter.name} from`}\n className={controlClasses()}\n value={fromValue}\n onChange={(e) => onChange({ from: e.target.value || null, to: range.to ?? null })}\n />\n <span aria-hidden=\"true\">–</span>\n <input\n type=\"date\"\n aria-label={`${filter.label ?? filter.name} to`}\n className={controlClasses()}\n value={toValue}\n onChange={(e) => onChange({ from: range.from ?? null, to: e.target.value || null })}\n />\n </div>\n </fieldset>\n );\n}\n","/**\n * `<TableCard>` — minimal mini-table widget rendered inside a dashboard.\n *\n * Mirrors the payload produced by `Arqel\\Widgets\\TableWidget::data()`:\n * `{ columns, records, limit, seeAllUrl, loadError? }`. The component is\n * intentionally schema-light — each column carries `name` + `label` and\n * cells extract `record[column.name]` verbatim. Renders a `role=\"alert\"`\n * panel when the PHP side surfaced a `loadError` instead of trying to\n * draw an empty table.\n */\n\nimport { cn } from '../utils/cn.js';\nimport { WidgetWrapper } from './WidgetWrapper.js';\n\nexport interface TableCardColumn {\n name: string;\n label?: string | null;\n}\n\nexport interface TableCardRecord {\n [key: string]: unknown;\n}\n\nexport interface TableCardWidget {\n name: string;\n type: 'table';\n heading?: string | null;\n description?: string | null;\n columns: TableCardColumn[];\n records: TableCardRecord[];\n limit: number;\n seeAllUrl?: string | null;\n loadError?: string | null;\n}\n\nexport interface TableCardProps {\n widget: TableCardWidget;\n className?: string | undefined;\n}\n\nexport function TableCard({ widget, className }: TableCardProps) {\n return (\n <WidgetWrapper\n heading={widget.heading ?? undefined}\n description={widget.description ?? undefined}\n className={className}\n >\n {widget.loadError ? (\n <div role=\"alert\" className=\"text-sm text-red-600\">\n {widget.loadError}\n </div>\n ) : (\n <>\n <div className=\"overflow-x-auto\">\n <table className={cn('w-full border-collapse text-sm', 'text-foreground')}>\n <thead>\n <tr className=\"border-b border-border text-left\">\n {widget.columns.map((column) => (\n <th\n key={column.name}\n scope=\"col\"\n className=\"px-2 py-1.5 font-medium text-muted-foreground\"\n >\n {column.label ?? column.name}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {widget.records.map((record, rowIndex) => (\n <tr\n // biome-ignore lint/suspicious/noArrayIndexKey: records may lack a stable id\n key={rowIndex}\n className=\"border-b border-border last:border-0\"\n >\n {widget.columns.map((column) => (\n <td key={column.name} className=\"px-2 py-1.5\">\n {formatCell(record[column.name])}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n {widget.seeAllUrl && (\n <div className=\"mt-3 text-right\">\n <a href={widget.seeAllUrl} className=\"text-sm text-primary\">\n See all →\n </a>\n </div>\n )}\n </>\n )}\n </WidgetWrapper>\n );\n}\n\nfunction formatCell(value: unknown): string {\n if (value === null || value === undefined) return '';\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {\n return String(value);\n }\n try {\n return JSON.stringify(value);\n } catch {\n return '';\n }\n}\n","/**\n * `<WidgetRenderer>` — dispatches widget payloads to the right card\n * by `widget.type` (`stat`, `chart`, `table`). Unknown / `custom` types\n * surface a `role=\"alert\"` so a misconfigured registry never silently\n * blanks the dashboard.\n *\n * Two server interactions live here:\n * 1. **Polling** — when `widget.poll > 0`, re-fetch the widget data\n * endpoint every N seconds and replace the inline payload.\n * 2. **Deferred** — when `widget.deferred === true` and `data` is\n * `null`, fetch once on mount.\n *\n * Both call the canonical endpoint\n * `/admin/dashboards/{dashboardId}/widgets/{widgetId}/data`. The\n * controller wraps the payload under `{ data: ... }`, so `doFetch`\n * unwraps that envelope before storing — the resulting shape matches\n * the SSR / Inertia path, which already seeds from `widget.data`. The\n * active `DashboardFilters` selection is forwarded as `filters[k]=v`\n * query params so deferred / poll refetches stay segmented.\n */\n\nimport { useEffect, useRef, useState } from 'react';\nimport { ChartCard } from './ChartCard.js';\nimport { StatCard } from './StatCard.js';\nimport { TableCard } from './TableCard.js';\n\nexport interface WidgetPayload {\n id?: string;\n name: string;\n type: string;\n component?: string;\n data?: unknown;\n poll?: number | null;\n deferred?: boolean;\n dashboardId?: string;\n widgetId?: string;\n [key: string]: unknown;\n}\n\nexport interface WidgetRendererProps {\n widget: WidgetPayload;\n filterValues?: Record<string, unknown>;\n fetcher?: ((url: string) => Promise<unknown>) | undefined;\n}\n\nexport function WidgetRenderer({ widget, filterValues, fetcher }: WidgetRendererProps) {\n const [data, setData] = useState<unknown>(widget.data ?? null);\n const filterRef = useRef(filterValues);\n filterRef.current = filterValues;\n\n const dashboardId = widget.dashboardId;\n // `Dashboard::findWidget` matches on `id()` (`<type>:<name>`), so the refetch\n // must target that — `Dashboard::resolve` now injects `widgetId`. Fall back to\n // the serialized `id` (also the full `id()`) before the bare `name`.\n const widgetId = widget.widgetId ?? widget.id ?? widget.name;\n const poll = typeof widget.poll === 'number' ? widget.poll : 0;\n const deferred = widget.deferred === true;\n // Capture the initial-payload presence in a ref so the effect doesn't\n // need to take `widget.data` as a dep (we only consult it once on mount).\n const hasInlineDataRef = useRef(widget.data !== null && widget.data !== undefined);\n\n useEffect(() => {\n if (!dashboardId || !widgetId) return undefined;\n\n const baseUrl = `/admin/dashboards/${dashboardId}/widgets/${widgetId}/data`;\n const doFetch = async () => {\n try {\n // Build the URL with the *live* filters so deferred / poll refetches\n // honour the dashboard's `DashboardFilters` selection. The server\n // reads `$request->input('filters')` as an array, so each entry is\n // serialised as `filters[key]=value`.\n const url = withFilters(baseUrl, filterRef.current);\n const body = fetcher\n ? await fetcher(url)\n : await fetch(url, { headers: { Accept: 'application/json' } }).then((r) => r.json());\n // Unwrap the controller envelope (`{ data: ... }`) so the fetch\n // payload shape matches the SSR / Inertia path, which already seeds\n // from the unwrapped `widget.data`. Defensive: accept bare payloads.\n const result =\n body && typeof body === 'object' && 'data' in (body as Record<string, unknown>)\n ? (body as Record<string, unknown>)['data']\n : body;\n setData(result);\n } catch {\n // Swallow — the inline payload (or the previous successful poll) stays.\n }\n };\n\n let interval: ReturnType<typeof setInterval> | undefined;\n\n if (deferred && !hasInlineDataRef.current) {\n void doFetch();\n }\n\n if (poll > 0) {\n interval = setInterval(() => {\n void doFetch();\n }, poll * 1000);\n }\n\n return () => {\n if (interval) clearInterval(interval);\n };\n }, [dashboardId, widgetId, poll, deferred, fetcher]);\n\n const merged = mergeData(widget, data);\n\n switch (widget.type) {\n case 'stat':\n return <StatCard widget={merged as never} />;\n case 'chart':\n return <ChartCard widget={merged as never} />;\n case 'table':\n return <TableCard widget={merged as never} />;\n default:\n return (\n <div role=\"alert\" className=\"text-sm text-red-600\">\n Widget type {widget.type} not registered\n </div>\n );\n }\n}\n\n/**\n * Append non-empty filter values as query params so the Laravel controller\n * resolves them via `$request->input('filters')` as a nested array. The shape\n * must mirror what the SSR / deep-link path produces (`router.get(path,\n * { filters })`, qs bracket notation) so a deferred / polling widget's lazy\n * refetch agrees with its SSR payload:\n * - **array** → `filters[key][]=el` per element (multi-`SelectControl`, #189)\n * - **plain object** → `filters[key][subKey]=val` per leaf (`DateRangeControl`\n * emits `{from,to}` → `filters[range][from]=..&filters[range][to]=..`)\n * - **scalar** → `filters[key]=value`\n * `null` / `undefined` / empty-string values (and leaves) are skipped to keep\n * the URL clean and match the prior scalar behaviour.\n */\nfunction withFilters(baseUrl: string, filters: Record<string, unknown> | undefined): string {\n if (!filters) return baseUrl;\n\n const params = new URLSearchParams();\n for (const [key, value] of Object.entries(filters)) {\n appendFilterValue(params, `filters[${key}]`, value);\n }\n\n const query = params.toString();\n return query ? `${baseUrl}?${query}` : baseUrl;\n}\n\n/**\n * Serialize a single filter value under `prefix`, recursing one level for the\n * array / plain-object shapes the dashboard filters emit (scalar | string[] |\n * `{from,to}`). `Array.isArray` is checked before the plain-object branch since\n * arrays are also objects. Empty leaves are skipped, never appended.\n */\nfunction appendFilterValue(params: URLSearchParams, prefix: string, value: unknown): void {\n if (value === null || value === undefined || value === '') return;\n\n if (Array.isArray(value)) {\n for (const el of value) {\n appendFilterValue(params, `${prefix}[]`, el);\n }\n return;\n }\n\n if (typeof value === 'object') {\n for (const [subKey, subVal] of Object.entries(value as Record<string, unknown>)) {\n appendFilterValue(params, `${prefix}[${subKey}]`, subVal);\n }\n return;\n }\n\n params.append(prefix, String(value));\n}\n\nfunction mergeData(widget: WidgetPayload, data: unknown): Record<string, unknown> {\n if (data && typeof data === 'object' && !Array.isArray(data)) {\n return { ...widget, ...(data as Record<string, unknown>) };\n }\n return { ...widget, data };\n}\n","/**\n * `<DashboardGrid>` — top-level layout for an Arqel dashboard.\n *\n * Renders the dashboard heading + description, the filter bar (when the\n * dashboard declares filters), and a CSS Grid of widgets. Column count\n * accepts either a flat int or a responsive map (`{sm,md,lg,xl,2xl}`)\n * matching `Arqel\\Widgets\\Dashboard::columns()`. Each widget honours\n * `columnSpan` (defaults to 1) via `gridColumn: span N`.\n *\n * Filter values are controlled internally when `onFilterChange` is not\n * provided; supply that prop (and `filterValues`) to lift state out\n * (e.g., to push it to the URL through Inertia).\n */\n\nimport { type CSSProperties, useState } from 'react';\nimport { cn } from '../utils/cn.js';\nimport { type DashboardFilterPayload, DashboardFilters } from './DashboardFilters.js';\nimport { type WidgetPayload, WidgetRenderer } from './WidgetRenderer.js';\n\nexport interface DashboardPayload {\n id: string;\n label: string;\n path?: string | null;\n heading?: string | null;\n description?: string | null;\n widgets: WidgetPayload[];\n filters?: DashboardFilterPayload[] | Record<string, unknown> | null;\n columns: number | Partial<Record<'sm' | 'md' | 'lg' | 'xl' | '2xl', number>>;\n}\n\nexport interface DashboardGridProps {\n dashboard: DashboardPayload;\n filterValues?: Record<string, unknown>;\n onFilterChange?: ((name: string, value: unknown) => void) | undefined;\n className?: string | undefined;\n}\n\nconst BREAKPOINT_PREFIX: Record<string, string> = {\n sm: 'sm:',\n md: 'md:',\n lg: 'lg:',\n xl: 'xl:',\n '2xl': '2xl:',\n};\n\nexport function DashboardGrid({\n dashboard,\n filterValues,\n onFilterChange,\n className,\n}: DashboardGridProps) {\n const [internalValues, setInternalValues] = useState<Record<string, unknown>>(filterValues ?? {});\n const values = filterValues ?? internalValues;\n\n const handleChange = (name: string, value: unknown) => {\n if (onFilterChange) {\n onFilterChange(name, value);\n } else {\n setInternalValues((prev) => ({ ...prev, [name]: value }));\n }\n };\n\n const filterPayload = normaliseFilters(dashboard.filters);\n const heading = dashboard.heading ?? dashboard.label;\n\n return (\n <div className={cn('space-y-4', className)}>\n <header className=\"flex flex-wrap items-start justify-between gap-3\">\n <div className=\"space-y-1\">\n {heading && <h1 className=\"text-xl font-semibold\">{heading}</h1>}\n {dashboard.description && (\n <p className=\"text-sm text-muted-foreground\">{dashboard.description}</p>\n )}\n </div>\n {filterPayload.length > 0 && (\n <DashboardFilters filters={filterPayload} values={values} onChange={handleChange} />\n )}\n </header>\n\n <div\n className={cn('grid gap-4', gridColsClass(dashboard.columns))}\n data-testid=\"dashboard-grid\"\n >\n {dashboard.widgets.map((widget) => {\n const rawSpan = (widget as { columnSpan?: unknown }).columnSpan;\n const span = typeof rawSpan === 'number' ? rawSpan : 1;\n const style: CSSProperties = { gridColumn: `span ${span}` };\n return (\n <div key={widget.name} style={style} data-widget-slot={widget.name}>\n <WidgetRenderer widget={widget} filterValues={values} />\n </div>\n );\n })}\n </div>\n </div>\n );\n}\n\nfunction gridColsClass(columns: DashboardPayload['columns']): string {\n if (typeof columns === 'number') {\n return `grid-cols-${Math.max(1, Math.min(12, columns))}`;\n }\n const parts: string[] = [];\n // Base fallback when no `sm` provided.\n parts.push('grid-cols-1');\n for (const bp of ['sm', 'md', 'lg', 'xl', '2xl'] as const) {\n const value = columns[bp];\n if (typeof value === 'number') {\n parts.push(`${BREAKPOINT_PREFIX[bp]}grid-cols-${Math.max(1, Math.min(12, value))}`);\n }\n }\n return parts.join(' ');\n}\n\nfunction normaliseFilters(raw: DashboardPayload['filters']): DashboardFilterPayload[] {\n if (!raw) return [];\n if (Array.isArray(raw)) return raw;\n // Legacy `Record<string, mixed>` passthrough — render as a select-less\n // empty list so we don't crash. The declarative path is preferred.\n return [];\n}\n"]}
|