@richardmcquiston01/dashboard-widgets-toolkit 0.2.0 → 0.4.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/react.cjs CHANGED
@@ -6,6 +6,30 @@ var jsxRuntime = require('react/jsx-runtime');
6
6
  // src/react/card.tsx
7
7
 
8
8
  // src/core/grid.ts
9
+ var WIDTH_COLUMNS = 12;
10
+ var MIN_WIDGET_WIDTH = 2;
11
+ var MAX_WIDGET_WIDTH = 12;
12
+ function isWidgetWidth(value) {
13
+ return typeof value === "number" && Number.isInteger(value) && value >= MIN_WIDGET_WIDTH && value <= MAX_WIDGET_WIDTH;
14
+ }
15
+ function widthForSize(size) {
16
+ switch (size) {
17
+ case "small":
18
+ return 3;
19
+ case "large":
20
+ return 6;
21
+ case "full":
22
+ return 12;
23
+ default:
24
+ return 4;
25
+ }
26
+ }
27
+ function itemWidth(item) {
28
+ return isWidgetWidth(item.width) ? item.width : widthForSize(item.defaultSize);
29
+ }
30
+ function usesWidthColumns(items) {
31
+ return items.some((item) => item.width !== void 0);
32
+ }
9
33
  function fillsWidth(fill) {
10
34
  return fill === "width" || fill === "both";
11
35
  }
@@ -18,6 +42,16 @@ function baseColumnSpan(size, columns, largeSpan = 2) {
18
42
  return 1;
19
43
  }
20
44
  function fillColumnSpans(items, columns, largeSpan = 2) {
45
+ return placeAndFill(
46
+ items,
47
+ columns,
48
+ (item, trackCount) => baseColumnSpan(item.defaultSize, trackCount, largeSpan)
49
+ );
50
+ }
51
+ function fillWidthSpans(items) {
52
+ return placeAndFill(items, WIDTH_COLUMNS, itemWidth);
53
+ }
54
+ function placeAndFill(items, columns, spanOf) {
21
55
  const trackCount = Math.max(1, Math.floor(columns));
22
56
  const spans = /* @__PURE__ */ new Map();
23
57
  let row = [];
@@ -36,11 +70,7 @@ function fillColumnSpans(items, columns, largeSpan = 2) {
36
70
  used = 0;
37
71
  }
38
72
  for (const item of items) {
39
- const span = baseColumnSpan(
40
- item.defaultSize,
41
- trackCount,
42
- largeSpan
43
- );
73
+ const span = Math.min(spanOf(item, trackCount), trackCount);
44
74
  if (used + span > trackCount && row.length > 0) closeRow();
45
75
  row.push({ item, span });
46
76
  used += span;
@@ -192,11 +222,24 @@ var DEFAULT_LABELS = {
192
222
  viewAsTable: "View as table",
193
223
  hide: (title) => `Hide ${title}`,
194
224
  show: (title) => `Show ${title}`,
195
- minimize: (title) => `Minimise ${title}`,
225
+ minimize: (title) => `Minimize ${title}`,
196
226
  expand: (title) => `Expand ${title}`,
197
227
  moveEarlier: (title) => `Move ${title} earlier`,
198
228
  moveLater: (title) => `Move ${title} later`,
199
229
  hiddenWidgets: "Hidden:",
230
+ minimizedWidgets: "Minimized:",
231
+ opensInNewTab: "(opens in a new tab)",
232
+ view: (title) => `View ${title}`,
233
+ close: "Close",
234
+ search: "Search",
235
+ filterBy: (column) => `Filter ${column}`,
236
+ sortBy: (column) => `Sort by ${column}`,
237
+ noResults: "No matching results.",
238
+ showingRows: (from, to, total) => `Showing ${from}\u2013${to} of ${total} results`,
239
+ previousPage: "Previous",
240
+ nextPage: "Next",
241
+ pageOf: (page, pageCount) => `Page ${page} of ${pageCount}`,
242
+ pagination: "Pagination",
200
243
  allHidden: "Every widget is hidden. Show one from the list above.",
201
244
  noWidgets: "No widgets to show.",
202
245
  andMore: (count) => `and ${count} more`,
@@ -711,16 +754,50 @@ function isSafeImageUrl(url) {
711
754
  const scheme = schemeOf(url);
712
755
  return scheme === null || scheme === "http" || scheme === "https";
713
756
  }
757
+ function isNewTabTarget(target) {
758
+ return target !== void 0 && target !== "" && target !== "_self";
759
+ }
714
760
  function WidgetLink({
715
761
  href,
716
762
  className,
717
763
  children
718
764
  }) {
719
- const { linkTarget } = useWidgetSettings();
765
+ const { linkTarget, labels } = useWidgetSettings();
720
766
  if (href === void 0 || !isSafeHref(href)) {
721
767
  return /* @__PURE__ */ jsxRuntime.jsx("span", { className, children });
722
768
  }
723
- return /* @__PURE__ */ jsxRuntime.jsx("a", { href, target: linkTarget, rel: "noreferrer", className, children });
769
+ const opensNewTab = isNewTabTarget(linkTarget);
770
+ return /* @__PURE__ */ jsxRuntime.jsxs("a", { href, target: linkTarget, rel: "noreferrer", className, children: [
771
+ children,
772
+ opensNewTab && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
773
+ /* @__PURE__ */ jsxRuntime.jsx(
774
+ "svg",
775
+ {
776
+ className: "dwt-external-icon",
777
+ viewBox: "0 0 12 12",
778
+ width: "1em",
779
+ height: "1em",
780
+ "aria-hidden": "true",
781
+ focusable: "false",
782
+ children: /* @__PURE__ */ jsxRuntime.jsx(
783
+ "path",
784
+ {
785
+ d: "M7 1h4v4M11 1 5.5 6.5M9 7.5V10a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h2.5",
786
+ fill: "none",
787
+ stroke: "currentColor",
788
+ strokeWidth: "1.4",
789
+ strokeLinecap: "round",
790
+ strokeLinejoin: "round"
791
+ }
792
+ )
793
+ }
794
+ ),
795
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "dwt-visually-hidden", children: [
796
+ " ",
797
+ labels.opensInNewTab
798
+ ] })
799
+ ] })
800
+ ] });
724
801
  }
