@devicechain/widget-sdk 0.21.1-rc.1 → 0.22.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,7 +14,8 @@ npm install @devicechain/widget-sdk @devicechain/dashboards react
14
14
  ```
15
15
 
16
16
  `@devicechain/dashboards` and `react` are **peer dependencies**: the SDK defines the React
17
- contexts a widget reads (selection, candidate lookup, time zone, theme root), and a context is
17
+ contexts a widget reads (selection, candidate lookup, time zone, theme root, time range, density,
18
+ freshness), and a context is
18
19
  identified by the object that created it, so every widget package and the host must resolve
19
20
  the same single copy of this package. Declare it as a peer in your own widget package too,
20
21
  never as a dependency.
@@ -27,6 +28,12 @@ references (`css`), formatters, the change-flash cue, option readers (`optString
27
28
  that only draws DOM imports from here and pulls in neither Apache ECharts nor MapLibre nor
28
29
  the built-in widgets.
29
30
 
31
+ A stream hook never returns one subscription's snapshot for another. Since 0.22,
32
+ `useAlarmStream` and `useLocationStream` return `loading: true` with no rows from the very
33
+ render whose subscription changed (a re-pointed slot, a new filter), until the new
34
+ subscription answers. Before 0.22 that one render still carried the previous subscription's
35
+ alarms or positions.
36
+
30
37
  Chart helpers live on a separate entry, `@devicechain/widget-sdk/echarts`: the `EChart`
31
38
  wrapper, `buildLineOption` / `buildGaugeOption` and the chart colour theme. `echarts` is an
32
39
  **optional peer dependency**, needed only by a widget that imports that entry.
@@ -82,3 +89,32 @@ for the built-in widgets.
82
89
 
83
90
  Trusted widget code runs with the host page's privileges. The declarations in a descriptor
84
91
  shape the API; they are not a security boundary.
