@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
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { useComposedRefs } from "@radix-ui/react-compose-refs";
|
|
3
3
|
import type { ForwardedRef, RefAttributes, ReactElement } from "react";
|
|
4
|
-
import { forwardRef } from "react";
|
|
4
|
+
import { forwardRef, useEffect } from "react";
|
|
5
5
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
6
6
|
import { useRef } from "react";
|
|
7
7
|
import { cx, part } from "../internal.js";
|
|
8
|
-
import type { AlarmEvent, Density } from "../types.js";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
import type { AlarmEvent, Density, MotionProps } from "../types.js";
|
|
9
|
+
import {
|
|
10
|
+
MotionProvider,
|
|
11
|
+
motionStyle,
|
|
12
|
+
highlightChange,
|
|
13
|
+
useMotion,
|
|
14
|
+
} from "../motion.js";
|
|
15
|
+
export interface AlarmFeedProps<T extends AlarmEvent = AlarmEvent>
|
|
16
|
+
extends Omit<React.HTMLAttributes<HTMLDivElement>, "children">,
|
|
17
|
+
MotionProps {
|
|
13
18
|
items: readonly T[];
|
|
14
19
|
children: (item: T, index: number) => React.ReactNode;
|
|
15
20
|
density?: Density;
|
|
@@ -27,6 +32,7 @@ function Root<T extends AlarmEvent>(
|
|
|
27
32
|
rowHeight = 80,
|
|
28
33
|
height = 400,
|
|
29
34
|
empty,
|
|
35
|
+
motion,
|
|
30
36
|
className,
|
|
31
37
|
style,
|
|
32
38
|
...props
|
|
@@ -35,6 +41,28 @@ function Root<T extends AlarmEvent>(
|
|
|
35
41
|
) {
|
|
36
42
|
const host = useRef<HTMLDivElement>(null);
|
|
37
43
|
const hostRef = useComposedRefs(host, forwardedRef);
|
|
44
|
+
const previousItems = useRef(items);
|
|
45
|
+
const resolvedMotion = useMotion(motion);
|
|
46
|
+
const duration =
|
|
47
|
+
typeof resolvedMotion === "object" ? resolvedMotion.duration : undefined;
|
|
48
|
+
const easing =
|
|
49
|
+
typeof resolvedMotion === "object" ? resolvedMotion.easing : undefined;
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
const old = previousItems.current;
|
|
52
|
+
previousItems.current = items;
|
|
53
|
+
if (old === items || resolvedMotion === false || !host.current) return;
|
|
54
|
+
const known = new Set(old.map((item) => item.id));
|
|
55
|
+
const added = new Set(
|
|
56
|
+
items.filter((item) => !known.has(item.id)).map((item) => item.id),
|
|
57
|
+
);
|
|
58
|
+
const stop: (() => void)[] = [];
|
|
59
|
+
for (const element of host.current.querySelectorAll<HTMLElement>(
|
|
60
|
+
"[data-slot=alarm-feed-item]",
|
|
61
|
+
))
|
|
62
|
+
if (added.has(element.dataset.alarmId!))
|
|
63
|
+
stop.push(highlightChange(element));
|
|
64
|
+
return () => stop.forEach((cancel) => cancel());
|
|
65
|
+
}, [items, resolvedMotion === false, duration, easing]);
|
|
38
66
|
const virtualizer = useVirtualizer({
|
|
39
67
|
count: items.length,
|
|
40
68
|
initialRect: { width: 0, height: virtualized ? height : 0 },
|
|
@@ -48,62 +76,69 @@ function Root<T extends AlarmEvent>(
|
|
|
48
76
|
? virtualizer.getVirtualItems()
|
|
49
77
|
: items.map((item, index) => ({ key: item.id, index, start: 0 }));
|
|
50
78
|
return (
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
79
|
+
<MotionProvider motion={motion}>
|
|
80
|
+
<div
|
|
81
|
+
{...props}
|
|
82
|
+
ref={hostRef}
|
|
83
|
+
tabIndex={props.tabIndex ?? (virtualized ? 0 : undefined)}
|
|
84
|
+
data-slot="alarm-feed"
|
|
85
|
+
data-density={density}
|
|
86
|
+
className={cx("watt-root overflow-auto", className)}
|
|
87
|
+
style={{
|
|
88
|
+
...(virtualized ? { height } : {}),
|
|
89
|
+
...style,
|
|
90
|
+
...motionStyle(motion),
|
|
91
|
+
}}
|
|
92
|
+
>
|
|
93
|
+
{!items.length ? (
|
|
94
|
+
empty
|
|
95
|
+
) : (
|
|
96
|
+
<div
|
|
97
|
+
role="list"
|
|
98
|
+
style={
|
|
99
|
+
virtualized
|
|
100
|
+
? { height: virtualizer.getTotalSize(), position: "relative" }
|
|
101
|
+
: undefined
|
|
102
|
+
}
|
|
103
|
+
>
|
|
104
|
+
{rows.map((row) => {
|
|
105
|
+
const item = items[row.index]!;
|
|
106
|
+
return (
|
|
107
|
+
<div
|
|
108
|
+
role="listitem"
|
|
109
|
+
key={row.key}
|
|
110
|
+
data-index={row.index}
|
|
111
|
+
ref={virtualized ? virtualizer.measureElement : undefined}
|
|
112
|
+
aria-posinset={row.index + 1}
|
|
113
|
+
aria-setsize={items.length}
|
|
114
|
+
data-slot="alarm-feed-item"
|
|
115
|
+
data-alarm-id={item.id}
|
|
116
|
+
data-severity={item.severity}
|
|
117
|
+
data-state={item.status}
|
|
118
|
+
className={cx(
|
|
119
|
+
"watt-motion border-b border-[var(--watt-border)] last:border-0",
|
|
120
|
+
density === "compact" ? "py-2.5" : "py-4",
|
|
121
|
+
)}
|
|
122
|
+
style={
|
|
123
|
+
virtualized
|
|
124
|
+
? {
|
|
125
|
+
position: "absolute",
|
|
126
|
+
top: 0,
|
|
127
|
+
left: 0,
|
|
128
|
+
width: "100%",
|
|
129
|
+
transform: `translateY(${row.start}px)`,
|
|
130
|
+
}
|
|
131
|
+
: undefined
|
|
132
|
+
}
|
|
133
|
+
>
|
|
134
|
+
{children(item, row.index)}
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
})}
|
|
138
|
+
</div>
|
|
139
|
+
)}
|
|
140
|
+
</div>
|
|
141
|
+
</MotionProvider>
|
|
107
142
|
);
|
|
108
143
|
}
|
|
109
144
|
const Severity = forwardRef<
|
|
@@ -11,8 +11,10 @@ import {
|
|
|
11
11
|
} from "@headless-tree/core";
|
|
12
12
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
13
13
|
import { cx } from "../internal.js";
|
|
14
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
15
|
+
import { MotionProvider, motionStyle } from "../motion.js";
|
|
14
16
|
import type { ForwardedRef, RefAttributes, ReactElement } from "react";
|
|
15
|
-
import type { Density, PlantAsset } from "../types.js";
|
|
17
|
+
import type { Density, MotionProps, PartProps, PlantAsset } from "../types.js";
|
|
16
18
|
export interface AssetTreeRow<T> {
|
|
17
19
|
item: T;
|
|
18
20
|
level: number;
|
|
@@ -30,10 +32,9 @@ export type AssetTreeItem<T extends PlantAsset> =
|
|
|
30
32
|
? never
|
|
31
33
|
: AssetTreeItem<C>
|
|
32
34
|
: never);
|
|
33
|
-
export interface AssetTreeProps<T extends PlantAsset = PlantAsset>
|
|
34
|
-
React.HTMLAttributes<HTMLDivElement>,
|
|
35
|
-
|
|
36
|
-
> {
|
|
35
|
+
export interface AssetTreeProps<T extends PlantAsset = PlantAsset>
|
|
36
|
+
extends Omit<React.HTMLAttributes<HTMLDivElement>, "children">,
|
|
37
|
+
MotionProps {
|
|
37
38
|
items: readonly T[];
|
|
38
39
|
children: (row: AssetTreeRow<AssetTreeItem<T>>) => React.ReactNode;
|
|
39
40
|
expandedIds?: string[];
|
|
@@ -64,6 +65,7 @@ function Root<T extends PlantAsset>(
|
|
|
64
65
|
height = 360,
|
|
65
66
|
rowHeight = density === "compact" ? 36 : 48,
|
|
66
67
|
empty,
|
|
68
|
+
motion,
|
|
67
69
|
className,
|
|
68
70
|
style,
|
|
69
71
|
...props
|
|
@@ -203,7 +205,8 @@ function Root<T extends PlantAsset>(
|
|
|
203
205
|
data-slot="asset-tree-row"
|
|
204
206
|
data-index={index}
|
|
205
207
|
data-selected={item.isSelected() || undefined}
|
|
206
|
-
|
|
208
|
+
data-expanded={item.isExpanded()}
|
|
209
|
+
className="watt-motion absolute top-0 left-0 flex w-full items-center border-b border-[var(--watt-border)] outline-offset-[-2px] data-[selected]:bg-[var(--watt-subtle)] focus-visible:outline-2 focus-visible:outline-[var(--watt-accent)]"
|
|
207
210
|
style={{
|
|
208
211
|
height: rowHeight,
|
|
209
212
|
paddingInlineStart: item.getItemMeta().level * 20,
|
|
@@ -224,34 +227,56 @@ function Root<T extends PlantAsset>(
|
|
|
224
227
|
);
|
|
225
228
|
};
|
|
226
229
|
return (
|
|
227
|
-
<
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
230
|
+
<MotionProvider motion={motion}>
|
|
231
|
+
<div
|
|
232
|
+
{...containerProps}
|
|
233
|
+
{...props}
|
|
234
|
+
ref={hostRef}
|
|
235
|
+
data-slot="asset-tree"
|
|
236
|
+
data-density={density}
|
|
237
|
+
className={cx("watt-root overflow-auto text-sm", className)}
|
|
238
|
+
style={{ height, ...style, ...motionStyle(motion) }}
|
|
239
|
+
>
|
|
240
|
+
{!items.length ? (
|
|
241
|
+
empty
|
|
242
|
+
) : (
|
|
243
|
+
<div
|
|
244
|
+
role="presentation"
|
|
245
|
+
style={{ height: virtualizer.getTotalSize(), position: "relative" }}
|
|
246
|
+
>
|
|
247
|
+
{virtualizer.getVirtualItems().map((row) => {
|
|
248
|
+
const item = visible[row.index];
|
|
249
|
+
return item && indexed.has(item.getId())
|
|
250
|
+
? renderRow(item, row.index, row.start)
|
|
251
|
+
: null;
|
|
252
|
+
})}
|
|
253
|
+
</div>
|
|
254
|
+
)}
|
|
255
|
+
</div>
|
|
256
|
+
</MotionProvider>
|
|
252
257
|
);
|
|
253
258
|
}
|
|
259
|
+
export interface AssetTreeIndicatorProps extends PartProps {
|
|
260
|
+
expanded: boolean;
|
|
261
|
+
}
|
|
262
|
+
const Indicator = forwardRef<HTMLElement, AssetTreeIndicatorProps>(
|
|
263
|
+
({ expanded, asChild, className, style, ...props }, ref) => {
|
|
264
|
+
const Component = asChild ? Slot : "span";
|
|
265
|
+
return (
|
|
266
|
+
<Component
|
|
267
|
+
{...props}
|
|
268
|
+
ref={ref as never}
|
|
269
|
+
aria-hidden={props["aria-hidden"] ?? true}
|
|
270
|
+
data-slot="asset-tree-indicator"
|
|
271
|
+
data-expanded={expanded}
|
|
272
|
+
className={cx("watt-motion inline-flex shrink-0", className)}
|
|
273
|
+
style={{ transform: `rotate(${expanded ? 90 : 0}deg)`, ...style }}
|
|
274
|
+
/>
|
|
275
|
+
);
|
|
276
|
+
},
|
|
277
|
+
);
|
|
254
278
|
export const AssetTree = {
|
|
279
|
+
Indicator,
|
|
255
280
|
Root: forwardRef(Root) as <T extends PlantAsset = PlantAsset>(
|
|
256
281
|
props: AssetTreeProps<T> & RefAttributes<HTMLDivElement>,
|
|
257
282
|
) => ReactElement,
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import { createContext, forwardRef, useContext } from "react";
|
|
3
3
|
import { Slot } from "@radix-ui/react-slot";
|
|
4
4
|
import { cx, part } from "../internal.js";
|
|
5
|
-
import type { PartProps, RootProps, Density } from "../types.js";
|
|
5
|
+
import type { PartProps, RootProps, Density, MotionProps } from "../types.js";
|
|
6
|
+
import { MotionProvider, motionStyle } from "../motion.js";
|
|
6
7
|
const Context = createContext<{
|
|
7
8
|
value: number | null;
|
|
8
9
|
density: Density;
|
|
@@ -13,25 +14,39 @@ function useSOC() {
|
|
|
13
14
|
throw new Error("BatterySOC parts require BatterySOC.Root");
|
|
14
15
|
return value;
|
|
15
16
|
}
|
|
16
|
-
export interface BatterySOCProps extends RootProps {
|
|
17
|
+
export interface BatterySOCProps extends RootProps, MotionProps {
|
|
17
18
|
value: number | null;
|
|
18
19
|
}
|
|
19
20
|
const Root = forwardRef<HTMLElement, BatterySOCProps>(
|
|
20
|
-
(
|
|
21
|
+
(
|
|
22
|
+
{
|
|
23
|
+
value,
|
|
24
|
+
asChild,
|
|
25
|
+
density = "comfortable",
|
|
26
|
+
motion,
|
|
27
|
+
className,
|
|
28
|
+
style,
|
|
29
|
+
...props
|
|
30
|
+
},
|
|
31
|
+
ref,
|
|
32
|
+
) => {
|
|
21
33
|
const Component = asChild ? Slot : "div";
|
|
22
34
|
const valid =
|
|
23
35
|
value !== null && Number.isFinite(value) && value >= 0 && value <= 100;
|
|
24
36
|
return (
|
|
25
|
-
<
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
<MotionProvider motion={motion}>
|
|
38
|
+
<Context.Provider value={{ value: valid ? value : null, density }}>
|
|
39
|
+
<Component
|
|
40
|
+
{...props}
|
|
41
|
+
ref={ref as never}
|
|
42
|
+
data-slot="battery-soc"
|
|
43
|
+
data-invalid={!valid || undefined}
|
|
44
|
+
data-density={density}
|
|
45
|
+
style={{ ...style, ...motionStyle(motion) }}
|
|
46
|
+
className={cx("watt-root flex flex-col gap-3", className)}
|
|
47
|
+
/>
|
|
48
|
+
</Context.Provider>
|
|
49
|
+
</MotionProvider>
|
|
35
50
|
);
|
|
36
51
|
},
|
|
37
52
|
);
|
|
@@ -68,7 +83,7 @@ const Fill = forwardRef<HTMLElement, PartProps>(
|
|
|
68
83
|
aria-hidden="true"
|
|
69
84
|
data-slot="battery-soc-fill"
|
|
70
85
|
className={cx(
|
|
71
|
-
"h-full bg-[var(--watt-accent)]
|
|
86
|
+
"watt-motion watt-soc-fill h-full bg-[var(--watt-accent)]",
|
|
72
87
|
className,
|
|
73
88
|
)}
|
|
74
89
|
style={{ width: `${value ?? 0}%`, ...style }}
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { createContext, forwardRef, useContext } from "react";
|
|
2
|
+
import { createContext, forwardRef, useContext, useRef } from "react";
|
|
3
|
+
import { useComposedRefs } from "@radix-ui/react-compose-refs";
|
|
3
4
|
import { Slot } from "@radix-ui/react-slot";
|
|
4
5
|
import { cx, part, useNow } from "../internal.js";
|
|
5
6
|
import { parseTimestamp, formatTime } from "../domain.js";
|
|
6
|
-
import type {
|
|
7
|
+
import type {
|
|
8
|
+
DataQuality,
|
|
9
|
+
MotionProps,
|
|
10
|
+
PartProps,
|
|
11
|
+
RootProps,
|
|
12
|
+
Timestamp,
|
|
13
|
+
} from "../types.js";
|
|
14
|
+
import { MotionProvider, motionStyle, useChangeFeedback } from "../motion.js";
|
|
7
15
|
interface Freshness {
|
|
8
16
|
timestamp: Timestamp | null;
|
|
9
17
|
quality?: DataQuality;
|
|
@@ -14,7 +22,7 @@ function useFreshness() {
|
|
|
14
22
|
if (!ctx) throw new Error("DataFreshness parts require DataFreshness.Root");
|
|
15
23
|
return ctx;
|
|
16
24
|
}
|
|
17
|
-
export interface DataFreshnessProps extends RootProps, Freshness {}
|
|
25
|
+
export interface DataFreshnessProps extends RootProps, Freshness, MotionProps {}
|
|
18
26
|
const Root = forwardRef<HTMLElement, DataFreshnessProps>(
|
|
19
27
|
(
|
|
20
28
|
{
|
|
@@ -23,29 +31,38 @@ const Root = forwardRef<HTMLElement, DataFreshnessProps>(
|
|
|
23
31
|
density = "comfortable",
|
|
24
32
|
asChild,
|
|
25
33
|
className,
|
|
34
|
+
motion,
|
|
35
|
+
style,
|
|
26
36
|
...props
|
|
27
37
|
},
|
|
28
38
|
ref,
|
|
29
39
|
) => {
|
|
40
|
+
const host = useRef<HTMLElement>(null);
|
|
41
|
+
const hostRef = useComposedRefs(host, ref);
|
|
42
|
+
useChangeFeedback(host, quality, motion);
|
|
30
43
|
const Component = asChild ? Slot : "span";
|
|
31
44
|
return (
|
|
32
|
-
<
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
className
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
<MotionProvider motion={motion}>
|
|
46
|
+
<Context.Provider value={{ timestamp, quality }}>
|
|
47
|
+
<Component
|
|
48
|
+
{...props}
|
|
49
|
+
ref={hostRef as never}
|
|
50
|
+
data-slot="data-freshness"
|
|
51
|
+
data-density={density}
|
|
52
|
+
data-quality={quality}
|
|
53
|
+
style={{ ...style, ...motionStyle(motion) }}
|
|
54
|
+
className={cx(
|
|
55
|
+
"watt-root watt-motion inline-flex items-center gap-2 text-sm text-[var(--watt-muted)] data-[density=compact]:gap-1 data-[density=compact]:text-xs",
|
|
56
|
+
className,
|
|
57
|
+
)}
|
|
58
|
+
/>
|
|
59
|
+
</Context.Provider>
|
|
60
|
+
</MotionProvider>
|
|
45
61
|
);
|
|
46
62
|
},
|
|
47
63
|
);
|
|
48
|
-
export interface FreshnessTimeProps
|
|
64
|
+
export interface FreshnessTimeProps
|
|
65
|
+
extends React.ComponentPropsWithoutRef<"time"> {
|
|
49
66
|
timeZone: string;
|
|
50
67
|
locale?: string;
|
|
51
68
|
formatter?: (timestamp: number) => React.ReactNode;
|
|
@@ -115,6 +115,29 @@ const Root = forwardRef<HTMLElement, EnergyFlowProps>(
|
|
|
115
115
|
[measure],
|
|
116
116
|
);
|
|
117
117
|
const hostRef = useComposedRefs(host, ref);
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
const root = host.current;
|
|
120
|
+
if (!root) return;
|
|
121
|
+
let visible = true;
|
|
122
|
+
const syncMotion = () => {
|
|
123
|
+
root.style.setProperty(
|
|
124
|
+
"--watt-flow-play-state",
|
|
125
|
+
visible && !document.hidden ? "running" : "paused",
|
|
126
|
+
);
|
|
127
|
+
};
|
|
128
|
+
const visibility = new IntersectionObserver(([entry]) => {
|
|
129
|
+
visible = entry?.isIntersecting ?? false;
|
|
130
|
+
syncMotion();
|
|
131
|
+
});
|
|
132
|
+
visibility.observe(root);
|
|
133
|
+
document.addEventListener("visibilitychange", syncMotion);
|
|
134
|
+
syncMotion();
|
|
135
|
+
return () => {
|
|
136
|
+
visibility.disconnect();
|
|
137
|
+
document.removeEventListener("visibilitychange", syncMotion);
|
|
138
|
+
root.style.removeProperty("--watt-flow-play-state");
|
|
139
|
+
};
|
|
140
|
+
}, []);
|
|
118
141
|
useEffect(() => {
|
|
119
142
|
observer.current = new ResizeObserver(measure);
|
|
120
143
|
if (host.current) observer.current.observe(host.current);
|
|
@@ -266,7 +289,7 @@ function Edge({
|
|
|
266
289
|
const duration = 3 / (settings.speed || 1);
|
|
267
290
|
const motionStyle = {
|
|
268
291
|
animationDuration: `${duration}s`,
|
|
269
|
-
animationPlayState: settings.speed === 0 ? "paused" : "running",
|
|
292
|
+
animationPlayState: settings.speed === 0 ? "paused" : "var(--watt-flow-play-state, running)",
|
|
270
293
|
} as React.CSSProperties;
|
|
271
294
|
return (
|
|
272
295
|
<LabelContext.Provider value={geometry.middle}>
|
|
@@ -1,27 +1,36 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { forwardRef } from "react";
|
|
2
|
+
import { forwardRef, useRef } from "react";
|
|
3
|
+
import { useComposedRefs } from "@radix-ui/react-compose-refs";
|
|
3
4
|
import { Slot } from "@radix-ui/react-slot";
|
|
4
5
|
import { cx, part } from "../internal.js";
|
|
5
6
|
import { formatNumber } from "../domain.js";
|
|
6
|
-
import type { PartProps, RootProps } from "../types.js";
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
import type { MotionProps, PartProps, RootProps } from "../types.js";
|
|
8
|
+
import { MotionProvider, motionStyle, useChangeFeedback } from "../motion.js";
|
|
9
|
+
export interface PowerMetricProps extends RootProps, MotionProps {}
|
|
10
|
+
const Root = forwardRef<HTMLElement, PowerMetricProps>(
|
|
11
|
+
(
|
|
12
|
+
{ asChild, density = "comfortable", motion, className, style, ...props },
|
|
13
|
+
ref,
|
|
14
|
+
) => {
|
|
9
15
|
const Component = asChild ? Slot : "div";
|
|
10
16
|
return (
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
className
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
<MotionProvider motion={motion}>
|
|
18
|
+
<Component
|
|
19
|
+
{...props}
|
|
20
|
+
ref={ref as never}
|
|
21
|
+
data-slot="power-metric"
|
|
22
|
+
data-density={density}
|
|
23
|
+
style={{ ...style, ...motionStyle(motion) }}
|
|
24
|
+
className={cx(
|
|
25
|
+
"watt-root flex flex-col gap-2 data-[density=compact]:gap-1",
|
|
26
|
+
className,
|
|
27
|
+
)}
|
|
28
|
+
/>
|
|
29
|
+
</MotionProvider>
|
|
21
30
|
);
|
|
22
31
|
},
|
|
23
32
|
);
|
|
24
|
-
export interface MetricValueProps extends PartProps {
|
|
33
|
+
export interface MetricValueProps extends PartProps, MotionProps {
|
|
25
34
|
value: number | null;
|
|
26
35
|
formatter?: (value: number) => React.ReactNode;
|
|
27
36
|
fallback?: React.ReactNode;
|
|
@@ -35,20 +44,26 @@ const Value = forwardRef<HTMLElement, MetricValueProps>(
|
|
|
35
44
|
fallback = "—",
|
|
36
45
|
children,
|
|
37
46
|
className,
|
|
47
|
+
motion,
|
|
48
|
+
style,
|
|
38
49
|
...props
|
|
39
50
|
},
|
|
40
51
|
ref,
|
|
41
52
|
) => {
|
|
53
|
+
const host = useRef<HTMLElement>(null);
|
|
54
|
+
const hostRef = useComposedRefs(host, ref);
|
|
55
|
+
useChangeFeedback(host, value, motion);
|
|
42
56
|
const Component = asChild ? Slot : "span";
|
|
43
57
|
return (
|
|
44
58
|
<Component
|
|
45
59
|
{...props}
|
|
46
|
-
ref={
|
|
60
|
+
ref={hostRef as never}
|
|
47
61
|
data-slot="power-metric-value"
|
|
48
62
|
className={cx(
|
|
49
|
-
"text-5xl font-medium tabular-nums tracking-tight",
|
|
63
|
+
"watt-motion text-5xl font-medium tabular-nums tracking-tight",
|
|
50
64
|
className,
|
|
51
65
|
)}
|
|
66
|
+
style={{ ...style, ...motionStyle(motion) }}
|
|
52
67
|
>
|
|
53
68
|
{children ??
|
|
54
69
|
(value !== null && Number.isFinite(value)
|
|
@@ -1,26 +1,45 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { forwardRef } from "react";
|
|
2
|
+
import { forwardRef, useRef } from "react";
|
|
3
|
+
import { useComposedRefs } from "@radix-ui/react-compose-refs";
|
|
3
4
|
import { Slot } from "@radix-ui/react-slot";
|
|
4
5
|
import { cx, part } from "../internal.js";
|
|
5
|
-
import type { EquipmentStatus, RootProps } from "../types.js";
|
|
6
|
-
|
|
6
|
+
import type { EquipmentStatus, MotionProps, RootProps } from "../types.js";
|
|
7
|
+
import { MotionProvider, motionStyle, useChangeFeedback } from "../motion.js";
|
|
8
|
+
export interface StatusIndicatorProps extends RootProps, MotionProps {
|
|
7
9
|
status: EquipmentStatus;
|
|
8
10
|
}
|
|
9
11
|
const Root = forwardRef<HTMLElement, StatusIndicatorProps>(
|
|
10
|
-
(
|
|
12
|
+
(
|
|
13
|
+
{
|
|
14
|
+
status,
|
|
15
|
+
density = "comfortable",
|
|
16
|
+
asChild,
|
|
17
|
+
motion,
|
|
18
|
+
className,
|
|
19
|
+
style,
|
|
20
|
+
...props
|
|
21
|
+
},
|
|
22
|
+
ref,
|
|
23
|
+
) => {
|
|
24
|
+
const host = useRef<HTMLElement>(null);
|
|
25
|
+
const hostRef = useComposedRefs(host, ref);
|
|
26
|
+
useChangeFeedback(host, status, motion);
|
|
11
27
|
const Component = asChild ? Slot : "span";
|
|
12
28
|
return (
|
|
13
|
-
<
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
className
|
|
22
|
-
|
|
23
|
-
|
|
29
|
+
<MotionProvider motion={motion}>
|
|
30
|
+
<Component
|
|
31
|
+
{...props}
|
|
32
|
+
ref={hostRef as never}
|
|
33
|
+
data-slot="status-indicator"
|
|
34
|
+
data-status={status}
|
|
35
|
+
data-density={density}
|
|
36
|
+
style={{ ...style, ...motionStyle(motion) }}
|
|
37
|
+
className={cx(
|
|
38
|
+
"watt-status watt-motion inline-flex items-center gap-2 text-sm data-[density=compact]:gap-1 data-[density=compact]:text-xs",
|
|
39
|
+
className,
|
|
40
|
+
)}
|
|
41
|
+
/>
|
|
42
|
+
</MotionProvider>
|
|
24
43
|
);
|
|
25
44
|
},
|
|
26
45
|
);
|