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