@devicechain/widgets 0.21.1-rc.1 → 0.22.0-rc.1

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 (78) hide show
  1. package/README.md +28 -7
  2. package/dist/aggregate-feed.d.ts +14 -0
  3. package/dist/aggregate-fold.d.ts +11 -0
  4. package/dist/aggregate-reading.d.ts +19 -0
  5. package/dist/aggregate-strings.d.ts +16 -0
  6. package/dist/binding-policy.d.ts +3 -1
  7. package/dist/builtin-descriptors.d.ts +1210 -8
  8. package/dist/builtin-registry.d.ts +1197 -8
  9. package/dist/category-geometry.d.ts +19 -0
  10. package/dist/category-items.d.ts +10 -0
  11. package/dist/chunks/aggregate-feed-6ZYKOQOB.js +130 -0
  12. package/dist/chunks/aggregate-feed-6ZYKOQOB.js.map +7 -0
  13. package/dist/chunks/aggregate-widgets-WVXAW3UL.js +895 -0
  14. package/dist/chunks/aggregate-widgets-WVXAW3UL.js.map +7 -0
  15. package/dist/chunks/chunk-2D7JNKFE.js +65 -0
  16. package/dist/chunks/chunk-2D7JNKFE.js.map +7 -0
  17. package/dist/chunks/chunk-S2OMBJLI.js +200 -0
  18. package/dist/chunks/chunk-S2OMBJLI.js.map +7 -0
  19. package/dist/chunks/chunk-XQQQNTBX.js +76 -0
  20. package/dist/chunks/chunk-XQQQNTBX.js.map +7 -0
  21. package/dist/chunks/chunk-YPFB6DAL.js +157 -0
  22. package/dist/chunks/chunk-YPFB6DAL.js.map +7 -0
  23. package/dist/chunks/chunk-ZR74ORIT.js +35 -0
  24. package/dist/chunks/chunk-ZR74ORIT.js.map +7 -0
  25. package/dist/chunks/control-widgets-UDS4ET3T.js +775 -0
  26. package/dist/chunks/control-widgets-UDS4ET3T.js.map +7 -0
  27. package/dist/chunks/fleet-feed-FK4ZMKIK.js +252 -0
  28. package/dist/chunks/fleet-feed-FK4ZMKIK.js.map +7 -0
  29. package/dist/chunks/level-widgets-MJB7TOLU.js +833 -0
  30. package/dist/chunks/level-widgets-MJB7TOLU.js.map +7 -0
  31. package/dist/command-control-core.d.ts +68 -0
  32. package/dist/command-control-strings.d.ts +32 -0
  33. package/dist/command-control.d.ts +31 -0
  34. package/dist/dashboard-renderer.d.ts +15 -4
  35. package/dist/fleet-feed.d.ts +21 -0
  36. package/dist/fleet-model.d.ts +43 -0
  37. package/dist/fleet-seed.d.ts +7 -0
  38. package/dist/fleet-strings.d.ts +25 -0
  39. package/dist/heading.d.ts +3 -0
  40. package/dist/index.d.ts +3 -2
  41. package/dist/index.js +1774 -676
  42. package/dist/index.js.map +4 -4
  43. package/dist/level-reading.d.ts +29 -0
  44. package/dist/level-readout.d.ts +25 -0
  45. package/dist/level-strings.d.ts +13 -0
  46. package/dist/options.d.ts +1 -1
  47. package/dist/time-range-bar.d.ts +28 -0
  48. package/dist/use-held.d.ts +1 -0
  49. package/dist/value-scale.d.ts +30 -0
  50. package/dist/widgets/aggregate-lazy.d.ts +305 -0
  51. package/dist/widgets/aggregate-widgets.d.ts +4 -0
  52. package/dist/widgets/alarm-count.d.ts +1 -0
  53. package/dist/widgets/alarm-table.d.ts +4 -1
  54. package/dist/widgets/bar-chart.d.ts +5 -0
  55. package/dist/widgets/battery.d.ts +7 -0
  56. package/dist/widgets/command-slider.d.ts +2 -0
  57. package/dist/widgets/command-toggle.d.ts +4 -0
  58. package/dist/widgets/compass.d.ts +2 -0
  59. package/dist/widgets/control-lazy.d.ts +92 -0
  60. package/dist/widgets/control-widgets.d.ts +2 -0
  61. package/dist/widgets/fleet-table.d.ts +13 -0
  62. package/dist/widgets/gauge.d.ts +52 -0
  63. package/dist/widgets/label.d.ts +1 -1
  64. package/dist/widgets/latest-card.d.ts +44 -1
  65. package/dist/widgets/lazy-chunk.d.ts +8 -0
  66. package/dist/widgets/level-gauge.d.ts +2 -0
  67. package/dist/widgets/level-lazy.d.ts +647 -0
  68. package/dist/widgets/level-widgets.d.ts +7 -0
  69. package/dist/widgets/linear-gauge.d.ts +2 -0
  70. package/dist/widgets/pie-chart.d.ts +2 -0
  71. package/dist/widgets/progress-ring.d.ts +3 -0
  72. package/dist/widgets/severity.d.ts +2 -1
  73. package/dist/widgets/signal-bars.d.ts +3 -0
  74. package/dist/widgets/stat-card.d.ts +3 -0
  75. package/dist/widgets/status-light.d.ts +7 -0
  76. package/dist/widgets/table.d.ts +9 -1
  77. package/dist/widgets/time-series-chart.d.ts +62 -4
  78. package/package.json +8 -8
package/dist/index.js CHANGED
@@ -1,76 +1,91 @@
1
+ import {
2
+ buildPayload,
3
+ commandStatusColor,
4
+ commandStatusLabel,
5
+ defaultValues,
6
+ isScalar,
7
+ isTerminalStatus,
8
+ parseBool,
9
+ parseParameterSchema,
10
+ validateParams
11
+ } from "./chunks/chunk-YPFB6DAL.js";
12
+ import {
13
+ readRange
14
+ } from "./chunks/chunk-ZR74ORIT.js";
15
+ import {
16
+ LevelReadout
17
+ } from "./chunks/chunk-XQQQNTBX.js";
18
+ import {
19
+ alarmSubscription,
20
+ commandSubscription,
21
+ measurementSpan,
22
+ measurementWindow,
23
+ membersMode
24
+ } from "./chunks/chunk-2D7JNKFE.js";
25
+
1
26
  // src/dashboard-renderer.tsx
2
27
  import {
3
- activeBreakpoint,
28
+ anchorFraction,
29
+ DEFAULT_CANVAS_DENSITY,
4
30
  defaultHistoryWindow,
31
+ effectiveLayout,
32
+ historyWindowFor,
33
+ initialTimeRange,
5
34
  fetchWidgetHistory,
6
- resolveWidgetBox,
35
+ layoutShape,
36
+ resolveFrame,
37
+ resolvePlacement,
7
38
  resolveWidgetDatasource,
39
+ spanLimitsOfLayout,
8
40
  UNKNOWN_WIDGET_TYPE as UNKNOWN_WIDGET_TYPE2
9
41
  } from "@devicechain/dashboards";
10
- import { useEffect as useEffect4, useMemo as useMemo5, useRef as useRef3, useState as useState6 } from "react";
42
+ import {
43
+ densityScale,
44
+ tokenStyle,
45
+ WidgetDensityProvider,
46
+ WidgetFrameStyleProvider,
47
+ WidgetTimeRangeProvider,
48
+ WidgetTokenStyles
49
+ } from "@devicechain/widget-sdk";
50
+ import { Fragment as Fragment5, useCallback as useCallback2, useEffect as useEffect5, useLayoutEffect, useMemo as useMemo6, useRef as useRef3, useState as useState8 } from "react";
11
51
 
12
52
  // src/connected-widget.tsx
13
53
  import {
14
54
  UNKNOWN_WIDGET_TYPE
15
55
  } from "@devicechain/dashboards";
16
- import { WidgetCandidatesProvider, WidgetSelectProvider } from "@devicechain/widget-sdk";
17
- import { useMemo as useMemo4 } from "react";
56
+ import {
57
+ newestSampleTime,
58
+ resolveStaleAfterSeconds,
59
+ WidgetCandidatesProvider,
60
+ WidgetFreshnessProvider,
61
+ WidgetSelectProvider,
62
+ useWidgetTimeRange as useWidgetTimeRange2
63
+ } from "@devicechain/widget-sdk";
64
+ import { useMemo as useMemo5 } from "react";
18
65
 
