@fcannizzaro/streamdeck-react 0.1.11 → 0.1.13

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 (53) hide show
  1. package/README.md +11 -8
  2. package/dist/action.js +0 -1
  3. package/dist/adapter/index.d.ts +2 -0
  4. package/dist/adapter/physical-device.d.ts +2 -0
  5. package/dist/adapter/physical-device.js +153 -0
  6. package/dist/adapter/types.d.ts +127 -0
  7. package/dist/bundler-shared.d.ts +10 -0
  8. package/dist/bundler-shared.js +28 -1
  9. package/dist/devtools/bridge.d.ts +2 -2
  10. package/dist/devtools/bridge.js +7 -8
  11. package/dist/devtools/highlight.d.ts +1 -2
  12. package/dist/devtools/highlight.js +4 -3
  13. package/dist/devtools/types.d.ts +5 -5
  14. package/dist/font-inline.js +1 -1
  15. package/dist/google-font.d.ts +61 -0
  16. package/dist/google-font.js +124 -0
  17. package/dist/hooks/animation.d.ts +1 -1
  18. package/dist/hooks/animation.js +2 -2
  19. package/dist/hooks/events.js +1 -1
  20. package/dist/hooks/sdk.js +11 -11
  21. package/dist/hooks/utility.js +3 -2
  22. package/dist/index.d.ts +5 -1
  23. package/dist/index.js +3 -1
  24. package/dist/plugin.js +102 -124
  25. package/dist/reconciler/vnode.d.ts +0 -2
  26. package/dist/reconciler/vnode.js +0 -1
  27. package/dist/render/cache.d.ts +5 -17
  28. package/dist/render/cache.js +7 -29
  29. package/dist/render/image-cache.d.ts +8 -7
  30. package/dist/render/image-cache.js +33 -17
  31. package/dist/render/metrics.d.ts +9 -10
  32. package/dist/render/metrics.js +36 -39
  33. package/dist/render/pipeline.d.ts +4 -14
  34. package/dist/render/pipeline.js +47 -111
  35. package/dist/render/png.d.ts +0 -9
  36. package/dist/render/png.js +5 -8
  37. package/dist/render/render-pool.d.ts +0 -2
  38. package/dist/render/render-pool.js +1 -12
  39. package/dist/rollup.d.ts +1 -1
  40. package/dist/rollup.js +3 -1
  41. package/dist/roots/registry.d.ts +5 -9
  42. package/dist/roots/registry.js +30 -47
  43. package/dist/roots/root.d.ts +7 -34
  44. package/dist/roots/root.js +23 -90
  45. package/dist/roots/touchstrip-root.d.ts +6 -32
  46. package/dist/roots/touchstrip-root.js +61 -181
  47. package/dist/types.d.ts +38 -20
  48. package/dist/vite.d.ts +1 -1
  49. package/dist/vite.js +3 -1
  50. package/package.json +14 -8
  51. package/dist/node_modules/.bun/xxhash-wasm@1.1.0/node_modules/xxhash-wasm/esm/xxhash-wasm.js +0 -3157
  52. package/dist/roots/flush-coordinator.d.ts +0 -18
  53. package/dist/roots/flush-coordinator.js +0 -38
@@ -1,9 +1,8 @@
1
1
  import { clearDirtyFlags, createVContainer, isContainerDirty } from "../reconciler/vnode.js";
2
2
  import { reconciler } from "../reconciler/renderer.js";
3
- import { computeNativeTouchstripCacheKey, computeTreeHash, fnv1a } from "../render/cache.js";
4
- import { getTouchstripNativeCache } from "../render/image-cache.js";
5
- import { metrics } from "../render/metrics.js";
6
- import { buildTakumiChildren, measureTree, renderSegmentToDataUri, renderToRaw, sliceToDataUriAsync } from "../render/pipeline.js";
3
+ import { computeTouchStripSegmentCacheKey, computeTreeHash, fnv1a } from "../render/cache.js";
4
+ import { getTouchStripSegmentCache } from "../render/image-cache.js";
5
+ import { measureTree, renderToRaw, sliceToDataUri } from "../render/pipeline.js";
7
6
  import { EventBus } from "../context/event-bus.js";
8
7
  import { DeviceContext, EventBusContext, GlobalSettingsContext } from "../context/providers.js";
9
8
  import { partialHasChanges, shallowEqualSettings } from "./settings-equality.js";
