@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.
Files changed (54) hide show
  1. package/README.md +2 -1
  2. package/dist/components/alarm-feed.d.ts +2 -2
  3. package/dist/components/alarm-feed.d.ts.map +1 -1
  4. package/dist/components/alarm-feed.js +38 -16
  5. package/dist/components/alarm-feed.js.map +1 -1
  6. package/dist/components/asset-tree.d.ts +6 -2
  7. package/dist/components/asset-tree.d.ts.map +1 -1
  8. package/dist/components/asset-tree.js +15 -8
  9. package/dist/components/asset-tree.js.map +1 -1
  10. package/dist/components/battery-soc.d.ts +2 -2
  11. package/dist/components/battery-soc.d.ts.map +1 -1
  12. package/dist/components/battery-soc.js +4 -3
  13. package/dist/components/battery-soc.js.map +1 -1
  14. package/dist/components/data-freshness.d.ts +2 -2
  15. package/dist/components/data-freshness.d.ts.map +1 -1
  16. package/dist/components/data-freshness.js +8 -3
  17. package/dist/components/data-freshness.js.map +1 -1
  18. package/dist/components/energy-flow.d.ts.map +1 -1
  19. package/dist/components/energy-flow.js +22 -1
  20. package/dist/components/energy-flow.js.map +1 -1
  21. package/dist/components/power-metric.d.ts +5 -3
  22. package/dist/components/power-metric.d.ts.map +1 -1
  23. package/dist/components/power-metric.js +10 -5
  24. package/dist/components/power-metric.js.map +1 -1
  25. package/dist/components/status-indicator.d.ts +2 -2
  26. package/dist/components/status-indicator.d.ts.map +1 -1
  27. package/dist/components/status-indicator.js +8 -3
  28. package/dist/components/status-indicator.js.map +1 -1
  29. package/dist/components/telemetry-chart.d.ts +5 -7
  30. package/dist/components/telemetry-chart.d.ts.map +1 -1
  31. package/dist/components/telemetry-chart.js +30 -21
  32. package/dist/components/telemetry-chart.js.map +1 -1
  33. package/dist/index.d.ts +2 -2
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/motion.d.ts +12 -0
  36. package/dist/motion.d.ts.map +1 -0
  37. package/dist/motion.js +90 -0
  38. package/dist/motion.js.map +1 -0
  39. package/dist/styles.css +39 -0
  40. package/dist/types.d.ts +10 -1
  41. package/dist/types.d.ts.map +1 -1
  42. package/package.json +1 -1
  43. package/src/components/alarm-feed.tsx +97 -62
  44. package/src/components/asset-tree.tsx +56 -31
  45. package/src/components/battery-soc.tsx +29 -14
  46. package/src/components/data-freshness.tsx +34 -17
  47. package/src/components/energy-flow.tsx +24 -1
  48. package/src/components/power-metric.tsx +32 -17
  49. package/src/components/status-indicator.tsx +34 -15
  50. package/src/components/telemetry-chart.tsx +60 -38
  51. package/src/index.ts +5 -1
  52. package/src/motion.tsx +117 -0
  53. package/src/styles.css +39 -0
  54. 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
