@watt-ui/react 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +43 -0
  3. package/README.md +48 -2
  4. package/dist/chart/data.d.ts +13 -0
  5. package/dist/chart/data.d.ts.map +1 -0
  6. package/dist/chart/data.js +92 -0
  7. package/dist/chart/data.js.map +1 -0
  8. package/dist/chart/engine.d.ts +2 -0
  9. package/dist/chart/engine.d.ts.map +1 -0
  10. package/dist/chart/engine.js +16 -0
  11. package/dist/chart/engine.js.map +1 -0
  12. package/dist/chart/options.d.ts +18 -0
  13. package/dist/chart/options.d.ts.map +1 -0
  14. package/dist/chart/options.js +136 -0
  15. package/dist/chart/options.js.map +1 -0
  16. package/dist/components/alarm-feed.d.ts +18 -0
  17. package/dist/components/alarm-feed.d.ts.map +1 -0
  18. package/dist/components/alarm-feed.js +45 -0
  19. package/dist/components/alarm-feed.js.map +1 -0
  20. package/dist/components/asset-tree.d.ts +33 -0
  21. package/dist/components/asset-tree.d.ts.map +1 -0
  22. package/dist/components/asset-tree.js +149 -0
  23. package/dist/components/asset-tree.js.map +1 -0
  24. package/dist/components/battery-soc.d.ts +11 -0
  25. package/dist/components/battery-soc.d.ts.map +1 -0
  26. package/dist/components/battery-soc.js +34 -0
  27. package/dist/components/battery-soc.js.map +1 -0
  28. package/dist/components/data-freshness.d.ts +26 -0
  29. package/dist/components/data-freshness.d.ts.map +1 -0
  30. package/dist/components/data-freshness.js +45 -0
  31. package/dist/components/data-freshness.js.map +1 -0
  32. package/dist/components/energy-flow.d.ts +39 -0
  33. package/dist/components/energy-flow.d.ts.map +1 -0
  34. package/dist/components/energy-flow.js +197 -0
  35. package/dist/components/energy-flow.js.map +1 -0
  36. package/dist/components/flow-geometry.d.ts +24 -0
  37. package/dist/components/flow-geometry.d.ts.map +1 -0
  38. package/dist/components/flow-geometry.js +57 -0
  39. package/dist/components/flow-geometry.js.map +1 -0
  40. package/dist/components/power-metric.d.ts +14 -0
  41. package/dist/components/power-metric.d.ts.map +1 -0
  42. package/dist/components/power-metric.js +25 -0
  43. package/dist/components/power-metric.js.map +1 -0
  44. package/dist/components/status-indicator.d.ts +10 -0
  45. package/dist/components/status-indicator.d.ts.map +1 -0
  46. package/dist/components/status-indicator.js +15 -0
  47. package/dist/components/status-indicator.js.map +1 -0
  48. package/dist/components/telemetry-chart.d.ts +50 -0
  49. package/dist/components/telemetry-chart.d.ts.map +1 -0
  50. package/dist/components/telemetry-chart.js +256 -0
  51. package/dist/components/telemetry-chart.js.map +1 -0
  52. package/dist/domain.d.ts +6 -0
  53. package/dist/domain.d.ts.map +1 -0
  54. package/dist/domain.js +43 -0
  55. package/dist/domain.js.map +1 -0
  56. package/dist/icons/index.d.ts +7 -0
  57. package/dist/icons/index.d.ts.map +1 -0
  58. package/dist/icons/index.js +38 -0
  59. package/dist/icons/index.js.map +1 -0
  60. package/dist/index.d.ts +19 -0
  61. package/dist/index.d.ts.map +1 -0
  62. package/dist/index.js +10 -0
  63. package/dist/index.js.map +1 -0
  64. package/dist/internal.d.ts +6 -0
  65. package/dist/internal.d.ts.map +1 -0
  66. package/dist/internal.js +45 -0
  67. package/dist/internal.js.map +1 -0
  68. package/dist/server.d.ts +10 -0
  69. package/dist/server.d.ts.map +1 -0
  70. package/dist/server.js +32 -0
  71. package/dist/server.js.map +1 -0
  72. package/dist/styles.css +134 -0
  73. package/dist/theme/shadcn.css +14 -0
  74. package/dist/types.d.ts +61 -0
  75. package/dist/types.d.ts.map +1 -0
  76. package/dist/types.js +2 -0
  77. package/dist/types.js.map +1 -0
  78. package/package.json +118 -4
  79. package/src/chart/data.ts +103 -0
  80. package/src/chart/engine.ts +19 -0
  81. package/src/chart/options.ts +178 -0
  82. package/src/components/alarm-feed.tsx +126 -0
  83. package/src/components/asset-tree.tsx +258 -0
  84. package/src/components/battery-soc.tsx +84 -0
  85. package/src/components/data-freshness.tsx +109 -0
  86. package/src/components/energy-flow.tsx +387 -0
  87. package/src/components/flow-geometry.ts +82 -0
  88. package/src/components/power-metric.tsx +71 -0
  89. package/src/components/status-indicator.tsx +31 -0
  90. package/src/components/telemetry-chart.tsx +476 -0
  91. package/src/domain.ts +64 -0
  92. package/src/icons/index.tsx +49 -0
  93. package/src/index.ts +37 -0
  94. package/src/internal.tsx +57 -0
  95. package/src/server.ts +47 -0
  96. package/src/styles.css +134 -0
  97. package/src/theme/shadcn.css +14 -0
  98. package/src/types.ts +64 -0
