@devicechain/widget-sdk 0.22.0 → 0.23.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.
- package/README.md +8 -0
- package/dist/chart/chart-options.d.ts +1 -0
- package/dist/chart/echart-engine.d.ts +3 -0
- package/dist/chart/echart.d.ts +7 -1
- package/dist/chunks/echart-engine-FO4HQ22H.js +25 -0
- package/dist/chunks/echart-engine-FO4HQ22H.js.map +7 -0
- package/dist/echarts.d.ts +1 -1
- package/dist/echarts.js +50 -14
- package/dist/echarts.js.map +2 -2
- package/dist/hooks.d.ts +2 -0
- package/dist/index.js +58 -12
- package/dist/index.js.map +3 -3
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -38,6 +38,14 @@ Chart helpers live on a separate entry, `@devicechain/widget-sdk/echarts`: the `
|
|
|
38
38
|
wrapper, `buildLineOption` / `buildGaugeOption` and the chart colour theme. `echarts` is an
|
|
39
39
|
**optional peer dependency**, needed only by a widget that imports that entry.
|
|
40
40
|
|
|
41
|
+
The chart engine itself is not part of that entry's static imports. `EChart` loads it on demand,
|
|
42
|
+
as one chunk, the first time a chart mounts, so a host bundler keeps ECharts out of the first
|
|
43
|
+
download. Until the engine arrives, the chart's box is empty. If the engine cannot be fetched,
|
|
44
|
+
`EChart` throws the error to the nearest error boundary, and the next mount fetches it again. A host that
|
|
45
|
+
wants charts drawn on its first frame awaits `loadChartEngine()` before rendering. A widget that
|
|
46
|
+
draws a chart can set `MyWidget.preload = loadChartEngine` on its component, and then
|
|
47
|
+
`preloadWidgets` from `@devicechain/widgets` fetches the engine for any board that contains it.
|
|
48
|
+
|
|
41
49
|
## Declare a widget
|
|
42
50
|
|
|
43
51
|
```tsx
|
|
@@ -11,6 +11,7 @@ export interface LineChartOptions {
|
|
|
11
11
|
precision?: number;
|
|
12
12
|
thresholds?: readonly Threshold[];
|
|
13
13
|
scale?: number;
|
|
14
|
+
gaps?: readonly string[];
|
|
14
15
|
}
|
|
15
16
|
export declare function buildLineOption(samples: MeasurementSample[], theme: ChartTheme, options?: LineChartOptions, size?: ElementSize): EChartOption;
|
|
16
17
|
export interface GaugeChartOptions {
|
package/dist/chart/echart.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import * as echarts from 'echarts/core';
|
|
1
|
+
import type * as echarts from 'echarts/core';
|
|
2
|
+
/**
|
|
3
|
+
* Fetches the chart engine that <EChart> draws with, so the charts a host renders after awaiting
|
|
4
|
+
* it draw on their first frame. Optional: an <EChart> that mounts first loads the engine itself.
|
|
5
|
+
* Rejects if the engine cannot be fetched; a later call (or mount) fetches it again.
|
|
6
|
+
*/
|
|
7
|
+
export declare function loadChartEngine(): Promise<void>;
|
|
2
8
|
export declare function loadThresholdMarkers(): Promise<void>;
|
|
3
9
|
export declare function optionUsesMarkers(option: EChartOption): boolean;
|
|
4
10
|
export type EChartOption = echarts.EChartsCoreOption;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// src/chart/echart-engine.ts
|
|
2
|
+
import { GaugeChart, LineChart } from "echarts/charts";
|
|
3
|
+
import { GridComponent, LegendPlainComponent, TooltipComponent } from "echarts/components";
|
|
4
|
+
import * as echarts from "echarts/core";
|
|
5
|
+
import { CanvasRenderer } from "echarts/renderers";
|
|
6
|
+
echarts.use([LineChart, GaugeChart, GridComponent, TooltipComponent, LegendPlainComponent, CanvasRenderer]);
|
|
7
|
+
var init2 = echarts.init;
|
|
8
|
+
var markers;
|
|
9
|
+
function registerThresholdMarkers() {
|
|
10
|
+
markers ?? (markers = import("./echart-markers-JDFD265P.js").then(
|
|
11
|
+
(m) => {
|
|
12
|
+
echarts.use(m.MARKER_COMPONENTS);
|
|
13
|
+
},
|
|
14
|
+
(err) => {
|
|
15
|
+
markers = void 0;
|
|
16
|
+
throw err;
|
|
17
|
+
}
|
|
18
|
+
));
|
|
19
|
+
return markers;
|
|
20
|
+
}
|
|
21
|
+
export {
|
|
22
|
+
init2 as init,
|
|
23
|
+
registerThresholdMarkers
|
|
24
|
+
};
|
|
25
|
+
//# sourceMappingURL=echart-engine-FO4HQ22H.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/chart/echart-engine.ts"],
|
|
4
|
+
"sourcesContent": ["// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// The chart engine: Apache ECharts (and zrender beneath it), registered with ONLY the chart types\n// and components the option builders emit -- line + gauge, grid/tooltip/legend, canvas renderer --\n// so tree-shaking keeps the rest of ECharts out of the bundle.\n//\n// \uD83D\uDD34 THIS MODULE IS LOADED LAZILY, AND IT IS THE ONLY MODULE THAT IMPORTS ECHARTS AT RUNTIME.\n// echart.tsx reaches it through `import('./echart-engine')`, so a host bundler emits ECharts as ONE\n// chunk, fetched the first time a chart mounts (or earlier, through loadChartEngine, which\n// preloadWidgets calls for a board that holds a chart). At ~170 KB gzip it is the largest single\n// dependency of a board, and a board with no chart never downloads it. A STATIC import of this\n// module, or of `echarts/*`, from anywhere in the SDK folds the whole engine back into every host's\n// first download: dist-shape.test.ts asserts the boundary on the built artifact.\n//\n// The threshold markers are imported from HERE, not from echart.tsx, for the same reason: their\n// chunk then depends on this one, which is already loaded whenever it is fetched, so the bundler\n// has no ECharts code that two independent lazy chunks share and no reason to split the engine.\n\nimport { GaugeChart, LineChart } from 'echarts/charts';\nimport { GridComponent, LegendPlainComponent, TooltipComponent } from 'echarts/components';\nimport * as echarts from 'echarts/core';\nimport { CanvasRenderer } from 'echarts/renderers';\n\n// \uD83D\uDD34 EVERY COMPONENT AN OPTION BUILDER EMITS MUST BE REGISTERED -- here, or (the threshold\n// markers) by registerThresholdMarkers below. ECharts ignores an option key whose component is\n// not registered -- silently, no warning -- so a `legend` or a `markLine` the builder sets\n// draws nothing. The legend was dropped this way for as long as multi-series charts existed.\n// echart.registration.test.tsx renders each through a real ECharts instance, so a builder\n// that grows a new component fails there until it is registered.\n//\n// The PLAIN legend, not LegendComponent: that one also installs the scrollable legend, which\n// no builder asks for (~1.8 KB gzip).\necharts.use([LineChart, GaugeChart, GridComponent, TooltipComponent, LegendPlainComponent, CanvasRenderer]);\n\nexport const init = echarts.init;\n\n// markLine / markArea are registered on first use (see echart-markers.ts for why).\nlet markers: Promise<void> | undefined;\n\nexport function registerThresholdMarkers(): Promise<void> {\n markers ??= import('./echart-markers').then(\n (m) => {\n echarts.use(m.MARKER_COMPONENTS);\n },\n (err: unknown) => {\n markers = undefined; // a failed chunk load is retried by the next chart that needs it\n throw err;\n },\n );\n return markers;\n}\n"],
|
|
5
|
+
"mappings": ";AAmBA,SAAS,YAAY,iBAAiB;AACtC,SAAS,eAAe,sBAAsB,wBAAwB;AACtE,YAAY,aAAa;AACzB,SAAS,sBAAsB;AAWvB,YAAI,CAAC,WAAW,YAAY,eAAe,kBAAkB,sBAAsB,cAAc,CAAC;AAEnG,IAAMA,QAAe;AAG5B,IAAI;AAEG,SAAS,2BAA0C;AACxD,wBAAY,OAAO,8BAAkB,EAAE;AAAA,IACrC,CAAC,MAAM;AACL,MAAQ,YAAI,EAAE,iBAAiB;AAAA,IACjC;AAAA,IACA,CAAC,QAAiB;AAChB,gBAAU;AACV,YAAM;AAAA,IACR;AAAA,EACF;AACA,SAAO;AACT;",
|
|
6
|
+
"names": ["init"]
|
|
7
|
+
}
|
package/dist/echarts.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { EChart, type EChartOption, type EChartProps } from './chart/echart';
|
|
1
|
+
export { EChart, loadChartEngine, type EChartOption, type EChartProps } from './chart/echart';
|
|
2
2
|
export { buildCompassOption, buildGaugeOption, buildLineOption, gaugeReadoutBox, severityFill, wrapHeading, type CompassChartOptions, type GaugeChartOptions, type LineChartOptions, } from './chart/chart-options';
|
|
3
3
|
export { resolveChartTheme, useChartTheme, type ChartTheme } from './chart/chart-theme';
|
package/dist/echarts.js
CHANGED
|
@@ -13,19 +13,28 @@ import {
|
|
|
13
13
|
} from "./chunks/chunk-A5BJ2ALW.js";
|
|
14
14
|
|
|
15
15
|
// src/chart/echart.tsx
|
|
16
|
-
import { GaugeChart, LineChart } from "echarts/charts";
|
|
17
|
-
import { GridComponent, LegendPlainComponent, TooltipComponent } from "echarts/components";
|
|
18
|
-
import * as echarts from "echarts/core";
|
|
19
|
-
import { CanvasRenderer } from "echarts/renderers";
|
|
20
16
|
import { useEffect, useRef, useState } from "react";
|
|
21
17
|
import { jsx } from "react/jsx-runtime";
|
|
22
|
-
|
|
18
|
+
var engine;
|
|
19
|
+
var engineLoading;
|
|
20
|
+
function loadEngine() {
|
|
21
|
+
engineLoading ?? (engineLoading = import("./chunks/echart-engine-FO4HQ22H.js").then(
|
|
22
|
+
(m) => engine = m,
|
|
23
|
+
(err) => {
|
|
24
|
+
engineLoading = void 0;
|
|
25
|
+
throw err;
|
|
26
|
+
}
|
|
27
|
+
));
|
|
28
|
+
return engineLoading;
|
|
29
|
+
}
|
|
30
|
+
function loadChartEngine() {
|
|
31
|
+
return loadEngine().then(() => void 0);
|
|
32
|
+
}
|
|
23
33
|
var markersReady = false;
|
|
24
34
|
var markersLoading;
|
|
25
35
|
function loadThresholdMarkers() {
|
|
26
|
-
markersLoading ?? (markersLoading =
|
|
27
|
-
(
|
|
28
|
-
echarts.use(m.MARKER_COMPONENTS);
|
|
36
|
+
markersLoading ?? (markersLoading = loadEngine().then((e) => e.registerThresholdMarkers()).then(
|
|
37
|
+
() => {
|
|
29
38
|
markersReady = true;
|
|
30
39
|
},
|
|
31
40
|
(err) => {
|
|
@@ -46,8 +55,21 @@ function optionUsesMarkers(option) {
|
|
|
46
55
|
function EChart({ option, className, style }) {
|
|
47
56
|
const containerRef = useRef(null);
|
|
48
57
|
const chartRef = useRef(null);
|
|
58
|
+
const [loaded, setLoaded] = useState(engine);
|
|
59
|
+
const [failed, setFailed] = useState();
|
|
49
60
|
const [markersLoaded, setMarkersLoaded] = useState(markersReady);
|
|
50
61
|
const needsMarkers = optionUsesMarkers(option) && !markersLoaded;
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (loaded) return;
|
|
64
|
+
let live = true;
|
|
65
|
+
loadEngine().then(
|
|
66
|
+
(e) => live && setLoaded(e),
|
|
67
|
+
(error) => live && setFailed({ error })
|
|
68
|
+
);
|
|
69
|
+
return () => {
|
|
70
|
+
live = false;
|
|
71
|
+
};
|
|
72
|
+
}, [loaded]);
|
|
51
73
|
useEffect(() => {
|
|
52
74
|
if (!needsMarkers) return;
|
|
53
75
|
let live = true;
|
|
@@ -62,8 +84,8 @@ function EChart({ option, className, style }) {
|
|
|
62
84
|
}, [needsMarkers]);
|
|
63
85
|
useEffect(() => {
|
|
64
86
|
const el = containerRef.current;
|
|
65
|
-
if (!el) return;
|
|
66
|
-
const chart =
|
|
87
|
+
if (!el || !loaded) return;
|
|
88
|
+
const chart = loaded.init(el);
|
|
67
89
|
chartRef.current = chart;
|
|
68
90
|
const observer = new ResizeObserver(() => chart.resize());
|
|
69
91
|
observer.observe(el);
|
|
@@ -72,11 +94,12 @@ function EChart({ option, className, style }) {
|
|
|
72
94
|
chart.dispose();
|
|
73
95
|
chartRef.current = null;
|
|
74
96
|
};
|
|
75
|
-
}, []);
|
|
97
|
+
}, [loaded]);
|
|
76
98
|
useEffect(() => {
|
|
77
99
|
chartRef.current?.setOption(option, { notMerge: true });
|
|
78
|
-
}, [option, markersLoaded]);
|
|
79
|
-
|
|
100
|
+
}, [option, markersLoaded, loaded]);
|
|
101
|
+
if (failed) throw failed.error;
|
|
102
|
+
return /* @__PURE__ */ jsx("div", { ref: containerRef, "data-echart": "", className, style: { width: "100%", height: "100%", ...style } });
|
|
80
103
|
}
|
|
81
104
|
|
|
82
105
|
// src/chart/chart-options.ts
|
|
@@ -96,6 +119,14 @@ function shortSide(size) {
|
|
|
96
119
|
const s = Math.min(size?.width ?? 0, size?.height ?? 0);
|
|
97
120
|
return s > 0 ? s : 200;
|
|
98
121
|
}
|
|
122
|
+
function breakAtGaps(points, gaps) {
|
|
123
|
+
let g = 0;
|
|
124
|
+
return points.flatMap((point, i) => {
|
|
125
|
+
let gap = Number.NaN;
|
|
126
|
+
while (gaps[g] < Date.parse(point[0])) gap = gaps[g++];
|
|
127
|
+
return i > 0 && !Number.isNaN(gap) ? [[new Date(gap).toISOString(), null], point] : [point];
|
|
128
|
+
});
|
|
129
|
+
}
|
|
99
130
|
function thresholdMarks(theme, thresholds, unit, precision, fontSize) {
|
|
100
131
|
const suffix = unit ? ` ${unit}` : "";
|
|
101
132
|
const ink = (t) => levelColor(theme.severity, t.level) ?? theme.mutedForeground;
|
|
@@ -136,6 +167,7 @@ function buildLineOption(samples, theme, options = {}, size) {
|
|
|
136
167
|
const marks = thresholds.length > 0 ? thresholdMarks(theme, thresholds, options.unit, options.precision, axisFont) : void 0;
|
|
137
168
|
const suffix = options.unit ? ` ${options.unit}` : "";
|
|
138
169
|
const precision = options.precision;
|
|
170
|
+
const gaps = (options.gaps ?? []).map(Date.parse).filter((g) => !Number.isNaN(g));
|
|
139
171
|
const series = names.map((name, i) => {
|
|
140
172
|
const color = theme.series[i % theme.series.length];
|
|
141
173
|
return {
|
|
@@ -145,7 +177,10 @@ function buildLineOption(samples, theme, options = {}, size) {
|
|
|
145
177
|
showSymbol: false,
|
|
146
178
|
lineStyle: { color },
|
|
147
179
|
itemStyle: { color },
|
|
148
|
-
data:
|
|
180
|
+
data: breakAtGaps(
|
|
181
|
+
samples.filter((s) => s.name === name && s.value != null && s.occurredTime != null).map((s) => [s.occurredTime, s.value]),
|
|
182
|
+
gaps
|
|
183
|
+
)
|
|
149
184
|
};
|
|
150
185
|
});
|
|
151
186
|
return {
|
|
@@ -382,6 +417,7 @@ export {
|
|
|
382
417
|
buildGaugeOption,
|
|
383
418
|
buildLineOption,
|
|
384
419
|
gaugeReadoutBox,
|
|
420
|
+
loadChartEngine,
|
|
385
421
|
resolveChartTheme,
|
|
386
422
|
severityFill,
|
|
387
423
|
useChartTheme,
|
package/dist/echarts.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/chart/echart.tsx", "../src/chart/chart-options.ts", "../src/chart/chart-theme.ts"],
|
|
4
|
-
"sourcesContent": ["// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// A thin React wrapper over Apache ECharts (ADR-039 chart engine).\n//\n// It imports from echarts/core and registers ONLY the chart types + components the\n// widgets use (line + gauge, grid/tooltip/legend, canvas renderer; threshold markers on demand) \u2014\n// tree-shaking away the rest of ECharts so the bundle carries only what a dashboard needs. The wrapper\n// owns the imperative lifecycle: init on mount, setOption on change, resize with the\n// container, dispose on unmount.\n\nimport { GaugeChart, LineChart } from 'echarts/charts';\nimport { GridComponent, LegendPlainComponent, TooltipComponent } from 'echarts/components';\nimport * as echarts from 'echarts/core';\nimport { CanvasRenderer } from 'echarts/renderers';\nimport { useEffect, useRef, useState } from 'react';\n\n// \uD83D\uDD34 EVERY COMPONENT AN OPTION BUILDER EMITS MUST BE REGISTERED -- here, or (the threshold\n// markers) by loadThresholdMarkers below. ECharts ignores an option key whose component is\n// not registered -- silently, no warning -- so a `legend` or a `markLine` the builder sets\n// draws nothing. The legend was dropped this way for as long as multi-series charts existed.\n// echart.registration.test.tsx renders each through a real ECharts instance, so a builder\n// that grows a new component fails there until it is registered.\n//\n// The PLAIN legend, not LegendComponent: that one also installs the scrollable legend, which\n// no builder asks for (~1.8 KB gzip).\necharts.use([LineChart, GaugeChart, GridComponent, TooltipComponent, LegendPlainComponent, CanvasRenderer]);\n\n// markLine / markArea are registered on first use (see echart-markers.ts for why). Until they\n// are, a chart that asks for them draws without them, and EChart re-applies its option once\n// they arrive -- a beat later on the first threshold chart of a page, never silently absent.\nlet markersReady = false;\nlet markersLoading: Promise<void> | undefined;\n\nexport function loadThresholdMarkers(): Promise<void> {\n markersLoading ??= import('./echart-markers').then(\n (m) => {\n echarts.use(m.MARKER_COMPONENTS);\n markersReady = true;\n },\n (err: unknown) => {\n markersLoading = undefined; // a failed chunk load is retried by the next chart that needs it\n throw err;\n },\n );\n return markersLoading;\n}\n\n// True when any series of the option carries a markLine or markArea.\nexport function optionUsesMarkers(option: EChartOption): boolean {\n const series = (option as { series?: unknown }).series;\n const list = Array.isArray(series) ? series : series ? [series] : [];\n return list.some((s) => {\n const m = s as { markLine?: unknown; markArea?: unknown } | null;\n return typeof m === 'object' && m !== null && (m.markLine != null || m.markArea != null);\n });\n}\n\n// A resolved ECharts option object (the shape echarts.setOption accepts). Widgets\n// build these with the pure builders in chart-options.ts.\nexport type EChartOption = echarts.EChartsCoreOption;\n\nexport interface EChartProps {\n option: EChartOption;\n className?: string;\n style?: React.CSSProperties;\n}\n\n// EChart fills its parent; the dashboard canvas sizes the parent, and a\n// ResizeObserver keeps the chart matched to it.\nexport function EChart({ option, className, style }: EChartProps) {\n const containerRef = useRef<HTMLDivElement>(null);\n const chartRef = useRef<echarts.ECharts | null>(null);\n const [markersLoaded, setMarkersLoaded] = useState(markersReady);\n const needsMarkers = optionUsesMarkers(option) && !markersLoaded;\n\n useEffect(() => {\n if (!needsMarkers) return;\n let live = true;\n loadThresholdMarkers().then(\n () => live && setMarkersLoaded(true),\n // Loud, not silent: the chart still draws, without its threshold lines.\n (err: unknown) => console.error('EChart: the threshold markers failed to load', err),\n );\n return () => {\n live = false;\n };\n }, [needsMarkers]);\n\n // Init + teardown once for the element's lifetime.\n useEffect(() => {\n const el = containerRef.current;\n if (!el) return;\n\n const chart = echarts.init(el);\n chartRef.current = chart;\n\n const observer = new ResizeObserver(() => chart.resize());\n observer.observe(el);\n\n return () => {\n observer.disconnect();\n chart.dispose();\n chartRef.current = null;\n };\n }, []);\n\n // Re-apply the option whenever it changes. notMerge:true so a shrinking series\n // (rolling window) doesn't leave stale points behind.\n useEffect(() => {\n chartRef.current?.setOption(option, { notMerge: true });\n }, [option, markersLoaded]);\n\n return (\n <div ref={containerRef} className={className} style={{ width: '100%', height: '100%', ...style }} />\n );\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Pure ECharts option builders. Kept free of React/DOM so the data\u2192chart mapping\n// is unit-testable without a canvas: a widget resolves a ChartTheme + samples and\n// hands the result straight to <EChart>.\n\nimport type { MeasurementSample } from '@devicechain/dashboards';\n\nimport { FIT_MIN_FONT_PX, fitValueFont } from '../fit-text';\nimport { formatValue } from '../format';\nimport type { ElementSize } from '../hooks';\nimport { isAlarmLevel, levelColor, type AlarmLevel } from '../severity';\nimport { bandAt, parseThresholds, thresholdBands, type Threshold } from '../thresholds';\n\nimport type { ChartTheme } from './chart-theme';\nimport type { EChartOption } from './echart';\n\n// distinctNames returns the measurement names present in `samples`, in first-seen\n// order \u2014 used when a chart's datasource doesn't pin an explicit series list.\nfunction distinctNames(samples: MeasurementSample[]): string[] {\n const seen: string[] = [];\n for (const s of samples) {\n if (!seen.includes(s.name)) seen.push(s.name);\n }\n return seen;\n}\n\n// A severity colour with an alpha, for a band drawn UNDER the data (markArea). The theme's\n// severity colours are comma-form hsl(), so the alpha form is a string edit.\nexport function severityFill(theme: ChartTheme, level: AlarmLevel, alpha: number): string {\n return theme.severity[level].replace(/^hsl\\(/, 'hsla(').replace(/\\)$/, `, ${alpha})`);\n}\n\n// Opacity of a threshold band behind a line: enough to read as a zone, faint enough that the\n// line and its grid stay the subject. A `normal` band has no fill at all.\nconst BAND_ALPHA = 0.14;\n\nconst clamp = (v: number, lo: number, hi: number): number => Math.max(lo, Math.min(hi, v));\n\n// gauge/line fonts and tick counts scale with the smaller container dimension so a\n// widget stays legible when resized. A zero/absent size (before first measure, or\n// in a non-DOM test) falls back to a nominal medium size.\nfunction shortSide(size?: ElementSize): number {\n const s = Math.min(size?.width ?? 0, size?.height ?? 0);\n return s > 0 ? s : 200;\n}\n\nexport interface LineChartOptions {\n // Measurement names to plot, one series each. Empty \u2192 every name seen in the\n // window (first-seen order).\n measurements?: string[];\n // Unit suffix: named on the y-axis and appended to every tooltip value.\n unit?: string;\n // Decimal places for axis labels and tooltip values; unset uses the default rounding.\n precision?: number;\n // Severity thresholds, drawn as a labelled line each plus a faint band between them.\n // Any order: the builder sorts them.\n thresholds?: readonly Threshold[];\n // The board's density scale (useWidgetScale().scale): multiplies the axis-font clamp. Default 1.\n scale?: number;\n}\n\n// The markLine / markArea for a chart's thresholds, attached to the first series (the markers\n// belong to a series in ECharts, and every series shares the one y-axis).\nfunction thresholdMarks(theme: ChartTheme, thresholds: readonly Threshold[], unit: string | undefined, precision: number | undefined, fontSize: number) {\n const suffix = unit ? ` ${unit}` : '';\n // A `normal` threshold is still drawn: it is where the value RETURNS to normal, which an\n // operator wants to see. It is drawn in the muted text colour, the same ink as the axis\n // labels, which reads in light and dark and says \"boundary\" without saying \"alarm\".\n const ink = (t: Threshold) => levelColor(theme.severity, t.level) ?? theme.mutedForeground;\n const markLine = {\n silent: true,\n symbol: 'none',\n animation: false,\n data: thresholds.map((t) => ({\n yAxis: t.value,\n name: t.label ?? String(t.value),\n lineStyle: { color: ink(t), type: 'dashed' as const, width: 1.5 },\n label: {\n show: true,\n position: 'insideEndTop' as const,\n color: ink(t),\n fontSize,\n formatter: () => t.label ?? `${formatValue(t.value, precision)}${suffix}`,\n },\n })),\n };\n const markArea = {\n silent: true,\n animation: false,\n data: thresholdBands(thresholds, -Infinity, Infinity)\n .filter((b) => isAlarmLevel(b.level))\n .map((b) => [\n { yAxis: b.from, itemStyle: { color: severityFill(theme, b.level as AlarmLevel, BAND_ALPHA) } },\n { yAxis: b.to },\n ]),\n };\n return { markLine, markArea };\n}\n\n// buildLineOption maps a rolling window of samples to a multi-series time chart,\n// one line per measurement name, coloured from the theme's chart palette.\nexport function buildLineOption(\n samples: MeasurementSample[],\n theme: ChartTheme,\n options: LineChartOptions = {},\n size?: ElementSize,\n): EChartOption {\n const names = options.measurements?.length ? options.measurements : distinctNames(samples);\n const scale = options.scale ?? 1;\n const axisFont = clamp(Math.round(shortSide(size) / 22), Math.round(8 * scale), Math.round(12 * scale));\n\n // Re-parsed (validated + sorted): the builder is public, and a caller may hand it a list\n // that never went through parseThresholds. Every rule below assumes ascending order.\n const thresholds = parseThresholds(options.thresholds ?? []);\n const lowest = thresholds.length > 0 ? thresholds[0].value : 0;\n const highest = thresholds.length > 0 ? thresholds[thresholds.length - 1].value : 0;\n const marks = thresholds.length > 0 ? thresholdMarks(theme, thresholds, options.unit, options.precision, axisFont) : undefined;\n const suffix = options.unit ? ` ${options.unit}` : '';\n const precision = options.precision;\n\n const series = names.map((name, i) => {\n const color = theme.series[i % theme.series.length];\n return {\n ...(i === 0 ? marks : undefined),\n name,\n type: 'line' as const,\n showSymbol: false,\n lineStyle: { color },\n itemStyle: { color },\n data: samples\n .filter((s) => s.name === name && s.value != null && s.occurredTime != null)\n .map((s) => [s.occurredTime as string, s.value as number]),\n };\n });\n\n return {\n animation: false,\n textStyle: { color: theme.foreground, fontSize: axisFont },\n grid: { left: 8, right: 12, top: names.length > 1 || options.unit ? 28 : 12, bottom: 8, containLabel: true },\n tooltip: {\n trigger: 'axis',\n valueFormatter: (v: unknown) => (typeof v === 'number' ? `${formatValue(v, precision)}${suffix}` : String(v ?? '\u2014')),\n },\n legend:\n names.length > 1 ? { textStyle: { color: theme.mutedForeground, fontSize: axisFont }, top: 0 } : undefined,\n xAxis: {\n type: 'time',\n axisLine: { lineStyle: { color: theme.border } },\n axisLabel: { color: theme.mutedForeground, fontSize: axisFont, hideOverlap: true },\n },\n yAxis: {\n type: 'value',\n // The unit rides the axis once, at its head, rather than on every tick.\n name: options.unit,\n nameTextStyle: { color: theme.mutedForeground, fontSize: axisFont, align: 'left' as const },\n splitLine: { lineStyle: { color: theme.border } },\n axisLabel: {\n color: theme.mutedForeground,\n fontSize: axisFont,\n // Rounded only when the author asked; otherwise ECharts' own tick rounding applies.\n ...(precision != null ? { formatter: (v: number) => formatValue(v, precision) } : {}),\n },\n // A threshold outside the data's range must still be on the chart: extend the range to\n // take it in. Only then -- a returned number FIXES that end of the axis (no nice\n // rounding, no zero baseline), so a threshold inside the data returns null and leaves\n // ECharts' own auto range exactly as it is without thresholds.\n ...(thresholds.length > 0\n ? {\n min: (e: { min: number }) => (lowest < e.min ? lowest : null),\n max: (e: { max: number }) => (highest > e.max ? highest : null),\n }\n : {}),\n },\n series,\n };\n}\n\nexport interface GaugeChartOptions {\n min?: number;\n max?: number;\n unit?: string;\n // Decimal places for the readout; unset uses the default rounding.\n precision?: number;\n // Severity thresholds, drawn as coloured bands on the arc (any order).\n thresholds?: readonly Threshold[];\n // The board's density scale: multiplies the label-font clamp and the readout's floor. Default 1.\n scale?: number;\n // Where the number is drawn. 'canvas' (the default) paints it inside the dial, as the builder\n // always has, so a widget that renders only the chart keeps its readout. 'none' leaves the dial\n // bare for a caller that draws the number itself as DOM text in gaugeReadoutBox, which the\n // readability gate can measure (the built-in gauge does).\n readout?: 'canvas' | 'none';\n // Where the needle is with no value. 'min' (the default) rests it at the scale's minimum, as the\n // builder always has. 'none' draws no needle, so an empty dial cannot be read as a reading of\n // its minimum (the built-in gauge, whose readout already says there is no value, asks for it).\n emptyNeedle?: 'min' | 'none';\n}\n\n// The dial's geometry as fractions of its box: where its centre sits and how much of the short\n// side's half it fills. The DOM readout is placed from the same numbers (gaugeReadoutBox).\nconst GAUGE_CENTER_Y = 0.58;\nconst GAUGE_RADIUS = 0.95;\n// How far below the hub the readout sits, as a fraction of the radius.\nconst GAUGE_READOUT_OFFSET = 0.3;\n\n// The box (px, relative to the chart) the gauge's DOM readout is centred in and fitted to: under\n// the hub and inside the dial, where the canvas detail used to be drawn. A zero size (before the\n// first measure) takes the nominal one the builder assumes.\nexport function gaugeReadoutBox(size?: ElementSize): { centerX: number; centerY: number; width: number; height: number } {\n const s = shortSide(size);\n const w = (size?.width ?? 0) > 0 ? size!.width : s;\n const h = (size?.height ?? 0) > 0 ? size!.height : s;\n return {\n centerX: w / 2,\n centerY: GAUGE_CENTER_Y * h + GAUGE_READOUT_OFFSET * GAUGE_RADIUS * (s / 2),\n width: s * 0.7,\n height: s * 0.25,\n };\n}\n\n// Below this short side (px) the gauge drops its axis labels: at that size they would be\n// drawn at the 8 px floor, which is noise rather than a scale.\nexport const GAUGE_AXIS_LABELS_MIN_SIDE = 120;\n\n// The arc's colour stops: ECharts wants [[endFraction, colour], ...] ending at 1. A neutral\n// stretch (below the lowest threshold, or a `normal` band) takes the track colour. With no bands: one track.\nfunction gaugeArcColors(bands: ReturnType<typeof thresholdBands>, min: number, max: number, theme: ChartTheme): [number, string][] {\n if (bands.length === 0) return [[1, theme.border]];\n return bands.map((b, i): [number, string] => [\n i === bands.length - 1 ? 1 : (b.to - min) / (max - min),\n levelColor(theme.severity, b.level) ?? theme.border,\n ]);\n}\n\n// buildGaugeOption maps a single latest value to a gauge. A null value (nothing\n// received yet) shows an em dash and rests the needle at the minimum. Ticks, fonts,\n// and the arc width scale with the widget size so it stays legible when resized\n// (ECharts' default gauge crams ~10 labels at fixed fonts on a small canvas).\nexport function buildGaugeOption(\n value: number | null,\n theme: ChartTheme,\n options: GaugeChartOptions = {},\n size?: ElementSize,\n): EChartOption {\n const min = options.min ?? 0;\n const max = options.max ?? 100;\n const thresholds = parseThresholds(options.thresholds ?? []); // validated + sorted, as above\n const bands = thresholdBands(thresholds, min, max);\n // Banded only when a COLOURED band actually lands on the arc: thresholds that all sit above\n // `max`, or only `normal` bands, leave a plain neutral track, which keeps the progress arc\n // like any other gauge.\n const banded = bands.some((b) => isAlarmLevel(b.level));\n // The needle takes the colour of the band the value sits in; with no bands, below the\n // lowest, or in a `normal` band it keeps the chart accent.\n const accent = levelColor(theme.severity, bandAt(thresholds, value)?.level) ?? theme.series[0];\n\n const s = shortSide(size);\n const arcWidth = clamp(Math.round(s / 26), 4, 12);\n const scale = options.scale ?? 1;\n const labelFont = clamp(Math.round(s / 20), Math.round(8 * scale), Math.round(13 * scale));\n const showAxisLabels = s >= GAUGE_AXIS_LABELS_MIN_SIDE;\n const unitSuffix = options.unit ? ` ${options.unit}` : '';\n // The readout is fitted to the dial's inner box and its own length, like every big number.\n const readout = value == null ? '\u2014' : `${formatValue(value, options.precision)}${unitSuffix}`;\n const valueFont = fitValueFont({ width: s * 0.7, height: s * 0.25 }, readout.length, {\n floor: Math.round(FIT_MIN_FONT_PX * scale),\n });\n return {\n animation: false,\n series: [\n {\n type: 'gauge',\n min,\n max,\n splitNumber: s < 160 ? 4 : 5, // fewer ticks than the default 10 \u2192 no cram\n radius: `${GAUGE_RADIUS * 100}%`,\n center: ['50%', `${GAUGE_CENTER_Y * 100}%`],\n // The progress arc would paint over the bands, so with bands the needle alone marks the value.\n progress: { show: !banded, width: arcWidth, itemStyle: { color: accent } },\n pointer: {\n show: value != null || options.emptyNeedle !== 'none',\n itemStyle: { color: accent },\n length: '55%',\n width: clamp(Math.round(s / 50), 2, 6),\n },\n anchor: { show: false },\n axisLine: { lineStyle: { width: arcWidth, color: gaugeArcColors(bands, min, max, theme) } },\n axisTick: { show: false },\n splitLine: { length: arcWidth, lineStyle: { color: theme.border, width: 1 } },\n axisLabel: { show: showAxisLabels, color: theme.mutedForeground, fontSize: labelFont, distance: arcWidth + 2 },\n detail:\n options.readout === 'none'\n ? { show: false }\n : {\n valueAnimation: false,\n color: theme.foreground,\n fontSize: valueFont,\n offsetCenter: [0, `${GAUGE_READOUT_OFFSET * 100}%`],\n formatter: (v: number) => (value == null ? '\u2014' : `${formatValue(v, options.precision)}${unitSuffix}`),\n },\n data: [{ value: value ?? min }],\n },\n ],\n };\n}\n\n// A heading in degrees folded onto [0, 360): 370 is 10 and -90 is 270, because a compass has no\n// \"beyond a full turn\" and a bearing sensor may well report either. 360 is 0, never 360, and a\n// value that is not a finite number has no heading at all (undefined), not a plausible one.\nexport function wrapHeading(value: number | null | undefined): number | undefined {\n if (value == null || !Number.isFinite(value)) return undefined;\n return ((value % 360) + 360) % 360;\n}\n\nexport interface CompassChartOptions {\n // The four point labels in N, E, S, W order (a locale writes West as \"O\", so the caller owns them).\n cardinals: readonly [string, string, string, string];\n thresholds?: readonly Threshold[];\n // The board's density scale; multiplies the label-font clamp. Default 1.\n scale?: number;\n}\n\n// buildCompassOption draws a full turn on the registered gauge series: north at the top, the\n// angle running clockwise, so 90 is east. The needle takes the colour of the band the (wrapped)\n// heading is in and the chart accent in a `normal` band or none. The degrees themselves are DOM\n// text beside it (compass.tsx). With no reading the needle rests at north.\nexport function buildCompassOption(\n value: number | null,\n theme: ChartTheme,\n options: CompassChartOptions,\n size?: ElementSize,\n): EChartOption {\n const heading = wrapHeading(value);\n const thresholds = parseThresholds(options.thresholds ?? []);\n const bands = thresholdBands(thresholds, 0, 360);\n const accent = levelColor(theme.severity, bandAt(thresholds, heading ?? null)?.level) ?? theme.series[0];\n const s = shortSide(size);\n const scale = options.scale ?? 1;\n const arcWidth = clamp(Math.round(s / 30), 3, 10);\n const labelFont = clamp(Math.round(s / 11), Math.round(10 * scale), Math.round(20 * scale));\n const [north, east, south, west] = options.cardinals;\n const points: Record<number, string> = { 0: north, 90: east, 180: south, 270: west };\n return {\n animation: false,\n series: [\n {\n type: 'gauge',\n min: 0,\n max: 360,\n startAngle: 90,\n endAngle: -270,\n splitNumber: 4,\n radius: '92%',\n center: ['50%', '50%'],\n progress: { show: false },\n pointer: { itemStyle: { color: accent }, length: '62%', width: clamp(Math.round(s / 36), 3, 8) },\n anchor: { show: true, size: clamp(Math.round(s / 24), 5, 12), itemStyle: { color: accent } },\n axisLine: { lineStyle: { width: arcWidth, color: gaugeArcColors(bands, 0, 360, theme) } },\n axisTick: { show: s >= GAUGE_AXIS_LABELS_MIN_SIDE, splitNumber: 3, length: arcWidth, lineStyle: { color: theme.border, width: 1 } },\n splitLine: { length: arcWidth * 1.6, lineStyle: { color: theme.mutedForeground, width: 2 } },\n // Only the four points are labelled; 360 is the same tick as 0, so it stays blank.\n axisLabel: {\n show: true,\n color: theme.foreground,\n fontSize: labelFont,\n distance: arcWidth * 1.6 + 6,\n formatter: (v: number) => points[v] ?? '',\n },\n detail: { show: false },\n data: [{ value: heading ?? 0 }],\n },\n ],\n };\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// The chart colour theme: concrete colours resolved from the host's CSS custom properties,\n// because an ECharts canvas cannot resolve `var(--x)`. Pure DOM + React, no echarts import,\n// but it lives on the `/echarts` subpath with the rest of the chart helpers so the SDK root\n// stays chart-free.\n\nimport { useMemo, useSyncExternalStore } from 'react';\n\nimport { useWidgetThemeRoot } from '../context';\nimport { severityColorsAt, severityFallbackColors, type AlarmLevel } from '../severity';\n\n// Concrete colors for an ECharts canvas, resolved from the CSS variables in scope\n// at a given element (charts can't use CSS vars directly).\nexport interface ChartTheme {\n foreground: string;\n mutedForeground: string;\n border: string;\n series: string[];\n // The four coloured threshold levels, concrete (comma-form `hsl(h, s%, l%)`, which every\n // canvas colour parser accepts and which severityFill can add an alpha to). `normal` has\n // none: it renders in the widget's own neutral (see severity.ts).\n severity: Record<AlarmLevel, string>;\n}\n\n// Fallbacks used when a variable is absent (e.g. jsdom, or a host that hasn't set\n// the token) so a chart still renders in sensible default colors.\nconst FALLBACK: ChartTheme = {\n foreground: 'hsl(240 10% 3.9%)',\n mutedForeground: 'hsl(240 3.8% 46.1%)',\n border: 'hsl(240 5.9% 90%)',\n series: [\n 'hsl(220 70% 50%)',\n 'hsl(160 60% 45%)',\n 'hsl(30 80% 55%)',\n 'hsl(280 65% 60%)',\n 'hsl(340 75% 55%)',\n ],\n severity: severityFallbackColors(),\n};\n\nfunction readVar(style: CSSStyleDeclaration, name: string, fallback: string): string {\n const raw = style.getPropertyValue(`--${name}`).trim();\n return raw ? `hsl(${raw})` : fallback;\n}\n\n// resolveChartTheme reads the theme tokens in scope at `el` into concrete colors\n// for ECharts. Reading at the element (not the document root) means a widget\n// nested under a re-themed subtree picks up the local override.\nexport function resolveChartTheme(el: Element): ChartTheme {\n const style = getComputedStyle(el);\n return {\n foreground: readVar(style, 'foreground', FALLBACK.foreground),\n mutedForeground: readVar(style, 'muted-foreground', FALLBACK.mutedForeground),\n border: readVar(style, 'border', FALLBACK.border),\n series: FALLBACK.series.map((fallback, i) => readVar(style, `chart-${i + 1}`, fallback)),\n severity: severityColorsAt(el, style),\n };\n}\n\n// --- shared chart-theme store ------------------------------------------------\n//\n// Charts need concrete colors resolved from the CSS-variable theme. Rather than\n// have every chart widget mount its own MutationObserver on <html> and recompute\n// the same theme, one page-wide observer resolves it once per change and every\n// widget reads the shared, memoized value via useSyncExternalStore \u2014 so a busy\n// dashboard pays O(1), not O(number of charts), per theme toggle.\n\nlet sharedTheme: ChartTheme | null = null;\nconst themeListeners = new Set<() => void>();\nlet themeObserver: MutationObserver | null = null;\n\nfunction recomputeTheme(): void {\n sharedTheme = resolveChartTheme(document.documentElement);\n}\n\nfunction subscribeTheme(listener: () => void): () => void {\n if (!themeObserver) {\n recomputeTheme();\n themeObserver = new MutationObserver(() => {\n recomputeTheme();\n for (const l of themeListeners) l();\n });\n themeObserver.observe(document.documentElement, {\n attributes: true,\n attributeFilter: ['class', 'style'],\n });\n }\n themeListeners.add(listener);\n return () => {\n themeListeners.delete(listener);\n if (themeListeners.size === 0 && themeObserver) {\n themeObserver.disconnect();\n themeObserver = null;\n sharedTheme = null;\n }\n };\n}\n\nfunction getThemeSnapshot(): ChartTheme {\n if (!sharedTheme) recomputeTheme();\n return sharedTheme as ChartTheme;\n}\n\n// useChartTheme returns the current chart colors and re-renders the caller when\n// the document theme changes (a light/dark class or inline-style swap on <html>).\nexport function useChartTheme(): ChartTheme {\n const shared = useSyncExternalStore(subscribeTheme, getThemeSnapshot, getThemeSnapshot);\n // A host that scopes its tokens to a container says so (WidgetThemeRoot); its tokens are\n // fixed for the container's life, so they are read once there rather than observed.\n const root = useWidgetThemeRoot();\n const scoped = useMemo(() => (root ? resolveChartTheme(root) : null), [root]);\n return scoped ?? shared;\n}\n\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// A thin React wrapper over Apache ECharts (ADR-039 chart engine).\n//\n// \uD83D\uDD34 ECHARTS IS NOT IMPORTED HERE. The engine (ECharts + zrender, ~170 KB gzip, the largest single\n// dependency of a board) lives in echart-engine.ts and is loaded on demand, ONCE, as one chunk:\n// the first <EChart> to mount fetches it, and a host that wants the first frame complete fetches\n// it first with loadChartEngine (preloadWidgets does, for a board that holds a chart). Only TYPES\n// come from echarts here, and they are erased.\n//\n// The public surface is unchanged: <EChart option> is still a synchronous component. Once the\n// engine is here, a mount initialises the chart exactly as it always did; before it is, the\n// component renders its (empty) container and draws as soon as the engine arrives. A failed load\n// is THROWN, so the widget's error boundary shows it rather than an empty box that looks like a\n// chart with nothing to say, and it is NOT remembered: the next mount (the boundary's Reset)\n// fetches again, and nothing retries on its own.\n//\n// The wrapper owns the imperative lifecycle: init on mount, setOption on change, resize with the\n// container, dispose on unmount.\n\nimport type * as echarts from 'echarts/core';\nimport { useEffect, useRef, useState } from 'react';\n\ntype Engine = typeof import('./echart-engine');\n\nlet engine: Engine | undefined;\nlet engineLoading: Promise<Engine> | undefined;\n\n// Memoized while in flight or loaded; a rejection is forgotten, so the next caller fetches again.\nfunction loadEngine(): Promise<Engine> {\n engineLoading ??= import('./echart-engine').then(\n (m) => (engine = m),\n (err: unknown) => {\n engineLoading = undefined;\n throw err;\n },\n );\n return engineLoading;\n}\n\n/**\n * Fetches the chart engine that <EChart> draws with, so the charts a host renders after awaiting\n * it draw on their first frame. Optional: an <EChart> that mounts first loads the engine itself.\n * Rejects if the engine cannot be fetched; a later call (or mount) fetches it again.\n */\nexport function loadChartEngine(): Promise<void> {\n return loadEngine().then(() => undefined);\n}\n\n// markLine / markArea are registered on first use (see echart-markers.ts for why). Until they\n// are, a chart that asks for them draws without them, and EChart re-applies its option once\n// they arrive -- a beat later on the first threshold chart of a page, never silently absent.\nlet markersReady = false;\nlet markersLoading: Promise<void> | undefined;\n\nexport function loadThresholdMarkers(): Promise<void> {\n markersLoading ??= loadEngine()\n .then((e) => e.registerThresholdMarkers())\n .then(\n () => {\n markersReady = true;\n },\n (err: unknown) => {\n markersLoading = undefined; // a failed chunk load is retried by the next chart that needs it\n throw err;\n },\n );\n return markersLoading;\n}\n\n// True when any series of the option carries a markLine or markArea.\nexport function optionUsesMarkers(option: EChartOption): boolean {\n const series = (option as { series?: unknown }).series;\n const list = Array.isArray(series) ? series : series ? [series] : [];\n return list.some((s) => {\n const m = s as { markLine?: unknown; markArea?: unknown } | null;\n return typeof m === 'object' && m !== null && (m.markLine != null || m.markArea != null);\n });\n}\n\n// A resolved ECharts option object (the shape echarts.setOption accepts). Widgets\n// build these with the pure builders in chart-options.ts.\nexport type EChartOption = echarts.EChartsCoreOption;\n\nexport interface EChartProps {\n option: EChartOption;\n className?: string;\n style?: React.CSSProperties;\n}\n\n// EChart fills its parent; the dashboard canvas sizes the parent, and a\n// ResizeObserver keeps the chart matched to it.\nexport function EChart({ option, className, style }: EChartProps) {\n const containerRef = useRef<HTMLDivElement>(null);\n const chartRef = useRef<echarts.ECharts | null>(null);\n const [loaded, setLoaded] = useState(engine);\n const [failed, setFailed] = useState<{ error: unknown }>();\n const [markersLoaded, setMarkersLoaded] = useState(markersReady);\n const needsMarkers = optionUsesMarkers(option) && !markersLoaded;\n\n // The engine, when this is the first chart to mount and nobody preloaded it.\n useEffect(() => {\n if (loaded) return;\n let live = true;\n loadEngine().then(\n (e) => live && setLoaded(e),\n (error: unknown) => live && setFailed({ error }),\n );\n return () => {\n live = false;\n };\n }, [loaded]);\n\n useEffect(() => {\n if (!needsMarkers) return;\n let live = true;\n loadThresholdMarkers().then(\n () => live && setMarkersLoaded(true),\n // Loud, not silent: the chart still draws, without its threshold lines.\n (err: unknown) => console.error('EChart: the threshold markers failed to load', err),\n );\n return () => {\n live = false;\n };\n }, [needsMarkers]);\n\n // Init + teardown once for the element's lifetime (from the moment the engine is here).\n useEffect(() => {\n const el = containerRef.current;\n if (!el || !loaded) return;\n\n const chart = loaded.init(el);\n chartRef.current = chart;\n\n const observer = new ResizeObserver(() => chart.resize());\n observer.observe(el);\n\n return () => {\n observer.disconnect();\n chart.dispose();\n chartRef.current = null;\n };\n }, [loaded]);\n\n // Re-apply the option whenever it changes. notMerge:true so a shrinking series\n // (rolling window) doesn't leave stale points behind.\n useEffect(() => {\n chartRef.current?.setOption(option, { notMerge: true });\n }, [option, markersLoaded, loaded]);\n\n // To the widget's error boundary: a chart that cannot draw says so.\n if (failed) throw failed.error;\n\n // `data-echart` marks the chart's box, so a host (and the readability gate) can tell a chart\n // that has drawn (ECharts stamps `_echarts_instance_` on it) from one still waiting.\n return (\n <div ref={containerRef} data-echart=\"\" className={className} style={{ width: '100%', height: '100%', ...style }} />\n );\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Pure ECharts option builders. Kept free of React/DOM so the data\u2192chart mapping\n// is unit-testable without a canvas: a widget resolves a ChartTheme + samples and\n// hands the result straight to <EChart>.\n\nimport type { MeasurementSample } from '@devicechain/dashboards';\n\nimport { FIT_MIN_FONT_PX, fitValueFont } from '../fit-text';\nimport { formatValue } from '../format';\nimport type { ElementSize } from '../hooks';\nimport { isAlarmLevel, levelColor, type AlarmLevel } from '../severity';\nimport { bandAt, parseThresholds, thresholdBands, type Threshold } from '../thresholds';\n\nimport type { ChartTheme } from './chart-theme';\nimport type { EChartOption } from './echart';\n\n// distinctNames returns the measurement names present in `samples`, in first-seen\n// order \u2014 used when a chart's datasource doesn't pin an explicit series list.\nfunction distinctNames(samples: MeasurementSample[]): string[] {\n const seen: string[] = [];\n for (const s of samples) {\n if (!seen.includes(s.name)) seen.push(s.name);\n }\n return seen;\n}\n\n// A severity colour with an alpha, for a band drawn UNDER the data (markArea). The theme's\n// severity colours are comma-form hsl(), so the alpha form is a string edit.\nexport function severityFill(theme: ChartTheme, level: AlarmLevel, alpha: number): string {\n return theme.severity[level].replace(/^hsl\\(/, 'hsla(').replace(/\\)$/, `, ${alpha})`);\n}\n\n// Opacity of a threshold band behind a line: enough to read as a zone, faint enough that the\n// line and its grid stay the subject. A `normal` band has no fill at all.\nconst BAND_ALPHA = 0.14;\n\nconst clamp = (v: number, lo: number, hi: number): number => Math.max(lo, Math.min(hi, v));\n\n// gauge/line fonts and tick counts scale with the smaller container dimension so a\n// widget stays legible when resized. A zero/absent size (before first measure, or\n// in a non-DOM test) falls back to a nominal medium size.\nfunction shortSide(size?: ElementSize): number {\n const s = Math.min(size?.width ?? 0, size?.height ?? 0);\n return s > 0 ? s : 200;\n}\n\nexport interface LineChartOptions {\n // Measurement names to plot, one series each. Empty \u2192 every name seen in the\n // window (first-seen order).\n measurements?: string[];\n // Unit suffix: named on the y-axis and appended to every tooltip value.\n unit?: string;\n // Decimal places for axis labels and tooltip values; unset uses the default rounding.\n precision?: number;\n // Severity thresholds, drawn as a labelled line each plus a faint band between them.\n // Any order: the builder sorts them.\n thresholds?: readonly Threshold[];\n // The board's density scale (useWidgetScale().scale): multiplies the axis-font clamp. Default 1.\n scale?: number;\n // Instants (ISO-8601, oldest first) after which nothing is known until the next sample: the live\n // feed was interrupted (MeasurementStreamState.gaps). Every line breaks at each one instead of\n // joining the samples on either side.\n gaps?: readonly string[];\n}\n\n// breakAtGaps puts an empty point -- which ECharts draws as a break in the line -- at each gap\n// that falls between two of a series' points. Points are in sample order (chronological).\nfunction breakAtGaps(points: Array<[string, number]>, gaps: readonly number[]): Array<[string, number | null]> {\n let g = 0;\n return points.flatMap((point, i): Array<[string, number | null]> => {\n // The last gap before this point; past the end of `gaps` the comparison is false.\n let gap = Number.NaN;\n while (gaps[g] < Date.parse(point[0])) gap = gaps[g++];\n return i > 0 && !Number.isNaN(gap) ? [[new Date(gap).toISOString(), null], point] : [point];\n });\n}\n\n// The markLine / markArea for a chart's thresholds, attached to the first series (the markers\n// belong to a series in ECharts, and every series shares the one y-axis).\nfunction thresholdMarks(theme: ChartTheme, thresholds: readonly Threshold[], unit: string | undefined, precision: number | undefined, fontSize: number) {\n const suffix = unit ? ` ${unit}` : '';\n // A `normal` threshold is still drawn: it is where the value RETURNS to normal, which an\n // operator wants to see. It is drawn in the muted text colour, the same ink as the axis\n // labels, which reads in light and dark and says \"boundary\" without saying \"alarm\".\n const ink = (t: Threshold) => levelColor(theme.severity, t.level) ?? theme.mutedForeground;\n const markLine = {\n silent: true,\n symbol: 'none',\n animation: false,\n data: thresholds.map((t) => ({\n yAxis: t.value,\n name: t.label ?? String(t.value),\n lineStyle: { color: ink(t), type: 'dashed' as const, width: 1.5 },\n label: {\n show: true,\n position: 'insideEndTop' as const,\n color: ink(t),\n fontSize,\n formatter: () => t.label ?? `${formatValue(t.value, precision)}${suffix}`,\n },\n })),\n };\n const markArea = {\n silent: true,\n animation: false,\n data: thresholdBands(thresholds, -Infinity, Infinity)\n .filter((b) => isAlarmLevel(b.level))\n .map((b) => [\n { yAxis: b.from, itemStyle: { color: severityFill(theme, b.level as AlarmLevel, BAND_ALPHA) } },\n { yAxis: b.to },\n ]),\n };\n return { markLine, markArea };\n}\n\n// buildLineOption maps a rolling window of samples to a multi-series time chart,\n// one line per measurement name, coloured from the theme's chart palette.\nexport function buildLineOption(\n samples: MeasurementSample[],\n theme: ChartTheme,\n options: LineChartOptions = {},\n size?: ElementSize,\n): EChartOption {\n const names = options.measurements?.length ? options.measurements : distinctNames(samples);\n const scale = options.scale ?? 1;\n const axisFont = clamp(Math.round(shortSide(size) / 22), Math.round(8 * scale), Math.round(12 * scale));\n\n // Re-parsed (validated + sorted): the builder is public, and a caller may hand it a list\n // that never went through parseThresholds. Every rule below assumes ascending order.\n const thresholds = parseThresholds(options.thresholds ?? []);\n const lowest = thresholds.length > 0 ? thresholds[0].value : 0;\n const highest = thresholds.length > 0 ? thresholds[thresholds.length - 1].value : 0;\n const marks = thresholds.length > 0 ? thresholdMarks(theme, thresholds, options.unit, options.precision, axisFont) : undefined;\n const suffix = options.unit ? ` ${options.unit}` : '';\n const precision = options.precision;\n const gaps = (options.gaps ?? []).map(Date.parse).filter((g) => !Number.isNaN(g));\n\n const series = names.map((name, i) => {\n const color = theme.series[i % theme.series.length];\n return {\n ...(i === 0 ? marks : undefined),\n name,\n type: 'line' as const,\n showSymbol: false,\n lineStyle: { color },\n itemStyle: { color },\n data: breakAtGaps(\n samples\n .filter((s) => s.name === name && s.value != null && s.occurredTime != null)\n .map((s): [string, number] => [s.occurredTime as string, s.value as number]),\n gaps,\n ),\n };\n });\n\n return {\n animation: false,\n textStyle: { color: theme.foreground, fontSize: axisFont },\n grid: { left: 8, right: 12, top: names.length > 1 || options.unit ? 28 : 12, bottom: 8, containLabel: true },\n tooltip: {\n trigger: 'axis',\n valueFormatter: (v: unknown) => (typeof v === 'number' ? `${formatValue(v, precision)}${suffix}` : String(v ?? '\u2014')),\n },\n legend:\n names.length > 1 ? { textStyle: { color: theme.mutedForeground, fontSize: axisFont }, top: 0 } : undefined,\n xAxis: {\n type: 'time',\n axisLine: { lineStyle: { color: theme.border } },\n axisLabel: { color: theme.mutedForeground, fontSize: axisFont, hideOverlap: true },\n },\n yAxis: {\n type: 'value',\n // The unit rides the axis once, at its head, rather than on every tick.\n name: options.unit,\n nameTextStyle: { color: theme.mutedForeground, fontSize: axisFont, align: 'left' as const },\n splitLine: { lineStyle: { color: theme.border } },\n axisLabel: {\n color: theme.mutedForeground,\n fontSize: axisFont,\n // Rounded only when the author asked; otherwise ECharts' own tick rounding applies.\n ...(precision != null ? { formatter: (v: number) => formatValue(v, precision) } : {}),\n },\n // A threshold outside the data's range must still be on the chart: extend the range to\n // take it in. Only then -- a returned number FIXES that end of the axis (no nice\n // rounding, no zero baseline), so a threshold inside the data returns null and leaves\n // ECharts' own auto range exactly as it is without thresholds.\n ...(thresholds.length > 0\n ? {\n min: (e: { min: number }) => (lowest < e.min ? lowest : null),\n max: (e: { max: number }) => (highest > e.max ? highest : null),\n }\n : {}),\n },\n series,\n };\n}\n\nexport interface GaugeChartOptions {\n min?: number;\n max?: number;\n unit?: string;\n // Decimal places for the readout; unset uses the default rounding.\n precision?: number;\n // Severity thresholds, drawn as coloured bands on the arc (any order).\n thresholds?: readonly Threshold[];\n // The board's density scale: multiplies the label-font clamp and the readout's floor. Default 1.\n scale?: number;\n // Where the number is drawn. 'canvas' (the default) paints it inside the dial, as the builder\n // always has, so a widget that renders only the chart keeps its readout. 'none' leaves the dial\n // bare for a caller that draws the number itself as DOM text in gaugeReadoutBox, which the\n // readability gate can measure (the built-in gauge does).\n readout?: 'canvas' | 'none';\n // Where the needle is with no value. 'min' (the default) rests it at the scale's minimum, as the\n // builder always has. 'none' draws no needle, so an empty dial cannot be read as a reading of\n // its minimum (the built-in gauge, whose readout already says there is no value, asks for it).\n emptyNeedle?: 'min' | 'none';\n}\n\n// The dial's geometry as fractions of its box: where its centre sits and how much of the short\n// side's half it fills. The DOM readout is placed from the same numbers (gaugeReadoutBox).\nconst GAUGE_CENTER_Y = 0.58;\nconst GAUGE_RADIUS = 0.95;\n// How far below the hub the readout sits, as a fraction of the radius.\nconst GAUGE_READOUT_OFFSET = 0.3;\n\n// The box (px, relative to the chart) the gauge's DOM readout is centred in and fitted to: under\n// the hub and inside the dial, where the canvas detail used to be drawn. A zero size (before the\n// first measure) takes the nominal one the builder assumes.\nexport function gaugeReadoutBox(size?: ElementSize): { centerX: number; centerY: number; width: number; height: number } {\n const s = shortSide(size);\n const w = (size?.width ?? 0) > 0 ? size!.width : s;\n const h = (size?.height ?? 0) > 0 ? size!.height : s;\n return {\n centerX: w / 2,\n centerY: GAUGE_CENTER_Y * h + GAUGE_READOUT_OFFSET * GAUGE_RADIUS * (s / 2),\n width: s * 0.7,\n height: s * 0.25,\n };\n}\n\n// Below this short side (px) the gauge drops its axis labels: at that size they would be\n// drawn at the 8 px floor, which is noise rather than a scale.\nexport const GAUGE_AXIS_LABELS_MIN_SIDE = 120;\n\n// The arc's colour stops: ECharts wants [[endFraction, colour], ...] ending at 1. A neutral\n// stretch (below the lowest threshold, or a `normal` band) takes the track colour. With no bands: one track.\nfunction gaugeArcColors(bands: ReturnType<typeof thresholdBands>, min: number, max: number, theme: ChartTheme): [number, string][] {\n if (bands.length === 0) return [[1, theme.border]];\n return bands.map((b, i): [number, string] => [\n i === bands.length - 1 ? 1 : (b.to - min) / (max - min),\n levelColor(theme.severity, b.level) ?? theme.border,\n ]);\n}\n\n// buildGaugeOption maps a single latest value to a gauge. A null value (nothing\n// received yet) shows an em dash and rests the needle at the minimum. Ticks, fonts,\n// and the arc width scale with the widget size so it stays legible when resized\n// (ECharts' default gauge crams ~10 labels at fixed fonts on a small canvas).\nexport function buildGaugeOption(\n value: number | null,\n theme: ChartTheme,\n options: GaugeChartOptions = {},\n size?: ElementSize,\n): EChartOption {\n const min = options.min ?? 0;\n const max = options.max ?? 100;\n const thresholds = parseThresholds(options.thresholds ?? []); // validated + sorted, as above\n const bands = thresholdBands(thresholds, min, max);\n // Banded only when a COLOURED band actually lands on the arc: thresholds that all sit above\n // `max`, or only `normal` bands, leave a plain neutral track, which keeps the progress arc\n // like any other gauge.\n const banded = bands.some((b) => isAlarmLevel(b.level));\n // The needle takes the colour of the band the value sits in; with no bands, below the\n // lowest, or in a `normal` band it keeps the chart accent.\n const accent = levelColor(theme.severity, bandAt(thresholds, value)?.level) ?? theme.series[0];\n\n const s = shortSide(size);\n const arcWidth = clamp(Math.round(s / 26), 4, 12);\n const scale = options.scale ?? 1;\n const labelFont = clamp(Math.round(s / 20), Math.round(8 * scale), Math.round(13 * scale));\n const showAxisLabels = s >= GAUGE_AXIS_LABELS_MIN_SIDE;\n const unitSuffix = options.unit ? ` ${options.unit}` : '';\n // The readout is fitted to the dial's inner box and its own length, like every big number.\n const readout = value == null ? '\u2014' : `${formatValue(value, options.precision)}${unitSuffix}`;\n const valueFont = fitValueFont({ width: s * 0.7, height: s * 0.25 }, readout.length, {\n floor: Math.round(FIT_MIN_FONT_PX * scale),\n });\n return {\n animation: false,\n series: [\n {\n type: 'gauge',\n min,\n max,\n splitNumber: s < 160 ? 4 : 5, // fewer ticks than the default 10 \u2192 no cram\n radius: `${GAUGE_RADIUS * 100}%`,\n center: ['50%', `${GAUGE_CENTER_Y * 100}%`],\n // The progress arc would paint over the bands, so with bands the needle alone marks the value.\n progress: { show: !banded, width: arcWidth, itemStyle: { color: accent } },\n pointer: {\n show: value != null || options.emptyNeedle !== 'none',\n itemStyle: { color: accent },\n length: '55%',\n width: clamp(Math.round(s / 50), 2, 6),\n },\n anchor: { show: false },\n axisLine: { lineStyle: { width: arcWidth, color: gaugeArcColors(bands, min, max, theme) } },\n axisTick: { show: false },\n splitLine: { length: arcWidth, lineStyle: { color: theme.border, width: 1 } },\n axisLabel: { show: showAxisLabels, color: theme.mutedForeground, fontSize: labelFont, distance: arcWidth + 2 },\n detail:\n options.readout === 'none'\n ? { show: false }\n : {\n valueAnimation: false,\n color: theme.foreground,\n fontSize: valueFont,\n offsetCenter: [0, `${GAUGE_READOUT_OFFSET * 100}%`],\n formatter: (v: number) => (value == null ? '\u2014' : `${formatValue(v, options.precision)}${unitSuffix}`),\n },\n data: [{ value: value ?? min }],\n },\n ],\n };\n}\n\n// A heading in degrees folded onto [0, 360): 370 is 10 and -90 is 270, because a compass has no\n// \"beyond a full turn\" and a bearing sensor may well report either. 360 is 0, never 360, and a\n// value that is not a finite number has no heading at all (undefined), not a plausible one.\nexport function wrapHeading(value: number | null | undefined): number | undefined {\n if (value == null || !Number.isFinite(value)) return undefined;\n return ((value % 360) + 360) % 360;\n}\n\nexport interface CompassChartOptions {\n // The four point labels in N, E, S, W order (a locale writes West as \"O\", so the caller owns them).\n cardinals: readonly [string, string, string, string];\n thresholds?: readonly Threshold[];\n // The board's density scale; multiplies the label-font clamp. Default 1.\n scale?: number;\n}\n\n// buildCompassOption draws a full turn on the registered gauge series: north at the top, the\n// angle running clockwise, so 90 is east. The needle takes the colour of the band the (wrapped)\n// heading is in and the chart accent in a `normal` band or none. The degrees themselves are DOM\n// text beside it (compass.tsx). With no reading the needle rests at north.\nexport function buildCompassOption(\n value: number | null,\n theme: ChartTheme,\n options: CompassChartOptions,\n size?: ElementSize,\n): EChartOption {\n const heading = wrapHeading(value);\n const thresholds = parseThresholds(options.thresholds ?? []);\n const bands = thresholdBands(thresholds, 0, 360);\n const accent = levelColor(theme.severity, bandAt(thresholds, heading ?? null)?.level) ?? theme.series[0];\n const s = shortSide(size);\n const scale = options.scale ?? 1;\n const arcWidth = clamp(Math.round(s / 30), 3, 10);\n const labelFont = clamp(Math.round(s / 11), Math.round(10 * scale), Math.round(20 * scale));\n const [north, east, south, west] = options.cardinals;\n const points: Record<number, string> = { 0: north, 90: east, 180: south, 270: west };\n return {\n animation: false,\n series: [\n {\n type: 'gauge',\n min: 0,\n max: 360,\n startAngle: 90,\n endAngle: -270,\n splitNumber: 4,\n radius: '92%',\n center: ['50%', '50%'],\n progress: { show: false },\n pointer: { itemStyle: { color: accent }, length: '62%', width: clamp(Math.round(s / 36), 3, 8) },\n anchor: { show: true, size: clamp(Math.round(s / 24), 5, 12), itemStyle: { color: accent } },\n axisLine: { lineStyle: { width: arcWidth, color: gaugeArcColors(bands, 0, 360, theme) } },\n axisTick: { show: s >= GAUGE_AXIS_LABELS_MIN_SIDE, splitNumber: 3, length: arcWidth, lineStyle: { color: theme.border, width: 1 } },\n splitLine: { length: arcWidth * 1.6, lineStyle: { color: theme.mutedForeground, width: 2 } },\n // Only the four points are labelled; 360 is the same tick as 0, so it stays blank.\n axisLabel: {\n show: true,\n color: theme.foreground,\n fontSize: labelFont,\n distance: arcWidth * 1.6 + 6,\n formatter: (v: number) => points[v] ?? '',\n },\n detail: { show: false },\n data: [{ value: heading ?? 0 }],\n },\n ],\n };\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// The chart colour theme: concrete colours resolved from the host's CSS custom properties,\n// because an ECharts canvas cannot resolve `var(--x)`. Pure DOM + React, no echarts import,\n// but it lives on the `/echarts` subpath with the rest of the chart helpers so the SDK root\n// stays chart-free.\n\nimport { useMemo, useSyncExternalStore } from 'react';\n\nimport { useWidgetThemeRoot } from '../context';\nimport { severityColorsAt, severityFallbackColors, type AlarmLevel } from '../severity';\n\n// Concrete colors for an ECharts canvas, resolved from the CSS variables in scope\n// at a given element (charts can't use CSS vars directly).\nexport interface ChartTheme {\n foreground: string;\n mutedForeground: string;\n border: string;\n series: string[];\n // The four coloured threshold levels, concrete (comma-form `hsl(h, s%, l%)`, which every\n // canvas colour parser accepts and which severityFill can add an alpha to). `normal` has\n // none: it renders in the widget's own neutral (see severity.ts).\n severity: Record<AlarmLevel, string>;\n}\n\n// Fallbacks used when a variable is absent (e.g. jsdom, or a host that hasn't set\n// the token) so a chart still renders in sensible default colors.\nconst FALLBACK: ChartTheme = {\n foreground: 'hsl(240 10% 3.9%)',\n mutedForeground: 'hsl(240 3.8% 46.1%)',\n border: 'hsl(240 5.9% 90%)',\n series: [\n 'hsl(220 70% 50%)',\n 'hsl(160 60% 45%)',\n 'hsl(30 80% 55%)',\n 'hsl(280 65% 60%)',\n 'hsl(340 75% 55%)',\n ],\n severity: severityFallbackColors(),\n};\n\nfunction readVar(style: CSSStyleDeclaration, name: string, fallback: string): string {\n const raw = style.getPropertyValue(`--${name}`).trim();\n return raw ? `hsl(${raw})` : fallback;\n}\n\n// resolveChartTheme reads the theme tokens in scope at `el` into concrete colors\n// for ECharts. Reading at the element (not the document root) means a widget\n// nested under a re-themed subtree picks up the local override.\nexport function resolveChartTheme(el: Element): ChartTheme {\n const style = getComputedStyle(el);\n return {\n foreground: readVar(style, 'foreground', FALLBACK.foreground),\n mutedForeground: readVar(style, 'muted-foreground', FALLBACK.mutedForeground),\n border: readVar(style, 'border', FALLBACK.border),\n series: FALLBACK.series.map((fallback, i) => readVar(style, `chart-${i + 1}`, fallback)),\n severity: severityColorsAt(el, style),\n };\n}\n\n// --- shared chart-theme store ------------------------------------------------\n//\n// Charts need concrete colors resolved from the CSS-variable theme. Rather than\n// have every chart widget mount its own MutationObserver on <html> and recompute\n// the same theme, one page-wide observer resolves it once per change and every\n// widget reads the shared, memoized value via useSyncExternalStore \u2014 so a busy\n// dashboard pays O(1), not O(number of charts), per theme toggle.\n\nlet sharedTheme: ChartTheme | null = null;\nconst themeListeners = new Set<() => void>();\nlet themeObserver: MutationObserver | null = null;\n\nfunction recomputeTheme(): void {\n sharedTheme = resolveChartTheme(document.documentElement);\n}\n\nfunction subscribeTheme(listener: () => void): () => void {\n if (!themeObserver) {\n recomputeTheme();\n themeObserver = new MutationObserver(() => {\n recomputeTheme();\n for (const l of themeListeners) l();\n });\n themeObserver.observe(document.documentElement, {\n attributes: true,\n attributeFilter: ['class', 'style'],\n });\n }\n themeListeners.add(listener);\n return () => {\n themeListeners.delete(listener);\n if (themeListeners.size === 0 && themeObserver) {\n themeObserver.disconnect();\n themeObserver = null;\n sharedTheme = null;\n }\n };\n}\n\nfunction getThemeSnapshot(): ChartTheme {\n if (!sharedTheme) recomputeTheme();\n return sharedTheme as ChartTheme;\n}\n\n// useChartTheme returns the current chart colors and re-renders the caller when\n// the document theme changes (a light/dark class or inline-style swap on <html>).\nexport function useChartTheme(): ChartTheme {\n const shared = useSyncExternalStore(subscribeTheme, getThemeSnapshot, getThemeSnapshot);\n // A host that scopes its tokens to a container says so (WidgetThemeRoot); its tokens are\n // fixed for the container's life, so they are read once there rather than observed.\n const root = useWidgetThemeRoot();\n const scoped = useMemo(() => (root ? resolveChartTheme(root) : null), [root]);\n return scoped ?? shared;\n}\n\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/hooks.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ export interface MeasurementStreamOptions {
|
|
|
20
20
|
pausedAt?: number;
|
|
21
21
|
now?: () => number;
|
|
22
22
|
initialSamples?: MeasurementSample[];
|
|
23
|
+
initialSamplesAsOf?: number;
|
|
24
|
+
onReconnect?: () => void;
|
|
23
25
|
}
|
|
24
26
|
export declare const MAX_SPAN_SAMPLES = 2000;
|
|
25
27
|
export declare function withinSpan(samples: MeasurementSample[], spanSeconds: number, end: number): MeasurementSample[];
|
package/dist/index.js
CHANGED
|
@@ -448,6 +448,30 @@ function evictOldest(samples, max, evicted) {
|
|
|
448
448
|
}
|
|
449
449
|
return samples.slice(cut);
|
|
450
450
|
}
|
|
451
|
+
function timeOf(sample) {
|
|
452
|
+
return sample.occurredTime ? Date.parse(sample.occurredTime) : Number.NaN;
|
|
453
|
+
}
|
|
454
|
+
function newestTime(samples) {
|
|
455
|
+
let newest = -Infinity;
|
|
456
|
+
for (const s of samples) {
|
|
457
|
+
const at = timeOf(s);
|
|
458
|
+
if (at > newest) newest = at;
|
|
459
|
+
}
|
|
460
|
+
return newest;
|
|
461
|
+
}
|
|
462
|
+
function settleGaps(buffer, seed, asOf) {
|
|
463
|
+
const { gaps, state } = buffer;
|
|
464
|
+
if (!seed || asOf === void 0) return buffer;
|
|
465
|
+
const open = gaps.filter((g) => asOf < g.at);
|
|
466
|
+
if (open.length === gaps.length) return buffer;
|
|
467
|
+
const cutoff = Math.max(newestTime(seed), gaps[gaps.length - open.length - 1].after);
|
|
468
|
+
const kept = (s) => !(timeOf(s) <= cutoff);
|
|
469
|
+
const latest = {};
|
|
470
|
+
for (const [name, s] of Object.entries(state.latest)) {
|
|
471
|
+
if (kept(s) || !seed.some((x) => x.name === name)) latest[name] = s;
|
|
472
|
+
}
|
|
473
|
+
return { key: buffer.key, state: { ...state, latest, samples: state.samples.filter(kept) }, gaps: open };
|
|
474
|
+
}
|
|
451
475
|
function withinSpan(samples, spanSeconds, end) {
|
|
452
476
|
const from = end - spanSeconds * 1e3;
|
|
453
477
|
return samples.filter((s) => {
|
|
@@ -460,6 +484,7 @@ function useMeasurementStream(hub, datasource, options = {}) {
|
|
|
460
484
|
const { windowSeconds, pausedAt } = options;
|
|
461
485
|
const windowSize = options.window ?? (windowSeconds !== void 0 ? MAX_SPAN_SAMPLES : 300);
|
|
462
486
|
const initialSamples = options.initialSamples;
|
|
487
|
+
const seedAsOf = options.initialSamplesAsOf;
|
|
463
488
|
const paused = pausedAt !== void 0;
|
|
464
489
|
const key = datasource ? JSON.stringify(datasource) : null;
|
|
465
490
|
const spanRef = useRef(windowSeconds);
|
|
@@ -471,24 +496,30 @@ function useMeasurementStream(hub, datasource, options = {}) {
|
|
|
471
496
|
const flushRef = useRef(() => {
|
|
472
497
|
});
|
|
473
498
|
const clock = options.now ?? Date.now;
|
|
499
|
+
const seedRef = useRef(initialSamples);
|
|
500
|
+
const onReconnectRef = useRef(options.onReconnect);
|
|
474
501
|
useEffect2(() => {
|
|
475
502
|
spanRef.current = windowSeconds;
|
|
476
503
|
clockRef.current = clock;
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
key,
|
|
480
|
-
state: EMPTY
|
|
504
|
+
seedRef.current = initialSamples;
|
|
505
|
+
onReconnectRef.current = options.onReconnect;
|
|
481
506
|
});
|
|
482
|
-
const
|
|
507
|
+
const [tagged, setTagged] = useState2({ key, state: EMPTY, gaps: [] });
|
|
508
|
+
const settled = settleGaps(tagged, initialSamples, seedAsOf);
|
|
509
|
+
const live = settled.key === key ? settled.state : EMPTY;
|
|
510
|
+
const gaps = settled.key === key ? settled.gaps : [];
|
|
483
511
|
useEffect2(() => {
|
|
484
|
-
setTagged(
|
|
512
|
+
setTagged((prev) => settleGaps(prev, initialSamples, seedAsOf));
|
|
513
|
+
}, [initialSamples, seedAsOf]);
|
|
514
|
+
useEffect2(() => {
|
|
515
|
+
setTagged({ key, state: EMPTY, gaps: [] });
|
|
485
516
|
queueRef.current = [];
|
|
486
517
|
queueLatestRef.current = {};
|
|
487
518
|
evictedRef.current = -Infinity;
|
|
488
519
|
flushRef.current = () => {
|
|
489
520
|
};
|
|
490
521
|
if (!datasource) return;
|
|
491
|
-
const update = (fn) => setTagged((prev) => prev.key === key ? {
|
|
522
|
+
const update = (fn) => setTagged((prev) => prev.key === key ? { ...prev, state: fn(prev.state) } : prev);
|
|
492
523
|
const fold = (prev, incoming, newest = latestOf(incoming)) => {
|
|
493
524
|
let samples = prev.samples.concat(incoming);
|
|
494
525
|
const span = spanRef.current;
|
|
@@ -513,14 +544,28 @@ function useMeasurementStream(hub, datasource, options = {}) {
|
|
|
513
544
|
}
|
|
514
545
|
update((prev) => fold(prev, [sample]));
|
|
515
546
|
},
|
|
516
|
-
error: (err) => update((prev) => ({ ...prev, error: err }))
|
|
547
|
+
error: (err) => update((prev) => ({ ...prev, error: err })),
|
|
548
|
+
// The feed resumed: what was sent meanwhile never arrived. Record where knowledge stops --
|
|
549
|
+
// the newest sample known, live, queued or seeded -- so the window shows a gap there, then
|
|
550
|
+
// ask the host for the history across it. Nothing known yet: no gap to show, but the
|
|
551
|
+
// history is still read again.
|
|
552
|
+
reconnected: () => {
|
|
553
|
+
const at = Date.now();
|
|
554
|
+
setTagged((prev) => {
|
|
555
|
+
if (prev.key !== key) return prev;
|
|
556
|
+
const after = newestTime([...Object.values(prev.state.latest), ...Object.values(queueLatestRef.current), ...seedRef.current ?? []]);
|
|
557
|
+
if (after === -Infinity) return prev;
|
|
558
|
+
return { ...prev, gaps: [...prev.gaps.slice(-7), { after, at }] };
|
|
559
|
+
});
|
|
560
|
+
onReconnectRef.current?.();
|
|
561
|
+
}
|
|
517
562
|
});
|
|
518
563
|
}, [hub, key, windowSize]);
|
|
519
564
|
useEffect2(() => {
|
|
520
565
|
pausedRef.current = paused;
|
|
521
566
|
if (!paused) flushRef.current();
|
|
522
567
|
}, [paused]);
|
|
523
|
-
|
|
568
|
+
const state = useMemo(() => {
|
|
524
569
|
const end = pausedAt ?? clock();
|
|
525
570
|
if (windowSeconds === void 0) {
|
|
526
571
|
if (!initialSamples || initialSamples.length === 0) return live;
|
|
@@ -538,10 +583,11 @@ function useMeasurementStream(hub, datasource, options = {}) {
|
|
|
538
583
|
const truncatedBefore = cut ? samples[0]?.occurredTime ?? void 0 : void 0;
|
|
539
584
|
const latest = initialSamples ? { ...latestOf(initialSamples), ...live.latest } : live.latest;
|
|
540
585
|
if (!truncatedBefore && latest === live.latest && samples.length === live.samples.length) return live;
|
|
541
|
-
const
|
|
542
|
-
if (truncatedBefore)
|
|
543
|
-
return
|
|
586
|
+
const state2 = { latest, samples, error: live.error };
|
|
587
|
+
if (truncatedBefore) state2.truncatedBefore = truncatedBefore;
|
|
588
|
+
return state2;
|
|
544
589
|
}, [initialSamples, live, windowSize, windowSeconds, pausedAt, clock]);
|
|
590
|
+
return gaps.length ? { ...state, gaps: gaps.map((g) => new Date(g.after).toISOString()) } : state;
|
|
545
591
|
}
|
|
546
592
|
var EMPTY_ALARMS = { alarms: [], total: 0, loading: true, error: null };
|
|
547
593
|
function useAlarmStream(hub, subscription) {
|
package/dist/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/define-widget.ts", "../src/registry.ts", "../src/frame.tsx", "../src/freshness-readout.tsx", "../src/widget-options.ts", "../src/freshness.ts", "../src/theme.ts", "../src/tokens.tsx", "../src/hooks.ts", "../src/scale.ts", "../src/connection-badge.tsx", "../src/flash.tsx", "../src/index.ts"],
|
|
4
|
-
"sourcesContent": ["// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\nimport type {\n ChannelSnapshot,\n OptionsOf,\n WidgetChannel,\n WidgetDescriptor,\n WidgetOptionSpecs,\n} from '@devicechain/dashboards';\nimport type { ComponentType } from 'react';\n\nimport type { WidgetEditorProps, WidgetProps } from './widget';\n\n// The trusted half of a registration: the React code. Kept apart from the descriptor so\n// the descriptor stays portable plain data and headless validators never load a component.\nexport interface WidgetImplementation<C extends WidgetChannel, S extends WidgetOptionSpecs> {\n // The only execution kind in this release: React code the host bundled at build time,\n // running with the host page's privileges.\n kind: 'trusted-react';\n component: ComponentType<WidgetProps<ChannelSnapshot<C>>>;\n editor?: ComponentType<WidgetEditorProps<OptionsOf<S>>>;\n}\n\nexport interface WidgetDefinition<\n C extends WidgetChannel = WidgetChannel,\n S extends WidgetOptionSpecs = WidgetOptionSpecs,\n> {\n descriptor: WidgetDescriptor<C, S>;\n implementation: WidgetImplementation<C, S>;\n}\n\n// A component with its props type erased. `any` is deliberate and confined to this alias:\n// `ComponentType<never>` would be the principled erasure, but a class component's\n// `defaultProps` is covariant in its props and rejects it, and a registry has to hold\n// components of every channel's props.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type ErasedComponent = ComponentType<any>;\n\n// A definition with its component types erased, which is what a registry stores: any\n// specific WidgetDefinition<C, S> is assignable to it.\nexport interface ErasedWidgetDefinition {\n descriptor: WidgetDescriptor;\n implementation: {\n kind: 'trusted-react';\n component: ErasedComponent;\n editor?: ErasedComponent;\n };\n}\n\n// defineWidget is an identity at runtime; it exists for inference. The channel and the\n// option schema are inferred from the literal, so a component whose data disagrees with\n// the channel, defaults that disagree with the schema, a binding that names a missing or\n// wrong-kind option, or an interaction the channel cannot carry is a COMPILE error \u2014 and\n// createWidgetRegistry checks the same things again at runtime for callers that bypass the\n// compiler. Every widget, built-in or third-party, is declared through this one helper.\nexport function defineWidget<const C extends WidgetChannel, const S extends WidgetOptionSpecs>(\n definition: WidgetDefinition<C, S>,\n): WidgetDefinition<C, S> {\n return definition;\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\nimport {\n WidgetCatalogError,\n collectCatalogIssues,\n createWidgetCatalog,\n type DescriptorIssue,\n type WidgetCatalog,\n type WidgetDescriptor,\n} from '@devicechain/dashboards';\n\nimport type { ErasedWidgetDefinition } from './define-widget';\n\n// Thrown by createWidgetRegistry with EVERY problem found, so a host fixes its composition\n// in one pass instead of one failed start per fault.\nexport class WidgetRegistryError extends WidgetCatalogError {\n constructor(issues: readonly DescriptorIssue[]) {\n super(issues, 'widget registry');\n this.name = 'WidgetRegistryError';\n }\n}\n\n// A sealed registry: descriptors (through the React-free catalog the parser sees) plus the\n// trusted implementations. Immutable \u2014 there is no register(), no process-global state and\n// no replacement while a dashboard is mounted. Two registries are independent.\nexport interface WidgetRegistry {\n // The React-free view of this registry, for parsing and headless validation.\n readonly catalog: WidgetCatalog;\n descriptor(id: string): WidgetDescriptor | undefined;\n implementation(id: string): ErasedWidgetDefinition['implementation'] | undefined;\n // Definitions in the order they were supplied.\n list(): readonly ErasedWidgetDefinition[];\n}\n\nfunction isComponent(value: unknown): boolean {\n if (typeof value === 'function') return true;\n // memo / forwardRef / lazy components are objects tagged with $$typeof.\n return typeof value === 'object' && value !== null && '$$typeof' in value;\n}\n\ntype Loose = Partial<ErasedWidgetDefinition> | null | undefined;\n\n// createWidgetRegistry validates every definition (descriptor invariants, duplicate ids,\n// the implementation shape) and returns a deeply frozen registry, or throws one\n// WidgetRegistryError listing all the problems.\nexport function createWidgetRegistry(definitions: readonly ErasedWidgetDefinition[]): WidgetRegistry {\n if (!Array.isArray(definitions)) {\n throw new WidgetRegistryError([{ id: '#?', path: '', message: 'definitions must be an array' }]);\n }\n const issues: DescriptorIssue[] = collectCatalogIssues(definitions.map((d) => (d as Loose)?.descriptor));\n\n definitions.forEach((definition, index) => {\n const id: unknown = (definition as Loose)?.descriptor?.id;\n const label = typeof id === 'string' && id !== '' ? id : `#${index}`;\n const add = (path: string, message: string): void => {\n issues.push({ id: label, path, message });\n };\n const impl = (definition as Loose)?.implementation;\n if (typeof impl !== 'object' || impl === null) {\n add('implementation', 'a definition needs an implementation');\n return;\n }\n if (impl.kind !== 'trusted-react') {\n add('implementation.kind', `kind must be \"trusted-react\", got ${JSON.stringify(impl.kind)}`);\n }\n if (!isComponent(impl.component)) add('implementation.component', 'component must be a React component');\n if (impl.editor !== undefined && !isComponent(impl.editor)) {\n add('implementation.editor', 'editor, when given, must be a React component');\n }\n });\n\n if (issues.length > 0) throw new WidgetRegistryError(issues);\n\n // Through the one public, validating constructor: no unvalidated path to a catalog exists.\n // The descriptors were already checked above (so this cannot throw for them); checking\n // again is the price of not exporting a catalog builder that skips validation.\n //\n // \uD83D\uDD34 The catalog freezes each descriptor IN PLACE (deeply), so the caller's descriptor\n // objects are frozen after this call. That is deliberate: a built-in descriptor holds its\n // option schema BY IDENTITY with WIDGET_OPTIONS, and a copy would break that identity.\n const catalog = createWidgetCatalog(definitions.map((d) => d.descriptor));\n const byId = new Map<string, ErasedWidgetDefinition>();\n for (const definition of definitions) {\n // The implementation object, unlike the descriptor, is copied before freezing, so the\n // caller's implementation object is left mutable (the components it names are shared).\n byId.set(\n definition.descriptor.id,\n Object.freeze({\n descriptor: catalog.get(definition.descriptor.id) as WidgetDescriptor,\n implementation: Object.freeze({ ...definition.implementation }),\n }),\n );\n }\n const ordered: readonly ErasedWidgetDefinition[] = Object.freeze([...byId.values()]);\n\n return Object.freeze({\n catalog,\n descriptor: (id: string) => catalog.get(id),\n implementation: (id: string) => byId.get(id)?.implementation,\n list: () => ordered,\n });\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// The shared card frame every widget renders inside \u2014 a bordered, themed surface\n// that fills its dashboard-canvas slot, with an optional title bar. Styled with\n// inline CSS-variable references so it themes from the host without Tailwind.\n//\n// The frame is SIZE-AWARE. Its content column -- the padded box the widget draws in -- is a\n// `container-type: size` query container, so HTML text can size itself with `cq` units on the\n// first frame, and the SAME box is measured (quantised to 4 px) and handed to everything inside\n// through context (useWidgetScale), so a canvas widget fits to the identical numbers. The header\n// gives way to the body as the widget shrinks: the subtitle goes first, then the title.\n//\n// What the card LOOKS like is the board's to decide (the frame a definition sets on its canvas\n// and per widget, resolved by the renderer and read here through useWidgetFrameStyle): a card, a\n// borderless flat panel, or nothing at all; its border width, radius and colour; its padding;\n// its background; and whether the header shows. Every value is a token. A widget mounted outside\n// a board gets the card.\n\nimport { resolveFrame, sameLabel, type FrameRadius, type ResolvedFrame, type SpaceTokenName } from '@devicechain/dashboards';\nimport { useMemo, type CSSProperties, type ReactNode } from 'react';\n\nimport {\n useWidgetDensityScale,\n useWidgetFrameStyle,\n useWidgetFreshness,\n useWidgetSubject,\n WidgetBodySizeProvider,\n} from './context';\nimport { FreshnessReadout, StaleBadge, useFreshness } from './freshness-readout';\nimport { useElementSize } from './hooks';\nimport { css } from './theme';\nimport { makeWidgetScale, type WidgetScale } from './scale';\nimport { fontVar, spaceVar, type SpaceToken } from './tokens';\n\n// Below either of these (px, content box) the age readout is dropped so a small widget keeps\n// its room for the value; a stale widget still shows its badge, in the corner.\nconst FRESHNESS_MIN_WIDTH = 150;\nconst FRESHNESS_MIN_HEIGHT = 110;\n\n// Frame heights (px at density 1, multiplied by the density scale) below which the header gives\n// way. The subtitle is the first thing to go; the title holds until there is hardly room for a\n// value at all. A dropped title stays available to assistive tech as the frame's aria-label.\nexport const FRAME_SUBTITLE_MIN_HEIGHT = 120;\nexport const FRAME_TITLE_MIN_HEIGHT = 76;\n\n// The content column's size is quantised to this many px before it reaches a widget.\nconst BODY_SIZE_STEP = 4;\n\n// The card a widget gets when no board says otherwise.\nconst DEFAULT_FRAME: ResolvedFrame = resolveFrame(undefined, undefined);\n\nconst RADIUS: Record<FrameRadius, string> = {\n none: '0px',\n sm: 'calc(var(--radius, 8px) / 2)',\n md: 'var(--radius, 8px)',\n lg: 'calc(var(--radius, 8px) * 1.5)',\n};\n\nfunction spaceTokenVar(token: SpaceTokenName): string {\n return token === 'space-0' ? '0px' : spaceVar(Number(token.slice('space-'.length)) as SpaceToken);\n}\n\nexport interface WidgetFrameProps {\n title?: string;\n // An explicit subject line; when omitted the frame falls back to the resolved-entity\n // subject from WidgetSubjectContext.\n subtitle?: string;\n // The widget's content. A function child receives the frame's measured scale (size class,\n // content-column size, token px), for a widget whose own render needs it: a component\n // rendered INSIDE the frame can call useWidgetScale() instead.\n children: ReactNode | ((scale: WidgetScale) => ReactNode);\n // Extra style for the body region.\n bodyStyle?: CSSProperties;\n // Remove the content column's padding, for a widget that fills its box edge to edge (a chart\n // canvas, a map, an image). The column is still the measured, queryable box.\n flush?: boolean;\n // The occurredTime of the ONE sample this widget is showing, for a widget that shows a single\n // value (null while it has none). When set, the age readout ages exactly that sample; left\n // undefined, it ages the newest sample the host holds for the widget. A single-value widget\n // must pass it: on a device reporting several measurements, the newest is often not the one\n // on screen, and the age of another measurement would make a frozen value look live.\n sampleTime?: string | null;\n}\n\n// The epoch-ms time of a sample timestamp, or undefined when it has none or is unparseable.\nfunction parseSampleTime(time: string | null): number | undefined {\n if (!time) return undefined;\n const at = Date.parse(time);\n return Number.isNaN(at) ? undefined : at;\n}\n\nexport function WidgetFrame({ title, subtitle, children, bodyStyle, flush, sampleTime }: WidgetFrameProps) {\n const subject = useWidgetSubject();\n // A subtitle that only repeats the title says nothing (a card titled by its device's\n // external id, under a subject line naming the same device).\n const rawSub = subtitle ?? subject;\n const dupSub = rawSub && sameLabel(rawSub, title) ? undefined : rawSub;\n\n // Present only for a widget whose descriptor opts in. Stale (a known threshold, passed) greys\n // the value out; with no threshold the age is shown and nothing is ever greyed.\n const freshness = useWidgetFreshness();\n const at = freshness === undefined ? undefined : sampleTime === undefined ? freshness.at : parseSampleTime(sampleTime);\n const reading = useFreshness(at, freshness?.staleAfterSeconds);\n const [rootRef, size] = useElementSize<HTMLDivElement>();\n const [columnRef, columnSize] = useElementSize<HTMLDivElement>(BODY_SIZE_STEP);\n const scale = useWidgetDensityScale();\n const frame = useWidgetFrameStyle() ?? DEFAULT_FRAME;\n // Unmeasured (first frame, jsdom) shows everything; the cq path needs no measure to be right.\n // `show` keeps the header whatever the size, `hide` drops it whatever the size.\n const measured = size.height > 0;\n const header = frame.header;\n const showSub = header === 'show' || (header === 'auto' && (!measured || size.height >= FRAME_SUBTITLE_MIN_HEIGHT * scale));\n const showTitle = header === 'show' || (header === 'auto' && (!measured || size.height >= FRAME_TITLE_MIN_HEIGHT * scale));\n const rule = frame.border.width > 0 ? `${frame.border.width}px solid ${css(frame.border.color)}` : 'none';\n const sub = showSub ? dupSub : undefined;\n const shownTitle = showTitle ? title : undefined;\n const widgetScale = useMemo(() => makeWidgetScale(columnSize, scale), [columnSize, scale]);\n const compact = size.width > 0 && (size.width < FRESHNESS_MIN_WIDTH || size.height < FRESHNESS_MIN_HEIGHT);\n const stale = reading?.stale === true;\n\n return (\n <div\n ref={rootRef}\n data-stale={stale ? 'true' : undefined}\n role={title && !showTitle ? 'group' : undefined}\n aria-label={title && !showTitle ? title : undefined}\n style={{\n position: 'relative',\n display: 'flex',\n flexDirection: 'column',\n width: '100%',\n height: '100%',\n boxSizing: 'border-box',\n background: frame.background === 'transparent' ? 'transparent' : css(frame.background),\n color: css('card-foreground'),\n border: rule,\n borderRadius: RADIUS[frame.border.radius],\n overflow: 'hidden',\n }}\n >\n {shownTitle || sub ? (\n <div\n style={{\n padding: `${spaceVar(2)} ${spaceVar(3)}`,\n borderBottom: rule,\n }}\n >\n {shownTitle ? (\n <div\n title={shownTitle}\n style={{\n fontSize: fontVar('sm'),\n fontWeight: 600,\n color: css('muted-foreground'),\n whiteSpace: 'nowrap',\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n }}\n >\n {shownTitle}\n </div>\n ) : null}\n {sub ? (\n <div\n title={sub}\n style={{\n fontSize: fontVar('xs'),\n fontWeight: 400,\n color: css('muted-foreground'),\n opacity: 0.75,\n marginTop: shownTitle ? 2 : 0,\n whiteSpace: 'nowrap',\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n }}\n >\n {sub}\n </div>\n ) : null}\n </div>\n ) : null}\n {/* The body clips: the card root already stops a widget painting outside the card, and\n this stops one that outgrows its slot painting up over the title bar. minWidth 0\n lets it shrink inside the flex column. */}\n <div\n data-widget-body=\"\"\n style={{\n position: 'relative',\n flex: 1,\n minHeight: 0,\n minWidth: 0,\n overflow: 'hidden',\n ...bodyStyle,\n ...(stale ? { opacity: 0.45, filter: 'grayscale(1)' } : null),\n }}\n >\n {/* The content column: absolutely filling the body so its size never depends on its\n children (a size container must not), padded by the frame so every widget gets the\n same inset, and the box both the cq units and the measured size describe. */}\n <div\n ref={columnRef}\n data-widget-content=\"\"\n style={{\n position: 'absolute',\n inset: 0,\n boxSizing: 'border-box',\n containerType: 'size',\n overflow: 'hidden',\n padding: flush ? 0 : spaceTokenVar(frame.padding),\n }}\n >\n <WidgetBodySizeProvider size={columnSize}>\n {typeof children === 'function' ? children(widgetScale) : children}\n </WidgetBodySizeProvider>\n </div>\n </div>\n {reading && !compact ? (\n <div\n data-widget-freshness=\"\"\n style={{ padding: `${spaceVar(1)} ${spaceVar(3)}`, borderTop: rule, flex: '0 0 auto' }}\n >\n <FreshnessReadout reading={reading} />\n </div>\n ) : null}\n {reading && compact && stale ? (\n <div style={{ position: 'absolute', top: spaceVar(1), right: spaceVar(1) }}>\n <StaleBadge />\n </div>\n ) : null}\n </div>\n );\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// The age readout: \"12 sec. ago\", ticking once a second, plus a \"stale\" badge when -- and only\n// when -- a threshold is known and passed. The frame draws it for any widget whose descriptor\n// opts in (`freshness: true`); a custom widget that wants it somewhere else can render\n// <Freshness> itself.\n\nimport { useEffect, useState, type CSSProperties } from 'react';\n\nimport { ageSeconds, formatAge, freshnessStrings, isStale } from './freshness';\nimport { css } from './theme';\nimport { fontVar } from './tokens';\n\n// One interval per period, shared by every subscriber: a board of two hundred widgets ticks\n// once a second, not two hundred times, and every readout on it moves together. The timer\n// exists only while something is subscribed.\ninterface Ticker {\n timer: ReturnType<typeof setInterval>;\n listeners: Set<(now: number) => void>;\n}\nconst tickers = new Map<number, Ticker>();\n\nfunction subscribeTick(everyMs: number, listener: (now: number) => void): () => void {\n let ticker = tickers.get(everyMs);\n if (!ticker) {\n const listeners = new Set<(now: number) => void>();\n const timer = setInterval(() => {\n const now = Date.now();\n for (const l of [...listeners]) l(now);\n }, everyMs);\n ticker = { timer, listeners };\n tickers.set(everyMs, ticker);\n }\n ticker.listeners.add(listener);\n const owned = ticker;\n return () => {\n owned.listeners.delete(listener);\n if (owned.listeners.size === 0 && tickers.get(everyMs) === owned) {\n clearInterval(owned.timer);\n tickers.delete(everyMs);\n }\n };\n}\n\n// useNow re-renders its caller every `everyMs` with the current time, off the shared ticker\n// for that period. One second is the finest unit the readout shows.\nexport function useNow(everyMs = 1000, enabled = true): number {\n const [now, setNow] = useState(() => Date.now());\n useEffect(() => {\n if (!enabled) return;\n setNow(Date.now());\n return subscribeTick(everyMs, setNow);\n }, [everyMs, enabled]);\n return now;\n}\n\nexport interface FreshnessReading {\n age: number;\n stale: boolean;\n}\n\n// useFreshness: the age of `at` (epoch ms) now, and whether it is stale against the threshold.\n// Undefined while there is no sample to age.\n//\n// \uD83D\uDD34 A sample time AHEAD of the viewer's clock (a device clock running fast) is aged from when\n// this viewer first saw it, not from the time it claims. Clamping the age at zero alone would\n// read \"now\" for as long as the device is ahead -- an hour-fast clock would keep a frozen\n// value looking live for an hour, which is exactly what the readout exists to prevent.\nexport function useFreshness(at: number | undefined, staleAfterSeconds: number | undefined): FreshnessReading | undefined {\n // No sample, no clock: a frame that does not opt in must not tick once a second.\n const now = useNow(1000, at !== undefined);\n // When each distinct sample time was first seen here; adjusted during render (React's\n // \"storing information from previous renders\" pattern) so the first paint is already right.\n const [seen, setSeen] = useState<{ at: number; seenAt: number } | undefined>(undefined);\n let current = seen;\n if (at !== undefined && seen?.at !== at) {\n current = { at, seenAt: Math.max(now, Date.now()) };\n setSeen(current);\n }\n if (at === undefined || current === undefined) return undefined;\n const age = ageSeconds(Math.min(at, current.seenAt), now);\n return { age, stale: isStale(age, staleAfterSeconds) };\n}\n\nexport function StaleBadge({ label }: { label?: string }) {\n return (\n <span\n data-stale-badge=\"\"\n style={{\n fontSize: fontVar('xs'),\n fontWeight: 600,\n textTransform: 'uppercase',\n letterSpacing: 0.4,\n padding: '0 5px',\n borderRadius: 4,\n border: `1px dashed ${css('muted-foreground')}`,\n color: css('foreground'),\n background: css('muted'),\n whiteSpace: 'nowrap',\n }}\n >\n {label ?? freshnessStrings().stale}\n </span>\n );\n}\n\nexport interface FreshnessProps {\n // The newest sample's time, epoch ms. Nothing renders without one.\n at: number | undefined;\n // The age past which the value is stale. Omit when unknown: the age is shown and nothing is\n // styled stale.\n staleAfterSeconds?: number;\n // Override the badge word (a host in a language the SDK does not ship).\n staleLabel?: string;\n style?: CSSProperties;\n}\n\nexport function Freshness({ at, staleAfterSeconds, staleLabel, style }: FreshnessProps) {\n const reading = useFreshness(at, staleAfterSeconds);\n if (!reading) return null;\n return <FreshnessReadout reading={reading} staleLabel={staleLabel} style={style} />;\n}\n\nexport function FreshnessReadout({\n reading,\n staleLabel,\n style,\n}: {\n reading: FreshnessReading;\n staleLabel?: string;\n style?: CSSProperties;\n}) {\n return (\n <span\n data-freshness=\"\"\n data-stale={reading.stale ? 'true' : undefined}\n title={freshnessStrings().ageLabel}\n style={{\n display: 'inline-flex',\n alignItems: 'center',\n gap: 6,\n fontSize: fontVar('xs'),\n color: css('muted-foreground'),\n fontVariantNumeric: 'tabular-nums',\n ...style,\n }}\n >\n <time>{formatAge(reading.age)}</time>\n {reading.stale ? <StaleBadge label={staleLabel} /> : null}\n </span>\n );\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Small helpers for reading a widget's opaque `options` bag and picking the measurement a\n// single-value widget should show. They are the SDK's, not the built-ins': a third-party\n// widget reads its own options the same defensive way.\n\nimport type { MeasurementSample, WidgetInstance } from '@devicechain/dashboards';\n\n// pickSample chooses the sample a single-value widget (card, gauge) shows: the\n// named measurement, or the first available when no name is pinned.\nexport function pickSample(\n latest: Record<string, MeasurementSample>,\n name: string | undefined,\n): MeasurementSample | undefined {\n return name ? latest[name] : Object.values(latest)[0];\n}\n\n// --- options helpers ---------------------------------------------------------\n// A widget's `options` is opaque JSON (Record<string, unknown>) owned by the\n// widget; these read a key defensively, returning undefined on the wrong type\n// rather than throwing on a hand-edited definition.\n\ntype Options = Record<string, unknown> | undefined;\n\nexport function optString(options: Options, key: string): string | undefined {\n const value = options?.[key];\n return typeof value === 'string' ? value : undefined;\n}\n\nexport function optNumber(options: Options, key: string): number | undefined {\n const value = options?.[key];\n return typeof value === 'number' && Number.isFinite(value) ? value : undefined;\n}\n\n// A boolean flag option: true only when explicitly stored true (absent/other \u2192 false), so\n// an opt-in feature stays off by default and a hand-edited value can't accidentally enable it.\nexport function optBoolean(options: Options, key: string): boolean {\n return options?.[key] === true;\n}\n\n// primaryMeasurementName is the measurement a single-value widget (card, gauge)\n// displays: an explicit `options.measurement`, else the first measurement its\n// datasource selects, else undefined (fall back to whatever arrives first).\nexport function primaryMeasurementName(widget: WidgetInstance): string | undefined {\n const explicit = optString(widget.options, 'measurement');\n if (explicit) return explicit;\n const ds = widget.datasource;\n if (ds && ds.measurements.length > 0) return ds.measurements[0];\n return undefined;\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// How old a widget's newest sample is, whether that is \"stale\", and how to say so. Pure, so\n// the precedence and the wording are testable without a DOM or a clock.\n\nimport type { MeasurementSample } from '@devicechain/dashboards';\n\nimport { optNumber } from './widget-options';\n\n// resolveStaleAfterSeconds is the age, in seconds, past which a widget's value is called stale,\n// or undefined when no honest threshold is known. Precedence:\n//\n// 1. the widget's own `staleAfterSeconds` option;\n// 2. (no source today) the device profile's expected reporting interval x 3 -- a profile\n// carries no measurement reporting interval at present, only a location-fix interval that\n// is documented as descriptive, so nothing is read here and nothing is invented;\n// 3. otherwise undefined: show the age, never style anything stale.\n//\n// \uD83D\uDD34 There is deliberately NO default. A fleet that reports hourly would be painted stale all\n// day by any number chosen here, and \"stale\" painted on healthy data teaches viewers to ignore\n// the badge. A non-positive value is not a threshold and counts as none.\nexport function resolveStaleAfterSeconds(options: Record<string, unknown> | undefined): number | undefined {\n const configured = optNumber(options, 'staleAfterSeconds');\n return configured !== undefined && configured > 0 ? configured : undefined;\n}\n\n// newestSampleTime is the latest occurredTime (epoch ms) among the named measurements' latest\n// samples, or undefined when none carries a parseable time. `names` empty or omitted means\n// every measurement the widget holds.\nexport function newestSampleTime(\n latest: Record<string, MeasurementSample>,\n names?: readonly string[],\n): number | undefined {\n let newest: number | undefined;\n for (const [name, sample] of Object.entries(latest)) {\n if (names && names.length > 0 && !names.includes(name)) continue;\n if (!sample.occurredTime) continue;\n const at = Date.parse(sample.occurredTime);\n if (Number.isNaN(at)) continue;\n if (newest === undefined || at > newest) newest = at;\n }\n return newest;\n}\n\n// ageSeconds is whole seconds since `at`, never negative: a device clock slightly ahead of the\n// viewer's would otherwise read \"in 3 seconds\".\nexport function ageSeconds(at: number, now: number): number {\n return Math.max(0, Math.floor((now - at) / 1000));\n}\n\n// isStale is true only when a threshold is known and the age has passed it.\nexport function isStale(age: number, staleAfter: number | undefined): boolean {\n return staleAfter !== undefined && age > staleAfter;\n}\n\n// The viewer's language, as the host app set it on <html lang>, else the browser's.\nexport function viewerLocale(): string | undefined {\n if (typeof document !== 'undefined' && document.documentElement.lang) return document.documentElement.lang;\n if (typeof navigator !== 'undefined' && navigator.language) return navigator.language;\n return undefined;\n}\n\n// One formatter per locale tag: every readout on a board re-formats once a second, and\n// building an Intl formatter is far dearer than using one.\nconst formatters = new Map<string, Intl.RelativeTimeFormat>();\n\nfunction relativeTimeFormat(locale: string | undefined): Intl.RelativeTimeFormat {\n const key = locale ?? '';\n let formatter = formatters.get(key);\n if (!formatter) {\n try {\n formatter = new Intl.RelativeTimeFormat(locale, { numeric: 'auto', style: 'short' });\n } catch {\n // An unparseable tag in <html lang> must not take a widget down: the runtime's default.\n formatter = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto', style: 'short' });\n }\n formatters.set(key, formatter);\n }\n return formatter;\n}\n\n// formatAge renders an age as \"12 sec. ago\" / \"hace 12 s\" in the viewer's locale, stepping up\n// to minutes, hours and days. Under a second it reads \"now\".\nexport function formatAge(age: number, locale: string | undefined = viewerLocale()): string {\n const formatter = relativeTimeFormat(locale);\n if (age < 60) return formatter.format(-age, 'second');\n if (age < 3600) return formatter.format(-Math.floor(age / 60), 'minute');\n if (age < 86400) return formatter.format(-Math.floor(age / 3600), 'hour');\n return formatter.format(-Math.floor(age / 86400), 'day');\n}\n\n// The SDK carries no i18n runtime, so the one word it adds to a board ships here, per\n// language, falling back to English. A host that wants another language passes `staleLabel`.\nexport interface FreshnessStrings {\n stale: string;\n ageLabel: string;\n}\n\nexport const FRESHNESS_STRINGS: Record<'en' | 'es', FreshnessStrings> = {\n en: { stale: 'stale', ageLabel: 'Last sample' },\n es: { stale: 'obsoleto', ageLabel: '\u00DAltima muestra' },\n};\n\nexport function freshnessStrings(locale: string | undefined = viewerLocale()): FreshnessStrings {\n const base = locale?.toLowerCase().split('-')[0];\n return base === 'es' ? FRESHNESS_STRINGS.es : FRESHNESS_STRINGS.en;\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Theming without Tailwind (widgets are embeddable \u2014 no Tailwind leakage).\n//\n// Widgets style themselves with inline styles that reference the SAME CSS custom\n// properties the console (and any host) defines \u2014 `--card`, `--foreground`,\n// `--border`, `--chart-1..5`, etc. A host that sets those variables themes the\n// widgets for free (light/dark included); a host without Tailwind still works.\n//\n// DOM elements can use `hsl(var(--card))` directly. ECharts renders to a <canvas>,\n// which cannot resolve CSS variables \u2014 so for charts we read the variables off the\n// live computed style and hand ECharts concrete color strings (resolveChartTheme).\n\nimport type { ThemeColorToken } from '@devicechain/dashboards';\n\n// css returns an `hsl(var(--name))` reference for use in an inline style. The host\n// defines the variable as HSL channels (e.g. `--card: 0 0% 100%`), matching the\n// console's token set.\nexport function css(name: ThemeVar): string {\n return `hsl(var(--${name}))`;\n}\n\n// The subset of the console's design tokens widgets rely on. Kept small and\n// semantic so the contract a host must provide is explicit and stable.\n//\n// Colours only: sizes are SizeTokens (see ./tokens). The chart ramp and the severity hues are\n// part of the set, so a widget colours itself from a token rather than a literal; `normal` has\n// NO severity token (it renders in the ordinary foreground, see ./severity).\n//\n// The list itself lives with the dashboard format (THEME_COLOR_TOKENS), because a definition\n// names these too, for a frame's border and background: one list, so a colour a widget can use\n// is exactly a colour a board may name.\nexport type ThemeVar = ThemeColorToken;\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Size tokens: the type ramp and spacing steps a widget draws from instead of writing px\n// literals, and the density scale they all multiply by.\n//\n// --dc-scale the board's density scale (0.9 compact / 1 comfortable / 1.5 wall)\n// --dc-font-<xs|sm|md|lg|xl|display> calc(<base px> * var(--dc-scale))\n// --dc-space-<1..4> calc(<base px> * var(--dc-scale))\n//\n// The `dc-` prefix is load-bearing: these are set on a board that lives inside someone else's\n// page, and the bare names collide -- Tailwind v4 keeps font FAMILIES in `--font-*` (a host's\n// `--font-display: \"Inter\"` is its documented example), so `var(--font-display, 28px)` in such\n// a host resolves to a family name and the font size is thrown away.\n//\n// The renderer sets them on its root (tokenStyle), so every widget under it scales with the\n// board's density; `fontVar('md')` is `var(--dc-font-md, 13px)`, which still renders the base\n// size anywhere the tokens are not set (a host that mounts a bare WidgetFrame). Canvas widgets\n// cannot read a CSS variable and take the numeric equivalent from useWidgetScale().px().\n//\n// Colour tokens are the ThemeVar set in ./theme; the severity hues are generated from the\n// table in ./severity (WidgetTokenStyles below injects them), so no colour is typed twice.\n\nimport { DENSITY_SCALE, SPACE_TOKEN_PX, type CanvasDensity } from '@devicechain/dashboards';\nimport type { CSSProperties } from 'react';\n\nimport { severityTokenCss } from './severity';\n\nexport const FONT_TOKENS = {\n xs: 11,\n sm: 12,\n md: 13,\n lg: 16,\n xl: 20,\n display: 28,\n} as const;\nexport type FontToken = keyof typeof FONT_TOKENS;\n\n// The spacing steps a widget draws from, read from the dashboard format's table (which also has\n// `space-0`, for a seamless board's gutter and a frameless widget's padding).\nexport const SPACE_TOKENS = {\n 1: SPACE_TOKEN_PX['space-1'],\n 2: SPACE_TOKEN_PX['space-2'],\n 3: SPACE_TOKEN_PX['space-3'],\n 4: SPACE_TOKEN_PX['space-4'],\n} as const;\nexport type SpaceToken = keyof typeof SPACE_TOKENS;\n\n// 'font-md' | 'space-3' -- the names useWidgetScale().px() accepts.\nexport type SizeToken = `font-${FontToken}` | `space-${SpaceToken}`;\n\nexport function densityScale(density: CanvasDensity | undefined): number {\n return DENSITY_SCALE[density ?? 'comfortable'];\n}\n\nexport function fontVar(token: FontToken): string {\n return `var(--dc-font-${token}, ${FONT_TOKENS[token]}px)`;\n}\n\nexport function spaceVar(token: SpaceToken): string {\n return `var(--dc-space-${token}, ${SPACE_TOKENS[token]}px)`;\n}\n\n// The numeric px of a size token at a scale, for a canvas that cannot read the variable.\nexport function tokenPx(token: SizeToken, scale = 1): number {\n const [kind, name] = token.split('-') as ['font' | 'space', string];\n const base = kind === 'font' ? FONT_TOKENS[name as FontToken] : SPACE_TOKENS[Number(name) as SpaceToken];\n return Math.round(base * scale * 100) / 100;\n}\n\n// The custom properties a board root sets for a density scale.\nexport function tokenStyle(scale: number): CSSProperties {\n const vars: Record<string, string> = { '--dc-scale': String(scale) };\n for (const [name, px] of Object.entries(FONT_TOKENS)) vars[`--dc-font-${name}`] = `calc(${px}px * var(--dc-scale))`;\n for (const [name, px] of Object.entries(SPACE_TOKENS)) vars[`--dc-space-${name}`] = `calc(${px}px * var(--dc-scale))`;\n return vars as CSSProperties;\n}\n\n// The severity hues as a stylesheet at ZERO specificity (`:where`), so a host that defines\n// `--sev-<level>` itself (any ordinary `:root` / `.dark` rule) always wins and a host that\n// does not still gets the colours. They sit on the document root, where useSeverityColors\n// reads them -- nothing is scoped to a board element, so no WidgetThemeRoot registration is\n// needed for them to resolve.\nexport function WidgetTokenStyles() {\n return <style data-dc-tokens=\"\">{severityTokenCss({ hostWins: true })}</style>;\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// React bindings between the imperative DashboardHub / DOM theme and widget state.\n\nimport type {\n AlarmSubscription,\n CommandSubscription,\n DashboardDefinition,\n DatasourceSelector,\n DeviceResolver,\n EntityCandidateLister,\n HubConnectionState,\n LocationSubscription,\n MemberResolver,\n MeasurementSample,\n SelectionCandidate,\n SlotBinding,\n WidgetDataSource,\n} from '@devicechain/dashboards';\nimport type {\n AggregateStreamState,\n AlarmStreamState,\n CommandStreamState,\n LocationStreamState,\n MeasurementStreamState,\n} from '@devicechain/dashboards';\nimport {\n bindingsWithoutScopedSlots,\n DashboardHub,\n hasScopedSlots,\n resolveContextBindings,\n resolveSlotCandidates,\n} from '@devicechain/dashboards';\nimport { useEffect, useMemo, useRef, useState, useSyncExternalStore, type RefObject } from 'react';\n\nimport { useWidgetCandidates, type WidgetCandidates } from './context';\n\n// The channel snapshots moved to @devicechain/dashboards (they are pure data over the hub's\n// row types); re-exported so every import site here keeps resolving.\nexport type { AggregateStreamState, AlarmStreamState, CommandStreamState, LocationStreamState, MeasurementStreamState };\n\n// useDashboardHub gives a host ONE live hub per (resolver, authorities) \u2014 compared by\n// value, so a re-rendered but equal authorities array keeps the hub \u2014 and disposes it when\n// it is replaced or the host unmounts.\n//\n// It deliberately takes no bindings. The hub holds none: the renderer resolves every slot\n// through the settled bindings (resolveWidgetDatasource) before a widget subscribes, so a\n// selection re-subscribes only the widgets bound to the re-pointed slot. A hub rebuilt per\n// selection would tear down every subscription on the board and re-fetch every history.\nexport function useDashboardHub(resolver: DeviceResolver, authorities: string[] | undefined): DashboardHub {\n const authoritiesKey = JSON.stringify(authorities ?? []);\n const hub = useMemo(\n () => new DashboardHub({ resolver, authorities }),\n // authorities read via authoritiesKey (value identity), not reference.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [resolver, authoritiesKey],\n );\n useEffect(() => () => hub.disposeAll(), [hub]);\n return hub;\n}\n\n// useResolvedBindings runs the scoped-slot cascade (ADR-039 selection amendment) for a\n// host: it takes the synchronous base manifest (effectiveBindings) plus the accumulated\n// selection overlay and returns the settled slot\u2192binding map the host feeds to the\n// renderer. Selection state lives in the host; this hook just derives bindings from it.\n//\n// The common scope-FREE dashboard needs no async work \u2014 the synchronous overlay (base\n// with the selection laid over it) is already correct, and the hook returns it directly,\n// preserving today's behavior. Only a dashboard with scoped slots runs the async pass,\n// guarded by a monotonic generation so a slow members response can't overwrite a newer\n// selection; it keeps the last settled map until the new one arrives (one re-key per\n// walk, never a stale partial). A resolution error is fail-safe: keep the last map.\nexport function useResolvedBindings(\n definition: DashboardDefinition,\n base: Record<string, SlotBinding>,\n selection: Record<string, SlotBinding>,\n resolver: MemberResolver,\n): Record<string, SlotBinding> {\n const scoped = useMemo(() => hasScopedSlots(definition), [definition]);\n // Selection wins over base. Correct as-is for root + manual slots and every scope-free\n // dashboard; scoped `first` slots are refined by the async pass below.\n const overlaid = useMemo(() => ({ ...base, ...selection }), [base, selection]);\n // Seed the settled map with scoped slots STRIPPED, so a scoped dashboard's first paint\n // is unbound-not-stale (a scoped slot's default may be out of context) until the cascade\n // derives it. For a scope-free dashboard this equals `overlaid`.\n const [resolved, setResolved] = useState(() => bindingsWithoutScopedSlots(definition, overlaid));\n const genRef = useRef(0);\n\n useEffect(() => {\n // Scope-free: `overlaid` is authoritative. Keep `resolved` synced to it anyway so that\n // if the definition later gains a scope (an authoring edit), the flip to the `resolved`\n // path starts from the CURRENT bindings, not a stale mount-time snapshot.\n if (!scoped) {\n setResolved((prev) => (sameBindings(prev, overlaid) ? prev : overlaid));\n return;\n }\n const gen = ++genRef.current;\n let live = true;\n resolveContextBindings(definition, base, selection, resolver)\n .then((next) => {\n if (!live || gen !== genRef.current) return;\n // Only swap when the value actually changed, so an equal re-resolve doesn't churn\n // a new object reference (and re-render every widget) for nothing.\n setResolved((prev) => (sameBindings(prev, next) ? prev : next));\n })\n .catch(() => {\n /* fail-safe: keep the last settled map */\n });\n return () => {\n live = false;\n };\n }, [scoped, definition, base, selection, resolver, overlaid]);\n\n return scoped ? resolved : overlaid;\n}\n\n// sameBindings is a value compare of two slot\u2192binding maps (stable-key JSON), so the\n// hook can suppress a no-op re-key.\nfunction sameBindings(a: Record<string, SlotBinding>, b: Record<string, SlotBinding>): boolean {\n const ak = Object.keys(a).sort();\n const bk = Object.keys(b).sort();\n if (ak.length !== bk.length) return false;\n for (let i = 0; i < ak.length; i += 1) {\n if (ak[i] !== bk[i]) return false;\n if (JSON.stringify(a[ak[i]]) !== JSON.stringify(b[bk[i]])) return false;\n }\n return true;\n}\n\n// useSlotCandidates builds the ambient candidate provider a host passes to the renderer\n// (ADR-039 selection amendment): a function slot\u2192candidates that a context/entity-selector\n// widget calls. It closes over the CURRENT resolved bindings, so it is rebuilt whenever a\n// binding changes \u2014 a selector's option set (a scoped child's members) then follows the\n// parent for free, and a widget re-fetches because the provider identity changed. Returns\n// undefined when no lister is supplied (a host that wires no selection), so the selector\n// stays inert. Keyed by the bindings' VALUE so an equal-but-new bindings object doesn't\n// churn the provider (and every open selector's fetch) each render.\nexport function useSlotCandidates(\n definition: DashboardDefinition,\n bindings: Record<string, SlotBinding>,\n resolver: MemberResolver,\n lister: EntityCandidateLister | undefined,\n): WidgetCandidates | undefined {\n const bindingsKey = JSON.stringify(bindings);\n return useMemo(\n () => {\n if (!lister) return undefined;\n return (slot: string) => resolveSlotCandidates(definition, slot, bindings, resolver, lister);\n },\n // bindings read via bindingsKey (value identity), not reference.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [definition, bindingsKey, resolver, lister],\n );\n}\n\n// The state a selector widget renders from: the target slot's current candidate options,\n// plus load/error flags. `wired` reports whether an ambient candidate provider exists \u2014 a\n// selector feature-detects on it (with the select callback) to stay inert where selection\n// isn't wired (edit/preview, a host that supplies no lister) rather than showing a dead picker.\nexport interface CandidatesState {\n candidates: SelectionCandidate[];\n loading: boolean;\n error: unknown | null;\n wired: boolean;\n}\n\n// useCandidates resolves the options a selector widget offers for `slot` through the ambient\n// candidate provider. Re-fetches when the provider (i.e. the resolved bindings) or the slot\n// changes, guarded by a monotonic generation so a slow list can't overwrite a newer one\n// (the parent-switch-then-open race). Fail-safe: an error yields an empty option set.\nexport function useCandidates(slot: string | undefined): CandidatesState {\n const provider = useWidgetCandidates();\n const wired = !!provider;\n // Seed loading true when a fetch is imminent (provider + slot present) so the first paint\n // reads \"Loading\u2026\", not a spurious \"No options\" for the frame before the effect runs.\n const [state, setState] = useState<Omit<CandidatesState, 'wired'>>(() => ({\n candidates: [],\n loading: Boolean(provider && slot),\n error: null,\n }));\n const genRef = useRef(0);\n\n useEffect(() => {\n if (!provider || !slot) {\n setState({ candidates: [], loading: false, error: null });\n return;\n }\n const gen = ++genRef.current;\n let live = true;\n // Clear any prior error when a fresh (healthy) fetch starts, so a retry doesn't show\n // \"Loading\u2026\" and a stale error banner together.\n setState((prev) => ({ ...prev, loading: true, error: null }));\n provider(slot)\n .then((candidates) => {\n if (live && gen === genRef.current) setState({ candidates, loading: false, error: null });\n })\n .catch((error: unknown) => {\n if (live && gen === genRef.current) setState({ candidates: [], loading: false, error });\n });\n return () => {\n live = false;\n };\n }, [provider, slot]);\n\n return { ...state, wired };\n}\n\nexport interface MeasurementStreamOptions {\n // Max samples retained in the rolling window (oldest dropped past this). With `windowSeconds`\n // this is the memory ceiling on a span (default MAX_SPAN_SAMPLES); without it, the whole\n // window (default 300).\n window?: number;\n // The window as a SPAN: only samples that occurred within this many seconds of now (or of\n // `pausedAt`) are kept, whatever their number, and `window` is demoted to a ceiling so a\n // busy device cannot grow the buffer without bound. Omit for the count-only window.\n windowSeconds?: number;\n // Freeze the stream at this instant (epoch ms). While set, live samples are BUFFERED, not merged:\n // the returned state does not change, its newest sample keeps its time (so an age readout\n // keeps counting), and the span is measured back from this instant. Clearing it merges\n // everything that arrived in the meantime at once -- the catch-up.\n pausedAt?: number;\n // The clock the span is measured against (epoch ms). Default: the wall clock. A data source that\n // plays a recorded run supplies its own, because the run's samples are dated by the run, not by\n // today, and a span measured from today would drop every one of them.\n now?: () => number;\n // Historical samples to seed the window with before live data arrives (from a\n // bucketedMeasurements backfill). Assumed chronological and older than the live\n // tail; live samples for the same measurement name override the seed's latest.\n initialSamples?: MeasurementSample[];\n}\n\n// The ceiling on retained samples when the window is a span. A span of days at a high event\n// rate would otherwise hold every raw reading; past this the OLDEST go first.\nexport const MAX_SPAN_SAMPLES = 2000;\n\nconst EMPTY: MeasurementStreamState = { latest: {}, samples: [], error: null };\n\n// Build a name\u2192last-sample map from a chronological list (last occurrence wins).\nfunction latestOf(samples: MeasurementSample[]): Record<string, MeasurementSample> {\n const latest: Record<string, MeasurementSample> = {};\n for (const s of samples) latest[s.name] = s;\n return latest;\n}\n\n// evictOldest keeps the newest `max` samples, recording in `evicted` the time of the newest one it\n// drops (the hook's mark for \"the window no longer reaches back to its start\").\nfunction evictOldest(samples: MeasurementSample[], max: number, evicted: { current: number }): MeasurementSample[] {\n if (samples.length <= max) return samples;\n const cut = samples.length - max;\n for (let i = 0; i < cut; i++) {\n const at = Date.parse(samples[i].occurredTime ?? '');\n if (at > evicted.current) evicted.current = at;\n }\n return samples.slice(cut);\n}\n\n// withinSpan drops the samples that occurred before `end - spanSeconds`. A sample whose time is\n// missing or unparseable is KEPT: it cannot be shown to be outside the window, and dropping it\n// would hide a reading the count ceiling would have kept.\nexport function withinSpan(samples: MeasurementSample[], spanSeconds: number, end: number): MeasurementSample[] {\n const from = end - spanSeconds * 1000;\n return samples.filter((s) => {\n if (!s.occurredTime) return true;\n const at = Date.parse(s.occurredTime);\n return Number.isNaN(at) || at >= from;\n });\n}\n\n// useMeasurementStream subscribes a widget's datasource through the hub and keeps\n// the latest value per measurement plus a bounded rolling window. It re-subscribes\n// when the datasource changes (compared by value, so a re-rendered-but-equal\n// selector doesn't churn the subscription) and tears down on unmount. When\n// initialSamples is given, it is merged ahead of the live tail so a chart shows\n// history immediately (and can arrive after the live stream opens).\nexport function useMeasurementStream(\n hub: WidgetDataSource,\n datasource: DatasourceSelector | undefined,\n options: MeasurementStreamOptions = {},\n): MeasurementStreamState {\n const { windowSeconds, pausedAt } = options;\n const windowSize = options.window ?? (windowSeconds !== undefined ? MAX_SPAN_SAMPLES : 300);\n const initialSamples = options.initialSamples;\n const paused = pausedAt !== undefined;\n\n // Value-compare the selector so an unchanged-but-new object reference doesn't\n // resubscribe every render.\n const key = datasource ? JSON.stringify(datasource) : null;\n\n // The span and the pause are read by the subscription's callback through refs, so changing the\n // range (or pausing) never tears the subscription down and loses what it has buffered.\n const spanRef = useRef(windowSeconds);\n const clockRef = useRef(options.now ?? Date.now);\n const pausedRef = useRef(paused);\n const queueRef = useRef<MeasurementSample[]>([]);\n // The newest QUEUED sample per measurement name. The queue is capped, so a long pause at a high\n // rate evicts its oldest entries -- and with them, possibly, the only sample of a measurement\n // that reports rarely. Without this, resuming would leave that measurement's latest value at\n // its pre-pause reading. Bounded by the number of measurement names, not by time.\n const queueLatestRef = useRef<Record<string, MeasurementSample>>({});\n // The time (epoch ms) of the newest sample the retention ceiling has evicted, live or queued.\n // While it is still inside the span, the window no longer covers the whole span and says so\n // (`truncatedBefore`), rather than drawing a short window under a long span's label.\n const evictedRef = useRef(-Infinity);\n const flushRef = useRef<() => void>(() => {});\n const clock = options.now ?? Date.now;\n useEffect(() => {\n spanRef.current = windowSeconds;\n clockRef.current = clock;\n }, [windowSeconds, clock]);\n\n // The live buffer is TAGGED with the selector it was filled from. The effect below\n // resets it when the selector changes, but an effect runs after the render that\n // carried the new selector \u2014 so without the tag that render would show the previous\n // device's values under the new one. A buffer whose tag is not the current key is\n // treated as empty, synchronously.\n const [tagged, setTagged] = useState<{ key: string | null; state: MeasurementStreamState }>({\n key,\n state: EMPTY,\n });\n const live = tagged.key === key ? tagged.state : EMPTY;\n\n useEffect(() => {\n setTagged({ key, state: EMPTY }); // reset the live buffer whenever the datasource changes (or clears)\n queueRef.current = [];\n queueLatestRef.current = {};\n evictedRef.current = -Infinity;\n flushRef.current = () => {};\n if (!datasource) return;\n\n // A callback for a selector the buffer has already moved past (a sink that answers\n // after its subscription was released) leaves the buffer alone rather than re-tagging\n // it to the old selector, which would blank the current one's values.\n const update = (fn: (prev: MeasurementStreamState) => MeasurementStreamState) =>\n setTagged((prev) => (prev.key === key ? { key, state: fn(prev.state) } : prev));\n\n const fold = (\n prev: MeasurementStreamState,\n incoming: MeasurementSample[],\n newest: Record<string, MeasurementSample> = latestOf(incoming),\n ): MeasurementStreamState => {\n let samples = prev.samples.concat(incoming);\n const span = spanRef.current;\n if (span !== undefined) samples = withinSpan(samples, span, clockRef.current());\n samples = evictOldest(samples, windowSize, evictedRef);\n return { latest: { ...prev.latest, ...newest }, samples, error: null };\n };\n\n flushRef.current = () => {\n const queued = queueRef.current;\n if (queued.length === 0) return;\n const newest = queueLatestRef.current;\n queueRef.current = [];\n queueLatestRef.current = {};\n update((prev) => fold(prev, queued, newest));\n };\n\n return hub.subscribeWidget(datasource, {\n next: (sample) => {\n if (pausedRef.current) {\n // Buffered, bounded: a pause that outlasts the ceiling keeps the newest.\n queueRef.current = evictOldest([...queueRef.current, sample], windowSize, evictedRef);\n queueLatestRef.current[sample.name] = sample;\n return;\n }\n update((prev) => fold(prev, [sample]));\n },\n error: (err) => update((prev) => ({ ...prev, error: err })),\n });\n // `datasource` is intentionally read via `key` (value identity), not reference.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [hub, key, windowSize]);\n\n // The pause gate. Resuming merges what queued while frozen. The flag flips before the flush,\n // so a sample landing between the two is merged rather than queued behind it.\n useEffect(() => {\n pausedRef.current = paused;\n if (!paused) flushRef.current();\n }, [paused]);\n\n // Layer the history seed under the live tail. Kept separate from the live buffer\n // so late-arriving history (async backfill) recomputes without touching the\n // subscription, and switching datasource clears live while the new seed applies.\n return useMemo(() => {\n // The span is measured back from the freeze instant while paused, from now otherwise.\n const end = pausedAt ?? clock();\n if (windowSeconds === undefined) {\n if (!initialSamples || initialSamples.length === 0) return live;\n const merged = initialSamples.concat(live.samples);\n return {\n latest: { ...latestOf(initialSamples), ...live.latest },\n samples: merged.length > windowSize ? merged.slice(merged.length - windowSize) : merged,\n error: live.error,\n };\n }\n // A span. Everything kept is inside it, so anything the ceiling cuts here -- or cut earlier\n // from the live buffer or the pause queue, while still inside it -- is a hole at the start of\n // the window. The ceiling drops the OLDEST, so what is kept is one contiguous run; it is\n // marked with where that run begins, so a chart can say it covers less than its span.\n const from = end - windowSeconds * 1000;\n const merged = withinSpan(initialSamples ? initialSamples.concat(live.samples) : live.samples, windowSeconds, end);\n const samples = merged.length > windowSize ? merged.slice(merged.length - windowSize) : merged;\n const cut = samples.length < merged.length || evictedRef.current >= from;\n const truncatedBefore = cut ? (samples[0]?.occurredTime ?? undefined) : undefined;\n const latest = initialSamples ? { ...latestOf(initialSamples), ...live.latest } : live.latest;\n if (!truncatedBefore && latest === live.latest && samples.length === live.samples.length) return live;\n const state: MeasurementStreamState = { latest, samples, error: live.error };\n if (truncatedBefore) state.truncatedBefore = truncatedBefore;\n return state;\n }, [initialSamples, live, windowSize, windowSeconds, pausedAt, clock]);\n}\n\nconst EMPTY_ALARMS: AlarmStreamState = { alarms: [], total: 0, loading: true, error: null };\n\n// useAlarmStream binds an alarm widget's scope+filters through the hub's alarm channel\n// and keeps the latest snapshot. It re-subscribes when the subscription changes\n// (value-compared, so a re-rendered-but-equal object doesn't churn) and tears down on\n// unmount. The hub delivers whole snapshots (query-then-reconcile), so this holds the\n// last one rather than accumulating \u2014 the query is the source of truth.\nexport function useAlarmStream(\n hub: WidgetDataSource,\n subscription: AlarmSubscription,\n): AlarmStreamState {\n // Value-compare the subscription so an unchanged-but-new object reference doesn't\n // resubscribe every render.\n const key = JSON.stringify(subscription);\n // The snapshot is TAGGED with the subscription it came from, as the measurement buffer is: the\n // effect that resets it runs after the render carrying the new subscription, and that render\n // must not show the previous scope's rows under it (nor hand them to a pause hold to keep).\n const [[at, state], setState] = useState<[string, AlarmStreamState]>([key, EMPTY_ALARMS]);\n\n useEffect(() => {\n setState([key, EMPTY_ALARMS]); // reset to loading whenever the subscription changes\n return hub.subscribeAlarms(subscription, {\n next: (snapshot) =>\n setState([key, { alarms: snapshot.alarms, total: snapshot.total, loading: false, error: null }]),\n error: (err) => setState(([, prev]) => [key, { ...prev, loading: false, error: err }]),\n });\n // `subscription` is intentionally read via `key` (value identity), not reference.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [hub, key]);\n\n return at === key ? state : EMPTY_ALARMS;\n}\n\nconst EMPTY_COMMANDS: CommandStreamState = {\n deviceToken: null,\n commands: [],\n total: 0,\n loading: true,\n error: null,\n};\n\n// useCommandStream binds a command widget's scope through the hub's control channel and\n// keeps the latest command-history snapshot. It re-subscribes when the subscription\n// changes (value-compared) and tears down on unmount. The hub delivers whole snapshots\n// (poll-then-emit), so this holds the last one rather than accumulating.\nexport function useCommandStream(\n hub: WidgetDataSource,\n subscription: CommandSubscription,\n): CommandStreamState {\n const [state, setState] = useState<CommandStreamState>(EMPTY_COMMANDS);\n\n // Value-compare the subscription so an unchanged-but-new object reference doesn't\n // resubscribe every render.\n const key = JSON.stringify(subscription);\n\n useEffect(() => {\n setState(EMPTY_COMMANDS); // reset to loading whenever the subscription changes\n return hub.subscribeCommands(subscription, {\n next: (snapshot) =>\n setState({\n deviceToken: snapshot.deviceToken,\n commands: snapshot.commands,\n total: snapshot.total,\n loading: false,\n error: null,\n }),\n error: (err) => setState((prev) => ({ ...prev, loading: false, error: err })),\n });\n // `subscription` is intentionally read via `key` (value identity), not reference.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [hub, key]);\n\n return state;\n}\n\nconst EMPTY_LOCATIONS: LocationStreamState = {\n locations: [],\n deviceTokens: [],\n forbidden: false,\n loading: true,\n error: null,\n};\n\n// useLocationStream binds a map widget's selector through the hub's location channel and\n// keeps the latest position snapshot. It re-subscribes when the subscription changes\n// (value-compared, so a re-rendered-but-equal object doesn't churn) and tears down on\n// unmount. The hub delivers whole snapshots (poll-then-emit), so this holds the last one\n// rather than accumulating.\n//\n// A `forbidden` snapshot CLEARS the positions as it sets the flag: an authority can be\n// revoked mid-session, and keeping the last-seen coordinates on screen under a\n// permission banner would leave a refused viewer looking at the very data they were\n// just refused.\nexport function useLocationStream(\n hub: WidgetDataSource,\n subscription: LocationSubscription,\n): LocationStreamState {\n // Value-compare the subscription so an unchanged-but-new object reference doesn't\n // resubscribe every render.\n const key = JSON.stringify(subscription);\n // Tagged with its subscription, as useAlarmStream's snapshot is: no render shows the previous\n // selector's markers under a new one.\n const [[at, state], setState] = useState<[string, LocationStreamState]>([key, EMPTY_LOCATIONS]);\n\n useEffect(() => {\n setState([key, EMPTY_LOCATIONS]); // reset to loading whenever the subscription changes\n return hub.subscribeLocations(subscription, {\n next: (snapshot) =>\n setState([\n key,\n snapshot.kind === 'forbidden'\n ? { locations: [], deviceTokens: [], forbidden: true, loading: false, error: null }\n : {\n locations: snapshot.locations,\n deviceTokens: snapshot.deviceTokens,\n forbidden: false,\n loading: false,\n error: null,\n },\n ]),\n error: (err) => setState(([, prev]) => [key, { ...prev, loading: false, error: err }]),\n });\n // `subscription` is intentionally read via `key` (value identity), not reference.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [hub, key]);\n\n return at === key ? state : EMPTY_LOCATIONS;\n}\n\n// A widget's bound-entity availability: 'unknown' until the async check resolves\n// (render optimistically meanwhile), then 'available', or 'unavailable' when the bound\n// device no longer exists (a deleted device's stable token, ADR-044).\nexport type DatasourceAvailability = 'unknown' | 'available' | 'unavailable';\n\n// While a widget shows \"unavailable\", re-check on this cadence so a device deleted then\n// recreated with the same token (ADR-042 frees tokens on delete) recovers on a long-lived\n// viewer without a reload. Only runs while unavailable \u2014 a live device is checked once.\nconst AVAILABILITY_REVALIDATE_MS = 60_000;\n\n// useDatasourceAvailability resolves whether a widget's bound device still exists,\n// WITHOUT blocking the widget's data stream (optimistic + async): it starts 'unknown'\n// (the widget renders normally), then flips to 'available'/'unavailable' once the hub's\n// existence check resolves. Fails open (\u2192'available') so a check outage never falsely\n// blanks a live widget. A widget with no datasource is trivially available (no query).\n// Re-checks when the datasource changes (value-compared), and periodically while\n// unavailable so a recreated device recovers.\nexport function useDatasourceAvailability(\n hub: WidgetDataSource,\n datasource: DatasourceSelector | undefined,\n): DatasourceAvailability {\n const [state, setState] = useState<DatasourceAvailability>('unknown');\n const key = datasource ? JSON.stringify(datasource) : null;\n\n useEffect(() => {\n // No datasource (label/image, or tenant-wide) has nothing to validate \u2014 available\n // immediately, no query, no re-render churn.\n if (!datasource) {\n setState('available');\n return;\n }\n setState('unknown');\n let cancelled = false;\n let timer: ReturnType<typeof setInterval> | undefined;\n\n const stopTimer = (): void => {\n if (timer) {\n clearInterval(timer);\n timer = undefined;\n }\n };\n\n const check = (): void => {\n hub\n .isDatasourceAvailable(datasource)\n .then((ok) => {\n if (cancelled) return;\n setState(ok ? 'available' : 'unavailable');\n if (ok) stopTimer(); // recovered (or was fine) \u2014 stop polling\n })\n .catch(() => {\n if (cancelled) return;\n setState('available'); // fail open\n stopTimer();\n });\n };\n\n check();\n // Poll only matters once unavailable; harmlessly runs until the first check clears it\n // for a live device (cleared in the resolver above, well before the first tick).\n timer = setInterval(check, AVAILABILITY_REVALIDATE_MS);\n\n return () => {\n cancelled = true;\n stopTimer();\n };\n // `datasource` is intentionally read via `key` (value identity), not reference.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [hub, key]);\n\n return state;\n}\n\nexport interface ElementSize {\n width: number;\n height: number;\n}\n\n// useElementSize tracks an element's content-box size via ResizeObserver, so a\n// widget can scale its contents (chart fonts, gauge ticks, a big number) to the\n// slot it was resized to \u2014 ECharts' resize() re-lays-out the canvas but never\n// scales absolute font sizes, so a small widget crams without this. Returns\n// {0,0} until measured (and where ResizeObserver is unavailable, e.g. jsdom); the\n// consumers treat a zero size as \"use default sizing\".\n//\n// `step` quantises each dimension DOWN to a multiple of it (default 1: exact), so a caller that\n// only needs a coarse size -- the frame feeding every widget's scale -- does not re-render for\n// sub-pixel or few-pixel jitter. Down, so text fitted to the quantised box always fits the\n// real one.\nexport function useElementSize<T extends HTMLElement>(step = 1): [RefObject<T | null>, ElementSize] {\n const ref = useRef<T>(null);\n const [size, setSize] = useState<ElementSize>({ width: 0, height: 0 });\n\n useEffect(() => {\n const el = ref.current;\n if (!el || typeof ResizeObserver === 'undefined') return;\n const observer = new ResizeObserver((entries) => {\n const rect = entries[0]?.contentRect;\n if (!rect) return;\n const width = step > 1 ? Math.floor(rect.width / step) * step : rect.width;\n const height = step > 1 ? Math.floor(rect.height / step) * step : rect.height;\n setSize((prev) => (prev.width === width && prev.height === height ? prev : { width, height }));\n });\n observer.observe(el);\n return () => observer.disconnect();\n }, [step]);\n\n return [ref, size];\n}\n\n// What useHubConnectionState needs of a hub: the live implementation (DashboardHub) has it, a\n// synthetic preview source does not, and a host holding only that reads `live`.\nexport interface ConnectionStateSource {\n readonly connectionState: HubConnectionState;\n onConnectionState(listener: () => void): () => void;\n}\n\nconst NO_CONNECTION_SOURCE: ConnectionStateSource = {\n connectionState: 'live',\n onConnectionState: () => () => {},\n};\n\n// useHubConnectionState follows a hub's socket state (live / reconnecting / offline) for a\n// header badge. It re-renders only when the state changes.\nexport function useHubConnectionState(hub: Partial<ConnectionStateSource> | undefined): HubConnectionState {\n // A source that reports no connection (a synthetic preview, a recording) is not \"down\":\n // there is no socket to lose, so it reads live.\n const source =\n hub && typeof hub.onConnectionState === 'function' ? (hub as ConnectionStateSource) : NO_CONNECTION_SOURCE;\n return useSyncExternalStore(\n (listener) => source.onConnectionState(listener),\n () => source.connectionState,\n );\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// useWidgetScale -- the numeric half of the size contract. HTML text sizes itself with `cq`\n// units off the frame's content column (fitValueFontCss); a canvas (an ECharts gauge) cannot\n// read those, so it asks this hook for the same box in numbers. Both read the one curve in\n// ./fit-text and the one box -- the frame's padded content column -- so they agree.\n\nimport { useEffect, useMemo, useReducer, useRef } from 'react';\n\nimport { useWidgetBodySize, useWidgetDensityScale, type WidgetBodySize } from './context';\nimport { stickyChars, type StickyChars } from './fit-text';\nimport { tokenPx, type SizeToken } from './tokens';\n\nexport type SizeClass = 'compact' | 'regular' | 'large';\n\n// The content-column sizes (px, at density 1; multiplied by the density scale) that separate the\n// classes. Compact: too small for a full variant (a value and a caption); large: room for\n// extras (a sparkline, a legend).\nexport const SIZE_CLASS_BREAKS = {\n compactWidth: 120,\n compactHeight: 72,\n largeWidth: 420,\n largeHeight: 220,\n} as const;\n\nexport function sizeClassOf(size: WidgetBodySize, scale = 1): SizeClass {\n // Unmeasured (first frame, jsdom) is `regular`: the default rendering, not a degraded one.\n if (!(size.width > 0) || !(size.height > 0)) return 'regular';\n const b = SIZE_CLASS_BREAKS;\n if (size.width < b.compactWidth * scale || size.height < b.compactHeight * scale) return 'compact';\n if (size.width >= b.largeWidth * scale && size.height >= b.largeHeight * scale) return 'large';\n return 'regular';\n}\n\nexport interface WidgetScale {\n sizeClass: SizeClass;\n // The board's density scale (1 outside a renderer).\n scale: number;\n // The frame's content column in px, quantised to 4.\n size: WidgetBodySize;\n // The px of a size token at this density, for a canvas.\n px: (token: SizeToken) => number;\n}\n\nexport function makeWidgetScale(size: WidgetBodySize, scale: number): WidgetScale {\n return { sizeClass: sizeClassOf(size, scale), scale, size, px: (token: SizeToken) => tokenPx(token, scale) };\n}\n\n// useWidgetScale reads the frame's content column, so it works in a component RENDERED INSIDE a\n// WidgetFrame -- not in the component that renders the frame (the frame's context does not\n// reach above it). A widget that needs the numbers for its own top-level render passes\n// WidgetFrame a function child instead: `<WidgetFrame>{(scale) => ...}</WidgetFrame>`.\nexport function useWidgetScale(): WidgetScale {\n const size = useWidgetBodySize();\n const scale = useWidgetDensityScale();\n return useMemo(() => makeWidgetScale(size, scale), [size, scale]);\n}\n\n// How long a readout keeps its longest recent length after it shrinks.\nconst STICKY_HOLD_MS = 60_000;\n\n// useStickyChars -- the character count to hand the font curve for a live readout: the longest\n// seen in the last minute, so 9.99 -> 10.01 does not resize the number. Holding the LONGER\n// length can only make the font smaller than the live value needs, never larger, so it cannot\n// make a readout overflow.\nexport function useStickyChars(chars: number, holdMs = STICKY_HOLD_MS): number {\n const held = useRef<StickyChars | undefined>(undefined);\n const [tick, wake] = useReducer((n: number) => n + 1, 0);\n // Written during render on purpose: the value is a pure function of (prev, chars, time) and\n // an extra render recomputes the same answer, so there is nothing to commit or clean up.\n held.current = stickyChars(held.current, chars, Date.now(), holdMs);\n const { value, since } = held.current;\n // A hold is released only by a render. A value that shrinks and then stays put (an alarm\n // count falling from 1000 to 9 and holding there) brings no further render, so without this\n // it would keep the long length's small font indefinitely, not for a minute: render again\n // when the hold runs out. `tick` re-arms the timer should it ever fire a moment early.\n useEffect(() => {\n if (value <= chars) return;\n const timer = setTimeout(wake, Math.max(0, since + holdMs - Date.now()));\n return () => clearTimeout(timer);\n }, [value, since, chars, holdMs, tick]);\n return value;\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// A small header badge for a hub's connection state. Presentational: the host supplies the\n// words (its own i18n), so the SDK adds no strings of its own here.\n\nimport type { HubConnectionState } from '@devicechain/dashboards';\n\nimport { css, type ThemeVar } from './theme';\nimport { fontVar } from './tokens';\n\nexport interface HubConnectionBadgeProps {\n state: HubConnectionState;\n labels: Record<HubConnectionState, string>;\n}\n\nconst DOT: Record<HubConnectionState, ThemeVar> = {\n live: 'primary',\n reconnecting: 'muted-foreground',\n offline: 'destructive',\n};\n\nexport function HubConnectionBadge({ state, labels }: HubConnectionBadgeProps) {\n return (\n <span\n role=\"status\"\n data-connection-state={state}\n style={{\n display: 'inline-flex',\n alignItems: 'center',\n gap: 6,\n fontSize: fontVar('sm'),\n padding: '2px 8px',\n borderRadius: 999,\n border: `1px solid ${css('border')}`,\n color: state === 'offline' ? css('destructive') : css('muted-foreground'),\n whiteSpace: 'nowrap',\n }}\n >\n <span\n aria-hidden\n style={{\n width: 8,\n height: 8,\n borderRadius: '50%',\n background: css(DOT[state]),\n opacity: state === 'reconnecting' ? 0.6 : 1,\n }}\n />\n {labels[state]}\n </span>\n );\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Directional change-flash \u2014 an opt-in cue (widget option `flashOnChange`) that briefly\n// tints a numeric value green when it rises and red when it falls, then fades back, so a\n// glance catches direction of change on a live board (cf. a trading \"last\" column). Purely\n// presentational: no hub/model change, driven off the value the widget already renders.\n//\n// Single-value widgets (latest-card) color the number; a multi-row widget (table,\n// alarm-table) renders each value through <FlashValue>, so React's per-key component\n// instances give every row its own independent flash with no map bookkeeping. The gauge\n// (an opaque ECharts canvas whose number can't be CSS-transitioned) tints its container\n// instead \u2014 see flashBackgroundStyle.\n\nimport { useEffect, useRef, useState, type CSSProperties } from 'react';\n\nimport { formatValue } from './format';\n\nexport type FlashDirection = 'up' | 'down' | null;\n\n// Semantic up/down colors, mid-saturation so they read on both light and dark host themes;\n// a host may override with the --flash-up / --flash-down custom properties. The background\n// variants are translucent so a tint sits over the widget's own surface without hiding it.\nconst UP = {\n fg: 'var(--flash-up, hsl(142 72% 40%))',\n bg: 'var(--flash-up-bg, hsl(142 72% 45% / 0.16))',\n};\nconst DOWN = {\n fg: 'var(--flash-down, hsl(0 72% 51%))',\n bg: 'var(--flash-down-bg, hsl(0 72% 55% / 0.16))',\n};\n\n// How long (ms) the solid tint holds before fading, and how long the fade back takes.\nconst HOLD_MS = 400;\nconst FADE_MS = 600;\n\n// useFlashOnChange watches a numeric value and returns the direction of its most recent\n// change ('up' | 'down') for a short window, then null. It stays null when the feature is\n// off, on the first value (no flash on mount), on a non-finite value, or when `identity`\n// changes \u2014 the last so a widget bound to an ANCHOR (whose one measurement slot interleaves\n// values from many member devices) adopts the new device's value instead of reading the\n// device-to-device jump as a real rise/fall. Pass the value's owning entity as `identity`\n// (e.g. `deviceToken:name`); leave it undefined for a single-device value.\n//\n// The held direction auto-clears after HOLD_MS; the consumer fades it out via flashTextStyle.\n// Every early-return path resets `direction` too: the effect only re-runs on a dep change,\n// and each such change has already cancelled the pending timer, so not clearing here would\n// strand a tint (e.g. value \u2192 null, or toggled off then on, mid-hold).\nexport function useFlashOnChange(\n value: number | null | undefined,\n enabled: boolean,\n identity?: string | number,\n): FlashDirection {\n const prev = useRef<number | null | undefined>(value);\n const prevIdentity = useRef(identity);\n const [direction, setDirection] = useState<FlashDirection>(null);\n\n useEffect(() => {\n const before = prev.current;\n const sameEntity = prevIdentity.current === identity;\n prev.current = value;\n prevIdentity.current = identity;\n if (\n !enabled ||\n !sameEntity ||\n typeof value !== 'number' ||\n typeof before !== 'number' ||\n !Number.isFinite(value) ||\n !Number.isFinite(before) ||\n value === before\n ) {\n setDirection(null); // a no-op when already null (React bails out); clears a stale tint otherwise\n return;\n }\n setDirection(value > before ? 'up' : 'down');\n const timer = setTimeout(() => setDirection(null), HOLD_MS);\n return () => clearTimeout(timer);\n }, [value, enabled, identity]);\n\n // Mask any lingering state when disabled, so toggling off is immediate.\n return enabled ? direction : null;\n}\n\n// flashTextStyle colors text for the current direction: snap to the tint (no transition)\n// while flashing, then fade `color` back to the element's own color when it clears. Merge\n// it AFTER an explicit base `color` so the tint overrides while set and the base shows\n// through (and animates) once cleared.\nexport function flashTextStyle(direction: FlashDirection, fadeMs: number = FADE_MS): CSSProperties {\n if (direction) return { color: direction === 'up' ? UP.fg : DOWN.fg, transition: 'none' };\n return { transition: `color ${fadeMs}ms ease-out` };\n}\n\n// flashBackgroundStyle is the same cue as a translucent background tint, for a widget whose\n// value can't be recolored directly (the gauge's canvas). Snap in, fade the background back.\nexport function flashBackgroundStyle(direction: FlashDirection, fadeMs: number = FADE_MS): CSSProperties {\n if (direction) {\n return { backgroundColor: direction === 'up' ? UP.bg : DOWN.bg, transition: 'none' };\n }\n return { transition: `background-color ${fadeMs}ms ease-out` };\n}\n\n// FlashValue renders a formatted numeric value that flashes on change \u2014 the reusable cell\n// for multi-row widgets. Each rendered instance owns its own flash state, so keying rows by\n// entity (React's list key) gives every row an independent cue for free.\nexport function FlashValue({\n value,\n precision,\n enabled,\n identity,\n baseColor = 'inherit',\n style,\n}: {\n value: number | null | undefined;\n precision?: number;\n enabled: boolean;\n // The value's owning entity (see useFlashOnChange): when it changes, adopt the new value\n // rather than flash. Distinguishes a real per-device change from an anchor's cross-device\n // interleave. Undefined for a single-device value.\n identity?: string | number;\n // The color the value fades back to (defaults to the inherited text color).\n baseColor?: string;\n style?: CSSProperties;\n}) {\n const direction = useFlashOnChange(value, enabled, identity);\n return (\n <span style={{ color: baseColor, ...style, ...flashTextStyle(direction) }}>\n {formatValue(value, precision)}\n </span>\n );\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// @devicechain/widget-sdk \u2014 the supported surface for writing a dashboard widget.\n//\n// A widget is declared once with `defineWidget` (a portable, JSON-only descriptor plus a\n// trusted React component) and made available to a host by listing it in\n// `createWidgetRegistry`. The built-in widgets are declared through exactly this\n// interface. The descriptor vocabulary is React-free and lives in @devicechain/dashboards;\n// it is re-exported here so a widget package imports from one place.\n\nexport { defineWidget } from './define-widget';\nexport type { ErasedComponent, ErasedWidgetDefinition, WidgetDefinition, WidgetImplementation } from './define-widget';\nexport { createWidgetRegistry, WidgetRegistryError, type WidgetRegistry } from './registry';\nexport type {\n EditorCommandChoice,\n EditorCommandChoices,\n WidgetComponent,\n WidgetEditorProps,\n WidgetEditorServices,\n WidgetProps,\n} from './widget';\n\n// The ambient host contexts (selection, candidates, time zone, theme root, subject). Defined\n// in this package and nowhere else \u2014 see ./context.\nexport {\n WidgetCandidatesProvider,\n WidgetDensityProvider,\n WidgetFrameStyleProvider,\n WidgetFreshnessProvider,\n WidgetSelectProvider,\n WidgetSubjectProvider,\n WidgetThemeRootProvider,\n WidgetTimeRangeProvider,\n WidgetTimeZoneProvider,\n useWidgetCandidates,\n useWidgetFrameStyle,\n useWidgetFreshness,\n useWidgetSelect,\n useWidgetSubject,\n useWidgetThemeRoot,\n useWidgetTimeRange,\n useWidgetTimeZone,\n type WidgetBodySize,\n type WidgetCandidates,\n type WidgetFreshness,\n type WidgetSelect,\n} from './context';\n\n// The building blocks every widget (built-in or third-party) composes from: the card frame,\n// theme-variable references, number/time formatting, the opt-in change-flash cue, option\n// readers, and the React bindings between the hub's channels and widget state. The chart\n// engine helpers are NOT here -- they are on the `/echarts` subpath, so importing this root\n// never pulls Apache ECharts (nor MapLibre, nor the built-in catalog).\nexport { WidgetFrame, type WidgetFrameProps } from './frame';\nexport { css, type ThemeVar } from './theme';\n// The curve's constants and the pure sticky-length step stay internal: a widget reaches them\n// through fitValueFont / fitValueFontCss / useStickyChars, so the curve can be retuned.\nexport { FIT_MIN_FONT_PX, fitValueFont, fitValueFontCss, type FitOptions } from './fit-text';\nexport {\n FONT_TOKENS,\n SPACE_TOKENS,\n WidgetTokenStyles,\n densityScale,\n fontVar,\n spaceVar,\n tokenPx,\n tokenStyle,\n type FontToken,\n type SizeToken,\n type SpaceToken,\n} from './tokens';\n// The body-size and density contexts are plumbing between the frame, the renderer and this\n// hook: a widget reads them through useWidgetScale, and only the renderer provides density.\nexport { SIZE_CLASS_BREAKS, useStickyChars, useWidgetScale, type SizeClass, type WidgetScale } from './scale';\nexport { HubConnectionBadge, type HubConnectionBadgeProps } from './connection-badge';\nexport { Freshness, FreshnessReadout, StaleBadge, useFreshness, useNow, type FreshnessProps, type FreshnessReading } from './freshness-readout';\nexport {\n ageSeconds,\n formatAge,\n freshnessStrings,\n isStale,\n newestSampleTime,\n resolveStaleAfterSeconds,\n viewerLocale,\n} from './freshness';\nexport { formatDateTime, formatTimestamp, formatValue } from './format';\nexport {\n ALARM_LEVELS,\n SEVERITY_LEVELS,\n SEVERITY_TOKENS,\n isAlarmLevel,\n isSeverityLevel,\n levelColor,\n severityTokenCss,\n severityVar,\n useSeverityColors,\n type AlarmLevel,\n type SeverityLevel,\n} from './severity';\nexport {\n bandAt,\n optThresholds,\n parseThresholds,\n thresholdBands,\n type Threshold,\n type ThresholdBand,\n} from './thresholds';\nexport {\n FlashValue,\n flashBackgroundStyle,\n flashTextStyle,\n useFlashOnChange,\n type FlashDirection,\n} from './flash';\nexport { optBoolean, optNumber, optString, pickSample, primaryMeasurementName } from './widget-options';\nexport {\n useAlarmStream,\n useCandidates,\n useCommandStream,\n useDashboardHub,\n useDatasourceAvailability,\n useElementSize,\n useHubConnectionState,\n useLocationStream,\n useMeasurementStream,\n MAX_SPAN_SAMPLES,\n withinSpan,\n useResolvedBindings,\n useSlotCandidates,\n type AggregateStreamState,\n type AlarmStreamState,\n type CandidatesState,\n type CommandStreamState,\n type ConnectionStateSource,\n type DatasourceAvailability,\n type ElementSize,\n type LocationStreamState,\n type MeasurementStreamOptions,\n type MeasurementStreamState,\n} from './hooks';\n\n// The portable descriptor vocabulary.\nexport {\n SDK_API_VERSION,\n WidgetCatalogError,\n defineWidgetDescriptor,\n isBuiltinWidgetId,\n isNamespacedWidgetId,\n validateWidgetDescriptor,\n type AckOptionRef,\n type BooleanOptionSpec,\n type BuiltinWidgetId,\n type ChannelBindingPolicy,\n type ChannelSnapshot,\n type ConstBinding,\n type DatasourcePolicy,\n type DatasourceSelectorKind,\n type DefaultsOf,\n type DescriptorIssue,\n type EnumOptionSpec,\n type ListItemFieldSpec,\n type ListOptionSpec,\n type GreaterThanConstraint,\n type HubConnectionState,\n type NamespacedWidgetId,\n type NumberOptionRef,\n type NumberOptionSpec,\n type OptionConstraint,\n type OptionSpec,\n type OptionsOf,\n type StringOptionRef,\n type StringOptionSpec,\n type WidgetCatalog,\n type WidgetChannel,\n type WidgetDescriptor,\n type WidgetDisplay,\n type WidgetId,\n type WidgetInteractionDeclaration,\n type WidgetOptionSpecs,\n} from '@devicechain/dashboards';\n"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": ["useMemo", "jsx", "useEffect", "useState", "useEffect", "useState", "merged", "useEffect", "useMemo", "useRef", "useMemo", "useRef", "useEffect", "jsx", "jsxs", "useMemo", "jsx", "jsxs", "useEffect", "useRef", "useState", "jsx", "useRef", "useState", "useEffect", "WidgetCatalogError"]
|
|
4
|
+
"sourcesContent": ["// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\nimport type {\n ChannelSnapshot,\n OptionsOf,\n WidgetChannel,\n WidgetDescriptor,\n WidgetOptionSpecs,\n} from '@devicechain/dashboards';\nimport type { ComponentType } from 'react';\n\nimport type { WidgetEditorProps, WidgetProps } from './widget';\n\n// The trusted half of a registration: the React code. Kept apart from the descriptor so\n// the descriptor stays portable plain data and headless validators never load a component.\nexport interface WidgetImplementation<C extends WidgetChannel, S extends WidgetOptionSpecs> {\n // The only execution kind in this release: React code the host bundled at build time,\n // running with the host page's privileges.\n kind: 'trusted-react';\n component: ComponentType<WidgetProps<ChannelSnapshot<C>>>;\n editor?: ComponentType<WidgetEditorProps<OptionsOf<S>>>;\n}\n\nexport interface WidgetDefinition<\n C extends WidgetChannel = WidgetChannel,\n S extends WidgetOptionSpecs = WidgetOptionSpecs,\n> {\n descriptor: WidgetDescriptor<C, S>;\n implementation: WidgetImplementation<C, S>;\n}\n\n// A component with its props type erased. `any` is deliberate and confined to this alias:\n// `ComponentType<never>` would be the principled erasure, but a class component's\n// `defaultProps` is covariant in its props and rejects it, and a registry has to hold\n// components of every channel's props.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type ErasedComponent = ComponentType<any>;\n\n// A definition with its component types erased, which is what a registry stores: any\n// specific WidgetDefinition<C, S> is assignable to it.\nexport interface ErasedWidgetDefinition {\n descriptor: WidgetDescriptor;\n implementation: {\n kind: 'trusted-react';\n component: ErasedComponent;\n editor?: ErasedComponent;\n };\n}\n\n// defineWidget is an identity at runtime; it exists for inference. The channel and the\n// option schema are inferred from the literal, so a component whose data disagrees with\n// the channel, defaults that disagree with the schema, a binding that names a missing or\n// wrong-kind option, or an interaction the channel cannot carry is a COMPILE error \u2014 and\n// createWidgetRegistry checks the same things again at runtime for callers that bypass the\n// compiler. Every widget, built-in or third-party, is declared through this one helper.\nexport function defineWidget<const C extends WidgetChannel, const S extends WidgetOptionSpecs>(\n definition: WidgetDefinition<C, S>,\n): WidgetDefinition<C, S> {\n return definition;\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\nimport {\n WidgetCatalogError,\n collectCatalogIssues,\n createWidgetCatalog,\n type DescriptorIssue,\n type WidgetCatalog,\n type WidgetDescriptor,\n} from '@devicechain/dashboards';\n\nimport type { ErasedWidgetDefinition } from './define-widget';\n\n// Thrown by createWidgetRegistry with EVERY problem found, so a host fixes its composition\n// in one pass instead of one failed start per fault.\nexport class WidgetRegistryError extends WidgetCatalogError {\n constructor(issues: readonly DescriptorIssue[]) {\n super(issues, 'widget registry');\n this.name = 'WidgetRegistryError';\n }\n}\n\n// A sealed registry: descriptors (through the React-free catalog the parser sees) plus the\n// trusted implementations. Immutable \u2014 there is no register(), no process-global state and\n// no replacement while a dashboard is mounted. Two registries are independent.\nexport interface WidgetRegistry {\n // The React-free view of this registry, for parsing and headless validation.\n readonly catalog: WidgetCatalog;\n descriptor(id: string): WidgetDescriptor | undefined;\n implementation(id: string): ErasedWidgetDefinition['implementation'] | undefined;\n // Definitions in the order they were supplied.\n list(): readonly ErasedWidgetDefinition[];\n}\n\nfunction isComponent(value: unknown): boolean {\n if (typeof value === 'function') return true;\n // memo / forwardRef / lazy components are objects tagged with $$typeof.\n return typeof value === 'object' && value !== null && '$$typeof' in value;\n}\n\ntype Loose = Partial<ErasedWidgetDefinition> | null | undefined;\n\n// createWidgetRegistry validates every definition (descriptor invariants, duplicate ids,\n// the implementation shape) and returns a deeply frozen registry, or throws one\n// WidgetRegistryError listing all the problems.\nexport function createWidgetRegistry(definitions: readonly ErasedWidgetDefinition[]): WidgetRegistry {\n if (!Array.isArray(definitions)) {\n throw new WidgetRegistryError([{ id: '#?', path: '', message: 'definitions must be an array' }]);\n }\n const issues: DescriptorIssue[] = collectCatalogIssues(definitions.map((d) => (d as Loose)?.descriptor));\n\n definitions.forEach((definition, index) => {\n const id: unknown = (definition as Loose)?.descriptor?.id;\n const label = typeof id === 'string' && id !== '' ? id : `#${index}`;\n const add = (path: string, message: string): void => {\n issues.push({ id: label, path, message });\n };\n const impl = (definition as Loose)?.implementation;\n if (typeof impl !== 'object' || impl === null) {\n add('implementation', 'a definition needs an implementation');\n return;\n }\n if (impl.kind !== 'trusted-react') {\n add('implementation.kind', `kind must be \"trusted-react\", got ${JSON.stringify(impl.kind)}`);\n }\n if (!isComponent(impl.component)) add('implementation.component', 'component must be a React component');\n if (impl.editor !== undefined && !isComponent(impl.editor)) {\n add('implementation.editor', 'editor, when given, must be a React component');\n }\n });\n\n if (issues.length > 0) throw new WidgetRegistryError(issues);\n\n // Through the one public, validating constructor: no unvalidated path to a catalog exists.\n // The descriptors were already checked above (so this cannot throw for them); checking\n // again is the price of not exporting a catalog builder that skips validation.\n //\n // \uD83D\uDD34 The catalog freezes each descriptor IN PLACE (deeply), so the caller's descriptor\n // objects are frozen after this call. That is deliberate: a built-in descriptor holds its\n // option schema BY IDENTITY with WIDGET_OPTIONS, and a copy would break that identity.\n const catalog = createWidgetCatalog(definitions.map((d) => d.descriptor));\n const byId = new Map<string, ErasedWidgetDefinition>();\n for (const definition of definitions) {\n // The implementation object, unlike the descriptor, is copied before freezing, so the\n // caller's implementation object is left mutable (the components it names are shared).\n byId.set(\n definition.descriptor.id,\n Object.freeze({\n descriptor: catalog.get(definition.descriptor.id) as WidgetDescriptor,\n implementation: Object.freeze({ ...definition.implementation }),\n }),\n );\n }\n const ordered: readonly ErasedWidgetDefinition[] = Object.freeze([...byId.values()]);\n\n return Object.freeze({\n catalog,\n descriptor: (id: string) => catalog.get(id),\n implementation: (id: string) => byId.get(id)?.implementation,\n list: () => ordered,\n });\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// The shared card frame every widget renders inside \u2014 a bordered, themed surface\n// that fills its dashboard-canvas slot, with an optional title bar. Styled with\n// inline CSS-variable references so it themes from the host without Tailwind.\n//\n// The frame is SIZE-AWARE. Its content column -- the padded box the widget draws in -- is a\n// `container-type: size` query container, so HTML text can size itself with `cq` units on the\n// first frame, and the SAME box is measured (quantised to 4 px) and handed to everything inside\n// through context (useWidgetScale), so a canvas widget fits to the identical numbers. The header\n// gives way to the body as the widget shrinks: the subtitle goes first, then the title.\n//\n// What the card LOOKS like is the board's to decide (the frame a definition sets on its canvas\n// and per widget, resolved by the renderer and read here through useWidgetFrameStyle): a card, a\n// borderless flat panel, or nothing at all; its border width, radius and colour; its padding;\n// its background; and whether the header shows. Every value is a token. A widget mounted outside\n// a board gets the card.\n\nimport { resolveFrame, sameLabel, type FrameRadius, type ResolvedFrame, type SpaceTokenName } from '@devicechain/dashboards';\nimport { useMemo, type CSSProperties, type ReactNode } from 'react';\n\nimport {\n useWidgetDensityScale,\n useWidgetFrameStyle,\n useWidgetFreshness,\n useWidgetSubject,\n WidgetBodySizeProvider,\n} from './context';\nimport { FreshnessReadout, StaleBadge, useFreshness } from './freshness-readout';\nimport { useElementSize } from './hooks';\nimport { css } from './theme';\nimport { makeWidgetScale, type WidgetScale } from './scale';\nimport { fontVar, spaceVar, type SpaceToken } from './tokens';\n\n// Below either of these (px, content box) the age readout is dropped so a small widget keeps\n// its room for the value; a stale widget still shows its badge, in the corner.\nconst FRESHNESS_MIN_WIDTH = 150;\nconst FRESHNESS_MIN_HEIGHT = 110;\n\n// Frame heights (px at density 1, multiplied by the density scale) below which the header gives\n// way. The subtitle is the first thing to go; the title holds until there is hardly room for a\n// value at all. A dropped title stays available to assistive tech as the frame's aria-label.\nexport const FRAME_SUBTITLE_MIN_HEIGHT = 120;\nexport const FRAME_TITLE_MIN_HEIGHT = 76;\n\n// The content column's size is quantised to this many px before it reaches a widget.\nconst BODY_SIZE_STEP = 4;\n\n// The card a widget gets when no board says otherwise.\nconst DEFAULT_FRAME: ResolvedFrame = resolveFrame(undefined, undefined);\n\nconst RADIUS: Record<FrameRadius, string> = {\n none: '0px',\n sm: 'calc(var(--radius, 8px) / 2)',\n md: 'var(--radius, 8px)',\n lg: 'calc(var(--radius, 8px) * 1.5)',\n};\n\nfunction spaceTokenVar(token: SpaceTokenName): string {\n return token === 'space-0' ? '0px' : spaceVar(Number(token.slice('space-'.length)) as SpaceToken);\n}\n\nexport interface WidgetFrameProps {\n title?: string;\n // An explicit subject line; when omitted the frame falls back to the resolved-entity\n // subject from WidgetSubjectContext.\n subtitle?: string;\n // The widget's content. A function child receives the frame's measured scale (size class,\n // content-column size, token px), for a widget whose own render needs it: a component\n // rendered INSIDE the frame can call useWidgetScale() instead.\n children: ReactNode | ((scale: WidgetScale) => ReactNode);\n // Extra style for the body region.\n bodyStyle?: CSSProperties;\n // Remove the content column's padding, for a widget that fills its box edge to edge (a chart\n // canvas, a map, an image). The column is still the measured, queryable box.\n flush?: boolean;\n // The occurredTime of the ONE sample this widget is showing, for a widget that shows a single\n // value (null while it has none). When set, the age readout ages exactly that sample; left\n // undefined, it ages the newest sample the host holds for the widget. A single-value widget\n // must pass it: on a device reporting several measurements, the newest is often not the one\n // on screen, and the age of another measurement would make a frozen value look live.\n sampleTime?: string | null;\n}\n\n// The epoch-ms time of a sample timestamp, or undefined when it has none or is unparseable.\nfunction parseSampleTime(time: string | null): number | undefined {\n if (!time) return undefined;\n const at = Date.parse(time);\n return Number.isNaN(at) ? undefined : at;\n}\n\nexport function WidgetFrame({ title, subtitle, children, bodyStyle, flush, sampleTime }: WidgetFrameProps) {\n const subject = useWidgetSubject();\n // A subtitle that only repeats the title says nothing (a card titled by its device's\n // external id, under a subject line naming the same device).\n const rawSub = subtitle ?? subject;\n const dupSub = rawSub && sameLabel(rawSub, title) ? undefined : rawSub;\n\n // Present only for a widget whose descriptor opts in. Stale (a known threshold, passed) greys\n // the value out; with no threshold the age is shown and nothing is ever greyed.\n const freshness = useWidgetFreshness();\n const at = freshness === undefined ? undefined : sampleTime === undefined ? freshness.at : parseSampleTime(sampleTime);\n const reading = useFreshness(at, freshness?.staleAfterSeconds);\n const [rootRef, size] = useElementSize<HTMLDivElement>();\n const [columnRef, columnSize] = useElementSize<HTMLDivElement>(BODY_SIZE_STEP);\n const scale = useWidgetDensityScale();\n const frame = useWidgetFrameStyle() ?? DEFAULT_FRAME;\n // Unmeasured (first frame, jsdom) shows everything; the cq path needs no measure to be right.\n // `show` keeps the header whatever the size, `hide` drops it whatever the size.\n const measured = size.height > 0;\n const header = frame.header;\n const showSub = header === 'show' || (header === 'auto' && (!measured || size.height >= FRAME_SUBTITLE_MIN_HEIGHT * scale));\n const showTitle = header === 'show' || (header === 'auto' && (!measured || size.height >= FRAME_TITLE_MIN_HEIGHT * scale));\n const rule = frame.border.width > 0 ? `${frame.border.width}px solid ${css(frame.border.color)}` : 'none';\n const sub = showSub ? dupSub : undefined;\n const shownTitle = showTitle ? title : undefined;\n const widgetScale = useMemo(() => makeWidgetScale(columnSize, scale), [columnSize, scale]);\n const compact = size.width > 0 && (size.width < FRESHNESS_MIN_WIDTH || size.height < FRESHNESS_MIN_HEIGHT);\n const stale = reading?.stale === true;\n\n return (\n <div\n ref={rootRef}\n data-stale={stale ? 'true' : undefined}\n role={title && !showTitle ? 'group' : undefined}\n aria-label={title && !showTitle ? title : undefined}\n style={{\n position: 'relative',\n display: 'flex',\n flexDirection: 'column',\n width: '100%',\n height: '100%',\n boxSizing: 'border-box',\n background: frame.background === 'transparent' ? 'transparent' : css(frame.background),\n color: css('card-foreground'),\n border: rule,\n borderRadius: RADIUS[frame.border.radius],\n overflow: 'hidden',\n }}\n >\n {shownTitle || sub ? (\n <div\n style={{\n padding: `${spaceVar(2)} ${spaceVar(3)}`,\n borderBottom: rule,\n }}\n >\n {shownTitle ? (\n <div\n title={shownTitle}\n style={{\n fontSize: fontVar('sm'),\n fontWeight: 600,\n color: css('muted-foreground'),\n whiteSpace: 'nowrap',\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n }}\n >\n {shownTitle}\n </div>\n ) : null}\n {sub ? (\n <div\n title={sub}\n style={{\n fontSize: fontVar('xs'),\n fontWeight: 400,\n color: css('muted-foreground'),\n opacity: 0.75,\n marginTop: shownTitle ? 2 : 0,\n whiteSpace: 'nowrap',\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n }}\n >\n {sub}\n </div>\n ) : null}\n </div>\n ) : null}\n {/* The body clips: the card root already stops a widget painting outside the card, and\n this stops one that outgrows its slot painting up over the title bar. minWidth 0\n lets it shrink inside the flex column. */}\n <div\n data-widget-body=\"\"\n style={{\n position: 'relative',\n flex: 1,\n minHeight: 0,\n minWidth: 0,\n overflow: 'hidden',\n ...bodyStyle,\n ...(stale ? { opacity: 0.45, filter: 'grayscale(1)' } : null),\n }}\n >\n {/* The content column: absolutely filling the body so its size never depends on its\n children (a size container must not), padded by the frame so every widget gets the\n same inset, and the box both the cq units and the measured size describe. */}\n <div\n ref={columnRef}\n data-widget-content=\"\"\n style={{\n position: 'absolute',\n inset: 0,\n boxSizing: 'border-box',\n containerType: 'size',\n overflow: 'hidden',\n padding: flush ? 0 : spaceTokenVar(frame.padding),\n }}\n >\n <WidgetBodySizeProvider size={columnSize}>\n {typeof children === 'function' ? children(widgetScale) : children}\n </WidgetBodySizeProvider>\n </div>\n </div>\n {reading && !compact ? (\n <div\n data-widget-freshness=\"\"\n style={{ padding: `${spaceVar(1)} ${spaceVar(3)}`, borderTop: rule, flex: '0 0 auto' }}\n >\n <FreshnessReadout reading={reading} />\n </div>\n ) : null}\n {reading && compact && stale ? (\n <div style={{ position: 'absolute', top: spaceVar(1), right: spaceVar(1) }}>\n <StaleBadge />\n </div>\n ) : null}\n </div>\n );\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// The age readout: \"12 sec. ago\", ticking once a second, plus a \"stale\" badge when -- and only\n// when -- a threshold is known and passed. The frame draws it for any widget whose descriptor\n// opts in (`freshness: true`); a custom widget that wants it somewhere else can render\n// <Freshness> itself.\n\nimport { useEffect, useState, type CSSProperties } from 'react';\n\nimport { ageSeconds, formatAge, freshnessStrings, isStale } from './freshness';\nimport { css } from './theme';\nimport { fontVar } from './tokens';\n\n// One interval per period, shared by every subscriber: a board of two hundred widgets ticks\n// once a second, not two hundred times, and every readout on it moves together. The timer\n// exists only while something is subscribed.\ninterface Ticker {\n timer: ReturnType<typeof setInterval>;\n listeners: Set<(now: number) => void>;\n}\nconst tickers = new Map<number, Ticker>();\n\nfunction subscribeTick(everyMs: number, listener: (now: number) => void): () => void {\n let ticker = tickers.get(everyMs);\n if (!ticker) {\n const listeners = new Set<(now: number) => void>();\n const timer = setInterval(() => {\n const now = Date.now();\n for (const l of [...listeners]) l(now);\n }, everyMs);\n ticker = { timer, listeners };\n tickers.set(everyMs, ticker);\n }\n ticker.listeners.add(listener);\n const owned = ticker;\n return () => {\n owned.listeners.delete(listener);\n if (owned.listeners.size === 0 && tickers.get(everyMs) === owned) {\n clearInterval(owned.timer);\n tickers.delete(everyMs);\n }\n };\n}\n\n// useNow re-renders its caller every `everyMs` with the current time, off the shared ticker\n// for that period. One second is the finest unit the readout shows.\nexport function useNow(everyMs = 1000, enabled = true): number {\n const [now, setNow] = useState(() => Date.now());\n useEffect(() => {\n if (!enabled) return;\n setNow(Date.now());\n return subscribeTick(everyMs, setNow);\n }, [everyMs, enabled]);\n return now;\n}\n\nexport interface FreshnessReading {\n age: number;\n stale: boolean;\n}\n\n// useFreshness: the age of `at` (epoch ms) now, and whether it is stale against the threshold.\n// Undefined while there is no sample to age.\n//\n// \uD83D\uDD34 A sample time AHEAD of the viewer's clock (a device clock running fast) is aged from when\n// this viewer first saw it, not from the time it claims. Clamping the age at zero alone would\n// read \"now\" for as long as the device is ahead -- an hour-fast clock would keep a frozen\n// value looking live for an hour, which is exactly what the readout exists to prevent.\nexport function useFreshness(at: number | undefined, staleAfterSeconds: number | undefined): FreshnessReading | undefined {\n // No sample, no clock: a frame that does not opt in must not tick once a second.\n const now = useNow(1000, at !== undefined);\n // When each distinct sample time was first seen here; adjusted during render (React's\n // \"storing information from previous renders\" pattern) so the first paint is already right.\n const [seen, setSeen] = useState<{ at: number; seenAt: number } | undefined>(undefined);\n let current = seen;\n if (at !== undefined && seen?.at !== at) {\n current = { at, seenAt: Math.max(now, Date.now()) };\n setSeen(current);\n }\n if (at === undefined || current === undefined) return undefined;\n const age = ageSeconds(Math.min(at, current.seenAt), now);\n return { age, stale: isStale(age, staleAfterSeconds) };\n}\n\nexport function StaleBadge({ label }: { label?: string }) {\n return (\n <span\n data-stale-badge=\"\"\n style={{\n fontSize: fontVar('xs'),\n fontWeight: 600,\n textTransform: 'uppercase',\n letterSpacing: 0.4,\n padding: '0 5px',\n borderRadius: 4,\n border: `1px dashed ${css('muted-foreground')}`,\n color: css('foreground'),\n background: css('muted'),\n whiteSpace: 'nowrap',\n }}\n >\n {label ?? freshnessStrings().stale}\n </span>\n );\n}\n\nexport interface FreshnessProps {\n // The newest sample's time, epoch ms. Nothing renders without one.\n at: number | undefined;\n // The age past which the value is stale. Omit when unknown: the age is shown and nothing is\n // styled stale.\n staleAfterSeconds?: number;\n // Override the badge word (a host in a language the SDK does not ship).\n staleLabel?: string;\n style?: CSSProperties;\n}\n\nexport function Freshness({ at, staleAfterSeconds, staleLabel, style }: FreshnessProps) {\n const reading = useFreshness(at, staleAfterSeconds);\n if (!reading) return null;\n return <FreshnessReadout reading={reading} staleLabel={staleLabel} style={style} />;\n}\n\nexport function FreshnessReadout({\n reading,\n staleLabel,\n style,\n}: {\n reading: FreshnessReading;\n staleLabel?: string;\n style?: CSSProperties;\n}) {\n return (\n <span\n data-freshness=\"\"\n data-stale={reading.stale ? 'true' : undefined}\n title={freshnessStrings().ageLabel}\n style={{\n display: 'inline-flex',\n alignItems: 'center',\n gap: 6,\n fontSize: fontVar('xs'),\n color: css('muted-foreground'),\n fontVariantNumeric: 'tabular-nums',\n ...style,\n }}\n >\n <time>{formatAge(reading.age)}</time>\n {reading.stale ? <StaleBadge label={staleLabel} /> : null}\n </span>\n );\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Small helpers for reading a widget's opaque `options` bag and picking the measurement a\n// single-value widget should show. They are the SDK's, not the built-ins': a third-party\n// widget reads its own options the same defensive way.\n\nimport type { MeasurementSample, WidgetInstance } from '@devicechain/dashboards';\n\n// pickSample chooses the sample a single-value widget (card, gauge) shows: the\n// named measurement, or the first available when no name is pinned.\nexport function pickSample(\n latest: Record<string, MeasurementSample>,\n name: string | undefined,\n): MeasurementSample | undefined {\n return name ? latest[name] : Object.values(latest)[0];\n}\n\n// --- options helpers ---------------------------------------------------------\n// A widget's `options` is opaque JSON (Record<string, unknown>) owned by the\n// widget; these read a key defensively, returning undefined on the wrong type\n// rather than throwing on a hand-edited definition.\n\ntype Options = Record<string, unknown> | undefined;\n\nexport function optString(options: Options, key: string): string | undefined {\n const value = options?.[key];\n return typeof value === 'string' ? value : undefined;\n}\n\nexport function optNumber(options: Options, key: string): number | undefined {\n const value = options?.[key];\n return typeof value === 'number' && Number.isFinite(value) ? value : undefined;\n}\n\n// A boolean flag option: true only when explicitly stored true (absent/other \u2192 false), so\n// an opt-in feature stays off by default and a hand-edited value can't accidentally enable it.\nexport function optBoolean(options: Options, key: string): boolean {\n return options?.[key] === true;\n}\n\n// primaryMeasurementName is the measurement a single-value widget (card, gauge)\n// displays: an explicit `options.measurement`, else the first measurement its\n// datasource selects, else undefined (fall back to whatever arrives first).\nexport function primaryMeasurementName(widget: WidgetInstance): string | undefined {\n const explicit = optString(widget.options, 'measurement');\n if (explicit) return explicit;\n const ds = widget.datasource;\n if (ds && ds.measurements.length > 0) return ds.measurements[0];\n return undefined;\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// How old a widget's newest sample is, whether that is \"stale\", and how to say so. Pure, so\n// the precedence and the wording are testable without a DOM or a clock.\n\nimport type { MeasurementSample } from '@devicechain/dashboards';\n\nimport { optNumber } from './widget-options';\n\n// resolveStaleAfterSeconds is the age, in seconds, past which a widget's value is called stale,\n// or undefined when no honest threshold is known. Precedence:\n//\n// 1. the widget's own `staleAfterSeconds` option;\n// 2. (no source today) the device profile's expected reporting interval x 3 -- a profile\n// carries no measurement reporting interval at present, only a location-fix interval that\n// is documented as descriptive, so nothing is read here and nothing is invented;\n// 3. otherwise undefined: show the age, never style anything stale.\n//\n// \uD83D\uDD34 There is deliberately NO default. A fleet that reports hourly would be painted stale all\n// day by any number chosen here, and \"stale\" painted on healthy data teaches viewers to ignore\n// the badge. A non-positive value is not a threshold and counts as none.\nexport function resolveStaleAfterSeconds(options: Record<string, unknown> | undefined): number | undefined {\n const configured = optNumber(options, 'staleAfterSeconds');\n return configured !== undefined && configured > 0 ? configured : undefined;\n}\n\n// newestSampleTime is the latest occurredTime (epoch ms) among the named measurements' latest\n// samples, or undefined when none carries a parseable time. `names` empty or omitted means\n// every measurement the widget holds.\nexport function newestSampleTime(\n latest: Record<string, MeasurementSample>,\n names?: readonly string[],\n): number | undefined {\n let newest: number | undefined;\n for (const [name, sample] of Object.entries(latest)) {\n if (names && names.length > 0 && !names.includes(name)) continue;\n if (!sample.occurredTime) continue;\n const at = Date.parse(sample.occurredTime);\n if (Number.isNaN(at)) continue;\n if (newest === undefined || at > newest) newest = at;\n }\n return newest;\n}\n\n// ageSeconds is whole seconds since `at`, never negative: a device clock slightly ahead of the\n// viewer's would otherwise read \"in 3 seconds\".\nexport function ageSeconds(at: number, now: number): number {\n return Math.max(0, Math.floor((now - at) / 1000));\n}\n\n// isStale is true only when a threshold is known and the age has passed it.\nexport function isStale(age: number, staleAfter: number | undefined): boolean {\n return staleAfter !== undefined && age > staleAfter;\n}\n\n// The viewer's language, as the host app set it on <html lang>, else the browser's.\nexport function viewerLocale(): string | undefined {\n if (typeof document !== 'undefined' && document.documentElement.lang) return document.documentElement.lang;\n if (typeof navigator !== 'undefined' && navigator.language) return navigator.language;\n return undefined;\n}\n\n// One formatter per locale tag: every readout on a board re-formats once a second, and\n// building an Intl formatter is far dearer than using one.\nconst formatters = new Map<string, Intl.RelativeTimeFormat>();\n\nfunction relativeTimeFormat(locale: string | undefined): Intl.RelativeTimeFormat {\n const key = locale ?? '';\n let formatter = formatters.get(key);\n if (!formatter) {\n try {\n formatter = new Intl.RelativeTimeFormat(locale, { numeric: 'auto', style: 'short' });\n } catch {\n // An unparseable tag in <html lang> must not take a widget down: the runtime's default.\n formatter = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto', style: 'short' });\n }\n formatters.set(key, formatter);\n }\n return formatter;\n}\n\n// formatAge renders an age as \"12 sec. ago\" / \"hace 12 s\" in the viewer's locale, stepping up\n// to minutes, hours and days. Under a second it reads \"now\".\nexport function formatAge(age: number, locale: string | undefined = viewerLocale()): string {\n const formatter = relativeTimeFormat(locale);\n if (age < 60) return formatter.format(-age, 'second');\n if (age < 3600) return formatter.format(-Math.floor(age / 60), 'minute');\n if (age < 86400) return formatter.format(-Math.floor(age / 3600), 'hour');\n return formatter.format(-Math.floor(age / 86400), 'day');\n}\n\n// The SDK carries no i18n runtime, so the one word it adds to a board ships here, per\n// language, falling back to English. A host that wants another language passes `staleLabel`.\nexport interface FreshnessStrings {\n stale: string;\n ageLabel: string;\n}\n\nexport const FRESHNESS_STRINGS: Record<'en' | 'es', FreshnessStrings> = {\n en: { stale: 'stale', ageLabel: 'Last sample' },\n es: { stale: 'obsoleto', ageLabel: '\u00DAltima muestra' },\n};\n\nexport function freshnessStrings(locale: string | undefined = viewerLocale()): FreshnessStrings {\n const base = locale?.toLowerCase().split('-')[0];\n return base === 'es' ? FRESHNESS_STRINGS.es : FRESHNESS_STRINGS.en;\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Theming without Tailwind (widgets are embeddable \u2014 no Tailwind leakage).\n//\n// Widgets style themselves with inline styles that reference the SAME CSS custom\n// properties the console (and any host) defines \u2014 `--card`, `--foreground`,\n// `--border`, `--chart-1..5`, etc. A host that sets those variables themes the\n// widgets for free (light/dark included); a host without Tailwind still works.\n//\n// DOM elements can use `hsl(var(--card))` directly. ECharts renders to a <canvas>,\n// which cannot resolve CSS variables \u2014 so for charts we read the variables off the\n// live computed style and hand ECharts concrete color strings (resolveChartTheme).\n\nimport type { ThemeColorToken } from '@devicechain/dashboards';\n\n// css returns an `hsl(var(--name))` reference for use in an inline style. The host\n// defines the variable as HSL channels (e.g. `--card: 0 0% 100%`), matching the\n// console's token set.\nexport function css(name: ThemeVar): string {\n return `hsl(var(--${name}))`;\n}\n\n// The subset of the console's design tokens widgets rely on. Kept small and\n// semantic so the contract a host must provide is explicit and stable.\n//\n// Colours only: sizes are SizeTokens (see ./tokens). The chart ramp and the severity hues are\n// part of the set, so a widget colours itself from a token rather than a literal; `normal` has\n// NO severity token (it renders in the ordinary foreground, see ./severity).\n//\n// The list itself lives with the dashboard format (THEME_COLOR_TOKENS), because a definition\n// names these too, for a frame's border and background: one list, so a colour a widget can use\n// is exactly a colour a board may name.\nexport type ThemeVar = ThemeColorToken;\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Size tokens: the type ramp and spacing steps a widget draws from instead of writing px\n// literals, and the density scale they all multiply by.\n//\n// --dc-scale the board's density scale (0.9 compact / 1 comfortable / 1.5 wall)\n// --dc-font-<xs|sm|md|lg|xl|display> calc(<base px> * var(--dc-scale))\n// --dc-space-<1..4> calc(<base px> * var(--dc-scale))\n//\n// The `dc-` prefix is load-bearing: these are set on a board that lives inside someone else's\n// page, and the bare names collide -- Tailwind v4 keeps font FAMILIES in `--font-*` (a host's\n// `--font-display: \"Inter\"` is its documented example), so `var(--font-display, 28px)` in such\n// a host resolves to a family name and the font size is thrown away.\n//\n// The renderer sets them on its root (tokenStyle), so every widget under it scales with the\n// board's density; `fontVar('md')` is `var(--dc-font-md, 13px)`, which still renders the base\n// size anywhere the tokens are not set (a host that mounts a bare WidgetFrame). Canvas widgets\n// cannot read a CSS variable and take the numeric equivalent from useWidgetScale().px().\n//\n// Colour tokens are the ThemeVar set in ./theme; the severity hues are generated from the\n// table in ./severity (WidgetTokenStyles below injects them), so no colour is typed twice.\n\nimport { DENSITY_SCALE, SPACE_TOKEN_PX, type CanvasDensity } from '@devicechain/dashboards';\nimport type { CSSProperties } from 'react';\n\nimport { severityTokenCss } from './severity';\n\nexport const FONT_TOKENS = {\n xs: 11,\n sm: 12,\n md: 13,\n lg: 16,\n xl: 20,\n display: 28,\n} as const;\nexport type FontToken = keyof typeof FONT_TOKENS;\n\n// The spacing steps a widget draws from, read from the dashboard format's table (which also has\n// `space-0`, for a seamless board's gutter and a frameless widget's padding).\nexport const SPACE_TOKENS = {\n 1: SPACE_TOKEN_PX['space-1'],\n 2: SPACE_TOKEN_PX['space-2'],\n 3: SPACE_TOKEN_PX['space-3'],\n 4: SPACE_TOKEN_PX['space-4'],\n} as const;\nexport type SpaceToken = keyof typeof SPACE_TOKENS;\n\n// 'font-md' | 'space-3' -- the names useWidgetScale().px() accepts.\nexport type SizeToken = `font-${FontToken}` | `space-${SpaceToken}`;\n\nexport function densityScale(density: CanvasDensity | undefined): number {\n return DENSITY_SCALE[density ?? 'comfortable'];\n}\n\nexport function fontVar(token: FontToken): string {\n return `var(--dc-font-${token}, ${FONT_TOKENS[token]}px)`;\n}\n\nexport function spaceVar(token: SpaceToken): string {\n return `var(--dc-space-${token}, ${SPACE_TOKENS[token]}px)`;\n}\n\n// The numeric px of a size token at a scale, for a canvas that cannot read the variable.\nexport function tokenPx(token: SizeToken, scale = 1): number {\n const [kind, name] = token.split('-') as ['font' | 'space', string];\n const base = kind === 'font' ? FONT_TOKENS[name as FontToken] : SPACE_TOKENS[Number(name) as SpaceToken];\n return Math.round(base * scale * 100) / 100;\n}\n\n// The custom properties a board root sets for a density scale.\nexport function tokenStyle(scale: number): CSSProperties {\n const vars: Record<string, string> = { '--dc-scale': String(scale) };\n for (const [name, px] of Object.entries(FONT_TOKENS)) vars[`--dc-font-${name}`] = `calc(${px}px * var(--dc-scale))`;\n for (const [name, px] of Object.entries(SPACE_TOKENS)) vars[`--dc-space-${name}`] = `calc(${px}px * var(--dc-scale))`;\n return vars as CSSProperties;\n}\n\n// The severity hues as a stylesheet at ZERO specificity (`:where`), so a host that defines\n// `--sev-<level>` itself (any ordinary `:root` / `.dark` rule) always wins and a host that\n// does not still gets the colours. They sit on the document root, where useSeverityColors\n// reads them -- nothing is scoped to a board element, so no WidgetThemeRoot registration is\n// needed for them to resolve.\nexport function WidgetTokenStyles() {\n return <style data-dc-tokens=\"\">{severityTokenCss({ hostWins: true })}</style>;\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// React bindings between the imperative DashboardHub / DOM theme and widget state.\n\nimport type {\n AlarmSubscription,\n CommandSubscription,\n DashboardDefinition,\n DatasourceSelector,\n DeviceResolver,\n EntityCandidateLister,\n HubConnectionState,\n LocationSubscription,\n MemberResolver,\n MeasurementSample,\n SelectionCandidate,\n SlotBinding,\n WidgetDataSource,\n} from '@devicechain/dashboards';\nimport type {\n AggregateStreamState,\n AlarmStreamState,\n CommandStreamState,\n LocationStreamState,\n MeasurementStreamState,\n} from '@devicechain/dashboards';\nimport {\n bindingsWithoutScopedSlots,\n DashboardHub,\n hasScopedSlots,\n resolveContextBindings,\n resolveSlotCandidates,\n} from '@devicechain/dashboards';\nimport { useEffect, useMemo, useRef, useState, useSyncExternalStore, type RefObject } from 'react';\n\nimport { useWidgetCandidates, type WidgetCandidates } from './context';\n\n// The channel snapshots moved to @devicechain/dashboards (they are pure data over the hub's\n// row types); re-exported so every import site here keeps resolving.\nexport type { AggregateStreamState, AlarmStreamState, CommandStreamState, LocationStreamState, MeasurementStreamState };\n\n// useDashboardHub gives a host ONE live hub per (resolver, authorities) \u2014 compared by\n// value, so a re-rendered but equal authorities array keeps the hub \u2014 and disposes it when\n// it is replaced or the host unmounts.\n//\n// It deliberately takes no bindings. The hub holds none: the renderer resolves every slot\n// through the settled bindings (resolveWidgetDatasource) before a widget subscribes, so a\n// selection re-subscribes only the widgets bound to the re-pointed slot. A hub rebuilt per\n// selection would tear down every subscription on the board and re-fetch every history.\nexport function useDashboardHub(resolver: DeviceResolver, authorities: string[] | undefined): DashboardHub {\n const authoritiesKey = JSON.stringify(authorities ?? []);\n const hub = useMemo(\n () => new DashboardHub({ resolver, authorities }),\n // authorities read via authoritiesKey (value identity), not reference.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [resolver, authoritiesKey],\n );\n useEffect(() => () => hub.disposeAll(), [hub]);\n return hub;\n}\n\n// useResolvedBindings runs the scoped-slot cascade (ADR-039 selection amendment) for a\n// host: it takes the synchronous base manifest (effectiveBindings) plus the accumulated\n// selection overlay and returns the settled slot\u2192binding map the host feeds to the\n// renderer. Selection state lives in the host; this hook just derives bindings from it.\n//\n// The common scope-FREE dashboard needs no async work \u2014 the synchronous overlay (base\n// with the selection laid over it) is already correct, and the hook returns it directly,\n// preserving today's behavior. Only a dashboard with scoped slots runs the async pass,\n// guarded by a monotonic generation so a slow members response can't overwrite a newer\n// selection; it keeps the last settled map until the new one arrives (one re-key per\n// walk, never a stale partial). A resolution error is fail-safe: keep the last map.\nexport function useResolvedBindings(\n definition: DashboardDefinition,\n base: Record<string, SlotBinding>,\n selection: Record<string, SlotBinding>,\n resolver: MemberResolver,\n): Record<string, SlotBinding> {\n const scoped = useMemo(() => hasScopedSlots(definition), [definition]);\n // Selection wins over base. Correct as-is for root + manual slots and every scope-free\n // dashboard; scoped `first` slots are refined by the async pass below.\n const overlaid = useMemo(() => ({ ...base, ...selection }), [base, selection]);\n // Seed the settled map with scoped slots STRIPPED, so a scoped dashboard's first paint\n // is unbound-not-stale (a scoped slot's default may be out of context) until the cascade\n // derives it. For a scope-free dashboard this equals `overlaid`.\n const [resolved, setResolved] = useState(() => bindingsWithoutScopedSlots(definition, overlaid));\n const genRef = useRef(0);\n\n useEffect(() => {\n // Scope-free: `overlaid` is authoritative. Keep `resolved` synced to it anyway so that\n // if the definition later gains a scope (an authoring edit), the flip to the `resolved`\n // path starts from the CURRENT bindings, not a stale mount-time snapshot.\n if (!scoped) {\n setResolved((prev) => (sameBindings(prev, overlaid) ? prev : overlaid));\n return;\n }\n const gen = ++genRef.current;\n let live = true;\n resolveContextBindings(definition, base, selection, resolver)\n .then((next) => {\n if (!live || gen !== genRef.current) return;\n // Only swap when the value actually changed, so an equal re-resolve doesn't churn\n // a new object reference (and re-render every widget) for nothing.\n setResolved((prev) => (sameBindings(prev, next) ? prev : next));\n })\n .catch(() => {\n /* fail-safe: keep the last settled map */\n });\n return () => {\n live = false;\n };\n }, [scoped, definition, base, selection, resolver, overlaid]);\n\n return scoped ? resolved : overlaid;\n}\n\n// sameBindings is a value compare of two slot\u2192binding maps (stable-key JSON), so the\n// hook can suppress a no-op re-key.\nfunction sameBindings(a: Record<string, SlotBinding>, b: Record<string, SlotBinding>): boolean {\n const ak = Object.keys(a).sort();\n const bk = Object.keys(b).sort();\n if (ak.length !== bk.length) return false;\n for (let i = 0; i < ak.length; i += 1) {\n if (ak[i] !== bk[i]) return false;\n if (JSON.stringify(a[ak[i]]) !== JSON.stringify(b[bk[i]])) return false;\n }\n return true;\n}\n\n// useSlotCandidates builds the ambient candidate provider a host passes to the renderer\n// (ADR-039 selection amendment): a function slot\u2192candidates that a context/entity-selector\n// widget calls. It closes over the CURRENT resolved bindings, so it is rebuilt whenever a\n// binding changes \u2014 a selector's option set (a scoped child's members) then follows the\n// parent for free, and a widget re-fetches because the provider identity changed. Returns\n// undefined when no lister is supplied (a host that wires no selection), so the selector\n// stays inert. Keyed by the bindings' VALUE so an equal-but-new bindings object doesn't\n// churn the provider (and every open selector's fetch) each render.\nexport function useSlotCandidates(\n definition: DashboardDefinition,\n bindings: Record<string, SlotBinding>,\n resolver: MemberResolver,\n lister: EntityCandidateLister | undefined,\n): WidgetCandidates | undefined {\n const bindingsKey = JSON.stringify(bindings);\n return useMemo(\n () => {\n if (!lister) return undefined;\n return (slot: string) => resolveSlotCandidates(definition, slot, bindings, resolver, lister);\n },\n // bindings read via bindingsKey (value identity), not reference.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [definition, bindingsKey, resolver, lister],\n );\n}\n\n// The state a selector widget renders from: the target slot's current candidate options,\n// plus load/error flags. `wired` reports whether an ambient candidate provider exists \u2014 a\n// selector feature-detects on it (with the select callback) to stay inert where selection\n// isn't wired (edit/preview, a host that supplies no lister) rather than showing a dead picker.\nexport interface CandidatesState {\n candidates: SelectionCandidate[];\n loading: boolean;\n error: unknown | null;\n wired: boolean;\n}\n\n// useCandidates resolves the options a selector widget offers for `slot` through the ambient\n// candidate provider. Re-fetches when the provider (i.e. the resolved bindings) or the slot\n// changes, guarded by a monotonic generation so a slow list can't overwrite a newer one\n// (the parent-switch-then-open race). Fail-safe: an error yields an empty option set.\nexport function useCandidates(slot: string | undefined): CandidatesState {\n const provider = useWidgetCandidates();\n const wired = !!provider;\n // Seed loading true when a fetch is imminent (provider + slot present) so the first paint\n // reads \"Loading\u2026\", not a spurious \"No options\" for the frame before the effect runs.\n const [state, setState] = useState<Omit<CandidatesState, 'wired'>>(() => ({\n candidates: [],\n loading: Boolean(provider && slot),\n error: null,\n }));\n const genRef = useRef(0);\n\n useEffect(() => {\n if (!provider || !slot) {\n setState({ candidates: [], loading: false, error: null });\n return;\n }\n const gen = ++genRef.current;\n let live = true;\n // Clear any prior error when a fresh (healthy) fetch starts, so a retry doesn't show\n // \"Loading\u2026\" and a stale error banner together.\n setState((prev) => ({ ...prev, loading: true, error: null }));\n provider(slot)\n .then((candidates) => {\n if (live && gen === genRef.current) setState({ candidates, loading: false, error: null });\n })\n .catch((error: unknown) => {\n if (live && gen === genRef.current) setState({ candidates: [], loading: false, error });\n });\n return () => {\n live = false;\n };\n }, [provider, slot]);\n\n return { ...state, wired };\n}\n\nexport interface MeasurementStreamOptions {\n // Max samples retained in the rolling window (oldest dropped past this). With `windowSeconds`\n // this is the memory ceiling on a span (default MAX_SPAN_SAMPLES); without it, the whole\n // window (default 300).\n window?: number;\n // The window as a SPAN: only samples that occurred within this many seconds of now (or of\n // `pausedAt`) are kept, whatever their number, and `window` is demoted to a ceiling so a\n // busy device cannot grow the buffer without bound. Omit for the count-only window.\n windowSeconds?: number;\n // Freeze the stream at this instant (epoch ms). While set, live samples are BUFFERED, not merged:\n // the returned state does not change, its newest sample keeps its time (so an age readout\n // keeps counting), and the span is measured back from this instant. Clearing it merges\n // everything that arrived in the meantime at once -- the catch-up.\n pausedAt?: number;\n // The clock the span is measured against (epoch ms). Default: the wall clock. A data source that\n // plays a recorded run supplies its own, because the run's samples are dated by the run, not by\n // today, and a span measured from today would drop every one of them.\n now?: () => number;\n // Historical samples to seed the window with before live data arrives (from a\n // bucketedMeasurements backfill). Assumed chronological and older than the live\n // tail; live samples for the same measurement name override the seed's latest.\n initialSamples?: MeasurementSample[];\n // When `initialSamples` was read up to (epoch ms, wall clock). After the live feed is interrupted,\n // only a seed read at or after the moment it resumed covers the outage: such a seed REPLACES the\n // live samples from before the interruption, and until one arrives the window reports the outage\n // as a gap (MeasurementStreamState.gaps) rather than joining the readings either side. Omitted:\n // no seed is ever taken as covering an outage, so an interruption stays a gap.\n initialSamplesAsOf?: number;\n // Called when the live feed resumes after an interruption, after the gap is recorded: the host\n // reads the widget's history again and hands it back as `initialSamples` + `initialSamplesAsOf`.\n onReconnect?: () => void;\n}\n\n// The ceiling on retained samples when the window is a span. A span of days at a high event\n// rate would otherwise hold every raw reading; past this the OLDEST go first.\nexport const MAX_SPAN_SAMPLES = 2000;\n\nconst EMPTY: MeasurementStreamState = { latest: {}, samples: [], error: null };\n\n// Build a name\u2192last-sample map from a chronological list (last occurrence wins).\nfunction latestOf(samples: MeasurementSample[]): Record<string, MeasurementSample> {\n const latest: Record<string, MeasurementSample> = {};\n for (const s of samples) latest[s.name] = s;\n return latest;\n}\n\n// evictOldest keeps the newest `max` samples, recording in `evicted` the time of the newest one it\n// drops (the hook's mark for \"the window no longer reaches back to its start\").\nfunction evictOldest(samples: MeasurementSample[], max: number, evicted: { current: number }): MeasurementSample[] {\n if (samples.length <= max) return samples;\n const cut = samples.length - max;\n for (let i = 0; i < cut; i++) {\n const at = Date.parse(samples[i].occurredTime ?? '');\n if (at > evicted.current) evicted.current = at;\n }\n return samples.slice(cut);\n}\n\n// An interruption of the live feed: `after` is the time (epoch ms) of the newest sample known when\n// the feed resumed, `at` the wall-clock instant it resumed. Nothing is known between `after` and the\n// next sample until a seed read at or after `at` arrives.\ninterface Gap {\n after: number;\n at: number;\n}\n\ninterface LiveBuffer {\n key: string | null;\n state: MeasurementStreamState;\n gaps: Gap[];\n}\n\nfunction timeOf(sample: MeasurementSample): number {\n return sample.occurredTime ? Date.parse(sample.occurredTime) : Number.NaN;\n}\n\n// The newest sample time among the given samples, or -Infinity when none carries one.\nfunction newestTime(samples: Iterable<MeasurementSample>): number {\n let newest = -Infinity;\n for (const s of samples) {\n const at = timeOf(s);\n if (at > newest) newest = at;\n }\n return newest;\n}\n\n// settleGaps folds into the live buffer every gap a seed answers -- one read at or after the\n// moment the feed resumed. The seed is then the history up to and across the outage, so the live\n// samples it covers go: those from before the interruption, and any older than the seed's newest\n// (they would draw backwards). A measurement's latest value from before the interruption gives way\n// to the seed's when the seed has one. A sample with no readable time is kept: it cannot be shown\n// to be covered. Pure: the render applies it at once, and an effect commits it to the buffer.\nfunction settleGaps(buffer: LiveBuffer, seed: MeasurementSample[] | undefined, asOf: number | undefined): LiveBuffer {\n const { gaps, state } = buffer;\n if (!seed || asOf === undefined) return buffer;\n // Gaps are recorded oldest first, so the ones this seed answers are a prefix.\n const open = gaps.filter((g) => asOf < g.at);\n if (open.length === gaps.length) return buffer;\n const cutoff = Math.max(newestTime(seed), gaps[gaps.length - open.length - 1].after);\n const kept = (s: MeasurementSample): boolean => !(timeOf(s) <= cutoff);\n const latest: Record<string, MeasurementSample> = {};\n for (const [name, s] of Object.entries(state.latest)) {\n if (kept(s) || !seed.some((x) => x.name === name)) latest[name] = s;\n }\n return { key: buffer.key, state: { ...state, latest, samples: state.samples.filter(kept) }, gaps: open };\n}\n\n// withinSpan drops the samples that occurred before `end - spanSeconds`. A sample whose time is\n// missing or unparseable is KEPT: it cannot be shown to be outside the window, and dropping it\n// would hide a reading the count ceiling would have kept.\nexport function withinSpan(samples: MeasurementSample[], spanSeconds: number, end: number): MeasurementSample[] {\n const from = end - spanSeconds * 1000;\n return samples.filter((s) => {\n if (!s.occurredTime) return true;\n const at = Date.parse(s.occurredTime);\n return Number.isNaN(at) || at >= from;\n });\n}\n\n// useMeasurementStream subscribes a widget's datasource through the hub and keeps\n// the latest value per measurement plus a bounded rolling window. It re-subscribes\n// when the datasource changes (compared by value, so a re-rendered-but-equal\n// selector doesn't churn the subscription) and tears down on unmount. When\n// initialSamples is given, it is merged ahead of the live tail so a chart shows\n// history immediately (and can arrive after the live stream opens).\nexport function useMeasurementStream(\n hub: WidgetDataSource,\n datasource: DatasourceSelector | undefined,\n options: MeasurementStreamOptions = {},\n): MeasurementStreamState {\n const { windowSeconds, pausedAt } = options;\n const windowSize = options.window ?? (windowSeconds !== undefined ? MAX_SPAN_SAMPLES : 300);\n const initialSamples = options.initialSamples;\n const seedAsOf = options.initialSamplesAsOf;\n const paused = pausedAt !== undefined;\n\n // Value-compare the selector so an unchanged-but-new object reference doesn't\n // resubscribe every render.\n const key = datasource ? JSON.stringify(datasource) : null;\n\n // The span and the pause are read by the subscription's callback through refs, so changing the\n // range (or pausing) never tears the subscription down and loses what it has buffered.\n const spanRef = useRef(windowSeconds);\n const clockRef = useRef(options.now ?? Date.now);\n const pausedRef = useRef(paused);\n const queueRef = useRef<MeasurementSample[]>([]);\n // The newest QUEUED sample per measurement name. The queue is capped, so a long pause at a high\n // rate evicts its oldest entries -- and with them, possibly, the only sample of a measurement\n // that reports rarely. Without this, resuming would leave that measurement's latest value at\n // its pre-pause reading. Bounded by the number of measurement names, not by time.\n const queueLatestRef = useRef<Record<string, MeasurementSample>>({});\n // The time (epoch ms) of the newest sample the retention ceiling has evicted, live or queued.\n // While it is still inside the span, the window no longer covers the whole span and says so\n // (`truncatedBefore`), rather than drawing a short window under a long span's label.\n const evictedRef = useRef(-Infinity);\n const flushRef = useRef<() => void>(() => {});\n const clock = options.now ?? Date.now;\n // Read by the subscription's callbacks: the seed on hand when the feed resumes (part of what was\n // known before the gap), and the host's re-read.\n const seedRef = useRef(initialSamples);\n const onReconnectRef = useRef(options.onReconnect);\n useEffect(() => {\n spanRef.current = windowSeconds;\n clockRef.current = clock;\n seedRef.current = initialSamples;\n onReconnectRef.current = options.onReconnect;\n });\n\n // The live buffer is TAGGED with the selector it was filled from. The effect below\n // resets it when the selector changes, but an effect runs after the render that\n // carried the new selector \u2014 so without the tag that render would show the previous\n // device's values under the new one. A buffer whose tag is not the current key is\n // treated as empty, synchronously.\n const [tagged, setTagged] = useState<LiveBuffer>({ key, state: EMPTY, gaps: [] });\n // The buffer as the current seed leaves it: a seed that covers an outage replaces what it covers\n // in this very render, and the effect below commits that so the buffer does not keep it.\n const settled = settleGaps(tagged, initialSamples, seedAsOf);\n const live = settled.key === key ? settled.state : EMPTY;\n const gaps = settled.key === key ? settled.gaps : [];\n useEffect(() => {\n setTagged((prev) => settleGaps(prev, initialSamples, seedAsOf));\n }, [initialSamples, seedAsOf]);\n\n useEffect(() => {\n setTagged({ key, state: EMPTY, gaps: [] }); // reset the live buffer whenever the datasource changes (or clears)\n queueRef.current = [];\n queueLatestRef.current = {};\n evictedRef.current = -Infinity;\n flushRef.current = () => {};\n if (!datasource) return;\n\n // A callback for a selector the buffer has already moved past (a sink that answers\n // after its subscription was released) leaves the buffer alone rather than re-tagging\n // it to the old selector, which would blank the current one's values.\n const update = (fn: (prev: MeasurementStreamState) => MeasurementStreamState) =>\n setTagged((prev) => (prev.key === key ? { ...prev, state: fn(prev.state) } : prev));\n\n const fold = (\n prev: MeasurementStreamState,\n incoming: MeasurementSample[],\n newest: Record<string, MeasurementSample> = latestOf(incoming),\n ): MeasurementStreamState => {\n let samples = prev.samples.concat(incoming);\n const span = spanRef.current;\n if (span !== undefined) samples = withinSpan(samples, span, clockRef.current());\n samples = evictOldest(samples, windowSize, evictedRef);\n return { latest: { ...prev.latest, ...newest }, samples, error: null };\n };\n\n flushRef.current = () => {\n const queued = queueRef.current;\n if (queued.length === 0) return;\n const newest = queueLatestRef.current;\n queueRef.current = [];\n queueLatestRef.current = {};\n update((prev) => fold(prev, queued, newest));\n };\n\n return hub.subscribeWidget(datasource, {\n next: (sample) => {\n if (pausedRef.current) {\n // Buffered, bounded: a pause that outlasts the ceiling keeps the newest.\n queueRef.current = evictOldest([...queueRef.current, sample], windowSize, evictedRef);\n queueLatestRef.current[sample.name] = sample;\n return;\n }\n update((prev) => fold(prev, [sample]));\n },\n error: (err) => update((prev) => ({ ...prev, error: err })),\n // The feed resumed: what was sent meanwhile never arrived. Record where knowledge stops --\n // the newest sample known, live, queued or seeded -- so the window shows a gap there, then\n // ask the host for the history across it. Nothing known yet: no gap to show, but the\n // history is still read again.\n reconnected: () => {\n const at = Date.now();\n setTagged((prev) => {\n if (prev.key !== key) return prev;\n // `latest` holds the newest sample of every name the window holds.\n const after = newestTime([...Object.values(prev.state.latest), ...Object.values(queueLatestRef.current), ...(seedRef.current ?? [])]);\n if (after === -Infinity) return prev;\n // The newest eight only, so a display left open for months with a flaky link does not\n // grow the list with every reconnect. An older gap lies before most of the window anyway.\n return { ...prev, gaps: [...prev.gaps.slice(-7), { after, at }] };\n });\n onReconnectRef.current?.();\n },\n });\n // `datasource` is intentionally read via `key` (value identity), not reference.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [hub, key, windowSize]);\n\n // The pause gate. Resuming merges what queued while frozen. The flag flips before the flush,\n // so a sample landing between the two is merged rather than queued behind it.\n useEffect(() => {\n pausedRef.current = paused;\n if (!paused) flushRef.current();\n }, [paused]);\n\n // Layer the history seed under the live tail. Kept separate from the live buffer\n // so late-arriving history (async backfill) recomputes without touching the\n // subscription, and switching datasource clears live while the new seed applies.\n const state = useMemo(() => {\n // The span is measured back from the freeze instant while paused, from now otherwise.\n const end = pausedAt ?? clock();\n if (windowSeconds === undefined) {\n if (!initialSamples || initialSamples.length === 0) return live;\n const merged = initialSamples.concat(live.samples);\n return {\n latest: { ...latestOf(initialSamples), ...live.latest },\n samples: merged.length > windowSize ? merged.slice(merged.length - windowSize) : merged,\n error: live.error,\n };\n }\n // A span. Everything kept is inside it, so anything the ceiling cuts here -- or cut earlier\n // from the live buffer or the pause queue, while still inside it -- is a hole at the start of\n // the window. The ceiling drops the OLDEST, so what is kept is one contiguous run; it is\n // marked with where that run begins, so a chart can say it covers less than its span.\n const from = end - windowSeconds * 1000;\n const merged = withinSpan(initialSamples ? initialSamples.concat(live.samples) : live.samples, windowSeconds, end);\n const samples = merged.length > windowSize ? merged.slice(merged.length - windowSize) : merged;\n const cut = samples.length < merged.length || evictedRef.current >= from;\n const truncatedBefore = cut ? (samples[0]?.occurredTime ?? undefined) : undefined;\n const latest = initialSamples ? { ...latestOf(initialSamples), ...live.latest } : live.latest;\n if (!truncatedBefore && latest === live.latest && samples.length === live.samples.length) return live;\n const state: MeasurementStreamState = { latest, samples, error: live.error };\n if (truncatedBefore) state.truncatedBefore = truncatedBefore;\n return state;\n }, [initialSamples, live, windowSize, windowSeconds, pausedAt, clock]);\n\n // The gaps still open, as instants. One before the window's first sample breaks nothing.\n return gaps.length ? { ...state, gaps: gaps.map((g) => new Date(g.after).toISOString()) } : state;\n}\n\nconst EMPTY_ALARMS: AlarmStreamState = { alarms: [], total: 0, loading: true, error: null };\n\n// useAlarmStream binds an alarm widget's scope+filters through the hub's alarm channel\n// and keeps the latest snapshot. It re-subscribes when the subscription changes\n// (value-compared, so a re-rendered-but-equal object doesn't churn) and tears down on\n// unmount. The hub delivers whole snapshots (query-then-reconcile), so this holds the\n// last one rather than accumulating \u2014 the query is the source of truth.\nexport function useAlarmStream(\n hub: WidgetDataSource,\n subscription: AlarmSubscription,\n): AlarmStreamState {\n // Value-compare the subscription so an unchanged-but-new object reference doesn't\n // resubscribe every render.\n const key = JSON.stringify(subscription);\n // The snapshot is TAGGED with the subscription it came from, as the measurement buffer is: the\n // effect that resets it runs after the render carrying the new subscription, and that render\n // must not show the previous scope's rows under it (nor hand them to a pause hold to keep).\n const [[at, state], setState] = useState<[string, AlarmStreamState]>([key, EMPTY_ALARMS]);\n\n useEffect(() => {\n setState([key, EMPTY_ALARMS]); // reset to loading whenever the subscription changes\n return hub.subscribeAlarms(subscription, {\n next: (snapshot) =>\n setState([key, { alarms: snapshot.alarms, total: snapshot.total, loading: false, error: null }]),\n error: (err) => setState(([, prev]) => [key, { ...prev, loading: false, error: err }]),\n });\n // `subscription` is intentionally read via `key` (value identity), not reference.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [hub, key]);\n\n return at === key ? state : EMPTY_ALARMS;\n}\n\nconst EMPTY_COMMANDS: CommandStreamState = {\n deviceToken: null,\n commands: [],\n total: 0,\n loading: true,\n error: null,\n};\n\n// useCommandStream binds a command widget's scope through the hub's control channel and\n// keeps the latest command-history snapshot. It re-subscribes when the subscription\n// changes (value-compared) and tears down on unmount. The hub delivers whole snapshots\n// (poll-then-emit), so this holds the last one rather than accumulating.\nexport function useCommandStream(\n hub: WidgetDataSource,\n subscription: CommandSubscription,\n): CommandStreamState {\n const [state, setState] = useState<CommandStreamState>(EMPTY_COMMANDS);\n\n // Value-compare the subscription so an unchanged-but-new object reference doesn't\n // resubscribe every render.\n const key = JSON.stringify(subscription);\n\n useEffect(() => {\n setState(EMPTY_COMMANDS); // reset to loading whenever the subscription changes\n return hub.subscribeCommands(subscription, {\n next: (snapshot) =>\n setState({\n deviceToken: snapshot.deviceToken,\n commands: snapshot.commands,\n total: snapshot.total,\n loading: false,\n error: null,\n }),\n error: (err) => setState((prev) => ({ ...prev, loading: false, error: err })),\n });\n // `subscription` is intentionally read via `key` (value identity), not reference.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [hub, key]);\n\n return state;\n}\n\nconst EMPTY_LOCATIONS: LocationStreamState = {\n locations: [],\n deviceTokens: [],\n forbidden: false,\n loading: true,\n error: null,\n};\n\n// useLocationStream binds a map widget's selector through the hub's location channel and\n// keeps the latest position snapshot. It re-subscribes when the subscription changes\n// (value-compared, so a re-rendered-but-equal object doesn't churn) and tears down on\n// unmount. The hub delivers whole snapshots (poll-then-emit), so this holds the last one\n// rather than accumulating.\n//\n// A `forbidden` snapshot CLEARS the positions as it sets the flag: an authority can be\n// revoked mid-session, and keeping the last-seen coordinates on screen under a\n// permission banner would leave a refused viewer looking at the very data they were\n// just refused.\nexport function useLocationStream(\n hub: WidgetDataSource,\n subscription: LocationSubscription,\n): LocationStreamState {\n // Value-compare the subscription so an unchanged-but-new object reference doesn't\n // resubscribe every render.\n const key = JSON.stringify(subscription);\n // Tagged with its subscription, as useAlarmStream's snapshot is: no render shows the previous\n // selector's markers under a new one.\n const [[at, state], setState] = useState<[string, LocationStreamState]>([key, EMPTY_LOCATIONS]);\n\n useEffect(() => {\n setState([key, EMPTY_LOCATIONS]); // reset to loading whenever the subscription changes\n return hub.subscribeLocations(subscription, {\n next: (snapshot) =>\n setState([\n key,\n snapshot.kind === 'forbidden'\n ? { locations: [], deviceTokens: [], forbidden: true, loading: false, error: null }\n : {\n locations: snapshot.locations,\n deviceTokens: snapshot.deviceTokens,\n forbidden: false,\n loading: false,\n error: null,\n },\n ]),\n error: (err) => setState(([, prev]) => [key, { ...prev, loading: false, error: err }]),\n });\n // `subscription` is intentionally read via `key` (value identity), not reference.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [hub, key]);\n\n return at === key ? state : EMPTY_LOCATIONS;\n}\n\n// A widget's bound-entity availability: 'unknown' until the async check resolves\n// (render optimistically meanwhile), then 'available', or 'unavailable' when the bound\n// device no longer exists (a deleted device's stable token, ADR-044).\nexport type DatasourceAvailability = 'unknown' | 'available' | 'unavailable';\n\n// While a widget shows \"unavailable\", re-check on this cadence so a device deleted then\n// recreated with the same token (ADR-042 frees tokens on delete) recovers on a long-lived\n// viewer without a reload. Only runs while unavailable \u2014 a live device is checked once.\nconst AVAILABILITY_REVALIDATE_MS = 60_000;\n\n// useDatasourceAvailability resolves whether a widget's bound device still exists,\n// WITHOUT blocking the widget's data stream (optimistic + async): it starts 'unknown'\n// (the widget renders normally), then flips to 'available'/'unavailable' once the hub's\n// existence check resolves. Fails open (\u2192'available') so a check outage never falsely\n// blanks a live widget. A widget with no datasource is trivially available (no query).\n// Re-checks when the datasource changes (value-compared), and periodically while\n// unavailable so a recreated device recovers.\nexport function useDatasourceAvailability(\n hub: WidgetDataSource,\n datasource: DatasourceSelector | undefined,\n): DatasourceAvailability {\n const [state, setState] = useState<DatasourceAvailability>('unknown');\n const key = datasource ? JSON.stringify(datasource) : null;\n\n useEffect(() => {\n // No datasource (label/image, or tenant-wide) has nothing to validate \u2014 available\n // immediately, no query, no re-render churn.\n if (!datasource) {\n setState('available');\n return;\n }\n setState('unknown');\n let cancelled = false;\n let timer: ReturnType<typeof setInterval> | undefined;\n\n const stopTimer = (): void => {\n if (timer) {\n clearInterval(timer);\n timer = undefined;\n }\n };\n\n const check = (): void => {\n hub\n .isDatasourceAvailable(datasource)\n .then((ok) => {\n if (cancelled) return;\n setState(ok ? 'available' : 'unavailable');\n if (ok) stopTimer(); // recovered (or was fine) \u2014 stop polling\n })\n .catch(() => {\n if (cancelled) return;\n setState('available'); // fail open\n stopTimer();\n });\n };\n\n check();\n // Poll only matters once unavailable; harmlessly runs until the first check clears it\n // for a live device (cleared in the resolver above, well before the first tick).\n timer = setInterval(check, AVAILABILITY_REVALIDATE_MS);\n\n return () => {\n cancelled = true;\n stopTimer();\n };\n // `datasource` is intentionally read via `key` (value identity), not reference.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [hub, key]);\n\n return state;\n}\n\nexport interface ElementSize {\n width: number;\n height: number;\n}\n\n// useElementSize tracks an element's content-box size via ResizeObserver, so a\n// widget can scale its contents (chart fonts, gauge ticks, a big number) to the\n// slot it was resized to \u2014 ECharts' resize() re-lays-out the canvas but never\n// scales absolute font sizes, so a small widget crams without this. Returns\n// {0,0} until measured (and where ResizeObserver is unavailable, e.g. jsdom); the\n// consumers treat a zero size as \"use default sizing\".\n//\n// `step` quantises each dimension DOWN to a multiple of it (default 1: exact), so a caller that\n// only needs a coarse size -- the frame feeding every widget's scale -- does not re-render for\n// sub-pixel or few-pixel jitter. Down, so text fitted to the quantised box always fits the\n// real one.\nexport function useElementSize<T extends HTMLElement>(step = 1): [RefObject<T | null>, ElementSize] {\n const ref = useRef<T>(null);\n const [size, setSize] = useState<ElementSize>({ width: 0, height: 0 });\n\n useEffect(() => {\n const el = ref.current;\n if (!el || typeof ResizeObserver === 'undefined') return;\n const observer = new ResizeObserver((entries) => {\n const rect = entries[0]?.contentRect;\n if (!rect) return;\n const width = step > 1 ? Math.floor(rect.width / step) * step : rect.width;\n const height = step > 1 ? Math.floor(rect.height / step) * step : rect.height;\n setSize((prev) => (prev.width === width && prev.height === height ? prev : { width, height }));\n });\n observer.observe(el);\n return () => observer.disconnect();\n }, [step]);\n\n return [ref, size];\n}\n\n// What useHubConnectionState needs of a hub: the live implementation (DashboardHub) has it, a\n// synthetic preview source does not, and a host holding only that reads `live`.\nexport interface ConnectionStateSource {\n readonly connectionState: HubConnectionState;\n onConnectionState(listener: () => void): () => void;\n}\n\nconst NO_CONNECTION_SOURCE: ConnectionStateSource = {\n connectionState: 'live',\n onConnectionState: () => () => {},\n};\n\n// useHubConnectionState follows a hub's socket state (live / reconnecting / offline) for a\n// header badge. It re-renders only when the state changes.\nexport function useHubConnectionState(hub: Partial<ConnectionStateSource> | undefined): HubConnectionState {\n // A source that reports no connection (a synthetic preview, a recording) is not \"down\":\n // there is no socket to lose, so it reads live.\n const source =\n hub && typeof hub.onConnectionState === 'function' ? (hub as ConnectionStateSource) : NO_CONNECTION_SOURCE;\n return useSyncExternalStore(\n (listener) => source.onConnectionState(listener),\n () => source.connectionState,\n );\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// useWidgetScale -- the numeric half of the size contract. HTML text sizes itself with `cq`\n// units off the frame's content column (fitValueFontCss); a canvas (an ECharts gauge) cannot\n// read those, so it asks this hook for the same box in numbers. Both read the one curve in\n// ./fit-text and the one box -- the frame's padded content column -- so they agree.\n\nimport { useEffect, useMemo, useReducer, useRef } from 'react';\n\nimport { useWidgetBodySize, useWidgetDensityScale, type WidgetBodySize } from './context';\nimport { stickyChars, type StickyChars } from './fit-text';\nimport { tokenPx, type SizeToken } from './tokens';\n\nexport type SizeClass = 'compact' | 'regular' | 'large';\n\n// The content-column sizes (px, at density 1; multiplied by the density scale) that separate the\n// classes. Compact: too small for a full variant (a value and a caption); large: room for\n// extras (a sparkline, a legend).\nexport const SIZE_CLASS_BREAKS = {\n compactWidth: 120,\n compactHeight: 72,\n largeWidth: 420,\n largeHeight: 220,\n} as const;\n\nexport function sizeClassOf(size: WidgetBodySize, scale = 1): SizeClass {\n // Unmeasured (first frame, jsdom) is `regular`: the default rendering, not a degraded one.\n if (!(size.width > 0) || !(size.height > 0)) return 'regular';\n const b = SIZE_CLASS_BREAKS;\n if (size.width < b.compactWidth * scale || size.height < b.compactHeight * scale) return 'compact';\n if (size.width >= b.largeWidth * scale && size.height >= b.largeHeight * scale) return 'large';\n return 'regular';\n}\n\nexport interface WidgetScale {\n sizeClass: SizeClass;\n // The board's density scale (1 outside a renderer).\n scale: number;\n // The frame's content column in px, quantised to 4.\n size: WidgetBodySize;\n // The px of a size token at this density, for a canvas.\n px: (token: SizeToken) => number;\n}\n\nexport function makeWidgetScale(size: WidgetBodySize, scale: number): WidgetScale {\n return { sizeClass: sizeClassOf(size, scale), scale, size, px: (token: SizeToken) => tokenPx(token, scale) };\n}\n\n// useWidgetScale reads the frame's content column, so it works in a component RENDERED INSIDE a\n// WidgetFrame -- not in the component that renders the frame (the frame's context does not\n// reach above it). A widget that needs the numbers for its own top-level render passes\n// WidgetFrame a function child instead: `<WidgetFrame>{(scale) => ...}</WidgetFrame>`.\nexport function useWidgetScale(): WidgetScale {\n const size = useWidgetBodySize();\n const scale = useWidgetDensityScale();\n return useMemo(() => makeWidgetScale(size, scale), [size, scale]);\n}\n\n// How long a readout keeps its longest recent length after it shrinks.\nconst STICKY_HOLD_MS = 60_000;\n\n// useStickyChars -- the character count to hand the font curve for a live readout: the longest\n// seen in the last minute, so 9.99 -> 10.01 does not resize the number. Holding the LONGER\n// length can only make the font smaller than the live value needs, never larger, so it cannot\n// make a readout overflow.\nexport function useStickyChars(chars: number, holdMs = STICKY_HOLD_MS): number {\n const held = useRef<StickyChars | undefined>(undefined);\n const [tick, wake] = useReducer((n: number) => n + 1, 0);\n // Written during render on purpose: the value is a pure function of (prev, chars, time) and\n // an extra render recomputes the same answer, so there is nothing to commit or clean up.\n held.current = stickyChars(held.current, chars, Date.now(), holdMs);\n const { value, since } = held.current;\n // A hold is released only by a render. A value that shrinks and then stays put (an alarm\n // count falling from 1000 to 9 and holding there) brings no further render, so without this\n // it would keep the long length's small font indefinitely, not for a minute: render again\n // when the hold runs out. `tick` re-arms the timer should it ever fire a moment early.\n useEffect(() => {\n if (value <= chars) return;\n const timer = setTimeout(wake, Math.max(0, since + holdMs - Date.now()));\n return () => clearTimeout(timer);\n }, [value, since, chars, holdMs, tick]);\n return value;\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// A small header badge for a hub's connection state. Presentational: the host supplies the\n// words (its own i18n), so the SDK adds no strings of its own here.\n\nimport type { HubConnectionState } from '@devicechain/dashboards';\n\nimport { css, type ThemeVar } from './theme';\nimport { fontVar } from './tokens';\n\nexport interface HubConnectionBadgeProps {\n state: HubConnectionState;\n labels: Record<HubConnectionState, string>;\n}\n\nconst DOT: Record<HubConnectionState, ThemeVar> = {\n live: 'primary',\n reconnecting: 'muted-foreground',\n offline: 'destructive',\n};\n\nexport function HubConnectionBadge({ state, labels }: HubConnectionBadgeProps) {\n return (\n <span\n role=\"status\"\n data-connection-state={state}\n style={{\n display: 'inline-flex',\n alignItems: 'center',\n gap: 6,\n fontSize: fontVar('sm'),\n padding: '2px 8px',\n borderRadius: 999,\n border: `1px solid ${css('border')}`,\n color: state === 'offline' ? css('destructive') : css('muted-foreground'),\n whiteSpace: 'nowrap',\n }}\n >\n <span\n aria-hidden\n style={{\n width: 8,\n height: 8,\n borderRadius: '50%',\n background: css(DOT[state]),\n opacity: state === 'reconnecting' ? 0.6 : 1,\n }}\n />\n {labels[state]}\n </span>\n );\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Directional change-flash \u2014 an opt-in cue (widget option `flashOnChange`) that briefly\n// tints a numeric value green when it rises and red when it falls, then fades back, so a\n// glance catches direction of change on a live board (cf. a trading \"last\" column). Purely\n// presentational: no hub/model change, driven off the value the widget already renders.\n//\n// Single-value widgets (latest-card) color the number; a multi-row widget (table,\n// alarm-table) renders each value through <FlashValue>, so React's per-key component\n// instances give every row its own independent flash with no map bookkeeping. The gauge\n// (an opaque ECharts canvas whose number can't be CSS-transitioned) tints its container\n// instead \u2014 see flashBackgroundStyle.\n\nimport { useEffect, useRef, useState, type CSSProperties } from 'react';\n\nimport { formatValue } from './format';\n\nexport type FlashDirection = 'up' | 'down' | null;\n\n// Semantic up/down colors, mid-saturation so they read on both light and dark host themes;\n// a host may override with the --flash-up / --flash-down custom properties. The background\n// variants are translucent so a tint sits over the widget's own surface without hiding it.\nconst UP = {\n fg: 'var(--flash-up, hsl(142 72% 40%))',\n bg: 'var(--flash-up-bg, hsl(142 72% 45% / 0.16))',\n};\nconst DOWN = {\n fg: 'var(--flash-down, hsl(0 72% 51%))',\n bg: 'var(--flash-down-bg, hsl(0 72% 55% / 0.16))',\n};\n\n// How long (ms) the solid tint holds before fading, and how long the fade back takes.\nconst HOLD_MS = 400;\nconst FADE_MS = 600;\n\n// useFlashOnChange watches a numeric value and returns the direction of its most recent\n// change ('up' | 'down') for a short window, then null. It stays null when the feature is\n// off, on the first value (no flash on mount), on a non-finite value, or when `identity`\n// changes \u2014 the last so a widget bound to an ANCHOR (whose one measurement slot interleaves\n// values from many member devices) adopts the new device's value instead of reading the\n// device-to-device jump as a real rise/fall. Pass the value's owning entity as `identity`\n// (e.g. `deviceToken:name`); leave it undefined for a single-device value.\n//\n// The held direction auto-clears after HOLD_MS; the consumer fades it out via flashTextStyle.\n// Every early-return path resets `direction` too: the effect only re-runs on a dep change,\n// and each such change has already cancelled the pending timer, so not clearing here would\n// strand a tint (e.g. value \u2192 null, or toggled off then on, mid-hold).\nexport function useFlashOnChange(\n value: number | null | undefined,\n enabled: boolean,\n identity?: string | number,\n): FlashDirection {\n const prev = useRef<number | null | undefined>(value);\n const prevIdentity = useRef(identity);\n const [direction, setDirection] = useState<FlashDirection>(null);\n\n useEffect(() => {\n const before = prev.current;\n const sameEntity = prevIdentity.current === identity;\n prev.current = value;\n prevIdentity.current = identity;\n if (\n !enabled ||\n !sameEntity ||\n typeof value !== 'number' ||\n typeof before !== 'number' ||\n !Number.isFinite(value) ||\n !Number.isFinite(before) ||\n value === before\n ) {\n setDirection(null); // a no-op when already null (React bails out); clears a stale tint otherwise\n return;\n }\n setDirection(value > before ? 'up' : 'down');\n const timer = setTimeout(() => setDirection(null), HOLD_MS);\n return () => clearTimeout(timer);\n }, [value, enabled, identity]);\n\n // Mask any lingering state when disabled, so toggling off is immediate.\n return enabled ? direction : null;\n}\n\n// flashTextStyle colors text for the current direction: snap to the tint (no transition)\n// while flashing, then fade `color` back to the element's own color when it clears. Merge\n// it AFTER an explicit base `color` so the tint overrides while set and the base shows\n// through (and animates) once cleared.\nexport function flashTextStyle(direction: FlashDirection, fadeMs: number = FADE_MS): CSSProperties {\n if (direction) return { color: direction === 'up' ? UP.fg : DOWN.fg, transition: 'none' };\n return { transition: `color ${fadeMs}ms ease-out` };\n}\n\n// flashBackgroundStyle is the same cue as a translucent background tint, for a widget whose\n// value can't be recolored directly (the gauge's canvas). Snap in, fade the background back.\nexport function flashBackgroundStyle(direction: FlashDirection, fadeMs: number = FADE_MS): CSSProperties {\n if (direction) {\n return { backgroundColor: direction === 'up' ? UP.bg : DOWN.bg, transition: 'none' };\n }\n return { transition: `background-color ${fadeMs}ms ease-out` };\n}\n\n// FlashValue renders a formatted numeric value that flashes on change \u2014 the reusable cell\n// for multi-row widgets. Each rendered instance owns its own flash state, so keying rows by\n// entity (React's list key) gives every row an independent cue for free.\nexport function FlashValue({\n value,\n precision,\n enabled,\n identity,\n baseColor = 'inherit',\n style,\n}: {\n value: number | null | undefined;\n precision?: number;\n enabled: boolean;\n // The value's owning entity (see useFlashOnChange): when it changes, adopt the new value\n // rather than flash. Distinguishes a real per-device change from an anchor's cross-device\n // interleave. Undefined for a single-device value.\n identity?: string | number;\n // The color the value fades back to (defaults to the inherited text color).\n baseColor?: string;\n style?: CSSProperties;\n}) {\n const direction = useFlashOnChange(value, enabled, identity);\n return (\n <span style={{ color: baseColor, ...style, ...flashTextStyle(direction) }}>\n {formatValue(value, precision)}\n </span>\n );\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// @devicechain/widget-sdk \u2014 the supported surface for writing a dashboard widget.\n//\n// A widget is declared once with `defineWidget` (a portable, JSON-only descriptor plus a\n// trusted React component) and made available to a host by listing it in\n// `createWidgetRegistry`. The built-in widgets are declared through exactly this\n// interface. The descriptor vocabulary is React-free and lives in @devicechain/dashboards;\n// it is re-exported here so a widget package imports from one place.\n\nexport { defineWidget } from './define-widget';\nexport type { ErasedComponent, ErasedWidgetDefinition, WidgetDefinition, WidgetImplementation } from './define-widget';\nexport { createWidgetRegistry, WidgetRegistryError, type WidgetRegistry } from './registry';\nexport type {\n EditorCommandChoice,\n EditorCommandChoices,\n WidgetComponent,\n WidgetEditorProps,\n WidgetEditorServices,\n WidgetProps,\n} from './widget';\n\n// The ambient host contexts (selection, candidates, time zone, theme root, subject). Defined\n// in this package and nowhere else \u2014 see ./context.\nexport {\n WidgetCandidatesProvider,\n WidgetDensityProvider,\n WidgetFrameStyleProvider,\n WidgetFreshnessProvider,\n WidgetSelectProvider,\n WidgetSubjectProvider,\n WidgetThemeRootProvider,\n WidgetTimeRangeProvider,\n WidgetTimeZoneProvider,\n useWidgetCandidates,\n useWidgetFrameStyle,\n useWidgetFreshness,\n useWidgetSelect,\n useWidgetSubject,\n useWidgetThemeRoot,\n useWidgetTimeRange,\n useWidgetTimeZone,\n type WidgetBodySize,\n type WidgetCandidates,\n type WidgetFreshness,\n type WidgetSelect,\n} from './context';\n\n// The building blocks every widget (built-in or third-party) composes from: the card frame,\n// theme-variable references, number/time formatting, the opt-in change-flash cue, option\n// readers, and the React bindings between the hub's channels and widget state. The chart\n// engine helpers are NOT here -- they are on the `/echarts` subpath, so importing this root\n// never pulls Apache ECharts (nor MapLibre, nor the built-in catalog).\nexport { WidgetFrame, type WidgetFrameProps } from './frame';\nexport { css, type ThemeVar } from './theme';\n// The curve's constants and the pure sticky-length step stay internal: a widget reaches them\n// through fitValueFont / fitValueFontCss / useStickyChars, so the curve can be retuned.\nexport { FIT_MIN_FONT_PX, fitValueFont, fitValueFontCss, type FitOptions } from './fit-text';\nexport {\n FONT_TOKENS,\n SPACE_TOKENS,\n WidgetTokenStyles,\n densityScale,\n fontVar,\n spaceVar,\n tokenPx,\n tokenStyle,\n type FontToken,\n type SizeToken,\n type SpaceToken,\n} from './tokens';\n// The body-size and density contexts are plumbing between the frame, the renderer and this\n// hook: a widget reads them through useWidgetScale, and only the renderer provides density.\nexport { SIZE_CLASS_BREAKS, useStickyChars, useWidgetScale, type SizeClass, type WidgetScale } from './scale';\nexport { HubConnectionBadge, type HubConnectionBadgeProps } from './connection-badge';\nexport { Freshness, FreshnessReadout, StaleBadge, useFreshness, useNow, type FreshnessProps, type FreshnessReading } from './freshness-readout';\nexport {\n ageSeconds,\n formatAge,\n freshnessStrings,\n isStale,\n newestSampleTime,\n resolveStaleAfterSeconds,\n viewerLocale,\n} from './freshness';\nexport { formatDateTime, formatTimestamp, formatValue } from './format';\nexport {\n ALARM_LEVELS,\n SEVERITY_LEVELS,\n SEVERITY_TOKENS,\n isAlarmLevel,\n isSeverityLevel,\n levelColor,\n severityTokenCss,\n severityVar,\n useSeverityColors,\n type AlarmLevel,\n type SeverityLevel,\n} from './severity';\nexport {\n bandAt,\n optThresholds,\n parseThresholds,\n thresholdBands,\n type Threshold,\n type ThresholdBand,\n} from './thresholds';\nexport {\n FlashValue,\n flashBackgroundStyle,\n flashTextStyle,\n useFlashOnChange,\n type FlashDirection,\n} from './flash';\nexport { optBoolean, optNumber, optString, pickSample, primaryMeasurementName } from './widget-options';\nexport {\n useAlarmStream,\n useCandidates,\n useCommandStream,\n useDashboardHub,\n useDatasourceAvailability,\n useElementSize,\n useHubConnectionState,\n useLocationStream,\n useMeasurementStream,\n MAX_SPAN_SAMPLES,\n withinSpan,\n useResolvedBindings,\n useSlotCandidates,\n type AggregateStreamState,\n type AlarmStreamState,\n type CandidatesState,\n type CommandStreamState,\n type ConnectionStateSource,\n type DatasourceAvailability,\n type ElementSize,\n type LocationStreamState,\n type MeasurementStreamOptions,\n type MeasurementStreamState,\n} from './hooks';\n\n// The portable descriptor vocabulary.\nexport {\n SDK_API_VERSION,\n WidgetCatalogError,\n defineWidgetDescriptor,\n isBuiltinWidgetId,\n isNamespacedWidgetId,\n validateWidgetDescriptor,\n type AckOptionRef,\n type BooleanOptionSpec,\n type BuiltinWidgetId,\n type ChannelBindingPolicy,\n type ChannelSnapshot,\n type ConstBinding,\n type DatasourcePolicy,\n type DatasourceSelectorKind,\n type DefaultsOf,\n type DescriptorIssue,\n type EnumOptionSpec,\n type ListItemFieldSpec,\n type ListOptionSpec,\n type GreaterThanConstraint,\n type HubConnectionState,\n type NamespacedWidgetId,\n type NumberOptionRef,\n type NumberOptionSpec,\n type OptionConstraint,\n type OptionSpec,\n type OptionsOf,\n type StringOptionRef,\n type StringOptionSpec,\n type WidgetCatalog,\n type WidgetChannel,\n type WidgetDescriptor,\n type WidgetDisplay,\n type WidgetId,\n type WidgetInteractionDeclaration,\n type WidgetOptionSpecs,\n} from '@devicechain/dashboards';\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["useMemo", "jsx", "useEffect", "useState", "useEffect", "useState", "merged", "state", "useEffect", "useMemo", "useRef", "useMemo", "useRef", "useEffect", "jsx", "jsxs", "useMemo", "jsx", "jsxs", "useEffect", "useRef", "useState", "jsx", "useRef", "useState", "useEffect", "WidgetCatalogError"]
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devicechain/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0-rc.1",
|
|
4
4
|
"description": "The DeviceChain widget SDK: the typed contract for writing a dashboard widget as a React component, and the sealed registry that makes it available to a host. Declare a widget once (portable descriptor plus trusted component) and register it beside the built-ins.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"devicechain",
|
|
@@ -51,12 +51,12 @@
|
|
|
51
51
|
"test": "vitest run"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
|
-
"@devicechain/dashboards": "0.
|
|
54
|
+
"@devicechain/dashboards": "0.23.0-rc.1",
|
|
55
55
|
"echarts": "^6.0.0",
|
|
56
56
|
"react": "^19.3.0"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
|
-
"@devicechain/dashboards": "0.
|
|
59
|
+
"@devicechain/dashboards": "0.23.0-rc.1",
|
|
60
60
|
"@testing-library/react": "^16.3.3",
|
|
61
61
|
"@types/node": "^26.6.5",
|
|
62
62
|
"@types/react": "^19.3.0",
|