- export interface AlarmFeedProps<T extends AlarmEvent = AlarmEvent> extends Omit<
10
- React.HTMLAttributes<HTMLDivElement>,
11
- "children"
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
- <div
52
- {...props}
53
- ref={hostRef}
54
- tabIndex={props.tabIndex ?? (virtualized ? 0 : undefined)}
55
- data-slot="alarm-feed"
56
- data-density={density}
57
- className={cx("watt-root overflow-auto", className)}
58
- style={{ ...(virtualized ? { height } : {}), ...style }}
59
- >
60
- {!items.length ? (
61
- empty
62
- ) : (
63
- <div
64
- role="list"
65
- style={
66
- virtualized
67
- ? { height: virtualizer.getTotalSize(), position: "relative" }
68
- : undefined
69
- }
70
- >
71
- {rows.map((row) => {
72
- const item = items[row.index]!;
73
- return (
74
- <div
75
- role="listitem"
76
- key={row.key}
77
- data-index={row.index}
78
- ref={virtualized ? virtualizer.measureElement : undefined}
79
- aria-posinset={row.index + 1}
80
- aria-setsize={items.length}
81
- data-slot="alarm-feed-item"
82
- data-severity={item.severity}
83
- data-state={item.status}
84
- className={cx(
85
- "border-b border-[var(--watt-border)] last:border-0",
86
- density === "compact" ? "py-2.5" : "py-4",
87
- )}
88
- style={
89
- virtualized
90
- ? {
91
- position: "absolute",
92
- top: 0,
93
- left: 0,
94
- width: "100%",
95
- transform: `translateY(${row.start}px)`,
96
- }
97
- : undefined
98
- }
99
- >
100
- {children(item, row.index)}
101
- </div>
102
- );
103
- })}
104
- </div>
105
- )}
106
- </div>
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> extends Omit<
34
- React.HTMLAttributes<HTMLDivElement>,
35
- "children"
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
- className="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)]"
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
- <div
228
- {...containerProps}
229
- {...props}
230
- ref={hostRef}
231
- data-slot="asset-tree"
232
- data-density={density}
233
- className={cx("watt-root overflow-auto text-sm", className)}
234
- style={{ height, ...style }}
235
- >
236
- {!items.length ? (
237
- empty
238
- ) : (
239
- <div
240
- role="presentation"
241
- style={{ height: virtualizer.getTotalSize(), position: "relative" }}
242
- >
243
- {virtualizer.getVirtualItems().map((row) => {
244
- const item = visible[row.index];
245
- return item && indexed.has(item.getId())
246
- ? renderRow(item, row.index, row.start)
247
- : null;
248
- })}
249
- </div>
250
- )}
251
- </div>
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
- ({ value, asChild, density = "comfortable", className, ...props }, ref) => {
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
- <Context.Provider value={{ value: valid ? value : null, density }}>
26
- <Component
27
- {...props}
28
- ref={ref as never}
29
- data-slot="battery-soc"
30
- data-invalid={!valid || undefined}
31
- data-density={density}
32
- className={cx("watt-root flex flex-col gap-3", className)}
33
- />
34
- </Context.Provider>
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)] transition-[width] duration-500 motion-reduce:transition-none",
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 { DataQuality, PartProps, RootProps, Timestamp } from "../types.js";
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
- <Context.Provider value={{ timestamp, quality }}>
33
- <Component
34
- {...props}
35
- ref={ref as never}
36
- data-slot="data-freshness"
37
- data-density={density}
38
- data-quality={quality}
39
- className={cx(
40
- "watt-root inline-flex items-center gap-2 text-sm text-[var(--watt-muted)] data-[density=compact]:gap-1 data-[density=compact]:text-xs",
41
- className,
42
- )}
43
- />
44
- </Context.Provider>
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 extends React.ComponentPropsWithoutRef<"time"> {
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
- const Root = forwardRef<HTMLElement, RootProps>(
8
- ({ asChild, density = "comfortable", className, ...props }, ref) => {
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
- <Component
12
- {...props}
13
- ref={ref as never}
14
- data-slot="power-metric"
15
- data-density={density}
16
- className={cx(
17
- "watt-root flex flex-col gap-2 data-[density=compact]:gap-1",
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={ref as never}
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
- export interface StatusIndicatorProps extends RootProps {
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
- ({ status, density = "comfortable", asChild, className, ...props }, ref) => {
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
- <Component
14
- {...props}
15
- ref={ref as never}
16
- data-slot="status-indicator"
17
- data-status={status}
18
- data-density={density}
19
- className={cx(
20
- "watt-status inline-flex items-center gap-2 text-sm data-[density=compact]:gap-1 data-[density=compact]:text-xs",
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
  );