@watt-ui/react 0.1.3 → 0.2.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.
Files changed (102) hide show
  1. package/NOTICE +4 -0
  2. package/README.md +9 -9
  3. package/dist/chart/category-engine.d.ts +2 -0
  4. package/dist/chart/category-engine.d.ts.map +1 -0
  5. package/dist/chart/category-engine.js +9 -0
  6. package/dist/chart/category-engine.js.map +1 -0
  7. package/dist/chart/category-options.d.ts +23 -0
  8. package/dist/chart/category-options.d.ts.map +1 -0
  9. package/dist/chart/category-options.js +120 -0
  10. package/dist/chart/category-options.js.map +1 -0
  11. package/dist/components/alarm-feed.d.ts.map +1 -1
  12. package/dist/components/alarm-feed.js +1 -1
  13. package/dist/components/alarm-feed.js.map +1 -1
  14. package/dist/components/asset-tree.d.ts.map +1 -1
  15. package/dist/components/asset-tree.js +16 -2
  16. package/dist/components/asset-tree.js.map +1 -1
  17. package/dist/components/battery-soc.d.ts.map +1 -1
  18. package/dist/components/battery-soc.js +5 -1
  19. package/dist/components/battery-soc.js.map +1 -1
  20. package/dist/components/category-chart.d.ts +37 -0
  21. package/dist/components/category-chart.d.ts.map +1 -0
  22. package/dist/components/category-chart.js +251 -0
  23. package/dist/components/category-chart.js.map +1 -0
  24. package/dist/components/energy-flow.d.ts +2 -1
  25. package/dist/components/energy-flow.d.ts.map +1 -1
  26. package/dist/components/energy-flow.js +2 -2
  27. package/dist/components/energy-flow.js.map +1 -1
  28. package/dist/components/equipment-card.d.ts +12 -0
  29. package/dist/components/equipment-card.d.ts.map +1 -0
  30. package/dist/components/equipment-card.js +18 -0
  31. package/dist/components/equipment-card.js.map +1 -0
  32. package/dist/components/equipment-heatmap.d.ts +33 -0
  33. package/dist/components/equipment-heatmap.d.ts.map +1 -0
  34. package/dist/components/equipment-heatmap.js +161 -0
  35. package/dist/components/equipment-heatmap.js.map +1 -0
  36. package/dist/components/equipment-table.d.ts +34 -0
  37. package/dist/components/equipment-table.d.ts.map +1 -0
  38. package/dist/components/equipment-table.js +141 -0
  39. package/dist/components/equipment-table.js.map +1 -0
  40. package/dist/components/metric-trend.d.ts +13 -0
  41. package/dist/components/metric-trend.d.ts.map +1 -0
  42. package/dist/components/metric-trend.js +30 -0
  43. package/dist/components/metric-trend.js.map +1 -0
  44. package/dist/components/operating-timeline.d.ts +31 -0
  45. package/dist/components/operating-timeline.d.ts.map +1 -0
  46. package/dist/components/operating-timeline.js +61 -0
  47. package/dist/components/operating-timeline.js.map +1 -0
  48. package/dist/components/plant-fleet-map.d.ts +32 -0
  49. package/dist/components/plant-fleet-map.d.ts.map +1 -0
  50. package/dist/components/plant-fleet-map.js +56 -0
  51. package/dist/components/plant-fleet-map.js.map +1 -0
  52. package/dist/components/threshold-gauge.d.ts +19 -0
  53. package/dist/components/threshold-gauge.d.ts.map +1 -0
  54. package/dist/components/threshold-gauge.js +54 -0
  55. package/dist/components/threshold-gauge.js.map +1 -0
  56. package/dist/components/time-range-selector.d.ts +16 -0
  57. package/dist/components/time-range-selector.d.ts.map +1 -0
  58. package/dist/components/time-range-selector.js +47 -0
  59. package/dist/components/time-range-selector.js.map +1 -0
  60. package/dist/icons/index.d.ts +17 -1
  61. package/dist/icons/index.d.ts.map +1 -1
  62. package/dist/icons/index.js +98 -1
  63. package/dist/icons/index.js.map +1 -1
  64. package/dist/index.d.ts +19 -0
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +9 -0
  67. package/dist/index.js.map +1 -1
  68. package/dist/map/land.d.ts +2 -0
  69. package/dist/map/land.d.ts.map +1 -0
  70. package/dist/map/land.js +3 -0
  71. package/dist/map/land.js.map +1 -0
  72. package/dist/server.d.ts +5 -0
  73. package/dist/server.d.ts.map +1 -1
  74. package/dist/server.js +21 -1
  75. package/dist/server.js.map +1 -1
  76. package/dist/styles.css +133 -4
  77. package/dist/virtual-focus.d.ts +3 -0
  78. package/dist/virtual-focus.d.ts.map +1 -0
  79. package/dist/virtual-focus.js +23 -0
  80. package/dist/virtual-focus.js.map +1 -0
  81. package/package.json +37 -1
  82. package/src/chart/category-engine.ts +8 -0
  83. package/src/chart/category-options.ts +158 -0
  84. package/src/components/alarm-feed.tsx +7 -2
  85. package/src/components/asset-tree.tsx +21 -5
  86. package/src/components/battery-soc.tsx +8 -0
  87. package/src/components/category-chart.tsx +452 -0
  88. package/src/components/energy-flow.tsx +3 -1
  89. package/src/components/equipment-card.tsx +46 -0
  90. package/src/components/equipment-heatmap.tsx +298 -0
  91. package/src/components/equipment-table.tsx +302 -0
  92. package/src/components/metric-trend.tsx +87 -0
  93. package/src/components/operating-timeline.tsx +182 -0
  94. package/src/components/plant-fleet-map.tsx +173 -0
  95. package/src/components/threshold-gauge.tsx +152 -0
  96. package/src/components/time-range-selector.tsx +126 -0
  97. package/src/icons/index.tsx +107 -0
  98. package/src/index.ts +44 -0
  99. package/src/map/land.ts +2 -0
  100. package/src/server.ts +28 -1
  101. package/src/styles.css +133 -4
  102. package/src/virtual-focus.ts +27 -0
