@devicechain/widgets 0.24.0-rc.1 → 0.25.0-rc.2

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.
Files changed (67) hide show
  1. package/README.md +11 -1
  2. package/dist/absolute-range-picker.d.ts +22 -0
  3. package/dist/binding-policy.d.ts +1 -0
  4. package/dist/builtin-descriptors.d.ts +69 -0
  5. package/dist/builtin-registry.d.ts +68 -0
  6. package/dist/chunks/absolute-range-picker-E57G2TPX.js +139 -0
  7. package/dist/chunks/absolute-range-picker-E57G2TPX.js.map +7 -0
  8. package/dist/chunks/{aggregate-feed-4FSBQYJL.js → aggregate-feed-XX36GNY6.js} +2 -2
  9. package/dist/chunks/{aggregate-widgets-2ZQBW3YF.js → aggregate-widgets-72HMPMBU.js} +99 -57
  10. package/dist/chunks/aggregate-widgets-72HMPMBU.js.map +7 -0
  11. package/dist/chunks/{chunk-USRYKW2J.js → chunk-7BUCJQFP.js} +10 -2
  12. package/dist/chunks/chunk-7BUCJQFP.js.map +7 -0
  13. package/dist/chunks/chunk-AF4M5OXQ.js +23 -0
  14. package/dist/chunks/chunk-AF4M5OXQ.js.map +7 -0
  15. package/dist/chunks/{chunk-XQQQNTBX.js → chunk-EQOWBDS7.js} +1 -1
  16. package/dist/chunks/{chunk-XQQQNTBX.js.map → chunk-EQOWBDS7.js.map} +1 -1
  17. package/dist/chunks/chunk-JVC6JYUR.js +48 -0
  18. package/dist/chunks/chunk-JVC6JYUR.js.map +7 -0
  19. package/dist/chunks/{chunk-JFH4QMLA.js → chunk-NYHNQ5VR.js} +5 -1
  20. package/dist/chunks/{chunk-JFH4QMLA.js.map → chunk-NYHNQ5VR.js.map} +2 -2
  21. package/dist/chunks/chunk-VTGKDO44.js +87 -0
  22. package/dist/chunks/chunk-VTGKDO44.js.map +7 -0
  23. package/dist/chunks/chunk-XJ7UF2CP.js +90 -0
  24. package/dist/chunks/chunk-XJ7UF2CP.js.map +7 -0
  25. package/dist/chunks/{compare-feed-AY3CVKUZ.js → compare-feed-5OLZHFRL.js} +48 -23
  26. package/dist/chunks/compare-feed-5OLZHFRL.js.map +7 -0
  27. package/dist/chunks/{event-markers-feed-GEGTIHXT.js → event-markers-feed-B3MVFYUT.js} +4 -4
  28. package/dist/chunks/export-rows-OEWIBIUY.js +11 -0
  29. package/dist/chunks/{fleet-feed-2SKDR6ZQ.js → fleet-feed-D4T3LC25.js} +10 -10
  30. package/dist/chunks/history-widgets-NSNXDKEC.js +336 -0
  31. package/dist/chunks/history-widgets-NSNXDKEC.js.map +7 -0
  32. package/dist/chunks/{level-widgets-MJB7TOLU.js → level-widgets-HE6EDSWC.js} +13 -9
  33. package/dist/chunks/level-widgets-HE6EDSWC.js.map +7 -0
  34. package/dist/chunks/no-reading-B4MPXAS2.js +22 -0
  35. package/dist/chunks/no-reading-B4MPXAS2.js.map +7 -0
  36. package/dist/chunks/{presence-widgets-2LIUKN4Y.js → presence-widgets-M5VGG3OO.js} +12 -8
  37. package/dist/chunks/presence-widgets-M5VGG3OO.js.map +7 -0
  38. package/dist/chunks/table-CEVB3JZS.js +8 -0
  39. package/dist/chunks/table-CEVB3JZS.js.map +7 -0
  40. package/dist/compare-model.d.ts +1 -1
  41. package/dist/documented-descriptors.d.ts +206 -0
  42. package/dist/export-rows.d.ts +24 -0
  43. package/dist/history-feed.d.ts +10 -0
  44. package/dist/history-model.d.ts +45 -0
  45. package/dist/history-strings.d.ts +20 -0
  46. package/dist/index.d.ts +1 -1
  47. package/dist/index.js +544 -189
  48. package/dist/index.js.map +4 -4
  49. package/dist/level-reading.d.ts +1 -0
  50. package/dist/no-reading-core.d.ts +25 -0
  51. package/dist/no-reading.d.ts +10 -0
  52. package/dist/time-range-bar.d.ts +26 -2
  53. package/dist/widgets/history-lazy.d.ts +70 -0
  54. package/dist/widgets/history-table.d.ts +7 -0
  55. package/dist/widgets/history-widgets.d.ts +2 -0
  56. package/dist/widgets/no-reading-lazy.d.ts +7 -0
  57. package/package.json +8 -8
  58. package/dist/chunks/aggregate-widgets-2ZQBW3YF.js.map +0 -7
  59. package/dist/chunks/chunk-USRYKW2J.js.map +0 -7
  60. package/dist/chunks/compare-feed-AY3CVKUZ.js.map +0 -7
  61. package/dist/chunks/level-widgets-MJB7TOLU.js.map +0 -7
  62. package/dist/chunks/presence-widgets-2LIUKN4Y.js.map +0 -7
  63. package/dist/chunks/table-6EVV7QZH.js +0 -7
  64. /package/dist/chunks/{aggregate-feed-4FSBQYJL.js.map → aggregate-feed-XX36GNY6.js.map} +0 -0
  65. /package/dist/chunks/{event-markers-feed-GEGTIHXT.js.map → event-markers-feed-B3MVFYUT.js.map} +0 -0
  66. /package/dist/chunks/{table-6EVV7QZH.js.map → export-rows-OEWIBIUY.js.map} +0 -0
  67. /package/dist/chunks/{fleet-feed-2SKDR6ZQ.js.map → fleet-feed-D4T3LC25.js.map} +0 -0
package/dist/index.js CHANGED
@@ -1,21 +1,20 @@
1
+ import {
2
+ EntitySelector
3
+ } from "./chunks/chunk-FM7OI5TQ.js";
4
+ import {
5
+ readRange
6
+ } from "./chunks/chunk-ZR74ORIT.js";
1
7
  import {
2
8
  Table
3
- } from "./chunks/chunk-USRYKW2J.js";
9
+ } from "./chunks/chunk-7BUCJQFP.js";
4
10
  import {
5
11
  ChunkLoadingFrame,
6
12
  lazyChunk
7
13
  } from "./chunks/chunk-BMFQOTYG.js";
8
14
  import {
9
- alarmSubscription,
10
- commandSubscription,
11
- groupsAlarmsByDeviceType,
12
- measurementIsMembersOnly,
13
- measurementSpan,
14
- measurementWindow,
15
- membersMode,
16
- presenceMode,
17
- wantsEventMarkers
18
- } from "./chunks/chunk-JFH4QMLA.js";
15
+ useHeld
16
+ } from "./chunks/chunk-XNEBAY4J.js";
17
+ import "./chunks/chunk-JVC6JYUR.js";
19
18
  import {
20
19
  AlarmCount,
21
20
  AlarmSeverity,
@@ -37,25 +36,29 @@ import {
37
36
  validateParams
38
37
  } from "./chunks/chunk-XSLFHCNI.js";
39
38
  import {
40
- useHeld
41
- } from "./chunks/chunk-XNEBAY4J.js";
39
+ alarmSubscription,
40
+ commandSubscription,
41
+ groupsAlarmsByDeviceType,
42
+ historyMode,
43
+ measurementIsMembersOnly,
44
+ measurementSpan,
45
+ measurementWindow,
46
+ membersMode,
47
+ presenceMode,
48
+ wantsEventMarkers
49
+ } from "./chunks/chunk-NYHNQ5VR.js";
42
50
  import {
43
51
  accessStrings
44
52
  } from "./chunks/chunk-BRH66T5F.js";
45
- import {
46
- EntitySelector
47
- } from "./chunks/chunk-FM7OI5TQ.js";
48
- import {
49
- readRange
50
- } from "./chunks/chunk-ZR74ORIT.js";
51
53
  import {
52
54
  LevelReadout
53
- } from "./chunks/chunk-XQQQNTBX.js";
55
+ } from "./chunks/chunk-EQOWBDS7.js";
54
56
 
55
57
  // src/dashboard-renderer.tsx
56
58
  import {
57
59
  anchorFraction,
58
60
  DEFAULT_CANVAS_DENSITY,
61
+ DEFAULT_TIME_RANGE_SECONDS,
59
62
  defaultHistoryWindow,
60
63
  effectiveLayout,
61
64
  historyWindowFor,
@@ -76,7 +79,7 @@ import {
76
79
  WidgetTimeRangeProvider,
77
80
  WidgetTokenStyles
78
81
  } from "@devicechain/widget-sdk";
79
- import { Fragment, useCallback as useCallback2, useEffect as useEffect3, useLayoutEffect, useMemo as useMemo5, useRef as useRef2, useState as useState4 } from "react";
82
+ import { Fragment, useCallback as useCallback3, useEffect as useEffect4, useLayoutEffect, useMemo as useMemo5, useRef as useRef2, useState as useState5 } from "react";
80
83
 
81
84
  // src/connected-widget.tsx
82
85
  import {
@@ -86,12 +89,14 @@ import {
86
89
  newestSampleTime,
87
90
  resolveStaleAfterSeconds,
88
91
  WidgetCandidatesProvider,
92
+ WidgetDeclaredMetricsProvider,
89
93
  WidgetFreshnessProvider,
90
94
  WidgetSelectProvider,
95
+ useWidgetScale as useWidgetScale3,
91
96
  useWidgetTimeRange as useWidgetTimeRange2
92
97
  } from "@devicechain/widget-sdk";
93
98
  import { isForbiddenError } from "@devicechain/client";
94
- import { useEffect as useEffect2, useMemo as useMemo4, useState as useState3 } from "react";
99
+ import { useCallback as useCallback2, useEffect as useEffect3, useMemo as useMemo4, useState as useState4 } from "react";
95
100
 
96
101
  // src/builtin-registry.ts
97
102
  import { createWidgetRegistry } from "@devicechain/widget-sdk";
@@ -1228,12 +1233,45 @@ var builtinWidgetDescriptors = {
1228
1233
  freshness: true,
1229
1234
  interactions: {},
1230
1235
  layout: { defaultSpan: { cols: 4, rows: 4 }, minSpan: { cols: 3, rows: 3 }, maxSpan: { cols: 8, rows: 8 } }
1236
+ }),
1237
+ // One device's stored history as rows: a row per time bucket over the span, a column per
1238
+ // measurement, each cell the bucket's AVERAGE (the table says "avg per 5m"). It reads the store's
1239
+ // buckets (`binding.history`), never the live feed, so it is time-ranged and takes one device.
1240
+ "history-table": defineRuntimeDescriptor({
1241
+ id: "history-table",
1242
+ version: VERSION,
1243
+ sdkApiVersion: 1,
1244
+ optionsVersion: 1,
1245
+ display: { title: "History table", category: "Telemetry", titleId: "widgetTypeHistoryTable", categoryId: "widgetCategoryTelemetry" },
1246
+ channel: "measurement",
1247
+ datasource: { mode: "required", selectors: ONE_DEVICE },
1248
+ binding: { windowSeconds: { option: "windowSeconds" }, history: { const: true } },
1249
+ options: {
1250
+ title: TITLE,
1251
+ columns: FLEET_COLUMNS,
1252
+ precision: PRECISION,
1253
+ oldestFirst: { kind: "boolean", labelId: "widgetLabelOldestFirst" },
1254
+ pageSize: {
1255
+ kind: "number",
1256
+ integer: true,
1257
+ min: 5,
1258
+ max: 100,
1259
+ labelId: "widgetLabelRowsPerPage"
1260
+ },
1261
+ staleAfterSeconds: STALE_AFTER_SECONDS,
1262
+ windowSeconds: WINDOW_SECONDS
1263
+ },
1264
+ defaults: { title: "History table" },
1265
+ // The frame's age is the table's: when its buckets were last read.
1266
+ freshness: true,
1267
+ interactions: {},
1268
+ layout: { defaultSpan: { cols: 6, rows: 5 }, minSpan: { cols: 4, rows: 3 } }
1231
1269
  })
1232
1270
  };