725
802
  function Thumbnail({
726
803
  url
@@ -942,6 +1019,7 @@ function WidgetCard({
942
1019
  actions,
943
1020
  minimized = false,
944
1021
  onToggleMinimized,
1022
+ onView,
945
1023
  status = "ok",
946
1024
  emptyText,
947
1025
  error,
@@ -950,6 +1028,8 @@ function WidgetCard({
950
1028
  size,
951
1029
  fill,
952
1030
  columnSpan,
1031
+ width,
1032
+ widgetKey,
953
1033
  headingLevel = 2,
954
1034
  className,
955
1035
  children
@@ -999,7 +1079,13 @@ function WidgetCard({
999
1079
  fillsWidth(fill) && "dwt-card--fill-width",
1000
1080
  className
1001
1081
  ),
1002
- ...columnSpan === void 0 ? {} : { style: { gridColumn: `span ${columnSpan}` } },
1082
+ ...widgetKey === void 0 ? {} : { "data-widget-key": widgetKey },
1083
+ ...columnSpan === void 0 && width === void 0 ? {} : {
1084
+ style: {
1085
+ ...width === void 0 ? {} : { "--dwt-width": width },
1086
+ ...columnSpan === void 0 ? {} : { gridColumn: `span ${columnSpan}` }
1087
+ }
1088
+ },
1003
1089
  children: [
1004
1090
  /* @__PURE__ */ jsxRuntime.jsxs("header", { className: slot("cardHeader", "dwt-card-header"), children: [
1005
1091
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "dwt-card-heading", children: [
@@ -1008,12 +1094,54 @@ function WidgetCard({
1008
1094
  {
1009
1095
  id: `${id}-title`,
1010
1096
  className: slot("cardTitle", "dwt-card-title"),
1011
- children: title
1097
+ children: onView === void 0 ? title : /* @__PURE__ */ jsxRuntime.jsx(
1098
+ "button",
1099
+ {
1100
+ type: "button",
1101
+ tabIndex: -1,
1102
+ className: "dwt-card-title-button",
1103
+ onClick: onView,
1104
+ children: title
1105
+ }
1106
+ )
1012
1107
  }
1013
1108
  ),
1014
1109
  description !== void 0 && !minimized && /* @__PURE__ */ jsxRuntime.jsx("p", { className: slot("cardDescription", "dwt-card-description"), children: description })
1015
1110
  ] }),
1016
- (actions !== void 0 || onToggleMinimized !== void 0) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: slot("cardActions", "dwt-card-actions"), children: [
1111
+ (actions !== void 0 || onToggleMinimized !== void 0 || onView !== void 0) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: slot("cardActions", "dwt-card-actions"), children: [
1112
+ onView !== void 0 && /* @__PURE__ */ jsxRuntime.jsx(
1113
+ "button",
1114
+ {
1115
+ type: "button",
1116
+ className: slot("button", "dwt-button", "dwt-icon-button"),
1117
+ "aria-label": labels.view(title),
1118
+ title: labels.view(title),
1119
+ onClick: onView,
1120
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
1121
+ "svg",
1122
+ {
1123
+ viewBox: "0 0 16 16",
1124
+ width: "16",
1125
+ height: "16",
1126
+ "aria-hidden": "true",
1127
+ focusable: "false",
1128
+ children: [
1129
+ /* @__PURE__ */ jsxRuntime.jsx(
1130
+ "path",
1131
+ {
1132
+ d: "M1 8s2.6-4.5 7-4.5S15 8 15 8s-2.6 4.5-7 4.5S1 8 1 8Z",
1133
+ fill: "none",
1134
+ stroke: "currentColor",
1135
+ strokeWidth: "1.4",
1136
+ strokeLinejoin: "round"
1137
+ }
1138
+ ),
1139
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "8", cy: "8", r: "2.1", fill: "currentColor" })
1140
+ ]
1141
+ }
1142
+ )
1143
+ }
1144
+ ),
1017
1145
  actions,
1018
1146
  onToggleMinimized !== void 0 && /* @__PURE__ */ jsxRuntime.jsx(
1019
1147
  "button",
@@ -1059,6 +1187,7 @@ function ResolvedWidgetCard({
1059
1187
  WidgetCard,
1060
1188
  {
1061
1189
  ...props,
1190
+ widgetKey: definition.key,
1062
1191
  title: widgetTitle(widget),
1063
1192
  status: widget.status,
1064
1193
  ...definition.description === void 0 ? {} : { description: definition.description },
@@ -1157,8 +1286,540 @@ function restoreWidget(layout, key) {
1157
1286
  function toggleMinimized(layout, key) {
1158
1287
  return isMinimized(layout, key) ? restoreWidget(layout, key) : minimizeWidget(layout, key);
1159
1288
  }
1289
+
1290
+ // src/core/result.ts
1291
+ function ok(value) {
1292
+ return { ok: true, value };
1293
+ }
1294
+ function err(error) {
1295
+ return { ok: false, error };
1296
+ }
1297
+ function describeUnknownError(cause) {
1298
+ if (cause instanceof Error) {
1299
+ return cause.message === "" ? cause.name : cause.message;
1300
+ }
1301
+ if (typeof cause === "string") {
1302
+ return cause;
1303
+ }
1304
+ try {
1305
+ const json = JSON.stringify(cause);
1306
+ return json ?? String(cause);
1307
+ } catch {
1308
+ return String(cause);
1309
+ }
1310
+ }
1311
+
1312
+ // src/core/detail.ts
1313
+ var DEFAULT_DETAIL_PAGE_SIZE = 25;
1314
+ function resolveDetailOptions(definition) {
1315
+ const detail = definition.detail;
1316
+ if (detail === void 0 || detail === false) return void 0;
1317
+ const options = detail === true ? {} : detail;
1318
+ return {
1319
+ title: options.title ?? definition.title,
1320
+ pageSize: options.pageSize ?? DEFAULT_DETAIL_PAGE_SIZE,
1321
+ mode: options.mode ?? "client"
1322
+ };
1323
+ }
1324
+ function defaultDetailQuery(pageSize = DEFAULT_DETAIL_PAGE_SIZE) {
1325
+ return { page: 1, pageSize };
1326
+ }
1327
+ function fold(text) {
1328
+ return text.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase();
1329
+ }
1330
+ function cellValue(cell) {
1331
+ return cell.value ?? cell.text;
1332
+ }
1333
+ function cellMatches(cell, needle) {
1334
+ return fold(String(cellValue(cell))).includes(needle);
1335
+ }
1336
+ function compareCells(a, b, collator) {
1337
+ const left = cellValue(a);
1338
+ const right = cellValue(b);
1339
+ if (typeof left === "number" && typeof right === "number") {
1340
+ return left - right;
1341
+ }
1342
+ if (typeof left === "number") return -1;
1343
+ if (typeof right === "number") return 1;
1344
+ return collator.compare(left, right);
1345
+ }
1346
+ function queryRows(data, query, locale) {
1347
+ const indexOf = new Map(
1348
+ data.columns.map((column, index) => [column.key, index])
1349
+ );
1350
+ const filterEntries = [];
1351
+ for (const [key, text] of Object.entries(query.filters ?? {})) {
1352
+ const index = indexOf.get(key);
1353
+ if (index === void 0) {
1354
+ return err(
1355
+ `Detail query: filter column "${key}" does not exist. Columns: ${listKeys(data)}.`
1356
+ );
1357
+ }
1358
+ const needle = fold(text.trim());
1359
+ if (needle !== "") filterEntries.push({ index, needle });
1360
+ }
1361
+ let sortIndex = -1;
1362
+ if (query.sort !== void 0) {
1363
+ const found = indexOf.get(query.sort.column);
1364
+ const column = found === void 0 ? void 0 : data.columns[found];
1365
+ if (found === void 0 || column === void 0) {
1366
+ return err(
1367
+ `Detail query: sort column "${query.sort.column}" does not exist. Columns: ${listKeys(data)}.`
1368
+ );
1369
+ }
1370
+ if (column.sortable === false) {
1371
+ return err(
1372
+ `Detail query: column "${column.key}" is not sortable (sortable: false).`
1373
+ );
1374
+ }
1375
+ sortIndex = found;
1376
+ }
1377
+ const search = fold((query.search ?? "").trim());
1378
+ let rows = data.rows.filter(
1379
+ (row) => filterEntries.every(({ index, needle }) => {
1380
+ const cell = row[index];
1381
+ return cell !== void 0 && cellMatches(cell, needle);
1382
+ }) && (search === "" || row.some((cell) => cellMatches(cell, search)))
1383
+ );
1384
+ if (query.sort !== void 0 && sortIndex >= 0) {
1385
+ const collator = new Intl.Collator(locale, {
1386
+ numeric: true,
1387
+ sensitivity: "base"
1388
+ });
1389
+ const sign = query.sort.direction === "desc" ? -1 : 1;
1390
+ rows = rows.map((row, position) => ({ row, position })).sort((a, b) => {
1391
+ const left = a.row[sortIndex];
1392
+ const right = b.row[sortIndex];
1393
+ const order = left === void 0 || right === void 0 ? 0 : sign * compareCells(left, right, collator);
1394
+ return order || a.position - b.position;
1395
+ }).map(({ row }) => row);
1396
+ }
1397
+ const pageSize = Math.max(1, Math.floor(query.pageSize));
1398
+ const pageCount = Math.max(1, Math.ceil(rows.length / pageSize));
1399
+ const page = Math.min(Math.max(1, Math.floor(query.page)), pageCount);
1400
+ return ok({
1401
+ rows: rows.slice((page - 1) * pageSize, page * pageSize),
1402
+ totalRows: rows.length,
1403
+ page,
1404
+ pageCount
1405
+ });
1406
+ }
1407
+ function listKeys(data) {
1408
+ return data.columns.map((column) => `"${column.key}"`).join(", ");
1409
+ }
1410
+ function describeType(value) {
1411
+ if (value === null) return "null";
1412
+ if (Array.isArray(value)) return "array";
1413
+ return typeof value;
1414
+ }
1415
+ function validateDetailData(data, subject = "Detail data") {
1416
+ const problems = [];
1417
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
1418
+ return err(`${subject}: must be an object, got ${describeType(data)}.`);
1419
+ }
1420
+ const record = data;
1421
+ const columns = record["columns"];
1422
+ const rows = record["rows"];
1423
+ const keys = /* @__PURE__ */ new Set();
1424
+ let columnCount = 0;
1425
+ if (!Array.isArray(columns) || columns.length === 0) {
1426
+ problems.push(
1427
+ `columns must be a non-empty array, got ${describeType(columns)}.`
1428
+ );
1429
+ } else {
1430
+ columnCount = columns.length;
1431
+ columns.forEach((column, index) => {
1432
+ const at = `columns[${index}]`;
1433
+ if (typeof column !== "object" || column === null) {
1434
+ problems.push(`${at} must be an object, got ${describeType(column)}.`);
1435
+ return;
1436
+ }
1437
+ const fields = column;
1438
+ const key = fields["key"];
1439
+ if (typeof key !== "string" || key === "") {
1440
+ problems.push(`${at}.key must be a non-empty string.`);
1441
+ } else if (keys.has(key)) {
1442
+ problems.push(`${at}.key "${key}" is used by an earlier column.`);
1443
+ } else {
1444
+ keys.add(key);
1445
+ }
1446
+ if (typeof fields["label"] !== "string") {
1447
+ problems.push(
1448
+ `${at}.label must be a string, got ${describeType(fields["label"])}.`
1449
+ );
1450
+ }
1451
+ for (const flag of ["numeric", "sortable", "filterable"]) {
1452
+ const flagValue = fields[flag];
1453
+ if (flagValue !== void 0 && typeof flagValue !== "boolean") {
1454
+ problems.push(
1455
+ `${at}.${flag} must be a boolean, got ${describeType(flagValue)}.`
1456
+ );
1457
+ }
1458
+ }
1459
+ });
1460
+ }
1461
+ if (!Array.isArray(rows)) {
1462
+ problems.push(`rows must be an array, got ${describeType(rows)}.`);
1463
+ } else {
1464
+ rows.forEach((row, rowIndex) => {
1465
+ if (!Array.isArray(row)) {
1466
+ problems.push(
1467
+ `rows[${rowIndex}] must be an array of cells, got ${describeType(row)}.`
1468
+ );
1469
+ return;
1470
+ }
1471
+ if (columnCount > 0 && row.length !== columnCount) {
1472
+ problems.push(
1473
+ `rows[${rowIndex}] has ${row.length} cells but there are ${columnCount} columns.`
1474
+ );
1475
+ }
1476
+ row.forEach((cell, cellIndex) => {
1477
+ const at = `rows[${rowIndex}][${cellIndex}]`;
1478
+ if (typeof cell !== "object" || cell === null) {
1479
+ problems.push(`${at} must be an object, got ${describeType(cell)}.`);
1480
+ return;
1481
+ }
1482
+ const fields = cell;
1483
+ if (typeof fields["text"] !== "string") {
1484
+ problems.push(
1485
+ `${at}.text must be a string, got ${describeType(fields["text"])}.`
1486
+ );
1487
+ }
1488
+ const value = fields["value"];
1489
+ if (value !== void 0 && typeof value !== "string" && !(typeof value === "number" && Number.isFinite(value))) {
1490
+ problems.push(
1491
+ `${at}.value must be a string or a finite number, got ${describeType(value)}.`
1492
+ );
1493
+ }
1494
+ const href = fields["href"];
1495
+ if (href !== void 0) {
1496
+ if (typeof href !== "string") {
1497
+ problems.push(
1498
+ `${at}.href must be a string, got ${describeType(href)}.`
1499
+ );
1500
+ } else if (!isSafeHref(href)) {
1501
+ problems.push(
1502
+ `${at}.href "${href}" is not allowed (only http, https, mailto and relative URLs).`
1503
+ );
1504
+ }
1505
+ }
1506
+ });
1507
+ });
1508
+ }
1509
+ const totalRows = record["totalRows"];
1510
+ if (totalRows !== void 0 && !(typeof totalRows === "number" && Number.isInteger(totalRows) && totalRows >= 0)) {
1511
+ problems.push(
1512
+ `totalRows must be a non-negative whole number, got ${describeType(totalRows)}.`
1513
+ );
1514
+ }
1515
+ if (problems.length > 0) {
1516
+ return err(`${subject}: ${problems.join(" ")}`);
1517
+ }
1518
+ return ok(data);
1519
+ }
1520
+ function deriveDetailData(data, locale) {
1521
+ if (data.kind === "TABLE") {
1522
+ if (data.footer !== void 0) return void 0;
1523
+ return {
1524
+ columns: data.columns.map((column, index) => ({
1525
+ key: `c${index}`,
1526
+ label: column.label,
1527
+ ...column.numeric === void 0 ? {} : { numeric: column.numeric }
1528
+ })),
1529
+ rows: data.rows
1530
+ };
1531
+ }
1532
+ if (data.kind === "BAR_LIST") {
1533
+ return {
1534
+ columns: [
1535
+ { key: "label", label: "Name" },
1536
+ { key: "value", label: "Value", numeric: true }
1537
+ ],
1538
+ rows: data.items.map((item) => [
1539
+ { text: item.label },
1540
+ {
1541
+ text: item.display ?? formatValue(item.value, "number", { locale }),
1542
+ value: item.value
1543
+ }
1544
+ ])
1545
+ };
1546
+ }
1547
+ return void 0;
1548
+ }
1549
+ function nextSort(current, column) {
1550
+ if (current?.column !== column) return { column, direction: "asc" };
1551
+ return current.direction === "asc" ? { column, direction: "desc" } : void 0;
1552
+ }
1553
+ function withSort(query, sort) {
1554
+ const { sort: _previous, ...rest } = query;
1555
+ return sort === void 0 ? { ...rest, page: 1 } : { ...rest, sort, page: 1 };
1556
+ }
1557
+ function SortGlyph({
1558
+ sort,
1559
+ column
1560
+ }) {
1561
+ if (sort?.column !== column) return /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: " \u2195" });
1562
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: sort.direction === "asc" ? " \u2191" : " \u2193" });
1563
+ }
1564
+ function Cell({
1565
+ cell,
1566
+ column
1567
+ }) {
1568
+ return /* @__PURE__ */ jsxRuntime.jsx("td", { className: column?.numeric === true ? "dwt-numeric" : void 0, children: cell.href === void 0 ? cell.text : /* @__PURE__ */ jsxRuntime.jsx(WidgetLink, { href: cell.href, className: "dwt-link", children: cell.text }) });
1569
+ }
1570
+ function WidgetDetail({
1571
+ title,
1572
+ data,
1573
+ query,
1574
+ onQueryChange,
1575
+ status = "ok",
1576
+ error,
1577
+ onRetry,
1578
+ headingLevel = 2,
1579
+ headingId,
1580
+ className
1581
+ }) {
1582
+ const slot = useSlotClassName();
1583
+ const { labels, locale } = useWidgetSettings();
1584
+ const id = react.useId();
1585
+ const Heading = `h${headingLevel}`;
1586
+ const localeTag = typeof locale === "string" ? locale : locale?.[0];
1587
+ let page;
1588
+ let problem;
1589
+ if (status === "ok" && data !== void 0) {
1590
+ const result = queryRows(data, query, localeTag);
1591
+ if (result.ok) page = result.value;
1592
+ else problem = result.error;
1593
+ }
1594
+ function change(patch) {
1595
+ onQueryChange({ ...query, ...patch, page: patch.page ?? 1 });
1596
+ }
1597
+ let body;
1598
+ if (status === "loading") {
1599
+ body = /* @__PURE__ */ jsxRuntime.jsx("p", { role: "status", className: slot("status", "dwt-status"), children: labels.loading });
1600
+ } else if (status === "error" || problem !== void 0) {
1601
+ body = /* @__PURE__ */ jsxRuntime.jsxs("div", { role: "alert", className: slot("error", "dwt-error"), children: [
1602
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "dwt-error-message", children: error ?? problem ?? "" }),
1603
+ onRetry !== void 0 && /* @__PURE__ */ jsxRuntime.jsx(
1604
+ "button",
1605
+ {
1606
+ type: "button",
1607
+ className: slot("button", "dwt-button"),
1608
+ onClick: onRetry,
1609
+ children: labels.retry
1610
+ }
1611
+ )
1612
+ ] });
1613
+ } else if (data !== void 0 && page !== void 0) {
1614
+ const columns = data.columns;
1615
+ const from = (page.page - 1) * query.pageSize + 1;
1616
+ const to = from + page.rows.length - 1;
1617
+ body = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1618
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "dwt-detail-controls", children: [
1619
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "dwt-detail-field", children: [
1620
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: labels.search }),
1621
+ /* @__PURE__ */ jsxRuntime.jsx(
1622
+ "input",
1623
+ {
1624
+ type: "search",
1625
+ value: query.search ?? "",
1626
+ onChange: (event) => change({ search: event.target.value })
1627
+ }
1628
+ )
1629
+ ] }),
1630
+ columns.filter((column) => column.filterable === true).map((column) => /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "dwt-detail-field", children: [
1631
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: labels.filterBy(column.label) }),
1632
+ /* @__PURE__ */ jsxRuntime.jsx(
1633
+ "input",
1634
+ {
1635
+ type: "search",
1636
+ value: query.filters?.[column.key] ?? "",
1637
+ onChange: (event) => change({
1638
+ filters: {
1639
+ ...query.filters,
1640
+ [column.key]: event.target.value
1641
+ }
1642
+ })
1643
+ }
1644
+ )
1645
+ ] }, column.key))
1646
+ ] }),
1647
+ /* @__PURE__ */ jsxRuntime.jsx(
1648
+ "p",
1649
+ {
1650
+ className: "dwt-detail-summary",
1651
+ role: "status",
1652
+ "aria-live": "polite",
1653
+ "aria-atomic": "true",
1654
+ children: page.totalRows === 0 ? labels.noResults : labels.showingRows(from, to, page.totalRows)
1655
+ }
1656
+ ),
1657
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "dwt-table-wrap", children: /* @__PURE__ */ jsxRuntime.jsxs("table", { className: slot("table", "dwt-table", "dwt-detail-table"), children: [
1658
+ /* @__PURE__ */ jsxRuntime.jsx("caption", { className: "dwt-visually-hidden", children: title }),
1659
+ /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsx("tr", { children: columns.map((column) => {
1660
+ const sorted = query.sort?.column === column.key;
1661
+ return /* @__PURE__ */ jsxRuntime.jsx(
1662
+ "th",
1663
+ {
1664
+ scope: "col",
1665
+ className: column.numeric === true ? "dwt-numeric" : void 0,
1666
+ "aria-sort": sorted ? query.sort?.direction === "asc" ? "ascending" : "descending" : void 0,
1667
+ children: column.sortable === false ? column.label : /* @__PURE__ */ jsxRuntime.jsxs(
1668
+ "button",
1669
+ {
1670
+ type: "button",
1671
+ className: "dwt-detail-sort",
1672
+ "aria-label": labels.sortBy(column.label),
1673
+ onClick: () => onQueryChange(
1674
+ withSort(query, nextSort(query.sort, column.key))
1675
+ ),
1676
+ children: [
1677
+ column.label,
1678
+ /* @__PURE__ */ jsxRuntime.jsx(SortGlyph, { sort: query.sort, column: column.key })
1679
+ ]
1680
+ }
1681
+ )
1682
+ },
1683
+ column.key
1684
+ );
1685
+ }) }) }),
1686
+ /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: page.rows.map((row, rowIndex) => /* @__PURE__ */ jsxRuntime.jsx("tr", { children: row.map((cell, cellIndex) => /* @__PURE__ */ jsxRuntime.jsx(
1687
+ Cell,
1688
+ {
1689
+ cell,
1690
+ column: columns[cellIndex]
1691
+ },
1692
+ cellIndex
1693
+ )) }, rowIndex)) })
1694
+ ] }) }),
1695
+ page.pageCount > 1 && /* @__PURE__ */ jsxRuntime.jsxs("nav", { className: "dwt-detail-pager", "aria-label": labels.pagination, children: [
1696
+ /* @__PURE__ */ jsxRuntime.jsx(
1697
+ "button",
1698
+ {
1699
+ type: "button",
1700
+ className: slot("button", "dwt-button"),
1701
+ disabled: page.page <= 1,
1702
+ onClick: () => onQueryChange({ ...query, page: page.page - 1 }),
1703
+ children: labels.previousPage
1704
+ }
1705
+ ),
1706
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: labels.pageOf(page.page, page.pageCount) }),
1707
+ /* @__PURE__ */ jsxRuntime.jsx(
1708
+ "button",
1709
+ {
1710
+ type: "button",
1711
+ className: slot("button", "dwt-button"),
1712
+ disabled: page.page >= page.pageCount,
1713
+ onClick: () => onQueryChange({ ...query, page: page.page + 1 }),
1714
+ children: labels.nextPage
1715
+ }
1716
+ )
1717
+ ] })
1718
+ ] });
1719
+ }
1720
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1721
+ "section",
1722
+ {
1723
+ "aria-labelledby": headingId ?? `${id}-title`,
1724
+ className: slot("detail", "dwt-detail", className),
1725
+ children: [
1726
+ /* @__PURE__ */ jsxRuntime.jsx(
1727
+ Heading,
1728
+ {
1729
+ id: headingId ?? `${id}-title`,
1730
+ className: slot("cardTitle", "dwt-card-title", "dwt-detail-title"),
1731
+ children: title
1732
+ }
1733
+ ),
1734
+ body
1735
+ ]
1736
+ }
1737
+ );
1738
+ }
1739
+ function WidgetDetailDialog({
1740
+ title,
1741
+ open,
1742
+ onClose,
1743
+ children
1744
+ }) {
1745
+ const slot = useSlotClassName();
1746
+ const { labels } = useWidgetSettings();
1747
+ const dialogRef = react.useRef(null);
1748
+ const titleId = react.useId();
1749
+ react.useEffect(() => {
1750
+ const dialog = dialogRef.current;
1751
+ if (dialog === null || !open) return void 0;
1752
+ const opener = document.activeElement;
1753
+ if (!dialog.open) dialog.showModal();
1754
+ return () => {
1755
+ if (dialog.open) dialog.close();
1756
+ if (opener instanceof HTMLElement) opener.focus();
1757
+ };
1758
+ }, [open]);
1759
+ return /* @__PURE__ */ jsxRuntime.jsx(
1760
+ "dialog",
1761
+ {
1762
+ ref: dialogRef,
1763
+ className: "dwt-detail-dialog",
1764
+ "aria-labelledby": titleId,
1765
+ onCancel: (event) => {
1766
+ event.preventDefault();
1767
+ onClose();
1768
+ },
1769
+ onClick: (event) => {
1770
+ if (event.target === dialogRef.current) onClose();
1771
+ },
1772
+ children: open && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "dwt-detail-dialog-body", children: [
1773
+ /* @__PURE__ */ jsxRuntime.jsx(
1774
+ "button",
1775
+ {
1776
+ type: "button",
1777
+ className: slot("button", "dwt-button", "dwt-detail-close"),
1778
+ onClick: onClose,
1779
+ children: labels.close
1780
+ }
1781
+ ),
1782
+ /* @__PURE__ */ jsxRuntime.jsx("span", { id: titleId, className: "dwt-visually-hidden", children: title }),
1783
+ children
1784
+ ] })
1785
+ }
1786
+ );
1787
+ }
1788
+ function useDetailData(definition, load, enabled) {
1789
+ const [state, setState] = react.useState({ status: "loading" });
1790
+ const [attempt, setAttempt] = react.useState(0);
1791
+ react.useEffect(() => {
1792
+ if (!enabled || load === void 0) return void 0;
1793
+ const controller = new AbortController();
1794
+ setState({ status: "loading" });
1795
+ Promise.resolve().then(
1796
+ () => load(definition, {
1797
+ signal: controller.signal,
1798
+ query: defaultDetailQuery()
1799
+ })
1800
+ ).then((raw) => {
1801
+ if (controller.signal.aborted) return;
1802
+ const checked = validateDetailData(
1803
+ raw,
1804
+ `Widget "${definition.key}" detail`
1805
+ );
1806
+ setState(
1807
+ checked.ok ? { status: "ok", data: checked.value } : { status: "error", error: checked.error }
1808
+ );
1809
+ }).catch((cause) => {
1810
+ if (controller.signal.aborted) return;
1811
+ setState({
1812
+ status: "error",
1813
+ error: `Widget "${definition.key}" detail: ${cause instanceof Error ? cause.message : String(cause)}`
1814
+ });
1815
+ });
1816
+ return () => controller.abort();
1817
+ }, [enabled, load, definition, attempt]);
1818
+ return { state, reload: () => setAttempt((count) => count + 1) };
1819
+ }
1160
1820
  var NARROW_VIEWPORT_QUERY = "(max-width: 640px)";