@@ -0,0 +1,452 @@
1
+ "use client";
2
+ import {
3
+ createContext,
4
+ forwardRef,
5
+ useContext,
6
+ useEffect,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ } from "react";
11
+ import { useComposedRefs } from "@radix-ui/react-compose-refs";
12
+ import type { EChartsType } from "echarts/core";
13
+ import { cx, part } from "../internal.js";
14
+ import { MotionProvider, motionStyle, useChangeFeedback } from "../motion.js";
15
+ import {
16
+ categoryOption,
17
+ validateCategories,
18
+ hasCategoryConflict,
19
+ type CategoryChartConfig,
20
+ type CategorySeries,
21
+ } from "../chart/category-options.js";
22
+ import type { RootProps, MotionProps } from "../types.js";
23
+ export interface CategoryInspection {
24
+ index: number;
25
+ category: string;
26
+ values: { id: string; label: string; unit: string; value: number | null }[];
27
+ }
28
+ export interface CategoryChartProps
29
+ extends
30
+ Omit<RootProps, "asChild" | "onError">,
31
+ MotionProps,
32
+ CategoryChartConfig {
33
+ onReady?: (chart: EChartsType) => void;
34
+ onError?: (error: Error) => void;
35
+ events?: Readonly<
36
+ Record<string, (event: unknown, chart: EChartsType) => void>
37
+ >;
38
+ }
39
+ interface CategoryContext extends Omit<CategoryChartProps, "hidden"> {
40
+ hidden: Set<string>;
41
+ toggle: (id: string) => void;
42
+ inspection: CategoryInspection | null;
43
+ inspect: (index: number | null) => void;
44
+ }
45
+ const Context = createContext<CategoryContext | null>(null);
46
+ function useCategory() {
47
+ const context = useContext(Context);
48
+ if (!context)
49
+ throw new Error("CategoryChart parts require CategoryChart.Root");
50
+ return context;
51
+ }
52
+ const Root = forwardRef<HTMLDivElement, CategoryChartProps>(
53
+ function CategoryRoot(
54
+ {
55
+ categories,
56
+ series,
57
+ orientation = "vertical",
58
+ stacked = false,
59
+ options,
60
+ events,
61
+ onReady,
62
+ onError,
63
+ density = "comfortable",
64
+ motion,
65
+ style,
66
+ className,
67
+ ...props
68
+ },
69
+ ref,
70
+ ) {
71
+ useMemo(
72
+ () => validateCategories({ categories, series }),
73
+ [categories, series],
74
+ );
75
+ useEffect(() => {
76
+ if (
77
+ process.env.NODE_ENV !== "production" &&
78
+ hasCategoryConflict({ categories, series, orientation, options })
79
+ )
80
+ console.warn(
81
+ "Watt CategoryChart props override conflicting ECharts data, axes, legend or tooltip options.",
82
+ );
83
+ }, [categories, series, orientation, options]);
84
+ const [hidden, setHidden] = useState(new Set<string>());
85
+ const [index, setIndex] = useState<number | null>(null);
86
+ const inspection: CategoryInspection | null =
87
+ index === null || categories[index] === undefined
88
+ ? null
89
+ : {
90
+ index,
91
+ category: categories[index]!,
92
+ values: series
93
+ .filter((s) => !hidden.has(s.id))
94
+ .map((s) => ({
95
+ id: s.id,
96
+ label: s.label,
97
+ unit: s.unit,
98
+ value:
99
+ s.values[index] !== null && Number.isFinite(s.values[index])
100
+ ? s.values[index]!
101
+ : null,
102
+ })),
103
+ };
104
+ return (
105
+ <MotionProvider motion={motion}>
106
+ <Context.Provider
107
+ value={{
108
+ categories,
109
+ series,
110
+ orientation,
111
+ stacked,
112
+ options,
113
+ events,
114
+ onReady,
115
+ onError,
116
+ hidden,
117
+ toggle: (id) =>
118
+ setHidden((old) => {
119
+ const next = new Set(old);
120
+ if (next.has(id)) next.delete(id);
121
+ else next.add(id);
122
+ return next;
123
+ }),
124
+ inspection,
125
+ inspect: setIndex,
126
+ }}
127
+ >
128
+ <div
129
+ {...props}
130
+ ref={ref}
131
+ data-slot="category-chart"
132
+ data-density={density}
133
+ className={cx("watt-root min-w-0", className)}
134
+ style={{ ...style, ...motionStyle(motion) }}
135
+ />
136
+ </Context.Provider>
137
+ </MotionProvider>
138
+ );
139
+ },
140
+ );
141
+ export interface CategoryCanvasProps extends Omit<
142
+ React.HTMLAttributes<HTMLDivElement>,
143
+ "children"
144
+ > {
145
+ height?: number;
146
+ previewSvg?: string;
147
+ }
148
+ const Canvas = forwardRef<HTMLDivElement, CategoryCanvasProps>(
149
+ function CategoryCanvas(
150
+ { height = 280, previewSvg, className, onKeyDown, ...props },
151
+ ref,
152
+ ) {
153
+ if (!Number.isFinite(height) || height <= 0)
154
+ throw new Error("CategoryChart.Canvas requires a positive height");
155
+ const context = useCategory();
156
+ const current = useRef(context);
157
+ current.current = context;
158
+ const host = useRef<HTMLDivElement>(null);
159
+ const hostRef = useComposedRefs(host, ref);
160
+ const instance = useRef<EChartsType | null>(null);
161
+ const render = useRef(() => {});
162
+ const bindings = useRef(new Map<string, (event: unknown) => void>());
163
+ const [ready, setReady] = useState(false);
164
+ const [error, setError] = useState(false);
165
+ useEffect(() => {
166
+ let alive = true,
167
+ frame = 0;
168
+ let observer: ResizeObserver | undefined,
169
+ theme: MutationObserver | undefined;
170
+ const fail = (reason: unknown) => {
171
+ if (!alive) return;
172
+ setError(true);
173
+ current.current.onError?.(
174
+ reason instanceof Error ? reason : new Error(String(reason)),
175
+ );
176
+ };
177
+ import("../chart/category-engine.js")
178
+ .then(({ initCategoryCanvas }) => {
179
+ if (!alive || !host.current) return;
180
+ const chart = initCategoryCanvas(host.current);
181
+ instance.current = chart;
182
+ let first = true;
183
+ render.current = () => {
184
+ cancelAnimationFrame(frame);
185
+ frame = requestAnimationFrame(() => {
186
+ if (!alive || !host.current) return;
187
+ try {
188
+ const styles = getComputedStyle(host.current),
189
+ ctx = current.current;
190
+ chart.resize({ silent: true });
191
+ chart.setOption(
192
+ categoryOption(
193
+ {
194
+ ...ctx,
195
+ series: ctx.series.filter((s) => !ctx.hidden.has(s.id)),
196
+ },
197
+ {
198
+ text:
199
+ styles.getPropertyValue("--watt-muted").trim() ||
200
+ "#64748b",
201
+ border:
202
+ styles.getPropertyValue("--watt-border").trim() ||
203
+ "#e2e8f0",
204
+ accent:
205
+ styles.getPropertyValue("--watt-accent").trim() ||
206
+ "#355dff",
207
+ },
208
+ ),
209
+ { notMerge: true },
210
+ );
211
+ setReady(true);
212
+ setError(false);
213
+ if (first) {
214
+ first = false;
215
+ ctx.onReady?.(chart);
216
+ }
217
+ } catch (reason) {
218
+ fail(reason);
219
+ }
220
+ });
221
+ };
222
+ chart.on("updateAxisPointer", (event: unknown) => {
223
+ const axes = (
224
+ event as { axesInfo?: { axisDim: string; value: unknown }[] }
225
+ ).axesInfo;
226
+ const axis = axes?.find(
227
+ (a) =>
228
+ a.axisDim ===
229
+ (current.current.orientation === "horizontal" ? "y" : "x"),
230
+ );
231
+ const index =
232
+ typeof axis?.value === "number"
233
+ ? axis.value
234
+ : current.current.categories.indexOf(String(axis?.value));
235
+ if (index >= 0 && index < current.current.categories.length)
236
+ current.current.inspect(index);
237
+ });
238
+ chart.getZr().on("globalout", () => current.current.inspect(null));
239
+ for (const name of Object.keys(current.current.events ?? {})) {
240
+ const handler = (event: unknown) =>
241
+ current.current.events?.[name]?.(event, chart);
242
+ chart.on(name, handler);
243
+ bindings.current.set(name, handler);
244
+ }
245
+ observer = new ResizeObserver(() => render.current());
246
+ observer.observe(host.current);
247
+ theme = new MutationObserver(() => render.current());
248
+ for (
249
+ let element: HTMLElement | null = host.current.parentElement;
250
+ element;
251
+ element = element.parentElement
252
+ )
253
+ theme.observe(element, {
254
+ attributes: true,
255
+ attributeFilter: ["class", "style", "data-theme"],
256
+ });
257
+ render.current();
258
+ })
259
+ .catch(fail);
260
+ return () => {
261
+ alive = false;
262
+ cancelAnimationFrame(frame);
263
+ observer?.disconnect();
264
+ theme?.disconnect();
265
+ instance.current?.dispose();
266
+ instance.current = null;
267
+ bindings.current.clear();
268
+ render.current = () => {};
269
+ };
270
+ }, []);
271
+ useEffect(
272
+ () => render.current(),
273
+ [
274
+ context.categories,
275
+ context.series,
276
+ context.orientation,
277
+ context.stacked,
278
+ context.options,
279
+ context.hidden,
280
+ className,
281
+ props.style,
282
+ ],
283
+ );
284
+ const eventNames = Object.keys(context.events ?? {})
285
+ .sort()
286
+ .join("\0");
287
+ useEffect(() => {
288
+ const chart = instance.current;
289
+ if (!chart || !ready) return;
290
+ const names = new Set(eventNames ? eventNames.split("\0") : []);
291
+ for (const [name, handler] of bindings.current)
292
+ if (!names.has(name)) {
293
+ chart.off(name, handler);
294
+ bindings.current.delete(name);
295
+ }
296
+ for (const name of names)
297
+ if (!bindings.current.has(name)) {
298
+ const handler = (event: unknown) =>
299
+ current.current.events?.[name]?.(event, chart);
300
+ chart.on(name, handler);
301
+ bindings.current.set(name, handler);
302
+ }
303
+ }, [eventNames, ready]);
304
+ return (
305
+ <div className="relative min-w-0" style={{ height }}>
306
+ {!ready && previewSvg && (
307
+ <div
308
+ aria-hidden="true"
309
+ className="absolute inset-0"
310
+ dangerouslySetInnerHTML={{ __html: previewSvg }}
311
+ />
312
+ )}
313
+ <div
314
+ {...props}
315
+ ref={hostRef}
316
+ role="img"
317
+ tabIndex={0}
318
+ aria-busy={!ready && !error}
319
+ data-error={error || undefined}
320
+ data-slot="category-chart-canvas"
321
+ className={cx(
322
+ "h-full w-full outline-offset-4 focus-visible:outline-2 focus-visible:outline-[var(--watt-accent)]",
323
+ className,
324
+ )}
325
+ onKeyDown={(event) => {
326
+ onKeyDown?.(event);
327
+ if (event.defaultPrevented || !context.categories.length) return;
328
+ if (event.key === "Escape") {
329
+ context.inspect(null);
330
+ return;
331
+ }
332
+ if (
333
+ ![
334
+ "ArrowLeft",
335
+ "ArrowRight",
336
+ "ArrowUp",
337
+ "ArrowDown",
338
+ "Home",
339
+ "End",
340
+ ].includes(event.key)
341
+ )
342
+ return;
343
+ event.preventDefault();
344
+ const next =
345
+ event.key === "Home"
346
+ ? 0
347
+ : event.key === "End"
348
+ ? context.categories.length - 1
349
+ : Math.max(
350
+ 0,
351
+ Math.min(
352
+ context.categories.length - 1,
353
+ (context.inspection?.index ?? -1) +
354
+ (["ArrowLeft", "ArrowUp"].includes(event.key)
355
+ ? -1
356
+ : 1),
357
+ ),
358
+ );
359
+ context.inspect(next);
360
+ instance.current?.dispatchAction({
361
+ type: "showTip",
362
+ seriesIndex: 0,
363
+ dataIndex: next,
364
+ });
365
+ }}
366
+ />
367
+ </div>
368
+ );
369
+ },
370
+ );
371
+ const Legend = forwardRef<
372
+ HTMLDivElement,
373
+ Omit<React.HTMLAttributes<HTMLDivElement>, "children"> & {
374
+ children?: (
375
+ series: CategorySeries & { hidden: boolean; toggle: () => void },
376
+ ) => React.ReactNode;
377
+ }
378
+ >(function CategoryLegend({ className, children, ...props }, ref) {
379
+ const context = useCategory();
380
+ return (
381
+ <div
382
+ {...props}
383
+ ref={ref}
384
+ data-slot="category-chart-legend"
385
+ className={cx("flex flex-wrap gap-4 text-sm", className)}
386
+ >
387
+ {context.series.map((series) => (
388
+ <span key={series.id}>
389
+ {children ? (
390
+ children({
391
+ ...series,
392
+ hidden: context.hidden.has(series.id),
393
+ toggle: () => context.toggle(series.id),
394
+ })
395
+ ) : (
396
+ <button
397
+ type="button"
398
+ aria-pressed={!context.hidden.has(series.id)}
399
+ className="watt-motion watt-chart-legend-toggle inline-flex items-center gap-2"
400
+ onClick={() => context.toggle(series.id)}
401
+ >
402
+ <span
403
+ className="watt-motion watt-category-marker h-2 w-2 rounded-full border border-current"
404
+ aria-hidden="true"
405
+ style={{
406
+ color: series.color ?? "var(--watt-accent)",
407
+ backgroundColor: context.hidden.has(series.id)
408
+ ? "transparent"
409
+ : "currentColor",
410
+ }}
411
+ />
412
+ {series.label}
413
+ </button>
414
+ )}
415
+ </span>
416
+ ))}
417
+ </div>
418
+ );
419
+ });
420
+ const Tooltip = forwardRef<
421
+ HTMLDivElement,
422
+ Omit<React.HTMLAttributes<HTMLDivElement>, "children"> & {
423
+ children: (point: CategoryInspection | null) => React.ReactNode;
424
+ }
425
+ >(function CategoryTooltip({ children, className, ...props }, ref) {
426
+ const { inspection } = useCategory();
427
+ const host = useRef<HTMLDivElement>(null);
428
+ const hostRef = useComposedRefs(host, ref);
429
+ useChangeFeedback(host, inspection !== null);
430
+ return (
431
+ <div
432
+ {...props}
433
+ ref={hostRef}
434
+ data-slot="category-chart-tooltip"
435
+ aria-live={props["aria-live"] ?? "polite"}
436
+ aria-atomic="true"
437
+ className={cx("watt-motion", className)}
438
+ >
439
+ {children(inspection)}
440
+ </div>
441
+ );
442
+ });
443
+ export const CategoryChart = {
444
+ Root,
445
+ Canvas,
446
+ Legend,
447
+ Tooltip,
448
+ Description: part(
449
+ "category-chart-description",
450
+ "text-sm text-[var(--watt-muted)]",
451
+ ),
452
+ };
@@ -243,6 +243,7 @@ export interface FlowEdgeProps extends EnergyFlowEdge, FlowStyle {
243
243
  className?: string;
244
244
  label?: string;
245
245
  style?: React.CSSProperties;
246
+ showDirection?: boolean;
246
247
  }
