@watt-ui/react 0.1.1 → 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 +1 -0
- 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/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 +36 -1
- 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/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 +36 -1
- package/src/types.ts +30 -4
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,9 +41,38 @@
|
|
|
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
|
-
transition: color 140ms ease-out;
|
|
45
76
|
}
|
|
46
77
|
.watt-status[data-status="normal"] {
|
|
47
78
|
color: var(--watt-positive);
|
|
@@ -121,6 +152,10 @@
|
|
|
121
152
|
}
|
|
122
153
|
}
|
|
123
154
|
@media (prefers-reduced-motion: reduce) {
|
|
155
|
+
.watt-motion {
|
|
156
|
+
transition-duration: 0s !important;
|
|
157
|
+
transition-property: none !important;
|
|
158
|
+
}
|
|
124
159
|
.watt-status {
|
|
125
160
|
transition: none;
|
|
126
161
|
}
|
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
|
}
|