@fcannizzaro/streamdeck-react 0.1.9 → 0.1.11

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 (74) hide show
  1. package/LICENSE +190 -21
  2. package/README.md +3 -1
  3. package/dist/action.d.ts +2 -2
  4. package/dist/action.js +2 -2
  5. package/dist/bundler-shared.d.ts +11 -0
  6. package/dist/bundler-shared.js +11 -0
  7. package/dist/context/event-bus.d.ts +1 -1
  8. package/dist/context/event-bus.js +1 -1
  9. package/dist/context/touchstrip-context.d.ts +2 -0
  10. package/dist/context/touchstrip-context.js +5 -0
  11. package/dist/devtools/bridge.d.ts +35 -7
  12. package/dist/devtools/bridge.js +153 -46
  13. package/dist/devtools/highlight.d.ts +6 -0
  14. package/dist/devtools/highlight.js +106 -57
  15. package/dist/devtools/index.js +6 -0
  16. package/dist/devtools/observers/lifecycle.d.ts +4 -4
  17. package/dist/devtools/server.d.ts +6 -1
  18. package/dist/devtools/server.js +6 -1
  19. package/dist/devtools/types.d.ts +50 -6
  20. package/dist/font-inline.d.ts +5 -1
  21. package/dist/font-inline.js +8 -3
  22. package/dist/hooks/animation.d.ts +154 -0
  23. package/dist/hooks/animation.js +381 -0
  24. package/dist/hooks/events.js +1 -5
  25. package/dist/hooks/touchstrip.d.ts +6 -0
  26. package/dist/hooks/touchstrip.js +37 -0
  27. package/dist/index.d.ts +7 -2
  28. package/dist/index.js +3 -2
  29. package/dist/manifest-codegen.d.ts +38 -0
  30. package/dist/manifest-codegen.js +110 -0
  31. package/dist/node_modules/.bun/xxhash-wasm@1.1.0/node_modules/xxhash-wasm/esm/xxhash-wasm.js +3157 -0
  32. package/dist/plugin.js +20 -9
  33. package/dist/reconciler/host-config.js +19 -1
  34. package/dist/reconciler/vnode.d.ts +26 -0
  35. package/dist/reconciler/vnode.js +41 -10
  36. package/dist/render/buffer-pool.d.ts +19 -0
  37. package/dist/render/buffer-pool.js +51 -0
  38. package/dist/render/cache.d.ts +41 -0
  39. package/dist/render/cache.js +159 -5
  40. package/dist/render/image-cache.d.ts +53 -0
  41. package/dist/render/image-cache.js +128 -0
  42. package/dist/render/metrics.d.ts +58 -0
  43. package/dist/render/metrics.js +101 -0
  44. package/dist/render/pipeline.d.ts +46 -1
  45. package/dist/render/pipeline.js +370 -36
  46. package/dist/render/png.d.ts +10 -1
  47. package/dist/render/png.js +31 -13
  48. package/dist/render/render-pool.d.ts +26 -0
  49. package/dist/render/render-pool.js +141 -0
  50. package/dist/render/svg.d.ts +7 -0
  51. package/dist/render/svg.js +139 -0
  52. package/dist/render/worker.d.ts +1 -0
  53. package/dist/rollup.d.ts +23 -9
  54. package/dist/rollup.js +24 -9
  55. package/dist/roots/flush-coordinator.d.ts +18 -0
  56. package/dist/roots/flush-coordinator.js +38 -0
  57. package/dist/roots/registry.d.ts +6 -4
  58. package/dist/roots/registry.js +47 -33
  59. package/dist/roots/root.d.ts +32 -2
  60. package/dist/roots/root.js +104 -14
  61. package/dist/roots/settings-equality.d.ts +5 -0
  62. package/dist/roots/settings-equality.js +24 -0
  63. package/dist/roots/touchstrip-root.d.ts +93 -0
  64. package/dist/roots/touchstrip-root.js +383 -0
  65. package/dist/types.d.ts +62 -16
  66. package/dist/vite.d.ts +22 -8
  67. package/dist/vite.js +24 -8
  68. package/package.json +5 -4
  69. package/dist/context/touchbar-context.d.ts +0 -2
  70. package/dist/context/touchbar-context.js +0 -5
  71. package/dist/hooks/touchbar.d.ts +0 -6
  72. package/dist/hooks/touchbar.js +0 -37
  73. package/dist/roots/touchbar-root.d.ts +0 -45
  74. package/dist/roots/touchbar-root.js +0 -175