19
- // src/binding-policy.ts
20
- import { optNumber, optString } from "@devicechain/widget-sdk";
21
- function numberValue(binding, options) {
22
- if (!binding) return void 0;
23
- let value;
24
- if (typeof binding.const === "number") value = binding.const;
25
- else if (binding.option !== void 0) value = optNumber(options, binding.option) ?? binding.default;
26
- if (value === void 0) return void 0;
27
- return binding.min !== void 0 ? Math.max(binding.min, value) : value;
28
- }
29
- function stringValue(binding, options) {
30
- if (!binding || binding.option === void 0) return void 0;
31
- return optString(options, binding.option) ?? binding.default;
32
- }
33
- function policy(descriptor) {
34
- return descriptor.binding;
35
- }
36
- function measurementWindow(descriptor, widget) {
37
- return numberValue(policy(descriptor).window, widget.options);
38
- }
39
- function parseAcknowledged(value) {
40
- if (value === "true") return true;
41
- if (value === "false") return false;
42
- return void 0;
43
- }
44
- function alarmSubscription(descriptor, widget) {
45
- const p = policy(descriptor);
46
- return {
47
- datasource: widget.datasource,
48
- state: stringValue(p.state, widget.options),
49
- severity: stringValue(p.severity, widget.options),
50
- acknowledged: parseAcknowledged(stringValue(p.acknowledged, widget.options)),
51
- pageSize: numberValue(p.pageSize, widget.options) ?? 1
52
- };
53
- }
54
- function commandSubscription(descriptor, widget) {
55
- return {
56
- datasource: widget.datasource,
57
- pageSize: numberValue(policy(descriptor).pageSize, widget.options) ?? 1
58
- };
66
+ // src/use-held.ts
67
+ import { useState } from "react";
68
+ function useHeld(value, hold, ready) {
69
+ const [held, setHeld] = useState(void 0);
70
+ if (!hold) {
71
+ if (held !== void 0) setHeld(void 0);
72
+ return value;
73
+ }
74
+ if (held === void 0 || ready(held.value) !== ready(value)) {
75
+ setHeld({ value });
76
+ return value;
77
+ }
78
+ return held.value;
59
79
  }
60
80
 
61
81
  // src/builtin-registry.ts
62
82
  import { createWidgetRegistry } from "@devicechain/widget-sdk";
63
83
 
64
- // src/widgets/alarm-count.tsx
65
- import {
66
- css,
67
- defineWidget,
68
- optString as optString2,
69
- useElementSize,
70
- WidgetFrame
71
- } from "@devicechain/widget-sdk";
84
+ // src/widgets/aggregate-lazy.tsx
85
+ import { defineWidget } from "@devicechain/widget-sdk";
72
86
 
73
87
  // src/builtin-descriptors.ts
88
+ import { SEVERITY_LEVELS } from "@devicechain/widget-sdk";
74
89
  import {
75
90
  createWidgetCatalog,
76
91
  defineWidgetDescriptor
@@ -92,7 +107,42 @@ var PRECISION = {
92
107
  max: 100,
93
108
  labelId: "widgetLabelPrecision",
94
109
  helpId: "widgetHintPrecision",
95
- doc: "Decimal places to round displayed values to. Unset shows the raw value."
110
+ doc: "Decimal places to round displayed values to. Unset rounds to at most 3 decimals (4 significant digits under 1)."
111
+ };
112
+ var THRESHOLDS = {
113
+ kind: "list",
114
+ maxItems: 12,
115
+ labelId: "widgetLabelThresholds",
116
+ helpId: "widgetHintThresholds",
117
+ doc: "Severity bands over the value. Each entry starts a band at its value that runs to the next entry; below the first, and in a normal band, the widget keeps its own colour.",
118
+ item: {
119
+ value: {
120
+ kind: "number",
121
+ required: true,
122
+ labelId: "widgetLabelThresholdValue",
123
+ doc: "The value at which this band starts."
124
+ },
125
+ level: {
126
+ kind: "enum",
127
+ required: true,
128
+ values: SEVERITY_LEVELS,
129
+ labelId: "widgetLabelThresholdLevel",
130
+ valueLabelIds: {
131
+ normal: "widgetSeverityNormal",
132
+ warning: "widgetSeverityWarning",
133
+ minor: "widgetSeverityMinor",
134
+ major: "widgetSeverityMajor",
135
+ critical: "widgetSeverityCritical"
136
+ },
137
+ placeholderId: "widgetPlaceholderThresholdLevel",
138
+ doc: "The severity of this band. The theme decides the colour; normal has none."
139
+ },
140
+ label: {
141
+ kind: "string",
142
+ labelId: "widgetLabelThresholdLabel",
143
+ doc: "Text shown on the chart line for this threshold. Defaults to its value."
144
+ }
145
+ }
96
146
  };
97
147
  var FLASH_ON_CHANGE = {
98
148
  kind: "boolean",
@@ -108,11 +158,68 @@ var WINDOW = {
108
158
  helpId: "widgetHintWindow",
109
159
  doc: "Samples retained across ALL of the widget's measurements before the oldest are dropped. Bounds memory; only the chart draws them. Default 300."
110
160
  };
161
+ var STALE_AFTER_SECONDS = {
162
+ kind: "number",
163
+ integer: true,
164
+ min: 1,
165
+ labelId: "widgetLabelStaleAfter",
166
+ helpId: "widgetHintStaleAfter",
167
+ doc: "Seconds after which the newest sample is called stale: the value is greyed out and badged. Unset shows the sample age only and never marks the widget stale."
168
+ };
111
169
  var UNIT = {
112
170
  kind: "string",
113
171
  labelId: "widgetLabelUnit",
114
172
  doc: "Unit suffix shown beside the value."
115
173
  };
174
+ var SCALE_MIN = {
175
+ kind: "number",
176
+ required: true,
177
+ labelId: "widgetLabelMin",
178
+ helpId: "widgetHintScaleMin",
179
+ doc: "Value at the empty end of the scale. Required: the widget does not guess a range."
180
+ };
181
+ var SCALE_MAX = {
182
+ kind: "number",
183
+ required: true,
184
+ labelId: "widgetLabelMax",
185
+ helpId: "widgetHintScaleMax",
186
+ doc: "Value at the full end of the scale. Required, and above min."
187
+ };
188
+ var CONTROL_COMMAND_NAME = {
189
+ kind: "string",
190
+ required: true,
191
+ labelId: "widgetCommandHeading",
192
+ helpId: "widgetCommandFieldHint",
193
+ control: "command-vocabulary",
194
+ doc: "Command key issued to the target device. Without it the control cannot send."
195
+ };
196
+ var CONTROL_COMMAND_LABEL = {
197
+ kind: "string",
198
+ labelId: "widgetCommandHeading",
199
+ helpId: "widgetCommandFieldHint",
200
+ control: "command-vocabulary",
201
+ doc: "Name of the control for assistive technology. Defaults to the command key."
202
+ };
203
+ var CONTROL_PARAMETER_SCHEMA = {
204
+ kind: "string",
205
+ format: "json",
206
+ labelId: "widgetCommandHeading",
207
+ helpId: "widgetCommandFieldHint",
208
+ control: "command-vocabulary",
209
+ doc: "The command's parameter descriptors, baked in as JSON at author time. Gives the control its range and its refusals."
210
+ };
211
+ var CONTROL_PARAMETER = {
212
+ kind: "string",
213
+ labelId: "widgetLabelParameter",
214
+ helpId: "widgetHintControlParameter",
215
+ doc: "Which parameter of the command this control sets. May be left blank when the command has exactly one parameter of the right type."
216
+ };
217
+ var MEASUREMENT = {
218
+ kind: "string",
219
+ labelId: "widgetLabelMeasurement",
220
+ helpId: "widgetHintMeasurement",
221
+ doc: "Which measurement to display. Defaults to the first the datasource selects."
222
+ };
116
223
  var ALARM_STATE = {
117
224
  kind: "enum",
118
225
  values: ["ACTIVE", "CLEARED"],
@@ -149,28 +256,78 @@ var SELECTION_TARGET = {
149
256
  };
150
257
  var VERSION = "1.0.0";
151
258
  var ALL_SELECTORS = ["device", "anchor", "slot"];
259
+ var ONE_DEVICE = ["device", "slot"];
152
260
  var NO_DATASOURCE = { mode: "none" };
153
261
  var ALARM_FILTERS = {
154
262
  state: { option: "state" },
155
263
  severity: { option: "severity" },
156
264
  acknowledged: { option: "acknowledged" }
157
265
  };
266
+ var WINDOW_SECONDS = {
267
+ kind: "number",
268
+ integer: true,
269
+ min: 60,
270
+ max: 604800,
271
+ labelId: "widgetLabelWindowSeconds",
272
+ helpId: "widgetHintWindowSeconds",
273
+ doc: "Seconds of history the widget covers, back from now. Leave unset to follow the board\u2019s time range; set it to pin this widget to a fixed span that the viewer\u2019s range picker does not change. 60 to 604800."
274
+ };
275
+ var CATEGORY_SORT = {
276
+ kind: "enum",
277
+ values: ["none", "ascending", "descending"],
278
+ labelId: "widgetLabelSort",
279
+ valueLabelIds: { none: "widgetSortNone", ascending: "widgetSortAscending", descending: "widgetSortDescending" },
280
+ placeholderId: "widgetPlaceholderDefault",
281
+ doc: "Order of the categories. Default: as the data source lists them."
282
+ };
283
+ var FLEET_MEASUREMENT = {
284
+ kind: "string",
285
+ required: true,
286
+ labelId: "widgetLabelMeasurement",
287
+ doc: "The measurement column this entry applies to."
288
+ };
289
+ var FLEET_COLUMNS = {
290
+ kind: "list",
291
+ maxItems: 24,
292
+ labelId: "widgetLabelColumns",
293
+ helpId: "widgetHintColumns",
294
+ doc: "Heading, unit and precision per measurement column; a column with none shows its name.",
295
+ item: {
296
+ measurement: FLEET_MEASUREMENT,
297
+ label: { kind: "string", labelId: "widgetLabelColumnHeading", doc: "Column heading. Defaults to the measurement name." },
298
+ unit: { ...UNIT },
299
+ precision: { ...PRECISION }
300
+ }
301
+ };
302
+ var FLEET_THRESHOLDS = {
303
+ ...THRESHOLDS,
304
+ item: {
305
+ measurement: FLEET_MEASUREMENT,
306
+ ...THRESHOLDS.item
307
+ }
308
+ };
158
309
  var builtinWidgetDescriptors = {
159
310
  "timeseries-chart": defineWidgetDescriptor({
160
311
  id: "timeseries-chart",
161
312
  version: VERSION,
162
313
  sdkApiVersion: 1,
163
- optionsVersion: 1,
314
+ optionsVersion: 2,
164
315
  display: { title: "Timeseries chart", category: "Telemetry", titleId: "widgetTypeTimeseriesChart", categoryId: "widgetCategoryTelemetry" },
165
316
  channel: "measurement",
166
- datasource: { mode: "required", selectors: ALL_SELECTORS },
167
- binding: { window: { option: "window", min: 1 } },
317
+ datasource: { mode: "required", selectors: ONE_DEVICE },
318
+ binding: { windowSeconds: { option: "windowSeconds" } },
168
319
  options: {
169
320
  title: TITLE,
170
- window: WINDOW
321
+ unit: UNIT,
322
+ precision: PRECISION,
323
+ thresholds: THRESHOLDS,
324
+ staleAfterSeconds: STALE_AFTER_SECONDS,
325
+ windowSeconds: WINDOW_SECONDS
171
326
  },
172
327
  defaults: { title: "Timeseries chart" },
173
- interactions: {}
328
+ interactions: {},
329
+ freshness: true,
330
+ layout: { defaultSpan: { cols: 6, rows: 4 }, minSpan: { cols: 4, rows: 3 } }
174
331
  }),
175
332
  "latest-card": defineWidgetDescriptor({
176
333
  id: "latest-card",
@@ -179,7 +336,7 @@ var builtinWidgetDescriptors = {
179
336
  optionsVersion: 1,
180
337
  display: { title: "Latest card", category: "Telemetry", titleId: "widgetTypeLatestCard", categoryId: "widgetCategoryTelemetry" },
181
338
  channel: "measurement",
182
- datasource: { mode: "required", selectors: ALL_SELECTORS },
339
+ datasource: { mode: "required", selectors: ONE_DEVICE },
183
340
  binding: { window: { option: "window", min: 1 } },
184
341
  options: {
185
342
  title: TITLE_OR_MEASUREMENT,
@@ -191,11 +348,15 @@ var builtinWidgetDescriptors = {
191
348
  },
192
349
  unit: UNIT,
193
350
  precision: PRECISION,
351
+ thresholds: THRESHOLDS,
194
352
  flashOnChange: FLASH_ON_CHANGE,
353
+ staleAfterSeconds: STALE_AFTER_SECONDS,
195
354
  window: WINDOW
196
355
  },
197
356
  defaults: { title: "Latest card" },
198
- interactions: {}
357
+ interactions: {},
358
+ freshness: true,
359
+ layout: { defaultSpan: { cols: 3, rows: 2 }, minSpan: { cols: 3, rows: 2 }, maxSpan: { cols: 6, rows: 4 } }
199
360
  }),
200
361
  gauge: defineWidgetDescriptor({
201
362
  id: "gauge",
@@ -204,7 +365,7 @@ var builtinWidgetDescriptors = {
204
365
  optionsVersion: 1,
205
366
  display: { title: "Gauge", category: "Telemetry", titleId: "widgetTypeGauge", categoryId: "widgetCategoryTelemetry" },
206
367
  channel: "measurement",
207
- datasource: { mode: "required", selectors: ALL_SELECTORS },
368
+ datasource: { mode: "required", selectors: ONE_DEVICE },
208
369
  binding: { window: { option: "window", min: 1 } },
209
370
  options: {
210
371
  title: TITLE_OR_MEASUREMENT,
@@ -225,12 +386,17 @@ var builtinWidgetDescriptors = {
225
386
  doc: "High end of the gauge scale. Default 100."
226
387
  },
227
388
  unit: UNIT,
389
+ precision: PRECISION,
390
+ thresholds: THRESHOLDS,
228
391
  flashOnChange: FLASH_ON_CHANGE,
392
+ staleAfterSeconds: STALE_AFTER_SECONDS,
229
393
  window: WINDOW
230
394
  },
231
395
  constraints: [{ kind: "greater-than", key: "max", than: "min" }],
232
396
  defaults: { title: "Gauge" },
233
- interactions: {}
397
+ interactions: {},
398
+ freshness: true,
399
+ layout: { defaultSpan: { cols: 3, rows: 3 }, minSpan: { cols: 3, rows: 3 }, maxSpan: { cols: 6, rows: 6 } }
234
400
  }),
235
401
  table: defineWidgetDescriptor({
236
402
  id: "table",
@@ -239,16 +405,19 @@ var builtinWidgetDescriptors = {
239
405
  optionsVersion: 1,
240
406
  display: { title: "Table", category: "Telemetry", titleId: "widgetTypeTable", categoryId: "widgetCategoryTelemetry" },
241
407
  channel: "measurement",
242
- datasource: { mode: "required", selectors: ALL_SELECTORS },
408
+ datasource: { mode: "required", selectors: ONE_DEVICE },
243
409
  binding: { window: { option: "window", min: 1 } },
244
410
  options: {
245
411
  title: TITLE,
246
412
  precision: PRECISION,
247
413
  flashOnChange: FLASH_ON_CHANGE,
414
+ staleAfterSeconds: STALE_AFTER_SECONDS,
248
415
  window: WINDOW
249
416
  },
250
417
  defaults: { title: "Table" },
251
- interactions: {}
418
+ interactions: {},
419
+ freshness: true,
420
+ layout: { defaultSpan: { cols: 6, rows: 3 }, minSpan: { cols: 4, rows: 2 } }
252
421
  }),
253
422
  // label and image are static: the `none` channel, no datasource, no subscription and no
254
423
  // history seed.
@@ -282,11 +451,12 @@ var builtinWidgetDescriptors = {
282
451
  min: 1,
283
452
  labelId: "widgetLabelFontSize",
284
453
  helpId: "widgetHintFontSize",
285
- doc: "Text size in pixels. Default 16."
454
+ doc: "Text size in pixels at the comfortable density; it scales with the board density. Default 16."
286
455
  }
287
456
  },
288
457
  defaults: { text: "New label" },
289
- interactions: {}
458
+ interactions: {},
459
+ layout: { defaultSpan: { cols: 3, rows: 1 }, minSpan: { cols: 1, rows: 1 }, maxSpan: { cols: 24, rows: 3 } }
290
460
  }),
291
461
  image: defineWidgetDescriptor({
292
462
  id: "image",
@@ -320,7 +490,8 @@ var builtinWidgetDescriptors = {
320
490
  }
321
491
  },
322
492
  defaults: { title: "Image" },
323
- interactions: {}
493
+ interactions: {},
494
+ layout: { defaultSpan: { cols: 3, rows: 2 }, minSpan: { cols: 1, rows: 1 } }
324
495
  }),
325
496
  "alarm-table": defineWidgetDescriptor({
326
497
  id: "alarm-table",
@@ -349,7 +520,8 @@ var builtinWidgetDescriptors = {
349
520
  selectionTarget: SELECTION_TARGET
350
521
  },
351
522
  defaults: { title: "Alarm table", state: "ACTIVE" },
352
- interactions: { alarmActions: true, selection: true }
523
+ interactions: { alarmActions: true, selection: true },
524
+ layout: { defaultSpan: { cols: 6, rows: 4 }, minSpan: { cols: 4, rows: 2 } }
353
525
  }),
354
526
  // The count shows the SERVER total and reads a page only to sample the worst loaded
355
527
  // severity, so its page size is a constant rather than an authorable option.
@@ -372,7 +544,8 @@ var builtinWidgetDescriptors = {
372
544
  acknowledged: ALARM_ACKNOWLEDGED
373
545
  },
374
546
  defaults: { title: "Alarm count", state: "ACTIVE" },
375
- interactions: {}
547
+ interactions: {},
548
+ layout: { defaultSpan: { cols: 3, rows: 2 }, minSpan: { cols: 2, rows: 2 }, maxSpan: { cols: 6, rows: 4 } }
376
549
  }),
377
550
  "command-button": defineWidgetDescriptor({
378
551
  id: "command-button",
@@ -381,7 +554,7 @@ var builtinWidgetDescriptors = {
381
554
  optionsVersion: 1,
382
555
  display: { title: "Command button", category: "Control", titleId: "widgetTypeCommandButton", categoryId: "widgetCategoryControl" },
383
556
  channel: "control",
384
- datasource: { mode: "required", selectors: ["device", "slot"] },
557
+ datasource: { mode: "required", selectors: ONE_DEVICE },
385
558
  binding: { pageSize: { option: "maxRows", default: 20, min: 1 } },
386
559
  options: {
387
560
  title: TITLE,
@@ -418,7 +591,8 @@ var builtinWidgetDescriptors = {
418
591
  }
419
592
  },
420
593
  defaults: { title: "Command button" },
421
- interactions: { commandSend: true }
594
+ interactions: { commandSend: true },
595
+ layout: { defaultSpan: { cols: 3, rows: 3 }, minSpan: { cols: 3, rows: 2 }, maxSpan: { cols: 6, rows: 6 } }
422
596
  }),
423
597
  "entity-selector": defineWidgetDescriptor({
424
598
  id: "entity-selector",
@@ -434,7 +608,9 @@ var builtinWidgetDescriptors = {
434
608
  selectionTarget: { ...SELECTION_TARGET, required: true }
435
609
  },
436
610
  defaults: { title: "Entity selector" },
437
- interactions: { selection: true, candidates: true }
611
+ interactions: { selection: true, candidates: true },
612
+ // A select control is one line high, so its rows are pinned; it widens to fit long names.
613
+ layout: { defaultSpan: { cols: 3, rows: 1 }, minSpan: { cols: 3, rows: 1 }, maxSpan: { cols: 12, rows: 1 }, resize: "horizontal" }
438
614
  }),
439
615
  map: defineWidgetDescriptor({
440
616
  id: "map",
@@ -470,23 +646,516 @@ var builtinWidgetDescriptors = {
470
646
  }
471
647
  },
472
648
  defaults: { title: "Map" },
473
- interactions: {}
649
+ interactions: {},
650
+ layout: { defaultSpan: { cols: 6, rows: 4 }, minSpan: { cols: 4, rows: 3 } }
651
+ }),
652
+ // ---- Levels and indicators ------------------------------------------------
653
+ // DOM/SVG single-value widgets on the measurement channel. A threshold band colours the fill
654
+ // or lamp; `normal` keeps the widget's neutral accent.
655
+ "linear-gauge": defineWidgetDescriptor({
656
+ id: "linear-gauge",
657
+ version: VERSION,
658
+ sdkApiVersion: 1,
659
+ optionsVersion: 1,
660
+ display: { title: "Linear gauge", category: "Telemetry", titleId: "widgetTypeLinearGauge", categoryId: "widgetCategoryTelemetry" },
661
+ channel: "measurement",
662
+ datasource: { mode: "required", selectors: ONE_DEVICE },
663
+ binding: { window: { option: "window", min: 1 } },
664
+ options: {
665
+ title: TITLE_OR_MEASUREMENT,
666
+ measurement: MEASUREMENT,
667
+ min: SCALE_MIN,
668
+ max: SCALE_MAX,
669
+ style: {
670
+ kind: "enum",
671
+ values: ["bar", "thermometer"],
672
+ labelId: "widgetLabelLinearStyle",
673
+ valueLabelIds: { bar: "widgetLinearStyleBar", thermometer: "widgetLinearStyleThermometer" },
674
+ placeholderId: "widgetPlaceholderDefault",
675
+ doc: "A plain bar (default) or a thermometer: a vertical tube with a bulb. A thermometer is always vertical."
676
+ },
677
+ orientation: {
678
+ kind: "enum",
679
+ values: ["horizontal", "vertical"],
680
+ labelId: "widgetLabelOrientation",
681
+ valueLabelIds: { horizontal: "widgetOrientationHorizontal", vertical: "widgetOrientationVertical" },
682
+ placeholderId: "widgetPlaceholderDefault",
683
+ doc: "Direction of a bar: horizontal (default) or vertical. Ignored by a thermometer."
684
+ },
685
+ unit: UNIT,
686
+ precision: PRECISION,
687
+ thresholds: THRESHOLDS,
688
+ staleAfterSeconds: STALE_AFTER_SECONDS,
689
+ window: WINDOW
690
+ },
691
+ constraints: [{ kind: "greater-than", key: "max", than: "min" }],
692
+ defaults: { title: "Linear gauge" },
693
+ interactions: {},
694
+ freshness: true,
695
+ layout: { defaultSpan: { cols: 4, rows: 3 }, minSpan: { cols: 3, rows: 3 }, maxSpan: { cols: 8, rows: 8 } }
696
+ }),
697
+ "level-gauge": defineWidgetDescriptor({
698
+ id: "level-gauge",
699
+ version: VERSION,
700
+ sdkApiVersion: 1,
701
+ optionsVersion: 1,
702
+ display: { title: "Level gauge", category: "Telemetry", titleId: "widgetTypeLevelGauge", categoryId: "widgetCategoryTelemetry" },
703
+ channel: "measurement",
704
+ datasource: { mode: "required", selectors: ONE_DEVICE },
705
+ binding: { window: { option: "window", min: 1 } },
706
+ options: {
707
+ title: TITLE_OR_MEASUREMENT,
708
+ measurement: MEASUREMENT,
709
+ min: SCALE_MIN,
710
+ max: SCALE_MAX,
711
+ readout: {
712
+ kind: "enum",
713
+ values: ["value", "percent"],
714
+ labelId: "widgetLabelReadout",
715
+ valueLabelIds: { value: "widgetReadoutValue", percent: "widgetReadoutPercent" },
716
+ placeholderId: "widgetPlaceholderDefault",
717
+ doc: "What the readout shows: the value with its unit (default) or the percent of the scale."
718
+ },
719
+ unit: UNIT,
720
+ precision: PRECISION,
721
+ thresholds: THRESHOLDS,
722
+ staleAfterSeconds: STALE_AFTER_SECONDS,
723
+ window: WINDOW
724
+ },
725
+ constraints: [{ kind: "greater-than", key: "max", than: "min" }],
726
+ defaults: { title: "Level gauge" },
727
+ interactions: {},
728
+ freshness: true,
729
+ layout: { defaultSpan: { cols: 3, rows: 4 }, minSpan: { cols: 2, rows: 3 }, maxSpan: { cols: 6, rows: 8 } }
730
+ }),
731
+ battery: defineWidgetDescriptor({
732
+ id: "battery",
733
+ version: VERSION,
734
+ sdkApiVersion: 1,
735
+ optionsVersion: 1,
736
+ display: { title: "Battery", category: "Telemetry", titleId: "widgetTypeBattery", categoryId: "widgetCategoryTelemetry" },
737
+ channel: "measurement",
738
+ datasource: { mode: "required", selectors: ONE_DEVICE },
739
+ binding: { window: { option: "window", min: 1 } },
740
+ options: {
741
+ title: TITLE_OR_MEASUREMENT,
742
+ measurement: MEASUREMENT,
743
+ min: {
744
+ kind: "number",
745
+ labelId: "widgetLabelMin",
746
+ helpId: "widgetHintBatteryRange",
747
+ doc: "Value of an empty battery. Default 0 (the scale is percent unless you set min and max)."
748
+ },
749
+ max: {
750
+ kind: "number",
751
+ labelId: "widgetLabelMax",
752
+ helpId: "widgetHintBatteryRange",
753
+ doc: "Value of a full battery. Default 100 (the scale is percent unless you set min and max)."
754
+ },
755
+ unit: UNIT,
756
+ precision: PRECISION,
757
+ thresholds: THRESHOLDS,
758
+ staleAfterSeconds: STALE_AFTER_SECONDS,
759
+ window: WINDOW
760
+ },
761
+ constraints: [{ kind: "greater-than", key: "max", than: "min" }],
762
+ defaults: { title: "Battery" },
763
+ interactions: {},
764
+ freshness: true,
765
+ layout: { defaultSpan: { cols: 3, rows: 2 }, minSpan: { cols: 2, rows: 2 }, maxSpan: { cols: 6, rows: 4 } }
766
+ }),
767
+ "status-light": defineWidgetDescriptor({
768
+ id: "status-light",
769
+ version: VERSION,
770
+ sdkApiVersion: 1,
771
+ optionsVersion: 1,
772
+ display: { title: "Status light", category: "Telemetry", titleId: "widgetTypeStatusLight", categoryId: "widgetCategoryTelemetry" },
773
+ channel: "measurement",
774
+ datasource: { mode: "required", selectors: ONE_DEVICE },
775
+ binding: { window: { option: "window", min: 1 } },
776
+ options: {
777
+ title: TITLE_OR_MEASUREMENT,
778
+ measurement: MEASUREMENT,
779
+ onAt: {
780
+ kind: "number",
781
+ labelId: "widgetLabelOnAt",
782
+ helpId: "widgetHintOnAt",
783
+ doc: "For an on/off signal: the lamp is lit at or above this value and dark below it. Unset keeps the lamp lit while a value is present, coloured by its threshold band."
784
+ },
785
+ unit: UNIT,
786
+ precision: PRECISION,
787
+ thresholds: THRESHOLDS,
788
+ staleAfterSeconds: STALE_AFTER_SECONDS,
789
+ window: WINDOW
790
+ },
791
+ defaults: { title: "Status light" },
792
+ interactions: {},
793
+ freshness: true,
794
+ layout: { defaultSpan: { cols: 2, rows: 2 }, minSpan: { cols: 2, rows: 2 }, maxSpan: { cols: 4, rows: 4 } }
795
+ }),
796
+ "signal-bars": defineWidgetDescriptor({
797
+ id: "signal-bars",
798
+ version: VERSION,
799
+ sdkApiVersion: 1,
800
+ optionsVersion: 1,
801
+ display: { title: "Signal bars", category: "Telemetry", titleId: "widgetTypeSignalBars", categoryId: "widgetCategoryTelemetry" },
802
+ channel: "measurement",
803
+ datasource: { mode: "required", selectors: ONE_DEVICE },
804
+ binding: { window: { option: "window", min: 1 } },
805
+ options: {
806
+ title: TITLE_OR_MEASUREMENT,
807
+ measurement: MEASUREMENT,
808
+ unit: UNIT,
809
+ precision: PRECISION,
810
+ // One bar per threshold: the entries ARE the steps, so they are required.
811
+ thresholds: {
812
+ ...THRESHOLDS,
813
+ maxItems: 8,
814
+ required: true,
815
+ helpId: "widgetHintSignalThresholds",
816
+ doc: "The steps of the signal, lowest first: one bar per entry, lit once the value reaches it and coloured by its level. At least two, at most eight."
817
+ },
818
+ staleAfterSeconds: STALE_AFTER_SECONDS,
819
+ window: WINDOW
820
+ },
821
+ defaults: { title: "Signal bars" },
822
+ interactions: {},
823
+ freshness: true,
824
+ layout: { defaultSpan: { cols: 2, rows: 2 }, minSpan: { cols: 2, rows: 2 }, maxSpan: { cols: 4, rows: 4 } }
825
+ }),
826
+ // ---- Radial family ----------------------------------------------------------
827
+ // A heading on a full turn, and a ring filled between a configured min and max. The compass
828
+ // draws on the registered gauge series; the ring is SVG. Both put their number in DOM text.
829
+ compass: defineWidgetDescriptor({
830
+ id: "compass",
831
+ version: VERSION,
832
+ sdkApiVersion: 1,
833
+ optionsVersion: 1,
834
+ display: { title: "Compass", category: "Telemetry", titleId: "widgetTypeCompass", categoryId: "widgetCategoryTelemetry" },
835
+ channel: "measurement",
836
+ datasource: { mode: "required", selectors: ONE_DEVICE },
837
+ binding: { window: { option: "window", min: 1 } },
838
+ options: {
839
+ title: TITLE_OR_MEASUREMENT,
840
+ measurement: MEASUREMENT,
841
+ precision: {
842
+ ...PRECISION,
843
+ doc: "Decimal places of the heading. Default 0 (whole degrees). The heading is rounded first, then folded onto 0 to 360, so it never reads 360."
844
+ },
845
+ thresholds: {
846
+ ...THRESHOLDS,
847
+ doc: "Severity bands over the heading in degrees (0 to 360, after folding). Each entry starts a band at its value that runs to the next entry; below the first, and in a normal band, the needle keeps its own colour."
848
+ },
849
+ staleAfterSeconds: STALE_AFTER_SECONDS,
850
+ window: WINDOW
851
+ },
852
+ defaults: { title: "Compass" },
853
+ interactions: {},
854
+ freshness: true,
855
+ layout: { defaultSpan: { cols: 4, rows: 4 }, minSpan: { cols: 3, rows: 3 }, maxSpan: { cols: 8, rows: 8 } }
856
+ }),
857
+ "progress-ring": defineWidgetDescriptor({
858
+ id: "progress-ring",
859
+ version: VERSION,
860
+ sdkApiVersion: 1,
861
+ optionsVersion: 1,
862
+ display: { title: "Progress ring", category: "Telemetry", titleId: "widgetTypeProgressRing", categoryId: "widgetCategoryTelemetry" },
863
+ channel: "measurement",
864
+ datasource: { mode: "required", selectors: ONE_DEVICE },
865
+ binding: { window: { option: "window", min: 1 } },
866
+ options: {
867
+ title: TITLE_OR_MEASUREMENT,
868
+ measurement: MEASUREMENT,
869
+ min: SCALE_MIN,
870
+ max: SCALE_MAX,
871
+ readout: {
872
+ kind: "enum",
873
+ values: ["percent", "value"],
874
+ labelId: "widgetLabelReadout",
875
+ valueLabelIds: { percent: "widgetReadoutPercent", value: "widgetReadoutValue" },
876
+ placeholderId: "widgetPlaceholderDefault",
877
+ doc: "What the middle of the ring shows: the percent of the scale (default) or the value with its unit."
878
+ },
879
+ unit: UNIT,
880
+ precision: PRECISION,
881
+ thresholds: THRESHOLDS,
882
+ staleAfterSeconds: STALE_AFTER_SECONDS,
883
+ window: WINDOW
884
+ },
885
+ constraints: [{ kind: "greater-than", key: "max", than: "min" }],
886
+ defaults: { title: "Progress ring" },
887
+ interactions: {},
888
+ freshness: true,
889
+ layout: { defaultSpan: { cols: 3, rows: 3 }, minSpan: { cols: 2, rows: 2 }, maxSpan: { cols: 6, rows: 6 } }
890
+ }),
891
+ "command-slider": defineWidgetDescriptor({
892
+ id: "command-slider",
893
+ version: VERSION,
894
+ sdkApiVersion: 1,
895
+ optionsVersion: 1,
896
+ display: { title: "Command slider", category: "Control", titleId: "widgetTypeCommandSlider", categoryId: "widgetCategoryControl" },
897
+ channel: "control",
898
+ datasource: { mode: "required", selectors: ONE_DEVICE },
899
+ // The control reads its last-sent value off this history, so it asks for a long one.
900
+ binding: { pageSize: { const: 50 } },
901
+ options: {
902
+ title: TITLE,
903
+ commandName: CONTROL_COMMAND_NAME,
904
+ commandLabel: CONTROL_COMMAND_LABEL,
905
+ parameterSchema: CONTROL_PARAMETER_SCHEMA,
906
+ parameter: CONTROL_PARAMETER,
907
+ min: {
908
+ kind: "number",
909
+ labelId: "widgetLabelMin",
910
+ helpId: "widgetHintSliderRange",
911
+ doc: "Lowest value, used only when the command's parameter declares no minimum."
912
+ },
913
+ max: {
914
+ kind: "number",
915
+ labelId: "widgetLabelMax",
916
+ helpId: "widgetHintSliderRange",
917
+ doc: "Highest value, used only when the command's parameter declares no maximum."
918
+ },
919
+ step: {
920
+ kind: "number",
921
+ min: 0,
922
+ labelId: "widgetLabelStep",
923
+ helpId: "widgetHintSliderStep",
924
+ doc: "Distance between values. Defaults to 1 for a whole-number parameter, else a hundredth of the range."
925
+ }
926
+ },
927
+ constraints: [{ kind: "greater-than", key: "max", than: "min" }],
928
+ defaults: { title: "Command slider" },
929
+ interactions: { commandSend: true },
930
+ layout: { defaultSpan: { cols: 4, rows: 2 }, minSpan: { cols: 3, rows: 2 }, maxSpan: { cols: 8, rows: 3 } }
931
+ }),
932
+ "command-toggle": defineWidgetDescriptor({
933
+ id: "command-toggle",
934
+ version: VERSION,
935
+ sdkApiVersion: 1,
936
+ optionsVersion: 1,
937
+ display: { title: "Command toggle", category: "Control", titleId: "widgetTypeCommandToggle", categoryId: "widgetCategoryControl" },
938
+ channel: "control",
939
+ datasource: { mode: "required", selectors: ONE_DEVICE },
940
+ binding: { pageSize: { const: 50 } },
941
+ options: {
942
+ title: TITLE,
943
+ commandName: CONTROL_COMMAND_NAME,
944
+ commandLabel: CONTROL_COMMAND_LABEL,
945
+ parameterSchema: CONTROL_PARAMETER_SCHEMA,
946
+ parameter: CONTROL_PARAMETER
947
+ },
948
+ defaults: { title: "Command toggle" },
949
+ interactions: { commandSend: true },
950
+ layout: { defaultSpan: { cols: 3, rows: 2 }, minSpan: { cols: 2, rows: 2 }, maxSpan: { cols: 6, rows: 3 } }
951
+ }),
952
+ "stat-card": defineWidgetDescriptor({
953
+ id: "stat-card",
954
+ version: VERSION,
955
+ sdkApiVersion: 1,
956
+ optionsVersion: 1,
957
+ display: { title: "Stat card", category: "Telemetry", titleId: "widgetTypeStatCard", categoryId: "widgetCategoryTelemetry" },
958
+ channel: "aggregate",
959
+ datasource: { mode: "required", selectors: ONE_DEVICE },
960
+ binding: { windowSeconds: { option: "windowSeconds" } },
961
+ options: {
962
+ title: TITLE_OR_MEASUREMENT,
963
+ measurement: MEASUREMENT,
964
+ stat: {
965
+ kind: "enum",
966
+ values: ["avg", "min", "max", "sum", "count", "last"],
967
+ labelId: "widgetLabelStat",
968
+ helpId: "widgetHintStat",
969
+ valueLabelIds: {
970
+ avg: "widgetStatAvg",
971
+ min: "widgetStatMin",
972
+ max: "widgetStatMax",
973
+ sum: "widgetStatSum",
974
+ count: "widgetStatCount",
975
+ last: "widgetStatLast"
976
+ },
977
+ placeholderId: "widgetPlaceholderDefault",
978
+ doc: "Which figure to show. Default average."
979
+ },
980
+ unit: UNIT,
981
+ precision: PRECISION,
982
+ thresholds: THRESHOLDS,
983
+ windowSeconds: WINDOW_SECONDS
984
+ },
985
+ defaults: { title: "Stat card" },
986
+ interactions: {},
987
+ layout: { defaultSpan: { cols: 3, rows: 3 }, minSpan: { cols: 3, rows: 2 }, maxSpan: { cols: 6, rows: 4 } }
988
+ }),
989
+ "bar-chart": defineWidgetDescriptor({
990
+ id: "bar-chart",
991
+ version: VERSION,
992
+ sdkApiVersion: 1,
993
+ optionsVersion: 1,
994
+ display: { title: "Bar chart", category: "Telemetry", titleId: "widgetTypeBarChart", categoryId: "widgetCategoryTelemetry" },
995
+ channel: "measurement",
996
+ datasource: { mode: "required", selectors: ALL_SELECTORS },
997
+ // One bar per measurement of a device, or (groupBy device) one bar per member of an anchor.
998
+ binding: { members: { option: "groupBy", equals: "device" } },
999
+ options: {
1000
+ title: TITLE,
1001
+ groupBy: {
1002
+ kind: "enum",
1003
+ values: ["measurement", "device"],
1004
+ labelId: "widgetLabelGroupBy",
1005
+ helpId: "widgetHintGroupBy",
1006
+ valueLabelIds: { measurement: "widgetGroupByMeasurement", device: "widgetGroupByDevice" },
1007
+ placeholderId: "widgetPlaceholderDefault",
1008
+ doc: "One bar per measurement (the default), or per device of the group for the measurement set below."
1009
+ },
1010
+ measurement: MEASUREMENT,
1011
+ unit: UNIT,
1012
+ precision: PRECISION,
1013
+ sort: CATEGORY_SORT
1014
+ },
1015
+ defaults: { title: "Bar chart" },
1016
+ interactions: {},
1017
+ layout: { defaultSpan: { cols: 6, rows: 4 }, minSpan: { cols: 4, rows: 3 } }
1018
+ }),
1019
+ "pie-chart": defineWidgetDescriptor({
1020
+ id: "pie-chart",
1021
+ version: VERSION,
1022
+ sdkApiVersion: 1,
1023
+ optionsVersion: 1,
1024
+ display: { title: "Pie chart", category: "Telemetry", titleId: "widgetTypePieChart", categoryId: "widgetCategoryTelemetry" },
1025
+ channel: "measurement",
1026
+ datasource: { mode: "required", selectors: ONE_DEVICE },
1027
+ binding: {},
1028
+ options: { title: TITLE, unit: UNIT, precision: PRECISION, sort: CATEGORY_SORT },
1029
+ defaults: { title: "Pie chart" },
1030
+ interactions: {},
1031
+ layout: { defaultSpan: { cols: 4, rows: 4 }, minSpan: { cols: 3, rows: 3 }, maxSpan: { cols: 8, rows: 8 } }
1032
+ }),
1033
+ // A new widget rather than a mode of `table`: that one is one device's measurements as rows, on a
1034
+ // channel that keys its values by name; this one is a fleet, a row per member and a column per
1035
+ // measurement, and reads its data by device.
1036
+ "fleet-table": defineWidgetDescriptor({
1037
+ id: "fleet-table",
1038
+ version: VERSION,
1039
+ sdkApiVersion: 1,
1040
+ optionsVersion: 1,
1041
+ display: { title: "Fleet table", category: "Telemetry", titleId: "widgetTypeFleetTable", categoryId: "widgetCategoryTelemetry" },
1042
+ channel: "measurement",
1043
+ datasource: { mode: "required", selectors: ALL_SELECTORS },
1044
+ binding: { windowSeconds: { option: "windowSeconds" }, members: { const: true } },
1045
+ options: {
1046
+ title: TITLE,
1047
+ columns: FLEET_COLUMNS,
1048
+ thresholds: FLEET_THRESHOLDS,
1049
+ staleAfterSeconds: STALE_AFTER_SECONDS,
1050
+ ageLagSeconds: {
1051
+ kind: "number",
1052
+ integer: true,
1053
+ min: 1,
1054
+ labelId: "widgetLabelAgeLag",
1055
+ helpId: "widgetHintAgeLag",
1056
+ doc: "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."
1057
+ },
1058
+ sortBy: {
1059
+ kind: "string",
1060
+ labelId: "widgetLabelSortBy",
1061
+ helpId: "widgetHintSortBy",
1062
+ doc: "The measurement the table is ordered by. Blank orders by device name."
1063
+ },
1064
+ sortDescending: { kind: "boolean", labelId: "widgetLabelSortDescending", doc: "Largest first." },
1065
+ pageSize: {
1066
+ kind: "number",
1067
+ integer: true,
1068
+ min: 5,
1069
+ max: 100,
1070
+ labelId: "widgetLabelPageSize",
1071
+ doc: "Devices per page. Default 20."
1072
+ },
1073
+ selectionTarget: SELECTION_TARGET,
1074
+ windowSeconds: WINDOW_SECONDS
1075
+ },
1076
+ defaults: { title: "Fleet table" },
1077
+ // The frame shows the age of the newest reading in the fleet; each cell shows its own.
1078
+ freshness: true,
1079
+ interactions: { selection: true },
1080
+ layout: { defaultSpan: { cols: 12, rows: 6 }, minSpan: { cols: 6, rows: 3 } }
474
1081
  })
475
1082
  };
476
1083
  var builtinWidgetCatalog = createWidgetCatalog(Object.values(builtinWidgetDescriptors));
477
1084
 
1085
+ // src/widgets/lazy-chunk.tsx
1086
+ import { useEffect, useState as useState2 } from "react";
1087
+ import { jsx } from "react/jsx-runtime";
1088
+ function lazyChunk(importer) {
1089
+ let loaded;
1090
+ let loading;
1091
+ const load = () => loading ?? (loading = importer().then(
1092
+ (m) => loaded = m,
1093
+ (err) => {
1094
+ loading = void 0;
1095
+ throw err;
1096
+ }
1097
+ ));
1098
+ function component(name) {
1099
+ function LazyChunkWidget(props) {
1100
+ const [mod, setMod] = useState2(loaded);
1101
+ const [error, setError] = useState2();
1102
+ useEffect(() => {
1103
+ if (!mod) load().then(setMod, setError);
1104
+ }, [mod]);
1105
+ if (error !== void 0) throw error;
1106
+ if (!mod) return null;
1107
+ const Component2 = mod[name];
1108
+ return /* @__PURE__ */ jsx(Component2, { ...props });
1109
+ }
1110
+ LazyChunkWidget.displayName = `Lazy(${name})`;
1111
+ return LazyChunkWidget;
1112
+ }
1113
+ return { load, component };
1114
+ }
1115
+
1116
+ // src/widgets/aggregate-lazy.tsx
1117
+ var chunk = lazyChunk(() => import("./chunks/aggregate-widgets-WVXAW3UL.js"));
1118
+ var loadAggregateWidgets = chunk.load;
1119
+ var statCardWidget = defineWidget({
1120
+ descriptor: builtinWidgetDescriptors["stat-card"],
1121
+ implementation: { kind: "trusted-react", component: chunk.component("StatCard") }
1122
+ });
1123
+ var barChartWidget = defineWidget({
1124
+ descriptor: builtinWidgetDescriptors["bar-chart"],
1125
+ implementation: { kind: "trusted-react", component: chunk.component("BarChart") }
1126
+ });
1127
+ var pieChartWidget = defineWidget({
1128
+ descriptor: builtinWidgetDescriptors["pie-chart"],
1129
+ implementation: { kind: "trusted-react", component: chunk.component("PieChart") }
1130
+ });
1131
+ var fleetTableWidget = defineWidget({
1132
+ descriptor: builtinWidgetDescriptors["fleet-table"],
1133
+ implementation: { kind: "trusted-react", component: chunk.component("FleetTable") }
1134
+ });
1135
+
1136
+ // src/widgets/alarm-count.tsx
1137
+ import {
1138
+ css as css2,
1139
+ defineWidget as defineWidget2,
1140
+ fitValueFont,
1141
+ fitValueFontCss,
1142
+ FIT_MIN_FONT_PX,
1143
+ fontVar,
1144
+ optString,
1145
+ spaceVar,
1146
+ useSeverityColors,
1147
+ useStickyChars,
1148
+ useWidgetScale,
1149
+ WidgetFrame
1150
+ } from "@devicechain/widget-sdk";
1151
+
478
1152
  // src/widgets/severity.ts
479
- var SEVERITY_COLORS = {
480
- CRITICAL: "#dc2626",
481
- // red-600
482
- MAJOR: "#f97316",
483
- // orange-500
484
- MINOR: "#d97706",
485
- // amber-600 (readable as foreground; console uses amber-400 as a fill)
486
- WARNING: "#0ea5e9",
487
- // sky-500 — cool end of the ramp, matching the console
488
- INDETERMINATE: "#64748b"
489
- // slate-500
1153
+ import { css } from "@devicechain/widget-sdk";
1154
+ var LEVEL_OF = {
1155
+ CRITICAL: "critical",
1156
+ MAJOR: "major",
1157
+ MINOR: "minor",
1158
+ WARNING: "warning"
490
1159
  };
491
1160
  var SEVERITY_ORDER = {
492
1161
  CRITICAL: 0,
@@ -495,9 +1164,9 @@ var SEVERITY_ORDER = {
495
1164
  WARNING: 3,
496
1165
  INDETERMINATE: 4
497
1166
  };
498
- var UNKNOWN_COLOR = "#64748b";
499
- function severityColor(severity) {
500
- return SEVERITY_COLORS[severity] ?? UNKNOWN_COLOR;
1167
+ function severityColor(colors, severity) {
1168
+ const level = LEVEL_OF[severity];
1169
+ return level ? colors[level] : css("muted-foreground");
501
1170
  }
502
1171
  function severityRank(severity) {
503
1172
  return SEVERITY_ORDER[severity] ?? 99;
@@ -515,86 +1184,133 @@ function highestSeverity(severities) {
515
1184
  }
516
1185
 
517
1186
  // src/widgets/alarm-count.tsx
518
- import { jsx, jsxs } from "react/jsx-runtime";
519
- var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
520
- function AlarmCount({ widget, data }) {
521
- const [sizeRef, size] = useElementSize();
1187
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
1188
+ var ALARM_COUNT_CAPTION_ROOM_PX = 20;
1189
+ function AlarmCountBody({ data }) {
1190
+ const { size, scale } = useWidgetScale();
1191
+ const severity = useSeverityColors();
522
1192
  const { total, alarms, loading } = data;
523
1193
  const worst = highestSeverity(alarms.map((a) => a.severity));
524
1194
  const alerting = total > 0 && worst != null;
525
- const color = alerting ? severityColor(worst) : css("muted-foreground");
526
- const valueFont = size.height > 0 ? clamp(Math.round(size.height / 2.8), 20, 56) : 40;
1195
+ const color = alerting ? severityColor(severity, worst) : css2("muted-foreground");
527
1196
  const display = loading && total === 0 ? "\u2014" : String(total);
1197
+ const chars = useStickyChars(display.length);
1198
+ const floor = Math.round(FIT_MIN_FONT_PX * scale);
1199
+ const measured = size.width > 0 && size.height > 0;
1200
+ const valueFont = measured ? fitValueFont(
1201
+ { width: size.width, height: Math.max(1, size.height - ALARM_COUNT_CAPTION_ROOM_PX * scale) },
1202
+ chars,
1203
+ { floor }
1204
+ ) : (
1205
+ // First frame: the same curve in cq units, resolved by the browser against the frame's
1206
+ // column (it does not take the caption's room off; the first measure does).
1207
+ fitValueFontCss(chars, { floor })
1208
+ );
528
1209
  const unit = total === 1 ? "alarm" : "alarms";
529
1210
  const ariaLabel = alerting ? `${total} ${unit}, worst severity ${severityLabel(worst)}` : `${total} ${unit}`;
530
- return /* @__PURE__ */ jsx(WidgetFrame, { title: optString2(widget.options, "title"), children: /* @__PURE__ */ jsxs(
1211
+ return /* @__PURE__ */ jsxs(
531
1212
  "div",
532
1213
  {
533
- ref: sizeRef,
534
1214
  style: {
535
1215
  display: "flex",
536
1216
  flexDirection: "column",
537
- justifyContent: "center",
1217
+ justifyContent: "safe center",
538
1218
  alignItems: "center",
539
1219
  height: "100%",
540
- padding: "12px 16px",
1220
+ boxSizing: "border-box",
1221
+ overflow: "hidden",
541
1222
  textAlign: "center"
542
1223
  },
543
1224
  children: [
544
- /* @__PURE__ */ jsx("div", { "aria-label": ariaLabel, style: { fontSize: valueFont, fontWeight: 700, lineHeight: 1, color }, children: display }),
1225
+ /* @__PURE__ */ jsx2(
1226
+ "div",
1227
+ {
1228
+ "aria-label": ariaLabel,
1229
+ style: {
1230
+ fontSize: valueFont,
1231
+ fontWeight: 700,
1232
+ lineHeight: 1,
1233
+ whiteSpace: "nowrap",
1234
+ fontVariantNumeric: "tabular-nums",
1235
+ color
1236
+ },
1237
+ children: display
1238
+ }
1239
+ ),
545
1240
  /* @__PURE__ */ jsxs(
546
1241
  "div",
547
1242
  {
548
1243
  style: {
549
- fontSize: 11,
550
- color: css("muted-foreground"),
551
- marginTop: 6,
1244
+ fontSize: fontVar("xs"),
1245
+ color: css2("muted-foreground"),
1246
+ marginTop: spaceVar(1),
552
1247
  textTransform: "uppercase",
553
1248
  letterSpacing: "0.05em"
554
1249
  },
555
1250
  children: [
556
- /* @__PURE__ */ jsx("span", { children: unit }),
557
- alerting ? /* @__PURE__ */ jsx("span", { children: ` \xB7 worst ${severityLabel(worst)}` }) : null
1251
+ /* @__PURE__ */ jsx2("span", { children: unit }),
1252
+ alerting ? /* @__PURE__ */ jsx2("span", { children: ` \xB7 worst ${severityLabel(worst)}` }) : null
558
1253
  ]
559
1254
  }
560
1255
  )
561
1256
  ]
562
1257
  }
563
- ) });
1258
+ );
1259
+ }
1260
+ function AlarmCount({ widget, data }) {
1261
+ return /* @__PURE__ */ jsx2(WidgetFrame, { title: optString(widget.options, "title"), children: /* @__PURE__ */ jsx2(AlarmCountBody, { data }) });
564
1262
  }
565
- var alarmCountWidget = defineWidget({
1263
+ var alarmCountWidget = defineWidget2({
566
1264
  descriptor: builtinWidgetDescriptors["alarm-count"],
567
1265
  implementation: { kind: "trusted-react", component: AlarmCount }
568
1266
  });
569
1267
 
570
1268
  // src/widgets/alarm-table.tsx
571
- import { useState } from "react";
1269
+ import { Fragment, useState as useState3 } from "react";
572
1270
  import {
573
- css as css2,
574
- defineWidget as defineWidget2,
1271
+ css as css3,
1272
+ fontVar as fontVar2,
1273
+ defineWidget as defineWidget3,
575
1274
  FlashValue,
576
1275
  formatDateTime,
577
1276
  optBoolean,
578
- optNumber as optNumber2,
579
- optString as optString3,
1277
+ optNumber,
1278
+ optString as optString2,
1279
+ useElementSize,
1280
+ useSeverityColors as useSeverityColors2,
580
1281
  useWidgetSelect,
581
1282
  useWidgetTimeZone,
582
1283
  WidgetFrame as WidgetFrame2
583
1284
  } from "@devicechain/widget-sdk";
584
- import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1285
+ import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
585
1286
  var cell = { padding: "6px 10px", textAlign: "left", whiteSpace: "nowrap" };
586
1287
  var headCell = {
587
1288
  ...cell,
588
1289
  position: "sticky",
589
1290
  top: 0,
590
- background: css2("card"),
591
- fontSize: 11,
1291
+ background: css3("card"),
1292
+ fontSize: fontVar2("xs"),
592
1293
  fontWeight: 600,
593
1294
  textTransform: "uppercase",
594
1295
  letterSpacing: "0.04em",
595
- color: css2("muted-foreground"),
596
- borderBottom: `1px solid ${css2("border")}`
1296
+ color: css3("muted-foreground"),
1297
+ borderBottom: `1px solid ${css3("border")}`
1298
+ };
1299
+ var ALARM_TABLE_NARROW_PX = 480;
1300
+ var narrowCell = {
1301
+ ...cell,
1302
+ padding: "6px 6px",
1303
+ whiteSpace: "normal",
1304
+ overflowWrap: "break-word",
1305
+ wordBreak: "normal"
597
1306
  };
1307
+ function hyphenBreaks(text) {
1308
+ const parts = text.split("-").map((p, i, all) => i < all.length - 1 ? `${p}-` : p);
1309
+ return parts.map((part, i) => /* @__PURE__ */ jsxs2(Fragment, { children: [
1310
+ i > 0 ? /* @__PURE__ */ jsx3("wbr", {}) : null,
1311
+ part
1312
+ ] }, i));
1313
+ }
598
1314
  function statusLabel(alarm) {
599
1315
  if (alarm.state === "CLEARED") return "Cleared";
600
1316
  return alarm.acknowledged ? "Acknowledged" : "Active";
@@ -602,41 +1318,45 @@ function statusLabel(alarm) {
602
1318
  function AlarmTable({ widget, data, actions }) {
603
1319
  const { alarms, total, loading } = data;
604
1320
  const canAct = actions?.can("alarm:write") ?? false;
605
- const columns = canAct ? 8 : 7;
606
- const precision = optNumber2(widget.options, "precision");
1321
+ const [sizeRef, size] = useElementSize();
1322
+ const severity = useSeverityColors2();
1323
+ const narrow = size.width > 0 && size.width < ALARM_TABLE_NARROW_PX;
1324
+ const columns = 5 + (narrow ? 0 : 2) + (canAct ? 1 : 0);
1325
+ const text = narrow ? narrowCell : cell;
1326
+ const precision = optNumber(widget.options, "precision");
607
1327
  const flash = optBoolean(widget.options, "flashOnChange");
608
1328
  const select = useWidgetSelect();
609
1329
  const timeZone = useWidgetTimeZone();
610
- const selectionTarget = optString3(widget.options, "selectionTarget");
1330
+ const selectionTarget = optString2(widget.options, "selectionTarget");
611
1331
  const drillTo = select && selectionTarget ? { select, slot: selectionTarget } : void 0;
612
- return /* @__PURE__ */ jsx2(WidgetFrame2, { title: optString3(widget.options, "title"), children: /* @__PURE__ */ jsxs2("div", { style: { display: "flex", flexDirection: "column", height: "100%" }, children: [
613
- /* @__PURE__ */ jsx2("div", { style: { flex: 1, minHeight: 0, overflow: "auto" }, children: /* @__PURE__ */ jsxs2(
1332
+ return /* @__PURE__ */ jsx3(WidgetFrame2, { title: optString2(widget.options, "title"), flush: true, children: /* @__PURE__ */ jsxs2("div", { style: { display: "flex", flexDirection: "column", height: "100%" }, children: [
1333
+ /* @__PURE__ */ jsx3("div", { ref: sizeRef, style: { flex: 1, minHeight: 0, overflow: "auto" }, children: /* @__PURE__ */ jsxs2(
614
1334
  "table",
615
1335
  {
616
1336
  style: {
617
1337
  width: "100%",
618
1338
  borderCollapse: "collapse",
619
- fontSize: 13,
620
- color: css2("foreground"),
1339
+ fontSize: fontVar2("md"),
1340
+ color: css3("foreground"),
621
1341
  fontVariantNumeric: "tabular-nums"
622
1342
  },
623
1343
  children: [
624
- /* @__PURE__ */ jsx2("thead", { children: /* @__PURE__ */ jsxs2("tr", { children: [
625
- /* @__PURE__ */ jsx2("th", { style: { ...headCell, width: 4, padding: 0 }, "aria-hidden": "true" }),
626
- /* @__PURE__ */ jsx2("th", { scope: "col", style: headCell, children: "Severity" }),
627
- /* @__PURE__ */ jsx2("th", { scope: "col", style: headCell, children: "Status" }),
628
- /* @__PURE__ */ jsx2("th", { scope: "col", style: headCell, children: "Originator" }),
629
- /* @__PURE__ */ jsx2("th", { scope: "col", style: headCell, children: "Alarm" }),
630
- /* @__PURE__ */ jsx2("th", { scope: "col", style: { ...headCell, textAlign: "right" }, children: "Value" }),
631
- /* @__PURE__ */ jsx2("th", { scope: "col", style: headCell, children: "Raised" }),
632
- canAct ? /* @__PURE__ */ jsx2("th", { scope: "col", style: { ...headCell, textAlign: "right" }, children: "Actions" }) : null
1344
+ /* @__PURE__ */ jsx3("thead", { children: /* @__PURE__ */ jsxs2("tr", { children: [
1345
+ /* @__PURE__ */ jsx3("th", { style: { ...headCell, width: 4, padding: 0 }, "aria-hidden": "true" }),
1346
+ /* @__PURE__ */ jsx3("th", { scope: "col", style: headCell, children: "Severity" }),
1347
+ narrow ? null : /* @__PURE__ */ jsx3("th", { scope: "col", style: headCell, children: "Status" }),
1348
+ /* @__PURE__ */ jsx3("th", { scope: "col", style: headCell, children: "Originator" }),
1349
+ /* @__PURE__ */ jsx3("th", { scope: "col", style: headCell, children: "Alarm" }),
1350
+ /* @__PURE__ */ jsx3("th", { scope: "col", style: { ...headCell, textAlign: "right" }, children: "Value" }),
1351
+ narrow ? null : /* @__PURE__ */ jsx3("th", { scope: "col", style: headCell, children: "Raised" }),
1352
+ canAct ? /* @__PURE__ */ jsx3("th", { scope: "col", style: { ...headCell, textAlign: "right" }, children: "Actions" }) : null
633
1353
  ] }) }),
634
- /* @__PURE__ */ jsx2("tbody", { children: alarms.length === 0 ? /* @__PURE__ */ jsx2("tr", { children: /* @__PURE__ */ jsx2("td", { colSpan: columns, style: { ...cell, textAlign: "center", color: css2("muted-foreground") }, children: loading ? "Loading\u2026" : "No alarms" }) }) : alarms.map((alarm) => {
635
- const color = severityColor(alarm.severity);
636
- return /* @__PURE__ */ jsxs2("tr", { style: { borderBottom: `1px solid ${css2("border")}` }, children: [
637
- /* @__PURE__ */ jsx2("td", { "aria-hidden": "true", style: { padding: 0, width: 4, background: color } }),
638
- /* @__PURE__ */ jsx2("td", { style: cell, children: /* @__PURE__ */ jsxs2("span", { style: { display: "inline-flex", alignItems: "center", gap: 6 }, children: [
639
- /* @__PURE__ */ jsx2(
1354
+ /* @__PURE__ */ jsx3("tbody", { children: alarms.length === 0 ? /* @__PURE__ */ jsx3("tr", { children: /* @__PURE__ */ jsx3("td", { colSpan: columns, style: { ...cell, textAlign: "center", color: css3("muted-foreground") }, children: loading ? "Loading\u2026" : "No alarms" }) }) : alarms.map((alarm) => {
1355
+ const color = severityColor(severity, alarm.severity);
1356
+ return /* @__PURE__ */ jsxs2("tr", { style: { borderBottom: `1px solid ${css3("border")}` }, children: [
1357
+ /* @__PURE__ */ jsx3("td", { "aria-hidden": "true", style: { padding: 0, width: 4, background: color } }),
1358
+ /* @__PURE__ */ jsx3("td", { style: cell, children: /* @__PURE__ */ jsxs2("span", { style: { display: "inline-flex", alignItems: "center", gap: 6 }, children: [
1359
+ /* @__PURE__ */ jsx3(
640
1360
  "span",
641
1361
  {
642
1362
  "aria-hidden": "true",
@@ -645,10 +1365,10 @@ function AlarmTable({ widget, data, actions }) {
645
1365
  ),
646
1366
  severityLabel(alarm.severity)
647
1367
  ] }) }),
648
- /* @__PURE__ */ jsx2("td", { style: { ...cell, color: css2("muted-foreground") }, children: statusLabel(alarm) }),
649
- /* @__PURE__ */ jsx2("td", { style: cell, children: /* @__PURE__ */ jsx2(OriginatorCell, { alarm, drillTo }) }),
650
- /* @__PURE__ */ jsx2("td", { style: cell, children: alarm.alarmKey }),
651
- /* @__PURE__ */ jsx2("td", { style: { ...cell, textAlign: "right" }, children: /* @__PURE__ */ jsx2(
1368
+ narrow ? null : /* @__PURE__ */ jsx3("td", { style: { ...cell, color: css3("muted-foreground") }, children: statusLabel(alarm) }),
1369
+ /* @__PURE__ */ jsx3("td", { style: text, children: /* @__PURE__ */ jsx3(OriginatorCell, { alarm, drillTo }) }),
1370
+ /* @__PURE__ */ jsx3("td", { style: text, children: hyphenBreaks(alarm.alarmKey) }),
1371
+ /* @__PURE__ */ jsx3("td", { style: { ...cell, textAlign: "right" }, children: /* @__PURE__ */ jsx3(
652
1372
  FlashValue,
653
1373
  {
654
1374
  value: alarm.lastValue,
@@ -657,8 +1377,8 @@ function AlarmTable({ widget, data, actions }) {
657
1377
  identity: alarm.token
658
1378
  }
659
1379
  ) }),
660
- /* @__PURE__ */ jsx2("td", { style: { ...cell, color: css2("muted-foreground") }, children: formatDateTime(alarm.raisedTime, timeZone) }),
661
- canAct ? /* @__PURE__ */ jsx2("td", { style: { ...cell, textAlign: "right" }, children: /* @__PURE__ */ jsx2(AlarmRowActions, { alarm, actions }) }) : null
1380
+ narrow ? null : /* @__PURE__ */ jsx3("td", { style: { ...cell, color: css3("muted-foreground") }, children: formatDateTime(alarm.raisedTime, timeZone) }),
1381
+ canAct ? /* @__PURE__ */ jsx3("td", { style: { ...cell, textAlign: "right" }, children: /* @__PURE__ */ jsx3(AlarmRowActions, { alarm, actions }) }) : null
662
1382
  ] }, alarm.token);
663
1383
  }) })
664
1384
  ]
@@ -669,9 +1389,9 @@ function AlarmTable({ widget, data, actions }) {
669
1389
  {
670
1390
  style: {
671
1391
  padding: "4px 10px",
672
- fontSize: 11,
673
- color: css2("muted-foreground"),
674
- borderTop: `1px solid ${css2("border")}`
1392
+ fontSize: fontVar2("xs"),
1393
+ color: css3("muted-foreground"),
1394
+ borderTop: `1px solid ${css3("border")}`
675
1395
  },
676
1396
  children: [
677
1397
  "Showing ",
@@ -689,7 +1409,7 @@ function OriginatorCell({
689
1409
  }) {
690
1410
  if (drillTo && alarm.originatorType === "device" && alarm.originatorToken) {
691
1411
  const token = alarm.originatorToken;
692
- return /* @__PURE__ */ jsx2(
1412
+ return /* @__PURE__ */ jsx3(
693
1413
  "button",
694
1414
  {
695
1415
  type: "button",
@@ -700,32 +1420,33 @@ function OriginatorCell({
700
1420
  border: "none",
701
1421
  background: "none",
702
1422
  font: "inherit",
703
- color: css2("primary"),
1423
+ color: css3("primary"),
704
1424
  cursor: "pointer",
705
1425
  textDecoration: "underline",
706
1426
  textUnderlineOffset: 2
707
1427
  },
708
- children: token
1428
+ children: hyphenBreaks(token)
709
1429
  }
710
1430
  );
711
1431
  }
712
- return /* @__PURE__ */ jsx2(Fragment, { children: alarm.originatorToken ?? alarm.originatorType });
1432
+ return /* @__PURE__ */ jsx3(Fragment2, { children: hyphenBreaks(alarm.originatorToken ?? alarm.originatorType) });
713
1433
  }
714
1434
  var actionButton = {
715
1435
  padding: "2px 8px",
716
- fontSize: 12,
1436
+ fontSize: fontVar2("sm"),
717
1437
  lineHeight: 1.4,
718
1438
  borderRadius: 4,
719
- border: `1px solid ${css2("border")}`,
720
- background: css2("card"),
721
- color: css2("foreground"),
1439
+ border: `1px solid ${css3("border")}`,
1440
+ background: css3("card"),
1441
+ color: css3("foreground"),
722
1442
  cursor: "pointer"
723
1443
  };
724
1444
  function AlarmRowActions({ alarm, actions }) {
725
- const [pending, setPending] = useState(null);
726
- const [failed, setFailed] = useState(false);
1445
+ const severity = useSeverityColors2();
1446
+ const [pending, setPending] = useState3(null);
1447
+ const [failed, setFailed] = useState3(false);
727
1448
  if (alarm.state !== "ACTIVE") {
728
- return /* @__PURE__ */ jsx2("span", { style: { color: css2("muted-foreground") }, children: "\u2014" });
1449
+ return /* @__PURE__ */ jsx3("span", { style: { color: css3("muted-foreground") }, children: "\u2014" });
729
1450
  }
730
1451
  const run = (kind, op) => {
731
1452
  setPending(kind);
@@ -741,17 +1462,17 @@ function AlarmRowActions({ alarm, actions }) {
741
1462
  promise.catch(() => setFailed(true)).finally(() => setPending(null));
742
1463
  };
743
1464
  return /* @__PURE__ */ jsxs2("span", { style: { display: "inline-flex", gap: 6, justifyContent: "flex-end", alignItems: "center" }, children: [
744
- failed ? /* @__PURE__ */ jsx2(
1465
+ failed ? /* @__PURE__ */ jsx3(
745
1466
  "span",
746
1467
  {
747
1468
  role: "img",
748
1469
  "aria-label": "Action failed \u2014 retry",
749
1470
  title: "Action failed \u2014 retry",
750
- style: { color: severityColor("CRITICAL"), fontSize: 12 },
1471
+ style: { color: severityColor(severity, "CRITICAL"), fontSize: fontVar2("sm") },
751
1472
  children: "\u26A0"
752
1473
  }
753
1474
  ) : null,
754
- !alarm.acknowledged ? /* @__PURE__ */ jsx2(
1475
+ !alarm.acknowledged ? /* @__PURE__ */ jsx3(
755
1476
  "button",
756
1477
  {
757
1478
  type: "button",
@@ -763,196 +1484,50 @@ function AlarmRowActions({ alarm, actions }) {
763
1484
  children: pending === "ack" ? "\u2026" : "Ack"
764
1485
  }
765
1486
  ) : null,
766
- /* @__PURE__ */ jsx2(
1487
+ /* @__PURE__ */ jsx3(
767
1488
  "button",
768
1489
  {
769
1490
  type: "button",
770
1491
  style: { ...actionButton, opacity: pending ? 0.6 : 1 },
771
1492
  disabled: pending !== null,
772
- "aria-busy": pending === "clear",
773
- "aria-label": `Clear alarm ${alarm.alarmKey}`,
774
- onClick: () => run("clear", () => actions.clearAlarm(alarm.token)),
775
- children: pending === "clear" ? "\u2026" : "Clear"
776
- }
777
- )
778
- ] });
779
- }
780
- var alarmTableWidget = defineWidget2({
781
- descriptor: builtinWidgetDescriptors["alarm-table"],
782
- implementation: { kind: "trusted-react", component: AlarmTable }
783
- });
784
-
785
- // src/widgets/command-button.tsx
786
- import { useEffect, useMemo, useRef, useState as useState2 } from "react";
787
- import {
788
- css as css3,
789
- defineWidget as defineWidget3,
790
- formatDateTime as formatDateTime2,
791
- optString as optString4,
792
- useWidgetTimeZone as useWidgetTimeZone2,
793
- WidgetFrame as WidgetFrame3
794
- } from "@devicechain/widget-sdk";
795
-
796
- // src/widgets/command-status.ts
797
- import { isTerminalCommandStatus } from "@devicechain/dashboards";
798
- var STATUS_COLORS = {
799
- SUCCESSFUL: "#16a34a",
800
- // green-600 — acknowledged by the device
801
- FAILED: "#dc2626",
802
- // red-600
803
- TIMEOUT: "#dc2626",
804
- // red-600 — never acknowledged in time
805
- EXPIRED: "#64748b",
806
- // slate-500 — lapsed before it was ever sent
807
- CANCELLED: "#7c3aed",
808
- // violet-600 — deliberately called off; a decision, not a failure
809
- QUEUED: "#0284c7",
810
- // sky-600 — in flight
811
- SENT: "#0284c7",
812
- HELD: "#d97706",
813
- // amber-600 — withheld: the device is absent, the command still stands
814
- PARKED: "#f59e0b"
815
- // amber-500 — published, nobody home; held for the device's return
816
- };
817
- var UNKNOWN_COLOR2 = "#64748b";
818
- function commandStatusColor(status) {
819
- return STATUS_COLORS[status] ?? UNKNOWN_COLOR2;
820
- }
821
- function isTerminalStatus(status) {
822
- return isTerminalCommandStatus(status);
823
- }
824
- function commandStatusLabel(status) {
825
- if (!status) return "";
826
- return status.charAt(0).toUpperCase() + status.slice(1).toLowerCase();
827
- }
828
-
829
- // src/widgets/command-params.ts
830
- var DATA_TYPES = /* @__PURE__ */ new Set(["DOUBLE", "INT", "BOOLEAN", "STRING"]);
831
- function parseParameterSchema(schema) {
832
- if (!schema) return [];
833
- let parsed;
834
- try {
835
- parsed = JSON.parse(schema);
836
- } catch {
837
- return [];
838
- }
839
- if (!Array.isArray(parsed)) return [];
840
- const out = [];
841
- for (const raw of parsed) {
842
- const norm = normalizeParam(raw);
843
- if (norm) out.push(norm);
844
- }
845
- return out;
846
- }
847
- function normalizeParam(raw) {
848
- if (typeof raw !== "object" || raw === null) return null;
849
- const r = raw;
850
- if (typeof r.name !== "string") return null;
851
- const param = { name: r.name };
852
- if (typeof r.description === "string") param.description = r.description;
853
- if (r.kind === "OBJECT") param.kind = "OBJECT";
854
- else if (r.kind === "SCALAR") param.kind = "SCALAR";
855
- if (typeof r.dataType === "string" && DATA_TYPES.has(r.dataType)) {
856
- param.dataType = r.dataType;
857
- }
858
- if (typeof r.unit === "string") param.unit = r.unit;
859
- if (r.required === true) param.required = true;
860
- if (typeof r.default === "string") param.default = r.default;
861
- if (typeof r.minValue === "number" && Number.isFinite(r.minValue)) param.minValue = r.minValue;
862
- if (typeof r.maxValue === "number" && Number.isFinite(r.maxValue)) param.maxValue = r.maxValue;
863
- if (Array.isArray(r.enum)) {
864
- const values = r.enum.filter((v) => typeof v === "string");
865
- if (values.length > 0) param.enum = values;
866
- }
867
- return param;
868
- }
869
- function isScalar(param) {
870
- return param.kind !== "OBJECT";
871
- }
872
- function parseBool(raw) {
873
- const s = raw.trim().toLowerCase();
874
- return s === "true" || s === "1" || s === "t" || s === "yes" || s === "on";
875
- }
876
- function defaultValues(params) {
877
- const values = {};
878
- for (const p of params) {
879
- if (!isScalar(p)) continue;
880
- if (p.dataType === "BOOLEAN") values[p.name] = parseBool(p.default ?? "") ? "true" : "false";
881
- else if (p.default != null) values[p.name] = p.default;
882
- }
883
- return values;
884
- }
885
- function validateParams(params, values) {
886
- const errors = {};
887
- for (const p of params) {
888
- if (!isScalar(p)) {
889
- if (p.required) errors[p.name] = "Structured parameter \u2014 not supported here";
890
- continue;
891
- }
892
- if (p.dataType === "BOOLEAN") continue;
893
- const raw = (values[p.name] ?? "").trim();
894
- if (raw === "") {
895
- if (p.required) errors[p.name] = "Required";
896
- continue;
897
- }
898
- if (p.dataType === "INT" || p.dataType === "DOUBLE") {
899
- const n = Number(raw);
900
- if (!Number.isFinite(n)) {
901
- errors[p.name] = "Must be a number";
902
- } else if (p.dataType === "INT" && !Number.isInteger(n)) {
903
- errors[p.name] = "Must be a whole number";
904
- } else if (p.minValue != null && n < p.minValue) {
905
- errors[p.name] = `Must be \u2265 ${p.minValue}`;
906
- } else if (p.maxValue != null && n > p.maxValue) {
907
- errors[p.name] = `Must be \u2264 ${p.maxValue}`;
908
- }
909
- }
910
- }
911
- return errors;
912
- }
913
- function buildPayload(params, values) {
914
- const payload = {};
915
- for (const p of params) {
916
- if (!isScalar(p)) continue;
917
- const raw = (values[p.name] ?? "").trim();
918
- if (p.dataType === "BOOLEAN") {
919
- payload[p.name] = parseBool(raw);
920
- continue;
921
- }
922
- if (raw === "") continue;
923
- payload[p.name] = coerce(p, raw);
924
- }
925
- return Object.keys(payload).length === 0 ? void 0 : JSON.stringify(payload);
926
- }
927
- function coerce(param, raw) {
928
- switch (param.dataType) {
929
- case "INT":
930
- case "DOUBLE": {
931
- const n = Number(raw);
932
- return Number.isFinite(n) ? n : raw;
933
- }
934
- case "BOOLEAN":
935
- return parseBool(raw);
936
- default:
937
- return raw;
938
- }
1493
+ "aria-busy": pending === "clear",
1494
+ "aria-label": `Clear alarm ${alarm.alarmKey}`,
1495
+ onClick: () => run("clear", () => actions.clearAlarm(alarm.token)),
1496
+ children: pending === "clear" ? "\u2026" : "Clear"
1497
+ }
1498
+ )
1499
+ ] });
939
1500
  }
1501
+ var alarmTableWidget = defineWidget3({
1502
+ descriptor: builtinWidgetDescriptors["alarm-table"],
1503
+ implementation: { kind: "trusted-react", component: AlarmTable }
1504
+ });
940
1505
 
941
1506
  // src/widgets/command-button.tsx
942
- import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1507
+ import { useEffect as useEffect2, useMemo, useRef, useState as useState4 } from "react";
1508
+ import {
1509
+ css as css4,
1510
+ fontVar as fontVar3,
1511
+ defineWidget as defineWidget4,
1512
+ formatDateTime as formatDateTime2,
1513
+ optString as optString3,
1514
+ useWidgetTimeZone as useWidgetTimeZone2,
1515
+ WidgetFrame as WidgetFrame3
1516
+ } from "@devicechain/widget-sdk";
1517
+ import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
943
1518
  function CommandButton({ widget, data, actions }) {
944
- const commandName = optString4(widget.options, "commandName");
945
- const commandLabel = optString4(widget.options, "commandLabel") ?? commandName;
946
- const parameterSchema = optString4(widget.options, "parameterSchema") ?? null;
1519
+ const commandName = optString3(widget.options, "commandName");
1520
+ const commandLabel = optString3(widget.options, "commandLabel") ?? commandName;
1521
+ const parameterSchema = optString3(widget.options, "parameterSchema") ?? null;
947
1522
  const params = useMemo(() => parseParameterSchema(parameterSchema), [parameterSchema]);
948
1523
  const schemaKey = `${commandName ?? ""}::${parameterSchema ?? ""}`;
949
- const [values, setValues] = useState2(() => defaultValues(params));
950
- const [errors, setErrors] = useState2({});
951
- const [pending, setPending] = useState2(false);
952
- const [sendError, setSendError] = useState2(null);
953
- const [sentToken, setSentToken] = useState2(null);
1524
+ const [values, setValues] = useState4(() => defaultValues(params));
1525
+ const [errors, setErrors] = useState4({});
1526
+ const [pending, setPending] = useState4(false);
1527
+ const [sendError, setSendError] = useState4(null);
1528
+ const [sentToken, setSentToken] = useState4(null);
954
1529
  const sendGen = useRef(0);
955
- useEffect(() => {
1530
+ useEffect2(() => {
956
1531
  sendGen.current += 1;
957
1532
  setValues(defaultValues(params));
958
1533
  setErrors({});
@@ -1002,10 +1577,10 @@ function CommandButton({ widget, data, actions }) {
1002
1577
  if (gen === sendGen.current) setPending(false);
1003
1578
  });
1004
1579
  };
1005
- return /* @__PURE__ */ jsx3(WidgetFrame3, { title: optString4(widget.options, "title"), children: /* @__PURE__ */ jsx3("div", { style: { display: "flex", flexDirection: "column", height: "100%", gap: 10 }, children: !commandName ? /* @__PURE__ */ jsx3("div", { style: { ...hint, margin: "auto" }, children: "No command configured." }) : /* @__PURE__ */ jsxs3(Fragment2, { children: [
1006
- /* @__PURE__ */ jsxs3("div", { style: { display: "flex", flexDirection: "column", gap: 8 }, children: [
1580
+ return /* @__PURE__ */ jsx4(WidgetFrame3, { title: optString3(widget.options, "title"), children: /* @__PURE__ */ jsx4("div", { style: { display: "flex", flexDirection: "column", height: "100%", gap: 10, overflowY: "auto" }, children: !commandName ? /* @__PURE__ */ jsx4("div", { style: { ...hint, margin: "auto" }, children: "No command configured." }) : /* @__PURE__ */ jsxs3(Fragment3, { children: [
1581
+ /* @__PURE__ */ jsxs3("div", { style: { display: "flex", flexDirection: "column", gap: 8, flexShrink: 0 }, children: [
1007
1582
  params.map(
1008
- (param) => isScalar(param) ? /* @__PURE__ */ jsx3(
1583
+ (param) => isScalar(param) ? /* @__PURE__ */ jsx4(
1009
1584
  ParamField,
1010
1585
  {
1011
1586
  widgetId: widget.id,
@@ -1019,7 +1594,7 @@ function CommandButton({ widget, data, actions }) {
1019
1594
  ) : (
1020
1595
  // A structured (OBJECT) parameter isn't form-editable. A REQUIRED one
1021
1596
  // blocks sending (validateParams flags it); flag that in the note.
1022
- /* @__PURE__ */ jsxs3("div", { style: { ...hint, color: param.required ? commandStatusColor("FAILED") : css3("muted-foreground") }, children: [
1597
+ /* @__PURE__ */ jsxs3("div", { style: { ...hint, color: param.required ? commandStatusColor("FAILED") : css4("muted-foreground") }, children: [
1023
1598
  param.name,
1024
1599
  ": ",
1025
1600
  param.required ? "required " : "",
@@ -1027,7 +1602,7 @@ function CommandButton({ widget, data, actions }) {
1027
1602
  ] }, param.name)
1028
1603
  )
1029
1604
  ),
1030
- /* @__PURE__ */ jsx3(
1605
+ /* @__PURE__ */ jsx4(
1031
1606
  "button",
1032
1607
  {
1033
1608
  type: "button",
@@ -1038,10 +1613,10 @@ function CommandButton({ widget, data, actions }) {
1038
1613
  children: pending ? "Sending\u2026" : `Send ${commandLabel}`
1039
1614
  }
1040
1615
  ),
1041
- !canWrite ? /* @__PURE__ */ jsx3("div", { style: hint, children: "You don\u2019t have permission to issue commands." }) : !hasSeam ? /* @__PURE__ */ jsx3("div", { style: hint, children: "Commands aren\u2019t available in this view." }) : !deviceToken ? /* @__PURE__ */ jsx3("div", { style: hint, children: "No device is selected for this command." }) : null,
1042
- sendError ? /* @__PURE__ */ jsx3("div", { role: "alert", style: { ...hint, color: commandStatusColor("FAILED") }, children: sendError }) : null
1616
+ !canWrite ? /* @__PURE__ */ jsx4("div", { style: hint, children: "You don\u2019t have permission to issue commands." }) : !hasSeam ? /* @__PURE__ */ jsx4("div", { style: hint, children: "Commands aren\u2019t available in this view." }) : !deviceToken ? /* @__PURE__ */ jsx4("div", { style: hint, children: "No device is selected for this command." }) : null,
1617
+ sendError ? /* @__PURE__ */ jsx4("div", { role: "alert", style: { ...hint, color: commandStatusColor("FAILED") }, children: sendError }) : null
1043
1618
  ] }),
1044
- /* @__PURE__ */ jsx3(CommandHistory, { commands: data.commands, sentToken, loading: data.loading, error: data.error })
1619
+ /* @__PURE__ */ jsx4(CommandHistory, { commands: data.commands, sentToken, loading: data.loading, error: data.error })
1045
1620
  ] }) }) });
1046
1621
  }
1047
1622
  function ParamField({
@@ -1055,13 +1630,13 @@ function ParamField({
1055
1630
  const labelId = `cmd-${widgetId}-${param.name}`;
1056
1631
  const unit = param.unit ? ` (${param.unit})` : "";
1057
1632
  return /* @__PURE__ */ jsxs3("label", { htmlFor: labelId, style: { display: "flex", flexDirection: "column", gap: 3 }, children: [
1058
- /* @__PURE__ */ jsxs3("span", { style: { fontSize: 12, color: css3("muted-foreground") }, children: [
1633
+ /* @__PURE__ */ jsxs3("span", { style: { fontSize: fontVar3("sm"), color: css4("muted-foreground") }, children: [
1059
1634
  param.name,
1060
1635
  unit,
1061
- param.required ? /* @__PURE__ */ jsx3("span", { style: { color: commandStatusColor("FAILED") }, children: " *" }) : null
1636
+ param.required ? /* @__PURE__ */ jsx4("span", { style: { color: commandStatusColor("FAILED") }, children: " *" }) : null
1062
1637
  ] }),
1063
- /* @__PURE__ */ jsx3(ParamInput, { id: labelId, param, value, disabled, onChange }),
1064
- error ? /* @__PURE__ */ jsx3("span", { style: { fontSize: 11, color: commandStatusColor("FAILED") }, children: error }) : null
1638
+ /* @__PURE__ */ jsx4(ParamInput, { id: labelId, param, value, disabled, onChange }),
1639
+ error ? /* @__PURE__ */ jsx4("span", { style: { fontSize: fontVar3("xs"), color: commandStatusColor("FAILED") }, children: error }) : null
1065
1640
  ] });
1066
1641
  }
1067
1642
  function ParamInput({
@@ -1073,12 +1648,12 @@ function ParamInput({
1073
1648
  }) {
1074
1649
  if (param.enum && param.enum.length > 0) {
1075
1650
  return /* @__PURE__ */ jsxs3("select", { id, style: inputStyle, value, disabled, onChange: (e) => onChange(e.target.value), children: [
1076
- !param.required ? /* @__PURE__ */ jsx3("option", { value: "", children: "\u2014" }) : null,
1077
- param.enum.map((opt) => /* @__PURE__ */ jsx3("option", { value: opt, children: opt }, opt))
1651
+ !param.required ? /* @__PURE__ */ jsx4("option", { value: "", children: "\u2014" }) : null,
1652
+ param.enum.map((opt) => /* @__PURE__ */ jsx4("option", { value: opt, children: opt }, opt))
1078
1653
  ] });
1079
1654
  }
1080
1655
  if (param.dataType === "BOOLEAN") {
1081
- return /* @__PURE__ */ jsx3(
1656
+ return /* @__PURE__ */ jsx4(
1082
1657
  "input",
1083
1658
  {
1084
1659
  id,
@@ -1091,7 +1666,7 @@ function ParamInput({
1091
1666
  );
1092
1667
  }
1093
1668
  const numeric = param.dataType === "INT" || param.dataType === "DOUBLE";
1094
- return /* @__PURE__ */ jsx3(
1669
+ return /* @__PURE__ */ jsx4(
1095
1670
  "input",
1096
1671
  {
1097
1672
  id,
@@ -1115,31 +1690,31 @@ function CommandHistory({
1115
1690
  }) {
1116
1691
  const empty = commands.length === 0;
1117
1692
  const timeZone = useWidgetTimeZone2();
1118
- return /* @__PURE__ */ jsx3(
1693
+ return /* @__PURE__ */ jsx4(
1119
1694
  "div",
1120
1695
  {
1121
1696
  style: {
1122
1697
  flex: 1,
1123
1698
  minHeight: 0,
1124
1699
  overflow: "auto",
1125
- borderTop: `1px solid ${css3("border")}`,
1700
+ borderTop: `1px solid ${css4("border")}`,
1126
1701
  paddingTop: 6
1127
1702
  },
1128
- children: empty ? /* @__PURE__ */ jsx3("div", { style: { ...hint, textAlign: "center", color: error ? commandStatusColor("FAILED") : css3("muted-foreground") }, children: error ? "Couldn\u2019t load command history." : loading ? "Loading\u2026" : "No commands issued yet." }) : /* @__PURE__ */ jsx3("ul", { style: { listStyle: "none", margin: 0, padding: 0, display: "flex", flexDirection: "column", gap: 4 }, children: commands.map((command) => /* @__PURE__ */ jsxs3(
1703
+ children: empty ? /* @__PURE__ */ jsx4("div", { style: { ...hint, textAlign: "center", color: error ? commandStatusColor("FAILED") : css4("muted-foreground") }, children: error ? "Couldn\u2019t load command history." : loading ? "Loading\u2026" : "No commands issued yet." }) : /* @__PURE__ */ jsx4("ul", { style: { listStyle: "none", margin: 0, padding: 0, display: "flex", flexDirection: "column", gap: 4 }, children: commands.map((command) => /* @__PURE__ */ jsxs3(
1129
1704
  "li",
1130
1705
  {
1131
1706
  style: {
1132
1707
  display: "flex",
1133
1708
  alignItems: "center",
1134
1709
  gap: 8,
1135
- fontSize: 12,
1710
+ fontSize: fontVar3("sm"),
1136
1711
  // Highlight the command just issued from this widget.
1137
1712
  fontWeight: command.token === sentToken ? 600 : 400
1138
1713
  },
1139
1714
  children: [
1140
- /* @__PURE__ */ jsx3(StatusBadge, { status: command.status }),
1141
- /* @__PURE__ */ jsx3("span", { style: { color: css3("foreground"), whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }, children: command.name }),
1142
- /* @__PURE__ */ jsx3("span", { style: { marginLeft: "auto", color: css3("muted-foreground"), whiteSpace: "nowrap" }, children: formatDateTime2(command.queuedTime, timeZone) })
1715
+ /* @__PURE__ */ jsx4(StatusBadge, { status: command.status }),
1716
+ /* @__PURE__ */ jsx4("span", { style: { color: css4("foreground"), whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }, children: command.name }),
1717
+ /* @__PURE__ */ jsx4("span", { style: { marginLeft: "auto", color: css4("muted-foreground"), whiteSpace: "nowrap" }, children: formatDateTime2(command.queuedTime, timeZone) })
1143
1718
  ]
1144
1719
  },
1145
1720
  command.token
@@ -1157,80 +1732,94 @@ function StatusBadge({ status }) {
1157
1732
  alignItems: "center",
1158
1733
  gap: 4,
1159
1734
  flexShrink: 0,
1160
- fontSize: 11,
1735
+ fontSize: fontVar3("xs"),
1161
1736
  fontWeight: 600,
1162
1737
  color
1163
1738
  },
1164
1739
  children: [
1165
- /* @__PURE__ */ jsx3("span", { "aria-hidden": "true", style: { width: 7, height: 7, borderRadius: "50%", background: color } }),
1740
+ /* @__PURE__ */ jsx4("span", { "aria-hidden": "true", style: { width: 7, height: 7, borderRadius: "50%", background: color } }),
1166
1741
  commandStatusLabel(status)
1167
1742
  ]
1168
1743
  }
1169
1744
  );
1170
1745
  }
1171
- var hint = { fontSize: 11, color: css3("muted-foreground") };
1746
+ var hint = { fontSize: fontVar3("xs"), color: css4("muted-foreground") };
1172
1747
  var SEND_FAILED = "Couldn\u2019t issue the command. Try again.";
1173
1748
  var inputStyle = {
1174
1749
  width: "100%",
1175
1750
  boxSizing: "border-box",
1176
1751
  padding: "4px 8px",
1177
- fontSize: 13,
1752
+ fontSize: fontVar3("md"),
1178
1753
  borderRadius: 4,
1179
- border: `1px solid ${css3("border")}`,
1180
- background: css3("background"),
1181
- color: css3("foreground")
1754
+ border: `1px solid ${css4("border")}`,
1755
+ background: css4("background"),
1756
+ color: css4("foreground")
1182
1757
  };
1183
1758
  var sendButton = {
1184
1759
  padding: "6px 12px",
1185
- fontSize: 13,
1760
+ fontSize: fontVar3("md"),
1186
1761
  fontWeight: 600,
1187
1762
  borderRadius: 4,
1188
1763
  border: "none",
1189
- background: css3("primary"),
1190
- color: css3("background"),
1764
+ background: css4("primary"),
1765
+ color: css4("background"),
1191
1766
  marginTop: 2
1192
1767
  };
1193
- var commandButtonWidget = defineWidget3({
1768
+ var commandButtonWidget = defineWidget4({
1194
1769
  descriptor: builtinWidgetDescriptors["command-button"],
1195
1770
  implementation: { kind: "trusted-react", component: CommandButton }
1196
1771
  });
1197
1772
 
1773
+ // src/widgets/control-lazy.tsx
1774
+ import { defineWidget as defineWidget5 } from "@devicechain/widget-sdk";
1775
+ var chunk2 = lazyChunk(() => import("./chunks/control-widgets-UDS4ET3T.js"));
1776
+ var loadControlWidgets = chunk2.load;
1777
+ var commandSliderWidget = defineWidget5({
1778
+ descriptor: builtinWidgetDescriptors["command-slider"],
1779
+ implementation: { kind: "trusted-react", component: chunk2.component("CommandSlider") }
1780
+ });
1781
+ var commandToggleWidget = defineWidget5({
1782
+ descriptor: builtinWidgetDescriptors["command-toggle"],
1783
+ implementation: { kind: "trusted-react", component: chunk2.component("CommandToggle") }
1784
+ });
1785
+
1198
1786
  // src/widgets/entity-selector.tsx
1199
- import { useEffect as useEffect2, useState as useState3 } from "react";
1787
+ import { useEffect as useEffect3, useState as useState5 } from "react";
1200
1788
  import {
1201
- css as css4,
1202
- defineWidget as defineWidget4,
1203
- optString as optString5,
1789
+ css as css5,
1790
+ fontVar as fontVar4,
1791
+ defineWidget as defineWidget6,
1792
+ optString as optString4,
1204
1793
  useCandidates,
1205
1794
  useWidgetSelect as useWidgetSelect2,
1206
1795
  WidgetFrame as WidgetFrame4
1207
1796
  } from "@devicechain/widget-sdk";
1208
- import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1797
+ import { Fragment as Fragment4, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1209
1798
  function tokenOf(b) {
1210
1799
  return b.kind === "device" ? b.deviceToken : b.anchor.targetToken;
1211
1800
  }
1212
1801
  var control = {
1213
1802
  width: "100%",
1214
- fontSize: 14,
1803
+ fontSize: fontVar4("md"),
1215
1804
  padding: "8px 10px",
1216
1805
  borderRadius: 6,
1217
- border: `1px solid ${css4("border")}`,
1218
- background: css4("background"),
1219
- color: css4("foreground")
1806
+ border: `1px solid ${css5("border")}`,
1807
+ background: css5("background"),
1808
+ color: css5("foreground")
1220
1809
  };
1221
1810
  var hint2 = {
1222
- fontSize: 12,
1223
- color: css4("muted-foreground")
1811
+ fontSize: fontVar4("sm"),
1812
+ color: css5("muted-foreground")
1224
1813
  };
1225
1814
  function EntitySelector({ widget }) {
1226
1815
  const select = useWidgetSelect2();
1227
- const target = optString5(widget.options, "selectionTarget");
1816
+ const target = optString4(widget.options, "selectionTarget");
1228
1817
  const { candidates, loading, error, wired } = useCandidates(target);
1229
- const title = optString5(widget.options, "title");
1230
- const [pending, setPending] = useState3(null);
1818
+ const title = optString4(widget.options, "title");
1819
+ const [pending, setPending] = useState5(null);
1231
1820
  const selected = candidates.find((c) => c.selected);
1232
1821
  const selectedToken = selected ? tokenOf(selected.binding) : "";
1233
- useEffect2(() => {
1822
+ useEffect3(() => {
1234
1823
  if (pending !== null && (selectedToken === pending || !candidates.some((c) => tokenOf(c.binding) === pending))) {
1235
1824
  setPending(null);
1236
1825
  }
@@ -1246,25 +1835,38 @@ function EntitySelector({ widget }) {
1246
1835
  select({ slot: target, binding: candidate.binding });
1247
1836
  }
1248
1837
  };
1249
- return /* @__PURE__ */ jsx4(WidgetFrame4, { title, children: /* @__PURE__ */ jsx4("div", { style: { display: "flex", flexDirection: "column", gap: 8, padding: "12px 16px", height: "100%" }, children: inert ? /* @__PURE__ */ jsx4("span", { style: hint2, children: select && target ? "Selection isn\u2019t available here." : select ? "No target slot configured." : "Selection is available on the live dashboard." }) : /* @__PURE__ */ jsxs4(Fragment3, { children: [
1250
- /* @__PURE__ */ jsxs4(
1251
- "select",
1252
- {
1253
- style: { ...control, opacity: loading ? 0.6 : 1 },
1254
- value,
1255
- disabled: candidates.length === 0,
1256
- onChange,
1257
- "aria-label": title || `Select ${target}`,
1258
- children: [
1259
- /* @__PURE__ */ jsx4("option", { value: "", disabled: true, children: loading ? "Loading\u2026" : candidates.length === 0 ? "No options" : "Select\u2026" }),
1260
- candidates.map((c) => /* @__PURE__ */ jsx4("option", { value: tokenOf(c.binding), children: c.label }, tokenOf(c.binding)))
1261
- ]
1262
- }
1263
- ),
1264
- error ? /* @__PURE__ */ jsx4("span", { style: hint2, children: "Couldn\u2019t load options." }) : null
1265
- ] }) }) });
1838
+ return /* @__PURE__ */ jsx5(WidgetFrame4, { title, children: /* @__PURE__ */ jsx5(
1839
+ "div",
1840
+ {
1841
+ style: {
1842
+ display: "flex",
1843
+ flexDirection: "column",
1844
+ gap: 8,
1845
+ height: "100%",
1846
+ boxSizing: "border-box",
1847
+ overflow: "auto"
1848
+ },
1849
+ children: inert ? /* @__PURE__ */ jsx5("span", { style: hint2, children: select && target ? "Selection isn\u2019t available here." : select ? "No target slot configured." : "Selection is available on the live dashboard." }) : /* @__PURE__ */ jsxs4(Fragment4, { children: [
1850
+ /* @__PURE__ */ jsxs4(
1851
+ "select",
1852
+ {
1853
+ style: { ...control, opacity: loading ? 0.6 : 1 },
1854
+ value,
1855
+ disabled: candidates.length === 0,
1856
+ onChange,
1857
+ "aria-label": title || `Select ${target}`,
1858
+ children: [
1859
+ /* @__PURE__ */ jsx5("option", { value: "", disabled: true, children: loading ? "Loading\u2026" : candidates.length === 0 ? "No options" : "Select\u2026" }),
1860
+ candidates.map((c) => /* @__PURE__ */ jsx5("option", { value: tokenOf(c.binding), children: c.label }, tokenOf(c.binding)))
1861
+ ]
1862
+ }
1863
+ ),
1864
+ error ? /* @__PURE__ */ jsx5("span", { style: hint2, children: "Couldn\u2019t load options." }) : null
1865
+ ] })
1866
+ }
1867
+ ) });
1266
1868
  }
1267
- var entitySelectorWidget = defineWidget4({
1869
+ var entitySelectorWidget = defineWidget6({
1268
1870
  descriptor: builtinWidgetDescriptors["entity-selector"],
1269
1871
  implementation: { kind: "trusted-react", component: EntitySelector }
1270
1872
  });
@@ -1272,58 +1874,105 @@ var entitySelectorWidget = defineWidget4({
1272
1874
  // src/widgets/gauge.tsx
1273
1875
  import { useMemo as useMemo2 } from "react";
1274
1876
  import {
1275
- defineWidget as defineWidget5,
1877
+ defineWidget as defineWidget7,
1276
1878
  flashBackgroundStyle,
1879
+ formatValue,
1277
1880
  optBoolean as optBoolean2,
1278
- optNumber as optNumber3,
1279
- optString as optString6,
1881
+ optNumber as optNumber2,
1882
+ optString as optString5,
1883
+ optThresholds,
1280
1884
  pickSample,
1281
1885
  primaryMeasurementName,
1282
- useElementSize as useElementSize2,
1283
1886
  useFlashOnChange,
1887
+ useWidgetScale as useWidgetScale2,
1284
1888
  WidgetFrame as WidgetFrame5
1285
1889
  } from "@devicechain/widget-sdk";
1286
- import { buildGaugeOption, EChart, useChartTheme } from "@devicechain/widget-sdk/echarts";
1287
- import { jsx as jsx5 } from "react/jsx-runtime";
1890
+ import { buildGaugeOption, EChart, gaugeReadoutBox, useChartTheme } from "@devicechain/widget-sdk/echarts";
1891
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1288
1892
  function Gauge({ widget, data }) {
1893
+ const name = primaryMeasurementName(widget);
1894
+ const sample = pickSample(data.latest, name);
1895
+ return /* @__PURE__ */ jsx6(WidgetFrame5, { title: optString5(widget.options, "title") ?? name, sampleTime: sample?.occurredTime ?? null, flush: true, children: /* @__PURE__ */ jsx6(GaugeBody, { widget, data }) });
1896
+ }
1897
+ function GaugeBody({ widget, data }) {
1289
1898
  const theme = useChartTheme();
1290
- const [sizeRef, size] = useElementSize2();
1899
+ const { size, scale } = useWidgetScale2();
1291
1900
  const name = primaryMeasurementName(widget);
1292
1901
  const sample = pickSample(data.latest, name);
1293
- const min = optNumber3(widget.options, "min");
1294
- const max = optNumber3(widget.options, "max");
1295
- const unit = optString6(widget.options, "unit");
1902
+ const min = optNumber2(widget.options, "min");
1903
+ const max = optNumber2(widget.options, "max");
1904
+ const unit = optString5(widget.options, "unit");
1905
+ const precision = optNumber2(widget.options, "precision");
1906
+ const thresholds = useMemo2(() => optThresholds(widget.options, "thresholds"), [widget.options]);
1296
1907
  const option = useMemo2(
1297
- () => buildGaugeOption(sample?.value ?? null, theme, { min, max, unit }, size),
1298
- [sample?.value, theme, min, max, unit, size.width, size.height]
1908
+ () => buildGaugeOption(sample?.value ?? null, theme, { min, max, thresholds, scale, readout: "none", emptyNeedle: "none" }, size),
1909
+ [sample?.value, theme, min, max, thresholds, scale, size]
1299
1910
  );
1300
1911
  const flashDirection = useFlashOnChange(
1301
1912
  sample?.value,
1302
1913
  optBoolean2(widget.options, "flashOnChange"),
1303
1914
  sample && `${sample.deviceToken}:${sample.name}`
1304
1915
  );
1305
- return /* @__PURE__ */ jsx5(WidgetFrame5, { title: optString6(widget.options, "title") ?? name, children: /* @__PURE__ */ jsx5("div", { ref: sizeRef, style: { width: "100%", height: "100%", ...flashBackgroundStyle(flashDirection) }, children: /* @__PURE__ */ jsx5(EChart, { option }) }) });
1916
+ const box = gaugeReadoutBox(size);
1917
+ const range = readRange(min, max, { min: 0, max: 100 });
1918
+ const value = sample?.value ?? void 0;
1919
+ const meter = range.ok && value !== void 0 && Number.isFinite(value) ? {
1920
+ role: "meter",
1921
+ "aria-valuemin": range.range.min,
1922
+ "aria-valuemax": range.range.max,
1923
+ "aria-valuenow": Math.min(range.range.max, Math.max(range.range.min, value)),
1924
+ "aria-valuetext": `${formatValue(value, precision)}${unit ? ` ${unit}` : ""}`
1925
+ } : {};
1926
+ return /* @__PURE__ */ jsxs5("div", { style: { position: "relative", width: "100%", height: "100%", ...flashBackgroundStyle(flashDirection) }, ...meter, children: [
1927
+ /* @__PURE__ */ jsx6(EChart, { option }),
1928
+ /* @__PURE__ */ jsx6(
1929
+ "div",
1930
+ {
1931
+ style: {
1932
+ position: "absolute",
1933
+ left: box.centerX - box.width / 2,
1934
+ top: box.centerY - box.height / 2,
1935
+ width: box.width,
1936
+ height: box.height,
1937
+ display: "flex",
1938
+ alignItems: "center",
1939
+ justifyContent: "center",
1940
+ pointerEvents: "none"
1941
+ },
1942
+ children: /* @__PURE__ */ jsx6(
1943
+ LevelReadout,
1944
+ {
1945
+ text: formatValue(value, precision),
1946
+ unit: value === void 0 ? void 0 : unit,
1947
+ size: { width: box.width, height: box.height },
1948
+ scale
1949
+ }
1950
+ )
1951
+ }
1952
+ )
1953
+ ] });
1306
1954
  }
1307
- var gaugeWidget = defineWidget5({
1955
+ var gaugeWidget = defineWidget7({
1308
1956
  descriptor: builtinWidgetDescriptors["gauge"],
1309
1957
  implementation: { kind: "trusted-react", component: Gauge }
1310
1958
  });
1311
1959
 
1312
1960
  // src/widgets/image.tsx
1313
1961
  import {
1314
- css as css5,
1315
- defineWidget as defineWidget6,
1316
- optString as optString7,
1962
+ css as css6,
1963
+ defineWidget as defineWidget8,
1964
+ fontVar as fontVar5,
1965
+ optString as optString6,
1317
1966
  WidgetFrame as WidgetFrame6
1318
1967
  } from "@devicechain/widget-sdk";
1319
- import { jsx as jsx6 } from "react/jsx-runtime";
1968
+ import { jsx as jsx7 } from "react/jsx-runtime";
1320
1969
  var FITS = /* @__PURE__ */ new Set(["contain", "cover", "fill"]);
1321
1970
  function Image({ widget }) {
1322
- const url = optString7(widget.options, "url");
1323
- const alt = optString7(widget.options, "alt") ?? "";
1324
- const fitOption = optString7(widget.options, "fit");
1971
+ const url = optString6(widget.options, "url");
1972
+ const alt = optString6(widget.options, "alt") ?? "";
1973
+ const fitOption = optString6(widget.options, "fit");
1325
1974
  const objectFit = fitOption && FITS.has(fitOption) ? fitOption : "contain";
1326
- return /* @__PURE__ */ jsx6(WidgetFrame6, { title: optString7(widget.options, "title"), children: url ? /* @__PURE__ */ jsx6("img", { src: url, alt, style: { width: "100%", height: "100%", objectFit, display: "block" } }) : /* @__PURE__ */ jsx6(
1975
+ return /* @__PURE__ */ jsx7(WidgetFrame6, { title: optString6(widget.options, "title"), flush: true, children: url ? /* @__PURE__ */ jsx7("img", { src: url, alt, style: { width: "100%", height: "100%", objectFit, display: "block" } }) : /* @__PURE__ */ jsx7(
1327
1976
  "div",
1328
1977
  {
1329
1978
  style: {
@@ -1331,52 +1980,59 @@ function Image({ widget }) {
1331
1980
  alignItems: "center",
1332
1981
  justifyContent: "center",
1333
1982
  height: "100%",
1334
- color: css5("muted-foreground"),
1335
- fontSize: 13
1983
+ color: css6("muted-foreground"),
1984
+ fontSize: fontVar5("md")
1336
1985
  },
1337
1986
  children: "No image URL"
1338
1987
  }
1339
1988
  ) });
1340
1989
  }
1341
- var imageWidget = defineWidget6({
1990
+ var imageWidget = defineWidget8({
1342
1991
  descriptor: builtinWidgetDescriptors["image"],
1343
1992
  implementation: { kind: "trusted-react", component: Image }
1344
1993
  });
1345
1994
 
1346
1995
  // src/widgets/label.tsx
1347
1996
  import {
1348
- css as css6,
1349
- defineWidget as defineWidget7,
1350
- optNumber as optNumber4,
1351
- optString as optString8,
1997
+ css as css7,
1998
+ defineWidget as defineWidget9,
1999
+ fontVar as fontVar6,
2000
+ spaceVar as spaceVar2,
2001
+ optNumber as optNumber3,
2002
+ optString as optString7,
1352
2003
  WidgetFrame as WidgetFrame7
1353
2004
  } from "@devicechain/widget-sdk";
1354
- import { jsx as jsx7 } from "react/jsx-runtime";
2005
+ import { jsx as jsx8 } from "react/jsx-runtime";
1355
2006
  var justify = {
1356
2007
  left: "flex-start",
1357
2008
  center: "center",
1358
2009
  right: "flex-end"
1359
2010
  };
1360
2011
  function Label({ widget }) {
1361
- const text = optString8(widget.options, "text") ?? "";
1362
- const align = optString8(widget.options, "align") ?? "center";
1363
- const fontSize = optNumber4(widget.options, "fontSize") ?? 16;
1364
- return /* @__PURE__ */ jsx7(WidgetFrame7, { title: optString8(widget.options, "title"), children: /* @__PURE__ */ jsx7(
2012
+ const text = optString7(widget.options, "text") ?? "";
2013
+ const align = optString7(widget.options, "align") ?? "center";
2014
+ const authored = optNumber3(widget.options, "fontSize");
2015
+ const fontSize = authored !== void 0 ? `calc(${authored}px * var(--dc-scale, 1))` : fontVar6("lg");
2016
+ return /* @__PURE__ */ jsx8(WidgetFrame7, { title: optString7(widget.options, "title"), flush: true, children: /* @__PURE__ */ jsx8(
1365
2017
  "div",
1366
2018
  {
1367
2019
  style: {
1368
2020
  display: "flex",
1369
- alignItems: "center",
2021
+ alignItems: "safe center",
1370
2022
  justifyContent: justify[align] ?? "center",
1371
2023
  height: "100%",
1372
- padding: 12
2024
+ boxSizing: "border-box",
2025
+ overflow: "hidden",
2026
+ // A label is often one row tall: the frame's uniform inset would leave it a line's
2027
+ // height or less, so it keeps a slim vertical inset and the usual side inset.
2028
+ padding: `${spaceVar2(1)} ${spaceVar2(3)}`
1373
2029
  },
1374
- children: /* @__PURE__ */ jsx7(
2030
+ children: /* @__PURE__ */ jsx8(
1375
2031
  "span",
1376
2032
  {
1377
2033
  style: {
1378
2034
  fontSize,
1379
- color: css6("foreground"),
2035
+ color: css7("foreground"),
1380
2036
  textAlign: align,
1381
2037
  whiteSpace: "pre-wrap"
1382
2038
  },
@@ -1386,96 +2042,166 @@ function Label({ widget }) {
1386
2042
  }
1387
2043
  ) });
1388
2044
  }
1389
- var labelWidget = defineWidget7({
2045
+ var labelWidget = defineWidget9({
1390
2046
  descriptor: builtinWidgetDescriptors["label"],
1391
2047
  implementation: { kind: "trusted-react", component: Label }
1392
2048
  });
1393
2049
 
1394
2050
  // src/widgets/latest-card.tsx
2051
+ import { useMemo as useMemo3 } from "react";
1395
2052
  import {
1396
- css as css7,
1397
- defineWidget as defineWidget8,
2053
+ bandAt,
2054
+ css as css8,
2055
+ defineWidget as defineWidget10,
2056
+ fitValueFont as fitValueFont2,
2057
+ fitValueFontCss as fitValueFontCss2,
2058
+ FIT_MIN_FONT_PX as FIT_MIN_FONT_PX2,
1398
2059
  flashTextStyle,
2060
+ fontVar as fontVar7,
1399
2061
  formatTimestamp,
1400
- formatValue,
2062
+ formatValue as formatValue2,
2063
+ levelColor,
1401
2064
  optBoolean as optBoolean3,
1402
- optNumber as optNumber5,
1403
- optString as optString9,
2065
+ optNumber as optNumber4,
2066
+ optString as optString8,
2067
+ optThresholds as optThresholds2,
1404
2068
  pickSample as pickSample2,
1405
2069
  primaryMeasurementName as primaryMeasurementName2,
1406
- useElementSize as useElementSize3,
2070
+ spaceVar as spaceVar3,
2071
+ useSeverityColors as useSeverityColors3,
2072
+ useStickyChars as useStickyChars2,
1407
2073
  useFlashOnChange as useFlashOnChange2,
1408
2074
  useWidgetTimeZone as useWidgetTimeZone3,
1409
2075
  WidgetFrame as WidgetFrame8
1410
2076
  } from "@devicechain/widget-sdk";
1411
- import { jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
1412
- var clamp2 = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
2077
+ import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
2078
+ var TIMESTAMP_ROOM_PX = 18;
1413
2079
  function LatestCard({ widget, data }) {
1414
- const [sizeRef, size] = useElementSize3();
2080
+ const severity = useSeverityColors3();
1415
2081
  const timeZone = useWidgetTimeZone3();
1416
2082
  const name = primaryMeasurementName2(widget);
1417
2083
  const sample = pickSample2(data.latest, name);
1418
- const title = optString9(widget.options, "title") ?? name;
1419
- const unit = optString9(widget.options, "unit");
1420
- const precision = optNumber5(widget.options, "precision");
1421
- const display = formatValue(sample?.value, precision);
2084
+ const title = optString8(widget.options, "title") ?? name;
2085
+ const unit = optString8(widget.options, "unit");
2086
+ const precision = optNumber4(widget.options, "precision");
2087
+ const display = formatValue2(sample?.value, precision);
2088
+ const thresholds = useMemo3(() => optThresholds2(widget.options, "thresholds"), [widget.options]);
2089
+ const band = bandAt(thresholds, sample?.value);
2090
+ const valueColor = levelColor(severity, band?.level) ?? css8("foreground");
1422
2091
  const flashDirection = useFlashOnChange2(
1423
2092
  sample?.value,
1424
2093
  optBoolean3(widget.options, "flashOnChange"),
1425
2094
  sample && `${sample.deviceToken}:${sample.name}`
1426
2095
  );
1427
- const valueFont = size.height > 0 ? clamp2(Math.round(size.height / 3.5), 16, 44) : 32;
1428
- return /* @__PURE__ */ jsx8(WidgetFrame8, { title, children: /* @__PURE__ */ jsxs5(
1429
- "div",
1430
- {
1431
- ref: sizeRef,
1432
- style: {
1433
- display: "flex",
1434
- flexDirection: "column",
1435
- justifyContent: "center",
1436
- height: "100%",
1437
- padding: "12px 16px"
1438
- },
1439
- children: [
1440
- /* @__PURE__ */ jsxs5(
1441
- "div",
1442
- {
1443
- style: {
1444
- fontSize: valueFont,
1445
- fontWeight: 600,
1446
- lineHeight: 1.1,
1447
- color: css7("foreground"),
1448
- ...flashTextStyle(flashDirection)
1449
- },
1450
- children: [
1451
- display,
1452
- sample?.value != null && unit ? /* @__PURE__ */ jsx8("span", { style: { fontSize: Math.round(valueFont / 2), color: css7("muted-foreground"), marginLeft: 6 }, children: unit }) : null
1453
- ]
1454
- }
1455
- ),
1456
- sample?.occurredTime ? /* @__PURE__ */ jsx8("div", { style: { fontSize: 11, color: css7("muted-foreground"), marginTop: 4 }, children: formatTimestamp(sample.occurredTime, timeZone) }) : null
1457
- ]
1458
- }
1459
- ) });
2096
+ const chars = useStickyChars2(display.length + (sample?.value != null && unit ? Math.ceil(unit.length / 2) + 1 : 0));
2097
+ return /* @__PURE__ */ jsx9(WidgetFrame8, { title, sampleTime: sample?.occurredTime ?? null, children: ({ size, scale }) => {
2098
+ const floor = Math.round(FIT_MIN_FONT_PX2 * scale);
2099
+ const measured = size.width > 0 && size.height > 0;
2100
+ const valuePx = measured ? fitValueFont2(size, chars, { floor }) : void 0;
2101
+ const valueFont = valuePx ?? fitValueFontCss2(chars, { floor });
2102
+ const showTimestamp = valuePx === void 0 || size.height - Math.ceil(valuePx * 1.1) >= TIMESTAMP_ROOM_PX * scale;
2103
+ return /* @__PURE__ */ jsxs6(
2104
+ "div",
2105
+ {
2106
+ style: {
2107
+ display: "flex",
2108
+ flexDirection: "column",
2109
+ justifyContent: "safe center",
2110
+ height: "100%",
2111
+ boxSizing: "border-box",
2112
+ overflow: "hidden"
2113
+ },
2114
+ children: [
2115
+ /* @__PURE__ */ jsxs6(
2116
+ "div",
2117
+ {
2118
+ "data-threshold-level": band?.level,
2119
+ title: band?.label,
2120
+ style: {
2121
+ fontSize: valueFont,
2122
+ fontWeight: 600,
2123
+ lineHeight: 1.1,
2124
+ // One line, and digits of one width: the fit counts characters, so a live value
2125
+ // must not wrap or shift sideways as its digits change.
2126
+ whiteSpace: "nowrap",
2127
+ fontVariantNumeric: "tabular-nums",
2128
+ color: valueColor,
2129
+ ...flashTextStyle(flashDirection)
2130
+ },
2131
+ children: [
2132
+ display,
2133
+ sample?.value != null && unit ? /* @__PURE__ */ jsx9(
2134
+ "span",
2135
+ {
2136
+ style: {
2137
+ fontSize: valuePx !== void 0 ? Math.round(valuePx / 2) : `calc(${valueFont} / 2)`,
2138
+ color: css8("muted-foreground"),
2139
+ marginLeft: spaceVar3(1)
2140
+ },
2141
+ children: unit
2142
+ }
2143
+ ) : null
2144
+ ]
2145
+ }
2146
+ ),
2147
+ sample?.occurredTime && showTimestamp ? /* @__PURE__ */ jsx9("div", { style: { fontSize: fontVar7("xs"), color: css8("muted-foreground"), marginTop: spaceVar3(1) }, children: formatTimestamp(sample.occurredTime, timeZone) }) : null
2148
+ ]
2149
+ }
2150
+ );
2151
+ } });
1460
2152
  }
1461
- var latestCardWidget = defineWidget8({
2153
+ var latestCardWidget = defineWidget10({
1462
2154
  descriptor: builtinWidgetDescriptors["latest-card"],
1463
2155
  implementation: { kind: "trusted-react", component: LatestCard }
1464
2156
  });
1465
2157
 
2158
+ // src/widgets/level-lazy.tsx
2159
+ import { defineWidget as defineWidget11 } from "@devicechain/widget-sdk";
2160
+ var chunk3 = lazyChunk(() => import("./chunks/level-widgets-MJB7TOLU.js"));
2161
+ var loadLevelWidgets = chunk3.load;
2162
+ var lazyComponent = chunk3.component;
2163
+ var linearGaugeWidget = defineWidget11({
2164
+ descriptor: builtinWidgetDescriptors["linear-gauge"],
2165
+ implementation: { kind: "trusted-react", component: lazyComponent("LinearGauge") }
2166
+ });
2167
+ var levelGaugeWidget = defineWidget11({
2168
+ descriptor: builtinWidgetDescriptors["level-gauge"],
2169
+ implementation: { kind: "trusted-react", component: lazyComponent("LevelGauge") }
2170
+ });
2171
+ var batteryWidget = defineWidget11({
2172
+ descriptor: builtinWidgetDescriptors["battery"],
2173
+ implementation: { kind: "trusted-react", component: lazyComponent("Battery") }
2174
+ });
2175
+ var statusLightWidget = defineWidget11({
2176
+ descriptor: builtinWidgetDescriptors["status-light"],
2177
+ implementation: { kind: "trusted-react", component: lazyComponent("StatusLight") }
2178
+ });
2179
+ var signalBarsWidget = defineWidget11({
2180
+ descriptor: builtinWidgetDescriptors["signal-bars"],
2181
+ implementation: { kind: "trusted-react", component: lazyComponent("SignalBars") }
2182
+ });
2183
+ var compassWidget = defineWidget11({
2184
+ descriptor: builtinWidgetDescriptors["compass"],
2185
+ implementation: { kind: "trusted-react", component: lazyComponent("Compass") }
2186
+ });
2187
+ var progressRingWidget = defineWidget11({
2188
+ descriptor: builtinWidgetDescriptors["progress-ring"],
2189
+ implementation: { kind: "trusted-react", component: lazyComponent("ProgressRing") }
2190
+ });
2191
+
1466
2192
  // src/widgets/map.tsx
1467
- import { useEffect as useEffect3, useRef as useRef2, useState as useState4 } from "react";
2193
+ import { useEffect as useEffect4, useRef as useRef2, useState as useState6 } from "react";
1468
2194
  import { renderableBasemap, resolveBasemap } from "@devicechain/client";
1469
2195
 
1470
2196
  // src/basemap-context.tsx
1471
2197
  import { createContext, useContext } from "react";
1472
- import { jsx as jsx9 } from "react/jsx-runtime";
2198
+ import { jsx as jsx10 } from "react/jsx-runtime";
1473
2199
  var TenantBasemapContext = createContext(null);
1474
2200
  function TenantBasemapProvider({
1475
2201
  basemap,
1476
2202
  children
1477
2203
  }) {
1478
- return /* @__PURE__ */ jsx9(TenantBasemapContext.Provider, { value: basemap, children });
2204
+ return /* @__PURE__ */ jsx10(TenantBasemapContext.Provider, { value: basemap, children });
1479
2205
  }
1480
2206
  function useTenantBasemap() {
1481
2207
  return useContext(TenantBasemapContext);
@@ -1483,13 +2209,13 @@ function useTenantBasemap() {
1483
2209
 
1484
2210
  // src/map-runtime-context.tsx
1485
2211
  import { createContext as createContext2, useContext as useContext2 } from "react";
1486
- import { jsx as jsx10 } from "react/jsx-runtime";
2212
+ import { jsx as jsx11 } from "react/jsx-runtime";
1487
2213
  var MapRuntimeContext = createContext2(null);
1488
2214
  function MapRuntimeProvider({
1489
2215
  runtime,
1490
2216
  children
1491
2217
  }) {
1492
- return /* @__PURE__ */ jsx10(MapRuntimeContext.Provider, { value: runtime, children });
2218
+ return /* @__PURE__ */ jsx11(MapRuntimeContext.Provider, { value: runtime, children });
1493
2219
  }
1494
2220
  function useMapRuntime() {
1495
2221
  return useContext2(MapRuntimeContext);
@@ -1497,9 +2223,10 @@ function useMapRuntime() {
1497
2223
 
1498
2224
  // src/widgets/map.tsx
1499
2225
  import {
1500
- css as css8,
1501
- defineWidget as defineWidget9,
1502
- optString as optString10,
2226
+ css as css9,
2227
+ fontVar as fontVar8,
2228
+ defineWidget as defineWidget12,
2229
+ optString as optString9,
1503
2230
  WidgetFrame as WidgetFrame9
1504
2231
  } from "@devicechain/widget-sdk";
1505
2232
 
@@ -1596,7 +2323,7 @@ function loadLandStyle() {
1596
2323
  }
1597
2324
 
1598
2325
  // src/widgets/map.tsx
1599
- import { jsx as jsx11, jsxs as jsxs6 } from "react/jsx-runtime";
2326
+ import { jsx as jsx12, jsxs as jsxs7 } from "react/jsx-runtime";
1600
2327
  var mapLibrePromise;
1601
2328
  function loadMapLibre(runtime) {
1602
2329
  if (!mapLibrePromise) {
@@ -1612,20 +2339,20 @@ function loadMapLibre(runtime) {
1612
2339
  }
1613
2340
  var SINGLE_DEVICE_ZOOM = 14;
1614
2341
  function MapWidget({ widget, data }) {
1615
- const title = optString10(widget.options, "title");
2342
+ const title = optString9(widget.options, "title");
1616
2343
  const positions = placeable(data.locations);
1617
2344
  const runtime = useMapRuntime();
1618
2345
  const basemap = renderableBasemap(
1619
2346
  resolveBasemap(
1620
2347
  {
1621
- tileUrl: optString10(widget.options, "tileUrl"),
1622
- attribution: optString10(widget.options, "attribution")
2348
+ tileUrl: optString9(widget.options, "tileUrl"),
2349
+ attribution: optString9(widget.options, "attribution")
1623
2350
  },
1624
2351
  useTenantBasemap()
1625
2352
  )
1626
2353
  );
1627
2354
  if (data.forbidden) {
1628
- return /* @__PURE__ */ jsx11(WidgetFrame9, { title, children: /* @__PURE__ */ jsx11(
2355
+ return /* @__PURE__ */ jsx12(WidgetFrame9, { title, children: /* @__PURE__ */ jsx12(
1629
2356
  MapNotice,
1630
2357
  {
1631
2358
  glyph: "\u{1F512}",
@@ -1635,7 +2362,7 @@ function MapWidget({ widget, data }) {
1635
2362
  ) });
1636
2363
  }
1637
2364
  if (!runtime) {
1638
- return /* @__PURE__ */ jsx11(WidgetFrame9, { title, children: /* @__PURE__ */ jsx11(
2365
+ return /* @__PURE__ */ jsx12(WidgetFrame9, { title, children: /* @__PURE__ */ jsx12(
1639
2366
  MapNotice,
1640
2367
  {
1641
2368
  glyph: "\u{1F9E9}",
@@ -1645,10 +2372,10 @@ function MapWidget({ widget, data }) {
1645
2372
  ) });
1646
2373
  }
1647
2374
  if (data.loading && positions.length === 0) {
1648
- return /* @__PURE__ */ jsx11(WidgetFrame9, { title, children: /* @__PURE__ */ jsx11(MapNotice, { heading: "Loading\u2026" }) });
2375
+ return /* @__PURE__ */ jsx12(WidgetFrame9, { title, children: /* @__PURE__ */ jsx12(MapNotice, { heading: "Loading\u2026" }) });
1649
2376
  }
1650
2377
  if (positions.length === 0) {
1651
- return /* @__PURE__ */ jsx11(WidgetFrame9, { title, children: /* @__PURE__ */ jsx11(
2378
+ return /* @__PURE__ */ jsx12(WidgetFrame9, { title, children: /* @__PURE__ */ jsx12(
1652
2379
  MapNotice,
1653
2380
  {
1654
2381
  heading: data.deviceTokens.length === 0 ? "No devices selected" : "No positions reported",
@@ -1656,11 +2383,11 @@ function MapWidget({ widget, data }) {
1656
2383
  }
1657
2384
  ) });
1658
2385
  }
1659
- return /* @__PURE__ */ jsx11(WidgetFrame9, { title, bodyStyle: { padding: 0 }, children: /* @__PURE__ */ jsx11(LiveMap, { basemap, positions, runtime }) });
2386
+ return /* @__PURE__ */ jsx12(WidgetFrame9, { title, flush: true, children: /* @__PURE__ */ jsx12(LiveMap, { basemap, positions, runtime }) });
1660
2387
  }
1661
2388
  function PlainPanel({ positions, note }) {
1662
2389
  const points = projectToPanel(positions);
1663
- return /* @__PURE__ */ jsxs6(
2390
+ return /* @__PURE__ */ jsxs7(
1664
2391
  "div",
1665
2392
  {
1666
2393
  "data-testid": "map-plain-panel",
@@ -1669,10 +2396,10 @@ function PlainPanel({ positions, note }) {
1669
2396
  width: "100%",
1670
2397
  height: "100%",
1671
2398
  overflow: "hidden",
1672
- background: css8("muted")
2399
+ background: css9("muted")
1673
2400
  },
1674
2401
  children: [
1675
- positions.map((position, i) => /* @__PURE__ */ jsx11(
2402
+ positions.map((position, i) => /* @__PURE__ */ jsx12(
1676
2403
  Marker,
1677
2404
  {
1678
2405
  position,
@@ -1685,7 +2412,7 @@ function PlainPanel({ positions, note }) {
1685
2412
  },
1686
2413
  position.deviceToken
1687
2414
  )),
1688
- /* @__PURE__ */ jsx11(
2415
+ /* @__PURE__ */ jsx12(
1689
2416
  "div",
1690
2417
  {
1691
2418
  "data-testid": "map-no-tiles-note",
@@ -1695,11 +2422,11 @@ function PlainPanel({ positions, note }) {
1695
2422
  right: 0,
1696
2423
  bottom: 0,
1697
2424
  padding: "4px 8px",
1698
- fontSize: 10,
2425
+ fontSize: fontVar8("sm"),
1699
2426
  lineHeight: 1.3,
1700
2427
  textAlign: "center",
1701
- color: css8("muted-foreground"),
1702
- background: css8("card"),
2428
+ color: css9("muted-foreground"),
2429
+ background: css9("card"),
1703
2430
  opacity: 0.9
1704
2431
  },
1705
2432
  children: note
@@ -1717,10 +2444,10 @@ function LiveMap({
1717
2444
  const containerRef = useRef2(null);
1718
2445
  const mapRef = useRef2(null);
1719
2446
  const markersRef = useRef2([]);
1720
- const [status, setStatus] = useState4("loading");
2447
+ const [status, setStatus] = useState6("loading");
1721
2448
  const tileUrl = basemap?.tileUrl ?? null;
1722
2449
  const attribution = basemap?.attribution ?? null;
1723
- useEffect3(() => {
2450
+ useEffect4(() => {
1724
2451
  let cancelled = false;
1725
2452
  setStatus("loading");
1726
2453
  Promise.all([
@@ -1748,7 +2475,7 @@ function LiveMap({
1748
2475
  mapRef.current = null;
1749
2476
  };
1750
2477
  }, [tileUrl, attribution, runtime.workerUrl]);
1751
- useEffect3(() => {
2478
+ useEffect4(() => {
1752
2479
  const map = mapRef.current;
1753
2480
  if (status !== "ready" || !map) return;
1754
2481
  let live = true;
@@ -1777,7 +2504,7 @@ function LiveMap({
1777
2504
  };
1778
2505
  }, [status, positionsKey(positions)]);
1779
2506
  if (status === "unavailable") {
1780
- return /* @__PURE__ */ jsx11(
2507
+ return /* @__PURE__ */ jsx12(
1781
2508
  PlainPanel,
1782
2509
  {
1783
2510
  positions,
@@ -1785,7 +2512,7 @@ function LiveMap({
1785
2512
  }
1786
2513
  );
1787
2514
  }
1788
- return /* @__PURE__ */ jsx11("div", { ref: containerRef, "data-testid": "map-canvas", style: { width: "100%", height: "100%" } });
2515
+ return /* @__PURE__ */ jsx12("div", { ref: containerRef, "data-testid": "map-canvas", style: { width: "100%", height: "100%" } });
1789
2516
  }
1790
2517
  function positionsKey(positions) {
1791
2518
  return positions.map((p) => `${p.deviceToken}:${p.latitude}:${p.longitude}:${p.heading ?? ""}`).join("|");
@@ -1794,8 +2521,8 @@ var MARKER_DOT_STYLE = {
1794
2521
  width: "12px",
1795
2522
  height: "12px",
1796
2523
  borderRadius: "50%",
1797
- background: css8("primary"),
1798
- border: `2px solid ${css8("card")}`,
2524
+ background: css9("primary"),
2525
+ border: `2px solid ${css9("card")}`,
1799
2526
  boxShadow: "0 0 0 1px rgba(0,0,0,0.25)",
1800
2527
  cursor: "default"
1801
2528
  };
@@ -1804,7 +2531,7 @@ function Marker({
1804
2531
  style
1805
2532
  }) {
1806
2533
  const label = describePosition(position);
1807
- return /* @__PURE__ */ jsx11(
2534
+ return /* @__PURE__ */ jsx12(
1808
2535
  "div",
1809
2536
  {
1810
2537
  "data-testid": "map-marker",
@@ -1816,72 +2543,74 @@ function Marker({
1816
2543
  );
1817
2544
  }
1818
2545
  function MapNotice({ glyph, heading, detail }) {
1819
- return /* @__PURE__ */ jsxs6(
2546
+ return /* @__PURE__ */ jsxs7(
1820
2547
  "div",
1821
2548
  {
1822
2549
  style: {
1823
2550
  display: "flex",
1824
2551
  flexDirection: "column",
1825
2552
  alignItems: "center",
1826
- justifyContent: "center",
2553
+ justifyContent: "safe center",
2554
+ overflow: "hidden",
1827
2555
  gap: 4,
1828
2556
  width: "100%",
1829
2557
  height: "100%",
1830
- padding: 12,
1831
2558
  boxSizing: "border-box",
1832
2559
  textAlign: "center",
1833
- color: css8("muted-foreground")
2560
+ color: css9("muted-foreground")
1834
2561
  },
1835
2562
  children: [
1836
- glyph ? /* @__PURE__ */ jsx11("div", { style: { fontSize: 20, lineHeight: 1 }, "aria-hidden": "true", children: glyph }) : null,
1837
- /* @__PURE__ */ jsx11("div", { style: { fontSize: 13, fontWeight: 600 }, children: heading }),
1838
- detail ? /* @__PURE__ */ jsx11("div", { style: { fontSize: 11, opacity: 0.8 }, children: detail }) : null
2563
+ glyph ? /* @__PURE__ */ jsx12("div", { style: { fontSize: fontVar8("xl"), lineHeight: 1 }, "aria-hidden": "true", children: glyph }) : null,
2564
+ /* @__PURE__ */ jsx12("div", { style: { fontSize: fontVar8("md"), fontWeight: 600 }, children: heading }),
2565
+ detail ? /* @__PURE__ */ jsx12("div", { style: { fontSize: fontVar8("sm"), opacity: 0.8 }, children: detail }) : null
1839
2566
  ]
1840
2567
  }
1841
2568
  );
1842
2569
  }
1843
- var mapWidget = defineWidget9({
2570
+ var mapWidget = defineWidget12({
1844
2571
  descriptor: builtinWidgetDescriptors["map"],
1845
2572
  implementation: { kind: "trusted-react", component: MapWidget }
1846
2573
  });
1847
2574
 
1848
2575
  // src/widgets/table.tsx
1849
2576
  import {
1850
- css as css9,
1851
- defineWidget as defineWidget10,
2577
+ css as css10,
2578
+ defineWidget as defineWidget13,
2579
+ fontVar as fontVar9,
2580
+ spaceVar as spaceVar4,
1852
2581
  FlashValue as FlashValue2,
1853
2582
  formatTimestamp as formatTimestamp2,
1854
2583
  optBoolean as optBoolean4,
1855
- optNumber as optNumber6,
1856
- optString as optString11,
2584
+ optNumber as optNumber5,
2585
+ optString as optString10,
1857
2586
  useWidgetTimeZone as useWidgetTimeZone4,
1858
2587
  WidgetFrame as WidgetFrame10
1859
2588
  } from "@devicechain/widget-sdk";
1860
- import { jsx as jsx12, jsxs as jsxs7 } from "react/jsx-runtime";
1861
- var cell2 = { padding: "6px 12px", textAlign: "left" };
2589
+ import { jsx as jsx13, jsxs as jsxs8 } from "react/jsx-runtime";
2590
+ var cell2 = { padding: `${spaceVar4(1)} ${spaceVar4(3)}`, textAlign: "left" };
1862
2591
  var headCell2 = {
1863
2592
  ...cell2,
1864
- fontSize: 11,
2593
+ fontSize: fontVar9("xs"),
1865
2594
  fontWeight: 600,
1866
2595
  textTransform: "uppercase",
1867
2596
  letterSpacing: "0.04em",
1868
- color: css9("muted-foreground"),
1869
- borderBottom: `1px solid ${css9("border")}`
2597
+ color: css10("muted-foreground"),
2598
+ borderBottom: `1px solid ${css10("border")}`
1870
2599
  };
1871
2600
  function Table({ widget, data }) {
1872
2601
  const timeZone = useWidgetTimeZone4();
1873
2602
  const rows = Object.values(data.latest).sort((a, b) => a.name.localeCompare(b.name));
1874
- const precision = optNumber6(widget.options, "precision");
2603
+ const precision = optNumber5(widget.options, "precision");
1875
2604
  const flash = optBoolean4(widget.options, "flashOnChange");
1876
- return /* @__PURE__ */ jsx12(WidgetFrame10, { title: optString11(widget.options, "title"), children: /* @__PURE__ */ jsx12("div", { style: { height: "100%", overflow: "auto" }, children: /* @__PURE__ */ jsxs7("table", { style: { width: "100%", borderCollapse: "collapse", fontSize: 13, color: css9("foreground") }, children: [
1877
- /* @__PURE__ */ jsx12("thead", { children: /* @__PURE__ */ jsxs7("tr", { children: [
1878
- /* @__PURE__ */ jsx12("th", { style: headCell2, children: "Measurement" }),
1879
- /* @__PURE__ */ jsx12("th", { style: headCell2, children: "Value" }),
1880
- /* @__PURE__ */ jsx12("th", { style: headCell2, children: "Time" })
2605
+ return /* @__PURE__ */ jsx13(WidgetFrame10, { title: optString10(widget.options, "title"), flush: true, children: /* @__PURE__ */ jsx13("div", { style: { height: "100%", overflow: "auto" }, children: /* @__PURE__ */ jsxs8("table", { style: { width: "100%", borderCollapse: "collapse", fontSize: fontVar9("md"), color: css10("foreground") }, children: [
2606
+ /* @__PURE__ */ jsx13("thead", { children: /* @__PURE__ */ jsxs8("tr", { children: [
2607
+ /* @__PURE__ */ jsx13("th", { style: headCell2, children: "Measurement" }),
2608
+ /* @__PURE__ */ jsx13("th", { style: headCell2, children: "Value" }),
2609
+ /* @__PURE__ */ jsx13("th", { style: headCell2, children: "Time" })
1881
2610
  ] }) }),
1882
- /* @__PURE__ */ jsx12("tbody", { children: rows.length === 0 ? /* @__PURE__ */ jsx12("tr", { children: /* @__PURE__ */ jsx12("td", { colSpan: 3, style: { ...cell2, textAlign: "center", color: css9("muted-foreground") }, children: "No data yet" }) }) : rows.map((row) => /* @__PURE__ */ jsxs7("tr", { children: [
1883
- /* @__PURE__ */ jsx12("td", { style: cell2, children: row.name }),
1884
- /* @__PURE__ */ jsx12("td", { style: { ...cell2, fontVariantNumeric: "tabular-nums" }, children: /* @__PURE__ */ jsx12(
2611
+ /* @__PURE__ */ jsx13("tbody", { children: rows.length === 0 ? /* @__PURE__ */ jsx13("tr", { children: /* @__PURE__ */ jsx13("td", { colSpan: 3, style: { ...cell2, textAlign: "center", color: css10("muted-foreground") }, children: "No data yet" }) }) : rows.map((row) => /* @__PURE__ */ jsxs8("tr", { children: [
2612
+ /* @__PURE__ */ jsx13("td", { style: cell2, children: row.name }),
2613
+ /* @__PURE__ */ jsx13("td", { style: { ...cell2, fontVariantNumeric: "tabular-nums" }, children: /* @__PURE__ */ jsx13(
1885
2614
  FlashValue2,
1886
2615
  {
1887
2616
  value: row.value,
@@ -1890,36 +2619,105 @@ function Table({ widget, data }) {
1890
2619
  identity: `${row.deviceToken}:${row.name}`
1891
2620
  }
1892
2621
  ) }),
1893
- /* @__PURE__ */ jsx12("td", { style: { ...cell2, color: css9("muted-foreground") }, children: formatTimestamp2(row.occurredTime, timeZone) })
2622
+ /* @__PURE__ */ jsx13("td", { style: { ...cell2, color: css10("muted-foreground") }, children: formatTimestamp2(row.occurredTime, timeZone) })
1894
2623
  ] }, row.name)) })
1895
2624
  ] }) }) });
1896
2625
  }
1897
- var tableWidget = defineWidget10({
2626
+ var tableWidget = defineWidget13({
1898
2627
  descriptor: builtinWidgetDescriptors["table"],
1899
2628
  implementation: { kind: "trusted-react", component: Table }
1900
2629
  });
1901
2630
 
1902
2631
  // src/widgets/time-series-chart.tsx
1903
- import { useMemo as useMemo3 } from "react";
2632
+ import { useMemo as useMemo4 } from "react";
2633
+ import { isValidSpanSeconds, spanLabel } from "@devicechain/dashboards";
1904
2634
  import {
1905
- defineWidget as defineWidget11,
1906
- optString as optString12,
1907
- useElementSize as useElementSize4,
2635
+ css as css11,
2636
+ defineWidget as defineWidget14,
2637
+ fontVar as fontVar10,
2638
+ formatDateTime as formatDateTime3,
2639
+ formatTimestamp as formatTimestamp3,
2640
+ optNumber as optNumber6,
2641
+ optString as optString11,
2642
+ optThresholds as optThresholds3,
2643
+ spaceVar as spaceVar5,
2644
+ useWidgetScale as useWidgetScale3,
2645
+ useWidgetTimeRange,
2646
+ useWidgetTimeZone as useWidgetTimeZone5,
2647
+ viewerLocale,
1908
2648
  WidgetFrame as WidgetFrame11
1909
2649
  } from "@devicechain/widget-sdk";
1910
2650
  import { buildLineOption, EChart as EChart2, useChartTheme as useChartTheme2 } from "@devicechain/widget-sdk/echarts";
1911
- import { jsx as jsx13 } from "react/jsx-runtime";
2651
+ import { jsx as jsx14, jsxs as jsxs9 } from "react/jsx-runtime";
1912
2652
  function TimeSeriesChart({ widget, data }) {
2653
+ return /* @__PURE__ */ jsx14(WidgetFrame11, { title: optString11(widget.options, "title"), flush: true, children: /* @__PURE__ */ jsx14(TimeSeriesBody, { widget, data }) });
2654
+ }
2655
+ function TimeSeriesBody({ widget, data }) {
1913
2656
  const theme = useChartTheme2();
1914
- const [sizeRef, size] = useElementSize4();
2657
+ const { size, scale } = useWidgetScale3();
1915
2658
  const measurements = widget.datasource?.measurements;
1916
- const option = useMemo3(
1917
- () => buildLineOption(data.samples, theme, { measurements }, size),
1918
- [data.samples, theme, measurements, size.width, size.height]
2659
+ const unit = optString11(widget.options, "unit");
2660
+ const precision = optNumber6(widget.options, "precision");
2661
+ const thresholds = useMemo4(() => optThresholds3(widget.options, "thresholds"), [widget.options]);
2662
+ const option = useMemo4(
2663
+ () => buildLineOption(data.samples, theme, { measurements, unit, precision, thresholds, scale }, size),
2664
+ [data.samples, theme, measurements, unit, precision, thresholds, scale, size]
2665
+ );
2666
+ return /* @__PURE__ */ jsxs9("div", { style: { position: "relative", width: "100%", height: "100%" }, children: [
2667
+ /* @__PURE__ */ jsx14(EChart2, { option }),
2668
+ /* @__PURE__ */ jsx14(SpanBadge, { widget, data })
2669
+ ] });
2670
+ }
2671
+ var SPAN_STRINGS = {
2672
+ en: {
2673
+ from: (time) => `from ${time}`,
2674
+ pinned: (span) => `Pinned to ${span}: the board's time range does not apply to this chart.`,
2675
+ cut: (time, span) => `Showing from ${time}, not the whole ${span}: older samples were dropped to bound memory.`
2676
+ },
2677
+ es: {
2678
+ from: (time) => `desde ${time}`,
2679
+ pinned: (span) => `Fijado a ${span}: el intervalo de tiempo del panel no se aplica a este gr\xE1fico.`,
2680
+ cut: (time, span) => `Se muestra desde ${time}, no todo el intervalo de ${span}: se descartaron las muestras m\xE1s antiguas para acotar la memoria.`
2681
+ }
2682
+ };
2683
+ var DAY_MS = 864e5;
2684
+ function SpanBadge({ widget, data }) {
2685
+ const range = useWidgetTimeRange();
2686
+ const timeZone = useWidgetTimeZone5();
2687
+ const pinned = optNumber6(widget.options, "windowSeconds");
2688
+ const span = isValidSpanSeconds(pinned) ? pinned : range?.spanSeconds;
2689
+ const showSpan = range !== void 0 && span !== range.spanSeconds;
2690
+ const cutAt = data.truncatedBefore;
2691
+ if (!showSpan && !cutAt) return null;
2692
+ const words = viewerLocale()?.toLowerCase().startsWith("es") ? SPAN_STRINGS.es : SPAN_STRINGS.en;
2693
+ const label = span === void 0 ? "" : spanLabel(span);
2694
+ const end = range?.paused && range.pausedAt !== void 0 ? range.pausedAt : Date.now();
2695
+ const time = cutAt ? end - Date.parse(cutAt) > DAY_MS ? formatDateTime3(cutAt, timeZone) : formatTimestamp3(cutAt, timeZone) : "";
2696
+ const text = [showSpan ? label : "", cutAt ? words.from(time) : ""].filter(Boolean).join(" \xB7 ");
2697
+ const title = [showSpan ? words.pinned(label) : "", cutAt ? words.cut(time, label) : ""].filter(Boolean).join(" ");
2698
+ return /* @__PURE__ */ jsx14(
2699
+ "span",
2700
+ {
2701
+ "data-chart-span": showSpan ? label : "",
2702
+ "data-chart-truncated": cutAt ? "" : void 0,
2703
+ title,
2704
+ style: {
2705
+ position: "absolute",
2706
+ top: spaceVar5(1),
2707
+ right: spaceVar5(2),
2708
+ padding: `0 ${spaceVar5(1)}`,
2709
+ fontSize: fontVar10("xs"),
2710
+ whiteSpace: "nowrap",
2711
+ color: css11("muted-foreground"),
2712
+ background: css11("muted"),
2713
+ borderRadius: 4,
2714
+ pointerEvents: "auto"
2715
+ },
2716
+ children: text
2717
+ }
1919
2718
  );
1920
- return /* @__PURE__ */ jsx13(WidgetFrame11, { title: optString12(widget.options, "title"), children: /* @__PURE__ */ jsx13("div", { ref: sizeRef, style: { width: "100%", height: "100%" }, children: /* @__PURE__ */ jsx13(EChart2, { option }) }) });
1921
2719
  }
1922
- var timeSeriesChartWidget = defineWidget11({
2720
+ var timeSeriesChartWidget = defineWidget14({
1923
2721
  descriptor: builtinWidgetDescriptors["timeseries-chart"],
1924
2722
  implementation: { kind: "trusted-react", component: TimeSeriesChart }
1925
2723
  });
@@ -1936,7 +2734,20 @@ var builtinWidgetDefinitions = [
1936
2734
  alarmCountWidget,
1937
2735
  commandButtonWidget,
1938
2736
  entitySelectorWidget,
1939
- mapWidget
2737
+ mapWidget,
2738
+ linearGaugeWidget,
2739
+ levelGaugeWidget,
2740
+ batteryWidget,
2741
+ statusLightWidget,
2742
+ signalBarsWidget,
2743
+ compassWidget,
2744
+ progressRingWidget,
2745
+ commandSliderWidget,
2746
+ commandToggleWidget,
2747
+ statCardWidget,
2748
+ barChartWidget,
2749
+ pieChartWidget,
2750
+ fleetTableWidget
1940
2751
  ];
1941
2752
  function createBuiltinWidgetRegistry() {
1942
2753
  return createWidgetRegistry(builtinWidgetDefinitions);
@@ -1951,7 +2762,7 @@ function defaultWidgetRegistry() {
1951
2762
 
1952
2763
  // src/connected-widget.tsx
1953
2764
  import {
1954
- optString as optString13,
2765
+ optString as optString12,
1955
2766
  useAlarmStream,
1956
2767
  useCommandStream,
1957
2768
  useDatasourceAvailability,
@@ -1963,7 +2774,7 @@ import {
1963
2774
  } from "@devicechain/widget-sdk";
1964
2775
 
1965
2776
  // src/widget-containment.tsx
1966
- import { Component, createContext as createContext3, useCallback, useContext as useContext3, useState as useState5 } from "react";
2777
+ import { Component, createContext as createContext3, useCallback, useContext as useContext3, useState as useState7 } from "react";
1967
2778
  import { WidgetFrame as WidgetFrame12 } from "@devicechain/widget-sdk";
1968
2779
 
1969
2780
  // src/widget-host.ts
@@ -2022,7 +2833,7 @@ function narrowCandidates(candidates, descriptor, report) {
2022
2833
  }
2023
2834
 
2024
2835
  // src/widget-containment.tsx
2025
- import { jsx as jsx14, jsxs as jsxs8 } from "react/jsx-runtime";
2836
+ import { jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
2026
2837
  var MESSAGE_STYLE = {
2027
2838
  display: "flex",
2028
2839
  flexDirection: "column",
@@ -2066,23 +2877,23 @@ var WidgetErrorBoundary = class extends Component {
2066
2877
  render() {
2067
2878
  const { error } = this.state;
2068
2879
  if (!error) return this.props.children;
2069
- return /* @__PURE__ */ jsx14(WidgetFrame12, { title: this.props.title, children: /* @__PURE__ */ jsxs8("div", { style: MESSAGE_STYLE, role: "alert", children: [
2070
- /* @__PURE__ */ jsx14("div", { style: { fontSize: 20, lineHeight: 1 }, "aria-hidden": "true", children: "\u26A0" }),
2071
- /* @__PURE__ */ jsx14("div", { style: { fontSize: 13, fontWeight: 600 }, children: "This widget failed to render" }),
2072
- /* @__PURE__ */ jsx14("div", { style: { fontSize: 11, opacity: 0.8, wordBreak: "break-word" }, children: error.message }),
2073
- /* @__PURE__ */ jsx14("button", { type: "button", style: BUTTON_STYLE, onClick: this.reset, children: "Reset widget" })
2880
+ return /* @__PURE__ */ jsx15(WidgetFrame12, { title: this.props.title, children: /* @__PURE__ */ jsxs10("div", { style: MESSAGE_STYLE, role: "alert", children: [
2881
+ /* @__PURE__ */ jsx15("div", { style: { fontSize: 20, lineHeight: 1 }, "aria-hidden": "true", children: "\u26A0" }),
2882
+ /* @__PURE__ */ jsx15("div", { style: { fontSize: 13, fontWeight: 600 }, children: "This widget failed to render" }),
2883
+ /* @__PURE__ */ jsx15("div", { style: { fontSize: 11, opacity: 0.8, wordBreak: "break-word" }, children: error.message }),
2884
+ /* @__PURE__ */ jsx15("button", { type: "button", style: BUTTON_STYLE, onClick: this.reset, children: "Reset widget" })
2074
2885
  ] }) });
2075
2886
  }
2076
2887
  };
2077
2888
  function WidgetContainment({ title, resetKey, children }) {
2078
- const [messages, setMessages] = useState5([]);
2889
+ const [messages, setMessages] = useState7([]);
2079
2890
  const report = useCallback((message) => {
2080
2891
  setMessages((prev) => prev.includes(message) ? prev : [...prev, message].slice(-3));
2081
2892
  }, []);
2082
2893
  const clear = useCallback(() => setMessages([]), []);
2083
- return /* @__PURE__ */ jsxs8("div", { style: { position: "relative", width: "100%", height: "100%" }, children: [
2084
- /* @__PURE__ */ jsx14(ReportContext.Provider, { value: report, children: /* @__PURE__ */ jsx14(WidgetErrorBoundary, { title, resetKey, onReset: clear, children }) }),
2085
- messages.length > 0 && /* @__PURE__ */ jsxs8(
2894
+ return /* @__PURE__ */ jsxs10("div", { style: { position: "relative", width: "100%", height: "100%" }, children: [
2895
+ /* @__PURE__ */ jsx15(ReportContext.Provider, { value: report, children: /* @__PURE__ */ jsx15(WidgetErrorBoundary, { title, resetKey, onReset: clear, children }) }),
2896
+ messages.length > 0 && /* @__PURE__ */ jsxs10(
2086
2897
  "div",
2087
2898
  {
2088
2899
  role: "status",
@@ -2101,8 +2912,8 @@ function WidgetContainment({ title, resetKey, children }) {
2101
2912
  alignItems: "center"
2102
2913
  },
2103
2914
  children: [
2104
- /* @__PURE__ */ jsx14("span", { style: { flex: 1, wordBreak: "break-word" }, children: messages.join(" \xB7 ") }),
2105
- /* @__PURE__ */ jsx14("button", { type: "button", "aria-label": "Dismiss", onClick: clear, style: { cursor: "pointer" }, children: "\xD7" })
2915
+ /* @__PURE__ */ jsx15("span", { style: { flex: 1, wordBreak: "break-word" }, children: messages.join(" \xB7 ") }),
2916
+ /* @__PURE__ */ jsx15("button", { type: "button", "aria-label": "Dismiss", onClick: clear, style: { cursor: "pointer" }, children: "\xD7" })
2106
2917
  ]
2107
2918
  }
2108
2919
  )
@@ -2110,12 +2921,12 @@ function WidgetContainment({ title, resetKey, children }) {
2110
2921
  }
2111
2922
 
2112
2923
  // src/connected-widget.tsx
2113
- import { jsx as jsx15, jsxs as jsxs9 } from "react/jsx-runtime";
2924
+ import { jsx as jsx16, jsxs as jsxs11 } from "react/jsx-runtime";
2114
2925
  function ConnectedWidget(props) {
2115
2926
  const { widget } = props;
2116
2927
  const registry = props.registry ?? defaultWidgetRegistry();
2117
2928
  const resetKey = JSON.stringify([widget.type, widget.options ?? null, widget.datasource ?? null]);
2118
- return /* @__PURE__ */ jsx15(WidgetContainment, { title: optString13(widget.options, "title"), resetKey, children: /* @__PURE__ */ jsx15(ConnectedWidgetBody, { ...props, registry }) });
2929
+ return /* @__PURE__ */ jsx16(WidgetContainment, { title: optString12(widget.options, "title"), resetKey, children: /* @__PURE__ */ jsx16(ConnectedWidgetBody, { ...props, registry }) });
2119
2930
  }
2120
2931
  function ConnectedWidgetBody({ widget, hub, registry, actions, initialSamples }) {
2121
2932
  const unresolved = widget.type === UNKNOWN_WIDGET_TYPE;
@@ -2124,13 +2935,13 @@ function ConnectedWidgetBody({ widget, hub, registry, actions, initialSamples })
2124
2935
  const readsDatasource = descriptor !== void 0 && descriptor.datasource.mode !== "none";
2125
2936
  const availability = useDatasourceAvailability(hub, readsDatasource ? widget.datasource : void 0);
2126
2937
  if (availability === "unavailable") {
2127
- return /* @__PURE__ */ jsx15(WidgetUnavailableFrame, { widget });
2938
+ return /* @__PURE__ */ jsx16(WidgetUnavailableFrame, { widget });
2128
2939
  }
2129
2940
  if (widget.type === UNKNOWN_WIDGET_TYPE) {
2130
- return /* @__PURE__ */ jsx15(UnresolvedWidgetFrame, { storedType: widget.raw.type, reason: widget.reason });
2941
+ return /* @__PURE__ */ jsx16(UnresolvedWidgetFrame, { storedType: widget.raw.type, reason: widget.reason });
2131
2942
  }
2132
2943
  if (!descriptor || !implementation) {
2133
- return /* @__PURE__ */ jsx15(
2944
+ return /* @__PURE__ */ jsx16(
2134
2945
  UnresolvedWidgetFrame,
2135
2946
  {
2136
2947
  storedType: widget.type,
@@ -2138,7 +2949,7 @@ function ConnectedWidgetBody({ widget, hub, registry, actions, initialSamples })
2138
2949
  }
2139
2950
  );
2140
2951
  }
2141
- return /* @__PURE__ */ jsx15(HostScope, { descriptor, actions, children: (hostActions) => /* @__PURE__ */ jsx15(
2952
+ return /* @__PURE__ */ jsx16(HostScope, { descriptor, actions, children: (hostActions) => /* @__PURE__ */ jsx16(
2142
2953
  ChannelBody,
2143
2954
  {
2144
2955
  widget,
@@ -2158,57 +2969,91 @@ function HostScope({
2158
2969
  const report = useReportRejection();
2159
2970
  const select = useWidgetSelect3();
2160
2971
  const candidates = useWidgetCandidates();
2161
- const narrowed = useMemo4(() => narrowActions(actions, descriptor, report), [actions, descriptor, report]);
2162
- const narrowedSelect = useMemo4(() => narrowSelect(select, descriptor), [select, descriptor]);
2163
- const narrowedCandidates = useMemo4(
2972
+ const narrowed = useMemo5(() => narrowActions(actions, descriptor, report), [actions, descriptor, report]);
2973
+ const narrowedSelect = useMemo5(() => narrowSelect(select, descriptor), [select, descriptor]);
2974
+ const narrowedCandidates = useMemo5(
2164
2975
  () => narrowCandidates(candidates, descriptor, report),
2165
2976
  [candidates, descriptor, report]
2166
2977
  );
2167
- return /* @__PURE__ */ jsx15(WidgetSelectProvider, { select: narrowedSelect, children: /* @__PURE__ */ jsx15(WidgetCandidatesProvider, { candidates: narrowedCandidates, children: children(narrowed) }) });
2978
+ return /* @__PURE__ */ jsx16(WidgetSelectProvider, { select: narrowedSelect, children: /* @__PURE__ */ jsx16(WidgetCandidatesProvider, { candidates: narrowedCandidates, children: children(narrowed) }) });
2168
2979
  }
2169
2980
  function ChannelBody(props) {
2170
2981
  switch (props.descriptor.channel) {
2171
2982
  case "alarm":
2172
- return /* @__PURE__ */ jsx15(AlarmConnectedWidget, { ...props });
2983
+ return /* @__PURE__ */ jsx16(AlarmConnectedWidget, { ...props });
2173
2984
  case "control":
2174
- return /* @__PURE__ */ jsx15(ControlConnectedWidget, { ...props });
2985
+ return /* @__PURE__ */ jsx16(ControlConnectedWidget, { ...props });
2175
2986
  case "location":
2176
- return /* @__PURE__ */ jsx15(LocationConnectedWidget, { ...props });
2987
+ return /* @__PURE__ */ jsx16(LocationConnectedWidget, { ...props });
2988
+ case "aggregate":
2989
+ return /* @__PURE__ */ jsx16(AggregateConnectedWidget, { ...props });
2177
2990
  case "selection":
2178
2991
  case "none":
2179
- return /* @__PURE__ */ jsx15(StaticConnectedWidget, { ...props });
2992
+ return /* @__PURE__ */ jsx16(StaticConnectedWidget, { ...props });
2180
2993
  case "measurement":
2181
- return /* @__PURE__ */ jsx15(MeasurementConnectedWidget, { ...props });
2994
+ return /* @__PURE__ */ jsx16(MeasurementConnectedWidget, { ...props });
2182
2995
  }
2183
2996
  }
2184
2997
  function StaticConnectedWidget({ widget, Component: Component2, actions }) {
2185
- return /* @__PURE__ */ jsx15(Component2, { widget, data: null, actions });
2998
+ return /* @__PURE__ */ jsx16(Component2, { widget, data: null, actions });
2999
+ }
3000
+ var FleetFeed = lazyChunk(() => import("./chunks/fleet-feed-FK4ZMKIK.js")).component("default");
3001
+ function MeasurementConnectedWidget(props) {
3002
+ const { widget, hub, descriptor, Component: Component2, actions } = props;
3003
+ const members = membersMode(descriptor, widget);
3004
+ if (members || widget.datasource?.kind === "anchor") {
3005
+ return /* @__PURE__ */ jsx16(FleetFeed, { hub, widget, descriptor, Component: Component2, actions, ErrorFrame: WidgetErrorFrame, refuse: !members });
3006
+ }
3007
+ return /* @__PURE__ */ jsx16(StreamConnectedWidget, { ...props });
2186
3008
  }
2187
- function MeasurementConnectedWidget({ widget, hub, descriptor, Component: Component2, actions, initialSamples }) {
3009
+ function StreamConnectedWidget({ widget, hub, descriptor, Component: Component2, actions, initialSamples }) {
3010
+ const range = useWidgetTimeRange2();
3011
+ const now = useMemo5(() => hub.now ? () => hub.now() : Date.now, [hub]);
2188
3012
  const data = useMeasurementStream(hub, widget.datasource, {
2189
3013
  window: measurementWindow(descriptor, widget),
3014
+ windowSeconds: measurementSpan(descriptor, widget, range?.spanSeconds),
3015
+ pausedAt: range?.paused ? range.pausedAt : void 0,
3016
+ now,
2190
3017
  initialSamples
2191
3018
  });
2192
- if (data.error) return /* @__PURE__ */ jsx15(WidgetErrorFrame, { widget, error: data.error });
2193
- return /* @__PURE__ */ jsx15(Component2, { widget, data, actions });
3019
+ const freshness = useMemo5(() => {
3020
+ if (descriptor.freshness !== true) return void 0;
3021
+ const pinned = optString12(widget.options, "measurement");
3022
+ const names = pinned ? [pinned] : widget.datasource?.measurements;
3023
+ return {
3024
+ at: newestSampleTime(data.latest, names),
3025
+ staleAfterSeconds: resolveStaleAfterSeconds(widget.options)
3026
+ };
3027
+ }, [descriptor, widget.options, widget.datasource, data.latest]);
3028
+ if (data.error) return /* @__PURE__ */ jsx16(WidgetErrorFrame, { widget, error: data.error });
3029
+ return /* @__PURE__ */ jsx16(WidgetFreshnessProvider, { freshness, children: /* @__PURE__ */ jsx16(Component2, { widget, data, actions }) });
3030
+ }
3031
+ var AggregateFeed = lazyChunk(() => import("./chunks/aggregate-feed-6ZYKOQOB.js")).component("default");
3032
+ function AggregateConnectedWidget({ widget, hub, descriptor, Component: Component2, actions }) {
3033
+ const props = { hub, widget, descriptor, Component: Component2, actions, ErrorFrame: WidgetErrorFrame };
3034
+ return widget.datasource?.kind === "anchor" ? /* @__PURE__ */ jsx16(FleetFeed, { ...props, refuse: true }) : /* @__PURE__ */ jsx16(AggregateFeed, { ...props });
2194
3035
  }
2195
3036
  function AlarmConnectedWidget({ widget, hub, descriptor, Component: Component2, actions }) {
2196
- const data = useAlarmStream(hub, alarmSubscription(descriptor, widget));
3037
+ const range = useWidgetTimeRange2();
3038
+ const live = useAlarmStream(hub, alarmSubscription(descriptor, widget));
3039
+ const data = useHeld(live, range?.paused === true, (s) => !s.loading);
2197
3040
  if (data.error && data.alarms.length === 0 && data.total === 0) {
2198
- return /* @__PURE__ */ jsx15(WidgetErrorFrame, { widget, error: data.error });
3041
+ return /* @__PURE__ */ jsx16(WidgetErrorFrame, { widget, error: data.error });
2199
3042
  }
2200
- return /* @__PURE__ */ jsx15(Component2, { widget, data, actions });
3043
+ return /* @__PURE__ */ jsx16(Component2, { widget, data, actions });
2201
3044
  }
2202
3045
  function ControlConnectedWidget({ widget, hub, descriptor, Component: Component2, actions }) {
2203
3046
  const data = useCommandStream(hub, commandSubscription(descriptor, widget));
2204
- return /* @__PURE__ */ jsx15(Component2, { widget, data, actions });
3047
+ return /* @__PURE__ */ jsx16(Component2, { widget, data, actions });
2205
3048
  }
2206
3049
  function LocationConnectedWidget({ widget, hub, Component: Component2, actions }) {
2207
- const data = useLocationStream(hub, { datasource: widget.datasource });
3050
+ const range = useWidgetTimeRange2();
3051
+ const live = useLocationStream(hub, { datasource: widget.datasource });
3052
+ const data = useHeld(live, range?.paused === true, (s) => !s.loading);
2208
3053
  if (data.error && data.locations.length === 0) {
2209
- return /* @__PURE__ */ jsx15(WidgetErrorFrame, { widget, error: data.error });
3054
+ return /* @__PURE__ */ jsx16(WidgetErrorFrame, { widget, error: data.error });
2210
3055
  }
2211
- return /* @__PURE__ */ jsx15(Component2, { widget, data, actions });
3056
+ return /* @__PURE__ */ jsx16(Component2, { widget, data, actions });
2212
3057
  }
2213
3058
  function UnresolvedWidgetFrame({
2214
3059
  storedType: stored,
@@ -2216,7 +3061,7 @@ function UnresolvedWidgetFrame({
2216
3061
  }) {
2217
3062
  const storedType = typeof stored === "string" ? stored : "";
2218
3063
  const heading = (reason?.code ?? "unregistered") === "unregistered" ? "This widget needs a newer viewer" : "This widget cannot be shown here";
2219
- return /* @__PURE__ */ jsx15(WidgetFrame13, { children: /* @__PURE__ */ jsxs9(
3064
+ return /* @__PURE__ */ jsx16(WidgetFrame13, { children: /* @__PURE__ */ jsxs11(
2220
3065
  "div",
2221
3066
  {
2222
3067
  style: {
@@ -2233,18 +3078,18 @@ function UnresolvedWidgetFrame({
2233
3078
  color: "hsl(var(--muted-foreground))"
2234
3079
  },
2235
3080
  children: [
2236
- /* @__PURE__ */ jsx15("div", { style: { fontSize: 13, fontWeight: 600 }, children: heading }),
2237
- storedType && !reason?.message.includes(storedType) && /* @__PURE__ */ jsxs9("div", { style: { fontSize: 11, opacity: 0.8 }, children: [
3081
+ /* @__PURE__ */ jsx16("div", { style: { fontSize: 13, fontWeight: 600 }, children: heading }),
3082
+ storedType && !reason?.message.includes(storedType) && /* @__PURE__ */ jsxs11("div", { style: { fontSize: 11, opacity: 0.8 }, children: [
2238
3083
  "Widget type: ",
2239
3084
  storedType
2240
3085
  ] }),
2241
- reason && /* @__PURE__ */ jsx15("div", { style: { fontSize: 11, opacity: 0.8, wordBreak: "break-word" }, children: reason.message })
3086
+ reason && /* @__PURE__ */ jsx16("div", { style: { fontSize: 11, opacity: 0.8, wordBreak: "break-word" }, children: reason.message })
2242
3087
  ]
2243
3088
  }
2244
3089
  ) });
2245
3090
  }
2246
3091
  function WidgetUnavailableFrame({ widget }) {
2247
- return /* @__PURE__ */ jsx15(WidgetFrame13, { title: optString13(widget.options, "title"), children: /* @__PURE__ */ jsxs9(
3092
+ return /* @__PURE__ */ jsx16(WidgetFrame13, { title: optString12(widget.options, "title"), children: /* @__PURE__ */ jsxs11(
2248
3093
  "div",
2249
3094
  {
2250
3095
  style: {
@@ -2261,15 +3106,15 @@ function WidgetUnavailableFrame({ widget }) {
2261
3106
  color: "hsl(var(--muted-foreground))"
2262
3107
  },
2263
3108
  children: [
2264
- /* @__PURE__ */ jsx15("div", { style: { fontSize: 20, lineHeight: 1 }, "aria-hidden": "true", children: "\u2298" }),
2265
- /* @__PURE__ */ jsx15("div", { style: { fontSize: 13, fontWeight: 600 }, children: "Device unavailable" }),
2266
- /* @__PURE__ */ jsx15("div", { style: { fontSize: 11, opacity: 0.8 }, children: "The bound device no longer exists." })
3109
+ /* @__PURE__ */ jsx16("div", { style: { fontSize: 20, lineHeight: 1 }, "aria-hidden": "true", children: "\u2298" }),
3110
+ /* @__PURE__ */ jsx16("div", { style: { fontSize: 13, fontWeight: 600 }, children: "Device unavailable" }),
3111
+ /* @__PURE__ */ jsx16("div", { style: { fontSize: 11, opacity: 0.8 }, children: "The bound device no longer exists." })
2267
3112
  ]
2268
3113
  }
2269
3114
  ) });
2270
3115
  }
2271
3116
  function WidgetErrorFrame({ widget, error }) {
2272
- return /* @__PURE__ */ jsx15(WidgetFrame13, { title: optString13(widget.options, "title"), children: /* @__PURE__ */ jsxs9(
3117
+ return /* @__PURE__ */ jsx16(WidgetFrame13, { title: optString12(widget.options, "title"), children: /* @__PURE__ */ jsxs11(
2273
3118
  "div",
2274
3119
  {
2275
3120
  style: {
@@ -2286,9 +3131,9 @@ function WidgetErrorFrame({ widget, error }) {
2286
3131
  color: "hsl(var(--muted-foreground))"
2287
3132
  },
2288
3133
  children: [
2289
- /* @__PURE__ */ jsx15("div", { style: { fontSize: 20, lineHeight: 1 }, children: "\u26A0" }),
2290
- /* @__PURE__ */ jsx15("div", { style: { fontSize: 13, fontWeight: 600 }, children: "Data unavailable" }),
2291
- /* @__PURE__ */ jsx15("div", { style: { fontSize: 11, opacity: 0.8, wordBreak: "break-word" }, children: String(error) })
3134
+ /* @__PURE__ */ jsx16("div", { style: { fontSize: 20, lineHeight: 1 }, children: "\u26A0" }),
3135
+ /* @__PURE__ */ jsx16("div", { style: { fontSize: 13, fontWeight: 600 }, children: "Data unavailable" }),
3136
+ /* @__PURE__ */ jsx16("div", { style: { fontSize: 11, opacity: 0.8, wordBreak: "break-word" }, children: String(error) })
2292
3137
  ]
2293
3138
  }
2294
3139
  ) });
@@ -2302,7 +3147,7 @@ import {
2302
3147
  WidgetThemeRootProvider,
2303
3148
  WidgetTimeZoneProvider
2304
3149
  } from "@devicechain/widget-sdk";
2305
- import { jsx as jsx16 } from "react/jsx-runtime";
3150
+ import { Fragment as Fragment6, jsx as jsx17, jsxs as jsxs12 } from "react/jsx-runtime";
2306
3151
  function DashboardRenderer({
2307
3152
  definition,
2308
3153
  hub,
@@ -2314,57 +3159,164 @@ function DashboardRenderer({
2314
3159
  select,
2315
3160
  candidates,
2316
3161
  timeZone,
2317
- themeRoot
3162
+ themeRoot,
3163
+ density,
3164
+ timeRange
2318
3165
  }) {
2319
- const breakpoint = useActiveBreakpoint(definition.canvas.breakpoints);
2320
- const widgets = useMemo5(() => resolveWidgets(definition.widgets, bindings), [definition.widgets, bindings]);
3166
+ const widgets = useMemo6(() => resolveWidgets(definition.widgets, bindings), [definition.widgets, bindings]);
2321
3167
  const registry = registryProp ?? defaultWidgetRegistry();
2322
- const histories = useWidgetHistories(widgets, seedHistory, registry);
2323
- const { grid, background: bg } = definition.canvas;
2324
- const rowGap = typeof grid.gap === "number" ? grid.gap : grid.gap.row;
2325
- const colGap = typeof grid.gap === "number" ? grid.gap : grid.gap.col;
3168
+ const defaultSeconds = definition.timeRange?.defaultSeconds;
3169
+ const range = useMemo6(
3170
+ () => timeRange ?? initialTimeRange(defaultSeconds),
3171
+ [timeRange, defaultSeconds]
3172
+ );
3173
+ const histories = useWidgetHistories(widgets, seedHistory, registry, range);
3174
+ const { grid, background: bg, frame: canvasFrame } = definition.canvas;
2326
3175
  const effectiveSizing = sizing ?? definition.canvas.sizing;
2327
- return /* @__PURE__ */ jsx16("div", { style: sizingStyle(effectiveSizing, bg), children: /* @__PURE__ */ jsx16(WidgetThemeRootProvider, { root: themeRoot, children: /* @__PURE__ */ jsx16(WidgetTimeZoneProvider, { timeZone, children: /* @__PURE__ */ jsx16(WidgetSelectProvider2, { select, children: /* @__PURE__ */ jsx16(WidgetCandidatesProvider2, { candidates, children: /* @__PURE__ */ jsx16(
3176
+ const effectiveDensity = density ?? definition.canvas.density ?? DEFAULT_CANVAS_DENSITY;
3177
+ const scale = densityScale(effectiveDensity);
3178
+ const { canvas } = definition;
3179
+ const shapeKey = useCallback2((width) => JSON.stringify(layoutShape(canvas, width, scale)), [canvas, scale]);
3180
+ const [boardRef, containerWidth] = useContainerWidth(shapeKey);
3181
+ const layout = useMemo6(
3182
+ () => effectiveLayout(definition, containerWidth, {
3183
+ scale,
3184
+ limitsOf: (w) => w.type === UNKNOWN_WIDGET_TYPE2 ? void 0 : spanLimitsOfLayout(registry.descriptor(w.type)?.layout)
3185
+ }),
3186
+ [definition, containerWidth, scale, registry]
3187
+ );
3188
+ const { breakpoint, columns } = layout;
3189
+ const lengths = gridCss(grid, columns);
3190
+ return /* @__PURE__ */ jsxs12(
2328
3191
  "div",
2329
3192
  {
2330
- style: {
2331
- display: "grid",
2332
- gridTemplateColumns: `repeat(${grid.columns}, minmax(0, 1fr))`,
2333
- gridAutoRows: `${grid.rowHeight}px`,
2334
- columnGap: colGap,
2335
- rowGap,
2336
- width: "100%"
2337
- },
2338
- children: widgets.map((widget) => {
2339
- const box = resolveWidgetBox(widget.layout, breakpoint);
2340
- return /* @__PURE__ */ jsx16("div", { style: gridItemStyle(box, grid.columns), children: /* @__PURE__ */ jsx16(WidgetSubjectProvider, { label: widgetSubjectLabel(widget), children: /* @__PURE__ */ jsx16(
2341
- ConnectedWidget,
3193
+ ref: boardRef,
3194
+ "data-density": effectiveDensity,
3195
+ "data-columns": columns,
3196
+ "data-time-paused": range.paused ? "" : void 0,
3197
+ style: { ...sizingStyle(effectiveSizing, bg), ...tokenStyle(scale) },
3198
+ children: [
3199
+ /* @__PURE__ */ jsx17(WidgetTokenStyles, {}),
3200
+ /* @__PURE__ */ jsx17(WidgetDensityProvider, { scale, children: /* @__PURE__ */ jsx17(WidgetThemeRootProvider, { root: themeRoot, children: /* @__PURE__ */ jsx17(WidgetTimeZoneProvider, { timeZone, children: /* @__PURE__ */ jsx17(WidgetTimeRangeProvider, { range, children: /* @__PURE__ */ jsx17(WidgetSelectProvider2, { select, children: /* @__PURE__ */ jsx17(WidgetCandidatesProvider2, { candidates, children: /* @__PURE__ */ jsx17("div", { style: { padding: lengths.padding }, children: /* @__PURE__ */ jsx17("div", { style: { containerType: "inline-size" }, children: /* @__PURE__ */ jsx17(
3201
+ "div",
2342
3202
  {
2343
- widget,
2344
- hub,
2345
- registry,
2346
- actions,
2347
- initialSamples: histories[widget.id]
3203
+ "data-grid": "",
3204
+ style: {
3205
+ display: "grid",
3206
+ gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`,
3207
+ gridAutoRows: CELL,
3208
+ columnGap: lengths.gutterX,
3209
+ rowGap: lengths.gutterY,
3210
+ width: "100%",
3211
+ ...{ "--dc-cell": lengths.cell }
3212
+ },
3213
+ children: /* @__PURE__ */ jsx17(
3214
+ PlacedWidgets,
3215
+ {
3216
+ widgets,
3217
+ breakpoint,
3218
+ columns,
3219
+ grid: layout.grid,
3220
+ lengths,
3221
+ render: (widget) => /* @__PURE__ */ jsx17(WidgetFrameStyleProvider, { frame: resolveFrame(canvasFrame, widgetFrame(widget)), children: /* @__PURE__ */ jsx17(WidgetSubjectProvider, { label: widgetSubjectLabel(widget), children: /* @__PURE__ */ jsx17(
3222
+ ConnectedWidget,
3223
+ {
3224
+ widget,
3225
+ hub,
3226
+ registry,
3227
+ actions,
3228
+ initialSamples: histories[widget.id]
3229
+ }
3230
+ ) }) })
3231
+ }
3232
+ )
2348
3233
  }
2349
- ) }) }, widget.id);
2350
- })
3234
+ ) }) }) }) }) }) }) }) })
3235
+ ]
2351
3236
  }
2352
- ) }) }) }) }) });
3237
+ );
2353
3238
  }
2354
- function gridItemStyle(box, columns) {
2355
- const col = Math.min(Math.max(0, box.col), columns - 1);
2356
- const colSpan = Math.min(Math.max(1, box.colSpan), columns - col);
3239
+ var CELL = "var(--dc-cell)";
3240
+ function gridLengthCss(len) {
3241
+ if (typeof len === "number") return `calc(${len}px * var(--dc-scale, 1))`;
3242
+ return len === "space-0" ? "0px" : `var(--dc-${len})`;
3243
+ }
3244
+ function gridCss(grid, columns) {
3245
+ const g = grid.gutter;
3246
+ const gutterX = gridLengthCss(typeof g === "object" ? g.x : g);
3247
+ const gutterY = gridLengthCss(typeof g === "object" ? g.y : g);
3248
+ const p = grid.padding;
3249
+ const padding = typeof p === "object" ? [p.top, p.right, p.bottom, p.left].map(gridLengthCss).join(" ") : gridLengthCss(p);
3250
+ return { gutterX, gutterY, padding, cell: `calc((100cqw - ${columns - 1} * ${gutterX}) / ${columns})` };
3251
+ }
3252
+ function widgetFrame(widget) {
3253
+ return widget.type === UNKNOWN_WIDGET_TYPE2 ? void 0 : widget.frame;
3254
+ }
3255
+ function gridItemStyle(p, columns) {
3256
+ const col = Math.min(Math.max(0, p.col), columns - 1);
3257
+ const cols = Math.min(Math.max(1, p.cols), columns - col);
2357
3258
  return {
2358
- gridColumn: `${col + 1} / span ${colSpan}`,
2359
- gridRow: `${box.row + 1} / span ${box.rowSpan}`,
2360
- zIndex: box.z,
2361
- transform: box.offset ? `translate(${box.offset.x}px, ${box.offset.y}px)` : void 0,
3259
+ gridColumn: `${col + 1} / span ${cols}`,
3260
+ gridRow: `${p.row + 1} / span ${p.rows}`,
2362
3261
  // Let a grid item shrink below its content's intrinsic size so a wide chart/table
2363
3262
  // fits its track instead of overflowing it.
2364
3263
  minWidth: 0,
2365
3264
  minHeight: 0
2366
3265
  };
2367
3266
  }
3267
+ var RELATIVE_Z = 100;
3268
+ function relativeBoxStyle(p, lengths) {
3269
+ const a = anchorFraction(p.anchor);
3270
+ const s = anchorFraction(p.self ?? p.anchor);
3271
+ const span = (n, gutter) => `calc(${n} * ${CELL} + ${n - 1} * ${gutter})`;
3272
+ const pitches = (n, gutter) => `${n} * (${CELL} + ${gutter})`;
3273
+ const fill = "fill" in p.size;
3274
+ return {
3275
+ position: "absolute",
3276
+ left: `calc(${a.fx * 100}% + ${pitches(p.offset?.x ?? 0, lengths.gutterX)})`,
3277
+ top: `calc(${a.fy * 100}% + ${pitches(p.offset?.y ?? 0, lengths.gutterY)})`,
3278
+ width: fill ? "100%" : span(p.size.cols, lengths.gutterX),
3279
+ height: fill ? "100%" : span(p.size.rows, lengths.gutterY),
3280
+ transform: `translate(${-s.fx * 100}%, ${-s.fy * 100}%)`,
3281
+ zIndex: RELATIVE_Z + (p.layer ?? 0),
3282
+ pointerEvents: "auto"
3283
+ };
3284
+ }
3285
+ function PlacedWidgets({
3286
+ widgets,
3287
+ breakpoint,
3288
+ columns,
3289
+ grid,
3290
+ lengths,
3291
+ render
3292
+ }) {
3293
+ const placed = widgets.map((w) => ({ widget: w, placement: resolvePlacement(w.layout, breakpoint) }));
3294
+ const dependents = /* @__PURE__ */ new Map();
3295
+ for (const { widget, placement } of placed) {
3296
+ if (placement.mode !== "relative") continue;
3297
+ dependents.set(placement.host, [...dependents.get(placement.host) ?? [], { widget, placement }]);
3298
+ }
3299
+ const relative = (host) => (dependents.get(host) ?? []).map(({ widget, placement }) => (
3300
+ // data-widget names the box a widget occupies, so a check over the rendered board (the
3301
+ // readability gate in apps/board-replay) can find it.
3302
+ /* @__PURE__ */ jsxs12("div", { "data-widget": widget.id, "data-host": host, style: relativeBoxStyle(placement, lengths), children: [
3303
+ render(widget),
3304
+ relative(widget.id)
3305
+ ] }, widget.id)
3306
+ ));
3307
+ return /* @__PURE__ */ jsx17(Fragment6, { children: placed.map(({ widget, placement }) => {
3308
+ if (placement.mode !== "grid") return null;
3309
+ const style = gridItemStyle(grid.get(widget.id) ?? placement, columns);
3310
+ return /* @__PURE__ */ jsxs12(Fragment5, { children: [
3311
+ /* @__PURE__ */ jsx17("div", { "data-widget": widget.id, style, children: render(widget) }),
3312
+ dependents.has(widget.id) ? (
3313
+ // The host's area again, as a layer for its relative widgets to be positioned in.
3314
+ // It takes no pointer events itself, so the host underneath stays usable.
3315
+ /* @__PURE__ */ jsx17("div", { style: { ...style, position: "relative", pointerEvents: "none", zIndex: RELATIVE_Z }, children: relative(widget.id) })
3316
+ ) : null
3317
+ ] }, widget.id);
3318
+ }) });
3319
+ }
2368
3320
  function sizingStyle(sizing, bg) {
2369
3321
  const base = {
2370
3322
  overflow: "auto",
@@ -2389,16 +3341,28 @@ function widgetSubjectLabel(widget) {
2389
3341
  if (ds.kind === "anchor") return ds.anchor.targetToken;
2390
3342
  return void 0;
2391
3343
  }
2392
- function useActiveBreakpoint(breakpoints) {
2393
- const [width, setWidth] = useState6(
2394
- () => typeof window === "undefined" ? 0 : window.innerWidth
2395
- );
2396
- useEffect4(() => {
2397
- const onResize = () => setWidth(window.innerWidth);
2398
- window.addEventListener("resize", onResize);
2399
- return () => window.removeEventListener("resize", onResize);
3344
+ function useContainerWidth(keyOf) {
3345
+ const ref = useRef3(null);
3346
+ const [width, setWidth] = useState8(0);
3347
+ const keyOfRef = useRef3(keyOf);
3348
+ const measure = useCallback2(() => {
3349
+ const el = ref.current;
3350
+ if (!el) return;
3351
+ const next = el.clientWidth;
3352
+ setWidth((prev) => keyOfRef.current(prev) === keyOfRef.current(next) ? prev : next);
2400
3353
  }, []);
2401
- return activeBreakpoint(breakpoints, width);
3354
+ useLayoutEffect(() => {
3355
+ keyOfRef.current = keyOf;
3356
+ measure();
3357
+ }, [keyOf, measure]);
3358
+ useLayoutEffect(() => {
3359
+ const el = ref.current;
3360
+ if (!el || typeof ResizeObserver === "undefined") return;
3361
+ const ro = new ResizeObserver(measure);
3362
+ ro.observe(el);
3363
+ return () => ro.disconnect();
3364
+ }, [measure]);
3365
+ return [ref, width];
2402
3366
  }
2403
3367
  function seedKey(widget, registry) {
2404
3368
  if (widget.type === UNKNOWN_WIDGET_TYPE2 || registry.descriptor(widget.type)?.channel !== "measurement") {
@@ -2406,11 +3370,11 @@ function seedKey(widget, registry) {
2406
3370
  }
2407
3371
  return JSON.stringify(widget.datasource ?? null);
2408
3372
  }
2409
- function useWidgetHistories(widgets, enabled, registry) {
2410
- const [seeds, setSeeds] = useState6({});
3373
+ function useWidgetHistories(widgets, enabled, registry, range) {
3374
+ const [seeds, setSeeds] = useState8({});
2411
3375
  const requested = useRef3({});
2412
3376
  const mounted = useRef3(true);
2413
- useEffect4(() => {
3377
+ useEffect5(() => {
2414
3378
  mounted.current = true;
2415
3379
  return () => {
2416
3380
  mounted.current = false;
@@ -2418,31 +3382,36 @@ function useWidgetHistories(widgets, enabled, registry) {
2418
3382
  }, []);
2419
3383
  const wanted = widgets.flatMap((w) => {
2420
3384
  const key = seedKey(w, registry);
2421
- return key === void 0 ? [] : [[w.id, key]];
3385
+ if (key === void 0) return [];
3386
+ const descriptor = w.type === UNKNOWN_WIDGET_TYPE2 ? void 0 : registry.descriptor(w.type);
3387
+ const span = descriptor ? measurementSpan(descriptor, w, range.spanSeconds) ?? null : null;
3388
+ return [[w.id, key, span]];
2422
3389
  });
2423
3390
  const wantedKey = JSON.stringify(wanted);
2424
- useEffect4(() => {
3391
+ useEffect5(() => {
2425
3392
  if (!enabled) {
2426
3393
  requested.current = {};
2427
3394
  setSeeds({});
2428
3395
  return;
2429
3396
  }
2430
- const historyWindow = defaultHistoryWindow();
3397
+ const now = Date.now();
2431
3398
  const byId = new Map(widgets.map((w) => [w.id, w]));
2432
3399
  const next = {};
2433
- for (const [id, key] of wanted) {
2434
- next[id] = key;
2435
- if (requested.current[id] === key) continue;
3400
+ for (const [id, key, span] of wanted) {
3401
+ const asked = `${key}|${span ?? ""}`;
3402
+ next[id] = asked;
3403
+ if (requested.current[id] === asked) continue;
2436
3404
  const widget = byId.get(id);
2437
3405
  if (!widget) continue;
3406
+ const historyWindow = span === null ? defaultHistoryWindow() : historyWindowFor(span, range.paused && range.pausedAt !== void 0 ? range.pausedAt : now);
2438
3407
  void fetchWidgetHistory(widget, historyWindow).then((samples) => {
2439
- if (!mounted.current || requested.current[id] !== key) return;
3408
+ if (!mounted.current || requested.current[id] !== asked) return;
2440
3409
  setSeeds((prev) => ({ ...prev, [id]: { key, samples } }));
2441
3410
  });
2442
3411
  }
2443
3412
  requested.current = next;
2444
3413
  }, [wantedKey, enabled]);
2445
- return useMemo5(() => {
3414
+ return useMemo6(() => {
2446
3415
  const out = {};
2447
3416
  if (!enabled) return out;
2448
3417
  for (const w of widgets) {
@@ -2453,6 +3422,125 @@ function useWidgetHistories(widgets, enabled, registry) {
2453
3422
  }, [widgets, seeds, enabled, registry]);
2454
3423
  }
2455
3424
 
3425
+ // src/time-range-bar.tsx
3426
+ import {
3427
+ DEFAULT_TIME_RANGE_SECONDS,
3428
+ TIME_RANGE_PRESETS,
3429
+ initialTimeRange as initialTimeRange2,
3430
+ isValidSpanSeconds as isValidSpanSeconds2,
3431
+ pauseRange,
3432
+ presetIdOf,
3433
+ resumeRange,
3434
+ spanLabel as spanLabel2,
3435
+ withSpan
3436
+ } from "@devicechain/dashboards";
3437
+ import { useCallback as useCallback3, useEffect as useEffect6, useMemo as useMemo7, useRef as useRef4, useState as useState9 } from "react";
3438
+ import { jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
3439
+ function useBoardTimeRange(defaultSeconds, link = {}) {
3440
+ const [state, setState] = useState9(() => initialTimeRange2(defaultSeconds, link));
3441
+ const lastDefault = useRef4(defaultSeconds);
3442
+ useEffect6(() => {
3443
+ if (lastDefault.current === defaultSeconds) return;
3444
+ lastDefault.current = defaultSeconds;
3445
+ setState((s) => withSpan(s, isValidSpanSeconds2(defaultSeconds) ? defaultSeconds : DEFAULT_TIME_RANGE_SECONDS));
3446
+ }, [defaultSeconds]);
3447
+ const controls = useMemo7(
3448
+ () => ({
3449
+ setSpan: (spanSeconds) => setState((s) => withSpan(s, spanSeconds)),
3450
+ pause: () => setState((s) => pauseRange(s, Date.now())),
3451
+ resume: () => setState((s) => resumeRange(s))
3452
+ }),
3453
+ []
3454
+ );
3455
+ return [state, controls];
3456
+ }
3457
+ var DEFAULT_TIME_RANGE_LABELS = {
3458
+ range: "Time range",
3459
+ pause: "Pause",
3460
+ resume: "Resume",
3461
+ paused: "Paused",
3462
+ pausedAt: (time) => `Paused at ${time}`,
3463
+ actionsLive: "commands still send"
3464
+ };
3465
+ var button = (active) => ({
3466
+ padding: "2px 8px",
3467
+ fontSize: 12,
3468
+ lineHeight: "18px",
3469
+ cursor: "pointer",
3470
+ color: active ? "hsl(var(--background))" : "hsl(var(--foreground))",
3471
+ background: active ? "hsl(var(--foreground))" : "transparent",
3472
+ border: "1px solid hsl(var(--border))",
3473
+ borderRadius: 4
3474
+ });
3475
+ function PausedIndicator({
3476
+ pausedAt,
3477
+ labels = DEFAULT_TIME_RANGE_LABELS
3478
+ }) {
3479
+ const time = pausedAt === void 0 ? void 0 : new Date(pausedAt).toLocaleTimeString();
3480
+ return /* @__PURE__ */ jsxs13(
3481
+ "span",
3482
+ {
3483
+ role: "status",
3484
+ "data-time-range-paused": "",
3485
+ title: time ? labels.pausedAt(time) : labels.paused,
3486
+ style: {
3487
+ display: "inline-flex",
3488
+ alignItems: "center",
3489
+ gap: 4,
3490
+ padding: "2px 8px",
3491
+ fontSize: 12,
3492
+ fontWeight: 700,
3493
+ letterSpacing: 0.4,
3494
+ textTransform: "uppercase",
3495
+ color: "hsl(var(--background))",
3496
+ background: "hsl(var(--foreground))",
3497
+ border: "2px solid hsl(var(--foreground))",
3498
+ borderRadius: 4
3499
+ },
3500
+ children: [
3501
+ /* @__PURE__ */ jsx18("span", { "aria-hidden": "true", children: "\u23F8" }),
3502
+ labels.paused,
3503
+ time ? /* @__PURE__ */ jsx18("span", { style: { fontWeight: 400, textTransform: "none" }, children: ` ${time}` }) : null,
3504
+ /* @__PURE__ */ jsx18("span", { "data-time-range-actions-live": "", style: { fontWeight: 400, textTransform: "none" }, children: `\xB7 ${labels.actionsLive}` })
3505
+ ]
3506
+ }
3507
+ );
3508
+ }
3509
+ function TimeRangeBar({
3510
+ state,
3511
+ controls,
3512
+ labels = DEFAULT_TIME_RANGE_LABELS
3513
+ }) {
3514
+ const preset = presetIdOf(state.spanSeconds);
3515
+ const onPause = useCallback3(() => state.paused ? controls.resume() : controls.pause(), [state.paused, controls]);
3516
+ return /* @__PURE__ */ jsxs13(
3517
+ "div",
3518
+ {
3519
+ "data-time-range-bar": "",
3520
+ role: "group",
3521
+ "aria-label": labels.range,
3522
+ style: { display: "inline-flex", alignItems: "center", gap: 6, flexWrap: "wrap" },
3523
+ children: [
3524
+ preset === void 0 ? /* @__PURE__ */ jsx18("button", { type: "button", "aria-pressed": "true", "data-range": spanLabel2(state.spanSeconds), style: button(true), children: spanLabel2(state.spanSeconds) }) : null,
3525
+ TIME_RANGE_PRESETS.map((p) => /* @__PURE__ */ jsx18(
3526
+ "button",
3527
+ {
3528
+ type: "button",
3529
+ "aria-pressed": p.id === preset,
3530
+ "data-range": p.id,
3531
+ onClick: () => controls.setSpan(p.seconds),
3532
+ style: button(p.id === preset),
3533
+ children: p.id
3534
+ },
3535
+ p.id
3536
+ )),
3537
+ /* @__PURE__ */ jsx18("button", { type: "button", "aria-pressed": state.paused, "data-time-range-pause": "", onClick: onPause, style: button(state.paused), children: state.paused ? labels.resume : labels.pause }),
3538
+ state.paused ? /* @__PURE__ */ jsx18(PausedIndicator, { pausedAt: state.pausedAt, labels }) : null
3539
+ ]
3540
+ }
3541
+ );
3542
+ }
3543
+
2456
3544
  // src/registry.ts
2457
3545
  var entries = Object.entries(builtinWidgetDescriptors);
2458
3546
  var WIDGET_CHANNEL = Object.freeze(
@@ -2542,7 +3630,7 @@ import {
2542
3630
  WidgetSelectProvider as WidgetSelectProvider3,
2543
3631
  useWidgetSelect as useWidgetSelect4,
2544
3632
  WidgetTimeZoneProvider as WidgetTimeZoneProvider2,
2545
- useWidgetTimeZone as useWidgetTimeZone5,
3633
+ useWidgetTimeZone as useWidgetTimeZone6,
2546
3634
  WidgetThemeRootProvider as WidgetThemeRootProvider2,
2547
3635
  useWidgetThemeRoot,
2548
3636
  WidgetCandidatesProvider as WidgetCandidatesProvider3,
@@ -2551,16 +3639,19 @@ import {
2551
3639
  flashTextStyle as flashTextStyle2,
2552
3640
  flashBackgroundStyle as flashBackgroundStyle2,
2553
3641
  FlashValue as FlashValue3,
3642
+ fitValueFont as fitValueFont3,
2554
3643
  useMeasurementStream as useMeasurementStream2,
2555
3644
  useAlarmStream as useAlarmStream2,
2556
3645
  useCommandStream as useCommandStream2,
2557
3646
  useLocationStream as useLocationStream2,
2558
3647
  useDatasourceAvailability as useDatasourceAvailability2,
2559
3648
  useDashboardHub,
3649
+ useHubConnectionState,
3650
+ HubConnectionBadge,
2560
3651
  useResolvedBindings,
2561
3652
  useSlotCandidates,
2562
3653
  useCandidates as useCandidates2,
2563
- css as css10
3654
+ css as css12
2564
3655
  } from "@devicechain/widget-sdk";
2565
3656
  import {
2566
3657
  EChart as EChart3,
@@ -2575,18 +3666,22 @@ export {
2575
3666
  BUNDLED_BASEMAP_ATTRIBUTION,
2576
3667
  CommandButton,
2577
3668
  ConnectedWidget,
3669
+ DEFAULT_TIME_RANGE_LABELS,
2578
3670
  DashboardRenderer,
2579
3671
  EChart3 as EChart,
2580
3672
  EntitySelector,
2581
3673
  FlashValue3 as FlashValue,
2582
3674
  Gauge,
3675
+ HubConnectionBadge,
2583
3676
  Image,
2584
3677
  Label,
2585
3678
  LatestCard,
2586
3679
  MapRuntimeProvider,
2587
3680
  MapWidget,
3681
+ PausedIndicator,
2588
3682
  Table,
2589
3683
  TenantBasemapProvider,
3684
+ TimeRangeBar,
2590
3685
  TimeSeriesChart,
2591
3686
  WIDGET_BINDS_DATASOURCE,
2592
3687
  WIDGET_CHANNEL,
@@ -2607,10 +3702,11 @@ export {
2607
3702
  commandStatusColor,
2608
3703
  commandStatusLabel,
2609
3704
  createBuiltinWidgetRegistry,
2610
- css10 as css,
3705
+ css12 as css,
2611
3706
  defaultValues,
2612
3707
  describePosition,
2613
3708
  enumOptionValues,
3709
+ fitValueFont3 as fitValueFont,
2614
3710
  flashBackgroundStyle2 as flashBackgroundStyle,
2615
3711
  flashTextStyle2 as flashTextStyle,
2616
3712
  formatDegrees,
@@ -2633,12 +3729,14 @@ export {
2633
3729
  severityRank,
2634
3730
  stripUnknownOptions,
2635
3731
  useAlarmStream2 as useAlarmStream,
3732
+ useBoardTimeRange,
2636
3733
  useCandidates2 as useCandidates,
2637
3734
  useChartTheme3 as useChartTheme,
2638
3735
  useCommandStream2 as useCommandStream,
2639
3736
  useDashboardHub,
2640
3737
  useDatasourceAvailability2 as useDatasourceAvailability,
2641
3738
  useFlashOnChange3 as useFlashOnChange,
3739
+ useHubConnectionState,
2642
3740
  useLocationStream2 as useLocationStream,
2643
3741
  useMapRuntime,
2644
3742
  useMeasurementStream2 as useMeasurementStream,
@@ -2648,7 +3746,7 @@ export {
2648
3746
  useWidgetCandidates2 as useWidgetCandidates,
2649
3747
  useWidgetSelect4 as useWidgetSelect,
2650
3748
  useWidgetThemeRoot,
2651
- useWidgetTimeZone5 as useWidgetTimeZone,
3749
+ useWidgetTimeZone6 as useWidgetTimeZone,
2652
3750
  validateDefinitionOptions,
2653
3751
  validateParams,
2654
3752
  validateWidgetOptions,