@arqel-dev/ui 0.10.1 → 0.11.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/dist/pages.js CHANGED
@@ -1,12 +1,13 @@
1
1
  import { clsx } from 'clsx';
2
2
  import { twMerge } from 'tailwind-merge';
3
- import { useMemo, useState, useRef, useEffect, useCallback, useId } from 'react';
3
+ import { useMemo, useState, useRef, useEffect, useCallback, Suspense, useId, lazy } from 'react';
4
4
  import { cva } from 'class-variance-authority';
5
5
  import { Slot, DropdownMenu as DropdownMenu$1, Dialog as Dialog$1 } from 'radix-ui';
6
6
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
7
7
  import { isFieldEntry, isLayoutEntry } from '@arqel-dev/types/forms';
8
8
  import { useArqelForm } from '@arqel-dev/hooks';
9
9
  import { usePage, router } from '@inertiajs/react';
10
+ import { ResponsiveContainer, LineChart as LineChart$1, CartesianGrid, XAxis, YAxis, Tooltip, Legend, Line, BarChart as BarChart$1, Bar, AreaChart as AreaChart$1, Area, PieChart as PieChart$1, Pie, Cell, RadarChart as RadarChart$1, PolarGrid, PolarAngleAxis, PolarRadiusAxis, Radar } from 'recharts';
10
11
  import { XIcon } from 'lucide-react';
11
12
  import { useReactTable, getCoreRowModel, flexRender } from '@tanstack/react-table';
12
13
 
@@ -808,6 +809,750 @@ var init_ArqelCreatePage = __esm({
808
809
  };
809
810
  }
810
811
  });