@@ -81,7 +81,7 @@ export interface SnapshotAction {
81
81
  tree: SerializedVNode | null;
82
82
  dataUri: string | null;
83
83
  }
84
- export interface SnapshotTouchBar {
84
+ export interface SnapshotTouchStrip {
85
85
  deviceId: string;
86
86
  deviceName: string;
87
87
  canvas: {
@@ -98,10 +98,11 @@ export interface SnapshotTouchBar {
98
98
  export interface SnapshotMessage extends BaseMessage {
99
99
  type: "snapshot";
100
100
  actions: SnapshotAction[];
101
- touchBars: SnapshotTouchBar[];
101
+ touchStrips: SnapshotTouchStrip[];
102
102
  recentConsole: ConsoleMessage[];
103
103
  recentNetwork: (NetworkRequestMessage | NetworkResponseMessage | NetworkErrorMessage)[];
104
104
  recentEvents: EventBusMessage[];
105
+ metrics?: MetricsData;
105
106
  }
106
107
  export interface ConsoleMessage extends BaseMessage {
107
108
  type: "console";
@@ -145,9 +146,11 @@ export interface RenderMessage extends BaseMessage {
145
146
  tree: SerializedVNode;
146
147
  dataUri: string;
147
148
  renderMs: number;
149
+ /** Per-render pipeline timing profile, when available. */
150
+ profile?: ProfileData;
148
151
  }
149
- export interface TouchBarRenderMessage extends BaseMessage {
150
- type: "render:touchbar";
152
+ export interface TouchStripRenderMessage extends BaseMessage {
153
+ type: "render:touchstrip";
151
154
  deviceId: string;
152
155
  canvas: {
153
156
  width: number;
@@ -160,6 +163,8 @@ export interface TouchBarRenderMessage extends BaseMessage {
160
163
  dataUri: string;
161
164
  }>;
162
165
  renderMs: number;
166
+ /** Per-render pipeline timing profile, when available. */
167
+ profile?: ProfileData;
163
168
  }
164
169
  export interface EventBusMessage extends BaseMessage {
165
170
  type: "event";
@@ -174,7 +179,7 @@ export interface LifecycleMessage extends BaseMessage {
174
179
  event: "appear" | "disappear";
175
180
  actionId: string;
176
181
  actionUuid: string;
177
- surface: "key" | "dial" | "touch" | "touchbar";
182
+ surface: "key" | "dial" | "touch" | "touchstrip";
178
183
  device: {
179
184
  id: string;
180
185
  type: number;
@@ -196,6 +201,45 @@ export interface HighlightRenderMessage extends BaseMessage {
196
201
  /** Data URI of the rendered image with highlight overlay, or null to clear. */
197
202
  dataUri: string | null;
198
203
  }
204
+ /** Per-render pipeline timing data, embedded in RenderMessage. */
205
+ export interface ProfileData {
206
+ vnodeToElementMs: number;
207
+ fromJsxMs: number;
208
+ takumiRenderMs: number;
209
+ hashMs: number;
210
+ base64Ms: number;
211
+ totalMs: number;
212
+ skipped: boolean;
213
+ /** Whether this render was served from the image cache. */
214
+ cacheHit: boolean;
215
+ treeDepth: number;
216
+ nodeCount: number;
217
+ }
218
+ /** Aggregate render metrics snapshot. */
219
+ export interface MetricsData {
220
+ /** Total flush() calls (render attempts). */
221
+ flushCount: number;
222
+ /** Flushes that reached the Takumi renderer. */
223
+ renderCount: number;
224
+ /** Tree hash cache hits. */
225
+ cacheHitCount: number;
226
+ /** Skipped due to clean tree (dirty flag check). */
227
+ dirtySkipCount: number;
228
+ /** Skipped due to identical output (post-render FNV-1a dedup). */
229
+ hashDedupCount: number;
230
+ /** Average Takumi render time in milliseconds. */
231
+ avgRenderMs: number;
232
+ /** Peak (worst-case) render time in milliseconds. */
233
+ peakRenderMs: number;
234
+ /** Image cache memory usage in bytes. */
235
+ imageCacheBytes: number;
236
+ /** TouchStrip cache memory usage in bytes. */
237
+ touchstripCacheBytes: number;
238
+ }
239
+ export interface MetricsMessage extends BaseMessage {
240
+ type: "metrics";
241
+ metrics: MetricsData;
242
+ }
199
243
  export interface RequestSnapshotMessage extends BaseMessage {
200
244
  type: "request:snapshot";
201
245
  }
@@ -207,6 +251,6 @@ export interface HighlightActionMessage extends BaseMessage {
207
251
  nodeId: number | null;
208
252
  }
209
253
  /** Messages the plugin server sends to browser clients (via SSE). */
210
- export type ServerMessage = ServerInfoMessage | SnapshotMessage | ConsoleMessage | NetworkRequestMessage | NetworkResponseMessage | NetworkErrorMessage | RenderMessage | TouchBarRenderMessage | EventBusMessage | LifecycleMessage | HighlightRenderMessage;
254
+ export type ServerMessage = ServerInfoMessage | SnapshotMessage | ConsoleMessage | NetworkRequestMessage | NetworkResponseMessage | NetworkErrorMessage | RenderMessage | TouchStripRenderMessage | EventBusMessage | LifecycleMessage | HighlightRenderMessage | MetricsMessage;
211
255
  /** Messages browser clients send to the plugin server (via POST /message). */
212
256
  export type ClientMessage = RequestSnapshotMessage | HighlightActionMessage;
@@ -18,5 +18,9 @@ export declare function resolveFontId(source: string, importer: string | undefin
18
18
  *
19
19
  * Follows symlinks before reading so that bun's internal
20
20
  * `node_modules/.bun/` layout resolves to the real file.
21
+ *
22
+ * Async: uses `fs.promises.readFile()` to avoid blocking the bundler
23
+ * event loop for large fonts (1-2 MB). Both Vite and Rollup `load`
24
+ * hooks support async return values.
21
25
  */
22
- export declare function loadFont(id: string): string | null;
26
+ export declare function loadFont(id: string): Promise<string | null>;
@@ -1,6 +1,7 @@
1
1
  import { dirname, resolve } from "node:path";
2
2
  import { createRequire } from "node:module";
3
- import { readFileSync, realpathSync } from "node:fs";
3
+ import { realpathSync } from "node:fs";
4
+ import { readFile } from "node:fs/promises";
4
5
  //#region src/font-inline.ts
5
6
  var FONT_RE = /\.(ttf|otf|woff2?)$/;
6
7
  /**
@@ -33,10 +34,14 @@ function resolveFontId(source, importer) {
33
34
  *
34
35
  * Follows symlinks before reading so that bun's internal
35
36
  * `node_modules/.bun/` layout resolves to the real file.
37
+ *
38
+ * Async: uses `fs.promises.readFile()` to avoid blocking the bundler
39
+ * event loop for large fonts (1-2 MB). Both Vite and Rollup `load`
40
+ * hooks support async return values.
36
41
  */
37
- function loadFont(id) {
42
+ async function loadFont(id) {
38
43
  if (!isFontFile(id)) return null;
39
- return `export default Buffer.from("${readFileSync(safeRealpath(id) ?? id).toString("base64")}", "base64");`;
44
+ return `export default Buffer.from("${(await readFile(safeRealpath(id) ?? id)).toString("base64")}", "base64");`;
40
45
  }
41
46
  function safeRealpath(p) {
42
47
  try {
@@ -0,0 +1,154 @@
1
+ /** A single number or a flat object of named numbers. */
2
+ export type AnimationTarget = number | Record<string, number>;
3
+ /** Maps an AnimationTarget shape to its animated output shape. */
4
+ export type AnimatedValue<T extends AnimationTarget> = T extends number ? number : {
5
+ [K in keyof T]: number;
6
+ };
7
+ export interface SpringConfig {
8
+ /** Stiffness coefficient (force per unit displacement). @default 170 */
9
+ tension: number;
10
+ /** Damping coefficient (force per unit velocity). @default 26 */
11
+ friction: number;
12
+ /** Mass of the simulated object. @default 1 */
13
+ mass: number;
14
+ /** Absolute velocity threshold below which the spring settles. @default 0.01 */
15
+ velocityThreshold: number;
16
+ /** Absolute displacement threshold below which the spring settles. @default 0.005 */
17
+ displacementThreshold: number;
18
+ /** Clamp output to target (no overshoot). @default false */
19
+ clamp: boolean;
20
+ }
21
+ export interface SpringResult<T extends AnimationTarget> {
22
+ /** Current interpolated value(s). */
23
+ value: AnimatedValue<T>;
24
+ /** Whether the spring is still in motion. */
25
+ isAnimating: boolean;
26
+ /** Imperatively update the target. */
27
+ set: (target: T) => void;
28
+ /** Jump immediately to a value (no animation). */
29
+ jump: (target: T) => void;
30
+ }
31
+ export type EasingName = "linear" | "easeIn" | "easeOut" | "easeInOut" | "easeInCubic" | "easeOutCubic" | "easeInOutCubic" | "easeInBack" | "easeOutBack" | "easeOutBounce";
32
+ export type EasingFn = (t: number) => number;
33
+ export interface TweenConfig {
34
+ /** Duration in milliseconds. @default 300 */
35
+ duration: number;
36
+ /** Easing function name or custom (t: number) => number. @default "easeOut" */
37
+ easing: EasingName | EasingFn;
38
+ /** Target FPS for the animation tick loop. @default 60 */
39
+ fps: number;
40
+ }
41
+ export interface TweenResult<T extends AnimationTarget> {
42
+ /** Current interpolated value(s). */
43
+ value: AnimatedValue<T>;
44
+ /** 0..1 normalized progress of the current transition. */
45
+ progress: number;
46
+ /** Whether the tween is still running. */
47
+ isAnimating: boolean;
48
+ /** Imperatively update the target (starts a new tween from current value). */
49
+ set: (target: T) => void;
50
+ /** Jump immediately to a value (no animation). */
51
+ jump: (target: T) => void;
52
+ }
53
+ interface SpringState {
54
+ position: number;
55
+ velocity: number;
56
+ }
57
+ /**
58
+ * Semi-implicit Euler step for a damped harmonic oscillator.
59
+ * Updates velocity first, then position — stable at low frame rates.
60
+ */
61
+ export declare function stepSpring(state: SpringState, target: number, config: SpringConfig, dtSeconds: number): SpringState;
62
+ /** Check if a spring channel has come to rest. */
63
+ export declare function isSettled(state: SpringState, target: number, config: SpringConfig): boolean;
64
+ export declare const SpringPresets: {
65
+ /** Default balanced spring. */
66
+ readonly default: {
67
+ readonly tension: 170;
68
+ readonly friction: 26;
69
+ readonly mass: 1;
70
+ };
71
+ /** Quick and responsive with slight overshoot. Good for press feedback. */
72
+ readonly stiff: {
73
+ readonly tension: 400;
74
+ readonly friction: 28;
75
+ readonly mass: 1;
76
+ };
77
+ /** Bouncy with visible oscillation. Good for playful UIs. */
78
+ readonly wobbly: {
79
+ readonly tension: 180;
80
+ readonly friction: 12;
81
+ readonly mass: 1;
82
+ };
83
+ /** Slow and smooth. Good for background transitions. */
84
+ readonly gentle: {
85
+ readonly tension: 120;
86
+ readonly friction: 14;
87
+ readonly mass: 1;
88
+ };
89
+ /** Very slow, molasses-like. Good for ambient drift. */
90
+ readonly molasses: {
91
+ readonly tension: 80;
92
+ readonly friction: 30;
93
+ readonly mass: 1;
94
+ };
95
+ /** Snappy with no overshoot. Good for precise UI elements. */
96
+ readonly snap: {
97
+ readonly tension: 300;
98
+ readonly friction: 36;
99
+ readonly mass: 1;
100
+ readonly clamp: true;
101
+ };
102
+ /** Heavy object feel. */
103
+ readonly heavy: {
104
+ readonly tension: 200;
105
+ readonly friction: 20;
106
+ readonly mass: 3;
107
+ };
108
+ };
109
+ export declare const Easings: Record<EasingName, EasingFn>;
110
+ /**
111
+ * Spring physics-based animation hook.
112
+ *
113
+ * Returns animated value(s) that follow the target with natural spring dynamics
114
+ * (damped harmonic oscillator). Supports single numbers and objects of numbers.
115
+ *
116
+ * Automatically starts/stops the tick loop when the spring is in motion or settled.
117
+ *
118
+ * @example
119
+ * ```tsx
120
+ * const { value: scale } = useSpring(pressed ? 0.85 : 1, SpringPresets.wobbly);
121
+ * ```
122
+ *
123
+ * @example
124
+ * ```tsx
125
+ * const { value } = useSpring({ x: targetX, opacity: show ? 1 : 0 }, SpringPresets.gentle);
126
+ * // value.x and value.opacity are plain numbers
127
+ * ```
128
+ */
129
+ export declare function useSpring<T extends AnimationTarget>(target: T, config?: Partial<SpringConfig> & {
130
+ fps?: number;
131
+ }): SpringResult<T>;
132
+ /**
133
+ * Duration + easing-based animation hook.
134
+ *
135
+ * Returns animated value(s) that smoothly transition to the target over the
136
+ * specified duration using an easing curve. Supports single numbers and objects
137
+ * of numbers.
138
+ *
139
+ * When the target changes mid-tween, a new tween starts from the current
140
+ * interpolated position (no discontinuity).
141
+ *
142
+ * @example
143
+ * ```tsx
144
+ * const { value: opacity } = useTween(visible ? 1 : 0, { duration: 500, easing: "easeInOut" });
145
+ * ```
146
+ *
147
+ * @example
148
+ * ```tsx
149
+ * const { value } = useTween({ y: expanded ? 0 : -50, opacity: expanded ? 1 : 0 });
150
+ * // value.y and value.opacity are plain numbers
151
+ * ```
152
+ */
153
+ export declare function useTween<T extends AnimationTarget>(target: T, config?: Partial<TweenConfig>): TweenResult<T>;
154
+ export {};
@@ -0,0 +1,381 @@
1
+ import { usePrevious, useTick } from "./utility.js";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
+ //#region src/hooks/animation.ts
4
+ var SPRING_DEFAULTS = {
5
+ tension: 170,
6
+ friction: 26,
7
+ mass: 1,
8
+ velocityThreshold: .01,
9
+ displacementThreshold: .005,
10
+ clamp: false
11
+ };
12
+ var TWEEN_DEFAULTS = {
13
+ duration: 300,
14
+ easing: "easeOut",
15
+ fps: 60
16
+ };
17
+ /** Max dt cap to prevent spring explosion after long pauses.
18
+ * If the process is suspended (debugger, GC pause), dt could be
19
+ * seconds-long, causing the spring to overshoot wildly. Capping
20
+ * at ~64ms (roughly one frame at 15fps) keeps the simulation stable. */
21
+ var MAX_DT_SEC = .064;
22
+ /**
23
+ * Semi-implicit Euler step for a damped harmonic oscillator.
24
+ * Updates velocity first, then position — stable at low frame rates.
25
+ */
26
+ function stepSpring(state, target, config, dtSeconds) {
27
+ const { tension, friction, mass } = config;
28
+ const displacement = state.position - target;
29
+ const acceleration = (-tension * displacement + -friction * state.velocity) / mass;
30
+ let velocity = state.velocity + acceleration * dtSeconds;
31
+ let position = state.position + velocity * dtSeconds;
32
+ if (config.clamp) {
33
+ if (displacement > 0 && position < target || displacement < 0 && position > target) {
34
+ position = target;
35
+ velocity = 0;
36
+ }
37
+ }
38
+ return {
39
+ position,
40
+ velocity
41
+ };
42
+ }
43
+ /** Check if a spring channel has come to rest. */
44
+ function isSettled(state, target, config) {
45
+ return Math.abs(state.velocity) < config.velocityThreshold && Math.abs(state.position - target) < config.displacementThreshold;
46
+ }
47
+ var SpringPresets = {
48
+ default: {
49
+ tension: 170,
50
+ friction: 26,
51
+ mass: 1
52
+ },
53
+ stiff: {
54
+ tension: 400,
55
+ friction: 28,
56
+ mass: 1
57
+ },
58
+ wobbly: {
59
+ tension: 180,
60
+ friction: 12,
61
+ mass: 1
62
+ },
63
+ gentle: {
64
+ tension: 120,
65
+ friction: 14,
66
+ mass: 1
67
+ },
68
+ molasses: {
69
+ tension: 80,
70
+ friction: 30,
71
+ mass: 1
72
+ },
73
+ snap: {
74
+ tension: 300,
75
+ friction: 36,
76
+ mass: 1,
77
+ clamp: true
78
+ },
79
+ heavy: {
80
+ tension: 200,
81
+ friction: 20,
82
+ mass: 3
83
+ }
84
+ };
85
+ var Easings = {
86
+ linear: (t) => t,
87
+ easeIn: (t) => t * t,
88
+ easeOut: (t) => t * (2 - t),
89
+ easeInOut: (t) => t < .5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
90
+ easeInCubic: (t) => t * t * t,
91
+ easeOutCubic: (t) => 1 - (1 - t) ** 3,
92
+ easeInOutCubic: (t) => t < .5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2,
93
+ easeInBack: (t) => {
94
+ const c = 1.70158;
95
+ return (c + 1) * t * t * t - c * t * t;
96
+ },
97
+ easeOutBack: (t) => {
98
+ const c = 1.70158;
99
+ return 1 + (c + 1) * (t - 1) ** 3 + c * (t - 1) ** 2;
100
+ },
101
+ easeOutBounce: (t) => {
102
+ const n1 = 7.5625;
103
+ const d1 = 2.75;
104
+ if (t < 1 / d1) return n1 * t * t;
105
+ if (t < 2 / d1) return n1 * (t -= 1.5 / d1) * t + .75;
106
+ if (t < 2.5 / d1) return n1 * (t -= 2.25 / d1) * t + .9375;
107
+ return n1 * (t -= 2.625 / d1) * t + .984375;
108
+ }
109
+ };
110
+ function resolveEasing(easing) {
111
+ return typeof easing === "function" ? easing : Easings[easing];
112
+ }
113
+ var SCALAR_KEY = "_";
114
+ function toChannelMap(target) {
115
+ if (typeof target === "number") return new Map([[SCALAR_KEY, target]]);
116
+ return new Map(Object.entries(target));
117
+ }
118
+ function snapshotValue(target) {
119
+ if (typeof target === "number") return target;
120
+ const result = {};
121
+ for (const [key, val] of Object.entries(target)) result[key] = val;
122
+ return result;
123
+ }
124
+ function snapshotFromSpringState(state, isObject) {
125
+ if (!isObject) return state.get(SCALAR_KEY).position;
126
+ const result = {};
127
+ for (const [key, s] of state) result[key] = s.position;
128
+ return result;
129
+ }
130
+ function snapshotFromMap(map, isObject) {
131
+ if (!isObject) return map.get(SCALAR_KEY);
132
+ const result = {};
133
+ for (const [key, val] of map) result[key] = val;
134
+ return result;
135
+ }
136
+ function forEachChannel(target, fn) {
137
+ if (typeof target === "number") fn(SCALAR_KEY, target);
138
+ else for (const [key, val] of Object.entries(target)) fn(key, val);
139
+ }
140
+ function shallowEqual(a, b) {
141
+ if (typeof a === "number" && typeof b === "number") return a === b;
142
+ if (typeof a !== typeof b) return false;
143
+ const aObj = a;
144
+ const bObj = b;
145
+ const aKeys = Object.keys(aObj);
146
+ const bKeys = Object.keys(bObj);
147
+ if (aKeys.length !== bKeys.length) return false;
148
+ return aKeys.every((k) => aObj[k] === bObj[k]);
149
+ }
150
+ function initializeSpringState(target) {
151
+ const map = /* @__PURE__ */ new Map();
152
+ forEachChannel(target, (key, value) => {
153
+ map.set(key, {
154
+ position: value,
155
+ velocity: 0
156
+ });
157
+ });
158
+ return map;
159
+ }
160
+ function snapSpringToTarget(state, target) {
161
+ forEachChannel(target, (key, value) => {
162
+ state.set(key, {
163
+ position: value,
164
+ velocity: 0
165
+ });
166
+ });
167
+ }
168
+ /**
169
+ * Spring physics-based animation hook.
170
+ *
171
+ * Returns animated value(s) that follow the target with natural spring dynamics
172
+ * (damped harmonic oscillator). Supports single numbers and objects of numbers.
173
+ *
174
+ * Automatically starts/stops the tick loop when the spring is in motion or settled.
175
+ *
176
+ * @example
177
+ * ```tsx
178
+ * const { value: scale } = useSpring(pressed ? 0.85 : 1, SpringPresets.wobbly);
179
+ * ```
180
+ *
181
+ * @example
182
+ * ```tsx
183
+ * const { value } = useSpring({ x: targetX, opacity: show ? 1 : 0 }, SpringPresets.gentle);
184
+ * // value.x and value.opacity are plain numbers
185
+ * ```
186
+ */
187
+ function useSpring(target, config) {
188
+ const resolvedConfig = useMemo(() => ({
189
+ ...SPRING_DEFAULTS,
190
+ ...config
191
+ }), [
192
+ config?.tension,
193
+ config?.friction,
194
+ config?.mass,
195
+ config?.velocityThreshold,
196
+ config?.displacementThreshold,
197
+ config?.clamp
198
+ ]);
199
+ const fps = config?.fps ?? 60;
200
+ const isObject = typeof target === "object" && target !== null;
201
+ const stateRef = useRef(null);
202
+ if (stateRef.current === null) stateRef.current = initializeSpringState(target);
203
+ const targetRef = useRef(target);
204
+ targetRef.current = target;
205
+ const configRef = useRef(resolvedConfig);
206
+ configRef.current = resolvedConfig;
207
+ const [value, setValue] = useState(() => snapshotValue(target));
208
+ const [isAnimating, setIsAnimating] = useState(false);
209
+ const prevTarget = usePrevious(target);
210
+ useEffect(() => {
211
+ if (prevTarget !== void 0 && !shallowEqual(prevTarget, target)) {
212
+ const state = stateRef.current;
213
+ forEachChannel(target, (key, val) => {
214
+ if (!state.has(key)) state.set(key, {
215
+ position: val,
216
+ velocity: 0
217
+ });
218
+ });
219
+ setIsAnimating(true);
220
+ }
221
+ }, [prevTarget, target]);
222
+ useTick((deltaMs) => {
223
+ const dt = Math.min(deltaMs / 1e3, MAX_DT_SEC);
224
+ const state = stateRef.current;
225
+ const currentTarget = targetRef.current;
226
+ const cfg = configRef.current;
227
+ let allSettled = true;
228
+ forEachChannel(currentTarget, (key, targetVal) => {
229
+ const channelState = state.get(key);
230
+ if (!channelState) return;
231
+ const next = stepSpring(channelState, targetVal, cfg, dt);
232
+ state.set(key, next);
233
+ if (!isSettled(next, targetVal, cfg)) allSettled = false;
234
+ });
235
+ if (allSettled) {
236
+ snapSpringToTarget(state, currentTarget);
237
+ setValue(snapshotValue(currentTarget));
238
+ setIsAnimating(false);
239
+ } else setValue(snapshotFromSpringState(state, isObject));
240
+ }, isAnimating ? fps : false);
241
+ const set = useCallback((newTarget) => {
242
+ targetRef.current = newTarget;
243
+ const state = stateRef.current;
244
+ forEachChannel(newTarget, (key, val) => {
245
+ if (!state.has(key)) state.set(key, {
246
+ position: val,
247
+ velocity: 0
248
+ });
249
+ });
250
+ setIsAnimating(true);
251
+ }, []);
252
+ const jump = useCallback((newTarget) => {
253
+ targetRef.current = newTarget;
254
+ snapSpringToTarget(stateRef.current, newTarget);
255
+ setValue(snapshotValue(newTarget));
256
+ setIsAnimating(false);
257
+ }, []);
258
+ return useMemo(() => ({
259
+ value,
260
+ isAnimating,
261
+ set,
262
+ jump
263
+ }), [
264
+ value,
265
+ isAnimating,
266
+ set,
267
+ jump
268
+ ]);
269
+ }
270
+ function interpolateTween(tween, easingFn) {
271
+ const easedT = easingFn(tween.duration > 0 ? Math.min(tween.elapsed / tween.duration, 1) : 1);
272
+ const result = /* @__PURE__ */ new Map();
273
+ for (const [key, fromVal] of tween.from) {
274
+ const toVal = tween.to.get(key) ?? fromVal;
275
+ result.set(key, fromVal + (toVal - fromVal) * easedT);
276
+ }
277
+ return result;
278
+ }
279
+ /**
280
+ * Duration + easing-based animation hook.
281
+ *
282
+ * Returns animated value(s) that smoothly transition to the target over the
283
+ * specified duration using an easing curve. Supports single numbers and objects
284
+ * of numbers.
285
+ *
286
+ * When the target changes mid-tween, a new tween starts from the current
287
+ * interpolated position (no discontinuity).
288
+ *
289
+ * @example
290
+ * ```tsx
291
+ * const { value: opacity } = useTween(visible ? 1 : 0, { duration: 500, easing: "easeInOut" });
292
+ * ```
293
+ *
294
+ * @example
295
+ * ```tsx
296
+ * const { value } = useTween({ y: expanded ? 0 : -50, opacity: expanded ? 1 : 0 });
297
+ * // value.y and value.opacity are plain numbers
298
+ * ```
299
+ */
300
+ function useTween(target, config) {
301
+ const duration = config?.duration ?? TWEEN_DEFAULTS.duration;
302
+ const easingFn = resolveEasing(config?.easing ?? TWEEN_DEFAULTS.easing);
303
+ const fps = config?.fps ?? TWEEN_DEFAULTS.fps;
304
+ const isObject = typeof target === "object" && target !== null;
305
+ const tweenRef = useRef(null);
306
+ if (tweenRef.current === null) {
307
+ const initial = toChannelMap(target);
308
+ tweenRef.current = {
309
+ from: new Map(initial),
310
+ to: new Map(initial),
311
+ elapsed: duration,
312
+ duration
313
+ };
314
+ }
315
+ const targetRef = useRef(target);
316
+ const easingRef = useRef(easingFn);
317
+ easingRef.current = easingFn;
318
+ const [value, setValue] = useState(() => snapshotValue(target));
319
+ const [progress, setProgress] = useState(1);
320
+ const [isAnimating, setIsAnimating] = useState(false);
321
+ const prevTarget = usePrevious(target);
322
+ useEffect(() => {
323
+ if (prevTarget !== void 0 && !shallowEqual(prevTarget, target)) {
324
+ const tween = tweenRef.current;
325
+ tween.from = interpolateTween(tween, easingRef.current);
326
+ tween.to = toChannelMap(target);
327
+ tween.elapsed = 0;
328
+ tween.duration = duration;
329
+ targetRef.current = target;
330
+ setIsAnimating(true);
331
+ }
332
+ }, [
333
+ prevTarget,
334
+ target,
335
+ duration
336
+ ]);
337
+ useTick((deltaMs) => {
338
+ const tween = tweenRef.current;
339
+ tween.elapsed = Math.min(tween.elapsed + deltaMs, tween.duration);
340
+ const t = tween.duration > 0 ? tween.elapsed / tween.duration : 1;
341
+ const result = interpolateTween(tween, easingRef.current);
342
+ setProgress(t);
343
+ setValue(snapshotFromMap(result, isObject));
344
+ if (t >= 1) setIsAnimating(false);
345
+ }, isAnimating ? fps : false);
346
+ const set = useCallback((newTarget) => {
347
+ const tween = tweenRef.current;
348
+ tween.from = interpolateTween(tween, easingRef.current);
349
+ tween.to = toChannelMap(newTarget);
350
+ tween.elapsed = 0;
351
+ tween.duration = duration;
352
+ targetRef.current = newTarget;
353
+ setIsAnimating(true);
354
+ }, [duration]);
355
+ const jump = useCallback((newTarget) => {
356
+ const tween = tweenRef.current;
357
+ const map = toChannelMap(newTarget);
358
+ tween.from = new Map(map);
359
+ tween.to = new Map(map);
360
+ tween.elapsed = tween.duration;
361
+ targetRef.current = newTarget;
362
+ setValue(snapshotValue(newTarget));
363
+ setProgress(1);
364
+ setIsAnimating(false);
365
+ }, []);
366
+ return useMemo(() => ({
367
+ value,
368
+ progress,
369
+ isAnimating,
370
+ set,
371
+ jump
372
+ }), [
373
+ value,
374
+ progress,
375
+ isAnimating,
376
+ set,
377
+ jump
378
+ ]);
379
+ }
380
+ //#endregion
381
+ export { Easings, SpringPresets, useSpring, useTween };
@@ -1,6 +1,6 @@
1
1
  import { EventBusContext, StreamDeckContext } from "../context/providers.js";
2
2
  import { useCallbackRef } from "./internal/useCallbackRef.js";
3
- import { useContext, useEffect, useRef } from "react";
3
+ import { useContext, useEffect } from "react";
4
4
  //#region src/hooks/events.ts
5
5
  function useEvent(event, callback) {
6
6
  const bus = useContext(EventBusContext);
@@ -37,11 +37,7 @@ function useTouchTap(callback) {
37
37
  }
38
38
  function useDialHint(hints) {
39
39
  const { action } = useContext(StreamDeckContext);
40
- const prevHints = useRef("");
41
40
  useEffect(() => {
42
- const serialized = JSON.stringify(hints);
43
- if (serialized === prevHints.current) return;
44
- prevHints.current = serialized;
45
41
  if ("setTriggerDescription" in action) action.setTriggerDescription({
46
42
  rotate: hints.rotate,
47
43
  push: hints.press,