@watt-ui/react 0.1.0 → 0.1.2
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 +2 -1
- package/dist/components/alarm-feed.d.ts +2 -2
- package/dist/components/alarm-feed.d.ts.map +1 -1
- package/dist/components/alarm-feed.js +38 -16
- package/dist/components/alarm-feed.js.map +1 -1
- package/dist/components/asset-tree.d.ts +6 -2
- package/dist/components/asset-tree.d.ts.map +1 -1
- package/dist/components/asset-tree.js +15 -8
- package/dist/components/asset-tree.js.map +1 -1
- package/dist/components/battery-soc.d.ts +2 -2
- package/dist/components/battery-soc.d.ts.map +1 -1
- package/dist/components/battery-soc.js +4 -3
- package/dist/components/battery-soc.js.map +1 -1
- package/dist/components/data-freshness.d.ts +2 -2
- package/dist/components/data-freshness.d.ts.map +1 -1
- package/dist/components/data-freshness.js +8 -3
- package/dist/components/data-freshness.js.map +1 -1
- package/dist/components/energy-flow.d.ts.map +1 -1
- package/dist/components/energy-flow.js +22 -1
- package/dist/components/energy-flow.js.map +1 -1
- package/dist/components/power-metric.d.ts +5 -3
- package/dist/components/power-metric.d.ts.map +1 -1
- package/dist/components/power-metric.js +10 -5
- package/dist/components/power-metric.js.map +1 -1
- package/dist/components/status-indicator.d.ts +2 -2
- package/dist/components/status-indicator.d.ts.map +1 -1
- package/dist/components/status-indicator.js +8 -3
- package/dist/components/status-indicator.js.map +1 -1
- package/dist/components/telemetry-chart.d.ts +5 -7
- package/dist/components/telemetry-chart.d.ts.map +1 -1
- package/dist/components/telemetry-chart.js +30 -21
- package/dist/components/telemetry-chart.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/motion.d.ts +12 -0
- package/dist/motion.d.ts.map +1 -0
- package/dist/motion.js +90 -0
- package/dist/motion.js.map +1 -0
- package/dist/styles.css +39 -0
- package/dist/types.d.ts +10 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/alarm-feed.tsx +97 -62
- package/src/components/asset-tree.tsx +56 -31
- package/src/components/battery-soc.tsx +29 -14
- package/src/components/data-freshness.tsx +34 -17
- package/src/components/energy-flow.tsx +24 -1
- package/src/components/power-metric.tsx +32 -17
- package/src/components/status-indicator.tsx +34 -15
- package/src/components/telemetry-chart.tsx +60 -38
- package/src/index.ts +5 -1
- package/src/motion.tsx +117 -0
- package/src/styles.css +39 -0
- package/src/types.ts +30 -4
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "react";
|
|
14
14
|
import type { EChartsCoreOption, EChartsType } from "echarts/core";
|
|
15
15
|
import { cx, part } from "../internal.js";
|
|
16
|
+
import { MotionProvider, motionStyle, useChangeFeedback } from "../motion.js";
|
|
16
17
|
import { nearestIndex, prepareSeries, lowerBound } from "../chart/data.js";
|
|
17
18
|
import {
|
|
18
19
|
chartOption,
|
|
@@ -24,6 +25,7 @@ import type {
|
|
|
24
25
|
TelemetrySeries,
|
|
25
26
|
TimeRange,
|
|
26
27
|
DataQuality,
|
|
28
|
+
MotionProps,
|
|
27
29
|
} from "../types.js";
|
|
28
30
|
export interface ChartInspection {
|
|
29
31
|
timestamp: number;
|
|
@@ -36,7 +38,9 @@ export interface ChartInspection {
|
|
|
36
38
|
quality?: DataQuality;
|
|
37
39
|
}[];
|
|
38
40
|
}
|
|
39
|
-
export interface TelemetryChartProps
|
|
41
|
+
export interface TelemetryChartProps
|
|
42
|
+
extends Omit<RootProps, "onError">,
|
|
43
|
+
MotionProps {
|
|
40
44
|
series: readonly TelemetrySeries[];
|
|
41
45
|
timeZone: string;
|
|
42
46
|
locale?: string;
|
|
@@ -83,6 +87,8 @@ const Root = forwardRef<HTMLDivElement, TelemetryChartProps>(
|
|
|
83
87
|
asChild,
|
|
84
88
|
children,
|
|
85
89
|
className,
|
|
90
|
+
style,
|
|
91
|
+
motion,
|
|
86
92
|
...props
|
|
87
93
|
},
|
|
88
94
|
ref,
|
|
@@ -142,34 +148,37 @@ const Root = forwardRef<HTMLDivElement, TelemetryChartProps>(
|
|
|
142
148
|
}, [options, prepared, range, timeZone]);
|
|
143
149
|
const Component = asChild ? Slot : "div";
|
|
144
150
|
return (
|
|
145
|
-
<
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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)}
|
|
151
|
+
<MotionProvider motion={motion}>
|
|
152
|
+
<Context.Provider
|
|
153
|
+
value={{
|
|
154
|
+
series: prepared,
|
|
155
|
+
timeZone,
|
|
156
|
+
locale,
|
|
157
|
+
range,
|
|
158
|
+
onRangeChange,
|
|
159
|
+
sampling,
|
|
160
|
+
options,
|
|
161
|
+
events,
|
|
162
|
+
inspection,
|
|
163
|
+
inspect,
|
|
164
|
+
hidden,
|
|
165
|
+
toggle,
|
|
166
|
+
onReady,
|
|
167
|
+
onError,
|
|
168
|
+
}}
|
|
169
169
|
>
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
170
|
+
<Component
|
|
171
|
+
{...props}
|
|
172
|
+
ref={ref}
|
|
173
|
+
data-slot="telemetry-chart"
|
|
174
|
+
data-density={density}
|
|
175
|
+
style={{ ...style, ...motionStyle(motion) }}
|
|
176
|
+
className={cx("watt-root min-w-0", className)}
|
|
177
|
+
>
|
|
178
|
+
{children}
|
|
179
|
+
</Component>
|
|
180
|
+
</Context.Provider>
|
|
181
|
+
</MotionProvider>
|
|
173
182
|
);
|
|
174
183
|
},
|
|
175
184
|
);
|
|
@@ -432,11 +441,18 @@ const Legend = forwardRef<
|
|
|
432
441
|
type="button"
|
|
433
442
|
aria-pressed={!ctx.hidden.has(s.id)}
|
|
434
443
|
onClick={() => ctx.toggle(s.id)}
|
|
435
|
-
className="inline-flex items-center gap-2"
|
|
444
|
+
className="watt-chart-legend-toggle inline-flex items-center gap-2"
|
|
436
445
|
>
|
|
437
446
|
<span
|
|
438
|
-
|
|
439
|
-
|
|
447
|
+
aria-hidden="true"
|
|
448
|
+
data-slot="telemetry-chart-legend-marker"
|
|
449
|
+
className="watt-motion h-2 w-2 rounded-full border border-current"
|
|
450
|
+
style={{
|
|
451
|
+
color: s.color ?? "var(--watt-accent)",
|
|
452
|
+
background: ctx.hidden.has(s.id)
|
|
453
|
+
? "transparent"
|
|
454
|
+
: "currentColor",
|
|
455
|
+
}}
|
|
440
456
|
/>
|
|
441
457
|
{s.label}
|
|
442
458
|
</button>
|
|
@@ -446,24 +462,30 @@ const Legend = forwardRef<
|
|
|
446
462
|
</div>
|
|
447
463
|
);
|
|
448
464
|
});
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
}) {
|
|
465
|
+
const Tooltip = forwardRef<
|
|
466
|
+
HTMLDivElement,
|
|
467
|
+
Omit<React.HTMLAttributes<HTMLDivElement>, "children"> & {
|
|
468
|
+
children: (inspection: ChartInspection | null) => React.ReactNode;
|
|
469
|
+
}
|
|
470
|
+
>(function Tooltip({ children, className, ...props }, ref) {
|
|
455
471
|
const { inspection } = useChart();
|
|
472
|
+
const host = useRef<HTMLDivElement>(null);
|
|
473
|
+
const hostRef = useComposedRefs(host, ref);
|
|
474
|
+
useChangeFeedback(host, inspection !== null);
|
|
456
475
|
return (
|
|
457
476
|
<div
|
|
458
477
|
{...props}
|
|
478
|
+
ref={hostRef}
|
|
459
479
|
data-slot="telemetry-chart-tooltip"
|
|
480
|
+
data-state={inspection ? "active" : "idle"}
|
|
481
|
+
className={cx("watt-motion", className)}
|
|
460
482
|
aria-live={props["aria-live"] ?? "polite"}
|
|
461
483
|
aria-atomic={props["aria-atomic"] ?? true}
|
|
462
484
|
>
|
|
463
485
|
{children(inspection)}
|
|
464
486
|
</div>
|
|
465
487
|
);
|
|
466
|
-
}
|
|
488
|
+
});
|
|
467
489
|
export const TelemetryChart = {
|
|
468
490
|
Root,
|
|
469
491
|
Canvas,
|
package/src/index.ts
CHANGED
|
@@ -8,7 +8,10 @@ export { AlarmFeed } from "./components/alarm-feed.js";
|
|
|
8
8
|
export { AssetTree } from "./components/asset-tree.js";
|
|
9
9
|
export { formatNumber, formatTime, parseTimestamp } from "./domain.js";
|
|
10
10
|
export type * from "./types.js";
|
|
11
|
-
export type {
|
|
11
|
+
export type {
|
|
12
|
+
PowerMetricProps,
|
|
13
|
+
MetricValueProps,
|
|
14
|
+
} from "./components/power-metric.js";
|
|
12
15
|
export type { StatusIndicatorProps } from "./components/status-indicator.js";
|
|
13
16
|
export type { BatterySOCProps } from "./components/battery-soc.js";
|
|
14
17
|
export type {
|
|
@@ -34,4 +37,5 @@ export type {
|
|
|
34
37
|
AssetTreeProps,
|
|
35
38
|
AssetTreeRow,
|
|
36
39
|
AssetTreeItem,
|
|
40
|
+
AssetTreeIndicatorProps,
|
|
37
41
|
} from "./components/asset-tree.js";
|
package/src/motion.tsx
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
createContext,
|
|
4
|
+
useContext,
|
|
5
|
+
useEffect,
|
|
6
|
+
useLayoutEffect,
|
|
7
|
+
useRef,
|
|
8
|
+
type CSSProperties,
|
|
9
|
+
type ReactNode,
|
|
10
|
+
type RefObject,
|
|
11
|
+
} from "react";
|
|
12
|
+
import type { Motion, MotionProps } from "./types.js";
|
|
13
|
+
|
|
14
|
+
const Context = createContext<Motion>(true);
|
|
15
|
+
const useMotionEffect =
|
|
16
|
+
typeof document === "undefined" ? useEffect : useLayoutEffect;
|
|
17
|
+
const noop = () => {};
|
|
18
|
+
export function useMotion(own?: Motion): Motion {
|
|
19
|
+
const parent = useContext(Context);
|
|
20
|
+
return own ?? parent;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function MotionProvider({
|
|
24
|
+
motion,
|
|
25
|
+
children,
|
|
26
|
+
}: MotionProps & { children: ReactNode }) {
|
|
27
|
+
const parent = useContext(Context);
|
|
28
|
+
return (
|
|
29
|
+
<Context.Provider value={motion ?? parent}>{children}</Context.Provider>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function motionStyle(motion?: Motion): CSSProperties {
|
|
34
|
+
if (motion === undefined) return {};
|
|
35
|
+
const duration =
|
|
36
|
+
motion === false ? 0 : motion === true ? 240 : (motion.duration ?? 240);
|
|
37
|
+
if (!Number.isFinite(duration) || duration < 0)
|
|
38
|
+
throw new Error(
|
|
39
|
+
"Motion duration must be finite and nonnegative milliseconds",
|
|
40
|
+
);
|
|
41
|
+
return {
|
|
42
|
+
"--watt-motion-duration": `${duration}ms`,
|
|
43
|
+
"--watt-motion-property": duration === 0 ? "none" : "initial",
|
|
44
|
+
"--watt-motion-easing":
|
|
45
|
+
typeof motion === "object"
|
|
46
|
+
? (motion.easing ?? "cubic-bezier(0.16, 1, 0.3, 1)")
|
|
47
|
+
: "cubic-bezier(0.16, 1, 0.3, 1)",
|
|
48
|
+
} as CSSProperties;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Bounded feedback only: never changes text, data, row geometry or DOM identity. */
|
|
52
|
+
export function animateMotion(
|
|
53
|
+
element: HTMLElement,
|
|
54
|
+
frames: Keyframe[],
|
|
55
|
+
): () => void {
|
|
56
|
+
if (!element.animate || document.hidden || !element.getClientRects().length)
|
|
57
|
+
return noop;
|
|
58
|
+
const preference = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
59
|
+
if (preference.matches) return noop;
|
|
60
|
+
const styles = getComputedStyle(element);
|
|
61
|
+
const time = styles.transitionDuration.split(",")[0] ?? "0s";
|
|
62
|
+
const duration = parseFloat(time) * (time.trim().endsWith("ms") ? 1 : 1000);
|
|
63
|
+
if (!(duration > 0)) return noop;
|
|
64
|
+
let animation: Animation;
|
|
65
|
+
try {
|
|
66
|
+
animation = element.animate(frames, {
|
|
67
|
+
duration,
|
|
68
|
+
easing: styles.transitionTimingFunction,
|
|
69
|
+
});
|
|
70
|
+
} catch {
|
|
71
|
+
return noop; // A consumer's unsupported CSS easing must not block live readings.
|
|
72
|
+
}
|
|
73
|
+
const detach = () => {
|
|
74
|
+
preference.removeEventListener("change", stop);
|
|
75
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
76
|
+
};
|
|
77
|
+
const stop = () => {
|
|
78
|
+
animation.cancel();
|
|
79
|
+
detach();
|
|
80
|
+
};
|
|
81
|
+
const onVisibility = () => {
|
|
82
|
+
if (document.hidden) stop();
|
|
83
|
+
};
|
|
84
|
+
preference.addEventListener("change", stop);
|
|
85
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
86
|
+
animation.addEventListener("finish", detach, { once: true });
|
|
87
|
+
animation.addEventListener("cancel", detach, { once: true });
|
|
88
|
+
return stop;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function highlightChange(element: HTMLElement): () => void {
|
|
92
|
+
const styles = getComputedStyle(element);
|
|
93
|
+
const background = styles.backgroundColor;
|
|
94
|
+
const accent = styles.getPropertyValue("--watt-accent").trim() || "#355dff";
|
|
95
|
+
return animateMotion(element, [
|
|
96
|
+
{ backgroundColor: `color-mix(in srgb, ${accent} 10%, ${background})` },
|
|
97
|
+
{ backgroundColor: background },
|
|
98
|
+
]);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function useChangeFeedback(
|
|
102
|
+
ref: RefObject<HTMLElement | null>,
|
|
103
|
+
value: unknown,
|
|
104
|
+
ownMotion?: Motion,
|
|
105
|
+
) {
|
|
106
|
+
const motion = useMotion(ownMotion);
|
|
107
|
+
const duration = typeof motion === "object" ? motion.duration : undefined;
|
|
108
|
+
const easing = typeof motion === "object" ? motion.easing : undefined;
|
|
109
|
+
const previous = useRef(value);
|
|
110
|
+
useMotionEffect(() => {
|
|
111
|
+
const changed = !Object.is(previous.current, value);
|
|
112
|
+
previous.current = value;
|
|
113
|
+
if (changed && motion !== false && ref.current)
|
|
114
|
+
return highlightChange(ref.current);
|
|
115
|
+
}, [value, ref, motion === false, duration, easing]);
|
|
116
|
+
}
|
|
117
|
+
|
package/src/styles.css
CHANGED
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
--watt-warning: #996009;
|
|
23
23
|
--watt-critical: #c43b40;
|
|
24
24
|
--watt-radius: 12px;
|
|
25
|
+
--watt-motion-duration: 240ms;
|
|
26
|
+
--watt-motion-easing: cubic-bezier(0.16, 1, 0.3, 1);
|
|
25
27
|
}
|
|
26
28
|
.dark,
|
|
27
29
|
[data-theme="dark"] {
|
|
@@ -39,6 +41,36 @@
|
|
|
39
41
|
color: var(--watt-foreground);
|
|
40
42
|
font-variant-numeric: tabular-nums;
|
|
41
43
|
}
|
|
44
|
+
.watt-motion {
|
|
45
|
+
transition-property: none;
|
|
46
|
+
transition-duration: var(--watt-motion-duration, 240ms);
|
|
47
|
+
transition-timing-function: var(--watt-motion-easing, ease-out);
|
|
48
|
+
}
|
|
49
|
+
.watt-motion.watt-status {
|
|
50
|
+
transition-property: var(--watt-motion-property, color);
|
|
51
|
+
}
|
|
52
|
+
.watt-soc-fill {
|
|
53
|
+
transition-property: var(--watt-motion-property, width);
|
|
54
|
+
}
|
|
55
|
+
[data-slot="battery-soc"][data-invalid] .watt-soc-fill {
|
|
56
|
+
transition-duration: 0s;
|
|
57
|
+
}
|
|
58
|
+
[data-slot="asset-tree-row"] {
|
|
59
|
+
transition-property: var(--watt-motion-property, background-color);
|
|
60
|
+
}
|
|
61
|
+
[data-slot="asset-tree-indicator"] {
|
|
62
|
+
transition-property: var(--watt-motion-property, transform);
|
|
63
|
+
}
|
|
64
|
+
[data-slot="telemetry-chart-legend-marker"] {
|
|
65
|
+
transition-property: var(--watt-motion-property, background-color, scale);
|
|
66
|
+
}
|
|
67
|
+
.watt-chart-legend-toggle[aria-pressed="false"] {
|
|
68
|
+
text-decoration: line-through;
|
|
69
|
+
}
|
|
70
|
+
.watt-chart-legend-toggle[aria-pressed="false"]
|
|
71
|
+
[data-slot="telemetry-chart-legend-marker"] {
|
|
72
|
+
scale: 0.75;
|
|
73
|
+
}
|
|
42
74
|
.watt-status {
|
|
43
75
|
color: var(--watt-muted);
|
|
44
76
|
}
|
|
@@ -120,6 +152,13 @@
|
|
|
120
152
|
}
|
|
121
153
|
}
|
|
122
154
|
@media (prefers-reduced-motion: reduce) {
|
|
155
|
+
.watt-motion {
|
|
156
|
+
transition-duration: 0s !important;
|
|
157
|
+
transition-property: none !important;
|
|
158
|
+
}
|
|
159
|
+
.watt-status {
|
|
160
|
+
transition: none;
|
|
161
|
+
}
|
|
123
162
|
.watt-flow-dash,
|
|
124
163
|
.watt-flow-pulse {
|
|
125
164
|
animation: none !important;
|
package/src/types.ts
CHANGED
|
@@ -1,15 +1,41 @@
|
|
|
1
|
-
import type { HTMLAttributes } from "react";
|
|
1
|
+
import type { CSSProperties, HTMLAttributes } from "react";
|
|
2
2
|
|
|
3
3
|
export type PowerUnit = "W" | "kW" | "MW";
|
|
4
4
|
export type EnergyUnit = "Wh" | "kWh" | "MWh";
|
|
5
5
|
export type MeasurementUnit =
|
|
6
|
-
|
|
6
|
+
| PowerUnit
|
|
7
|
+
| EnergyUnit
|
|
8
|
+
| "V"
|
|
9
|
+
| "A"
|
|
10
|
+
| "Hz"
|
|
11
|
+
| "°C"
|
|
12
|
+
| "%"
|
|
13
|
+
| "W/m²";
|
|
7
14
|
export type Timestamp = string | number;
|
|
8
15
|
export type DataQuality =
|
|
9
|
-
|
|
16
|
+
| "good"
|
|
17
|
+
| "uncertain"
|
|
18
|
+
| "stale"
|
|
19
|
+
| "missing"
|
|
20
|
+
| "bad"
|
|
21
|
+
| "offline";
|
|
10
22
|
export type EquipmentStatus =
|
|
11
|
-
|
|
23
|
+
| "normal"
|
|
24
|
+
| "standby"
|
|
25
|
+
| "stopped"
|
|
26
|
+
| "warning"
|
|
27
|
+
| "fault"
|
|
28
|
+
| "offline";
|
|
12
29
|
export type Density = "compact" | "comfortable";
|
|
30
|
+
export interface MotionOptions {
|
|
31
|
+
/** Duration in milliseconds; zero disables transitions. */
|
|
32
|
+
duration?: number;
|
|
33
|
+
easing?: CSSProperties["transitionTimingFunction"];
|
|
34
|
+
}
|
|
35
|
+
export type Motion = boolean | MotionOptions;
|
|
36
|
+
export interface MotionProps {
|
|
37
|
+
motion?: Motion;
|
|
38
|
+
}
|
|
13
39
|
export interface PartProps extends HTMLAttributes<HTMLElement> {
|
|
14
40
|
asChild?: boolean;
|
|
15
41
|
}
|