812
+ function resolveColumnSpan(columnSpan) {
813
+ if (columnSpan === void 0) return void 0;
814
+ if (typeof columnSpan === "number") return `col-span-${columnSpan}`;
815
+ return columnSpan;
816
+ }
817
+ function WidgetWrapper({
818
+ heading,
819
+ description,
820
+ loading = false,
821
+ error = null,
822
+ onRetry,
823
+ columnSpan,
824
+ className,
825
+ children
826
+ }) {
827
+ const baseClass = cn(
828
+ "rounded border bg-background p-4",
829
+ resolveColumnSpan(columnSpan),
830
+ className
831
+ );
832
+ if (loading) {
833
+ return /* @__PURE__ */ jsx(
834
+ "section",
835
+ {
836
+ "aria-label": heading,
837
+ "aria-busy": "true",
838
+ className: baseClass,
839
+ "data-widget-state": "loading",
840
+ children: /* @__PURE__ */ jsx("div", { className: "animate-pulse h-32 bg-muted rounded" })
841
+ }
842
+ );
843
+ }
844
+ if (error) {
845
+ return /* @__PURE__ */ jsx("section", { "aria-label": heading, className: baseClass, "data-widget-state": "error", children: /* @__PURE__ */ jsxs("div", { role: "alert", className: "flex flex-col gap-2 text-sm text-destructive", children: [
846
+ /* @__PURE__ */ jsx("p", { children: error.message }),
847
+ onRetry && /* @__PURE__ */ jsx(
848
+ "button",
849
+ {
850
+ type: "button",
851
+ onClick: onRetry,
852
+ className: "self-start rounded border px-2 py-1 text-xs text-foreground hover:bg-muted",
853
+ children: "Retry"
854
+ }
855
+ )
856
+ ] }) });
857
+ }
858
+ return /* @__PURE__ */ jsxs("section", { "aria-label": heading, className: baseClass, "data-widget-state": "ready", children: [
859
+ heading && /* @__PURE__ */ jsx("h2", { className: "text-sm font-medium text-muted-foreground", children: heading }),
860
+ description && /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: description }),
861
+ /* @__PURE__ */ jsx("div", { className: cn(heading || description ? "mt-3" : void 0), children })
862
+ ] });
863
+ }
864
+ var init_WidgetWrapper = __esm({
865
+ "src/widgets/WidgetWrapper.tsx"() {
866
+ init_cn();
867
+ }
868
+ });
869
+
870
+ // src/widgets/types.ts
871
+ function toRowShape(data) {
872
+ return data.labels.map((label, i) => {
873
+ const row = { name: label };
874
+ data.datasets.forEach((ds, j) => {
875
+ row[`dataset${j}`] = ds.data[i] ?? 0;
876
+ });
877
+ return row;
878
+ });
879
+ }
880
+ function colorFor(dataset, index) {
881
+ return dataset.color ?? PALETTE[index % PALETTE.length] ?? "#2563eb";
882
+ }
883
+ var PALETTE;
884
+ var init_types = __esm({
885
+ "src/widgets/types.ts"() {
886
+ PALETTE = [
887
+ "#2563eb",
888
+ "#10b981",
889
+ "#f59e0b",
890
+ "#ef4444",
891
+ "#8b5cf6",
892
+ "#ec4899",
893
+ "#14b8a6",
894
+ "#f97316"
895
+ ];
896
+ }
897
+ });
898
+
899
+ // src/widgets/charts/LineChart.tsx
900
+ var LineChart_exports = {};
901
+ __export(LineChart_exports, {
902
+ LineChart: () => LineChart
903
+ });
904
+ function LineChart({ chartData, height, showLegend, showGrid }) {
905
+ const rows = toRowShape(chartData);
906
+ return /* @__PURE__ */ jsx("div", { "data-testid": "chart-line", style: { width: "100%", height }, children: /* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxs(LineChart$1, { data: rows, children: [
907
+ showGrid ? /* @__PURE__ */ jsx(CartesianGrid, { strokeDasharray: "3 3" }) : null,
908
+ /* @__PURE__ */ jsx(XAxis, { dataKey: "name" }),
909
+ /* @__PURE__ */ jsx(YAxis, {}),
910
+ /* @__PURE__ */ jsx(Tooltip, {}),
911
+ showLegend ? /* @__PURE__ */ jsx(Legend, {}) : null,
912
+ chartData.datasets.map((ds, i) => /* @__PURE__ */ jsx(
913
+ Line,
914
+ {
915
+ type: "monotone",
916
+ dataKey: `dataset${i}`,
917
+ name: ds.label,
918
+ stroke: colorFor(ds, i),
919
+ strokeWidth: 2,
920
+ dot: false
921
+ },
922
+ ds.label
923
+ ))
924
+ ] }) }) });
925
+ }
926
+ var init_LineChart = __esm({
927
+ "src/widgets/charts/LineChart.tsx"() {
928
+ init_types();
929
+ }
930
+ });
931
+
932
+ // src/widgets/charts/BarChart.tsx
933
+ var BarChart_exports = {};
934
+ __export(BarChart_exports, {
935
+ BarChart: () => BarChart
936
+ });
937
+ function BarChart({ chartData, height, showLegend, showGrid }) {
938
+ const rows = toRowShape(chartData);
939
+ return /* @__PURE__ */ jsx("div", { "data-testid": "chart-bar", style: { width: "100%", height }, children: /* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxs(BarChart$1, { data: rows, children: [
940
+ showGrid ? /* @__PURE__ */ jsx(CartesianGrid, { strokeDasharray: "3 3" }) : null,
941
+ /* @__PURE__ */ jsx(XAxis, { dataKey: "name" }),
942
+ /* @__PURE__ */ jsx(YAxis, {}),
943
+ /* @__PURE__ */ jsx(Tooltip, {}),
944
+ showLegend ? /* @__PURE__ */ jsx(Legend, {}) : null,
945
+ chartData.datasets.map((ds, i) => /* @__PURE__ */ jsx(Bar, { dataKey: `dataset${i}`, name: ds.label, fill: colorFor(ds, i) }, ds.label))
946
+ ] }) }) });
947
+ }
948
+ var init_BarChart = __esm({
949
+ "src/widgets/charts/BarChart.tsx"() {
950
+ init_types();
951
+ }
952
+ });
953
+
954
+ // src/widgets/charts/AreaChart.tsx
955
+ var AreaChart_exports = {};
956
+ __export(AreaChart_exports, {
957
+ AreaChart: () => AreaChart
958
+ });
959
+ function AreaChart({ chartData, height, showLegend, showGrid }) {
960
+ const rows = toRowShape(chartData);
961
+ return /* @__PURE__ */ jsx("div", { "data-testid": "chart-area", style: { width: "100%", height }, children: /* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxs(AreaChart$1, { data: rows, children: [
962
+ showGrid ? /* @__PURE__ */ jsx(CartesianGrid, { strokeDasharray: "3 3" }) : null,
963
+ /* @__PURE__ */ jsx(XAxis, { dataKey: "name" }),
964
+ /* @__PURE__ */ jsx(YAxis, {}),
965
+ /* @__PURE__ */ jsx(Tooltip, {}),
966
+ showLegend ? /* @__PURE__ */ jsx(Legend, {}) : null,
967
+ chartData.datasets.map((ds, i) => {
968
+ const color = colorFor(ds, i);
969
+ return /* @__PURE__ */ jsx(
970
+ Area,
971
+ {
972
+ type: "monotone",
973
+ dataKey: `dataset${i}`,
974
+ name: ds.label,
975
+ stroke: color,
976
+ fill: color,
977
+ fillOpacity: 0.3
978
+ },
979
+ ds.label
980
+ );
981
+ })
982
+ ] }) }) });
983
+ }
984
+ var init_AreaChart = __esm({
985
+ "src/widgets/charts/AreaChart.tsx"() {
986
+ init_types();
987
+ }
988
+ });
989
+
990
+ // src/widgets/charts/PieChart.tsx
991
+ var PieChart_exports = {};
992
+ __export(PieChart_exports, {
993
+ PieChart: () => PieChart
994
+ });
995
+ function PieChart({
996
+ chartData,
997
+ height,
998
+ showLegend,
999
+ innerRadius = 0,
1000
+ testId = "chart-pie"
1001
+ }) {
1002
+ const first = chartData.datasets[0];
1003
+ const slices = (first?.data ?? []).map((value, i) => ({
1004
+ name: chartData.labels[i] ?? `Slice ${i + 1}`,
1005
+ value
1006
+ }));
1007
+ return /* @__PURE__ */ jsx("div", { "data-testid": testId, style: { width: "100%", height }, children: /* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxs(PieChart$1, { children: [
1008
+ /* @__PURE__ */ jsx(
1009
+ Pie,
1010
+ {
1011
+ data: slices,
1012
+ dataKey: "value",
1013
+ nameKey: "name",
1014
+ innerRadius,
1015
+ outerRadius: "80%",
1016
+ label: true,
1017
+ children: slices.map((slice, i) => /* @__PURE__ */ jsx(Cell, { fill: colorFor(first ?? { }, i) }, slice.name))
1018
+ }
1019
+ ),
1020
+ /* @__PURE__ */ jsx(Tooltip, {}),
1021
+ showLegend ? /* @__PURE__ */ jsx(Legend, {}) : null
1022
+ ] }) }) });
1023
+ }
1024
+ var init_PieChart = __esm({
1025
+ "src/widgets/charts/PieChart.tsx"() {
1026
+ init_types();
1027
+ }
1028
+ });
1029
+
1030
+ // src/widgets/charts/DonutChart.tsx
1031
+ var DonutChart_exports = {};
1032
+ __export(DonutChart_exports, {
1033
+ DonutChart: () => DonutChart
1034
+ });
1035
+ function DonutChart(props) {
1036
+ return /* @__PURE__ */ jsx(PieChart, { ...props, innerRadius: 60, testId: "chart-donut" });
1037
+ }
1038
+ var init_DonutChart = __esm({
1039
+ "src/widgets/charts/DonutChart.tsx"() {
1040
+ init_PieChart();
1041
+ }
1042
+ });
1043
+
1044
+ // src/widgets/charts/RadarChart.tsx
1045
+ var RadarChart_exports = {};
1046
+ __export(RadarChart_exports, {
1047
+ RadarChart: () => RadarChart
1048
+ });
1049
+ function RadarChart({ chartData, height, showLegend, showGrid }) {
1050
+ const rows = toRowShape(chartData);
1051
+ return /* @__PURE__ */ jsx("div", { "data-testid": "chart-radar", style: { width: "100%", height }, children: /* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxs(RadarChart$1, { data: rows, children: [
1052
+ showGrid ? /* @__PURE__ */ jsx(PolarGrid, {}) : null,
1053
+ /* @__PURE__ */ jsx(PolarAngleAxis, { dataKey: "name" }),
1054
+ /* @__PURE__ */ jsx(PolarRadiusAxis, {}),
1055
+ chartData.datasets.map((ds, i) => {
1056
+ const color = colorFor(ds, i);
1057
+ return /* @__PURE__ */ jsx(
1058
+ Radar,
1059
+ {
1060
+ dataKey: `dataset${i}`,
1061
+ name: ds.label,
1062
+ stroke: color,
1063
+ fill: color,
1064
+ fillOpacity: 0.3
1065
+ },
1066
+ ds.label
1067
+ );
1068
+ }),
1069
+ /* @__PURE__ */ jsx(Tooltip, {}),
1070
+ showLegend ? /* @__PURE__ */ jsx(Legend, {}) : null
1071
+ ] }) }) });
1072
+ }
1073
+ var init_RadarChart = __esm({
1074
+ "src/widgets/charts/RadarChart.tsx"() {
1075
+ init_types();
1076
+ }
1077
+ });
1078
+ function ChartCard({ widget }) {
1079
+ const Chart = REGISTRY[widget.chartType] ?? LineChart2;
1080
+ return /* @__PURE__ */ jsx(WidgetWrapper, { heading: widget.heading, description: widget.description, children: /* @__PURE__ */ jsx(
1081
+ Suspense,
1082
+ {
1083
+ fallback: /* @__PURE__ */ jsx(
1084
+ "div",
1085
+ {
1086
+ role: "status",
1087
+ "aria-label": "Loading chart",
1088
+ "data-testid": "chart-fallback",
1089
+ className: "animate-pulse rounded bg-muted",
1090
+ style: { height: widget.height }
1091
+ }
1092
+ ),
1093
+ children: /* @__PURE__ */ jsx(
1094
+ Chart,
1095
+ {
1096
+ chartData: widget.chartData,
1097
+ chartOptions: widget.chartOptions,
1098
+ height: widget.height,
1099
+ showLegend: widget.showLegend,
1100
+ showGrid: widget.showGrid
1101
+ }
1102
+ )
1103
+ }
1104
+ ) });
1105
+ }
1106
+ var LineChart2, BarChart2, AreaChart2, PieChart2, DonutChart2, RadarChart2, REGISTRY;
1107
+ var init_ChartCard = __esm({
1108
+ "src/widgets/ChartCard.tsx"() {
1109
+ init_WidgetWrapper();
1110
+ LineChart2 = lazy(() => Promise.resolve().then(() => (init_LineChart(), LineChart_exports)).then((m) => ({ default: m.LineChart })));
1111
+ BarChart2 = lazy(() => Promise.resolve().then(() => (init_BarChart(), BarChart_exports)).then((m) => ({ default: m.BarChart })));
1112
+ AreaChart2 = lazy(() => Promise.resolve().then(() => (init_AreaChart(), AreaChart_exports)).then((m) => ({ default: m.AreaChart })));
1113
+ PieChart2 = lazy(() => Promise.resolve().then(() => (init_PieChart(), PieChart_exports)).then((m) => ({ default: m.PieChart })));
1114
+ DonutChart2 = lazy(
1115
+ () => Promise.resolve().then(() => (init_DonutChart(), DonutChart_exports)).then((m) => ({ default: m.DonutChart }))
1116
+ );
1117
+ RadarChart2 = lazy(
1118
+ () => Promise.resolve().then(() => (init_RadarChart(), RadarChart_exports)).then((m) => ({ default: m.RadarChart }))
1119
+ );
1120
+ REGISTRY = {
1121
+ line: LineChart2,
1122
+ bar: BarChart2,
1123
+ area: AreaChart2,
1124
+ pie: PieChart2,
1125
+ donut: DonutChart2,
1126
+ radar: RadarChart2
1127
+ };
1128
+ }
1129
+ });
1130
+ function DashboardFilters({ filters, values, onChange, className }) {
1131
+ if (!filters || filters.length === 0) return null;
1132
+ return /* @__PURE__ */ jsx("div", { className: cn("flex flex-wrap gap-2", className), "data-testid": "dashboard-filters", children: filters.map((filter) => /* @__PURE__ */ jsx(
1133
+ FilterControl,
1134
+ {
1135
+ filter,
1136
+ value: values[filter.name],
1137
+ onChange: (v) => onChange(filter.name, v)
1138
+ },
1139
+ filter.name
1140
+ )) });
1141
+ }
1142
+ function FilterControl({
1143
+ filter,
1144
+ value,
1145
+ onChange
1146
+ }) {
1147
+ if (filter.type === "select") {
1148
+ return /* @__PURE__ */ jsx(SelectControl, { filter, value, onChange });
1149
+ }
1150
+ if (filter.type === "date_range") {
1151
+ return /* @__PURE__ */ jsx(
1152
+ DateRangeControl,
1153
+ {
1154
+ filter,
1155
+ value,
1156
+ onChange
1157
+ }
1158
+ );
1159
+ }
1160
+ return null;
1161
+ }
1162
+ function controlClasses() {
1163
+ return cn(
1164
+ "h-9 rounded-sm border border-[var(--input)]",
1165
+ "bg-background px-2 text-sm",
1166
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
1167
+ );
1168
+ }
1169
+ function normaliseOptions2(raw) {
1170
+ if (!raw) return [];
1171
+ if (Array.isArray(raw)) {
1172
+ return raw.map((o) => ({ value: String(o.value), label: String(o.label ?? o.value) }));
1173
+ }
1174
+ return Object.entries(raw).map(([k, v]) => ({ value: String(k), label: String(v) }));
1175
+ }
1176
+ function SelectControl({
1177
+ filter,
1178
+ value,
1179
+ onChange
1180
+ }) {
1181
+ const options = normaliseOptions2(filter.options);
1182
+ return /* @__PURE__ */ jsxs("label", { className: "flex flex-col gap-1 text-xs text-muted-foreground", children: [
1183
+ filter.label ?? filter.name,
1184
+ /* @__PURE__ */ jsxs(
1185
+ "select",
1186
+ {
1187
+ "aria-label": filter.label ?? filter.name,
1188
+ className: controlClasses(),
1189
+ value: value === void 0 || value === null ? "" : String(value),
1190
+ onChange: (e) => onChange(e.target.value === "" ? null : e.target.value),
1191
+ children: [
1192
+ /* @__PURE__ */ jsx("option", { value: "", children: "All" }),
1193
+ options.map((opt) => /* @__PURE__ */ jsx("option", { value: opt.value, children: opt.label }, opt.value))
1194
+ ]
1195
+ }
1196
+ )
1197
+ ] });
1198
+ }
1199
+ function DateRangeControl({
1200
+ filter,
1201
+ value,
1202
+ onChange
1203
+ }) {
1204
+ const range = value && typeof value === "object" ? value : {};
1205
+ const fromValue = typeof range.from === "string" ? range.from : "";
1206
+ const toValue = typeof range.to === "string" ? range.to : "";
1207
+ return /* @__PURE__ */ jsxs("fieldset", { className: "flex flex-col gap-1 text-xs text-muted-foreground", children: [
1208
+ /* @__PURE__ */ jsx("legend", { children: filter.label ?? filter.name }),
1209
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
1210
+ /* @__PURE__ */ jsx(
1211
+ "input",
1212
+ {
1213
+ type: "date",
1214
+ "aria-label": `${filter.label ?? filter.name} from`,
1215
+ className: controlClasses(),
1216
+ value: fromValue,
1217
+ onChange: (e) => onChange({ from: e.target.value || null, to: range.to ?? null })
1218
+ }
1219
+ ),
1220
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\u2013" }),
1221
+ /* @__PURE__ */ jsx(
1222
+ "input",
1223
+ {
1224
+ type: "date",
1225
+ "aria-label": `${filter.label ?? filter.name} to`,
1226
+ className: controlClasses(),
1227
+ value: toValue,
1228
+ onChange: (e) => onChange({ from: range.from ?? null, to: e.target.value || null })
1229
+ }
1230
+ )
1231
+ ] })
1232
+ ] });
1233
+ }
1234
+ var init_DashboardFilters = __esm({
1235
+ "src/widgets/DashboardFilters.tsx"() {
1236
+ init_cn();
1237
+ }
1238
+ });
1239
+ function Sparkline({ points, color }) {
1240
+ if (points.length < 2) return null;
1241
+ const min = Math.min(...points);
1242
+ const max = Math.max(...points);
1243
+ const range = max - min || 1;
1244
+ const stepX = 100 / (points.length - 1);
1245
+ const coords = points.map((value, index) => {
1246
+ const x = index * stepX;
1247
+ const y = 30 - (value - min) / range * 30;
1248
+ return `${x.toFixed(2)},${y.toFixed(2)}`;
1249
+ }).join(" ");
1250
+ return /* @__PURE__ */ jsx(
1251
+ "svg",
1252
+ {
1253
+ viewBox: "0 0 100 30",
1254
+ preserveAspectRatio: "none",
1255
+ className: cn("mt-3 h-8 w-full", COLOR_STROKE[color]),
1256
+ role: "img",
1257
+ "aria-label": "Trend sparkline",
1258
+ "data-testid": "stat-card-sparkline",
1259
+ children: /* @__PURE__ */ jsx(
1260
+ "polyline",
1261
+ {
1262
+ fill: "none",
1263
+ stroke: "currentColor",
1264
+ strokeWidth: "1.5",
1265
+ strokeLinecap: "round",
1266
+ strokeLinejoin: "round",
1267
+ points: coords
1268
+ }
1269
+ )
1270
+ }
1271
+ );
1272
+ }
1273
+ function StatCard({ widget, className, columnSpan }) {
1274
+ const { heading, description, value, statDescription, descriptionIcon, color, chart, url } = widget;
1275
+ const hasChart = Array.isArray(chart) && chart.length >= 2;
1276
+ const body = /* @__PURE__ */ jsxs(Fragment, { children: [
1277
+ /* @__PURE__ */ jsx("div", { className: cn("text-4xl font-bold tabular-nums", COLOR_TEXT[color]), children: value ?? "\u2014" }),
1278
+ statDescription && /* @__PURE__ */ jsxs("div", { className: "mt-2 flex items-center gap-1 text-xs text-muted-foreground", children: [
1279
+ descriptionIcon && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", "data-testid": "stat-card-icon", children: descriptionIcon }),
1280
+ /* @__PURE__ */ jsx("span", { children: statDescription })
1281
+ ] }),
1282
+ hasChart && /* @__PURE__ */ jsx(Sparkline, { points: chart, color })
1283
+ ] });
1284
+ const inner = url ? /* @__PURE__ */ jsx(
1285
+ "a",
1286
+ {
1287
+ href: url,
1288
+ className: "block rounded outline-none focus-visible:ring-2 focus-visible:ring-ring",
1289
+ "data-testid": "stat-card-link",
1290
+ children: body
1291
+ }
1292
+ ) : body;
1293
+ return /* @__PURE__ */ jsx(
1294
+ WidgetWrapper,
1295
+ {
1296
+ heading,
1297
+ description,
1298
+ columnSpan,
1299
+ className,
1300
+ children: inner
1301
+ }
1302
+ );
1303
+ }
1304
+ var COLOR_TEXT, COLOR_STROKE;
1305
+ var init_StatCard = __esm({
1306
+ "src/widgets/StatCard.tsx"() {
1307
+ init_cn();
1308
+ init_WidgetWrapper();
1309
+ COLOR_TEXT = {
1310
+ primary: "text-primary",
1311
+ secondary: "text-secondary-foreground",
1312
+ success: "text-green-600",
1313
+ warning: "text-amber-600",
1314
+ danger: "text-red-600",
1315
+ info: "text-sky-600"
1316
+ };
1317
+ COLOR_STROKE = {
1318
+ primary: "stroke-primary",
1319
+ secondary: "stroke-secondary-foreground",
1320
+ success: "stroke-green-500",
1321
+ warning: "stroke-amber-500",
1322
+ danger: "stroke-red-500",
1323
+ info: "stroke-sky-500"
1324
+ };
1325
+ }
1326
+ });
1327
+ function TableCard({ widget, className }) {
1328
+ return /* @__PURE__ */ jsx(
1329
+ WidgetWrapper,
1330
+ {
1331
+ heading: widget.heading ?? void 0,
1332
+ description: widget.description ?? void 0,
1333
+ className,
1334
+ children: widget.loadError ? /* @__PURE__ */ jsx("div", { role: "alert", className: "text-sm text-red-600", children: widget.loadError }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1335
+ /* @__PURE__ */ jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxs("table", { className: cn("w-full border-collapse text-sm", "text-foreground"), children: [
1336
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { className: "border-b border-border text-left", children: widget.columns.map((column) => /* @__PURE__ */ jsx(
1337
+ "th",
1338
+ {
1339
+ scope: "col",
1340
+ className: "px-2 py-1.5 font-medium text-muted-foreground",
1341
+ children: column.label ?? column.name
1342
+ },
1343
+ column.name
1344
+ )) }) }),
1345
+ /* @__PURE__ */ jsx("tbody", { children: widget.records.map((record, rowIndex) => /* @__PURE__ */ jsx(
1346
+ "tr",
1347
+ {
1348
+ className: "border-b border-border last:border-0",
1349
+ children: widget.columns.map((column) => /* @__PURE__ */ jsx("td", { className: "px-2 py-1.5", children: formatCell(record[column.name]) }, column.name))
1350
+ },
1351
+ rowIndex
1352
+ )) })
1353
+ ] }) }),
1354
+ 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" }) })
1355
+ ] })
1356
+ }
1357
+ );
1358
+ }
1359
+ function formatCell(value) {
1360
+ if (value === null || value === void 0) return "";
1361
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
1362
+ return String(value);
1363
+ }
1364
+ try {
1365
+ return JSON.stringify(value);
1366
+ } catch {
1367
+ return "";
1368
+ }
1369
+ }
1370
+ var init_TableCard = __esm({
1371
+ "src/widgets/TableCard.tsx"() {
1372
+ init_cn();
1373
+ init_WidgetWrapper();
1374
+ }
1375
+ });
1376
+ function WidgetRenderer({ widget, filterValues, fetcher }) {
1377
+ const [data, setData] = useState(widget.data ?? null);
1378
+ const filterRef = useRef(filterValues);
1379
+ filterRef.current = filterValues;
1380
+ const dashboardId = widget.dashboardId;
1381
+ const widgetId = widget.widgetId ?? widget.name;
1382
+ const poll = typeof widget.poll === "number" ? widget.poll : 0;
1383
+ const deferred = widget.deferred === true;
1384
+ const hasInlineDataRef = useRef(widget.data !== null && widget.data !== void 0);
1385
+ useEffect(() => {
1386
+ if (!dashboardId || !widgetId) return void 0;
1387
+ const url = `/admin/dashboards/${dashboardId}/widgets/${widgetId}/data`;
1388
+ const doFetch = async () => {
1389
+ try {
1390
+ const result = fetcher ? await fetcher(url) : await fetch(url, { headers: { Accept: "application/json" } }).then((r) => r.json());
1391
+ setData(result);
1392
+ } catch {
1393
+ }
1394
+ };
1395
+ let interval;
1396
+ if (deferred && !hasInlineDataRef.current) {
1397
+ void doFetch();
1398
+ }
1399
+ if (poll > 0) {
1400
+ interval = setInterval(() => {
1401
+ void doFetch();
1402
+ }, poll * 1e3);
1403
+ }
1404
+ return () => {
1405
+ if (interval) clearInterval(interval);
1406
+ };
1407
+ }, [dashboardId, widgetId, poll, deferred, fetcher]);
1408
+ const merged = mergeData(widget, data);
1409
+ switch (widget.type) {
1410
+ case "stat":
1411
+ return /* @__PURE__ */ jsx(StatCard, { widget: merged });
1412
+ case "chart":
1413
+ return /* @__PURE__ */ jsx(ChartCard, { widget: merged });
1414
+ case "table":
1415
+ return /* @__PURE__ */ jsx(TableCard, { widget: merged });
1416
+ default:
1417
+ return /* @__PURE__ */ jsxs("div", { role: "alert", className: "text-sm text-red-600", children: [
1418
+ "Widget type ",
1419
+ widget.type,
1420
+ " not registered"
1421
+ ] });
1422
+ }
1423
+ }
1424
+ function mergeData(widget, data) {
1425
+ if (data && typeof data === "object" && !Array.isArray(data)) {
1426
+ return { ...widget, ...data };
1427
+ }
1428
+ return { ...widget, data };
1429
+ }
1430
+ var init_WidgetRenderer = __esm({
1431
+ "src/widgets/WidgetRenderer.tsx"() {
1432
+ init_ChartCard();
1433
+ init_StatCard();
1434
+ init_TableCard();
1435
+ }
1436
+ });
1437
+ function DashboardGrid({
1438
+ dashboard,
1439
+ filterValues,
1440
+ onFilterChange,
1441
+ className
1442
+ }) {
1443
+ const [internalValues, setInternalValues] = useState(filterValues ?? {});
1444
+ const values = filterValues ?? internalValues;
1445
+ const handleChange = (name, value) => {
1446
+ if (onFilterChange) {
1447
+ onFilterChange(name, value);
1448
+ } else {
1449
+ setInternalValues((prev) => ({ ...prev, [name]: value }));
1450
+ }
1451
+ };
1452
+ const filterPayload = normaliseFilters(dashboard.filters);
1453
+ const heading = dashboard.heading ?? dashboard.label;
1454
+ return /* @__PURE__ */ jsxs("div", { className: cn("space-y-4", className), children: [
1455
+ /* @__PURE__ */ jsxs("header", { className: "flex flex-wrap items-start justify-between gap-3", children: [
1456
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
1457
+ heading && /* @__PURE__ */ jsx("h1", { className: "text-xl font-semibold", children: heading }),
1458
+ dashboard.description && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: dashboard.description })
1459
+ ] }),
1460
+ filterPayload.length > 0 && /* @__PURE__ */ jsx(DashboardFilters, { filters: filterPayload, values, onChange: handleChange })
1461
+ ] }),
1462
+ /* @__PURE__ */ jsx(
1463
+ "div",
1464
+ {
1465
+ className: cn("grid gap-4", gridColsClass(dashboard.columns)),
1466
+ "data-testid": "dashboard-grid",
1467
+ children: dashboard.widgets.map((widget) => {
1468
+ const rawSpan = widget.columnSpan;
1469
+ const span = typeof rawSpan === "number" ? rawSpan : 1;
1470
+ const style = { gridColumn: `span ${span}` };
1471
+ return /* @__PURE__ */ jsx("div", { style, "data-widget-slot": widget.name, children: /* @__PURE__ */ jsx(WidgetRenderer, { widget, filterValues: values }) }, widget.name);
1472
+ })
1473
+ }
1474
+ )
1475
+ ] });
1476
+ }
1477
+ function gridColsClass(columns) {
1478
+ if (typeof columns === "number") {
1479
+ return `grid-cols-${Math.max(1, Math.min(12, columns))}`;
1480
+ }
1481
+ const parts = [];
1482
+ parts.push("grid-cols-1");
1483
+ for (const bp of ["sm", "md", "lg", "xl", "2xl"]) {
1484
+ const value = columns[bp];
1485
+ if (typeof value === "number") {
1486
+ parts.push(`${BREAKPOINT_PREFIX[bp]}grid-cols-${Math.max(1, Math.min(12, value))}`);
1487
+ }
1488
+ }
1489
+ return parts.join(" ");
1490
+ }
1491
+ function normaliseFilters(raw) {
1492
+ if (!raw) return [];
1493
+ if (Array.isArray(raw)) return raw;
1494
+ return [];
1495
+ }
1496
+ var BREAKPOINT_PREFIX;
1497
+ var init_DashboardGrid = __esm({
1498
+ "src/widgets/DashboardGrid.tsx"() {
1499
+ init_cn();
1500
+ init_DashboardFilters();
1501
+ init_WidgetRenderer();
1502
+ BREAKPOINT_PREFIX = {
1503
+ sm: "sm:",
1504
+ md: "md:",
1505
+ lg: "lg:",
1506
+ xl: "xl:",
1507
+ "2xl": "2xl:"
1508
+ };
1509
+ }
1510
+ });
1511
+
1512
+ // src/widgets/index.ts
1513
+ var init_widgets = __esm({
1514
+ "src/widgets/index.ts"() {
1515
+ init_DashboardGrid();
1516
+ }
1517
+ });
1518
+
1519
+ // src/pages/ArqelDashboardPage.tsx
1520
+ var ArqelDashboardPage_exports = {};
1521
+ __export(ArqelDashboardPage_exports, {
1522
+ default: () => ArqelDashboardPage
1523
+ });
1524
+ function ArqelDashboardPage() {
1525
+ const { dashboard, filterValues } = usePage().props;
1526
+ const handleFilterChange = (name, value) => {
1527
+ const current = filterValues ?? {};
1528
+ const next = { ...current };
1529
+ if (value === void 0 || value === null || value === "") {
1530
+ delete next[name];
1531
+ } else {
1532
+ next[name] = value;
1533
+ }
1534
+ const path = dashboard.path ?? `/admin/dashboards/${dashboard.id}`;
1535
+ const params = Object.keys(next).length > 0 ? { filters: next } : {};
1536
+ router.get(path, params, {
1537
+ preserveState: true,
1538
+ preserveScroll: true,
1539
+ replace: true
1540
+ });
1541
+ };
1542
+ return /* @__PURE__ */ jsx(
1543
+ DashboardGrid,
1544
+ {
1545
+ dashboard,
1546
+ filterValues: filterValues ?? {},
1547
+ onFilterChange: handleFilterChange
1548
+ }
1549
+ );
1550
+ }
1551
+ var init_ArqelDashboardPage = __esm({
1552
+ "src/pages/ArqelDashboardPage.tsx"() {
1553
+ init_widgets();
1554
+ }
1555
+ });
811
1556
 