1233
1271
  var builtinWidgetCatalog = createWidgetCatalog(Object.values(builtinWidgetDescriptors));
1234
1272
 
1235
1273
  // src/widgets/aggregate-lazy.tsx
1236
- var chunk = lazyChunk(() => import("./chunks/aggregate-widgets-2ZQBW3YF.js"));
1274
+ var chunk = lazyChunk(() => import("./chunks/aggregate-widgets-72HMPMBU.js"));
1237
1275
  var loadAggregateWidgets = chunk.load;
1238
1276
  var statCardWidget = defineWidget({
1239
1277
  descriptor: builtinWidgetDescriptors["stat-card"],
@@ -1290,32 +1328,41 @@ var commandButtonWidget = defineWidget3({
1290
1328
  implementation: { kind: "trusted-react", component: chunk3.component("CommandButton") }
1291
1329
  });
1292
1330
 
1293
- // src/widgets/presence-lazy.tsx
1331
+ // src/widgets/history-lazy.tsx
1294
1332
  import { defineWidget as defineWidget4 } from "@devicechain/widget-sdk";
1295
- var chunk4 = lazyChunk(() => import("./chunks/presence-widgets-2LIUKN4Y.js"));
1296
- var loadPresenceWidgets = chunk4.load;
1297
- var deviceStatusWidget = defineWidget4({
1333
+ var chunk4 = lazyChunk(() => import("./chunks/history-widgets-NSNXDKEC.js"));
1334
+ var loadHistoryWidgets = chunk4.load;
1335
+ var historyTableWidget = defineWidget4({
1336
+ descriptor: builtinWidgetDescriptors["history-table"],
1337
+ implementation: { kind: "trusted-react", component: chunk4.component("HistoryTable") }
1338
+ });
1339
+
1340
+ // src/widgets/presence-lazy.tsx
1341
+ import { defineWidget as defineWidget5 } from "@devicechain/widget-sdk";
1342
+ var chunk5 = lazyChunk(() => import("./chunks/presence-widgets-M5VGG3OO.js"));
1343
+ var loadPresenceWidgets = chunk5.load;
1344
+ var deviceStatusWidget = defineWidget5({
1298
1345
  descriptor: builtinWidgetDescriptors["device-status"],
1299
- implementation: { kind: "trusted-react", component: chunk4.component("DeviceStatus") }
1346
+ implementation: { kind: "trusted-react", component: chunk5.component("DeviceStatus") }
1300
1347
  });
1301
- var onlineDoughnutWidget = defineWidget4({
1348
+ var onlineDoughnutWidget = defineWidget5({
1302
1349
  descriptor: builtinWidgetDescriptors["online-doughnut"],
1303
- implementation: { kind: "trusted-react", component: chunk4.component("OnlineDoughnut") }
1350
+ implementation: { kind: "trusted-react", component: chunk5.component("OnlineDoughnut") }
1304
1351
  });
1305
1352
 
1306
1353
  // src/widgets/selection-lazy.tsx
1307
- import { defineWidget as defineWidget5 } from "@devicechain/widget-sdk";
1308
- var chunk5 = lazyChunk(() => import("./chunks/entity-selector-3JJFI4DT.js"));
1309
- var loadSelectionWidgets = chunk5.load;
1310
- var entitySelectorWidget = defineWidget5({
1354
+ import { defineWidget as defineWidget6 } from "@devicechain/widget-sdk";
1355
+ var chunk6 = lazyChunk(() => import("./chunks/entity-selector-3JJFI4DT.js"));
1356
+ var loadSelectionWidgets = chunk6.load;
1357
+ var entitySelectorWidget = defineWidget6({
1311
1358
  descriptor: builtinWidgetDescriptors["entity-selector"],
1312
- implementation: { kind: "trusted-react", component: chunk5.component("EntitySelector") }
1359
+ implementation: { kind: "trusted-react", component: chunk6.component("EntitySelector") }
1313
1360
  });
1314
1361
 
1315
1362
  // src/widgets/gauge.tsx
1316
1363
  import { useMemo } from "react";
1317
1364
  import {
1318
- defineWidget as defineWidget6,
1365
+ defineWidget as defineWidget7,
1319
1366
  flashBackgroundStyle,
1320
1367
  formatValue,
1321
1368
  optBoolean,
@@ -1329,6 +1376,24 @@ import {
1329
1376
  WidgetFrame
1330
1377
  } from "@devicechain/widget-sdk";
1331
1378
  import { buildGaugeOption, EChart, gaugeReadoutBox, loadChartEngine, useChartTheme } from "@devicechain/widget-sdk/echarts";
1379
+
1380
+ // src/widgets/no-reading-lazy.tsx
1381
+ import { useEffect, useState } from "react";
1382
+ var NoReading = lazyChunk(() => import("./chunks/no-reading-B4MPXAS2.js")).component("NoReading");
1383
+ function useSettledEmpty(empty, ms = 1500) {
1384
+ const [settled, setSettled] = useState(false);
1385
+ useEffect(() => {
1386
+ if (!empty) {
1387
+ setSettled(false);
1388
+ return;
1389
+ }
1390
+ const timer = setTimeout(() => setSettled(true), ms);
1391
+ return () => clearTimeout(timer);
1392
+ }, [empty, ms]);
1393
+ return empty && settled;
1394
+ }
1395
+
1396
+ // src/widgets/gauge.tsx
1332
1397
  import { jsx, jsxs } from "react/jsx-runtime";
1333
1398
  function Gauge({ widget, data }) {
1334
1399
  const name = primaryMeasurementName(widget);
@@ -1364,6 +1429,10 @@ function GaugeBody({ widget, data }) {
1364
1429
  "aria-valuenow": Math.min(range.range.max, Math.max(range.range.min, value)),
1365
1430
  "aria-valuetext": `${formatValue(value, precision)}${unit ? ` ${unit}` : ""}`
1366
1431
  } : {};
1432
+ const explain = useSettledEmpty(value === void 0);
1433
+ if (explain) {
1434
+ return /* @__PURE__ */ jsx(NoReading, { unbound: widget.datasource?.kind === "unbound", measurement: name, waitMs: 0 });
1435
+ }
1367
1436
  return /* @__PURE__ */ jsxs("div", { style: { position: "relative", width: "100%", height: "100%", ...flashBackgroundStyle(flashDirection) }, ...meter, children: [
1368
1437
  /* @__PURE__ */ jsx(EChart, { option }),
1369
1438
  /* @__PURE__ */ jsx(
@@ -1394,7 +1463,7 @@ function GaugeBody({ widget, data }) {
1394
1463
  ] });
1395
1464
  }
1396
1465
  Gauge.preload = loadChartEngine;
1397
- var gaugeWidget = defineWidget6({
1466
+ var gaugeWidget = defineWidget7({
1398
1467
  descriptor: builtinWidgetDescriptors["gauge"],
1399
1468
  implementation: { kind: "trusted-react", component: Gauge }
1400
1469
  });
@@ -1402,7 +1471,7 @@ var gaugeWidget = defineWidget6({
1402
1471
  // src/widgets/image.tsx
1403
1472
  import {
1404
1473
  css,
1405
- defineWidget as defineWidget7,
1474
+ defineWidget as defineWidget8,
1406
1475
  fontVar,
1407
1476
  optString as optString2,
1408
1477
  WidgetFrame as WidgetFrame2
@@ -1429,7 +1498,7 @@ function Image({ widget }) {
1429
1498
  }
1430
1499
  ) });
1431
1500
  }
1432
- var imageWidget = defineWidget7({
1501
+ var imageWidget = defineWidget8({
1433
1502
  descriptor: builtinWidgetDescriptors["image"],
1434
1503
  implementation: { kind: "trusted-react", component: Image }
1435
1504
  });
@@ -1437,7 +1506,7 @@ var imageWidget = defineWidget7({
1437
1506
  // src/widgets/label.tsx
1438
1507
  import {
1439
1508
  css as css2,
1440
- defineWidget as defineWidget8,
1509
+ defineWidget as defineWidget9,
1441
1510
  fontVar as fontVar2,
1442
1511
  spaceVar,
1443
1512
  optNumber as optNumber2,
@@ -1484,7 +1553,7 @@ function Label({ widget }) {
1484
1553
  }
1485
1554
  ) });
1486
1555
  }
1487
- var labelWidget = defineWidget8({
1556
+ var labelWidget = defineWidget9({
1488
1557
  descriptor: builtinWidgetDescriptors["label"],
1489
1558
  implementation: { kind: "trusted-react", component: Label }
1490
1559
  });
@@ -1494,7 +1563,7 @@ import { useMemo as useMemo2 } from "react";
1494
1563
  import {
1495
1564
  bandAt,
1496
1565
  css as css3,
1497
- defineWidget as defineWidget9,
1566
+ defineWidget as defineWidget10,
1498
1567
  fitValueFont,
1499
1568
  fitValueFontCss,
1500
1569
  FIT_MIN_FONT_PX,
@@ -1537,6 +1606,9 @@ function LatestCard({ widget, data }) {
1537
1606
  );
1538
1607
  const chars = useStickyChars(display.length + (sample?.value != null && unit ? Math.ceil(unit.length / 2) + 1 : 0));
1539
1608
  return /* @__PURE__ */ jsx4(WidgetFrame4, { title, sampleTime: sample?.occurredTime ?? null, children: ({ size, scale }) => {
1609
+ if (sample?.value == null) {
1610
+ return /* @__PURE__ */ jsx4(NoReading, { unbound: widget.datasource?.kind === "unbound", measurement: name });
1611
+ }
1540
1612
  const floor = Math.round(FIT_MIN_FONT_PX * scale);
1541
1613
  const measured = size.width > 0 && size.height > 0;
1542
1614
  const valuePx = measured ? fitValueFont(size, chars, { floor }) : void 0;
@@ -1592,48 +1664,48 @@ function LatestCard({ widget, data }) {
1592
1664
  );
1593
1665
  } });
1594
1666
  }
1595
- var latestCardWidget = defineWidget9({
1667
+ var latestCardWidget = defineWidget10({
1596
1668
  descriptor: builtinWidgetDescriptors["latest-card"],
1597
1669
  implementation: { kind: "trusted-react", component: LatestCard }
1598
1670
  });
1599
1671
 
1600
1672
  // src/widgets/level-lazy.tsx
1601
- import { defineWidget as defineWidget10 } from "@devicechain/widget-sdk";
1673
+ import { defineWidget as defineWidget11 } from "@devicechain/widget-sdk";
1602
1674
  import { loadChartEngine as loadChartEngine2 } from "@devicechain/widget-sdk/echarts";
1603
- var chunk6 = lazyChunk(() => import("./chunks/level-widgets-MJB7TOLU.js"));
1604
- var loadLevelWidgets = chunk6.load;
1605
- var lazyComponent = chunk6.component;
1606
- var linearGaugeWidget = defineWidget10({
1675
+ var chunk7 = lazyChunk(() => import("./chunks/level-widgets-HE6EDSWC.js"));
1676
+ var loadLevelWidgets = chunk7.load;
1677
+ var lazyComponent = chunk7.component;
1678
+ var linearGaugeWidget = defineWidget11({
1607
1679
  descriptor: builtinWidgetDescriptors["linear-gauge"],
1608
1680
  implementation: { kind: "trusted-react", component: lazyComponent("LinearGauge") }
1609
1681
  });
1610
- var levelGaugeWidget = defineWidget10({
1682
+ var levelGaugeWidget = defineWidget11({
1611
1683
  descriptor: builtinWidgetDescriptors["level-gauge"],
1612
1684
  implementation: { kind: "trusted-react", component: lazyComponent("LevelGauge") }
1613
1685
  });
1614
- var batteryWidget = defineWidget10({
1686
+ var batteryWidget = defineWidget11({
1615
1687
  descriptor: builtinWidgetDescriptors["battery"],
1616
1688
  implementation: { kind: "trusted-react", component: lazyComponent("Battery") }
1617
1689
  });
1618
- var statusLightWidget = defineWidget10({
1690
+ var statusLightWidget = defineWidget11({
1619
1691
  descriptor: builtinWidgetDescriptors["status-light"],
1620
1692
  implementation: { kind: "trusted-react", component: lazyComponent("StatusLight") }
1621
1693
  });
1622
- var signalBarsWidget = defineWidget10({
1694
+ var signalBarsWidget = defineWidget11({
1623
1695
  descriptor: builtinWidgetDescriptors["signal-bars"],
1624
1696
  implementation: { kind: "trusted-react", component: lazyComponent("SignalBars") }
1625
1697
  });
1626
- var compassWidget = defineWidget10({
1698
+ var compassWidget = defineWidget11({
1627
1699
  descriptor: builtinWidgetDescriptors["compass"],
1628
1700
  implementation: { kind: "trusted-react", component: lazyComponent("Compass", loadChartEngine2) }
1629
1701
  });
1630
- var progressRingWidget = defineWidget10({
1702
+ var progressRingWidget = defineWidget11({
1631
1703
  descriptor: builtinWidgetDescriptors["progress-ring"],
1632
1704
  implementation: { kind: "trusted-react", component: lazyComponent("ProgressRing") }
1633
1705
  });
1634
1706
 
1635
1707
  // src/widgets/map.tsx
1636
- import { useEffect, useRef, useState } from "react";
1708
+ import { useEffect as useEffect2, useRef, useState as useState2 } from "react";
1637
1709
  import { renderableBasemap, resolveBasemap } from "@devicechain/client";
1638
1710
 
1639
1711
  // src/basemap-context.tsx
@@ -1668,7 +1740,7 @@ function useMapRuntime() {
1668
1740
  import {
1669
1741
  css as css4,
1670
1742
  fontVar as fontVar4,
1671
- defineWidget as defineWidget11,
1743
+ defineWidget as defineWidget12,
1672
1744
  optString as optString5,
1673
1745
  useWidgetSelect,
1674
1746
  WidgetFrame as WidgetFrame5
@@ -1897,13 +1969,13 @@ function LiveMap({
1897
1969
  const containerRef = useRef(null);
1898
1970
  const mapRef = useRef(null);
1899
1971
  const markersRef = useRef([]);
1900
- const [status, setStatus] = useState("loading");
1972
+ const [status, setStatus] = useState2("loading");
1901
1973
  const pickRef = useRef(pick);
1902
1974
  pickRef.current = pick;
1903
1975
  const selectable = pick !== void 0;
1904
1976
  const tileUrl = basemap?.tileUrl ?? null;
1905
1977
  const attribution = basemap?.attribution ?? null;
1906
- useEffect(() => {
1978
+ useEffect2(() => {
1907
1979
  let cancelled = false;
1908
1980
  setStatus("loading");
1909
1981
  Promise.all([
@@ -1931,7 +2003,7 @@ function LiveMap({
1931
2003
  mapRef.current = null;
1932
2004
  };
1933
2005
  }, [tileUrl, attribution, runtime.workerUrl]);
1934
- useEffect(() => {
2006
+ useEffect2(() => {
1935
2007
  const map = mapRef.current;
1936
2008
  if (status !== "ready" || !map) return;
1937
2009
  let live = true;
@@ -2065,18 +2137,18 @@ function MapNotice({ glyph, heading, detail }) {
2065
2137
  }
2066
2138
  );
2067
2139
  }
2068
- var mapWidget = defineWidget11({
2140
+ var mapWidget = defineWidget12({
2069
2141
  descriptor: builtinWidgetDescriptors["map"],
2070
2142
  implementation: { kind: "trusted-react", component: MapWidget }
2071
2143
  });
2072
2144
 
2073
2145
  // src/widgets/table-lazy.tsx
2074
- import { defineWidget as defineWidget12 } from "@devicechain/widget-sdk";
2075
- var chunk7 = lazyChunk(() => import("./chunks/table-6EVV7QZH.js"));
2076
- var loadTableWidget = chunk7.load;
2077
- var tableWidget = defineWidget12({
2146
+ import { defineWidget as defineWidget13 } from "@devicechain/widget-sdk";
2147
+ var chunk8 = lazyChunk(() => import("./chunks/table-CEVB3JZS.js"));
2148
+ var loadTableWidget = chunk8.load;
2149
+ var tableWidget = defineWidget13({
2078
2150
  descriptor: builtinWidgetDescriptors["table"],
2079
- implementation: { kind: "trusted-react", component: chunk7.component("Table") }
2151
+ implementation: { kind: "trusted-react", component: chunk8.component("Table") }
2080
2152
  });
2081
2153
 
2082
2154
  // src/widgets/time-series-chart.tsx
@@ -2084,13 +2156,15 @@ import { useMemo as useMemo3 } from "react";
2084
2156
  import { isValidSpanSeconds, spanLabel } from "@devicechain/dashboards";
2085
2157
  import {
2086
2158
  css as css5,
2087
- defineWidget as defineWidget13,
2159
+ defineWidget as defineWidget14,
2088
2160
  fontVar as fontVar5,
2089
2161
  formatDateTime,
2090
2162
  formatTimestamp as formatTimestamp2,
2091
2163
  optNumber as optNumber4,
2092
2164
  optString as optString6,
2093
2165
  optThresholds as optThresholds3,
2166
+ pickSample as pickSample3,
2167
+ primaryMeasurementName as primaryMeasurementName3,
2094
2168
  spaceVar as spaceVar3,
2095
2169
  useWidgetScale as useWidgetScale2,
2096
2170
  useWidgetTimeRange,
@@ -2101,7 +2175,23 @@ import {
2101
2175
  import { buildLineOption, EChart as EChart2, loadChartEngine as loadChartEngine3, useChartTheme as useChartTheme2 } from "@devicechain/widget-sdk/echarts";
2102
2176
  import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
2103
2177
  function TimeSeriesChart({ widget, data, markers }) {
2104
- return /* @__PURE__ */ jsx8(WidgetFrame6, { title: optString6(widget.options, "title"), flush: true, children: /* @__PURE__ */ jsx8(TimeSeriesBody, { widget, data, markers }) });
2178
+ const range = useWidgetTimeRange();
2179
+ const pinned = optNumber4(widget.options, "windowSeconds");
2180
+ const spanSeconds = isValidSpanSeconds(pinned) ? pinned : range?.spanSeconds;
2181
+ const exportRows = () => {
2182
+ const device = widget.datasource?.kind === "device" ? widget.datasource.deviceToken : void 0;
2183
+ return import("./chunks/export-rows-OEWIBIUY.js").then(
2184
+ (m) => m.samplesExport({
2185
+ subject: optString6(widget.options, "title") ?? device ?? "chart",
2186
+ samples: data.samples,
2187
+ unit: optString6(widget.options, "unit"),
2188
+ precision: optNumber4(widget.options, "precision"),
2189
+ seriesIs: "measurement",
2190
+ spanSeconds
2191
+ })
2192
+ );
2193
+ };
2194
+ return /* @__PURE__ */ jsx8(WidgetFrame6, { title: optString6(widget.options, "title"), flush: true, exportRows, children: /* @__PURE__ */ jsx8(TimeSeriesBody, { widget, data, markers }) });
2105
2195
  }
2106
2196
  function TimeSeriesBody({ widget, data, markers }) {
2107
2197
  const theme = useChartTheme2();
@@ -2116,6 +2206,21 @@ function TimeSeriesBody({ widget, data, markers }) {
2116
2206
  const base = buildLineOption(data.samples, theme, { measurements, unit, precision, thresholds, scale, gaps: data.gaps, min, max }, size);
2117
2207
  return markers ? markers.decorate(base, size) : base;
2118
2208
  }, [data.samples, data.gaps, theme, measurements, unit, precision, thresholds, scale, size, markers, min, max]);
2209
+ const name = measurements && measurements.length > 1 ? void 0 : primaryMeasurementName3(widget);
2210
+ const explain = useSettledEmpty(data.samples.length === 0);
2211
+ if (explain) {
2212
+ return /* @__PURE__ */ jsx8(
2213
+ NoReading,
2214
+ {
2215
+ unbound: widget.datasource?.kind === "unbound",
2216
+ measurement: name,
2217
+ lastSample: pickSample3(data.latest, name),
2218
+ unit,
2219
+ precision,
2220
+ waitMs: 0
2221
+ }
2222
+ );
2223
+ }
2119
2224
  const chart = /* @__PURE__ */ jsxs4("div", { style: { position: "relative", width: "100%", height: "100%" }, children: [
2120
2225
  /* @__PURE__ */ jsx8(EChart2, { option }),
2121
2226
  /* @__PURE__ */ jsx8(SpanBadge, { widget, data })
@@ -2176,7 +2281,7 @@ function SpanBadge({ widget, data }) {
2176
2281
  );
2177
2282
  }
2178
2283
  TimeSeriesChart.preload = loadChartEngine3;
2179
- var timeSeriesChartWidget = defineWidget13({
2284
+ var timeSeriesChartWidget = defineWidget14({
2180
2285
  descriptor: builtinWidgetDescriptors["timeseries-chart"],
2181
2286
  implementation: { kind: "trusted-react", component: TimeSeriesChart }
2182
2287
  });
@@ -2210,7 +2315,8 @@ var builtinWidgetDefinitions = [
2210
2315
  fleetTableWidget,
2211
2316
  fleetCountWidget,
2212
2317
  deviceStatusWidget,
2213
- onlineDoughnutWidget
2318
+ onlineDoughnutWidget,
2319
+ historyTableWidget
2214
2320
  ];
2215
2321
  function createBuiltinWidgetRegistry() {
2216
2322
  return createWidgetRegistry(builtinWidgetDefinitions);
@@ -2225,7 +2331,11 @@ function defaultWidgetRegistry() {
2225
2331
 
2226
2332
  // src/connected-widget.tsx
2227
2333
  import {
2334
+ css as css6,
2335
+ fontVar as fontVar6,
2228
2336
  optString as optString7,
2337
+ spaceVar as spaceVar4,
2338
+ viewerLocale as viewerLocale2,
2229
2339
  useAlarmStream,
2230
2340
  useCommandStream,
2231
2341
  useDatasourceAvailability,
@@ -2237,7 +2347,7 @@ import {
2237
2347
  } from "@devicechain/widget-sdk";
2238
2348
 
2239
2349
  // src/widget-containment.tsx
2240
- import { Component, createContext as createContext3, useCallback, useContext as useContext3, useState as useState2 } from "react";
2350
+ import { Component, createContext as createContext3, useCallback, useContext as useContext3, useState as useState3 } from "react";
2241
2351
  import { WidgetFrame as WidgetFrame7 } from "@devicechain/widget-sdk";
2242
2352
 
2243
2353
  // src/widget-host.ts
@@ -2349,7 +2459,7 @@ var WidgetErrorBoundary = class extends Component {
2349
2459
  }
2350
2460
  };
2351
2461
  function WidgetContainment({ title, resetKey, children }) {
2352
- const [messages, setMessages] = useState2([]);
2462
+ const [messages, setMessages] = useState3([]);
2353
2463
  const report = useCallback((message) => {
2354
2464
  setMessages((prev) => prev.includes(message) ? prev : [...prev, message].slice(-3));
2355
2465
  }, []);
@@ -2392,6 +2502,7 @@ function ConnectedWidget(props) {
2392
2502
  return /* @__PURE__ */ jsx10(WidgetContainment, { title: optString7(widget.options, "title"), resetKey, children: /* @__PURE__ */ jsx10(ConnectedWidgetBody, { ...props, registry }) });
2393
2503
  }
2394
2504
  function ConnectedWidgetBody({ widget, hub, registry, actions, initialSamples, initialSamplesAsOf, onReconnect }) {
2505
+ const range = useWidgetTimeRange2();
2395
2506
  const unresolved = widget.type === UNKNOWN_WIDGET_TYPE;
2396
2507
  const descriptor = unresolved ? void 0 : registry.descriptor(widget.type);
2397
2508
  const implementation = unresolved ? void 0 : registry.implementation(widget.type);
@@ -2416,7 +2527,7 @@ function ConnectedWidgetBody({ widget, hub, registry, actions, initialSamples, i
2416
2527
  }
2417
2528
  );
2418
2529
  }
2419
- return /* @__PURE__ */ jsx10(HostScope, { descriptor, actions, children: (hostActions) => /* @__PURE__ */ jsx10(
2530
+ const body = /* @__PURE__ */ jsx10(DeclaredScope, { hub, datasource: widget.datasource, children: /* @__PURE__ */ jsx10(HostScope, { descriptor, actions, children: (hostActions) => /* @__PURE__ */ jsx10(
2420
2531
  ChannelBody,
2421
2532
  {
2422
2533
  widget,
@@ -2428,12 +2539,50 @@ function ConnectedWidgetBody({ widget, hub, registry, actions, initialSamples, i
2428
2539
  initialSamplesAsOf,
2429
2540
  onReconnect
2430
2541
  }
2431
- ) });
2542
+ ) }) });
2543
+ return range?.absolute !== void 0 && showsCurrentStateOnly(descriptor) ? /* @__PURE__ */ jsx10(CurrentStateBadge, { children: body }) : body;
2544
+ }
2545
+ function showsCurrentStateOnly(descriptor) {
2546
+ return descriptor.channel === "alarm" || descriptor.channel === "location" || presenceMode(descriptor);
2547
+ }
2548
+ var CURRENT_STATE_STRINGS = {
2549
+ en: { label: "Now", title: "Shows the current state: the fixed time window does not apply to this widget." },
2550
+ es: { label: "Ahora", title: "Muestra el estado actual: la ventana de tiempo fija no se aplica a este widget." }
2551
+ };
2552
+ function CurrentStateBadge({ children }) {
2553
+ const words = viewerLocale2()?.toLowerCase().startsWith("es") ? CURRENT_STATE_STRINGS.es : CURRENT_STATE_STRINGS.en;
2554
+ return /* @__PURE__ */ jsxs6("div", { "data-current-state": "", style: { position: "relative", width: "100%", height: "100%", minWidth: 0, minHeight: 0 }, children: [
2555
+ children,
2556
+ /* @__PURE__ */ jsx10(
2557
+ "span",
2558
+ {
2559
+ "data-current-state-badge": "",
2560
+ role: "note",
2561
+ title: words.title,
2562
+ "aria-label": words.title,
2563
+ style: {
2564
+ position: "absolute",
2565
+ // Bottom LEFT: a map keeps its attribution bottom right, and a chart its span badge top right.
2566
+ bottom: spaceVar4(1),
2567
+ left: spaceVar4(2),
2568
+ padding: `0 ${spaceVar4(1)}`,
2569
+ fontSize: fontVar6("xs"),
2570
+ fontWeight: 600,
2571
+ whiteSpace: "nowrap",
2572
+ color: css6("background"),
2573
+ background: css6("foreground"),
2574
+ borderRadius: 4,
2575
+ zIndex: 1
2576
+ },
2577
+ children: words.label
2578
+ }
2579
+ )
2580
+ ] });
2432
2581
  }
2433
2582
  function useDatasourceSupported(hub, datasource) {
2434
2583
  const key = datasource ? JSON.stringify(datasource) : null;
2435
- const [refused, setRefused] = useState3(null);
2436
- useEffect2(() => {
2584
+ const [refused, setRefused] = useState4(null);
2585
+ useEffect3(() => {
2437
2586
  if (!datasource || !hub.isDatasourceSupported) return;
2438
2587
  let cancelled = false;
2439
2588
  hub.isDatasourceSupported(datasource).then(
@@ -2450,6 +2599,26 @@ function useDatasourceSupported(hub, datasource) {
2450
2599
  }, [hub, key]);
2451
2600
  return refused === null || refused !== key;
2452
2601
  }
2602
+ function DeclaredScope({ hub, datasource, children }) {
2603
+ const token = datasource?.kind === "device" && datasource.deviceToken ? datasource.deviceToken : void 0;
2604
+ const [asked, setAsked] = useState4(false);
2605
+ const [got, setGot] = useState4();
2606
+ const ask = useCallback2(() => setAsked(true), []);
2607
+ useEffect3(() => {
2608
+ if (!asked || !token || !hub.declaredMetricsForDevice) return;
2609
+ let live = true;
2610
+ hub.declaredMetricsForDevice(token).then(
2611
+ (names) => live && setGot({ token, names }),
2612
+ () => void 0
2613
+ );
2614
+ return () => {
2615
+ live = false;
2616
+ };
2617
+ }, [asked, token, hub]);
2618
+ const declared = got && got.token === token ? got.names : void 0;
2619
+ const value = useMemo4(() => ({ declared, ask }), [declared, ask]);
2620
+ return /* @__PURE__ */ jsx10(WidgetDeclaredMetricsProvider, { value, children });
2621
+ }
2453
2622
  function HostScope({
2454
2623
  descriptor,
2455
2624
  actions,
@@ -2486,8 +2655,9 @@ function ChannelBody(props) {
2486
2655
  function StaticConnectedWidget({ widget, Component: Component2, actions }) {
2487
2656
  return /* @__PURE__ */ jsx10(Component2, { widget, data: null, actions });
2488
2657
  }
2489
- var FleetFeed = lazyChunk(() => import("./chunks/fleet-feed-2SKDR6ZQ.js")).component("default", void 0, ChunkLoadingFrame);
2490
- var PresenceFeed = lazyChunk(() => import("./chunks/presence-widgets-2LIUKN4Y.js")).component("PresenceFeed");
2658
+ var FleetFeed = lazyChunk(() => import("./chunks/fleet-feed-D4T3LC25.js")).component("default", void 0, ChunkLoadingFrame);
2659
+ var PresenceFeed = lazyChunk(() => import("./chunks/presence-widgets-M5VGG3OO.js")).component("PresenceFeed");
2660
+ var HistoryFeed = lazyChunk(() => import("./chunks/history-widgets-NSNXDKEC.js")).component("HistoryFeed");
2491
2661
  function MeasurementConnectedWidget(props) {
2492
2662
  if (presenceMode(props.descriptor)) {
2493
2663
  return /* @__PURE__ */ jsx10(PresenceFeed, { hub: props.hub, widget: props.widget, descriptor: props.descriptor, Component: props.Component, actions: props.actions, ErrorFrame: WidgetErrorFrame });
@@ -2497,6 +2667,9 @@ function MeasurementConnectedWidget(props) {
2497
2667
  if (members || widget.datasource?.kind === "anchor") {
2498
2668
  return /* @__PURE__ */ jsx10(FleetFeed, { hub, widget, descriptor, Component: Component2, actions, ErrorFrame: WidgetErrorFrame, refuse: !members });
2499
2669
  }
2670
+ if (historyMode(descriptor)) {
2671
+ return /* @__PURE__ */ jsx10(HistoryFeed, { hub, widget, descriptor, Component: Component2, actions });
2672
+ }
2500
2673
  return /* @__PURE__ */ jsx10(StreamConnectedWidget, { ...props });
2501
2674
  }
2502
2675
  function StreamConnectedWidget({ widget, hub, descriptor, Component: Component2, actions, initialSamples, initialSamplesAsOf, onReconnect }) {
@@ -2523,8 +2696,8 @@ function StreamConnectedWidget({ widget, hub, descriptor, Component: Component2,
2523
2696
  if (data.error) return /* @__PURE__ */ jsx10(WidgetErrorFrame, { widget, error: data.error });
2524
2697
  return /* @__PURE__ */ jsx10(WidgetFreshnessProvider, { freshness, children: wantsEventMarkers(descriptor, widget) ? /* @__PURE__ */ jsx10(EventMarkersFeed, { hub, widget, descriptor, data, Component: Component2, actions }) : /* @__PURE__ */ jsx10(Component2, { widget, data, actions }) });
2525
2698
  }
2526
- var EventMarkersFeed = lazyChunk(() => import("./chunks/event-markers-feed-GEGTIHXT.js")).component("default");
2527
- var AggregateFeed = lazyChunk(() => import("./chunks/aggregate-feed-4FSBQYJL.js")).component("default");
2699
+ var EventMarkersFeed = lazyChunk(() => import("./chunks/event-markers-feed-B3MVFYUT.js")).component("default");
2700
+ var AggregateFeed = lazyChunk(() => import("./chunks/aggregate-feed-XX36GNY6.js")).component("default");
2528
2701
  function AggregateConnectedWidget({ widget, hub, descriptor, Component: Component2, actions }) {
2529
2702
  const props = { hub, widget, descriptor, Component: Component2, actions, ErrorFrame: WidgetErrorFrame };
2530
2703
  return widget.datasource?.kind === "anchor" ? /* @__PURE__ */ jsx10(FleetFeed, { ...props, refuse: true }) : /* @__PURE__ */ jsx10(AggregateFeed, { ...props });
@@ -2600,52 +2773,45 @@ function UnresolvedWidgetFrame({
2600
2773
  }
2601
2774
  ) });
2602
2775
  }
2776
+ function StatePane({
2777
+ widget,
2778
+ kind,
2779
+ glyph,
2780
+ title,
2781
+ detail
2782
+ }) {
2783
+ return /* @__PURE__ */ jsx10(WidgetFrame8, { title: optString7(widget.options, "title"), children: /* @__PURE__ */ jsx10(StateBody, { kind, glyph, title, detail }) });
2784
+ }
2785
+ function StateBody({ kind, glyph, title, detail }) {
2786
+ const { height } = useWidgetScale3().size;
2787
+ const measured = height > 0;
2788
+ return /* @__PURE__ */ jsxs6("div", { role: "status", "data-empty-state": kind, style: STATE_STYLE, children: [
2789
+ !measured || height >= 84 ? /* @__PURE__ */ jsx10("div", { style: { fontSize: 20, lineHeight: 1 }, "aria-hidden": "true", children: glyph }) : null,
2790
+ /* @__PURE__ */ jsx10("div", { style: { fontSize: 13, fontWeight: 600 }, children: title }),
2791
+ detail && (!measured || height >= 64) ? /* @__PURE__ */ jsx10("div", { style: { fontSize: 11, opacity: 0.8, wordBreak: "break-word" }, children: detail }) : null
2792
+ ] });
2793
+ }
2603
2794
  function WidgetForbiddenFrame({ widget }) {
2604
- return /* @__PURE__ */ jsx10(WidgetFrame8, { title: optString7(widget.options, "title"), children: /* @__PURE__ */ jsxs6("div", { role: "status", style: STATE_STYLE, children: [
2605
- /* @__PURE__ */ jsx10("div", { style: { fontSize: 20, lineHeight: 1 }, "aria-hidden": "true", children: "\u2298" }),
2606
- /* @__PURE__ */ jsx10("div", { style: { fontSize: 13, fontWeight: 600 }, children: accessStrings().forbidden })
2607
- ] }) });
2795
+ return /* @__PURE__ */ jsx10(StatePane, { widget, kind: "forbidden", glyph: "\u2298", title: accessStrings().forbidden });
2608
2796
  }
2609
2797
  function WidgetUnavailableFrame({ widget }) {
2610
- return /* @__PURE__ */ jsx10(WidgetFrame8, { title: optString7(widget.options, "title"), children: /* @__PURE__ */ jsxs6(
2611
- "div",
2612
- {
2613
- style: STATE_STYLE,
2614
- children: [
2615
- /* @__PURE__ */ jsx10("div", { style: { fontSize: 20, lineHeight: 1 }, "aria-hidden": "true", children: "\u2298" }),
2616
- /* @__PURE__ */ jsx10("div", { style: { fontSize: 13, fontWeight: 600 }, children: "Device unavailable" }),
2617
- /* @__PURE__ */ jsx10("div", { style: { fontSize: 11, opacity: 0.8 }, children: "The bound device no longer exists." })
2618
- ]
2619
- }
2620
- ) });
2798
+ return /* @__PURE__ */ jsx10(StatePane, { widget, kind: "gone", glyph: "\u2298", title: "Device unavailable", detail: "The bound device no longer exists." });
2621
2799
  }
2622
2800
  function WidgetNotForCustomerUsersFrame({ widget }) {
2623
- return /* @__PURE__ */ jsx10(WidgetFrame8, { title: optString7(widget.options, "title"), children: /* @__PURE__ */ jsxs6(
2624
- "div",
2801
+ return /* @__PURE__ */ jsx10(
2802
+ StatePane,
2625
2803
  {
2626
- role: "status",
2627
- style: STATE_STYLE,
2628
- children: [
2629
- /* @__PURE__ */ jsx10("div", { style: { fontSize: 20, lineHeight: 1 }, "aria-hidden": "true", children: "\u2298" }),
2630
- /* @__PURE__ */ jsx10("div", { style: { fontSize: 13, fontWeight: 600 }, children: "Not available for customer users" }),
2631
- /* @__PURE__ */ jsx10("div", { style: { fontSize: 11, opacity: 0.8 }, children: "This widget is bound to an area or an asset, which customer users cannot view." })
2632
- ]
2804
+ widget,
2805
+ kind: "unsupported",
2806
+ glyph: "\u2298",
2807
+ title: "Not available for customer users",
2808
+ detail: "This widget is bound to an area or an asset, which customer users cannot view."
2633
2809
  }
2634
- ) });
2810
+ );
2635
2811
  }
2636
2812
  function WidgetErrorFrame({ widget, error }) {
2637
2813
  if (isForbiddenError(error)) return /* @__PURE__ */ jsx10(WidgetForbiddenFrame, { widget });
2638
- return /* @__PURE__ */ jsx10(WidgetFrame8, { title: optString7(widget.options, "title"), children: /* @__PURE__ */ jsxs6(
2639
- "div",
2640
- {
2641
- style: STATE_STYLE,
2642
- children: [
2643
- /* @__PURE__ */ jsx10("div", { style: { fontSize: 20, lineHeight: 1 }, children: "\u26A0" }),
2644
- /* @__PURE__ */ jsx10("div", { style: { fontSize: 13, fontWeight: 600 }, children: "Data unavailable" }),
2645
- /* @__PURE__ */ jsx10("div", { style: { fontSize: 11, opacity: 0.8, wordBreak: "break-word" }, children: String(error) })
2646
- ]
2647
- }
2648
- ) });
2814
+ return /* @__PURE__ */ jsx10(StatePane, { widget, kind: "error", glyph: "\u2026", title: "Data unavailable", detail: String(error) });
2649
2815
  }
2650
2816
 
2651
2817
  // src/dashboard-renderer.tsx
@@ -2680,12 +2846,13 @@ function DashboardRenderer({
2680
2846
  [timeRange, defaultSeconds]
2681
2847
  );
2682
2848
  const { histories, reseed } = useWidgetHistories(widgets, seedHistory, registry, range);
2849
+ const windowKey = windowTag(range);
2683
2850
  const { grid, background: bg, frame: canvasFrame } = definition.canvas;
2684
2851
  const effectiveSizing = sizing ?? definition.canvas.sizing;
2685
2852
  const effectiveDensity = density ?? definition.canvas.density ?? DEFAULT_CANVAS_DENSITY;
2686
2853
  const scale = densityScale(effectiveDensity);
2687
2854
  const { canvas } = definition;
2688
- const shapeKey = useCallback2((width) => JSON.stringify(layoutShape(canvas, width, scale)), [canvas, scale]);
2855
+ const shapeKey = useCallback3((width) => JSON.stringify(layoutShape(canvas, width, scale)), [canvas, scale]);
2689
2856
  const [boardRef, containerWidth] = useContainerWidth(shapeKey);
2690
2857
  const layout = useMemo5(
2691
2858
  () => effectiveLayout(definition, containerWidth, {
@@ -2737,7 +2904,8 @@ function DashboardRenderer({
2737
2904
  initialSamples: histories[widget.id]?.samples,
2738
2905
  initialSamplesAsOf: histories[widget.id]?.asOf,
2739
2906
  onReconnect: () => reseed(widget.id)
2740
- }
2907
+ },
2908
+ windowKey
2741
2909
  ) }) })
2742
2910
  }
2743
2911
  )
@@ -2854,9 +3022,9 @@ function widgetSubjectLabel(widget) {
2854
3022
  }
2855
3023
  function useContainerWidth(keyOf) {
2856
3024
  const ref = useRef2(null);
2857
- const [width, setWidth] = useState4(0);
3025
+ const [width, setWidth] = useState5(0);
2858
3026
  const keyOfRef = useRef2(keyOf);
2859
- const measure = useCallback2(() => {
3027
+ const measure = useCallback3(() => {
2860
3028
  const el = ref.current;
2861
3029
  if (!el) return;
2862
3030
  const next = el.clientWidth;
@@ -2875,6 +3043,9 @@ function useContainerWidth(keyOf) {
2875
3043
  }, [measure]);
2876
3044
  return [ref, width];
2877
3045
  }
3046
+ function windowTag(range) {
3047
+ return range.absolute ? `${range.absolute.fromMs}-${range.absolute.toMs}` : "";
3048
+ }
2878
3049
  var RESEED_RETRY_MS = 3e4;
2879
3050
  function seedKey(widget, registry) {
2880
3051
  if (widget.type === UNKNOWN_WIDGET_TYPE2 || registry.descriptor(widget.type)?.channel !== "measurement") {
@@ -2883,18 +3054,18 @@ function seedKey(widget, registry) {
2883
3054
  return JSON.stringify(widget.datasource ?? null);
2884
3055
  }
2885
3056
  function useWidgetHistories(widgets, enabled, registry, range) {
2886
- const [seeds, setSeeds] = useState4({});
3057
+ const [seeds, setSeeds] = useState5({});
2887
3058
  const requested = useRef2({});
2888
3059
  const reseeds = useRef2({});
2889
- const [reseedTick, bumpReseeds] = useState4(0);
3060
+ const [reseedTick, bumpReseeds] = useState5(0);
2890
3061
  const mounted = useRef2(true);
2891
- useEffect3(() => {
3062
+ useEffect4(() => {
2892
3063
  mounted.current = true;
2893
3064
  return () => {
2894
3065
  mounted.current = false;
2895
3066
  };
2896
3067
  }, []);
2897
- const reseed = useCallback2((id) => {
3068
+ const reseed = useCallback3((id) => {
2898
3069
  reseeds.current[id] = (reseeds.current[id] ?? 0) + 1;
2899
3070
  bumpReseeds((n) => n + 1);
2900
3071
  }, []);
@@ -2905,8 +3076,9 @@ function useWidgetHistories(widgets, enabled, registry, range) {
2905
3076
  const span = descriptor ? measurementSpan(descriptor, w, range.spanSeconds) ?? null : null;
2906
3077
  return [[w.id, key, span]];
2907
3078
  });
2908
- const wantedKey = JSON.stringify(wanted);
2909
- useEffect3(() => {
3079
+ const tag = windowTag(range);
3080
+ const wantedKey = JSON.stringify([tag, wanted]);
3081
+ useEffect4(() => {
2910
3082
  if (!enabled) {
2911
3083
  requested.current = {};
2912
3084
  setSeeds({});
@@ -2916,7 +3088,7 @@ function useWidgetHistories(widgets, enabled, registry, range) {
2916
3088
  const byId = new Map(widgets.map((w) => [w.id, w]));
2917
3089
  const next = {};
2918
3090
  for (const [id, key, span] of wanted) {
2919
- const base = `${key}|${span ?? ""}|`;
3091
+ const base = `${key}|${span ?? ""}|${tag}|`;
2920
3092
  const n = reseeds.current[id] ?? 0;
2921
3093
  const asked = `${base}${range.paused && n ? "p" : n}`;
2922
3094
  const previous = requested.current[id];
@@ -2928,12 +3100,12 @@ function useWidgetHistories(widgets, enabled, registry, range) {
2928
3100
  const widget = byId.get(id);
2929
3101
  if (widget?.datasource?.kind !== "device") continue;
2930
3102
  const end = range.paused && range.pausedAt !== void 0 ? range.pausedAt : now;
2931
- const historyWindow = span === null ? defaultHistoryWindow() : historyWindowFor(span, end);
3103
+ const historyWindow = span !== null ? historyWindowFor(span, end) : range.absolute ? historyWindowFor(DEFAULT_TIME_RANGE_SECONDS, end) : defaultHistoryWindow();
2932
3104
  const current = () => mounted.current && requested.current[id] === asked;
2933
3105
  void fetchWidgetHistory(widget, historyWindow, { rethrow: n > 0 }).then(
2934
3106
  (samples) => {
2935
3107
  if (!current()) return;
2936
- setSeeds((prev) => ({ ...prev, [id]: { key, samples, asOf: end } }));
3108
+ setSeeds((prev) => ({ ...prev, [id]: { key, window: tag, samples, asOf: end } }));
2937
3109
  },
2938
3110
  () => {
2939
3111
  setTimeout(() => current() && reseed(id), RESEED_RETRY_MS);
@@ -2947,45 +3119,80 @@ function useWidgetHistories(widgets, enabled, registry, range) {
2947
3119
  if (!enabled) return out;
2948
3120
  for (const w of widgets) {
2949
3121
  const seed = seeds[w.id];
2950
- out[w.id] = seed && seed.key === seedKey(w, registry) ? seed : void 0;
3122
+ out[w.id] = seed && seed.key === seedKey(w, registry) && seed.window === tag ? seed : void 0;
2951
3123
  }
2952
3124
  return out;
2953
- }, [widgets, seeds, enabled, registry]);
3125
+ }, [widgets, seeds, enabled, registry, tag]);
2954
3126
  return { histories, reseed };
2955
3127
  }
2956
3128
 
2957
3129
  // src/time-range-bar.tsx
2958
3130
  import {
2959
- DEFAULT_TIME_RANGE_SECONDS,
3131
+ DEFAULT_TIME_RANGE_SECONDS as DEFAULT_TIME_RANGE_SECONDS2,
2960
3132
  TIME_RANGE_PRESETS,
3133
+ absoluteRange,
3134
+ absoluteToParams,
2961
3135
  initialTimeRange as initialTimeRange2,
2962
3136
  isValidSpanSeconds as isValidSpanSeconds2,
3137
+ leaveAbsolute,
2963
3138
  pauseRange,
2964
3139
  presetIdOf,
2965
3140
  resumeRange,
2966
3141
  spanLabel as spanLabel2,
2967
3142
  withSpan
2968
3143
  } from "@devicechain/dashboards";
2969
- import { useCallback as useCallback3, useEffect as useEffect4, useMemo as useMemo6, useRef as useRef3, useState as useState5 } from "react";
2970
- import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
3144
+ import { useCallback as useCallback4, useEffect as useEffect5, useMemo as useMemo6, useRef as useRef3, useState as useState6 } from "react";
3145
+ import { Fragment as Fragment3, jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
2971
3146
  function useBoardTimeRange(defaultSeconds, link = {}) {
2972
- const [state, setState] = useState5(() => initialTimeRange2(defaultSeconds, link));
3147
+ const [state, setState] = useState6(() => initialTimeRange2(defaultSeconds, link));
2973
3148
  const lastDefault = useRef3(defaultSeconds);
2974
- useEffect4(() => {
3149
+ useEffect5(() => {
2975
3150
  if (lastDefault.current === defaultSeconds) return;
2976
3151
  lastDefault.current = defaultSeconds;
2977
- setState((s) => withSpan(s, isValidSpanSeconds2(defaultSeconds) ? defaultSeconds : DEFAULT_TIME_RANGE_SECONDS));
3152
+ setState((s) => withSpan(s, isValidSpanSeconds2(defaultSeconds) ? defaultSeconds : DEFAULT_TIME_RANGE_SECONDS2));
2978
3153
  }, [defaultSeconds]);
2979
3154
  const controls = useMemo6(
2980
3155
  () => ({
2981
- setSpan: (spanSeconds) => setState((s) => withSpan(s, spanSeconds)),
3156
+ // A preset click while a fixed window is showing leaves it: back to live at that span.
3157
+ setSpan: (spanSeconds) => setState((s) => withSpan(leaveAbsolute(s), spanSeconds)),
2982
3158
  pause: () => setState((s) => pauseRange(s, Date.now())),
2983
- resume: () => setState((s) => resumeRange(s))
3159
+ resume: () => setState((s) => resumeRange(s)),
3160
+ setAbsolute: (fromMs, toMs) => {
3161
+ const made = absoluteRange(fromMs, toMs);
3162
+ if ("problem" in made) return made.problem;
3163
+ setState(made.state);
3164
+ return void 0;
3165
+ }
2984
3166
  }),
2985
3167
  []
2986
3168
  );
2987
3169
  return [state, controls];
2988
3170
  }
3171
+ var DEFAULT_ABSOLUTE_LABELS = {
3172
+ custom: "Custom range",
3173
+ from: "From",
3174
+ to: "To",
3175
+ apply: "Apply",
3176
+ cancel: "Cancel",
3177
+ zone: (zone, offset) => `Times are in ${zone} (${offset}). Shared links use UTC.`,
3178
+ problem: {
3179
+ unparseable: "Enter both a start and an end.",
3180
+ order: "The start must be before the end.",
3181
+ tooShort: "The window must be at least one minute.",
3182
+ tooLong: "The window can be at most 31 days.",
3183
+ future: "The end cannot be in the future.",
3184
+ tooEarly: "The start is too far in the past."
3185
+ },
3186
+ fixed: "Fixed window",
3187
+ backToLive: "Back to live",
3188
+ copyLink: "Copy link",
3189
+ linkCopied: "Link copied",
3190
+ loadFailed: "The range picker could not be loaded. Try again.",
3191
+ dismiss: "Dismiss",
3192
+ ignored: (reason, raw) => `The link's time window was ignored: ${reason} (${raw}). Showing the live board.`,
3193
+ incomplete: "both from and to are required",
3194
+ unzoned: "times must be ISO 8601 with a zone, such as 2026-10-22T02:00:00Z or +02:00"
3195
+ };
2989
3196
  var DEFAULT_TIME_RANGE_LABELS = {
2990
3197
  range: "Time range",
2991
3198
  pause: "Pause",
@@ -2994,6 +3201,13 @@ var DEFAULT_TIME_RANGE_LABELS = {
2994
3201
  pausedAt: (time) => `Paused at ${time}`,
2995
3202
  actionsLive: "commands still send"
2996
3203
  };
3204
+ var absoluteLabels = (labels) => ({ ...DEFAULT_ABSOLUTE_LABELS, ...labels.absolute });
3205
+ function rejectedLinkNote(rejected, labels = DEFAULT_TIME_RANGE_LABELS) {
3206
+ const l = absoluteLabels(labels);
3207
+ const raw = `from=${rejected.from || "-"} to=${rejected.to || "-"}`;
3208
+ const reason = rejected.reason === "incomplete" ? l.incomplete : rejected.reason === "unparseable" ? l.unzoned : l.problem[rejected.reason];
3209
+ return l.ignored(reason, raw);
3210
+ }
2997
3211
  var button = (active) => ({
2998
3212
  padding: "2px 8px",
2999
3213
  fontSize: 12,
@@ -3038,40 +3252,169 @@ function PausedIndicator({
3038
3252
  }
3039
3253
  );
3040
3254
  }
3041
- function TimeRangeBar({
3042
- state,
3043
- controls,
3255
+ function formatAbsoluteRange(a) {
3256
+ const f = new Intl.DateTimeFormat(void 0, { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", timeZoneName: "short" });
3257
+ return `${f.format(a.fromMs)} \u2013 ${f.format(a.toMs)}`;
3258
+ }
3259
+ function AbsoluteIndicator({
3260
+ absolute,
3044
3261
  labels = DEFAULT_TIME_RANGE_LABELS
3045
3262
  }) {
3046
- const preset = presetIdOf(state.spanSeconds);
3047
- const onPause = useCallback3(() => state.paused ? controls.resume() : controls.pause(), [state.paused, controls]);
3263
+ const l = absoluteLabels(labels);
3264
+ const utc = absoluteToParams(absolute);
3048
3265
  return /* @__PURE__ */ jsxs8(
3049
- "div",
3266
+ "span",
3050
3267
  {
3051
- "data-time-range-bar": "",
3052
- role: "group",
3053
- "aria-label": labels.range,
3054
- style: { display: "inline-flex", alignItems: "center", gap: 6, flexWrap: "wrap" },
3268
+ role: "status",
3269
+ "data-time-range-absolute": "",
3270
+ title: `${utc.from} \u2013 ${utc.to} (UTC)`,
3271
+ style: {
3272
+ display: "inline-flex",
3273
+ alignItems: "center",
3274
+ flexWrap: "wrap",
3275
+ gap: 4,
3276
+ padding: "2px 8px",
3277
+ fontSize: 12,
3278
+ fontWeight: 700,
3279
+ color: "hsl(var(--background))",
3280
+ background: "hsl(var(--foreground))",
3281
+ border: "2px solid hsl(var(--foreground))",
3282
+ borderRadius: 4
3283
+ },
3055
3284
  children: [
3056
- preset === void 0 ? /* @__PURE__ */ jsx12("button", { type: "button", "aria-pressed": "true", "data-range": spanLabel2(state.spanSeconds), style: button(true), children: spanLabel2(state.spanSeconds) }) : null,
3057
- TIME_RANGE_PRESETS.map((p) => /* @__PURE__ */ jsx12(
3058
- "button",
3059
- {
3060
- type: "button",
3061
- "aria-pressed": p.id === preset,
3062
- "data-range": p.id,
3063
- onClick: () => controls.setSpan(p.seconds),
3064
- style: button(p.id === preset),
3065
- children: p.id
3066
- },
3067
- p.id
3068
- )),
3069
- /* @__PURE__ */ jsx12("button", { type: "button", "aria-pressed": state.paused, "data-time-range-pause": "", onClick: onPause, style: button(state.paused), children: state.paused ? labels.resume : labels.pause }),
3070
- state.paused ? /* @__PURE__ */ jsx12(PausedIndicator, { pausedAt: state.pausedAt, labels }) : null
3285
+ /* @__PURE__ */ jsx12("span", { style: { letterSpacing: 0.4, textTransform: "uppercase" }, children: l.fixed }),
3286
+ /* @__PURE__ */ jsx12("span", { "data-time-range-window": "", style: { fontWeight: 400 }, children: formatAbsoluteRange(absolute) }),
3287
+ /* @__PURE__ */ jsx12("span", { "data-time-range-actions-live": "", style: { fontWeight: 400 }, children: `\xB7 ${labels.actionsLive}` })
3071
3288
  ]
3072
3289
  }
3073
3290
  );
3074
3291
  }
3292
+ function absoluteLinkFor(href, a) {
3293
+ const url = new URL(href);
3294
+ const { from, to } = absoluteToParams(a);
3295
+ url.searchParams.delete("range");
3296
+ url.searchParams.delete("paused");
3297
+ url.searchParams.set("from", from);
3298
+ url.searchParams.set("to", to);
3299
+ return url.toString();
3300
+ }
3301
+ function TimeRangeBar({
3302
+ state,
3303
+ controls,
3304
+ labels = DEFAULT_TIME_RANGE_LABELS,
3305
+ notice
3306
+ }) {
3307
+ const abs = state.absolute;
3308
+ const preset = abs ? void 0 : presetIdOf(state.spanSeconds);
3309
+ const onPause = useCallback4(() => state.paused ? controls.resume() : controls.pause(), [state.paused, controls]);
3310
+ const l = absoluteLabels(labels);
3311
+ const [Picker, setPicker] = useState6();
3312
+ const [open, setOpen] = useState6(false);
3313
+ const [loadFailed, setLoadFailed] = useState6(false);
3314
+ const [copied, setCopied] = useState6(false);
3315
+ const [noticeShown, setNoticeShown] = useState6(true);
3316
+ useEffect5(() => setNoticeShown(true), [notice]);
3317
+ const openPicker = () => {
3318
+ if (open) {
3319
+ setOpen(false);
3320
+ return;
3321
+ }
3322
+ setLoadFailed(false);
3323
+ if (Picker) {
3324
+ setOpen(true);
3325
+ return;
3326
+ }
3327
+ import("./chunks/absolute-range-picker-E57G2TPX.js").then(
3328
+ (m) => {
3329
+ setPicker(() => m.default);
3330
+ setOpen(true);
3331
+ },
3332
+ () => setLoadFailed(true)
3333
+ );
3334
+ };
3335
+ const copyLink = () => {
3336
+ if (!abs) return;
3337
+ navigator.clipboard?.writeText(absoluteLinkFor(window.location.href, abs)).then(
3338
+ () => {
3339
+ setCopied(true);
3340
+ window.setTimeout(() => setCopied(false), 2e3);
3341
+ },
3342
+ () => void 0
3343
+ );
3344
+ };
3345
+ return /* @__PURE__ */ jsxs8("div", { style: { position: "relative", display: "inline-block", maxWidth: "100%" }, children: [
3346
+ /* @__PURE__ */ jsxs8(
3347
+ "div",
3348
+ {
3349
+ "data-time-range-bar": "",
3350
+ role: "group",
3351
+ "aria-label": labels.range,
3352
+ style: { display: "inline-flex", alignItems: "center", gap: 6, flexWrap: "wrap" },
3353
+ children: [
3354
+ !abs && preset === void 0 ? /* @__PURE__ */ jsx12("button", { type: "button", "aria-pressed": "true", "data-range": spanLabel2(state.spanSeconds), style: button(true), children: spanLabel2(state.spanSeconds) }) : null,
3355
+ TIME_RANGE_PRESETS.map((p) => /* @__PURE__ */ jsx12(
3356
+ "button",
3357
+ {
3358
+ type: "button",
3359
+ "aria-pressed": p.id === preset,
3360
+ "data-range": p.id,
3361
+ onClick: () => controls.setSpan(p.seconds),
3362
+ style: button(p.id === preset),
3363
+ children: p.id
3364
+ },
3365
+ p.id
3366
+ )),
3367
+ /* @__PURE__ */ jsx12(
3368
+ "button",
3369
+ {
3370
+ type: "button",
3371
+ "aria-pressed": abs !== void 0,
3372
+ "aria-expanded": open,
3373
+ "data-time-range-custom": "",
3374
+ onClick: openPicker,
3375
+ style: button(abs !== void 0),
3376
+ children: l.custom
3377
+ }
3378
+ ),
3379
+ abs ? /* @__PURE__ */ jsxs8(Fragment3, { children: [
3380
+ /* @__PURE__ */ jsx12("button", { type: "button", "data-time-range-live": "", onClick: () => controls.resume(), style: button(false), children: l.backToLive }),
3381
+ /* @__PURE__ */ jsx12("button", { type: "button", "data-time-range-copy": "", onClick: copyLink, style: button(false), children: copied ? l.linkCopied : l.copyLink }),
3382
+ /* @__PURE__ */ jsx12(AbsoluteIndicator, { absolute: abs, labels })
3383
+ ] }) : /* @__PURE__ */ jsxs8(Fragment3, { children: [
3384
+ /* @__PURE__ */ jsx12("button", { type: "button", "aria-pressed": state.paused, "data-time-range-pause": "", onClick: onPause, style: button(state.paused), children: state.paused ? labels.resume : labels.pause }),
3385
+ state.paused ? /* @__PURE__ */ jsx12(PausedIndicator, { pausedAt: state.pausedAt, labels }) : null
3386
+ ] })
3387
+ ]
3388
+ }
3389
+ ),
3390
+ open && Picker ? /* @__PURE__ */ jsx12("div", { style: { position: "absolute", right: 0, top: "100%", marginTop: 6, zIndex: 30 }, children: /* @__PURE__ */ jsx12(
3391
+ Picker,
3392
+ {
3393
+ initial: abs,
3394
+ labels: l,
3395
+ onApply: (from, to) => {
3396
+ const problem = controls.setAbsolute(from, to);
3397
+ if (!problem) setOpen(false);
3398
+ return problem;
3399
+ },
3400
+ onCancel: () => setOpen(false)
3401
+ }
3402
+ ) }) : null,
3403
+ loadFailed ? /* @__PURE__ */ jsx12("div", { role: "alert", "data-time-range-load-failed": "", style: { fontSize: 12, color: "hsl(var(--destructive))" }, children: l.loadFailed }) : null,
3404
+ notice && noticeShown ? /* @__PURE__ */ jsxs8(
3405
+ "div",
3406
+ {
3407
+ role: "status",
3408
+ "data-time-range-notice": "",
3409
+ style: { display: "flex", gap: 8, alignItems: "baseline", marginTop: 4, fontSize: 12, color: "hsl(var(--muted-foreground))" },
3410
+ children: [
3411
+ /* @__PURE__ */ jsx12("span", { style: { minWidth: 0, overflowWrap: "anywhere" }, children: notice }),
3412
+ /* @__PURE__ */ jsx12("button", { type: "button", onClick: () => setNoticeShown(false), style: { ...button(false), flex: "0 0 auto" }, children: l.dismiss })
3413
+ ]
3414
+ }
3415
+ ) : null
3416
+ ] });
3417
+ }
3075
3418
 
3076
3419
  // src/use-bound-devices.ts
3077
3420
  import {
@@ -3082,7 +3425,7 @@ import {
3082
3425
  validateBinds,
3083
3426
  withBindParams
3084
3427
  } from "@devicechain/dashboards";
3085
- import { useCallback as useCallback4, useEffect as useEffect5, useRef as useRef4, useState as useState6 } from "react";
3428
+ import { useCallback as useCallback5, useEffect as useEffect6, useRef as useRef4, useState as useState7 } from "react";
3086
3429
  var NO_PROBLEMS = { issues: [], missing: [], failed: [] };
3087
3430
  function hasBindParams(search, definition) {
3088
3431
  const { binds, issues } = parseBindParams(search, definition);
@@ -3094,16 +3437,16 @@ function useBoundDevices({
3094
3437
  address,
3095
3438
  enabled = true
3096
3439
  }) {
3097
- const [core, setCore] = useState6(() => ({
3440
+ const [core, setCore] = useState7(() => ({
3098
3441
  selection: {},
3099
3442
  synced: bindParamsOf(address.search)
3100
3443
  }));
3101
3444
  const { selection } = core;
3102
- const [phase, setPhase] = useState6(
3445
+ const [phase, setPhase] = useState7(
3103
3446
  () => hasBindParams(address.search, definition) ? "checking" : "ready"
3104
3447
  );
3105
- const [problems, setProblems] = useState6(null);
3106
- const [okBinds, setOkBinds] = useState6({});
3448
+ const [problems, setProblems] = useState7(null);
3449
+ const [okBinds, setOkBinds] = useState7({});
3107
3450
  const definitionRef = useRef4(definition);
3108
3451
  definitionRef.current = definition;
3109
3452
  const resolverRef = useRef4(resolver);
@@ -3112,9 +3455,9 @@ function useBoundDevices({
3112
3455
  replaceRef.current = address.replaceSearch;
3113
3456
  const seenRef = useRef4(null);
3114
3457
  const generation = useRef4(0);
3115
- const [attempt, setAttempt] = useState6(0);
3458
+ const [attempt, setAttempt] = useState7(0);
3116
3459
  const search = address.search;
3117
- useEffect5(() => {
3460
+ useEffect6(() => {
3118
3461
  if (!enabled) return;
3119
3462
  const slice = bindParamsOf(search);
3120
3463
  if (seenRef.current === slice) return;
@@ -3143,7 +3486,7 @@ function useBoundDevices({
3143
3486
  }
3144
3487
  });
3145
3488
  }, [search, enabled, attempt]);
3146
- useEffect5(() => {
3489
+ useEffect6(() => {
3147
3490
  if (!enabled || phase !== "ready") return;
3148
3491
  const current = bindParamsOf(search);
3149
3492
  if (current !== core.synced) return;
@@ -3154,16 +3497,16 @@ function useBoundDevices({
3154
3497
  setCore((c) => ({ ...c, synced: nextSlice }));
3155
3498
  replaceRef.current(next);
3156
3499
  }, [core, definition, phase, enabled, search]);
3157
- const select = useCallback4((t) => {
3500
+ const select = useCallback5((t) => {
3158
3501
  setCore((c) => ({ ...c, selection: { ...c.selection, [t.slot]: t.binding } }));
3159
3502
  }, []);
3160
- const reset = useCallback4(() => setCore((c) => ({ ...c, selection: {} })), []);
3161
- const dismiss = useCallback4(() => {
3503
+ const reset = useCallback5(() => setCore((c) => ({ ...c, selection: {} })), []);
3504
+ const dismiss = useCallback5(() => {
3162
3505
  setCore((c) => ({ ...c, selection: bindsToSelection(okBinds) }));
3163
3506
  setProblems(null);
3164
3507
  setPhase("ready");
3165
3508
  }, [okBinds]);
3166
- const retry = useCallback4(() => {
3509
+ const retry = useCallback5(() => {
3167
3510
  seenRef.current = null;
3168
3511
  setAttempt((n) => n + 1);
3169
3512
  }, []);
@@ -3171,17 +3514,17 @@ function useBoundDevices({
3171
3514
  }
3172
3515
 
3173
3516
  // src/bound-devices-notice.tsx
3174
- import { css as css6, fontVar as fontVar6 } from "@devicechain/widget-sdk";
3517
+ import { css as css7, fontVar as fontVar7 } from "@devicechain/widget-sdk";
3175
3518
  import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
3176
3519
  function BoundDevicesNotice({ title, lines, dismissLabel, onDismiss, retryLabel, onRetry }) {
3177
3520
  const button2 = {
3178
3521
  minHeight: 44,
3179
3522
  padding: "0 16px",
3180
3523
  borderRadius: 8,
3181
- border: `1px solid ${css6("border")}`,
3182
- background: css6("card"),
3183
- color: css6("foreground"),
3184
- fontSize: fontVar6("md"),
3524
+ border: `1px solid ${css7("border")}`,
3525
+ background: css7("card"),
3526
+ color: css7("foreground"),
3527
+ fontSize: fontVar7("md"),
3185
3528
  cursor: "pointer"
3186
3529
  };
3187
3530
  return /* @__PURE__ */ jsxs9(
@@ -3199,13 +3542,13 @@ function BoundDevicesNotice({ title, lines, dismissLabel, onDismiss, retryLabel,
3199
3542
  margin: "24px auto",
3200
3543
  padding: 16,
3201
3544
  borderRadius: 8,
3202
- border: `1px solid ${css6("border")}`,
3203
- background: css6("card"),
3204
- color: css6("foreground")
3545
+ border: `1px solid ${css7("border")}`,
3546
+ background: css7("card"),
3547
+ color: css7("foreground")
3205
3548
  },
3206
3549
  children: [
3207
- /* @__PURE__ */ jsx13("div", { style: { fontSize: fontVar6("lg"), fontWeight: 600 }, children: title }),
3208
- /* @__PURE__ */ jsx13("ul", { style: { margin: 0, paddingLeft: 20, display: "flex", flexDirection: "column", gap: 4, fontSize: fontVar6("md") }, children: lines.map((line, i) => /* @__PURE__ */ jsx13("li", { style: { overflowWrap: "anywhere" }, children: line }, i)) }),
3550
+ /* @__PURE__ */ jsx13("div", { style: { fontSize: fontVar7("lg"), fontWeight: 600 }, children: title }),
3551
+ /* @__PURE__ */ jsx13("ul", { style: { margin: 0, paddingLeft: 20, display: "flex", flexDirection: "column", gap: 4, fontSize: fontVar7("md") }, children: lines.map((line, i) => /* @__PURE__ */ jsx13("li", { style: { overflowWrap: "anywhere" }, children: line }, i)) }),
3209
3552
  /* @__PURE__ */ jsxs9("div", { style: { display: "flex", flexWrap: "wrap", gap: 8 }, children: [
3210
3553
  retryLabel && onRetry ? /* @__PURE__ */ jsx13("button", { type: "button", style: button2, onClick: onRetry, "data-testid": "bound-devices-retry", children: retryLabel }) : null,
3211
3554
  /* @__PURE__ */ jsx13("button", { type: "button", style: button2, onClick: onDismiss, "data-testid": "bound-devices-dismiss", children: dismissLabel })
@@ -3297,6 +3640,7 @@ var OPTION_DOCS = {
3297
3640
  ageLagSeconds: "Show a reading's age under it only when the reading is at least this many seconds older than the newest reading in the table (or stale). Unset shows every reading's age.",
3298
3641
  sortBy: "The measurement the table is ordered by. Blank orders by device name.",
3299
3642
  sortDescending: "Largest first.",
3643
+ oldestFirst: "List the oldest bucket first. Unset lists the newest first.",
3300
3644
  pageSize: "Devices per page. Default 20.",
3301
3645
  maxSeries: "Most lines drawn when the chart shows one line per device. Default 5, at most 12. Each line costs one stored-history read, and the chart colours repeat after five; a group larger than this shows the first devices of the group and says how many it left out.",
3302
3646
  show: "What to count: devices in an abnormal band of the measurement (default), or all devices."
@@ -3346,6 +3690,12 @@ var OPTION_DOC_OVERRIDES = {
3346
3690
  "online-doughnut": {
3347
3691
  staleAfterSeconds: "Marks the ring stale once the status of its devices has not been read for longer than this many seconds (it is re-read every 15 seconds, so only reads that keep failing make it stale). Unset, the age is shown and nothing is marked stale."
3348
3692
  },
3693
+ "history-table": {
3694
+ columns: "Which measurements are columns, in order, with a heading, unit and precision each. With none, the columns are the datasource\u2019s measurements (or, when it names none, every measurement the device reported in the range). Every figure is the average of the readings in a time bucket.",
3695
+ precision: "Decimal places to round displayed averages to, for a column that sets none. Unset rounds to at most 3 decimals (4 significant digits under 1). The CSV export is rounded the same way.",
3696
+ pageSize: "Rows per page. Default 10. The CSV export holds every row, not just the page.",
3697
+ staleAfterSeconds: "Marks the table stale once its history has not been read for longer than this many seconds (it is re-read at the end of each time bucket, so only reads that keep failing make it stale). Unset, the age is shown and nothing is marked stale."
3698
+ },
3349
3699
  "fleet-count": {
3350
3700
  measurement: "The measurement whose bands flag a device. Defaults to the first the datasource selects."
3351
3701
  },
@@ -3429,7 +3779,7 @@ var WIDGET_BINDS_DATASOURCE = Object.freeze(
3429
3779
  );
3430
3780
 
3431
3781
  // src/index.ts
3432
- import { pickSample as pickSample3, optBoolean as optBoolean3 } from "@devicechain/widget-sdk";
3782
+ import { pickSample as pickSample4, optBoolean as optBoolean3 } from "@devicechain/widget-sdk";
3433
3783
 
3434
3784
  // src/options.ts
3435
3785
  import {
@@ -3529,7 +3879,7 @@ import {
3529
3879
  useResolvedBindings,
3530
3880
  useSlotCandidates,
3531
3881
  useCandidates,
3532
- css as css7
3882
+ css as css8
3533
3883
  } from "@devicechain/widget-sdk";
3534
3884
  import {
3535
3885
  EChart as EChart3,
@@ -3539,6 +3889,7 @@ import {
3539
3889
  useChartTheme as useChartTheme3
3540
3890
  } from "@devicechain/widget-sdk/echarts";
3541
3891
  export {
3892
+ AbsoluteIndicator,
3542
3893
  AlarmCount,
3543
3894
  AlarmSeverity,
3544
3895
  AlarmTable,
@@ -3546,6 +3897,7 @@ export {
3546
3897
  BoundDevicesNotice,
3547
3898
  CommandButton,
3548
3899
  ConnectedWidget,
3900
+ DEFAULT_ABSOLUTE_LABELS,
3549
3901
  DEFAULT_TIME_RANGE_LABELS,
3550
3902
  DashboardRenderer,
3551
3903
  EChart3 as EChart,
@@ -3572,6 +3924,7 @@ export {
3572
3924
  WidgetSubjectProvider2 as WidgetSubjectProvider,
3573
3925
  WidgetThemeRootProvider2 as WidgetThemeRootProvider,
3574
3926
  WidgetTimeZoneProvider2 as WidgetTimeZoneProvider,
3927
+ absoluteLinkFor,
3575
3928
  buildGaugeOption2 as buildGaugeOption,
3576
3929
  buildLineOption2 as buildLineOption,
3577
3930
  buildPayload,
@@ -3583,7 +3936,7 @@ export {
3583
3936
  commandStatusColor,
3584
3937
  commandStatusLabel,
3585
3938
  createBuiltinWidgetRegistry2 as createBuiltinWidgetRegistry,
3586
- css7 as css,
3939
+ css8 as css,
3587
3940
  defaultValues,
3588
3941
  describeBoundDevicesProblems,
3589
3942
  describePosition,
@@ -3591,6 +3944,7 @@ export {
3591
3944
  fitValueFont2 as fitValueFont,
3592
3945
  flashBackgroundStyle2 as flashBackgroundStyle,
3593
3946
  flashTextStyle2 as flashTextStyle,
3947
+ formatAbsoluteRange,
3594
3948
  formatDegrees,
3595
3949
  highestSeverity,
3596
3950
  isScalar,
@@ -3600,11 +3954,12 @@ export {
3600
3954
  numberOptionSpec,
3601
3955
  optBoolean3 as optBoolean,
3602
3956
  parseParameterSchema,
3603
- pickSample3 as pickSample,
3957
+ pickSample4 as pickSample,
3604
3958
  placeable,
3605
3959
  preloadWidgets,
3606
3960
  projectToPanel,
3607
3961
  rasterStyleFor,
3962
+ rejectedLinkNote,
3608
3963
  resolveChartTheme,
3609
3964
  resolveWidgets,
3610
3965
  severityColor,