1161
- function useFillSpans(definitions, gridRef) {
1821
+ var WIDE_GRID_PX = 900;
1822
+ function useFillSpans(definitions, gridRef, twelve) {
1162
1823
  const [metrics, setMetrics] = react.useState(void 0);
1163
1824
  const needsSpans = definitions.some(
1164
1825
  (definition) => fillsWidth(definition.fill)
@@ -1171,6 +1832,13 @@ function useFillSpans(definitions, gridRef) {
1171
1832
  }
1172
1833
  function measure() {
1173
1834
  if (grid === null) return;
1835
+ if (twelve) {
1836
+ const wide = grid.clientWidth > WIDE_GRID_PX;
1837
+ setMetrics(
1838
+ (previous) => !wide ? void 0 : previous?.columns === WIDTH_COLUMNS ? previous : { columns: WIDTH_COLUMNS, largeSpan: 2 }
1839
+ );
1840
+ return;
1841
+ }
1174
1842
  const tracks = getComputedStyle(grid).gridTemplateColumns;
1175
1843
  const columns = tracks === "" || tracks === "none" ? 0 : tracks.trim().split(/\s+/).length;
1176
1844
  const largeSpan = window.matchMedia(NARROW_VIEWPORT_QUERY).matches ? 1 : 2;
@@ -1182,12 +1850,15 @@ function useFillSpans(definitions, gridRef) {
1182
1850
  const observer = new ResizeObserver(measure);
1183
1851
  observer.observe(grid);
1184
1852
  return () => observer.disconnect();
1185
- }, [needsSpans, gridRef]);
1853
+ }, [needsSpans, gridRef, twelve]);
1186
1854
  return react.useMemo(
1187
- () => metrics === void 0 ? /* @__PURE__ */ new Map() : fillColumnSpans(definitions, metrics.columns, metrics.largeSpan),
1188
- [definitions, metrics]
1855
+ () => metrics === void 0 ? /* @__PURE__ */ new Map() : twelve ? fillWidthSpans(definitions) : fillColumnSpans(definitions, metrics.columns, metrics.largeSpan),
1856
+ [definitions, metrics, twelve]
1189
1857
  );
1190
1858
  }