@@ -0,0 +1,476 @@
1
+ "use client";
2
+ import { useComposedRefs } from "@radix-ui/react-compose-refs";
3
+ import { Slot } from "@radix-ui/react-slot";
4
+ import {
5
+ createContext,
6
+ forwardRef,
7
+ useCallback,
8
+ useContext,
9
+ useEffect,
10
+ useMemo,
11
+ useRef,
12
+ useState,
13
+ } from "react";
14
+ import type { EChartsCoreOption, EChartsType } from "echarts/core";
15
+ import { cx, part } from "../internal.js";
16
+ import { nearestIndex, prepareSeries, lowerBound } from "../chart/data.js";
17
+ import {
18
+ chartOption,
19
+ hasManagedConflict,
20
+ type ChartConfig,
21
+ } from "../chart/options.js";
22
+ import type {
23
+ RootProps,
24
+ TelemetrySeries,
25
+ TimeRange,
26
+ DataQuality,
27
+ } from "../types.js";
28
+ export interface ChartInspection {
29
+ timestamp: number;
30
+ values: {
31
+ id: string;
32
+ label: string;
33
+ unit: string;
34
+ timestamp: number;
35
+ value: number | null;
36
+ quality?: DataQuality;
37
+ }[];
38
+ }
39
+ export interface TelemetryChartProps extends Omit<RootProps, "onError"> {
40
+ series: readonly TelemetrySeries[];
41
+ timeZone: string;
42
+ locale?: string;
43
+ range?: TimeRange;
44
+ onRangeChange?: (range: TimeRange) => void;
45
+ sampling?: "minmax" | "none";
46
+ options?: EChartsCoreOption;
47
+ events?: Readonly<
48
+ Record<string, (event: unknown, chart: EChartsType) => void>
49
+ >;
50
+ onReady?: (chart: EChartsType) => void;
51
+ onError?: (error: Error) => void;
52
+ }
53
+ interface ChartContext extends ChartConfig {
54
+ inspection: ChartInspection | null;
55
+ inspect: (timestamp: number | null) => void;
56
+ hidden: Set<string>;
57
+ toggle: (id: string) => void;
58
+ onRangeChange?: TelemetryChartProps["onRangeChange"];
59
+ events?: TelemetryChartProps["events"];
60
+ onReady?: TelemetryChartProps["onReady"];
61
+ onError?: TelemetryChartProps["onError"];
62
+ }
63
+ const Context = createContext<ChartContext | null>(null);
64
+ function useChart() {
65
+ const ctx = useContext(Context);
66
+ if (!ctx) throw new Error("Chart parts require TelemetryChart.Root");
67
+ return ctx;
68
+ }
69
+ const Root = forwardRef<HTMLDivElement, TelemetryChartProps>(
70
+ (
71
+ {
72
+ series,
73
+ timeZone,
74
+ locale,
75
+ range,
76
+ onRangeChange,
77
+ sampling = "minmax",
78
+ options,
79
+ events,
80
+ onReady,
81
+ onError,
82
+ density = "comfortable",
83
+ asChild,
84
+ children,
85
+ className,
86
+ ...props
87
+ },
88
+ ref,
89
+ ) => {
90
+ const prepared = useMemo(() => prepareSeries(series), [series]);
91
+ const [hidden, setHidden] = useState<Set<string>>(() => new Set());
92
+ const [timestamp, setTimestamp] = useState<number | null>(null);
93
+ const inspect = useCallback(
94
+ (time: number | null) => setTimestamp(time),
95
+ [],
96
+ );
97
+ const toggle = useCallback(
98
+ (id: string) =>
99
+ setHidden((old) => {
100
+ const next = new Set(old);
101
+ if (next.has(id)) next.delete(id);
102
+ else next.add(id);
103
+ return next;
104
+ }),
105
+ [],
106
+ );
107
+ const inspection = useMemo<ChartInspection | null>(
108
+ () =>
109
+ timestamp === null
110
+ ? null
111
+ : {
112
+ timestamp,
113
+ values: prepared
114
+ .filter((s) => !hidden.has(s.id))
115
+ .flatMap((s) => {
116
+ const index = nearestIndex(s.points, timestamp);
117
+ const p = s.points[index];
118
+ return p
119
+ ? [
120
+ {
121
+ id: s.id,
122
+ label: s.label,
123
+ unit: s.unit,
124
+ timestamp: p[0],
125
+ value: p[1],
126
+ quality: s.sourcePoints[index]?.quality,
127
+ },
128
+ ]
129
+ : [];
130
+ }),
131
+ },
132
+ [timestamp, prepared, hidden],
133
+ );
134
+ useEffect(() => {
135
+ if (
136
+ hasManagedConflict({ series: prepared, range, timeZone, options }) &&
137
+ process.env.NODE_ENV !== "production"
138
+ )
139
+ console.warn(
140
+ "Watt UI: explicit chart props override series, axes, zoom, tooltip and legend options.",
141
+ );
142
+ }, [options, prepared, range, timeZone]);
143
+ const Component = asChild ? Slot : "div";
144
+ return (
145
+ <Context.Provider
146
+ value={{
147
+ series: prepared,
148
+ timeZone,
149
+ locale,
150
+ range,
151
+ onRangeChange,
152
+ sampling,
153
+ options,
154
+ events,
155
+ inspection,
156
+ inspect,
157
+ hidden,
158
+ toggle,
159
+ onReady,
160
+ onError,
161
+ }}
162
+ >
163
+ <Component
164
+ {...props}
165
+ ref={ref}
166
+ data-slot="telemetry-chart"
167
+ data-density={density}
168
+ className={cx("watt-root min-w-0", className)}
169
+ >
170
+ {children}
171
+ </Component>
172
+ </Context.Provider>
173
+ );
174
+ },
175
+ );
176
+ export interface ChartCanvasProps extends React.HTMLAttributes<HTMLDivElement> {
177
+ /** Trusted output of renderTelemetrySVG, never untrusted HTML. */
178
+ previewSvg?: string;
179
+ height?: number;
180
+ }
181
+ const Canvas = forwardRef<HTMLDivElement, ChartCanvasProps>(
182
+ (
183
+ { previewSvg, height = 300, className, style, onKeyDown, ...props },
184
+ forwardedRef,
185
+ ) => {
186
+ const ctx = useChart();
187
+ const ctxRef = useRef(ctx);
188
+ ctxRef.current = ctx;
189
+ const host = useRef<HTMLDivElement>(null);
190
+ const instance = useRef<EChartsType | null>(null);
191
+ const localRange = useRef<TimeRange | undefined>(undefined);
192
+ const previousRange = useRef(ctx.range);
193
+ const bindings = useRef(new Map<string, (event: unknown) => void>());
194
+ const [ready, setReady] = useState(false);
195
+ const [error, setError] = useState<Error | null>(null);
196
+ const canvasRef = useComposedRefs(host, forwardedRef);
197
+ const render = useRef<() => void>(() => {});
198
+ useEffect(() => {
199
+ let alive = true,
200
+ frame = 0,
201
+ firstRendered = false;
202
+ let observer: ResizeObserver | undefined;
203
+ import("../chart/engine.js")
204
+ .then(({ initCanvas }) => {
205
+ if (!alive || !host.current) return;
206
+ const chart = initCanvas(host.current);
207
+ instance.current = chart;
208
+ render.current = () => {
209
+ cancelAnimationFrame(frame);
210
+ frame = requestAnimationFrame(() => {
211
+ if (!alive || !host.current) return;
212
+ try {
213
+ const current = ctxRef.current;
214
+ const styles = getComputedStyle(host.current);
215
+ chart.resize({ silent: true });
216
+ chart.setOption(
217
+ chartOption(
218
+ {
219
+ ...current,
220
+ range: current.range ?? localRange.current,
221
+ series: current.series.filter(
222
+ (s) => !current.hidden.has(s.id),
223
+ ),
224
+ },
225
+ host.current.clientWidth,
226
+ {
227
+ text:
228
+ styles.getPropertyValue("--watt-muted").trim() ||
229
+ "#667085",
230
+ border:
231
+ styles.getPropertyValue("--watt-border").trim() ||
232
+ "#e7e9ee",
233
+ accent:
234
+ styles.getPropertyValue("--watt-accent").trim() ||
235
+ "#355dff",
236
+ },
237
+ ),
238
+ { notMerge: true, lazyUpdate: false },
239
+ );
240
+ setReady(true);
241
+ if (!firstRendered) {
242
+ firstRendered = true;
243
+ ctxRef.current.onReady?.(chart);
244
+ }
245
+ } catch (reason) {
246
+ const failure =
247
+ reason instanceof Error ? reason : new Error(String(reason));
248
+ setError(failure);
249
+ ctxRef.current.onError?.(failure);
250
+ }
251
+ });
252
+ };
253
+ chart.on("updateAxisPointer", (event: unknown) => {
254
+ const e = event as { axesInfo?: { value: number }[] };
255
+ const value = e.axesInfo?.[0]?.value;
256
+ if (value !== undefined) ctxRef.current.inspect(value);
257
+ });
258
+ chart.on("datazoom", () => {
259
+ const zoom = (
260
+ chart.getOption() as {
261
+ dataZoom?: { startValue?: number; endValue?: number }[];
262
+ }
263
+ ).dataZoom?.[0];
264
+ if (
265
+ typeof zoom?.startValue === "number" &&
266
+ typeof zoom.endValue === "number" &&
267
+ zoom.startValue < zoom.endValue
268
+ ) {
269
+ localRange.current = [zoom.startValue, zoom.endValue];
270
+ ctxRef.current.onRangeChange?.(localRange.current);
271
+ }
272
+ });
273
+ for (const name of Object.keys(ctxRef.current.events ?? {})) {
274
+ const handler = (event: unknown) =>
275
+ ctxRef.current.events?.[name]?.(event, chart);
276
+ bindings.current.set(name, handler);
277
+ chart.on(name, handler);
278
+ }
279
+ observer = new ResizeObserver(() => render.current());
280
+ observer.observe(host.current);
281
+ render.current();
282
+ })
283
+ .catch((reason) => {
284
+ if (alive) {
285
+ const failure =
286
+ reason instanceof Error ? reason : new Error(String(reason));
287
+ setError(failure);
288
+ ctxRef.current.onError?.(failure);
289
+ }
290
+ });
291
+ return () => {
292
+ alive = false;
293
+ cancelAnimationFrame(frame);
294
+ observer?.disconnect();
295
+ instance.current?.dispose();
296
+ instance.current = null;
297
+ bindings.current.clear();
298
+ render.current = () => {};
299
+ };
300
+ }, []);
301
+ useEffect(() => {
302
+ if (previousRange.current !== ctx.range) {
303
+ if (ctx.range === undefined) localRange.current = undefined;
304
+ previousRange.current = ctx.range;
305
+ }
306
+ render.current();
307
+ }, [
308
+ ctx.series,
309
+ ctx.range,
310
+ ctx.sampling,
311
+ ctx.options,
312
+ ctx.hidden,
313
+ ctx.timeZone,
314
+ ctx.locale,
315
+ ]);
316
+ const eventNames = Object.keys(ctx.events ?? {})
317
+ .sort()
318
+ .join("\0");
319
+ useEffect(() => {
320
+ const chart = instance.current;
321
+ if (!chart || !ready) return;
322
+ const names = new Set(eventNames ? eventNames.split("\0") : []);
323
+ for (const [name, handler] of bindings.current)
324
+ if (!names.has(name)) {
325
+ chart.off(name, handler);
326
+ bindings.current.delete(name);
327
+ }
328
+ for (const name of names)
329
+ if (!bindings.current.has(name)) {
330
+ const handler = (event: unknown) =>
331
+ ctxRef.current.events?.[name]?.(event, chart);
332
+ bindings.current.set(name, handler);
333
+ chart.on(name, handler);
334
+ }
335
+ }, [eventNames, ready]);
336
+ return (
337
+ <div
338
+ data-slot="telemetry-chart-canvas-wrapper"
339
+ className="relative min-w-0"
340
+ style={{ height }}
341
+ >
342
+ {!ready && previewSvg && (
343
+ <div
344
+ aria-hidden="true"
345
+ className="watt-chart-preview absolute inset-0"
346
+ dangerouslySetInnerHTML={{ __html: previewSvg }}
347
+ />
348
+ )}
349
+ <div
350
+ {...props}
351
+ ref={canvasRef}
352
+ role="img"
353
+ tabIndex={0}
354
+ aria-busy={!ready && !error}
355
+ data-slot="telemetry-chart-canvas"
356
+ data-error={!!error || undefined}
357
+ className={cx(
358
+ "h-full w-full outline-offset-4 focus-visible:outline-2 focus-visible:outline-[var(--watt-accent)]",
359
+ className,
360
+ )}
361
+ style={style}
362
+ onKeyDown={(event) => {
363
+ onKeyDown?.(event);
364
+ if (event.defaultPrevented) return;
365
+ const points = ctx.series.find(
366
+ (s) => !ctx.hidden.has(s.id),
367
+ )?.points;
368
+ if (!points?.length) return;
369
+ if (
370
+ ["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)
371
+ ) {
372
+ event.preventDefault();
373
+ const i = ctx.inspection
374
+ ? lowerBound(points, ctx.inspection.timestamp)
375
+ : -1;
376
+ const index =
377
+ event.key === "Home"
378
+ ? 0
379
+ : event.key === "End"
380
+ ? points.length - 1
381
+ : Math.max(
382
+ 0,
383
+ Math.min(
384
+ points.length - 1,
385
+ i + (event.key === "ArrowRight" ? 1 : -1),
386
+ ),
387
+ );
388
+ const time = points[index]![0];
389
+ ctx.inspect(time);
390
+ instance.current?.dispatchAction({
391
+ type: "showTip",
392
+ x: instance.current.convertToPixel({ xAxisIndex: 0 }, time),
393
+ y: 50,
394
+ });
395
+ } else if (event.key === "Escape") ctx.inspect(null);
396
+ }}
397
+ />
398
+ </div>
399
+ );
400
+ },
401
+ );
402
+ const Legend = forwardRef<
403
+ HTMLDivElement,
404
+ Omit<React.HTMLAttributes<HTMLDivElement>, "children"> & {
405
+ children?: (series: {
406
+ id: string;
407
+ label: string;
408
+ color?: string;
409
+ hidden: boolean;
410
+ toggle: () => void;
411
+ }) => React.ReactNode;
412
+ }
413
+ >(({ children, className, ...props }, ref) => {
414
+ const ctx = useChart();
415
+ return (
416
+ <div
417
+ {...props}
418
+ ref={ref}
419
+ data-slot="telemetry-chart-legend"
420
+ className={cx("flex flex-wrap gap-4 text-sm", className)}
421
+ >
422
+ {ctx.series.map((s) => (
423
+ <span key={s.id}>
424
+ {children ? (
425
+ children({
426
+ ...s,
427
+ hidden: ctx.hidden.has(s.id),
428
+ toggle: () => ctx.toggle(s.id),
429
+ })
430
+ ) : (
431
+ <button
432
+ type="button"
433
+ aria-pressed={!ctx.hidden.has(s.id)}
434
+ onClick={() => ctx.toggle(s.id)}
435
+ className="inline-flex items-center gap-2"
436
+ >
437
+ <span
438
+ className="h-2 w-2 rounded-full"
439
+ style={{ background: s.color ?? "var(--watt-accent)" }}
440
+ />
441
+ {s.label}
442
+ </button>
443
+ )}
444
+ </span>
445
+ ))}
446
+ </div>
447
+ );
448
+ });
449
+ function Tooltip({
450
+ children,
451
+ ...props
452
+ }: Omit<React.HTMLAttributes<HTMLDivElement>, "children"> & {
453
+ children: (inspection: ChartInspection | null) => React.ReactNode;
454
+ }) {
455
+ const { inspection } = useChart();
456
+ return (
457
+ <div
458
+ {...props}
459
+ data-slot="telemetry-chart-tooltip"
460
+ aria-live={props["aria-live"] ?? "polite"}
461
+ aria-atomic={props["aria-atomic"] ?? true}
462
+ >
463
+ {children(inspection)}
464
+ </div>
465
+ );
466
+ }
467
+ export const TelemetryChart = {
468
+ Root,
469
+ Canvas,
470
+ Legend,
471
+ Tooltip,
472
+ Description: part(
473
+ "telemetry-chart-description",
474
+ "text-sm text-[var(--watt-muted)]",
475
+ ),
476
+ };
package/src/domain.ts ADDED
@@ -0,0 +1,64 @@
1
+ import type { Timestamp } from "./types.js";
2
+
3
+ /** Strings require an explicit ISO timezone; numbers are Unix milliseconds. */
4
+ export function parseTimestamp(
5
+ value: Timestamp | null | undefined,
6
+ ): number | null {
7
+ if (typeof value === "number")
8
+ return Number.isFinite(value) && Math.abs(value) <= 8.64e15 ? value : null;
9
+ if (
10
+ !value ||
11
+ !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(
12
+ value,
13
+ )
14
+ )
15
+ return null;
16
+ const [year, month, day] = value.slice(0, 10).split("-").map(Number) as [
17
+ number,
18
+ number,
19
+ number,
20
+ ];
21
+ const days =
22
+ [
23
+ 31,
24
+ year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28,
25
+ 31,
26
+ 30,
27
+ 31,
28
+ 30,
29
+ 31,
30
+ 31,
31
+ 30,
32
+ 31,
33
+ 30,
34
+ 31,
35
+ ][month - 1] ?? 0;
36
+ if (month < 1 || month > 12 || day < 1 || day > days) return null;
37
+ const result = Date.parse(value);
38
+ return Number.isFinite(result) ? result : null;
39
+ }
40
+ export function formatNumber(
41
+ value: number | null | undefined,
42
+ locale = "en-US",
43
+ digits = 1,
44
+ ): string {
45
+ return value == null || !Number.isFinite(value)
46
+ ? "—"
47
+ : new Intl.NumberFormat(locale, { maximumFractionDigits: digits }).format(
48
+ value,
49
+ );
50
+ }
51
+ export function formatTime(
52
+ value: Timestamp,
53
+ timeZone: string,
54
+ locale = "en-GB",
55
+ ): string {
56
+ const time = parseTimestamp(value);
57
+ return time === null
58
+ ? "—"
59
+ : new Intl.DateTimeFormat(locale, {
60
+ timeZone,
61
+ hour: "2-digit",
62
+ minute: "2-digit",
63
+ }).format(time);
64
+ }
@@ -0,0 +1,49 @@
1
+ import { createLucideIcon } from "lucide-react";
2
+ export {
3
+ SolarPanel,
4
+ Sun,
5
+ Battery,
6
+ BatteryCharging,
7
+ UtilityPole,
8
+ Factory,
9
+ House,
10
+ CircuitBoard,
11
+ PlugZap,
12
+ Zap,
13
+ } from "lucide-react";
14
+ export type { LucideProps as EnergyIconProps } from "lucide-react";
15
+ export const Inverter = createLucideIcon("inverter", [
16
+ ["rect", { x: "5", y: "3", width: "14", height: "18", rx: "2", key: "body" }],
17
+ [
18
+ "path",
19
+ {
20
+ d: "M8 7h3m-3 3h3m3-3h2m-2 3h2M8 16c1-3 2 3 3 0s2 3 3 0 2 0 2 0",
21
+ key: "controls",
22
+ },
23
+ ],
24
+ ]);
25
+ export const Busbar = createLucideIcon("busbar", [
26
+ ["path", { d: "M3 6h18M5 6v9m7-9v9m7-9v9", key: "bus" }],
27
+ ["circle", { cx: "5", cy: "18", r: "2", key: "a" }],
28
+ ["circle", { cx: "12", cy: "18", r: "2", key: "b" }],
29
+ ["circle", { cx: "19", cy: "18", r: "2", key: "c" }],
30
+ ]);
31
+ export const BatteryRack = createLucideIcon("battery-rack", [
32
+ ["rect", { x: "5", y: "3", width: "14", height: "18", rx: "2", key: "rack" }],
33
+ [
34
+ "path",
35
+ {
36
+ d: "M5 9h14M5 15h14M8 6h2m-2 6h2m-2 6h2m6-12h.01M16 12h.01M16 18h.01",
37
+ key: "modules",
38
+ },
39
+ ],
40
+ ]);
41
+ export const Transformer = createLucideIcon("transformer", [
42
+ [
43
+ "path",
44
+ {
45
+ d: "M7 3v3m10-3v3M7 18v3m10-3v3M10 6H7a3 3 0 0 0 0 6 3 3 0 0 0 0 6h3m4-12h3a3 3 0 0 1 0 6 3 3 0 0 1 0 6h-3M12 5v14",
46
+ key: "windings",
47
+ },
48
+ ],
49
+ ]);
package/src/index.ts ADDED
@@ -0,0 +1,37 @@
1
+ export { PowerMetric } from "./components/power-metric.js";
2
+ export { StatusIndicator } from "./components/status-indicator.js";
3
+ export { DataFreshness } from "./components/data-freshness.js";
4
+ export { BatterySOC } from "./components/battery-soc.js";
5
+ export { TelemetryChart } from "./components/telemetry-chart.js";
6
+ export { EnergyFlow } from "./components/energy-flow.js";
7
+ export { AlarmFeed } from "./components/alarm-feed.js";
8
+ export { AssetTree } from "./components/asset-tree.js";
9
+ export { formatNumber, formatTime, parseTimestamp } from "./domain.js";
10
+ export type * from "./types.js";
11
+ export type { MetricValueProps } from "./components/power-metric.js";
12
+ export type { StatusIndicatorProps } from "./components/status-indicator.js";
13
+ export type { BatterySOCProps } from "./components/battery-soc.js";
14
+ export type {
15
+ DataFreshnessProps,
16
+ FreshnessTimeProps,
17
+ FreshnessElapsedProps,
18
+ } from "./components/data-freshness.js";
19
+ export type {
20
+ TelemetryChartProps,
21
+ ChartCanvasProps,
22
+ ChartInspection,
23
+ } from "./components/telemetry-chart.js";
24
+ export type {
25
+ EnergyFlowProps,
26
+ FlowAnimation,
27
+ FlowStyle,
28
+ FlowNodeProps,
29
+ FlowPortProps,
30
+ FlowEdgeProps,
31
+ } from "./components/energy-flow.js";
32
+ export type { AlarmFeedProps } from "./components/alarm-feed.js";
33
+ export type {
34
+ AssetTreeProps,
35
+ AssetTreeRow,
36
+ AssetTreeItem,
37
+ } from "./components/asset-tree.js";
@@ -0,0 +1,57 @@
1
+ "use client";
2
+ import { forwardRef, useSyncExternalStore } from "react";
3
+ import { Slot } from "@radix-ui/react-slot";
4
+ import { clsx, type ClassValue } from "clsx";
5
+ import { twMerge } from "tailwind-merge";
6
+ import type { PartProps } from "./types.js";
7
+ export function cx(...values: ClassValue[]) {
8
+ return twMerge(clsx(values));
9
+ }
10
+ export function part(slot: string, defaults = "", tag: "div" | "span" = "div") {
11
+ const Part = forwardRef<HTMLElement, PartProps>(
12
+ ({ asChild, className, ...props }, ref) => {
13
+ const Component = asChild ? Slot : tag;
14
+ return (
15
+ <Component
16
+ {...props}
17
+ ref={ref as never}
18
+ data-slot={slot}
19
+ className={cx(defaults, className)}
20
+ />
21
+ );
22
+ },
23
+ );
24
+ Part.displayName = slot;
25
+ return Part;
26
+ }
27
+ let instant: number | null = null;
28
+ let timer: ReturnType<typeof setInterval> | undefined;
29
+ const listeners = new Set<() => void>();
30
+ function subscribe(listener: () => void) {
31
+ listeners.add(listener);
32
+ if (!timer) {
33
+ instant = Date.now();
34
+ timer = setInterval(() => {
35
+ instant = Date.now();
36
+ listeners.forEach((fn) => fn());
37
+ }, 1000);
38
+ }
39
+ return () => {
40
+ listeners.delete(listener);
41
+ if (!listeners.size) {
42
+ clearInterval(timer);
43
+ timer = undefined;
44
+ instant = null;
45
+ }
46
+ };
47
+ }
48
+ const snapshot = () => instant;
49
+ const serverSnapshot = () => null;
50
+ const noopSubscribe = () => () => {};
51
+ export function useNow(enabled: boolean) {
52
+ return useSyncExternalStore(
53
+ enabled ? subscribe : noopSubscribe,
54
+ snapshot,
55
+ serverSnapshot,
56
+ );
57
+ }