@devicechain/widget-sdk 0.0.0-stage → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 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, canvas renderer) \u2014 tree-shaking away the\n// 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, TooltipComponent } from 'echarts/components';\nimport * as echarts from 'echarts/core';\nimport { CanvasRenderer } from 'echarts/renderers';\nimport { useEffect, useRef } from 'react';\n\necharts.use([LineChart, GaugeChart, GridComponent, TooltipComponent, CanvasRenderer]);\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\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]);\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 type { ElementSize } from '../hooks';\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\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}\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 axisFont = clamp(Math.round(shortSide(size) / 22), 8, 12);\n\n const series = names.map((name, i) => {\n const color = theme.series[i % theme.series.length];\n return {\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 ? 28 : 12, bottom: 8, containLabel: true },\n tooltip: { trigger: 'axis' },\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 splitLine: { lineStyle: { color: theme.border } },\n axisLabel: { color: theme.mutedForeground, fontSize: axisFont },\n },\n series,\n };\n}\n\nexport interface GaugeChartOptions {\n min?: number;\n max?: number;\n unit?: string;\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 accent = theme.series[0];\n const unitSuffix = options.unit ? ` ${options.unit}` : '';\n\n const s = shortSide(size);\n const arcWidth = clamp(Math.round(s / 26), 4, 12);\n const labelFont = clamp(Math.round(s / 20), 8, 13);\n const valueFont = clamp(Math.round(s / 7), 14, 40);\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: '95%',\n center: ['50%', '58%'],\n progress: { show: true, width: arcWidth, itemStyle: { color: accent } },\n pointer: { itemStyle: { color: accent }, length: '55%', width: clamp(Math.round(s / 50), 2, 6) },\n anchor: { show: false },\n axisLine: { lineStyle: { width: arcWidth, color: [[1, theme.border]] } },\n axisTick: { show: false },\n splitLine: { length: arcWidth, lineStyle: { color: theme.border, width: 1 } },\n axisLabel: { color: theme.mutedForeground, fontSize: labelFont, distance: arcWidth + 2 },\n detail: {\n valueAnimation: false,\n color: theme.foreground,\n fontSize: valueFont,\n offsetCenter: [0, '30%'],\n formatter: (v: number) => (value == null ? '\u2014' : `${v}${unitSuffix}`),\n },\n data: [{ value: value ?? min }],\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';\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}\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};\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 };\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"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,13 @@
1
+ import { type CSSProperties } from 'react';
2
+ export type FlashDirection = 'up' | 'down' | null;
3
+ export declare function useFlashOnChange(value: number | null | undefined, enabled: boolean, identity?: string | number): FlashDirection;
4
+ export declare function flashTextStyle(direction: FlashDirection, fadeMs?: number): CSSProperties;
5
+ export declare function flashBackgroundStyle(direction: FlashDirection, fadeMs?: number): CSSProperties;
6
+ export declare function FlashValue({ value, precision, enabled, identity, baseColor, style, }: {
7
+ value: number | null | undefined;
8
+ precision?: number;
9
+ enabled: boolean;
10
+ identity?: string | number;
11
+ baseColor?: string;
12
+ style?: CSSProperties;
13
+ }): import("react").JSX.Element;
@@ -0,0 +1,3 @@
1
+ export declare function formatTimestamp(iso: string | null, timeZone?: string): string;
2
+ export declare function formatDateTime(iso: string | null, timeZone?: string): string;
3
+ export declare function formatValue(value: number | null | undefined, precision?: number): string;
@@ -0,0 +1,8 @@
1
+ import { type CSSProperties, type ReactNode } from 'react';
2
+ export interface WidgetFrameProps {
3
+ title?: string;
4
+ subtitle?: string;
5
+ children: ReactNode;
6
+ bodyStyle?: CSSProperties;
7
+ }
8
+ export declare function WidgetFrame({ title, subtitle, children, bodyStyle }: WidgetFrameProps): import("react").JSX.Element;
@@ -0,0 +1,31 @@
1
+ import type { AlarmSubscription, CommandSubscription, DashboardDefinition, DatasourceSelector, DeviceResolver, EntityCandidateLister, LocationSubscription, MemberResolver, MeasurementSample, SelectionCandidate, SlotBinding, WidgetDataSource } from '@devicechain/dashboards';
2
+ import type { AlarmStreamState, CommandStreamState, LocationStreamState, MeasurementStreamState } from '@devicechain/dashboards';
3
+ import { DashboardHub } from '@devicechain/dashboards';
4
+ import { type RefObject } from 'react';
5
+ import { type WidgetCandidates } from './context';
6
+ export type { AlarmStreamState, CommandStreamState, LocationStreamState, MeasurementStreamState };
7
+ export declare function useDashboardHub(resolver: DeviceResolver, authorities: string[] | undefined): DashboardHub;
8
+ export declare function useResolvedBindings(definition: DashboardDefinition, base: Record<string, SlotBinding>, selection: Record<string, SlotBinding>, resolver: MemberResolver): Record<string, SlotBinding>;
9
+ export declare function useSlotCandidates(definition: DashboardDefinition, bindings: Record<string, SlotBinding>, resolver: MemberResolver, lister: EntityCandidateLister | undefined): WidgetCandidates | undefined;
10
+ export interface CandidatesState {
11
+ candidates: SelectionCandidate[];
12
+ loading: boolean;
13
+ error: unknown | null;
14
+ wired: boolean;
15
+ }
16
+ export declare function useCandidates(slot: string | undefined): CandidatesState;
17
+ export interface MeasurementStreamOptions {
18
+ window?: number;
19
+ initialSamples?: MeasurementSample[];
20
+ }
21
+ export declare function useMeasurementStream(hub: WidgetDataSource, datasource: DatasourceSelector | undefined, options?: MeasurementStreamOptions): MeasurementStreamState;
22
+ export declare function useAlarmStream(hub: WidgetDataSource, subscription: AlarmSubscription): AlarmStreamState;
23
+ export declare function useCommandStream(hub: WidgetDataSource, subscription: CommandSubscription): CommandStreamState;
24
+ export declare function useLocationStream(hub: WidgetDataSource, subscription: LocationSubscription): LocationStreamState;
25
+ export type DatasourceAvailability = 'unknown' | 'available' | 'unavailable';
26
+ export declare function useDatasourceAvailability(hub: WidgetDataSource, datasource: DatasourceSelector | undefined): DatasourceAvailability;
27
+ export interface ElementSize {
28
+ width: number;
29
+ height: number;
30
+ }
31
+ export declare function useElementSize<T extends HTMLElement>(): [RefObject<T | null>, ElementSize];
@@ -0,0 +1,12 @@
1
+ export { defineWidget } from './define-widget';
2
+ export type { ErasedComponent, ErasedWidgetDefinition, WidgetDefinition, WidgetImplementation } from './define-widget';
3
+ export { createWidgetRegistry, WidgetRegistryError, type WidgetRegistry } from './registry';
4
+ export type { EditorCommandChoice, EditorCommandChoices, WidgetComponent, WidgetEditorProps, WidgetEditorServices, WidgetProps, } from './widget';
5
+ export { WidgetCandidatesProvider, WidgetSelectProvider, WidgetSubjectProvider, WidgetThemeRootProvider, WidgetTimeZoneProvider, useWidgetCandidates, useWidgetSelect, useWidgetSubject, useWidgetThemeRoot, useWidgetTimeZone, type WidgetCandidates, type WidgetSelect, } from './context';
6
+ export { WidgetFrame, type WidgetFrameProps } from './frame';
7
+ export { css, type ThemeVar } from './theme';
8
+ export { formatDateTime, formatTimestamp, formatValue } from './format';
9
+ export { FlashValue, flashBackgroundStyle, flashTextStyle, useFlashOnChange, type FlashDirection, } from './flash';
10
+ export { optBoolean, optNumber, optString, pickSample, primaryMeasurementName } from './widget-options';
11
+ export { useAlarmStream, useCandidates, useCommandStream, useDashboardHub, useDatasourceAvailability, useElementSize, useLocationStream, useMeasurementStream, useResolvedBindings, useSlotCandidates, type AlarmStreamState, type CandidatesState, type CommandStreamState, type DatasourceAvailability, type ElementSize, type LocationStreamState, type MeasurementStreamOptions, type MeasurementStreamState, } from './hooks';
12
+ export { SDK_API_VERSION, WidgetCatalogError, defineWidgetDescriptor, isBuiltinWidgetId, isNamespacedWidgetId, validateWidgetDescriptor, type AckOptionRef, type BooleanOptionSpec, type BuiltinWidgetId, type ChannelBindingPolicy, type ChannelSnapshot, type ConstBinding, type DatasourcePolicy, type DatasourceSelectorKind, type DefaultsOf, type DescriptorIssue, type EnumOptionSpec, type GreaterThanConstraint, type NamespacedWidgetId, type NumberOptionRef, type NumberOptionSpec, type OptionConstraint, type OptionSpec, type OptionsOf, type StringOptionRef, type StringOptionSpec, type WidgetCatalog, type WidgetChannel, type WidgetDescriptor, type WidgetDisplay, type WidgetId, type WidgetInteractionDeclaration, type WidgetOptionSpecs, } from '@devicechain/dashboards';
package/dist/index.js ADDED
@@ -0,0 +1,546 @@
1
+ import {
2
+ WidgetCandidatesProvider,
3
+ WidgetSelectProvider,
4
+ WidgetSubjectProvider,
5
+ WidgetThemeRootProvider,
6
+ WidgetTimeZoneProvider,
7
+ useWidgetCandidates,
8
+ useWidgetSelect,
9
+ useWidgetSubject,
10
+ useWidgetThemeRoot,
11
+ useWidgetTimeZone
12
+ } from "./chunks/chunk-CC2V7LVI.js";
13
+
14
+ // src/define-widget.ts
15
+ function defineWidget(definition) {
16
+ return definition;
17
+ }
18
+
19
+ // src/registry.ts
20
+ import {
21
+ WidgetCatalogError,
22
+ collectCatalogIssues,
23
+ createWidgetCatalog
24
+ } from "@devicechain/dashboards";
25
+ var WidgetRegistryError = class extends WidgetCatalogError {
26
+ constructor(issues) {
27
+ super(issues, "widget registry");
28
+ this.name = "WidgetRegistryError";
29
+ }
30
+ };
31
+ function isComponent(value) {
32
+ if (typeof value === "function") return true;
33
+ return typeof value === "object" && value !== null && "$$typeof" in value;
34
+ }
35
+ function createWidgetRegistry(definitions) {
36
+ if (!Array.isArray(definitions)) {
37
+ throw new WidgetRegistryError([{ id: "#?", path: "", message: "definitions must be an array" }]);
38
+ }
39
+ const issues = collectCatalogIssues(definitions.map((d) => d?.descriptor));
40
+ definitions.forEach((definition, index) => {
41
+ const id = definition?.descriptor?.id;
42
+ const label = typeof id === "string" && id !== "" ? id : `#${index}`;
43
+ const add = (path, message) => {
44
+ issues.push({ id: label, path, message });
45
+ };
46
+ const impl = definition?.implementation;
47
+ if (typeof impl !== "object" || impl === null) {
48
+ add("implementation", "a definition needs an implementation");
49
+ return;
50
+ }
51
+ if (impl.kind !== "trusted-react") {
52
+ add("implementation.kind", `kind must be "trusted-react", got ${JSON.stringify(impl.kind)}`);
53
+ }
54
+ if (!isComponent(impl.component)) add("implementation.component", "component must be a React component");
55
+ if (impl.editor !== void 0 && !isComponent(impl.editor)) {
56
+ add("implementation.editor", "editor, when given, must be a React component");
57
+ }
58
+ });
59
+ if (issues.length > 0) throw new WidgetRegistryError(issues);
60
+ const catalog = createWidgetCatalog(definitions.map((d) => d.descriptor));
61
+ const byId = /* @__PURE__ */ new Map();
62
+ for (const definition of definitions) {
63
+ byId.set(
64
+ definition.descriptor.id,
65
+ Object.freeze({
66
+ descriptor: catalog.get(definition.descriptor.id),
67
+ implementation: Object.freeze({ ...definition.implementation })
68
+ })
69
+ );
70
+ }
71
+ const ordered = Object.freeze([...byId.values()]);
72
+ return Object.freeze({
73
+ catalog,
74
+ descriptor: (id) => catalog.get(id),
75
+ implementation: (id) => byId.get(id)?.implementation,
76
+ list: () => ordered
77
+ });
78
+ }
79
+
80
+ // src/theme.ts
81
+ function css(name) {
82
+ return `hsl(var(--${name}))`;
83
+ }
84
+
85
+ // src/frame.tsx
86
+ import { jsx, jsxs } from "react/jsx-runtime";
87
+ function WidgetFrame({ title, subtitle, children, bodyStyle }) {
88
+ const subject = useWidgetSubject();
89
+ const sub = subtitle ?? subject;
90
+ return /* @__PURE__ */ jsxs(
91
+ "div",
92
+ {
93
+ style: {
94
+ display: "flex",
95
+ flexDirection: "column",
96
+ width: "100%",
97
+ height: "100%",
98
+ boxSizing: "border-box",
99
+ background: css("card"),
100
+ color: css("card-foreground"),
101
+ border: `1px solid ${css("border")}`,
102
+ borderRadius: "var(--radius, 8px)",
103
+ overflow: "hidden"
104
+ },
105
+ children: [
106
+ title || sub ? /* @__PURE__ */ jsxs(
107
+ "div",
108
+ {
109
+ style: {
110
+ padding: "8px 12px",
111
+ borderBottom: `1px solid ${css("border")}`
112
+ },
113
+ children: [
114
+ title ? /* @__PURE__ */ jsx("div", { style: { fontSize: 12, fontWeight: 600, color: css("muted-foreground") }, children: title }) : null,
115
+ sub ? /* @__PURE__ */ jsx(
116
+ "div",
117
+ {
118
+ title: sub,
119
+ style: {
120
+ fontSize: 11,
121
+ fontWeight: 400,
122
+ color: css("muted-foreground"),
123
+ opacity: 0.75,
124
+ marginTop: title ? 2 : 0,
125
+ whiteSpace: "nowrap",
126
+ overflow: "hidden",
127
+ textOverflow: "ellipsis"
128
+ },
129
+ children: sub
130
+ }
131
+ ) : null
132
+ ]
133
+ }
134
+ ) : null,
135
+ /* @__PURE__ */ jsx("div", { style: { flex: 1, minHeight: 0, ...bodyStyle }, children })
136
+ ]
137
+ }
138
+ );
139
+ }
140
+
141
+ // src/format.ts
142
+ function formatTimestamp(iso, timeZone) {
143
+ if (!iso) return "";
144
+ const date = new Date(iso);
145
+ if (Number.isNaN(date.getTime())) return iso;
146
+ return timeZone ? date.toLocaleTimeString(void 0, { timeZone, timeZoneName: "short" }) : date.toLocaleTimeString();
147
+ }
148
+ function formatDateTime(iso, timeZone) {
149
+ if (!iso) return "";
150
+ const date = new Date(iso);
151
+ if (Number.isNaN(date.getTime())) return iso;
152
+ return timeZone ? date.toLocaleString(void 0, { timeZone, timeZoneName: "short" }) : date.toLocaleString();
153
+ }
154
+ function formatValue(value, precision) {
155
+ if (value == null) return "\u2014";
156
+ if (precision == null) return String(value);
157
+ const places = Math.min(100, Math.max(0, Math.trunc(precision)));
158
+ return value.toFixed(places);
159
+ }
160
+
161
+ // src/flash.tsx
162
+ import { useEffect, useRef, useState } from "react";
163
+ import { jsx as jsx2 } from "react/jsx-runtime";
164
+ var UP = {
165
+ fg: "var(--flash-up, hsl(142 72% 40%))",
166
+ bg: "var(--flash-up-bg, hsl(142 72% 45% / 0.16))"
167
+ };
168
+ var DOWN = {
169
+ fg: "var(--flash-down, hsl(0 72% 51%))",
170
+ bg: "var(--flash-down-bg, hsl(0 72% 55% / 0.16))"
171
+ };
172
+ var HOLD_MS = 400;
173
+ var FADE_MS = 600;
174
+ function useFlashOnChange(value, enabled, identity) {
175
+ const prev = useRef(value);
176
+ const prevIdentity = useRef(identity);
177
+ const [direction, setDirection] = useState(null);
178
+ useEffect(() => {
179
+ const before = prev.current;
180
+ const sameEntity = prevIdentity.current === identity;
181
+ prev.current = value;
182
+ prevIdentity.current = identity;
183
+ if (!enabled || !sameEntity || typeof value !== "number" || typeof before !== "number" || !Number.isFinite(value) || !Number.isFinite(before) || value === before) {
184
+ setDirection(null);
185
+ return;
186
+ }
187
+ setDirection(value > before ? "up" : "down");
188
+ const timer = setTimeout(() => setDirection(null), HOLD_MS);
189
+ return () => clearTimeout(timer);
190
+ }, [value, enabled, identity]);
191
+ return enabled ? direction : null;
192
+ }
193
+ function flashTextStyle(direction, fadeMs = FADE_MS) {
194
+ if (direction) return { color: direction === "up" ? UP.fg : DOWN.fg, transition: "none" };
195
+ return { transition: `color ${fadeMs}ms ease-out` };
196
+ }
197
+ function flashBackgroundStyle(direction, fadeMs = FADE_MS) {
198
+ if (direction) {
199
+ return { backgroundColor: direction === "up" ? UP.bg : DOWN.bg, transition: "none" };
200
+ }
201
+ return { transition: `background-color ${fadeMs}ms ease-out` };
202
+ }
203
+ function FlashValue({
204
+ value,
205
+ precision,
206
+ enabled,
207
+ identity,
208
+ baseColor = "inherit",
209
+ style
210
+ }) {
211
+ const direction = useFlashOnChange(value, enabled, identity);
212
+ return /* @__PURE__ */ jsx2("span", { style: { color: baseColor, ...style, ...flashTextStyle(direction) }, children: formatValue(value, precision) });
213
+ }
214
+
215
+ // src/widget-options.ts
216
+ function pickSample(latest, name) {
217
+ return name ? latest[name] : Object.values(latest)[0];
218
+ }
219
+ function optString(options, key) {
220
+ const value = options?.[key];
221
+ return typeof value === "string" ? value : void 0;
222
+ }
223
+ function optNumber(options, key) {
224
+ const value = options?.[key];
225
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
226
+ }
227
+ function optBoolean(options, key) {
228
+ return options?.[key] === true;
229
+ }
230
+ function primaryMeasurementName(widget) {
231
+ const explicit = optString(widget.options, "measurement");
232
+ if (explicit) return explicit;
233
+ const ds = widget.datasource;
234
+ if (ds && ds.measurements.length > 0) return ds.measurements[0];
235
+ return void 0;
236
+ }
237
+
238
+ // src/hooks.ts
239
+ import {
240
+ bindingsWithoutScopedSlots,
241
+ DashboardHub,
242
+ hasScopedSlots,
243
+ resolveContextBindings,
244
+ resolveSlotCandidates
245
+ } from "@devicechain/dashboards";
246
+ import { useEffect as useEffect2, useMemo, useRef as useRef2, useState as useState2 } from "react";
247
+ function useDashboardHub(resolver, authorities) {
248
+ const authoritiesKey = JSON.stringify(authorities ?? []);
249
+ const hub = useMemo(
250
+ () => new DashboardHub({ resolver, authorities }),
251
+ // authorities read via authoritiesKey (value identity), not reference.
252
+ // eslint-disable-next-line react-hooks/exhaustive-deps
253
+ [resolver, authoritiesKey]
254
+ );
255
+ useEffect2(() => () => hub.disposeAll(), [hub]);
256
+ return hub;
257
+ }
258
+ function useResolvedBindings(definition, base, selection, resolver) {
259
+ const scoped = useMemo(() => hasScopedSlots(definition), [definition]);
260
+ const overlaid = useMemo(() => ({ ...base, ...selection }), [base, selection]);
261
+ const [resolved, setResolved] = useState2(() => bindingsWithoutScopedSlots(definition, overlaid));
262
+ const genRef = useRef2(0);
263
+ useEffect2(() => {
264
+ if (!scoped) {
265
+ setResolved((prev) => sameBindings(prev, overlaid) ? prev : overlaid);
266
+ return;
267
+ }
268
+ const gen = ++genRef.current;
269
+ let live = true;
270
+ resolveContextBindings(definition, base, selection, resolver).then((next) => {
271
+ if (!live || gen !== genRef.current) return;
272
+ setResolved((prev) => sameBindings(prev, next) ? prev : next);
273
+ }).catch(() => {
274
+ });
275
+ return () => {
276
+ live = false;
277
+ };
278
+ }, [scoped, definition, base, selection, resolver, overlaid]);
279
+ return scoped ? resolved : overlaid;
280
+ }
281
+ function sameBindings(a, b) {
282
+ const ak = Object.keys(a).sort();
283
+ const bk = Object.keys(b).sort();
284
+ if (ak.length !== bk.length) return false;
285
+ for (let i = 0; i < ak.length; i += 1) {
286
+ if (ak[i] !== bk[i]) return false;
287
+ if (JSON.stringify(a[ak[i]]) !== JSON.stringify(b[bk[i]])) return false;
288
+ }
289
+ return true;
290
+ }
291
+ function useSlotCandidates(definition, bindings, resolver, lister) {
292
+ const bindingsKey = JSON.stringify(bindings);
293
+ return useMemo(
294
+ () => {
295
+ if (!lister) return void 0;
296
+ return (slot) => resolveSlotCandidates(definition, slot, bindings, resolver, lister);
297
+ },
298
+ // bindings read via bindingsKey (value identity), not reference.
299
+ // eslint-disable-next-line react-hooks/exhaustive-deps
300
+ [definition, bindingsKey, resolver, lister]
301
+ );
302
+ }
303
+ function useCandidates(slot) {
304
+ const provider = useWidgetCandidates();
305
+ const wired = !!provider;
306
+ const [state, setState] = useState2(() => ({
307
+ candidates: [],
308
+ loading: Boolean(provider && slot),
309
+ error: null
310
+ }));
311
+ const genRef = useRef2(0);
312
+ useEffect2(() => {
313
+ if (!provider || !slot) {
314
+ setState({ candidates: [], loading: false, error: null });
315
+ return;
316
+ }
317
+ const gen = ++genRef.current;
318
+ let live = true;
319
+ setState((prev) => ({ ...prev, loading: true, error: null }));
320
+ provider(slot).then((candidates) => {
321
+ if (live && gen === genRef.current) setState({ candidates, loading: false, error: null });
322
+ }).catch((error) => {
323
+ if (live && gen === genRef.current) setState({ candidates: [], loading: false, error });
324
+ });
325
+ return () => {
326
+ live = false;
327
+ };
328
+ }, [provider, slot]);
329
+ return { ...state, wired };
330
+ }
331
+ var EMPTY = { latest: {}, samples: [], error: null };
332
+ function latestOf(samples) {
333
+ const latest = {};
334
+ for (const s of samples) latest[s.name] = s;
335
+ return latest;
336
+ }
337
+ function useMeasurementStream(hub, datasource, options = {}) {
338
+ const windowSize = options.window ?? 300;
339
+ const initialSamples = options.initialSamples;
340
+ const key = datasource ? JSON.stringify(datasource) : null;
341
+ const [tagged, setTagged] = useState2({
342
+ key,
343
+ state: EMPTY
344
+ });
345
+ const live = tagged.key === key ? tagged.state : EMPTY;
346
+ useEffect2(() => {
347
+ setTagged({ key, state: EMPTY });
348
+ if (!datasource) return;
349
+ const update = (fn) => setTagged((prev) => prev.key === key ? { key, state: fn(prev.state) } : prev);
350
+ return hub.subscribeWidget(datasource, {
351
+ next: (sample) => update((prev) => {
352
+ const samples = prev.samples.concat(sample);
353
+ if (samples.length > windowSize) samples.splice(0, samples.length - windowSize);
354
+ return { latest: { ...prev.latest, [sample.name]: sample }, samples, error: null };
355
+ }),
356
+ error: (err) => update((prev) => ({ ...prev, error: err }))
357
+ });
358
+ }, [hub, key, windowSize]);
359
+ return useMemo(() => {
360
+ if (!initialSamples || initialSamples.length === 0) return live;
361
+ const merged = initialSamples.concat(live.samples);
362
+ const samples = merged.length > windowSize ? merged.slice(merged.length - windowSize) : merged;
363
+ return {
364
+ latest: { ...latestOf(initialSamples), ...live.latest },
365
+ samples,
366
+ error: live.error
367
+ };
368
+ }, [initialSamples, live, windowSize]);
369
+ }
370
+ var EMPTY_ALARMS = { alarms: [], total: 0, loading: true, error: null };
371
+ function useAlarmStream(hub, subscription) {
372
+ const [state, setState] = useState2(EMPTY_ALARMS);
373
+ const key = JSON.stringify(subscription);
374
+ useEffect2(() => {
375
+ setState(EMPTY_ALARMS);
376
+ return hub.subscribeAlarms(subscription, {
377
+ next: (snapshot) => setState({ alarms: snapshot.alarms, total: snapshot.total, loading: false, error: null }),
378
+ error: (err) => setState((prev) => ({ ...prev, loading: false, error: err }))
379
+ });
380
+ }, [hub, key]);
381
+ return state;
382
+ }
383
+ var EMPTY_COMMANDS = {
384
+ deviceToken: null,
385
+ commands: [],
386
+ total: 0,
387
+ loading: true,
388
+ error: null
389
+ };
390
+ function useCommandStream(hub, subscription) {
391
+ const [state, setState] = useState2(EMPTY_COMMANDS);
392
+ const key = JSON.stringify(subscription);
393
+ useEffect2(() => {
394
+ setState(EMPTY_COMMANDS);
395
+ return hub.subscribeCommands(subscription, {
396
+ next: (snapshot) => setState({
397
+ deviceToken: snapshot.deviceToken,
398
+ commands: snapshot.commands,
399
+ total: snapshot.total,
400
+ loading: false,
401
+ error: null
402
+ }),
403
+ error: (err) => setState((prev) => ({ ...prev, loading: false, error: err }))
404
+ });
405
+ }, [hub, key]);
406
+ return state;
407
+ }
408
+ var EMPTY_LOCATIONS = {
409
+ locations: [],
410
+ deviceTokens: [],
411
+ forbidden: false,
412
+ loading: true,
413
+ error: null
414
+ };
415
+ function useLocationStream(hub, subscription) {
416
+ const [state, setState] = useState2(EMPTY_LOCATIONS);
417
+ const key = JSON.stringify(subscription);
418
+ useEffect2(() => {
419
+ setState(EMPTY_LOCATIONS);
420
+ return hub.subscribeLocations(subscription, {
421
+ next: (snapshot) => setState(
422
+ snapshot.kind === "forbidden" ? { locations: [], deviceTokens: [], forbidden: true, loading: false, error: null } : {
423
+ locations: snapshot.locations,
424
+ deviceTokens: snapshot.deviceTokens,
425
+ forbidden: false,
426
+ loading: false,
427
+ error: null
428
+ }
429
+ ),
430
+ error: (err) => setState((prev) => ({ ...prev, loading: false, error: err }))
431
+ });
432
+ }, [hub, key]);
433
+ return state;
434
+ }
435
+ var AVAILABILITY_REVALIDATE_MS = 6e4;
436
+ function useDatasourceAvailability(hub, datasource) {
437
+ const [state, setState] = useState2("unknown");
438
+ const key = datasource ? JSON.stringify(datasource) : null;
439
+ useEffect2(() => {
440
+ if (!datasource) {
441
+ setState("available");
442
+ return;
443
+ }
444
+ setState("unknown");
445
+ let cancelled = false;
446
+ let timer;
447
+ const stopTimer = () => {
448
+ if (timer) {
449
+ clearInterval(timer);
450
+ timer = void 0;
451
+ }
452
+ };
453
+ const check = () => {
454
+ hub.isDatasourceAvailable(datasource).then((ok) => {
455
+ if (cancelled) return;
456
+ setState(ok ? "available" : "unavailable");
457
+ if (ok) stopTimer();
458
+ }).catch(() => {
459
+ if (cancelled) return;
460
+ setState("available");
461
+ stopTimer();
462
+ });
463
+ };
464
+ check();
465
+ timer = setInterval(check, AVAILABILITY_REVALIDATE_MS);
466
+ return () => {
467
+ cancelled = true;
468
+ stopTimer();
469
+ };
470
+ }, [hub, key]);
471
+ return state;
472
+ }
473
+ function useElementSize() {
474
+ const ref = useRef2(null);
475
+ const [size, setSize] = useState2({ width: 0, height: 0 });
476
+ useEffect2(() => {
477
+ const el = ref.current;
478
+ if (!el || typeof ResizeObserver === "undefined") return;
479
+ const observer = new ResizeObserver((entries) => {
480
+ const rect = entries[0]?.contentRect;
481
+ if (!rect) return;
482
+ setSize(
483
+ (prev) => prev.width === rect.width && prev.height === rect.height ? prev : { width: rect.width, height: rect.height }
484
+ );
485
+ });
486
+ observer.observe(el);
487
+ return () => observer.disconnect();
488
+ }, []);
489
+ return [ref, size];
490
+ }
491
+
492
+ // src/index.ts
493
+ import {
494
+ SDK_API_VERSION,
495
+ WidgetCatalogError as WidgetCatalogError2,
496
+ defineWidgetDescriptor,
497
+ isBuiltinWidgetId,
498
+ isNamespacedWidgetId,
499
+ validateWidgetDescriptor
500
+ } from "@devicechain/dashboards";
501
+ export {
502
+ FlashValue,
503
+ SDK_API_VERSION,
504
+ WidgetCandidatesProvider,
505
+ WidgetCatalogError2 as WidgetCatalogError,
506
+ WidgetFrame,
507
+ WidgetRegistryError,
508
+ WidgetSelectProvider,
509
+ WidgetSubjectProvider,
510
+ WidgetThemeRootProvider,
511
+ WidgetTimeZoneProvider,
512
+ createWidgetRegistry,
513
+ css,
514
+ defineWidget,
515
+ defineWidgetDescriptor,
516
+ flashBackgroundStyle,
517
+ flashTextStyle,
518
+ formatDateTime,
519
+ formatTimestamp,
520
+ formatValue,
521
+ isBuiltinWidgetId,
522
+ isNamespacedWidgetId,
523
+ optBoolean,
524
+ optNumber,
525
+ optString,
526
+ pickSample,
527
+ primaryMeasurementName,
528
+ useAlarmStream,
529
+ useCandidates,
530
+ useCommandStream,
531
+ useDashboardHub,
532
+ useDatasourceAvailability,
533
+ useElementSize,
534
+ useFlashOnChange,
535
+ useLocationStream,
536
+ useMeasurementStream,
537
+ useResolvedBindings,
538
+ useSlotCandidates,
539
+ useWidgetCandidates,
540
+ useWidgetSelect,
541
+ useWidgetSubject,
542
+ useWidgetThemeRoot,
543
+ useWidgetTimeZone,
544
+ validateWidgetDescriptor
545
+ };
546
+ //# sourceMappingURL=index.js.map