1859
+ function widthProps(twelve, definition) {
1860
+ return twelve ? { width: itemWidth(definition) } : {};
1861
+ }
1191
1862
  function spanProps(spans, key) {
1192
1863
  const columnSpan = spans.get(key);
1193
1864
  return columnSpan === void 0 ? {} : { columnSpan };
@@ -1201,23 +1872,76 @@ function WidgetGrid({
1201
1872
  const slot = useSlotClassName();
1202
1873
  const gridRef = react.useRef(null);
1203
1874
  const definitions = widgets.map((w) => w.definition);
1204
- const spans = useFillSpans(definitions, gridRef);
1205
- return /* @__PURE__ */ jsxRuntime.jsx("div", { ref: gridRef, className: slot("grid", "dwt-grid", className), children: widgets.map((widget) => /* @__PURE__ */ jsxRuntime.jsx(
1206
- ResolvedWidgetCard,
1875
+ const twelve = usesWidthColumns(definitions);
1876
+ const spans = useFillSpans(
1877
+ definitions,
1878
+ gridRef,
1879
+ twelve
1880
+ );
1881
+ return /* @__PURE__ */ jsxRuntime.jsx(
1882
+ "div",
1883
+ {
1884
+ ref: gridRef,
1885
+ className: slot(
1886
+ "grid",
1887
+ "dwt-grid",
1888
+ twelve && "dwt-grid--twelve",
1889
+ className
1890
+ ),
1891
+ children: widgets.map((widget) => /* @__PURE__ */ jsxRuntime.jsx(
1892
+ ResolvedWidgetCard,
1893
+ {
1894
+ widget,
1895
+ ...spanProps(spans, widget.definition.key),
1896
+ ...widthProps(twelve, widget.definition),
1897
+ ...headingLevel === void 0 ? {} : { headingLevel },
1898
+ ...onRetry === void 0 || widget.status !== "error" ? {} : { onRetry: () => onRetry(widget.definition.key) }
1899
+ },
1900
+ widget.definition.key
1901
+ ))
1902
+ }
1903
+ );
1904
+ }
1905
+ function DetailHost({
1906
+ widget,
1907
+ loadDetail,
1908
+ onClose
1909
+ }) {
1910
+ const { locale } = useWidgetSettings();
1911
+ const { definition } = widget;
1912
+ const options = resolveDetailOptions(definition);
1913
+ const pageSize = options?.pageSize ?? 25;
1914
+ const [query, setQuery] = react.useState(defaultDetailQuery(pageSize));
1915
+ const derived = widget.status === "ok" ? deriveDetailData(widget.data, locale) : void 0;
1916
+ const load = react.useMemo(
1917
+ () => loadDetail ?? (derived === void 0 ? void 0 : () => derived),
1918
+ [loadDetail, derived]
1919
+ );
1920
+ const { state, reload } = useDetailData(definition, load, true);
1921
+ const title = options?.title ?? definition.title;
1922
+ return /* @__PURE__ */ jsxRuntime.jsx(WidgetDetailDialog, { title, open: true, onClose, children: /* @__PURE__ */ jsxRuntime.jsx(
1923
+ WidgetDetail,
1207
1924
  {
1208
- widget,
1209
- ...spanProps(spans, widget.definition.key),
1210
- ...headingLevel === void 0 ? {} : { headingLevel },
1211
- ...onRetry === void 0 || widget.status !== "error" ? {} : { onRetry: () => onRetry(widget.definition.key) }
1212
- },
1213
- widget.definition.key
1214
- )) });
1925
+ title,
1926
+ query,
1927
+ onQueryChange: setQuery,
1928
+ status: load === void 0 ? "error" : state.status,
1929
+ ...load === void 0 ? {
1930
+ error: `Widget "${definition.key}" has no detail data: pass loadDetail, or use a TABLE without a footer or a BAR_LIST.`
1931
+ } : {},
1932
+ ...state.data === void 0 ? {} : { data: state.data },
1933
+ ...state.error === void 0 ? {} : { error: state.error },
1934
+ onRetry: reload
1935
+ }
1936
+ ) });
1215
1937
  }
1216
1938
  function DashboardInner({
1217
1939
  widgets,
1218
1940
  layout = EMPTY_LAYOUT,
1219
1941
  onLayoutChange,
1220
1942
  onRetry,
1943
+ loadDetail,
1944
+ onOpenDetail,
1221
1945
  headingLevel,
1222
1946
  className
1223
1947
  }) {
@@ -1230,8 +1954,16 @@ function DashboardInner({
1230
1954
  const visible = visibleWidgets(definitions, layout);
1231
1955
  const hidden = hiddenWidgets(definitions, layout);
1232
1956
  const editable = onLayoutChange !== void 0;
1957
+ const minimized = editable ? visible.filter((definition) => isMinimized(layout, definition.key)) : [];
1958
+ const shown = editable ? visible.filter((definition) => !isMinimized(layout, definition.key)) : visible;
1959
+ const [detailKey, setDetailKey] = react.useState(void 0);
1233
1960
  const gridRef = react.useRef(null);
1234
- const spans = useFillSpans(visible, gridRef);
1961
+ const twelve = usesWidthColumns(shown);
1962
+ const spans = useFillSpans(
1963
+ shown,
1964
+ gridRef,
1965
+ twelve
1966
+ );
1235
1967
  function change(next) {
1236
1968
  if (next !== layout) onLayoutChange?.(next);
1237
1969
  }
@@ -1253,66 +1985,110 @@ function DashboardInner({
1253
1985
  definition.key
1254
1986
  ))
1255
1987
  ] }),
1256
- visible.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: slot("empty", "dwt-empty", "dwt-dashboard-empty"), children: definitions.length === 0 ? labels.noWidgets : labels.allHidden }) : /* @__PURE__ */ jsxRuntime.jsx("div", { ref: gridRef, className: slot("grid", "dwt-grid"), children: visible.map((definition, index) => {
1257
- const widget = byKey.get(
1258
- definition.key
1259
- );
1260
- if (widget === void 0) return null;
1261
- const key = definition.key;
1262
- const title = definition.title;
1263
- const actions = editable ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1264
- /* @__PURE__ */ jsxRuntime.jsx(
1265
- "button",
1266
- {
1267
- type: "button",
1268
- className: slot("button", "dwt-button", "dwt-icon-button"),
1269
- "aria-label": labels.moveEarlier(title),
1270
- title: labels.moveEarlier(title),
1271
- disabled: index === 0,
1272
- onClick: () => change(moveWidgetBy(definitions, layout, key, -1)),
1273
- children: /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u2191" })
1274
- }
1275
- ),
1276
- /* @__PURE__ */ jsxRuntime.jsx(
1277
- "button",
1278
- {
1279
- type: "button",
1280
- className: slot("button", "dwt-button", "dwt-icon-button"),
1281
- "aria-label": labels.moveLater(title),
1282
- title: labels.moveLater(title),
1283
- disabled: index === visible.length - 1,
1284
- onClick: () => change(moveWidgetBy(definitions, layout, key, 1)),
1285
- children: /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u2193" })
1286
- }
1287
- ),
1288
- /* @__PURE__ */ jsxRuntime.jsx(
1289
- "button",
1290
- {
1291
- type: "button",
1292
- className: slot("button", "dwt-button", "dwt-icon-button"),
1293
- "aria-label": labels.hide(title),
1294
- title: labels.hide(title),
1295
- onClick: () => change(hideWidget(layout, key)),
1296
- children: /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\xD7" })
1297
- }
1298
- )
1299
- ] }) : void 0;
1300
- return /* @__PURE__ */ jsxRuntime.jsx(
1301
- ResolvedWidgetCard,
1302
- {
1303
- widget,
1304
- ...spanProps(spans, key),
1305
- minimized: isMinimized(layout, key),
1306
- ...actions === void 0 ? {} : { actions },
1307
- ...editable ? {
1308
- onToggleMinimized: () => change(toggleMinimized(layout, key))
1309
- } : {},
1310
- ...headingLevel === void 0 ? {} : { headingLevel },
1311
- ...onRetry === void 0 || widget.status !== "error" ? {} : { onRetry: () => onRetry(key) }
1312
- },
1313
- key
1314
- );
1315
- }) })
1988
+ minimized.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(
1989
+ "div",
1990
+ {
1991
+ className: slot("hiddenBar", "dwt-hidden-bar", "dwt-minimized-bar"),
1992
+ children: [
1993
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "dwt-hidden-label", children: labels.minimizedWidgets }),
1994
+ minimized.map((definition) => /* @__PURE__ */ jsxRuntime.jsxs(
1995
+ "button",
1996
+ {
1997
+ type: "button",
1998
+ className: slot("button", "dwt-button", "dwt-show-button"),
1999
+ "aria-label": labels.expand(definition.title),
2000
+ onClick: () => change(toggleMinimized(layout, definition.key)),
2001
+ children: [
2002
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u25B8 " }),
2003
+ definition.title
2004
+ ]
2005
+ },
2006
+ definition.key
2007
+ ))
2008
+ ]
2009
+ }
2010
+ ),
2011
+ visible.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: slot("empty", "dwt-empty", "dwt-dashboard-empty"), children: definitions.length === 0 ? labels.noWidgets : labels.allHidden }) : shown.length === 0 ? null : /* @__PURE__ */ jsxRuntime.jsx(
2012
+ "div",
2013
+ {
2014
+ ref: gridRef,
2015
+ className: slot("grid", "dwt-grid", twelve && "dwt-grid--twelve"),
2016
+ children: shown.map((definition, index) => {
2017
+ const widget = byKey.get(
2018
+ definition.key
2019
+ );
2020
+ if (widget === void 0) return null;
2021
+ const key = definition.key;
2022
+ const title = definition.title;
2023
+ const actions = editable ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2024
+ /* @__PURE__ */ jsxRuntime.jsx(
2025
+ "button",
2026
+ {
2027
+ type: "button",
2028
+ className: slot("button", "dwt-button", "dwt-icon-button"),
2029
+ "aria-label": labels.moveEarlier(title),
2030
+ title: labels.moveEarlier(title),
2031
+ disabled: index === 0,
2032
+ onClick: () => change(moveWidgetBy(definitions, layout, key, -1)),
2033
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u2191" })
2034
+ }
2035
+ ),
2036
+ /* @__PURE__ */ jsxRuntime.jsx(
2037
+ "button",
2038
+ {
2039
+ type: "button",
2040
+ className: slot("button", "dwt-button", "dwt-icon-button"),
2041
+ "aria-label": labels.moveLater(title),
2042
+ title: labels.moveLater(title),
2043
+ disabled: index === shown.length - 1,
2044
+ onClick: () => change(moveWidgetBy(definitions, layout, key, 1)),
2045
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u2193" })
2046
+ }
2047
+ ),
2048
+ /* @__PURE__ */ jsxRuntime.jsx(
2049
+ "button",
2050
+ {
2051
+ type: "button",
2052
+ className: slot("button", "dwt-button", "dwt-icon-button"),
2053
+ "aria-label": labels.hide(title),
2054
+ title: labels.hide(title),
2055
+ onClick: () => change(hideWidget(layout, key)),
2056
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\xD7" })
2057
+ }
2058
+ )
2059
+ ] }) : void 0;
2060
+ const viewable = resolveDetailOptions(definition) !== void 0 && (onOpenDetail !== void 0 || loadDetail !== void 0 || widget.status === "ok" && deriveDetailData(widget.data) !== void 0);
2061
+ return /* @__PURE__ */ jsxRuntime.jsx(
2062
+ ResolvedWidgetCard,
2063
+ {
2064
+ widget,
2065
+ ...viewable ? {
2066
+ onView: () => onOpenDetail === void 0 ? setDetailKey(key) : onOpenDetail(key)
2067
+ } : {},
2068
+ ...spanProps(spans, key),
2069
+ ...widthProps(twelve, definition),
2070
+ minimized: isMinimized(layout, key),
2071
+ ...actions === void 0 ? {} : { actions },
2072
+ ...editable ? {
2073
+ onToggleMinimized: () => change(toggleMinimized(layout, key))
2074
+ } : {},
2075
+ ...headingLevel === void 0 ? {} : { headingLevel },
2076
+ ...onRetry === void 0 || widget.status !== "error" ? {} : { onRetry: () => onRetry(key) }
2077
+ },
2078
+ key
2079
+ );
2080
+ })
2081
+ }
2082
+ ),
2083
+ detailKey !== void 0 && byKey.get(detailKey) !== void 0 && /* @__PURE__ */ jsxRuntime.jsx(
2084
+ DetailHost,
2085
+ {
2086
+ widget: byKey.get(detailKey),
2087
+ loadDetail,
2088
+ onClose: () => setDetailKey(void 0)
2089
+ },
2090
+ detailKey
2091
+ )
1316
2092
  ] });