247
248
  function Edge({
248
249
  source,
@@ -257,6 +258,7 @@ function Edge({
257
258
  className,
258
259
  label,
259
260
  style,
261
+ showDirection = true,
260
262
  }: FlowEdgeProps) {
261
263
  const ctx = useFlow();
262
264
  const id = `watt-${useId().replace(/:/g, "")}`;
@@ -331,7 +333,7 @@ function Edge({
331
333
  fill="none"
332
334
  stroke="currentColor"
333
335
  strokeWidth={width}
334
- markerEnd={`url(#${id})`}
336
+ markerEnd={showDirection ? `url(#${id})` : undefined}
335
337
  className={
336
338
  settings.type === "dash"
337
339
  ? "watt-flow-dash"
@@ -0,0 +1,46 @@
1
+ "use client";
2
+ import { forwardRef } from "react";
3
+ import { Slot } from "@radix-ui/react-slot";
4
+ import { cx, part } from "../internal.js";
5
+ import { MotionProvider, motionStyle } from "../motion.js";
6
+ import type { RootProps, MotionProps } from "../types.js";
7
+ export interface EquipmentCardProps extends RootProps, MotionProps {}
8
+
9
+ export const EquipmentCard = {
10
+ Root: forwardRef<HTMLElement, EquipmentCardProps>(function EquipmentCardRoot(
11
+ { asChild, density = "comfortable", motion, style, className, ...props },
12
+ ref,
13
+ ) {
14
+ const Component = asChild ? Slot : "section";
15
+ return (
16
+ <MotionProvider motion={motion}>
17
+ <Component
18
+ {...props}
19
+ ref={ref as never}
20
+ data-slot="equipment-card"
21
+ data-density={density}
22
+ style={{ ...style, ...motionStyle(motion) }}
23
+ className={cx(
24
+ "watt-root grid gap-4 data-[density=compact]:gap-3",
25
+ className,
26
+ )}
27
+ />
28
+ </MotionProvider>
29
+ );
30
+ }),
31
+ Header: part(
32
+ "equipment-card-header",
33
+ "flex items-start justify-between gap-4",
34
+ ),
35
+ Title: part("equipment-card-title", "font-medium", "span"),
36
+ Description: part(
37
+ "equipment-card-description",
38
+ "text-sm text-[var(--watt-muted)]",
39
+ "span",
40
+ ),
41
+ Content: part("equipment-card-content", "min-w-0"),
42
+ Footer: part(
43
+ "equipment-card-footer",
44
+ "flex flex-wrap items-center gap-4 text-sm",
45
+ ),
46
+ };