812
1557
  // src/pages/ArqelEditPage.tsx
813
1558
  var ArqelEditPage_exports = {};
@@ -1671,7 +2416,7 @@ function TableFilters({
1671
2416
  return /* @__PURE__ */ jsxs("fieldset", { className: cn("flex flex-wrap items-end gap-2 border-0 p-0", className), children: [
1672
2417
  /* @__PURE__ */ jsx("legend", { className: "sr-only", children: "Filters" }),
1673
2418
  filters.map((filter) => /* @__PURE__ */ jsx(
1674
- FilterControl,
2419
+ FilterControl2,
1675
2420
  {
1676
2421
  filter,
1677
2422
  value: values[filter.name],
@@ -1686,7 +2431,7 @@ function TableFilters({
1686
2431
  ] })
1687
2432
  ] });
1688
2433
  }
1689
- function FilterControl({
2434
+ function FilterControl2({
1690
2435
  filter,
1691
2436
  value,
1692
2437
  onChange
@@ -1705,7 +2450,7 @@ function FilterControl({
1705
2450
  return null;
1706
2451
  }
1707
2452
  }
1708
- function controlClasses() {
2453
+ function controlClasses2() {
1709
2454
  return cn(
1710
2455
  "h-9 rounded-sm border border-[var(--input)]",
1711
2456
  "bg-background px-3 text-sm",
@@ -1722,7 +2467,7 @@ function SelectFilter({
1722
2467
  /* @__PURE__ */ jsxs(
1723
2468
  "select",
1724
2469
  {
1725
- className: controlClasses(),
2470
+ className: controlClasses2(),
1726
2471
  value: value === void 0 || value === null ? "" : String(value),
1727
2472
  onChange: (e) => onChange(e.target.value === "" ? null : e.target.value),
1728
2473
  children: [
@@ -1745,7 +2490,7 @@ function MultiSelectFilter({
1745
2490
  "select",
1746
2491
  {
1747
2492
  multiple: true,
1748
- className: cn(controlClasses(), "h-auto min-h-[2.25rem] py-1"),
2493
+ className: cn(controlClasses2(), "h-auto min-h-[2.25rem] py-1"),
1749
2494
  value: arr.map(String),
1750
2495
  onChange: (e) => {
1751
2496
  const next = Array.from(e.target.selectedOptions, (o) => o.value);
@@ -1767,7 +2512,7 @@ function TextFilter({
1767
2512
  "input",
1768
2513
  {
1769
2514
  type: "text",
1770
- className: controlClasses(),
2515
+ className: controlClasses2(),
1771
2516
  value: typeof value === "string" ? value : "",
1772
2517
  onChange: (e) => onChange(e.target.value === "" ? null : e.target.value)
1773
2518
  }
@@ -1785,7 +2530,7 @@ function TernaryFilter({
1785
2530
  /* @__PURE__ */ jsxs(
1786
2531
  "select",
1787
2532
  {
1788
- className: controlClasses(),
2533
+ className: controlClasses2(),
1789
2534
  value: current,
1790
2535
  onChange: (e) => {
1791
2536
  const v = e.target.value;
@@ -2270,6 +3015,7 @@ var init_ArqelShowPage = __esm({
2270
3015
 
2271
3016
  // src/pages/index.ts
2272
3017
  init_ArqelCreatePage();
3018
+ init_ArqelDashboardPage();
2273
3019
  init_ArqelEditPage();
2274
3020
  init_ArqelIndexPage();
2275
3021
  init_ArqelShowPage();
@@ -2277,9 +3023,10 @@ var arqelPages = {
2277
3023
  "arqel::index": () => Promise.resolve().then(() => (init_ArqelIndexPage(), ArqelIndexPage_exports)),
2278
3024
  "arqel::create": () => Promise.resolve().then(() => (init_ArqelCreatePage(), ArqelCreatePage_exports)),
2279
3025
  "arqel::edit": () => Promise.resolve().then(() => (init_ArqelEditPage(), ArqelEditPage_exports)),
2280
- "arqel::show": () => Promise.resolve().then(() => (init_ArqelShowPage(), ArqelShowPage_exports))
3026
+ "arqel::show": () => Promise.resolve().then(() => (init_ArqelShowPage(), ArqelShowPage_exports)),
3027
+ "arqel::dashboard": () => Promise.resolve().then(() => (init_ArqelDashboardPage(), ArqelDashboardPage_exports))
2281
3028
  };
2282
3029
 
2283
- export { ArqelCreatePage, ArqelEditPage, ArqelIndexPage, ArqelShowPage, arqelPages };
3030
+ export { ArqelCreatePage, ArqelDashboardPage, ArqelEditPage, ArqelIndexPage, ArqelShowPage, arqelPages };
2284
3031
  //# sourceMappingURL=pages.js.map
2285
3032
  //# sourceMappingURL=pages.js.map