1317
2093
  }
1318
2094
  function Dashboard({
@@ -1337,6 +2113,656 @@ function Dashboard({
1337
2113
  );
1338
2114
  }
1339
2115
 
2116
+ // src/core/definition.ts
2117
+ function sortDefinitions(definitions) {
2118
+ return definitions.map((definition, index) => ({ definition, index })).sort(
2119
+ (a, b) => (a.definition.sortOrder ?? 0) - (b.definition.sortOrder ?? 0) || a.index - b.index
2120
+ ).map(({ definition }) => definition);
2121
+ }
2122
+ function isWidgetVisibleTo(definition, roles) {
2123
+ if (definition.active === false) {
2124
+ return false;
2125
+ }
2126
+ if (definition.roles === void 0 || definition.roles.length === 0) {
2127
+ return true;
2128
+ }
2129
+ if (roles === void 0) {
2130
+ return false;
2131
+ }
2132
+ return definition.roles.some((role) => roles.includes(role));
2133
+ }
2134
+
2135
+ // src/core/payload.ts
2136
+ var WIDGET_KINDS = [
2137
+ "TEXT",
2138
+ "KPI",
2139
+ "GAUGE",
2140
+ "TABLE",
2141
+ "BAR_LIST",
2142
+ "ALERT_LIST",
2143
+ "GRAPH"
2144
+ ];
2145
+ function isWidgetKind(value) {
2146
+ return typeof value === "string" && WIDGET_KINDS.includes(value);
2147
+ }
2148
+ var VALUE_FORMATS = ["number", "currency", "percent"];
2149
+ var MAX_GRAPH_SERIES = 8;
2150
+ var DEFAULT_EMPTY_TEXT = "Nothing to show yet.";
2151
+ function isWidgetEmptyState(payload) {
2152
+ return "empty" in payload && payload.empty === true;
2153
+ }
2154
+ function emptyTextFor(payload) {
2155
+ if (isWidgetEmptyState(payload)) {
2156
+ return payload.text;
2157
+ }
2158
+ switch (payload.kind) {
2159
+ case "TABLE":
2160
+ return payload.rows.length === 0 ? DEFAULT_EMPTY_TEXT : null;
2161
+ case "BAR_LIST":
2162
+ return payload.items.length === 0 ? DEFAULT_EMPTY_TEXT : null;
2163
+ case "ALERT_LIST":
2164
+ return payload.items.length === 0 ? payload.emptyText : null;
2165
+ case "GRAPH":
2166
+ return payload.series.every((series) => series.points.length === 0) ? DEFAULT_EMPTY_TEXT : null;
2167
+ case "TEXT":
2168
+ case "KPI":
2169
+ case "GAUGE":
2170
+ return null;
2171
+ }
2172
+ }
2173
+
2174
+ // src/core/validate.ts
2175
+ var MAX_REPORTED_PROBLEMS = 10;
2176
+ function describeType2(value) {
2177
+ if (value === null) return "null";
2178
+ if (Array.isArray(value)) return "array";
2179
+ if (typeof value === "number" && !Number.isFinite(value)) {
2180
+ return String(value);
2181
+ }
2182
+ return typeof value;
2183
+ }
2184
+ function isRecord(value) {
2185
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2186
+ }
2187
+ function join(path, field) {
2188
+ return path === "" ? field : `${path}.${field}`;
2189
+ }
2190
+ function requireString(record, field, path, problems) {
2191
+ const value = record[field];
2192
+ if (typeof value !== "string") {
2193
+ problems.push(
2194
+ `${join(path, field)} must be a string, got ${describeType2(value)}.`
2195
+ );
2196
+ }
2197
+ }
2198
+ function optionalString(record, field, path, problems) {
2199
+ if (record[field] !== void 0) {
2200
+ requireString(record, field, path, problems);
2201
+ }
2202
+ }
2203
+ function requireNumber(record, field, path, problems) {
2204
+ const value = record[field];
2205
+ if (typeof value !== "number" || !Number.isFinite(value)) {
2206
+ problems.push(
2207
+ `${join(path, field)} must be a finite number, got ${describeType2(value)}.`
2208
+ );
2209
+ }
2210
+ }
2211
+ function optionalNumber(record, field, path, problems) {
2212
+ if (record[field] !== void 0) {
2213
+ requireNumber(record, field, path, problems);
2214
+ }
2215
+ }
2216
+ function optionalBoolean(record, field, path, problems) {
2217
+ const value = record[field];
2218
+ if (value !== void 0 && typeof value !== "boolean") {
2219
+ problems.push(
2220
+ `${join(path, field)} must be a boolean, got ${describeType2(value)}.`
2221
+ );
2222
+ }
2223
+ }
2224
+ function optionalUrl(record, field, path, problems, isSafe, allowed) {
2225
+ const value = record[field];
2226
+ if (value === void 0) return;
2227
+ if (typeof value !== "string") {
2228
+ problems.push(
2229
+ `${join(path, field)} must be a string, got ${describeType2(value)}.`
2230
+ );
2231
+ } else if (!isSafe(value)) {
2232
+ problems.push(
2233
+ `${join(path, field)} must be ${allowed}, got ${JSON.stringify(value.slice(0, 80))}.`
2234
+ );
2235
+ }
2236
+ }
2237
+ function requireArray(record, field, path, problems) {
2238
+ const value = record[field];
2239
+ if (!Array.isArray(value)) {
2240
+ problems.push(
2241
+ `${join(path, field)} must be an array, got ${describeType2(value)}.`
2242
+ );
2243
+ return null;
2244
+ }
2245
+ return value;
2246
+ }
2247
+ function requireOneOf(record, field, options, path, problems) {
2248
+ const value = record[field];
2249
+ if (typeof value !== "string" || !options.includes(value)) {
2250
+ problems.push(
2251
+ `${join(path, field)} must be one of ${options.map((o) => `"${o}"`).join(", ")}, got ${typeof value === "string" ? JSON.stringify(value) : describeType2(value)}.`
2252
+ );
2253
+ }
2254
+ }
2255
+ function eachRecord(items, path, problems, check) {
2256
+ items.forEach((item, index) => {
2257
+ const itemPath = `${path}[${index}]`;
2258
+ if (isRecord(item)) {
2259
+ check(item, itemPath);
2260
+ } else {
2261
+ problems.push(
2262
+ `${itemPath} must be an object, got ${describeType2(item)}.`
2263
+ );
2264
+ }
2265
+ });
2266
+ }
2267
+ var HREF_RULE = "an http(s), mailto or relative URL";
2268
+ var IMAGE_RULE = "an http(s) or relative URL";
2269
+ function checkPayloadFields(kind, data, problems) {
2270
+ switch (kind) {
2271
+ case "TEXT":
2272
+ requireString(data, "value", "", problems);
2273
+ requireString(data, "label", "", problems);
2274
+ return;
2275
+ case "KPI":
2276
+ requireNumber(data, "value", "", problems);
2277
+ if (data["previous"] !== null) {
2278
+ optionalNumber(data, "previous", "", problems);
2279
+ }
2280
+ requireOneOf(data, "format", VALUE_FORMATS, "", problems);
2281
+ optionalString(data, "currency", "", problems);
2282
+ requireString(data, "label", "", problems);
2283
+ optionalString(data, "hint", "", problems);
2284
+ optionalBoolean(data, "higherIsBetter", "", problems);
2285
+ return;
2286
+ case "GAUGE":
2287
+ requireNumber(data, "value", "", problems);
2288
+ requireNumber(data, "max", "", problems);
2289
+ requireString(data, "label", "", problems);
2290
+ if (typeof data["max"] === "number" && data["max"] <= 0) {
2291
+ problems.push(`max must be greater than 0, got ${data["max"]}.`);
2292
+ }
2293
+ return;
2294
+ case "TABLE": {
2295
+ const columns = requireArray(data, "columns", "", problems);
2296
+ if (columns !== null) {
2297
+ eachRecord(columns, "columns", problems, (column, path) => {
2298
+ requireString(column, "label", path, problems);
2299
+ optionalBoolean(column, "numeric", path, problems);
2300
+ });
2301
+ }
2302
+ const rows = requireArray(data, "rows", "", problems);
2303
+ if (rows !== null) {
2304
+ rows.forEach((row, rowIndex) => {
2305
+ const rowPath = `rows[${rowIndex}]`;
2306
+ if (!Array.isArray(row)) {
2307
+ problems.push(
2308
+ `${rowPath} must be an array of cells, got ${describeType2(row)}.`
2309
+ );
2310
+ return;
2311
+ }
2312
+ if (columns !== null && row.length !== columns.length) {
2313
+ problems.push(
2314
+ `${rowPath} has ${row.length} cell(s) but there are ${columns.length} column(s).`
2315
+ );
2316
+ }
2317
+ eachRecord(row, rowPath, problems, (cell, cellPath) => {
2318
+ requireString(cell, "text", cellPath, problems);
2319
+ optionalUrl(
2320
+ cell,
2321
+ "href",
2322
+ cellPath,
2323
+ problems,
2324
+ isSafeHref,
2325
+ HREF_RULE
2326
+ );
2327
+ });
2328
+ });
2329
+ }
2330
+ optionalString(data, "footer", "", problems);
2331
+ return;
2332
+ }
2333
+ case "BAR_LIST": {
2334
+ const items = requireArray(data, "items", "", problems);
2335
+ if (items !== null) {
2336
+ eachRecord(items, "items", problems, (item, path) => {
2337
+ requireString(item, "label", path, problems);
2338
+ requireNumber(item, "value", path, problems);
2339
+ optionalString(item, "display", path, problems);
2340
+ });
2341
+ }
2342
+ optionalNumber(data, "total", "", problems);
2343
+ return;
2344
+ }
2345
+ case "ALERT_LIST": {
2346
+ const items = requireArray(data, "items", "", problems);
2347
+ if (items !== null) {
2348
+ eachRecord(items, "items", problems, (item, path) => {
2349
+ requireString(item, "title", path, problems);
2350
+ optionalUrl(item, "href", path, problems, isSafeHref, HREF_RULE);
2351
+ optionalUrl(
2352
+ item,
2353
+ "thumbnailUrl",
2354
+ path,
2355
+ problems,
2356
+ isSafeImageUrl,
2357
+ IMAGE_RULE
2358
+ );
2359
+ requireString(item, "valueLabel", path, problems);
2360
+ optionalString(item, "detail", path, problems);
2361
+ });
2362
+ }
2363
+ requireNumber(data, "total", "", problems);
2364
+ requireString(data, "emptyText", "", problems);
2365
+ if (items !== null && typeof data["total"] === "number" && data["total"] < items.length) {
2366
+ problems.push(
2367
+ `total (${data["total"]}) must be at least the number of items (${items.length}).`
2368
+ );
2369
+ }
2370
+ return;
2371
+ }
2372
+ case "GRAPH": {
2373
+ const series = requireArray(data, "series", "", problems);
2374
+ if (series !== null) {
2375
+ if (series.length > MAX_GRAPH_SERIES) {
2376
+ problems.push(
2377
+ `series has ${series.length} entries; at most ${MAX_GRAPH_SERIES} are allowed (fold the rest into "Other").`
2378
+ );
2379
+ }
2380
+ eachRecord(series, "series", problems, (entry, path) => {
2381
+ requireString(entry, "name", path, problems);
2382
+ const points = requireArray(entry, "points", path, problems);
2383
+ if (points !== null) {
2384
+ eachRecord(
2385
+ points,
2386
+ `${path}.points`,
2387
+ problems,
2388
+ (point, pointPath) => {
2389
+ requireString(point, "label", pointPath, problems);
2390
+ requireNumber(point, "value", pointPath, problems);
2391
+ }
2392
+ );
2393
+ }
2394
+ });
2395
+ }
2396
+ requireOneOf(data, "chartType", ["bar", "line"], "", problems);
2397
+ requireOneOf(data, "valueFormat", VALUE_FORMATS, "", problems);
2398
+ optionalString(data, "currency", "", problems);
2399
+ optionalString(data, "xLabel", "", problems);
2400
+ return;
2401
+ }
2402
+ }
2403
+ }
2404
+ function formatProblems(prefix, problems) {
2405
+ const shown = problems.slice(0, MAX_REPORTED_PROBLEMS);
2406
+ const more = problems.length - shown.length;
2407
+ return `${prefix}: ${shown.join(" ")}${more > 0 ? ` (and ${more} more problem(s))` : ""}`;
2408
+ }
2409
+ function validateWidgetData(value, options = {}) {
2410
+ const subject = options.widgetKey === void 0 ? "Widget data" : `Widget ${JSON.stringify(options.widgetKey)}`;
2411
+ if (!isRecord(value)) {
2412
+ return err(`${subject}: must be an object, got ${describeType2(value)}.`);
2413
+ }
2414
+ if (value["empty"] === true) {
2415
+ if (typeof value["text"] !== "string") {
2416
+ return err(
2417
+ `${subject} (empty): text must be a string, got ${describeType2(value["text"])}.`
2418
+ );
2419
+ }
2420
+ return ok(value);
2421
+ }
2422
+ const kind = value["kind"];
2423
+ if (!isWidgetKind(kind)) {
2424
+ return err(
2425
+ `${subject}: kind must be one of ${WIDGET_KINDS.join(", ")} (or the payload must be {"empty": true, "text": ...}), got ${typeof kind === "string" ? JSON.stringify(kind) : describeType2(kind)}.`
2426
+ );
2427
+ }
2428
+ if (options.expectedKind !== void 0 && kind !== options.expectedKind) {
2429
+ return err(
2430
+ `${subject}: expected kind ${options.expectedKind} from its definition, got ${kind}.`
2431
+ );
2432
+ }
2433
+ const problems = [];
2434
+ checkPayloadFields(kind, value, problems);
2435
+ if (problems.length > 0) {
2436
+ return err(formatProblems(`${subject} (${kind})`, problems));
2437
+ }
2438
+ return ok(value);
2439
+ }
2440
+
2441
+ // src/core/resolve.ts
2442
+ function resolvePayload(definition, payload, options = {}) {
2443
+ let checked;
2444
+ if (options.validate === false) {
2445
+ checked = payload;
2446
+ } else {
2447
+ const result = validateWidgetData(payload, {
2448
+ widgetKey: definition.key,
2449
+ expectedKind: definition.kind
2450
+ });
2451
+ if (!result.ok) {
2452
+ return { definition, status: "error", error: result.error };
2453
+ }
2454
+ checked = result.value;
2455
+ }
2456
+ const emptyText = emptyTextFor(checked);
2457
+ if (emptyText !== null) {
2458
+ return { definition, status: "empty", emptyText };
2459
+ }
2460
+ return { definition, status: "ok", data: checked };
2461
+ }
2462
+ function failedWidget(definition, error) {
2463
+ return { definition, status: "error", error };
2464
+ }
2465
+ function loadingWidgets(definitions) {
2466
+ return definitions.map((definition) => ({ definition, status: "loading" }));
2467
+ }
2468
+ function cacheKeyFor(definition, options) {
2469
+ return options.cacheKey === void 0 ? definition.key : options.cacheKey(definition);
2470
+ }
2471
+ var LoadInterruption = class extends Error {
2472
+ };
2473
+ async function resolveWidget(definition, provider, context, options = {}) {
2474
+ const quotedKey = JSON.stringify(definition.key);
2475
+ if (provider === void 0) {
2476
+ return failedWidget(
2477
+ definition,
2478
+ `Widget ${quotedKey}: no provider is registered for this key.`
2479
+ );
2480
+ }
2481
+ if (options.signal?.aborted === true) {
2482
+ return failedWidget(
2483
+ definition,
2484
+ `Widget ${quotedKey}: loading was canceled before it started.`
2485
+ );
2486
+ }
2487
+ const controller = new AbortController();
2488
+ const onExternalAbort = () => controller.abort(options.signal?.reason);
2489
+ options.signal?.addEventListener("abort", onExternalAbort, { once: true });
2490
+ let timedOut = false;
2491
+ const timeoutMs = options.timeoutMs !== void 0 && options.timeoutMs > 0 ? options.timeoutMs : void 0;
2492
+ const timer = timeoutMs === void 0 ? void 0 : setTimeout(() => {
2493
+ timedOut = true;
2494
+ controller.abort(new Error(`Timed out after ${timeoutMs} ms.`));
2495
+ }, timeoutMs);
2496
+ const interrupted = new Promise((_resolve, reject) => {
2497
+ controller.signal.addEventListener(
2498
+ "abort",
2499
+ () => reject(new LoadInterruption()),
2500
+ { once: true }
2501
+ );
2502
+ });
2503
+ let payload;
2504
+ try {
2505
+ payload = await Promise.race([
2506
+ Promise.resolve().then(() => {
2507
+ if (controller.signal.aborted) throw new LoadInterruption();
2508
+ return provider(context, definition, { signal: controller.signal });
2509
+ }),
2510
+ interrupted
2511
+ ]);
2512
+ } catch (cause) {
2513
+ if (cause instanceof LoadInterruption) {
2514
+ return failedWidget(
2515
+ definition,
2516
+ timedOut ? `Widget ${quotedKey}: provider timed out after ${timeoutMs} ms.` : `Widget ${quotedKey}: loading was canceled.`
2517
+ );
2518
+ }
2519
+ return failedWidget(
2520
+ definition,
2521
+ `Widget ${quotedKey}: provider failed: ${describeUnknownError(cause)}`
2522
+ );
2523
+ } finally {
2524
+ if (timer !== void 0) clearTimeout(timer);
2525
+ options.signal?.removeEventListener("abort", onExternalAbort);
2526
+ }
2527
+ const resolved = resolvePayload(definition, payload, options);
2528
+ if (resolved.status !== "ok") return resolved;
2529
+ const storedAt = Date.now();
2530
+ if (options.cache !== void 0) {
2531
+ try {
2532
+ await options.cache.set(cacheKeyFor(definition, options), {
2533
+ payload: resolved.data,
2534
+ storedAt
2535
+ });
2536
+ } catch {
2537
+ }
2538
+ }
2539
+ return { ...resolved, updatedAt: storedAt };
2540
+ }
2541
+ function widgetsFor(definitions, roles) {
2542
+ return sortDefinitions(
2543
+ definitions.filter((definition) => isWidgetVisibleTo(definition, roles))
2544
+ );
2545
+ }
2546
+
2547
+ // src/core/loader.ts
2548
+ function createWidgetLoader(options) {
2549
+ const { definitions, providers, context, ...resolveOptions } = options;
2550
+ const visible = widgetsFor(definitions, context.roles);
2551
+ let snapshot = loadingWidgets(visible);
2552
+ const listeners = /* @__PURE__ */ new Set();
2553
+ const started = /* @__PURE__ */ new Set();
2554
+ const generations = /* @__PURE__ */ new Map();
2555
+ const inFlight = /* @__PURE__ */ new Map();
2556
+ let disposed = false;
2557
+ function publish(next) {
2558
+ snapshot = next;
2559
+ for (const listener of [...listeners]) {
2560
+ try {
2561
+ listener();
2562
+ } catch {
2563
+ }
2564
+ }
2565
+ }
2566
+ function replace(key, widget) {
2567
+ publish(
2568
+ snapshot.map(
2569
+ (current) => current.definition.key === key ? widget : current
2570
+ )
2571
+ );
2572
+ }
2573
+ function abortAll(reason) {
2574
+ for (const controller of inFlight.values()) controller.abort(reason);
2575
+ inFlight.clear();
2576
+ }
2577
+ resolveOptions.signal?.addEventListener(
2578
+ "abort",
2579
+ () => abortAll(resolveOptions.signal?.reason),
2580
+ { once: true }
2581
+ );
2582
+ async function run(definition, readCache) {
2583
+ const key = definition.key;
2584
+ const generation = (generations.get(key) ?? 0) + 1;
2585
+ generations.set(key, generation);
2586
+ inFlight.get(key)?.abort(new Error("Superseded by a newer load."));
2587
+ const controller = new AbortController();
2588
+ inFlight.set(key, controller);
2589
+ const isCurrent = () => !disposed && generations.get(key) === generation;
2590
+ if (readCache && resolveOptions.cache !== void 0) {
2591
+ try {
2592
+ const entry = await resolveOptions.cache.get(
2593
+ cacheKeyFor(definition, resolveOptions)
2594
+ );
2595
+ if (entry !== void 0 && isCurrent()) {
2596
+ const cached = resolvePayload(
2597
+ definition,
2598
+ entry.payload,
2599
+ resolveOptions
2600
+ );
2601
+ if (cached.status === "ok") {
2602
+ replace(key, { ...cached, updatedAt: entry.storedAt, stale: true });
2603
+ }
2604
+ }
2605
+ } catch {
2606
+ }
2607
+ }
2608
+ if (!isCurrent()) return;
2609
+ const provider = Object.prototype.hasOwnProperty.call(providers, key) ? providers[key] : void 0;
2610
+ const result = await resolveWidget(definition, provider, context, {
2611
+ ...resolveOptions,
2612
+ signal: controller.signal
2613
+ });
2614
+ if (!isCurrent()) return;
2615
+ inFlight.delete(key);
2616
+ replace(key, result);
2617
+ }
2618
+ function start(definition, readCache) {
2619
+ started.add(definition.key);
2620
+ void run(definition, readCache);
2621
+ }
2622
+ function definitionFor(key) {
2623
+ return visible.find((definition) => definition.key === key);
2624
+ }
2625
+ function load(key) {
2626
+ if (disposed || started.has(key)) return;
2627
+ const definition = definitionFor(key);
2628
+ if (definition !== void 0) start(definition, true);
2629
+ }
2630
+ function loadAll() {
2631
+ for (const definition of visible) load(definition.key);
2632
+ }
2633
+ function refresh(key, refreshOptions) {
2634
+ if (disposed) return;
2635
+ const keys = key === void 0 ? [...started] : [key];
2636
+ for (const target of keys) {
2637
+ const definition = definitionFor(target);
2638
+ if (definition === void 0) continue;
2639
+ if (refreshOptions?.skipInFlight === true && inFlight.has(target)) {
2640
+ continue;
2641
+ }
2642
+ const current = snapshot.find(
2643
+ (widget) => widget.definition.key === target
2644
+ );
2645
+ if (current?.status === "error") {
2646
+ replace(target, { definition, status: "loading" });
2647
+ }
2648
+ start(definition, false);
2649
+ }
2650
+ }
2651
+ function dispose() {
2652
+ disposed = true;
2653
+ abortAll(new Error("The widget loader was disposed."));
2654
+ listeners.clear();
2655
+ }
2656
+ function subscribe(listener) {
2657
+ listeners.add(listener);
2658
+ return () => {
2659
+ listeners.delete(listener);
2660
+ };
2661
+ }
2662
+ return {
2663
+ getSnapshot: () => snapshot,
2664
+ subscribe,
2665
+ load,
2666
+ loadAll,
2667
+ refresh,
2668
+ dispose
2669
+ };
2670
+ }
2671
+
2672
+ // src/react/use-widgets.ts
2673
+ function useWidgets(definitions, providers, context, options = {}) {
2674
+ const {
2675
+ loadWhen = "mount",
2676
+ rootMargin = "200px",
2677
+ refreshMs = 0,
2678
+ validate,
2679
+ timeoutMs,
2680
+ signal,
2681
+ cache,
2682
+ cacheKey
2683
+ } = options;
2684
+ const placeholders = react.useMemo(
2685
+ () => loadingWidgets(widgetsFor(definitions, context.roles)),
2686
+ [definitions, context.roles]
2687
+ );
2688
+ const [widgets, setWidgets] = react.useState(placeholders);
2689
+ const [loader, setLoader] = react.useState(null);
2690
+ const loaderRef = react.useRef(null);
2691
+ const gridRef = react.useRef(null);
2692
+ react.useEffect(() => {
2693
+ const created = createWidgetLoader({
2694
+ definitions,
2695
+ providers,
2696
+ context,
2697
+ ...validate === void 0 ? {} : { validate },
2698
+ ...timeoutMs === void 0 ? {} : { timeoutMs },
2699
+ ...signal === void 0 ? {} : { signal },
2700
+ ...cache === void 0 ? {} : { cache },
2701
+ ...cacheKey === void 0 ? {} : { cacheKey }
2702
+ });
2703
+ loaderRef.current = created;
2704
+ setWidgets(created.getSnapshot());
2705
+ const unsubscribe = created.subscribe(
2706
+ () => setWidgets(created.getSnapshot())
2707
+ );
2708
+ setLoader(created);
2709
+ if (loadWhen === "mount" || typeof IntersectionObserver === "undefined") {
2710
+ created.loadAll();
2711
+ }
2712
+ return () => {
2713
+ unsubscribe();
2714
+ created.dispose();
2715
+ loaderRef.current = null;
2716
+ setLoader(null);
2717
+ };
2718
+ }, [
2719
+ definitions,
2720
+ providers,
2721
+ context,
2722
+ validate,
2723
+ timeoutMs,
2724
+ signal,
2725
+ cache,
2726
+ cacheKey,
2727
+ loadWhen
2728
+ ]);
2729
+ const cardKeys = widgets.map((widget) => widget.definition.key).join("\0");
2730
+ react.useEffect(() => {
2731
+ const grid = gridRef.current;
2732
+ if (loader === null || grid === null || loadWhen !== "visible" || typeof IntersectionObserver === "undefined") {
2733
+ return void 0;
2734
+ }
2735
+ const observer = new IntersectionObserver(
2736
+ (entries) => {
2737
+ for (const entry of entries) {
2738
+ const key = entry.target.dataset["widgetKey"];
2739
+ if (entry.isIntersecting && key !== void 0) {
2740
+ loader.load(key);
2741
+ observer.unobserve(entry.target);
2742
+ }
2743
+ }
2744
+ },
2745
+ { rootMargin }
2746
+ );
2747
+ grid.querySelectorAll("[data-widget-key]").forEach((card) => observer.observe(card));
2748
+ return () => observer.disconnect();
2749
+ }, [loader, loadWhen, rootMargin, cardKeys]);
2750
+ react.useEffect(() => {
2751
+ if (loader === null || !(refreshMs > 0)) return void 0;
2752
+ const timer = setInterval(() => {
2753
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") {
2754
+ return;
2755
+ }
2756
+ loader.refresh(void 0, { skipInFlight: true });
2757
+ }, refreshMs);
2758
+ return () => clearInterval(timer);
2759
+ }, [loader, refreshMs]);
2760
+ const refresh = react.useCallback((key) => {
2761
+ loaderRef.current?.refresh(key);
2762
+ }, []);
2763
+ return { widgets, refresh, gridRef };
2764
+ }
2765
+
1340
2766
  exports.AlertListWidget = AlertListWidget;
1341
2767
  exports.BarListWidget = BarListWidget;
1342
2768
  exports.DEFAULT_LABELS = DEFAULT_LABELS;
@@ -1351,6 +2777,8 @@ exports.TextWidget = TextWidget;
1351
2777
  exports.Thumbnail = Thumbnail;
1352
2778
  exports.WidgetCard = WidgetCard;
1353
2779
  exports.WidgetContent = WidgetContent;
2780
+ exports.WidgetDetail = WidgetDetail;
2781
+ exports.WidgetDetailDialog = WidgetDetailDialog;
1354
2782
  exports.WidgetGrid = WidgetGrid;
1355
2783
  exports.WidgetLink = WidgetLink;
1356
2784
  exports.WidgetSettingsProvider = WidgetSettingsProvider;
@@ -1359,7 +2787,9 @@ exports.buildChartModel = buildChartModel;
1359
2787
  exports.describeChart = describeChart;
1360
2788
  exports.joinClassNames = joinClassNames;
1361
2789
  exports.seriesColor = seriesColor;
2790
+ exports.useDetailData = useDetailData;
1362
2791
  exports.useWidgetSettings = useWidgetSettings;
2792
+ exports.useWidgets = useWidgets;
1363
2793
  exports.widgetTitle = widgetTitle;
1364
2794
  //# sourceMappingURL=react.cjs.map
1365
2795
  //# sourceMappingURL=react.cjs.map