@@ -12,7 +11,7 @@ import { createElement } from "react";
12
11
  //#region src/roots/touchstrip-root.ts
13
12
  var SEGMENT_WIDTH = 200;
14
13
  var SEGMENT_HEIGHT = 100;
15
- var DEFAULT_TOUCHSTRIP_FPS = 60;
14
+ var DEFAULT_TOUCH_STRIP_FPS = 30;
16
15
  var TouchStripRoot = class TouchStripRoot {
17
16
  eventBus = new EventBus();
18
17
  container;
@@ -20,34 +19,15 @@ var TouchStripRoot = class TouchStripRoot {
20
19
  columns = /* @__PURE__ */ new Map();
21
20
  globalSettings;
22
21
  setGlobalSettingsFn;
23
- renderDebounceMs;
24
22
  renderConfig;
25
23
  deviceInfo;
26
24
  disposed = false;
27
- fps;
28
25
  pluginWrapper;
26
+ renderDebounceMs = 17;
29
27
  _renderCount = 0;
30
28
  _lastRenderReport = 0;
31
- static RENDER_WARN_THRESHOLD = 65;
32
- _rendering = false;
33
- _pendingFlush = false;
34
- _recentRenders = [];
35
- _lastInteraction = 0;
36
- static ANIMATION_WINDOW_MS = 100;
37
- static ANIMATION_THRESHOLD = 2;
38
- static INTERACTION_COOLDOWN_MS = 500;
39
- static IDLE_THRESHOLD_MS = 2e3;
40
- _lastFlushTime = 0;
41
- /** Current render priority (lower = higher priority). Used by flush coordinator. */
42
- get priority() {
43
- const now = Date.now();
44
- const cutoff = now - TouchStripRoot.ANIMATION_WINDOW_MS;
45
- while (this._recentRenders.length > 0 && this._recentRenders[0] < cutoff) this._recentRenders.shift();
46
- if (this._recentRenders.length > TouchStripRoot.ANIMATION_THRESHOLD) return 0;
47
- if (now - this._lastInteraction < TouchStripRoot.INTERACTION_COOLDOWN_MS) return 1;
48
- if (this._lastFlushTime > 0 && now - this._lastFlushTime > TouchStripRoot.IDLE_THRESHOLD_MS) return 3;
49
- return 2;
50
- }
29
+ static RENDER_WARN_THRESHOLD = 35;
30
+ _lastSegmentUriHash = 0;
51
31
  /** Last rendered per-column data URIs. Used by devtools snapshots. */
52
32
  lastSegmentUris = /* @__PURE__ */ new Map();
53
33
  /**
@@ -93,14 +73,11 @@ var TouchStripRoot = class TouchStripRoot {
93
73
  }
94
74
  globalSettingsValue;
95
75
  touchStripValue;
96
- constructor(component, deviceInfo, initialGlobalSettings, renderConfig, renderDebounceMs, onGlobalSettingsChange, pluginWrapper, touchStripFPS, flushCoordinator) {
76
+ constructor(component, deviceInfo, initialGlobalSettings, renderConfig, onGlobalSettingsChange, pluginWrapper) {
97
77
  this.component = component;
98
- this.flushCoordinator = flushCoordinator;
99
78
  this.deviceInfo = deviceInfo;
100
79
  this.globalSettings = { ...initialGlobalSettings };
101
80
  this.renderConfig = renderConfig;
102
- this.fps = touchStripFPS ?? DEFAULT_TOUCHSTRIP_FPS;
103
- this.renderDebounceMs = touchStripFPS != null ? Math.max(1, Math.round(1e3 / touchStripFPS)) : renderDebounceMs;
104
81
  this.pluginWrapper = pluginWrapper;
105
82
  this.setGlobalSettingsFn = (partial) => {
106
83
  const hasChanges = partialHasChanges(this.globalSettings, partial);
@@ -125,8 +102,7 @@ var TouchStripRoot = class TouchStripRoot {
125
102
  width: 0,
126
103
  height: SEGMENT_HEIGHT,
127
104
  columns: [],
128
- segmentWidth: SEGMENT_WIDTH,
129
- fps: this.fps
105
+ segmentWidth: SEGMENT_WIDTH
130
106
  };
131
107
  this.container = createVContainer(() => {
132
108
  this.flush();
@@ -138,7 +114,7 @@ var TouchStripRoot = class TouchStripRoot {
138
114
  }, (err) => {
139
115
  console.error("[@fcannizzaro/streamdeck-react] TouchStrip recoverable error:", err);
140
116
  }, () => {});
141
- this.eventBus.ownerId = `touchstrip:${deviceInfo.id}`;
117
+ this.eventBus.ownerId = `touchStrip:${deviceInfo.id}`;
142
118
  }
143
119
  addColumn(column, actionId, sdkAction) {
144
120
  this.columns.set(column, {
@@ -166,8 +142,7 @@ var TouchStripRoot = class TouchStripRoot {
166
142
  width: (sortedColumns.length > 0 ? sortedColumns[sortedColumns.length - 1] + 1 : 0) * SEGMENT_WIDTH,
167
143
  height: SEGMENT_HEIGHT,
168
144
  columns: sortedColumns,
169
- segmentWidth: SEGMENT_WIDTH,
170
- fps: this.fps
145
+ segmentWidth: SEGMENT_WIDTH
171
146
  };
172
147
  }
173
148
  render() {
@@ -184,59 +159,21 @@ var TouchStripRoot = class TouchStripRoot {
184
159
  if (this.disposed) return;
185
160
  this.render();
186
161
  }
187
- /** Record a user interaction for adaptive debounce. */
188
- markInteraction() {
189
- this._lastInteraction = Date.now();
190
- }
191
- get effectiveDebounceMs() {
192
- const now = Date.now();
193
- const cutoff = now - TouchStripRoot.ANIMATION_WINDOW_MS;
194
- while (this._recentRenders.length > 0 && this._recentRenders[0] < cutoff) this._recentRenders.shift();
195
- if (this._recentRenders.length > TouchStripRoot.ANIMATION_THRESHOLD) return 0;
196
- if (now - this._lastInteraction < TouchStripRoot.INTERACTION_COOLDOWN_MS) return Math.min(this.renderDebounceMs, 16);
197
- return this.renderDebounceMs;
198
- }
199
162
  async flush() {
200
163
  if (this.disposed) return;
201
- if (this._rendering) {
202
- this._pendingFlush = true;
203
- return;
204
- }
205
- this._recentRenders.push(Date.now());
206
- const debounce = this.effectiveDebounceMs;
207
- if (debounce > 0 && this.container.renderTimer !== null) clearTimeout(this.container.renderTimer);
208
- if (debounce > 0) this.container.renderTimer = setTimeout(() => {
164
+ if (this.renderDebounceMs > 0) this.container.renderTimer = setTimeout(async () => {
209
165
  this.container.renderTimer = null;
210
- this.submitFlush();
211
- }, debounce);
212
- else this.submitFlush();
213
- }
214
- /**
215
- * Submit this root for flushing. Routes through the coordinator
216
- * (priority-ordered) when available, or flushes directly.
217
- */
218
- submitFlush() {
219
- if (this.disposed) return;
220
- if (this.flushCoordinator) this.flushCoordinator.requestFlush(this);
221
- else this.doFlush();
222
- }
223
- /**
224
- * Execute the flush. Called by FlushCoordinator in priority order,
225
- * or directly when no coordinator is present.
226
- */
227
- async executeFlush() {
228
- await this.doFlush();
166
+ await this.doFlush();
167
+ }, this.renderDebounceMs);
168
+ else await this.doFlush();
229
169
  }
230
170
  async doFlush() {
231
171
  if (this.disposed || this.columns.size === 0) return;
232
- this._rendering = true;
233
- this._pendingFlush = false;
234
- this._lastFlushTime = Date.now();
235
172
  if (this.renderConfig.debug) {
236
173
  this._renderCount++;
237
174
  const now = Date.now();
238
175
  if (now - this._lastRenderReport > 1e3) {
239
- if (this._renderCount > TouchStripRoot.RENDER_WARN_THRESHOLD) console.warn(`[@fcannizzaro/streamdeck-react] TouchStrip rendered ${this._renderCount}x in 1s (FPS target: ${this.fps})`);
176
+ if (this._renderCount > TouchStripRoot.RENDER_WARN_THRESHOLD) console.warn(`[@fcannizzaro/streamdeck-react] TouchStrip rendered ${this._renderCount}x in 1s (max ${DEFAULT_TOUCH_STRIP_FPS}fps)`);
240
177
  this._renderCount = 0;
241
178
  this._lastRenderReport = now;
242
179
  }
@@ -244,121 +181,64 @@ var TouchStripRoot = class TouchStripRoot {
244
181
  try {
245
182
  const width = this.touchStripValue.width;
246
183
  if (width === 0) return;
247
- if (this.renderConfig.touchstripImageFormat !== "png") {
248
- metrics.recordFlush();
249
- if (this.renderConfig.caching && !isContainerDirty(this.container)) {
250
- metrics.recordDirtySkip();
251
- return;
252
- }
253
- const profiling = this.renderConfig.onProfile != null;
254
- const t0 = profiling ? performance.now() : 0;
255
- const sortedColumns = [...this.columns.keys()].sort((a, b) => a - b);
256
- let treeHash;
257
- let cacheKey;
258
- let cacheHit = false;
259
- if (this.renderConfig.caching && this.renderConfig.touchstripCacheMaxBytes > 0) {
260
- treeHash = computeTreeHash(this.container);
261
- cacheKey = computeNativeTouchstripCacheKey(treeHash, width, SEGMENT_HEIGHT, this.renderConfig.devicePixelRatio, this.renderConfig.touchstripImageFormat, sortedColumns);
262
- const cache = getTouchstripNativeCache(this.renderConfig.touchstripCacheMaxBytes);
263
- const cached = cache.get(cacheKey);
264
- if (cached !== void 0) {
265
- metrics.recordCacheHit();
266
- cacheHit = true;
267
- for (const [column, uri] of cached) {
268
- this.lastSegmentUris.set(column, uri);
269
- if (!this.suppressHardwarePush) this.columns.get(column)?.sdkAction.setFeedback({ canvas: uri }).catch(() => {});
270
- }
271
- if (profiling) {
272
- const tNow = performance.now();
273
- const stats = measureTree(this.container.children);
274
- this.renderConfig.onProfile({
275
- vnodeToElementMs: 0,
276
- fromJsxMs: 0,
277
- takumiRenderMs: tNow - t0,
278
- hashMs: 0,
279
- base64Ms: 0,
280
- totalMs: tNow - t0,
281
- skipped: false,
282
- cacheHit: true,
283
- treeDepth: stats.depth,
284
- nodeCount: stats.count,
285
- cacheStats: cache.stats
286
- });
287
- }
288
- clearDirtyFlags(this.container);
289
- }
290
- }
291
- if (!cacheHit) {
292
- const takumiChildren = buildTakumiChildren(this.container);
293
- const segmentResults = [];
294
- const renderPromises = [...this.columns.entries()].map(async ([column]) => {
295
- const sliceUri = await renderSegmentToDataUri(this.container, width, SEGMENT_HEIGHT, column, SEGMENT_WIDTH, this.renderConfig.touchstripImageFormat, this.renderConfig, takumiChildren);
296
- if (sliceUri != null) {
297
- segmentResults.push([column, sliceUri]);
298
- this.lastSegmentUris.set(column, sliceUri);
299
- }
300
- });
301
- await Promise.all(renderPromises);
302
- segmentResults.sort((a, b) => a[0] - b[0]);
303
- let skipped = false;
304
- if (this.renderConfig.caching) {
305
- const uriHash = fnv1a(segmentResults.map(([col, uri]) => `${col}:${uri}`).join("\0"));
306
- if (uriHash === this.container.lastSvgHash) {
307
- metrics.recordHashDedup();
308
- skipped = true;
309
- } else this.container.lastSvgHash = uriHash;
310
- }
311
- if (!skipped && !this.suppressHardwarePush) for (const [column, uri] of segmentResults) this.columns.get(column)?.sdkAction.setFeedback({ canvas: uri }).catch(() => {});
312
- const elapsedMs = (profiling ? performance.now() : 0) - t0;
313
- metrics.recordRender(elapsedMs);
314
- if (this.renderConfig.caching && this.renderConfig.touchstripCacheMaxBytes > 0) {
315
- if (treeHash === void 0 || cacheKey === void 0) {
316
- treeHash = computeTreeHash(this.container);
317
- cacheKey = computeNativeTouchstripCacheKey(treeHash, width, SEGMENT_HEIGHT, this.renderConfig.devicePixelRatio, this.renderConfig.touchstripImageFormat, sortedColumns);
318
- }
319
- const cache = getTouchstripNativeCache(this.renderConfig.touchstripCacheMaxBytes);
320
- let byteSize = 64;
321
- for (const [, uri] of segmentResults) byteSize += uri.length * 2 + 16;
322
- cache.set(cacheKey, segmentResults, byteSize);
323
- }
184
+ if (this.renderConfig.caching && !isContainerDirty(this.container)) return;
185
+ const sortedColumns = [...this.columns.keys()].sort((a, b) => a - b);
186
+ let cacheKey;
187
+ const profiling = this.renderConfig.onProfile != null;
188
+ const tPhase2 = profiling ? performance.now() : 0;
189
+ if (this.renderConfig.caching && this.renderConfig.touchStripCacheMaxBytes > 0) {
190
+ cacheKey = computeTouchStripSegmentCacheKey(computeTreeHash(this.container), width, SEGMENT_HEIGHT, this.renderConfig.devicePixelRatio, sortedColumns);
191
+ const cached = getTouchStripSegmentCache(this.renderConfig.touchStripCacheMaxBytes).get(cacheKey);
192
+ if (cached !== void 0) {
193
+ for (const [column, uri] of cached) this.lastSegmentUris.set(column, uri);
194
+ if (!this.suppressHardwarePush) for (const [column, uri] of cached) this.columns.get(column)?.sdkAction.setFeedback({ canvas: uri }).catch(() => {});
195
+ clearDirtyFlags(this.container);
324
196
  if (profiling) {
197
+ const tEnd = performance.now();
325
198
  const stats = measureTree(this.container.children);
326
- const nativeCache = this.renderConfig.touchstripCacheMaxBytes > 0 ? getTouchstripNativeCache(this.renderConfig.touchstripCacheMaxBytes) : null;
327
199
  this.renderConfig.onProfile({
328
- vnodeToElementMs: 0,
329
- fromJsxMs: 0,
330
- takumiRenderMs: elapsedMs,
331
- hashMs: 0,
200
+ vnodeConversionMs: 0,
201
+ takumiRenderMs: 0,
202
+ hashMs: tEnd - tPhase2,
332
203
  base64Ms: 0,
333
- totalMs: elapsedMs,
334
- skipped,
335
- cacheHit: false,
204
+ totalMs: tEnd - tPhase2,
205
+ skipped: false,
206
+ cacheHit: true,
336
207
  treeDepth: stats.depth,
337
208
  nodeCount: stats.count,
338
- cacheStats: nativeCache?.stats ?? null
209
+ cacheStats: null
339
210
  });
340
211
  }
341
- clearDirtyFlags(this.container);
212
+ this.renderConfig.onRender?.(this.container, "");
213
+ return;
342
214
  }
343
- } else {
344
- const result = await renderToRaw(this.container, width, SEGMENT_HEIGHT, this.renderConfig);
345
- if (result === null || this.disposed) return;
346
- const feedbackPromises = [...this.columns.entries()].map(async ([column, entry]) => {
347
- const sliceUri = await sliceToDataUriAsync(result.buffer, result.width, result.height, column, SEGMENT_WIDTH, SEGMENT_HEIGHT);
348
- this.lastSegmentUris.set(column, sliceUri);
349
- if (!this.suppressHardwarePush) entry.sdkAction.setFeedback({ canvas: sliceUri }).catch(() => {});
350
- });
351
- await Promise.all(feedbackPromises);
352
215
  }
216
+ const result = await renderToRaw(this.container, width, SEGMENT_HEIGHT, this.renderConfig);
217
+ if (result === null || this.disposed) return;
218
+ const segmentResults = [];
219
+ for (const [column] of this.columns) {
220
+ const sliceUri = sliceToDataUri(result.buffer, result.width, result.height, column, SEGMENT_WIDTH, SEGMENT_HEIGHT);
221
+ segmentResults.push([column, sliceUri]);
222
+ this.lastSegmentUris.set(column, sliceUri);
223
+ }
224
+ segmentResults.sort((a, b) => a[0] - b[0]);
225
+ let skipped = false;
226
+ if (this.renderConfig.caching) {
227
+ const uriHash = fnv1a(segmentResults.map(([col, uri]) => `${col}:${uri}`).join("\0"));
228
+ if (uriHash === this._lastSegmentUriHash) skipped = true;
229
+ else this._lastSegmentUriHash = uriHash;
230
+ }
231
+ if (!skipped && !this.suppressHardwarePush) for (const [column, uri] of segmentResults) this.columns.get(column)?.sdkAction.setFeedback({ canvas: uri }).catch(() => {});
232
+ if (cacheKey !== void 0 && this.renderConfig.caching && this.renderConfig.touchStripCacheMaxBytes > 0) {
233
+ const cache = getTouchStripSegmentCache(this.renderConfig.touchStripCacheMaxBytes);
234
+ let byteSize = 64;
235
+ for (const [, uri] of segmentResults) byteSize += uri.length * 2 + 16;
236
+ cache.set(cacheKey, segmentResults, byteSize);
237
+ }
238
+ clearDirtyFlags(this.container);
353
239
  this.renderConfig.onRender?.(this.container, "");
354
240
  } catch (err) {
355
241
  console.error("[@fcannizzaro/streamdeck-react] TouchStrip render error:", err);
356
- } finally {
357
- this._rendering = false;
358
- if (this._pendingFlush && !this.disposed) {
359
- this._pendingFlush = false;
360
- await this.doFlush();
361
- }
362
242
  }
363
243
  }
364
244
  updateGlobalSettings(settings) {
package/dist/types.d.ts CHANGED
@@ -1,6 +1,18 @@
1
1
  import { ComponentType, ReactNode } from 'react';
2
- import { Action, Controller, Coordinates, DeviceType, DialAction, KeyAction, Size } from '@elgato/streamdeck';
3
2
  import { JsonObject, JsonValue } from '@elgato/utils';
3
+ import { AdapterActionHandle, StreamDeckAdapter } from './adapter/types';
4
+ /** Controller surface type. */
5
+ export type Controller = "Keypad" | "Encoder";
6
+ /** Grid coordinates for a key or encoder on a device. */
7
+ export interface Coordinates {
8
+ column: number;
9
+ row: number;
10
+ }
11
+ /** Device grid size (number of key columns and rows). */
12
+ export interface Size {
13
+ columns: number;
14
+ rows: number;
15
+ }
4
16
  export interface FontConfig {
5
17
  name: string;
6
18
  data: ArrayBuffer | Buffer;
@@ -61,11 +73,26 @@ export interface TouchStripLayout {
61
73
  items: TouchStripLayoutItem[];
62
74
  }
63
75
  export type EncoderLayout = string | TouchStripLayout;
76
+ /** Takumi renderer backend selection. `"native-binding"` uses the native Rust NAPI addon (`@takumi-rs/core`). `"wasm"` uses the WebAssembly build (`@takumi-rs/wasm`), suitable for WebContainer and browser environments. */
77
+ export type TakumiBackend = "native-binding" | "wasm";
64
78
  export interface PluginConfig {
79
+ /** Stream Deck adapter. Defaults to physicalDevice() (Elgato SDK). */
80
+ adapter?: StreamDeckAdapter;
65
81
  fonts: FontConfig[];
66
82
  actions: ActionDefinition[];
67
83
  wrapper?: WrapperComponent;
68
- renderDebounceMs?: number;
84
+ /**
85
+ * Takumi renderer backend.
86
+ *
87
+ * - `"native-binding"` — uses `@takumi-rs/core` (native Rust NAPI addon).
88
+ * Requires a platform-specific binary (e.g. `@takumi-rs/core-darwin-arm64`).
89
+ * - `"wasm"` — uses `@takumi-rs/wasm` (WebAssembly build).
90
+ * Requires `@takumi-rs/wasm` to be installed. WOFF fonts are not supported
91
+ * in this mode — use TTF/OTF only. Worker threads are force-disabled.
92
+ *
93
+ * @default "native-binding"
94
+ */
95
+ takumi?: TakumiBackend;
69
96
  imageFormat?: "png" | "webp";
70
97
  caching?: boolean;
71
98
  devicePixelRatio?: number;
@@ -76,12 +103,10 @@ export interface PluginConfig {
76
103
  debug?: boolean;
77
104
  /** Maximum image cache size in bytes for key/dial renders. Set to 0 to disable. @default 16777216 (16 MB) */
78
105
  imageCacheMaxBytes?: number;
79
- /** Maximum touchstrip raw buffer cache size in bytes. Set to 0 to disable. @default 8388608 (8 MB) */
80
- touchstripCacheMaxBytes?: number;
81
- /** Offload Takumi rendering to a worker thread. Set to false to disable. @default true */
106
+ /** Maximum TouchStrip raw buffer cache size in bytes. Set to 0 to disable. @default 8388608 (8 MB) */
107
+ touchStripCacheMaxBytes?: number;
108
+ /** Offload Takumi rendering to a worker thread. Set to false to disable. Automatically disabled when `takumi` is `"wasm"`. @default true */
82
109
  useWorker?: boolean;
83
- /** Image format for touchstrip segment encoding. `"webp"` renders each segment directly via Takumi (faster, no custom PNG encoder). `"png"` uses the raw+crop+deflate path. @default "webp" */
84
- touchstripImageFormat?: "webp" | "png";
85
110
  }
86
111
  export interface Plugin {
87
112
  connect(): Promise<void>;
@@ -112,10 +137,8 @@ export interface ActionConfig<S extends JsonObject = JsonObject> {
112
137
  uuid: string;
113
138
  key?: ComponentType;
114
139
  dial?: ComponentType;
115
- /** Full-strip touchstrip component. When set, replaces per-encoder `dial` display with a single shared React tree that spans the entire touch strip. */
140
+ /** Full-strip TouchStrip component. When set, replaces per-encoder `dial` display with a single shared React tree that spans the entire touch strip. */
116
141
  touchStrip?: ComponentType;
117
- /** Target frame rate for the touchstrip animation loop and render pipeline. Controls both `useTick` cadence (via `useTouchStrip().fps`) and the render debounce. @default 60 */
118
- touchStripFPS?: number;
119
142
  /** Encoder feedback layout. Defaults to a full-width `pixmap` canvas layout. Custom layouts should include a `pixmap` item keyed as `canvas`. */
120
143
  dialLayout?: EncoderLayout;
121
144
  wrapper?: WrapperComponent;
@@ -127,8 +150,6 @@ export type ActionConfigInput<S extends JsonObject = JsonObject> = [keyof Manife
127
150
  ] ? ActionConfig<S> : {
128
151
  [UUID in ActionUUID]: {
129
152
  uuid: UUID;
130
- /** Target frame rate for the touchstrip animation loop and render pipeline. Controls both `useTick` cadence (via `useTouchStrip().fps`) and the render debounce. @default 60 */
131
- touchStripFPS?: number;
132
153
  /** Encoder feedback layout. Defaults to a full-width `pixmap` canvas layout. Custom layouts should include a `pixmap` item keyed as `canvas`. */
133
154
  dialLayout?: EncoderLayout;
134
155
  wrapper?: WrapperComponent;
@@ -139,10 +160,8 @@ export interface ActionDefinition<S extends JsonObject = JsonObject> {
139
160
  uuid: string;
140
161
  key?: ComponentType;
141
162
  dial?: ComponentType;
142
- /** Full-strip touchstrip component. When set, replaces per-encoder `dial` display with a single shared React tree that spans the entire touch strip. */
163
+ /** Full-strip TouchStrip component. When set, replaces per-encoder `dial` display with a single shared React tree that spans the entire touch strip. */
143
164
  touchStrip?: ComponentType;
144
- /** Target frame rate for the touchstrip animation loop and render pipeline. @default 60 */
145
- touchStripFPS?: number;
146
165
  /** Encoder feedback layout. Defaults to a full-width `pixmap` canvas layout. Custom layouts should include a `pixmap` item keyed as `canvas`. */
147
166
  dialLayout?: EncoderLayout;
148
167
  wrapper?: WrapperComponent;
@@ -150,7 +169,8 @@ export interface ActionDefinition<S extends JsonObject = JsonObject> {
150
169
  }
151
170
  export interface DeviceInfo {
152
171
  id: string;
153
- type: DeviceType;
172
+ /** Numeric device type matching Elgato DeviceType enum values (e.g. 7 = StreamDeckPlus). */
173
+ type: number;
154
174
  size: Size;
155
175
  name: string;
156
176
  }
@@ -167,8 +187,8 @@ export interface CanvasInfo {
167
187
  type: "key" | "dial" | "touch";
168
188
  }
169
189
  export interface StreamDeckAccess {
170
- action: Action | DialAction | KeyAction;
171
- sdk: typeof import('@elgato/streamdeck').default;
190
+ action: AdapterActionHandle;
191
+ adapter: StreamDeckAdapter;
172
192
  }
173
193
  export interface KeyDownPayload {
174
194
  settings: JsonObject;
@@ -216,8 +236,6 @@ export interface TouchStripInfo {
216
236
  columns: number[];
217
237
  /** Width of each encoder segment in pixels (always 200). */
218
238
  segmentWidth: number;
219
- /** Target frame rate for the animation loop. Pass to `useTick` for matched cadence. */
220
- fps: number;
221
239
  }
222
240
  export interface TouchStripTapPayload {
223
241
  /** Absolute tap position across the full strip width. */
package/dist/vite.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Plugin } from 'vite';
2
2
  import { StreamDeckTargetOptions } from './bundler-shared';
3
- export type { StreamDeckPlatform, StreamDeckArch, StreamDeckTarget, StreamDeckTargetOptions, } from './bundler-shared';
3
+ export type { StreamDeckPlatform, StreamDeckArch, StreamDeckTarget, StreamDeckTargetOptions, TakumiBackend, } from './bundler-shared';
4
4
  export interface StreamDeckReactOptions extends StreamDeckTargetOptions {
5
5
  /**
6
6
  * The plugin UUID used to restart the plugin after each build
package/dist/vite.js CHANGED
@@ -1,4 +1,4 @@
1
- import { NOOP_DEVTOOLS_CODE, NOOP_DEVTOOLS_ID, copyNativeBindings, isLibraryDevtoolsImport, shouldStripDevtools } from "./bundler-shared.js";
1
+ import { NOOP_DEVTOOLS_CODE, NOOP_DEVTOOLS_ID, TAKUMI_NATIVE_LOADER_CODE, TAKUMI_NATIVE_LOADER_ID, copyNativeBindings, isLibraryDevtoolsImport, shouldStripDevtools } from "./bundler-shared.js";
2
2
  import { loadFont, resolveFontId } from "./font-inline.js";
3
3
  import { generateManifestTypes } from "./manifest-codegen.js";
4
4
  import { resolve } from "node:path";
@@ -47,10 +47,12 @@ function streamDeckReact(options = {}) {
47
47
  },
48
48
  resolveId(source, importer) {
49
49
  if (stripDevtools && isLibraryDevtoolsImport(source, importer)) return NOOP_DEVTOOLS_ID;
50
+ if (options.takumi !== "wasm" && source === "@takumi-rs/core") return TAKUMI_NATIVE_LOADER_ID;
50
51
  return resolveFontId(source, importer);
51
52
  },
52
53
  load(id) {
53
54
  if (id === "\0streamdeck-react:noop-devtools") return NOOP_DEVTOOLS_CODE;
55
+ if (id === "\0streamdeck-react:takumi-native") return TAKUMI_NATIVE_LOADER_CODE;
54
56
  return loadFont(id);
55
57
  },
56
58
  writeBundle() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fcannizzaro/streamdeck-react",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Build Stream Deck plugins with React — render components directly to keys, dials, and touch screens",
5
5
  "keywords": [
6
6
  "elgato",
@@ -60,29 +60,35 @@
60
60
  "test": "bun test ./src"
61
61
  },
62
62
  "dependencies": {
63
- "@elgato/streamdeck": "^2.0.2",
64
- "@takumi-rs/core": "^0.71.7",
65
- "@takumi-rs/helpers": "^0.71.7",
66
- "react-reconciler": "^0.33.0",
67
- "xxhash-wasm": "^1.1.0"
63
+ "@takumi-rs/core": "^0.73.1",
64
+ "@takumi-rs/helpers": "^0.73.1",
65
+ "react-reconciler": "^0.33.0"
68
66
  },
69
67
  "devDependencies": {
70
- "@happy-dom/global-registrator": "^20.8.3",
68
+ "@happy-dom/global-registrator": "^20.8.4",
71
69
  "@types/react": "^19.2.14",
72
70
  "@types/react-dom": "^19.2.3",
73
71
  "@types/react-reconciler": "^0.33.0",
74
72
  "react-dom": "^19.2.4",
75
73
  "rollup": "^4.59.0",
76
- "vite": "8.0.0-beta.18",
74
+ "vite": "8.0.0",
77
75
  "vite-plugin-dts": "^4.5.4"
78
76
  },
79
77
  "peerDependencies": {
78
+ "@elgato/streamdeck": "^2.0.2",
79
+ "@takumi-rs/wasm": "^0.71.7",
80
80
  "react": "^18.0.0 || ^19.0.0",
81
81
  "rollup": "^4.0.0",
82
82
  "typescript": "^5",
83
83
  "vite": "^7.0.0 || ^8.0.0"
84
84
  },
85
85
  "peerDependenciesMeta": {
86
+ "@elgato/streamdeck": {
87
+ "optional": true
88
+ },
89
+ "@takumi-rs/wasm": {
90
+ "optional": true
91
+ },
86
92
  "rollup": {
87
93
  "optional": true
88
94
  },