92
+
93
+ ## Upgrading from 0.21
94
+
95
+ 0.22 removes no export. A TypeScript build can notice three changes:
96
+
97
+ - `ChartTheme` has a required `severity` field. This matters only if you build a theme by hand
98
+ instead of calling `useChartTheme` or `resolveChartTheme`.
99
+ - The unions `WidgetChannel` (now with `'aggregate'`), `WidgetId`, `BuiltinWidgetId`, `ThemeVar`
100
+ and `OptionSpec` (now with `ListOptionSpec`) gained members, so an exhaustive `switch` over one
101
+ of them needs a new case.
102
+ - `formatValue` with no precision rounds to 3 decimals, or 4 significant digits below 1, where it
103
+ used to print the number unrounded.
104
+
105
+ Additions, which need no change:
106
+
107
+ - `WidgetFrame` takes `flush`, which removes the content padding for a widget that fills its box
108
+ edge to edge, and `sampleTime`, the time of the one sample a single-value widget shows, so its
109
+ age readout ages that sample rather than the newest one the host holds.
110
+ - `useElementSize` takes an optional `step` that rounds each dimension down to a multiple of it,
111
+ so a caller that needs only a coarse size does not re-render for every pixel.
112
+ - New exports for size and density tokens (`fontVar`, `spaceVar`, `tokenStyle`,
113
+ `useWidgetScale`, `fitValueFont`), sample age (`useFreshness`, `Freshness`, `StaleBadge`,
114
+ `formatAge`), alarm severity (`SEVERITY_LEVELS`, `severityVar`, `useSeverityColors`),
115
+ thresholds (`parseThresholds`, `thresholdBands`, `bandAt`), the board's time range
116
+ (`useWidgetTimeRange`) and the hub connection (`useHubConnectionState`, `HubConnectionBadge`).
117
+
118
+ `SDK_API_VERSION` is unchanged. See
119
+ [If you build against the npm packages](https://docs.devicechain.io/deployment/releases-and-upgrades#v0220-packages)
120
+ in the release's upgrade guide.
@@ -1,14 +1,40 @@
1
1
  import type { MeasurementSample } from '@devicechain/dashboards';
2
2
  import type { ElementSize } from '../hooks';
3
+ import { type AlarmLevel } from '../severity';
4
+ import { type Threshold } from '../thresholds';
3
5
  import type { ChartTheme } from './chart-theme';
4
6
  import type { EChartOption } from './echart';
7
+ export declare function severityFill(theme: ChartTheme, level: AlarmLevel, alpha: number): string;
5
8
  export interface LineChartOptions {
6
9
  measurements?: string[];
10
+ unit?: string;
11
+ precision?: number;
12
+ thresholds?: readonly Threshold[];
13
+ scale?: number;
7
14
  }
8
15
  export declare function buildLineOption(samples: MeasurementSample[], theme: ChartTheme, options?: LineChartOptions, size?: ElementSize): EChartOption;
9
16
  export interface GaugeChartOptions {
10
17
  min?: number;
11
18
  max?: number;
12
19
  unit?: string;
20
+ precision?: number;
21
+ thresholds?: readonly Threshold[];
22
+ scale?: number;
23
+ readout?: 'canvas' | 'none';
24
+ emptyNeedle?: 'min' | 'none';
13
25
  }
26
+ export declare function gaugeReadoutBox(size?: ElementSize): {
27
+ centerX: number;
28
+ centerY: number;
29
+ width: number;
30
+ height: number;
31
+ };
32
+ export declare const GAUGE_AXIS_LABELS_MIN_SIDE = 120;
14
33
  export declare function buildGaugeOption(value: number | null, theme: ChartTheme, options?: GaugeChartOptions, size?: ElementSize): EChartOption;
34
+ export declare function wrapHeading(value: number | null | undefined): number | undefined;
35
+ export interface CompassChartOptions {
36
+ cardinals: readonly [string, string, string, string];
37
+ thresholds?: readonly Threshold[];
38
+ scale?: number;
39
+ }
40
+ export declare function buildCompassOption(value: number | null, theme: ChartTheme, options: CompassChartOptions, size?: ElementSize): EChartOption;
@@ -1,8 +1,10 @@
1
+ import { type AlarmLevel } from '../severity';
1
2
  export interface ChartTheme {
2
3
  foreground: string;
3
4
  mutedForeground: string;
4
5
  border: string;
5
6
  series: string[];
7
+ severity: Record<AlarmLevel, string>;
6
8
  }
7
9
  export declare function resolveChartTheme(el: Element): ChartTheme;
8
10
  export declare function useChartTheme(): ChartTheme;
@@ -0,0 +1,2 @@
1
+ import { MarkLineComponent } from 'echarts/components';
2
+ export declare const MARKER_COMPONENTS: (typeof MarkLineComponent)[];
@@ -1,4 +1,6 @@
1
1
  import * as echarts from 'echarts/core';
2
+ export declare function loadThresholdMarkers(): Promise<void>;
3
+ export declare function optionUsesMarkers(option: EChartOption): boolean;
2
4
  export type EChartOption = echarts.EChartsCoreOption;
3
5
  export interface EChartProps {
4
6
  option: EChartOption;
@@ -0,0 +1,318 @@
1
+ // src/context.tsx
2
+ import { createContext, useContext } from "react";
3
+ import { jsx } from "react/jsx-runtime";
4
+ var WidgetSubjectContext = createContext(void 0);
5
+ function useWidgetSubject() {
6
+ return useContext(WidgetSubjectContext);
7
+ }
8
+ function WidgetSubjectProvider({
9
+ label,
10
+ children
11
+ }) {
12
+ return /* @__PURE__ */ jsx(WidgetSubjectContext.Provider, { value: label, children });
13
+ }
14
+ var WidgetTimeZoneContext = createContext(void 0);
15
+ function WidgetTimeZoneProvider({
16
+ timeZone,
17
+ children
18
+ }) {
19
+ return /* @__PURE__ */ jsx(WidgetTimeZoneContext.Provider, { value: timeZone, children });
20
+ }
21
+ function useWidgetTimeZone() {
22
+ return useContext(WidgetTimeZoneContext);
23
+ }
24
+ var WidgetThemeRootContext = createContext(void 0);
25
+ function WidgetThemeRootProvider({
26
+ root,
27
+ children
28
+ }) {
29
+ return /* @__PURE__ */ jsx(WidgetThemeRootContext.Provider, { value: root, children });
30
+ }
31
+ function useWidgetThemeRoot() {
32
+ return useContext(WidgetThemeRootContext);
33
+ }
34
+ var WidgetDensityContext = createContext(1);
35
+ function WidgetDensityProvider({ scale, children }) {
36
+ return /* @__PURE__ */ jsx(WidgetDensityContext.Provider, { value: scale, children });
37
+ }
38
+ function useWidgetDensityScale() {
39
+ return useContext(WidgetDensityContext);
40
+ }
41
+ var ZERO_SIZE = { width: 0, height: 0 };
42
+ var WidgetBodySizeContext = createContext(ZERO_SIZE);
43
+ function WidgetBodySizeProvider({ size, children }) {
44
+ return /* @__PURE__ */ jsx(WidgetBodySizeContext.Provider, { value: size, children });
45
+ }
46
+ function useWidgetBodySize() {
47
+ return useContext(WidgetBodySizeContext);
48
+ }
49
+ var WidgetSelectContext = createContext(void 0);
50
+ function WidgetSelectProvider({
51
+ select,
52
+ children
53
+ }) {
54
+ return /* @__PURE__ */ jsx(WidgetSelectContext.Provider, { value: select, children });
55
+ }
56
+ function useWidgetSelect() {
57
+ return useContext(WidgetSelectContext);
58
+ }
59
+ var WidgetCandidatesContext = createContext(void 0);
60
+ function WidgetCandidatesProvider({
61
+ candidates,
62
+ children
63
+ }) {
64
+ return /* @__PURE__ */ jsx(WidgetCandidatesContext.Provider, { value: candidates, children });
65
+ }
66
+ function useWidgetCandidates() {
67
+ return useContext(WidgetCandidatesContext);
68
+ }
69
+ var WidgetFreshnessContext = createContext(void 0);
70
+ function WidgetFreshnessProvider({
71
+ freshness,
72
+ children
73
+ }) {
74
+ return /* @__PURE__ */ jsx(WidgetFreshnessContext.Provider, { value: freshness, children });
75
+ }
76
+ function useWidgetFreshness() {
77
+ return useContext(WidgetFreshnessContext);
78
+ }
79
+ var WidgetFrameStyleContext = createContext(void 0);
80
+ function WidgetFrameStyleProvider({ frame, children }) {
81
+ return /* @__PURE__ */ jsx(WidgetFrameStyleContext.Provider, { value: frame, children });
82
+ }
83
+ function useWidgetFrameStyle() {
84
+ return useContext(WidgetFrameStyleContext);
85
+ }
86
+ var WidgetTimeRangeContext = createContext(void 0);
87
+ function WidgetTimeRangeProvider({
88
+ range,
89
+ children
90
+ }) {
91
+ return /* @__PURE__ */ jsx(WidgetTimeRangeContext.Provider, { value: range, children });
92
+ }
93
+ function useWidgetTimeRange() {
94
+ return useContext(WidgetTimeRangeContext);
95
+ }
96
+
97
+ // src/severity.ts
98
+ import { useMemo, useSyncExternalStore } from "react";
99
+ var SEVERITY_LEVELS = ["normal", "warning", "minor", "major", "critical"];
100
+ function isSeverityLevel(value) {
101
+ return typeof value === "string" && SEVERITY_LEVELS.includes(value);
102
+ }
103
+ var ALARM_LEVELS = ["warning", "minor", "major", "critical"];
104
+ function isAlarmLevel(level) {
105
+ return level !== void 0 && level !== "normal";
106
+ }
107
+ function levelColor(colors, level) {
108
+ return isAlarmLevel(level) ? colors[level] : void 0;
109
+ }
110
+ var SEVERITY_TOKENS = {
111
+ warning: { light: "199 89% 44%", dark: "199 89% 62%" },
112
+ minor: { light: "32 95% 40%", dark: "38 92% 56%" },
113
+ major: { light: "25 95% 48%", dark: "25 95% 62%" },
114
+ critical: { light: "0 72% 46%", dark: "0 84% 66%" }
115
+ };
116
+ function severityVar(level) {
117
+ return `--sev-${level}`;
118
+ }
119
+ function severityTokenCss(options = {}) {
120
+ const wrap = (selector) => options.hostWins ? `:where(${selector})` : selector;
121
+ const block = (selector, side) => [
122
+ `${wrap(selector)} {`,
123
+ ...ALARM_LEVELS.map((l) => ` ${severityVar(l)}: ${SEVERITY_TOKENS[l][side]};`),
124
+ "}"
125
+ ].join("\n");
126
+ return `${block(":root", "light")}
127
+ ${block(".dark", "dark")}
128
+ `;
129
+ }
130
+ function hslFromChannels(channels) {
131
+ const [h, s, l] = channels.replace(/,/g, " ").trim().split(/\s+/);
132
+ return `hsl(${h}, ${s}, ${l})`;
133
+ }
134
+ function severityColors(dark, host) {
135
+ const out = {};
136
+ for (const level of ALARM_LEVELS) {
137
+ out[level] = hslFromChannels(host(severityVar(level)) || SEVERITY_TOKENS[level][dark ? "dark" : "light"]);
138
+ }
139
+ return out;
140
+ }
141
+ function isDark(el) {
142
+ return el.closest(".dark") !== null || el.ownerDocument.documentElement.classList.contains("dark");
143
+ }
144
+ function severityFallbackColors() {
145
+ return severityColors(false, () => void 0);
146
+ }
147
+ function severityColorsAt(el, style = getComputedStyle(el)) {
148
+ return severityColors(isDark(el), (name) => style.getPropertyValue(name).trim() || void 0);
149
+ }
150
+ var shared = null;
151
+ var listeners = /* @__PURE__ */ new Set();
152
+ var observer = null;
153
+ function subscribe(listener) {
154
+ if (!observer) {
155
+ shared = severityColorsAt(document.documentElement);
156
+ observer = new MutationObserver(() => {
157
+ shared = severityColorsAt(document.documentElement);
158
+ for (const l of listeners) l();
159
+ });
160
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class", "style"] });
161
+ }
162
+ listeners.add(listener);
163
+ return () => {
164
+ listeners.delete(listener);
165
+ if (listeners.size === 0 && observer) {
166
+ observer.disconnect();
167
+ observer = null;
168
+ shared = null;
169
+ }
170
+ };
171
+ }
172
+ function snapshot() {
173
+ shared ?? (shared = severityColorsAt(document.documentElement));
174
+ return shared;
175
+ }
176
+ function useSeverityColors() {
177
+ const page = useSyncExternalStore(subscribe, snapshot, snapshot);
178
+ const root = useWidgetThemeRoot();
179
+ const scoped = useMemo(() => root ? severityColorsAt(root) : null, [root]);
180
+ return scoped ?? page;
181
+ }
182
+
183
+ // src/fit-text.ts
184
+ var FIT_MIN_FONT_PX = 12;
185
+ var FIT_FALLBACK_FONT_PX = 32;
186
+ var FIT_HEIGHT_SHARE = 0.6;
187
+ var FIT_GLYPH_WIDTH = 0.65;
188
+ function charCount(chars) {
189
+ return Math.max(1, Math.ceil(Number.isFinite(chars) ? chars : 1));
190
+ }
191
+ function fitValueFont(size, chars, options = {}) {
192
+ const floor = options.floor ?? FIT_MIN_FONT_PX;
193
+ if (!(size.width > 0) || !(size.height > 0)) return Math.max(floor, FIT_FALLBACK_FONT_PX);
194
+ const byHeight = size.height * FIT_HEIGHT_SHARE;
195
+ const byWidth = size.width / (charCount(chars) * FIT_GLYPH_WIDTH);
196
+ return Math.max(floor, Math.floor(Math.min(byHeight, byWidth)));
197
+ }
198
+ function fitValueFontCss(chars, options = {}) {
199
+ const floor = options.floor ?? FIT_MIN_FONT_PX;
200
+ const perChar = 100 / (charCount(chars) * FIT_GLYPH_WIDTH);
201
+ return `max(${floor}px, min(${FIT_HEIGHT_SHARE * 100}cqh, ${Number(perChar.toFixed(3))}cqw))`;
202
+ }
203
+ function stickyChars(prev, chars, now, holdMs) {
204
+ if (!prev || chars >= prev.value) return { value: chars, since: now };
205
+ if (now - prev.since >= holdMs) return { value: chars, since: now };
206
+ return prev;
207
+ }
208
+
209
+ // src/format.ts
210
+ function formatTimestamp(iso, timeZone) {
211
+ if (!iso) return "";
212
+ const date = new Date(iso);
213
+ if (Number.isNaN(date.getTime())) return iso;
214
+ return timeZone ? date.toLocaleTimeString(void 0, { timeZone, timeZoneName: "short" }) : date.toLocaleTimeString();
215
+ }
216
+ function formatDateTime(iso, timeZone) {
217
+ if (!iso) return "";
218
+ const date = new Date(iso);
219
+ if (Number.isNaN(date.getTime())) return iso;
220
+ return timeZone ? date.toLocaleString(void 0, { timeZone, timeZoneName: "short" }) : date.toLocaleString();
221
+ }
222
+ var DEFAULT_FRACTION_DIGITS = 3;
223
+ var DEFAULT_SMALL_SIGNIFICANT = 4;
224
+ function defaultFormat(value) {
225
+ if (!Number.isFinite(value)) return String(value);
226
+ const rounded = Math.abs(value) >= 1 ? Number(value.toFixed(DEFAULT_FRACTION_DIGITS)) : Number(value.toPrecision(DEFAULT_SMALL_SIGNIFICANT));
227
+ return String(rounded);
228
+ }
229
+ function formatValue(value, precision) {
230
+ if (value == null) return "\u2014";
231
+ if (precision == null) return defaultFormat(value);
232
+ const places = Math.min(100, Math.max(0, Math.trunc(precision)));
233
+ return value.toFixed(places);
234
+ }
235
+
236
+ // src/thresholds.ts
237
+ function parseThresholds(raw) {
238
+ if (!Array.isArray(raw)) return [];
239
+ const out = [];
240
+ for (const item of raw) {
241
+ if (typeof item !== "object" || item === null) continue;
242
+ const { value, level, label } = item;
243
+ if (typeof value !== "number" || !Number.isFinite(value) || !isSeverityLevel(level)) continue;
244
+ out.push(typeof label === "string" && label !== "" ? { value, level, label } : { value, level });
245
+ }
246
+ return out.map((t, i) => ({ t, i })).sort((a, b) => a.t.value - b.t.value || a.i - b.i).map(({ t }) => t);
247
+ }
248
+ function optThresholds(options, key = "thresholds") {
249
+ return parseThresholds(options?.[key]);
250
+ }
251
+ function bandAt(thresholds, value) {
252
+ if (value == null || !Number.isFinite(value)) return void 0;
253
+ let hit;
254
+ for (const t of thresholds) {
255
+ if (value >= t.value) hit = t;
256
+ else break;
257
+ }
258
+ return hit;
259
+ }
260
+ function thresholdBands(thresholds, min, max) {
261
+ if (!(max > min)) return [];
262
+ const bands = [];
263
+ const push = (from, to, level, label) => {
264
+ const a = Math.max(from, min);
265
+ const b = Math.min(to, max);
266
+ if (b > a) bands.push(label === void 0 ? { from: a, to: b, level } : { from: a, to: b, level, label });
267
+ };
268
+ if (thresholds.length === 0) return [];
269
+ push(-Infinity, thresholds[0].value, void 0);
270
+ thresholds.forEach((t, i) => push(t.value, thresholds[i + 1]?.value ?? Infinity, t.level, t.label));
271
+ return bands;
272
+ }
273
+
274
+ export {
275
+ useWidgetSubject,
276
+ WidgetSubjectProvider,
277
+ WidgetTimeZoneProvider,
278
+ useWidgetTimeZone,
279
+ WidgetThemeRootProvider,
280
+ useWidgetThemeRoot,
281
+ WidgetDensityProvider,
282
+ useWidgetDensityScale,
283
+ WidgetBodySizeProvider,
284
+ useWidgetBodySize,
285
+ WidgetSelectProvider,
286
+ useWidgetSelect,
287
+ WidgetCandidatesProvider,
288
+ useWidgetCandidates,
289
+ WidgetFreshnessProvider,
290
+ useWidgetFreshness,
291
+ WidgetFrameStyleProvider,
292
+ useWidgetFrameStyle,
293
+ WidgetTimeRangeProvider,
294
+ useWidgetTimeRange,
295
+ SEVERITY_LEVELS,
296
+ isSeverityLevel,
297
+ ALARM_LEVELS,
298
+ isAlarmLevel,
299
+ levelColor,
300
+ SEVERITY_TOKENS,
301
+ severityVar,
302
+ severityTokenCss,
303
+ severityFallbackColors,
304
+ severityColorsAt,
305
+ useSeverityColors,
306
+ FIT_MIN_FONT_PX,
307
+ fitValueFont,
308
+ fitValueFontCss,
309
+ stickyChars,
310
+ formatTimestamp,
311
+ formatDateTime,
312
+ formatValue,
313
+ parseThresholds,
314
+ optThresholds,
315
+ bandAt,
316
+ thresholdBands
317
+ };
318
+ //# sourceMappingURL=chunk-A5BJ2ALW.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/context.tsx", "../../src/severity.ts", "../../src/fit-text.ts", "../../src/format.ts", "../../src/thresholds.ts"],
4
+ "sourcesContent": ["// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// The ambient host context a widget reads: the resolved subject, the display time zone, the\n// theme root, the selection callback and the candidate provider.\n//\n// \uD83D\uDD34 THESE CONTEXTS ARE DEFINED HERE AND NOWHERE ELSE. A React context is identified by the\n// OBJECT createContext returned, so two copies of this module \u2014 one bundled into a widget\n// package, one in the host \u2014 are two unrelated contexts: the host's provider writes to one\n// and the widget reads the other, and the selector silently does nothing. Exactly one\n// definition, in this package, which `@devicechain/widgets` and every third-party widget\n// package depend on as a PEER (never a dependency) so the host's install supplies the\n// single copy.\n\nimport { createContext, useContext, type ReactNode } from 'react';\n\nimport type { ResolvedFrame, SelectionCandidate, SelectionTarget, TimeRangeState } from '@devicechain/dashboards';\n\n// WidgetSubjectContext carries the resolved entity a widget shows (its datasource's\n// bound device/anchor), computed once by the renderer and provided per widget. The\n// frame reads it as a subtitle so every board answers \"which asset is this?\" \u2014 and,\n// because it reads the RESOLVED binding, it updates for free once selection re-points a\n// slot (ADR-039 selection-overlay amendment). Kept as ambient context, not a widget\n// prop, so the pure widget contract (widget, data) stays untouched and no widget needs\n// to know how a subject is resolved.\nconst WidgetSubjectContext = createContext<string | undefined>(undefined);\n\n// useWidgetSubject reads the resolved subject line (the frame's fallback subtitle).\nexport function useWidgetSubject(): string | undefined {\n return useContext(WidgetSubjectContext);\n}\n\nexport function WidgetSubjectProvider({\n label,\n children,\n}: {\n label: string | undefined;\n children: ReactNode;\n}) {\n return <WidgetSubjectContext.Provider value={label}>{children}</WidgetSubjectContext.Provider>;\n}\n\n// WidgetTimeZone is the zone widgets print instants in. Undefined (the default) means the\n// viewer's own zone, which is what every live host wants; a host replaying a recording\n// pins one so the board's times agree with the recording's own clock.\nconst WidgetTimeZoneContext = createContext<string | undefined>(undefined);\n\nexport function WidgetTimeZoneProvider({\n timeZone,\n children,\n}: {\n timeZone: string | undefined;\n children: ReactNode;\n}) {\n return <WidgetTimeZoneContext.Provider value={timeZone}>{children}</WidgetTimeZoneContext.Provider>;\n}\n\nexport function useWidgetTimeZone(): string | undefined {\n return useContext(WidgetTimeZoneContext);\n}\n\n// WidgetThemeRoot is the element chart colors are read from. Undefined (the default) means\n// the document root, which is where a console or dashboard app sets its tokens. A host that\n// scopes its tokens to a container (an embed on a page whose own stylesheet owns the root)\n// provides that container, and charts resolve their colors there instead.\nconst WidgetThemeRootContext = createContext<Element | undefined>(undefined);\n\nexport function WidgetThemeRootProvider({\n root,\n children,\n}: {\n root: Element | undefined;\n children: ReactNode;\n}) {\n return <WidgetThemeRootContext.Provider value={root}>{children}</WidgetThemeRootContext.Provider>;\n}\n\nexport function useWidgetThemeRoot(): Element | undefined {\n return useContext(WidgetThemeRootContext);\n}\n\n// The density scale the board renders at (0.9 compact / 1 comfortable / 1.5 wall), provided by\n// the renderer. Outside a renderer it is 1.\nconst WidgetDensityContext = createContext<number>(1);\n\nexport function WidgetDensityProvider({ scale, children }: { scale: number; children: ReactNode }) {\n return <WidgetDensityContext.Provider value={scale}>{children}</WidgetDensityContext.Provider>;\n}\n\nexport function useWidgetDensityScale(): number {\n return useContext(WidgetDensityContext);\n}\n\n// The size of the widget frame's padded content column in px (quantised), provided by\n// WidgetFrame to everything rendered inside it. {0,0} until measured, and outside a frame.\nexport interface WidgetBodySize {\n width: number;\n height: number;\n}\nconst ZERO_SIZE: WidgetBodySize = { width: 0, height: 0 };\nconst WidgetBodySizeContext = createContext<WidgetBodySize>(ZERO_SIZE);\n\nexport function WidgetBodySizeProvider({ size, children }: { size: WidgetBodySize; children: ReactNode }) {\n return <WidgetBodySizeContext.Provider value={size}>{children}</WidgetBodySizeContext.Provider>;\n}\n\nexport function useWidgetBodySize(): WidgetBodySize {\n return useContext(WidgetBodySizeContext);\n}\n\n// WidgetSelect is a view-interaction callback (ADR-039 selection amendment): a widget\n// drives a slot selection (an alarm-table originator drill, a context-selector pick) by\n// naming its target slot + the entity to bind. Kept DISTINCT from WidgetActions (the\n// write seam) \u2014 selection changes only the view's binding overlay, never the backend.\n// Provided by the renderer from the host; absent (undefined) in edit mode and any host\n// that wires no selection, so a widget feature-detects it before offering a drill.\nexport type WidgetSelect = (target: SelectionTarget) => void;\n\nconst WidgetSelectContext = createContext<WidgetSelect | undefined>(undefined);\n\nexport function WidgetSelectProvider({\n select,\n children,\n}: {\n select: WidgetSelect | undefined;\n children: ReactNode;\n}) {\n return <WidgetSelectContext.Provider value={select}>{children}</WidgetSelectContext.Provider>;\n}\n\n// useWidgetSelect returns the ambient selection callback, or undefined where selection is\n// not wired (edit mode, a read-only embedder). A widget renders its drill affordance only\n// when this is defined.\nexport function useWidgetSelect(): WidgetSelect | undefined {\n return useContext(WidgetSelectContext);\n}\n\n// WidgetCandidates is the ambient companion to WidgetSelect (ADR-039 selection amendment): a\n// context/entity-selector widget asks it for the options a target slot may bind to. The host\n// computes candidates from the definition's scope + resolved bindings (useSlotCandidates), so\n// the widget stays presentational and the pure widget contract stays untouched. Distinct from\n// the hub's data channels \u2014 candidates are a fetch-on-open list, not a live stream. Undefined\n// where not wired (edit/preview, a host that supplies no selection) \u2192 the selector is inert.\nexport type WidgetCandidates = (slot: string) => Promise<SelectionCandidate[]>;\n\nconst WidgetCandidatesContext = createContext<WidgetCandidates | undefined>(undefined);\n\nexport function WidgetCandidatesProvider({\n candidates,\n children,\n}: {\n candidates: WidgetCandidates | undefined;\n children: ReactNode;\n}) {\n return <WidgetCandidatesContext.Provider value={candidates}>{children}</WidgetCandidatesContext.Provider>;\n}\n\n// useWidgetCandidates returns the ambient candidate provider, or undefined where selection is\n// not wired. A selector widget renders its picker only when both this and useWidgetSelect are\n// defined.\nexport function useWidgetCandidates(): WidgetCandidates | undefined {\n return useContext(WidgetCandidatesContext);\n}\n\n// WidgetFreshness is what the host knows about how current a widget's data is: the time of its\n// newest sample (epoch ms) and the age past which that is stale. Provided only for a widget\n// whose descriptor opts in (`freshness: true`); the frame reads it and draws the age readout.\n// Absent, the frame draws nothing, so a widget that does not opt in is unchanged.\n//\n// `staleAfterSeconds` is undefined when no threshold is known, which means the age is shown\n// and nothing is ever styled stale. `at` is undefined while no sample has arrived.\nexport interface WidgetFreshness {\n at: number | undefined;\n staleAfterSeconds: number | undefined;\n}\n\nconst WidgetFreshnessContext = createContext<WidgetFreshness | undefined>(undefined);\n\nexport function WidgetFreshnessProvider({\n freshness,\n children,\n}: {\n freshness: WidgetFreshness | undefined;\n children: ReactNode;\n}) {\n return <WidgetFreshnessContext.Provider value={freshness}>{children}</WidgetFreshnessContext.Provider>;\n}\n\nexport function useWidgetFreshness(): WidgetFreshness | undefined {\n return useContext(WidgetFreshnessContext);\n}\n\n// The frame a board gives a widget: its canvas default merged with the widget's own override\n// (resolveFrame in @devicechain/dashboards). Absent -- a widget mounted outside a board -- the\n// frame is today's card.\nconst WidgetFrameStyleContext = createContext<ResolvedFrame | undefined>(undefined);\n\nexport function WidgetFrameStyleProvider({ frame, children }: { frame: ResolvedFrame | undefined; children: ReactNode }) {\n return <WidgetFrameStyleContext.Provider value={frame}>{children}</WidgetFrameStyleContext.Provider>;\n}\n\nexport function useWidgetFrameStyle(): ResolvedFrame | undefined {\n return useContext(WidgetFrameStyleContext);\n}\n\n// The board's time range: the span its time-series widgets draw (seconds back from now) and\n// whether the viewer has frozen the board. Provided by the renderer. A widget that draws\n// history reads it to label its axis or to ask its own questions; the built-in time-series chart\n// needs no code of its own, because the host sizes its window from the same value. `pausedAt`\n// is set exactly while `paused`, and is the instant the window ends at while frozen.\n//\n// Undefined outside a board (a widget mounted on its own), which is \"no range is imposed\" -- not\n// a default span. A widget must handle that rather than assume an hour.\nconst WidgetTimeRangeContext = createContext<TimeRangeState | undefined>(undefined);\n\nexport function WidgetTimeRangeProvider({\n range,\n children,\n}: {\n range: TimeRangeState | undefined;\n children: ReactNode;\n}) {\n return <WidgetTimeRangeContext.Provider value={range}>{children}</WidgetTimeRangeContext.Provider>;\n}\n\nexport function useWidgetTimeRange(): TimeRangeState | undefined {\n return useContext(WidgetTimeRangeContext);\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// The severity levels a threshold names, and the colours the ABNORMAL ones resolve to. A\n// threshold names a LEVEL, never a colour: an author cannot write `#f00`, so a board stays coherent under a re-themed host,\n// dark mode and (later) a greyscale operator theme where colour means severity and nothing\n// else.\n//\n// \uD83D\uDD11 `normal` HAS NO COLOUR. Colour appears only when something is abnormal (the ISA-101\n// rule): a value in a `normal` band renders exactly as it would with no thresholds at all --\n// the card's ordinary text, the gauge's neutral track and accent needle, no chart fill. So a\n// healthy \"below X\" rule and a healthy \"above X\" rule look the same. `normal` stays in the\n// level vocabulary because it is how a band ENDS (\"low fuel below 15\" is critical from 0,\n// normal from 15); it simply is not one of the coloured levels, and there is no\n// `--sev-normal` token. Each widget's own neutral is different (text, track, accent), which\n// is why the theme does not pretend to own one.\n//\n// \uD83D\uDD11 DEFINED ONCE, HERE. The values below are the single source of the `--sev-*` custom\n// properties. A host that defines `--sev-<level>` as HSL channels (the same convention as\n// every other token) wins; a host that does not still renders the colours below, light or\n// dark, so a threshold never draws as nothing for want of a token. `severityTokenCss()` emits\n// the same values as a stylesheet so a host's token set can be generated from this table\n// instead of re-typed.\n\nimport { useMemo, useSyncExternalStore } from 'react';\n\nimport { useWidgetThemeRoot } from './context';\n\nexport const SEVERITY_LEVELS = ['normal', 'warning', 'minor', 'major', 'critical'] as const;\nexport type SeverityLevel = (typeof SEVERITY_LEVELS)[number];\n\nexport function isSeverityLevel(value: unknown): value is SeverityLevel {\n return typeof value === 'string' && (SEVERITY_LEVELS as readonly string[]).includes(value);\n}\n\n// The levels that carry a colour: every level but `normal`.\nexport const ALARM_LEVELS = ['warning', 'minor', 'major', 'critical'] as const;\nexport type AlarmLevel = (typeof ALARM_LEVELS)[number];\n\nexport function isAlarmLevel(level: SeverityLevel | undefined): level is AlarmLevel {\n return level !== undefined && level !== 'normal';\n}\n\n// The colour a level renders in, or undefined for `normal` (and for no level): the caller then\n// uses its own neutral colour.\nexport function levelColor(colors: Readonly<Record<AlarmLevel, string>>, level: SeverityLevel | undefined): string | undefined {\n return isAlarmLevel(level) ? colors[level] : undefined;\n}\n\n// HSL channels (\"h s% l%\"), the form the host tokens use. The warm-to-cool ramp mirrors the\n// alarm severities (red, orange, amber, sky) so one hue means one severity on a card, a gauge\n// band and an alarm row. The dark column is lighter so each reads as INK against a dark ground.\nexport const SEVERITY_TOKENS: Readonly<Record<AlarmLevel, { light: string; dark: string }>> = {\n warning: { light: '199 89% 44%', dark: '199 89% 62%' },\n minor: { light: '32 95% 40%', dark: '38 92% 56%' },\n major: { light: '25 95% 48%', dark: '25 95% 62%' },\n critical: { light: '0 72% 46%', dark: '0 84% 66%' },\n};\n\n// The custom-property name a host defines for a coloured level.\nexport function severityVar(level: AlarmLevel): string {\n return `--sev-${level}`;\n}\n\n// The tokens as CSS, `:root` light and `.dark` dark -- what a host stylesheet pastes (or a\n// build step writes) so the host and this package cannot disagree about a colour. `hostWins`\n// wraps the selectors in `:where()` (zero specificity) for a stylesheet injected alongside a\n// host's own: any ordinary host rule for the same variable then beats it.\nexport function severityTokenCss(options: { hostWins?: boolean } = {}): string {\n const wrap = (selector: string) => (options.hostWins ? `:where(${selector})` : selector);\n const block = (selector: string, side: 'light' | 'dark') =>\n [\n `${wrap(selector)} {`,\n ...ALARM_LEVELS.map((l) => ` ${severityVar(l)}: ${SEVERITY_TOKENS[l][side]};`),\n '}',\n ].join('\\n');\n return `${block(':root', 'light')}\\n${block('.dark', 'dark')}\\n`;\n}\n\n// --- concrete colours ---------------------------------------------------------------\n//\n// A canvas (and an inline style that must be one value) needs a concrete colour, not\n// `var(--sev-x)`. These resolve the level colours in scope at an element: the host's\n// `--sev-<level>` token when it defines one, else the table above for the light or dark ground.\n// They live HERE, not with the chart theme, so a DOM-only widget (a card colouring its value\n// by threshold) gets them from the SDK root without reaching into the `/echarts` entry; the\n// chart theme reads its severity colours through the same functions.\n\n// A channel triple (\"h s% l%\", space- or comma-separated) as a comma-form hsl() colour, the\n// form every canvas colour parser accepts and severityFill can add an alpha to.\nfunction hslFromChannels(channels: string): string {\n const [h, s, l] = channels.replace(/,/g, ' ').trim().split(/\\s+/);\n return `hsl(${h}, ${s}, ${l})`;\n}\n\nfunction severityColors(dark: boolean, host: (cssVar: string) => string | undefined): Record<AlarmLevel, string> {\n const out = {} as Record<AlarmLevel, string>;\n for (const level of ALARM_LEVELS) {\n out[level] = hslFromChannels(host(severityVar(level)) || SEVERITY_TOKENS[level][dark ? 'dark' : 'light']);\n }\n return out;\n}\n\n// Dark when the element sits under a `.dark` ancestor -- the host convention every token block\n// here uses -- or the document root carries it.\nfunction isDark(el: Element): boolean {\n return el.closest('.dark') !== null || el.ownerDocument.documentElement.classList.contains('dark');\n}\n\n// The light colours with no host involved (jsdom, a host with no tokens yet).\nexport function severityFallbackColors(): Record<AlarmLevel, string> {\n return severityColors(false, () => undefined);\n}\n\n// The colours in scope at `el`. `style` is its computed style when the caller already has it.\nexport function severityColorsAt(el: Element, style: CSSStyleDeclaration = getComputedStyle(el)): Record<AlarmLevel, string> {\n return severityColors(isDark(el), (name) => style.getPropertyValue(name).trim() || undefined);\n}\n\n// One page-wide observer, shared by every caller (the same O(1)-per-toggle shape as the chart\n// theme store): re-resolved when <html> changes class (light/dark) or inline style (tokens).\nlet shared: Record<AlarmLevel, string> | null = null;\nconst listeners = new Set<() => void>();\nlet observer: MutationObserver | null = null;\n\nfunction subscribe(listener: () => void): () => void {\n if (!observer) {\n shared = severityColorsAt(document.documentElement);\n observer = new MutationObserver(() => {\n shared = severityColorsAt(document.documentElement);\n for (const l of listeners) l();\n });\n observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'style'] });\n }\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n if (listeners.size === 0 && observer) {\n observer.disconnect();\n observer = null;\n shared = null;\n }\n };\n}\n\nfunction snapshot(): Record<AlarmLevel, string> {\n shared ??= severityColorsAt(document.documentElement);\n return shared;\n}\n\n// useSeverityColors returns the four concrete severity colours (none for `normal`) and re-renders the caller when\n// the document theme changes. A container-scoped theme (WidgetThemeRoot) is read once there,\n// as the chart theme does.\nexport function useSeverityColors(): Record<AlarmLevel, string> {\n const page = useSyncExternalStore(subscribe, snapshot, snapshot);\n const root = useWidgetThemeRoot();\n const scoped = useMemo(() => (root ? severityColorsAt(root) : null), [root]);\n return scoped ?? page;\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// fitValueFont -- the one font-size curve for a big readout (a card's number, a KPI count, a\n// gauge's value). Every widget used to carry its own clamp of the card HEIGHT with a hard cap\n// (44 px, 56 px); that ignored the width, so a long string overflowed a wide-but-short or\n// narrow-but-tall box, and it stopped growing on a wall display. This curve reads both\n// dimensions and the string length.\n//\n// font = min( height * HEIGHT_SHARE , width / (chars * GLYPH_WIDTH) )\n//\n// then floored at MIN_FONT_PX. There is no upper cap: a bigger box gets a bigger number.\n//\n// `size` is the box the text may occupy, so a caller passes its content box (the measured\n// element minus its own padding). HEIGHT_SHARE leaves room for the line's leading and for a\n// secondary line (unit, caption) beneath it. GLYPH_WIDTH is the advance of a digit as a\n// fraction of the font size. Measured: system faces (Segoe UI, Roboto, Arial bold) set a\n// digit at 0.56-0.58 em, but the console's Inter sets them at 0.61-0.62 em on average and\n// 0.67 em at the widest in the 600-700 weights a readout uses. 0.65 covers Inter's digits to\n// within a few percent (which the caller's padding absorbs) without shrinking a system-font\n// number much; a '.' or '-' counted as a full character adds slack.\n\nexport const FIT_MIN_FONT_PX = 12;\n// What to use before the first measure (ElementSize is {0,0} until then) or in a non-DOM test.\nexport const FIT_FALLBACK_FONT_PX = 32;\n\n// The curve's two constants. Exported so the CSS path (fitValueFontCss, `cq` units) and the\n// JS path (fitValueFont, a canvas widget's numeric font) are built from THESE and cannot\n// drift: a number in a card and a number in a gauge of the same size look the same.\nexport const FIT_HEIGHT_SHARE = 0.6;\nexport const FIT_GLYPH_WIDTH = 0.65;\n\nexport interface FitOptions {\n // The smallest font the curve returns, in px. Density scales THIS (a wall display's floor is\n // larger, a compact board's smaller); the curve itself never changes. Default FIT_MIN_FONT_PX.\n floor?: number;\n}\n\nfunction charCount(chars: number): number {\n return Math.max(1, Math.ceil(Number.isFinite(chars) ? chars : 1));\n}\n\nexport function fitValueFont(size: { width: number; height: number }, chars: number, options: FitOptions = {}): number {\n const floor = options.floor ?? FIT_MIN_FONT_PX;\n if (!(size.width > 0) || !(size.height > 0)) return Math.max(floor, FIT_FALLBACK_FONT_PX);\n const byHeight = size.height * FIT_HEIGHT_SHARE;\n const byWidth = size.width / (charCount(chars) * FIT_GLYPH_WIDTH);\n return Math.max(floor, Math.floor(Math.min(byHeight, byWidth)));\n}\n\n// The same curve as a CSS length, for text inside a `container-type: size` box (the widget\n// frame's content column): `cqh`/`cqw` are that box's height and width, so the browser resolves\n// the font on the very first frame -- no {0,0} measure flash -- and re-resolves on every\n// resize with no JS. The box a caller passes to fitValueFont is the SAME box these units\n// measure (the frame's padded content column).\nexport function fitValueFontCss(chars: number, options: FitOptions = {}): string {\n const floor = options.floor ?? FIT_MIN_FONT_PX;\n const perChar = 100 / (charCount(chars) * FIT_GLYPH_WIDTH);\n return `max(${floor}px, min(${FIT_HEIGHT_SHARE * 100}cqh, ${Number(perChar.toFixed(3))}cqw))`;\n}\n\n// A big readout's character count held at the longest recently seen, so a value crossing\n// 9.99 -> 10.01 does not jump the font size by a character's worth (~20 % on a short\n// string). The count grows at once and shrinks only after `holdMs` with nothing longer.\nexport interface StickyChars {\n value: number;\n since: number;\n}\n\nexport function stickyChars(prev: StickyChars | undefined, chars: number, now: number, holdMs: number): StickyChars {\n if (!prev || chars >= prev.value) return { value: chars, since: now };\n if (now - prev.since >= holdMs) return { value: chars, since: now };\n return prev;\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Both formatters take an optional IANA `timeZone`. Omitted, they are exactly what they\n// always were: the viewer's own zone and locale defaults, which is right for a live board.\n// A host that plays a RECORDING pins one (the renderer's `timeZone` prop), because the\n// recorded instants are the point and a viewer's zone would shift them across a date line;\n// a pinned zone also prints its short name, so the reading says which clock it is.\n\n// formatTimestamp renders an RFC3339 event time as a locale time string, leaving\n// an unparseable value untouched and an absent one blank.\nexport function formatTimestamp(iso: string | null, timeZone?: string): string {\n if (!iso) return '';\n const date = new Date(iso);\n if (Number.isNaN(date.getTime())) return iso;\n return timeZone ? date.toLocaleTimeString(undefined, { timeZone, timeZoneName: 'short' }) : date.toLocaleTimeString();\n}\n\n// formatDateTime renders an RFC3339 time as a locale date+time string. Unlike\n// formatTimestamp (time-only, right for a live measurement tick) this keeps the date,\n// because an alarm can have been raised days ago and a bare \"09:14:32\" would mislead.\nexport function formatDateTime(iso: string | null, timeZone?: string): string {\n if (!iso) return '';\n const date = new Date(iso);\n if (Number.isNaN(date.getTime())) return iso;\n return timeZone ? date.toLocaleString(undefined, { timeZone, timeZoneName: 'short' }) : date.toLocaleString();\n}\n\n// With no precision configured a value is still not printed at full float width: a sensor's\n// 30.203057033956792 is noise on a card, a gauge or an alarm row. So an unset precision shows\n// at most DEFAULT_FRACTION_DIGITS decimals (trailing zeros dropped), and a value under 1 keeps\n// DEFAULT_SMALL_SIGNIFICANT significant digits instead, so a 0.000123 reading is not rounded\n// to nothing. An author who wants exact decimals sets `precision`.\nexport const DEFAULT_FRACTION_DIGITS = 3;\nexport const DEFAULT_SMALL_SIGNIFICANT = 4;\n\nfunction defaultFormat(value: number): string {\n if (!Number.isFinite(value)) return String(value);\n const rounded =\n Math.abs(value) >= 1 ? Number(value.toFixed(DEFAULT_FRACTION_DIGITS)) : Number(value.toPrecision(DEFAULT_SMALL_SIGNIFICANT));\n return String(rounded);\n}\n\n// formatValue renders a measurement value, applying a fixed precision when given\n// (else the default rounding above) and showing an em dash for a missing value.\n//\n// The precision is clamped to toFixed's domain because it arrives from an opaque stored\n// definition: outside 0..100 toFixed throws a RangeError, which is an exception thrown\n// during render \u2014 it unmounts the widget's whole subtree rather than degrading, and every\n// other bad option value in this package degrades. The console's config panel can author\n// a negative one today, and a hand-edited definition always could.\nexport function formatValue(value: number | null | undefined, precision?: number): string {\n if (value == null) return '\u2014';\n if (precision == null) return defaultFormat(value);\n const places = Math.min(100, Math.max(0, Math.trunc(precision)));\n return value.toFixed(places);\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Thresholds: where a measurement crosses from one severity to the next.\n//\n// A threshold STARTS a band: it holds from its `value` up to the next threshold's value (or\n// without end, for the last). Below the lowest threshold there is no band, so a value there\n// reads in the widget's own neutral colour, as does a value in a `normal` band (normal has no\n// colour; see severity.ts), so a healthy value looks the same under either kind of rule.\n// This one rule covers both directions:\n// - \"overheat above 105\" -> [{ value: 105, level: 'critical' }]\n// - \"low fuel below 15\" -> [{ value: 0, level: 'critical' }, { value: 15, level: 'normal' }]\n// A value ON a threshold belongs to the band that threshold starts (>=), never the one below.\n\nimport { isSeverityLevel, type SeverityLevel } from './severity';\n\nexport interface Threshold {\n value: number;\n level: SeverityLevel;\n label?: string;\n}\n\n// A band resolved over a numeric range. `level` is undefined for the neutral stretch below the\n// lowest threshold. `to` is Infinity for the last band.\nexport interface ThresholdBand {\n from: number;\n to: number;\n level: SeverityLevel | undefined;\n label?: string;\n}\n\n// parseThresholds reads the stored option defensively: a hand-edited definition may carry\n// anything, and a threshold that cannot be read must drop out rather than throw during render\n// (or, worse, colour a value by a guess). Only a finite `value` and a known `level` survive --\n// so a free colour in the `level` slot is dropped, and an extra `color` key is never read.\n// The result is sorted ascending by value (stable, so equal values keep their authored order\n// and the later one wins).\nexport function parseThresholds(raw: unknown): Threshold[] {\n if (!Array.isArray(raw)) return [];\n const out: Threshold[] = [];\n for (const item of raw) {\n if (typeof item !== 'object' || item === null) continue;\n const { value, level, label } = item as Record<string, unknown>;\n if (typeof value !== 'number' || !Number.isFinite(value) || !isSeverityLevel(level)) continue;\n out.push(typeof label === 'string' && label !== '' ? { value, level, label } : { value, level });\n }\n return out.map((t, i) => ({ t, i })).sort((a, b) => a.t.value - b.t.value || a.i - b.i).map(({ t }) => t);\n}\n\nexport function optThresholds(options: Record<string, unknown> | undefined, key = 'thresholds'): Threshold[] {\n return parseThresholds(options?.[key]);\n}\n\n// bandAt returns the threshold whose band holds `value`, or undefined below the lowest.\n// `thresholds` must be sorted (parseThresholds does). A null value has no band.\nexport function bandAt(thresholds: readonly Threshold[], value: number | null | undefined): Threshold | undefined {\n if (value == null || !Number.isFinite(value)) return undefined;\n let hit: Threshold | undefined;\n for (const t of thresholds) {\n if (value >= t.value) hit = t;\n else break;\n }\n return hit;\n}\n\n// thresholdBands lays the bands over [min, max]: a neutral lead-in up to the first threshold,\n// then one band per threshold, each clipped to the range. Bands that fall wholly outside the\n// range are dropped, and the band in effect AT `min` is carried in from a threshold below it.\n// The result tiles [min, max] exactly, which is what a gauge arc needs.\nexport function thresholdBands(thresholds: readonly Threshold[], min: number, max: number): ThresholdBand[] {\n if (!(max > min)) return [];\n const bands: ThresholdBand[] = [];\n const push = (from: number, to: number, level: SeverityLevel | undefined, label?: string) => {\n const a = Math.max(from, min);\n const b = Math.min(to, max);\n if (b > a) bands.push(label === undefined ? { from: a, to: b, level } : { from: a, to: b, level, label });\n };\n if (thresholds.length === 0) return [];\n push(-Infinity, thresholds[0].value, undefined);\n thresholds.forEach((t, i) => push(t.value, thresholds[i + 1]?.value ?? Infinity, t.level, t.label));\n return bands;\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,7 @@
1
+ // src/chart/echart-markers.ts
2
+ import { MarkAreaComponent, MarkLineComponent } from "echarts/components";
3
+ var MARKER_COMPONENTS = [MarkLineComponent, MarkAreaComponent];
4
+ export {
5
+ MARKER_COMPONENTS
6
+ };
7
+ //# sourceMappingURL=echart-markers-JDFD265P.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/chart/echart-markers.ts"],
4
+ "sourcesContent": ["// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// The threshold markers (markLine + markArea), split out of echart.tsx so they load only when\n// a chart actually draws thresholds. Together they are ~7 KB gzip, and most boards -- every\n// gauge, and every chart without thresholds -- never use them. echart.tsx imports this module\n// DYNAMICALLY; a static import of it anywhere would fold it back into the initial bundle.\n\nimport { MarkAreaComponent, MarkLineComponent } from 'echarts/components';\n\nexport const MARKER_COMPONENTS = [MarkLineComponent, MarkAreaComponent];\n"],
5
+ "mappings": ";AAQA,SAAS,mBAAmB,yBAAyB;AAE9C,IAAM,oBAAoB,CAAC,mBAAmB,iBAAiB;",
6
+ "names": []
7
+ }
@@ -0,0 +1,6 @@
1
+ import type { HubConnectionState } from '@devicechain/dashboards';
2
+ export interface HubConnectionBadgeProps {
3
+ state: HubConnectionState;
4
+ labels: Record<HubConnectionState, string>;
5
+ }
6
+ export declare function HubConnectionBadge({ state, labels }: HubConnectionBadgeProps): import("react").JSX.Element;
package/dist/context.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type ReactNode } from 'react';
2
- import type { SelectionCandidate, SelectionTarget } from '@devicechain/dashboards';
2
+ import type { ResolvedFrame, SelectionCandidate, SelectionTarget, TimeRangeState } from '@devicechain/dashboards';
3
3
  export declare function useWidgetSubject(): string | undefined;
4
4
  export declare function WidgetSubjectProvider({ label, children, }: {
5
5
  label: string | undefined;
@@ -15,6 +15,20 @@ export declare function WidgetThemeRootProvider({ root, children, }: {
15
15
  children: ReactNode;
16
16
  }): import("react").JSX.Element;
17
17
  export declare function useWidgetThemeRoot(): Element | undefined;
18
+ export declare function WidgetDensityProvider({ scale, children }: {
19
+ scale: number;
20
+ children: ReactNode;
21
+ }): import("react").JSX.Element;
22
+ export declare function useWidgetDensityScale(): number;
23
+ export interface WidgetBodySize {
24
+ width: number;
25
+ height: number;
26
+ }
27
+ export declare function WidgetBodySizeProvider({ size, children }: {
28
+ size: WidgetBodySize;
29
+ children: ReactNode;
30
+ }): import("react").JSX.Element;
31
+ export declare function useWidgetBodySize(): WidgetBodySize;
18
32
  export type WidgetSelect = (target: SelectionTarget) => void;
19
33
  export declare function WidgetSelectProvider({ select, children, }: {
20
34
  select: WidgetSelect | undefined;
@@ -27,3 +41,22 @@ export declare function WidgetCandidatesProvider({ candidates, children, }: {
27
41
  children: ReactNode;
28
42
  }): import("react").JSX.Element;
29
43
  export declare function useWidgetCandidates(): WidgetCandidates | undefined;
44
+ export interface WidgetFreshness {
45
+ at: number | undefined;
46
+ staleAfterSeconds: number | undefined;
47
+ }
48
+ export declare function WidgetFreshnessProvider({ freshness, children, }: {
49
+ freshness: WidgetFreshness | undefined;
50
+ children: ReactNode;
51
+ }): import("react").JSX.Element;
52
+ export declare function useWidgetFreshness(): WidgetFreshness | undefined;
53
+ export declare function WidgetFrameStyleProvider({ frame, children }: {
54
+ frame: ResolvedFrame | undefined;
55
+ children: ReactNode;
56
+ }): import("react").JSX.Element;
57
+ export declare function useWidgetFrameStyle(): ResolvedFrame | undefined;
58
+ export declare function WidgetTimeRangeProvider({ range, children, }: {
59
+ range: TimeRangeState | undefined;
60
+ children: ReactNode;
61
+ }): import("react").JSX.Element;
62
+ export declare function useWidgetTimeRange(): TimeRangeState | undefined;
package/dist/echarts.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { EChart, type EChartOption, type EChartProps } from './chart/echart';
2
- export { buildGaugeOption, buildLineOption, type GaugeChartOptions, type LineChartOptions, } from './chart/chart-options';
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';