@fcannizzaro/streamdeck-react 0.1.12 → 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.
package/README.md CHANGED
@@ -63,11 +63,13 @@ export const counterAction = defineAction({
63
63
  Register it in your plugin entrypoint:
64
64
 
65
65
  ```ts
66
- import { createPlugin } from "@fcannizzaro/streamdeck-react";
66
+ import { createPlugin, googleFont } from "@fcannizzaro/streamdeck-react";
67
67
  import { counterAction } from "./actions/counter.tsx";
68
68
 
69
+ const inter = await googleFont("Inter");
70
+
69
71
  const plugin = createPlugin({
70
- fonts: [],
72
+ fonts: [inter],
71
73
  actions: [counterAction],
72
74
  });
73
75
 
@@ -83,12 +85,13 @@ await plugin.connect();
83
85
 
84
86
  ## Samples
85
87
 
86
- - `samples/counter/` - local state, settings hook, dial interaction
87
- - `samples/zustand/` - shared external store across multiple actions
88
- - `samples/jotai/` - shared atom state with a plugin-level wrapper
89
- - `samples/pokemon/` - richer plugin example with custom wrappers
90
- - `samples/snake/` - snake game on the Stream Deck+ touch strip using dial controls and touch tap
91
- - `samples/weather/` - weather forecast dials with animated detail panels and a shared Zustand store
88
+ - `samples/counter/` local state, persisted settings, dial interaction
89
+ - `samples/zustand/` shared state across keys via a module-scope Zustand store
90
+ - `samples/jotai/` shared atom state with a plugin-level Jotai Provider wrapper
91
+ - `samples/pokemon/` data fetching with TanStack Query and remote image rendering
92
+ - `samples/animation/` spring bounce, fade-slide, and spring dial animations
93
+ - `samples/snake/` snake game on the Stream Deck+ TouchStrip using dial controls and touch tap
94
+ - `samples/weather/` — weather forecast dials with animated detail panels and a shared Zustand store
92
95
 
93
96
  ## DevTools
94
97
 
@@ -4,8 +4,16 @@ export interface StreamDeckTarget {
4
4
  platform: StreamDeckPlatform;
5
5
  arch: StreamDeckArch;
6
6
  }
7
+ /** Takumi renderer backend selection. Mirrors the runtime `TakumiBackend` type for build-time configuration. */
8
+ export type TakumiBackend = "native-binding" | "wasm";
7
9
  export interface StreamDeckTargetOptions {
8
10
  targets?: StreamDeckTarget[];
11
+ /**
12
+ * Takumi renderer backend. When `"wasm"`, native `.node` binding
13
+ * copying is skipped entirely during the build.
14
+ * @default "native-binding"
15
+ */
16
+ takumi?: TakumiBackend;
9
17
  }
10
18
  export interface ResolvedTarget extends StreamDeckTarget {
11
19
  pkg: string;
@@ -17,6 +25,8 @@ export declare function isArch(value: string): value is StreamDeckArch;
17
25
  export declare function isDevelopmentMode(watchMode: boolean | undefined): boolean;
18
26
  export declare const NOOP_DEVTOOLS_ID = "\0streamdeck-react:noop-devtools";
19
27
  export declare const NOOP_DEVTOOLS_CODE = "export function startDevtoolsServer() {}";
28
+ export declare const TAKUMI_NATIVE_LOADER_ID = "\0streamdeck-react:takumi-native";
29
+ export declare const TAKUMI_NATIVE_LOADER_CODE: string;
20
30
  /**
21
31
  * Returns true when devtools should be stripped from the bundle.
22
32
  * Only strips in explicit production mode (NODE_ENV=production) to avoid
@@ -40,6 +40,32 @@ function isDevelopmentMode(watchMode) {
40
40
  var NOOP_DEVTOOLS_ID = "\0streamdeck-react:noop-devtools";
41
41
  var NOOP_DEVTOOLS_CODE = "export function startDevtoolsServer() {}";
42
42
  var DEVTOOLS_IMPORT_SOURCE = "./devtools/index.js";
43
+ var TAKUMI_NATIVE_LOADER_ID = "\0streamdeck-react:takumi-native";
44
+ var TAKUMI_NATIVE_LOADER_CODE = `
45
+ import { createRequire } from "node:module";
46
+ const require = createRequire(import.meta.url);
47
+ let binding = null;
48
+ if (process.platform === "darwin") {
49
+ if (process.arch === "arm64") {
50
+ try { binding = require("./core.darwin-arm64.node"); } catch {}
51
+ } else if (process.arch === "x64") {
52
+ try { binding = require("./core.darwin-x64.node"); } catch {}
53
+ }
54
+ } else if (process.platform === "win32") {
55
+ if (process.arch === "arm64") {
56
+ try { binding = require("./core.win32-arm64-msvc.node"); } catch {}
57
+ } else if (process.arch === "x64") {
58
+ try { binding = require("./core.win32-x64-msvc.node"); } catch {}
59
+ }
60
+ }
61
+ if (!binding) {
62
+ throw new Error(
63
+ "[@fcannizzaro/streamdeck-react] Failed to load @takumi-rs/core native binding for " +
64
+ process.platform + "-" + process.arch
65
+ );
66
+ }
67
+ export const { Renderer, OutputFormat, DitheringAlgorithm, AnimationOutputFormat, extractResourceUrls } = binding;
68
+ `.trim();
43
69
  /**
44
70
  * Returns true when devtools should be stripped from the bundle.
45
71
  * Only strips in explicit production mode (NODE_ENV=production) to avoid
@@ -92,6 +118,7 @@ function expandTargets(targets) {
92
118
  * In production: missing bindings throw an error (the plugin won't work).
93
119
  */
94
120
  function copyNativeBindings(outDir, isDevelopment, options, warn) {
121
+ if (options.takumi === "wasm") return;
95
122
  try {
96
123
  const requestedTargets = normalizeTargetRequests(options, isDevelopment);
97
124
  if (requestedTargets.length === 0) {
@@ -129,4 +156,4 @@ function copyNativeBindings(outDir, isDevelopment, options, warn) {
129
156
  }
130
157
  }
131
158
  //#endregion
132
- export { NOOP_DEVTOOLS_CODE, NOOP_DEVTOOLS_ID, copyNativeBindings, isDevelopmentMode, isLibraryDevtoolsImport, shouldStripDevtools };
159
+ export { NOOP_DEVTOOLS_CODE, NOOP_DEVTOOLS_ID, TAKUMI_NATIVE_LOADER_CODE, TAKUMI_NATIVE_LOADER_ID, copyNativeBindings, isDevelopmentMode, isLibraryDevtoolsImport, shouldStripDevtools };
@@ -1,7 +1,7 @@
1
+ import { readFile } from "node:fs/promises";
1
2
  import { dirname, resolve } from "node:path";
2
3
  import { createRequire } from "node:module";
3
4
  import { realpathSync } from "node:fs";
4
- import { readFile } from "node:fs/promises";
5
5
  //#region src/font-inline.ts
6
6
  var FONT_RE = /\.(ttf|otf|woff2?)$/;
7
7
  /**
@@ -0,0 +1,61 @@
1
+ import { FontConfig } from './types';
2
+ type FontWeight = FontConfig["weight"];
3
+ type FontStyle = FontConfig["style"];
4
+ export interface GoogleFontVariant {
5
+ weight?: FontWeight;
6
+ style?: FontStyle;
7
+ }
8
+ /**
9
+ * Fetch a Google Font as a ready-to-use `FontConfig`.
10
+ *
11
+ * Downloads TTF font data directly from Google Fonts — no npm package
12
+ * needed. The returned config can be passed straight into
13
+ * `createPlugin({ fonts: [...] })`.
14
+ *
15
+ * Font files are cached to `.google-fonts/` in the current working
16
+ * directory. Subsequent calls read from disk without network access.
17
+ *
18
+ * @param family - The Google Font family name (e.g. `"Inter"`, `"Roboto"`).
19
+ * @returns A `FontConfig` with weight 400 and normal style.
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * const inter = await googleFont("Inter");
24
+ * createPlugin({ fonts: [inter], actions: [...] });
25
+ * ```
26
+ */
27
+ export declare function googleFont(family: string): Promise<FontConfig>;
28
+ /**
29
+ * Fetch a specific weight/style variant of a Google Font.
30
+ *
31
+ * @param family - The Google Font family name.
32
+ * @param variant - Weight and/or style to fetch.
33
+ * @returns A single `FontConfig`.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * const bold = await googleFont("Inter", { weight: 700 });
38
+ * ```
39
+ */
40
+ export declare function googleFont(family: string, variant: GoogleFontVariant): Promise<FontConfig>;
41
+ /**
42
+ * Fetch multiple weight/style variants of a Google Font in one call.
43
+ *
44
+ * All variants are fetched in parallel for maximum throughput.
45
+ *
46
+ * @param family - The Google Font family name.
47
+ * @param variants - Array of weight/style combinations to fetch.
48
+ * @returns An array of `FontConfig` objects, one per variant.
49
+ *
50
+ * @example
51
+ * ```ts
52
+ * const fonts = await googleFont("Inter", [
53
+ * { weight: 400 },
54
+ * { weight: 700 },
55
+ * { weight: 700, style: "italic" },
56
+ * ]);
57
+ * createPlugin({ fonts, actions: [...] });
58
+ * ```
59
+ */
60
+ export declare function googleFont(family: string, variants: GoogleFontVariant[]): Promise<FontConfig[]>;
61
+ export {};
@@ -0,0 +1,124 @@
1
+ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ //#region src/google-font.ts
4
+ var TTF_USER_AGENT = "Mozilla/4.0";
5
+ var GOOGLE_FONTS_CSS2_BASE = "https://fonts.googleapis.com/css2";
6
+ var CACHE_DIR = ".google-fonts";
7
+ function sanitizeName(family) {
8
+ return family.toLowerCase().replace(/\s+/g, "-");
9
+ }
10
+ function cacheFilePath(family, weight, style) {
11
+ return join(process.cwd(), CACHE_DIR, `${sanitizeName(family)}-${weight}-${style}.ttf`);
12
+ }
13
+ async function fileExists(path) {
14
+ try {
15
+ await access(path);
16
+ return true;
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+ async function ensureCacheDir() {
22
+ await mkdir(join(process.cwd(), CACHE_DIR), { recursive: true });
23
+ }
24
+ async function readCached(family, weight, style) {
25
+ const path = cacheFilePath(family, weight, style);
26
+ if (!await fileExists(path)) return null;
27
+ return {
28
+ name: family,
29
+ data: (await readFile(path)).buffer,
30
+ weight,
31
+ style
32
+ };
33
+ }
34
+ async function writeCache(family, weight, style, data) {
35
+ await ensureCacheDir();
36
+ await writeFile(cacheFilePath(family, weight, style), Buffer.from(data));
37
+ }
38
+ function buildCss2Url(family, variants) {
39
+ const hasItalic = variants.some((v) => v.style === "italic");
40
+ const encodedFamily = encodeURIComponent(family);
41
+ if (hasItalic) {
42
+ const specs = variants.map((v) => {
43
+ return `${v.style === "italic" ? 1 : 0},${v.weight ?? 400}`;
44
+ });
45
+ specs.sort();
46
+ return `${GOOGLE_FONTS_CSS2_BASE}?family=${encodedFamily}:ital,wght@${specs.join(";")}`;
47
+ }
48
+ const weights = variants.map((v) => v.weight ?? 400);
49
+ weights.sort((a, b) => a - b);
50
+ return `${GOOGLE_FONTS_CSS2_BASE}?family=${encodedFamily}:wght@${weights.join(";")}`;
51
+ }
52
+ function parseFontFaces(css) {
53
+ const results = [];
54
+ const seen = /* @__PURE__ */ new Set();
55
+ const blockRegex = /@font-face\s*\{([^}]+)\}/g;
56
+ let match;
57
+ while ((match = blockRegex.exec(css)) !== null) {
58
+ const block = match[1];
59
+ const urlMatch = block.match(/url\(([^)]+)\)/);
60
+ const weightMatch = block.match(/font-weight:\s*(\d+)/);
61
+ const styleMatch = block.match(/font-style:\s*(normal|italic)/);
62
+ if (!urlMatch?.[1]) continue;
63
+ const weight = Number(weightMatch?.[1] ?? 400);
64
+ const style = styleMatch?.[1] ?? "normal";
65
+ const key = `${weight}:${style}`;
66
+ if (seen.has(key)) continue;
67
+ seen.add(key);
68
+ results.push({
69
+ url: urlMatch[1],
70
+ weight,
71
+ style
72
+ });
73
+ }
74
+ return results;
75
+ }
76
+ async function fetchGoogleFonts(family, variants) {
77
+ const results = [];
78
+ const uncached = [];
79
+ for (const v of variants) {
80
+ const weight = v.weight ?? 400;
81
+ const style = v.style ?? "normal";
82
+ const cached = await readCached(family, weight, style);
83
+ if (cached) results.push(cached);
84
+ else uncached.push({
85
+ weight,
86
+ style
87
+ });
88
+ }
89
+ if (uncached.length === 0) return results;
90
+ const url = buildCss2Url(family, uncached);
91
+ const cssResponse = await fetch(url, { headers: { "User-Agent": TTF_USER_AGENT } });
92
+ if (!cssResponse.ok) throw new Error(`Google Fonts: failed to fetch CSS for "${family}" (HTTP ${cssResponse.status})`);
93
+ const faces = parseFontFaces(await cssResponse.text());
94
+ if (faces.length === 0) throw new Error(`Google Fonts: no font faces found in CSS response for "${family}". Verify the family name is correct at https://fonts.google.com`);
95
+ const downloaded = await Promise.all(faces.map(async (face) => {
96
+ const fontResponse = await fetch(face.url);
97
+ if (!fontResponse.ok) throw new Error(`Google Fonts: failed to download font file for "${family}" weight ${face.weight} (HTTP ${fontResponse.status})`);
98
+ const data = await fontResponse.arrayBuffer();
99
+ writeCache(family, face.weight, face.style, data).catch(() => {});
100
+ return {
101
+ name: family,
102
+ data,
103
+ weight: face.weight,
104
+ style: face.style
105
+ };
106
+ }));
107
+ return [...results, ...downloaded];
108
+ }
109
+ async function googleFont(family, variantOrVariants) {
110
+ if (variantOrVariants === void 0) return (await fetchGoogleFonts(family, [{
111
+ weight: 400,
112
+ style: "normal"
113
+ }]))[0];
114
+ if (!Array.isArray(variantOrVariants)) return (await fetchGoogleFonts(family, [{
115
+ weight: variantOrVariants.weight ?? 400,
116
+ style: variantOrVariants.style ?? "normal"
117
+ }]))[0];
118
+ return fetchGoogleFonts(family, variantOrVariants.map((v) => ({
119
+ weight: v.weight ?? 400,
120
+ style: v.style ?? "normal"
121
+ })));
122
+ }
123
+ //#endregion
124
+ export { googleFont };
package/dist/index.d.ts CHANGED
@@ -23,7 +23,9 @@ export { ProgressBar } from './components/ProgressBar';
23
23
  export { CircularGauge } from './components/CircularGauge';
24
24
  export { ErrorBoundary } from './components/ErrorBoundary';
25
25
  export { tw } from './tw/index';
26
- export type { PluginConfig, FontConfig, ActionConfig, ActionConfigInput, ActionDefinition, ActionUUID, ManifestActions, EncoderLayout, WrapperComponent, Controller, Coordinates, Size, DeviceInfo, ActionInfo, CanvasInfo, TouchStripLayout, TouchStripLayoutItem, KeyDownPayload, KeyUpPayload, DialRotatePayload, DialPressPayload, TouchTapPayload, DialHints, StreamDeckAccess, TouchStripInfo, TouchStripTapPayload, TouchStripDialRotatePayload, TouchStripDialPressPayload, } from './types';
26
+ export { googleFont } from './google-font';
27
+ export type { GoogleFontVariant } from './google-font';
28
+ export type { PluginConfig, Plugin, FontConfig, ActionConfig, ActionConfigInput, ActionDefinition, ActionUUID, ManifestActions, EncoderLayout, WrapperComponent, TakumiBackend, Controller, Coordinates, Size, DeviceInfo, ActionInfo, CanvasInfo, TouchStripLayout, TouchStripLayoutItem, KeyDownPayload, KeyUpPayload, DialRotatePayload, DialPressPayload, TouchTapPayload, DialHints, StreamDeckAccess, TouchStripInfo, TouchStripTapPayload, TouchStripDialRotatePayload, TouchStripDialPressPayload, } from './types';
27
29
  export type { TapOptions, LongPressOptions, DoubleTapOptions } from './hooks/gestures';
28
30
  export type { BoxProps } from './components/Box';
29
31
  export type { TextProps } from './components/Text';
package/dist/index.js CHANGED
@@ -18,4 +18,5 @@ import { ProgressBar } from "./components/ProgressBar.js";
18
18
  import { CircularGauge } from "./components/CircularGauge.js";
19
19
  import { ErrorBoundary } from "./components/ErrorBoundary.js";
20
20
  import { tw } from "./tw/index.js";
21
- export { Box, CircularGauge, Easings, ErrorBoundary, Icon, Image, ProgressBar, SpringPresets, Text, createPlugin, defineAction, physicalDevice, tw, useAction, useCanvas, useDevice, useDialDown, useDialHint, useDialRotate, useDialUp, useDoubleTap, useGlobalSettings, useInterval, useKeyDown, useKeyUp, useLongPress, useOpenUrl, usePrevious, usePropertyInspector, useSendToPI, useSettings, useShowAlert, useShowOk, useSpring, useStreamDeck, useSwitchProfile, useTap, useTick, useTimeout, useTitle, useTouchStrip, useTouchStripDialDown, useTouchStripDialRotate, useTouchStripDialUp, useTouchStripTap, useTouchTap, useTween, useWillAppear, useWillDisappear };
21
+ import { googleFont } from "./google-font.js";
22
+ export { Box, CircularGauge, Easings, ErrorBoundary, Icon, Image, ProgressBar, SpringPresets, Text, createPlugin, defineAction, googleFont, physicalDevice, tw, useAction, useCanvas, useDevice, useDialDown, useDialHint, useDialRotate, useDialUp, useDoubleTap, useGlobalSettings, useInterval, useKeyDown, useKeyUp, useLongPress, useOpenUrl, usePrevious, usePropertyInspector, useSendToPI, useSettings, useShowAlert, useShowOk, useSpring, useStreamDeck, useSwitchProfile, useTap, useTick, useTimeout, useTitle, useTouchStrip, useTouchStripDialDown, useTouchStripDialRotate, useTouchStripDialUp, useTouchStripTap, useTouchTap, useTween, useWillAppear, useWillDisappear };
package/dist/plugin.js CHANGED
@@ -7,46 +7,55 @@ import { Renderer } from "@takumi-rs/core";
7
7
  //#region src/plugin.ts
8
8
  function createPlugin(config) {
9
9
  const adapter = config.adapter ?? physicalDevice();
10
- const renderer = new Renderer({ fonts: config.fonts.map((f) => ({
11
- name: f.name,
12
- data: f.data,
13
- weight: f.weight,
14
- style: f.style
15
- })) });
16
- const renderPool = config.useWorker !== false ? new RenderPool(config.fonts) : null;
17
- const renderConfig = {
18
- renderer,
19
- imageFormat: config.imageFormat ?? "png",
20
- caching: config.caching ?? true,
21
- devicePixelRatio: config.devicePixelRatio ?? 1,
22
- debug: config.debug ?? process.env.NODE_ENV !== "production",
23
- imageCacheMaxBytes: config.imageCacheMaxBytes ?? 16 * 1024 * 1024,
24
- touchStripCacheMaxBytes: config.touchStripCacheMaxBytes ?? 8 * 1024 * 1024,
25
- renderPool
26
- };
27
- const registry = new RootRegistry(renderConfig, adapter, async (settings) => {
28
- await adapter.setGlobalSettings(settings);
29
- }, config.wrapper);
30
- adapter.getGlobalSettings().then((gs) => {
31
- registry.setGlobalSettings(gs);
32
- }).catch((err) => {
33
- console.error("[@fcannizzaro/streamdeck-react] Failed to load global settings:", err);
34
- });
35
- adapter.onGlobalSettingsChanged((settings) => {
36
- registry.setGlobalSettings(settings);
37
- });
38
- for (const definition of config.actions) registerActionWithAdapter(adapter, definition, registry, config.onActionError);
39
- if (renderConfig.debug) metrics.enable();
40
- if (config.devtools) startDevtoolsServer({
41
- devtoolsName: adapter.pluginUUID,
42
- registry,
43
- renderConfig
44
- });
45
10
  return { async connect() {
11
+ const takumiMode = config.takumi ?? "native-binding";
12
+ const renderPool = (takumiMode === "wasm" ? false : config.useWorker !== false) ? new RenderPool(config.fonts) : null;
13
+ const renderConfig = {
14
+ renderer: await initializeRenderer(takumiMode, config.fonts),
15
+ imageFormat: config.imageFormat ?? "png",
16
+ caching: config.caching ?? true,
17
+ devicePixelRatio: config.devicePixelRatio ?? 1,
18
+ debug: config.debug ?? process.env.NODE_ENV !== "production",
19
+ imageCacheMaxBytes: config.imageCacheMaxBytes ?? 16 * 1024 * 1024,
20
+ touchStripCacheMaxBytes: config.touchStripCacheMaxBytes ?? 8 * 1024 * 1024,
21
+ renderPool
22
+ };
23
+ const registry = new RootRegistry(renderConfig, adapter, async (settings) => {
24
+ await adapter.setGlobalSettings(settings);
25
+ }, config.wrapper);
26
+ adapter.getGlobalSettings().then((gs) => {
27
+ registry.setGlobalSettings(gs);
28
+ }).catch((err) => {
29
+ console.error("[@fcannizzaro/streamdeck-react] Failed to load global settings:", err);
30
+ });
31
+ adapter.onGlobalSettingsChanged((settings) => {
32
+ registry.setGlobalSettings(settings);
33
+ });
34
+ for (const definition of config.actions) registerActionWithAdapter(adapter, definition, registry, config.onActionError);
35
+ if (renderConfig.debug) metrics.enable();
36
+ if (config.devtools) startDevtoolsServer({
37
+ devtoolsName: adapter.pluginUUID,
38
+ registry,
39
+ renderConfig
40
+ });
46
41
  if (renderPool != null) renderPool.initialize().catch(() => {});
47
42
  await adapter.connect();
48
43
  } };
49
44
  }
45
+ async function initializeRenderer(mode, fonts) {
46
+ const fontData = fonts.map((f) => ({
47
+ name: f.name,
48
+ data: f.data,
49
+ weight: f.weight,
50
+ style: f.style
51
+ }));
52
+ if (mode === "wasm") {
53
+ const wasm = await import("@takumi-rs/wasm");
54
+ await wasm.default();
55
+ return new wasm.Renderer({ fonts: fontData });
56
+ }
57
+ return new Renderer({ fonts: fontData });
58
+ }
50
59
  function registerActionWithAdapter(adapter, definition, registry, onError) {
51
60
  const handleError = (actionId, err) => {
52
61
  const error = err instanceof Error ? err : new Error(String(err));
package/dist/rollup.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Plugin } from 'rollup';
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 StreamDeckRollupOptions extends StreamDeckTargetOptions {
5
5
  /**
6
6
  * Path to the plugin `manifest.json`. When omitted, the plugin
package/dist/rollup.js CHANGED
@@ -1,4 +1,4 @@
1
- import { NOOP_DEVTOOLS_CODE, NOOP_DEVTOOLS_ID, copyNativeBindings, isDevelopmentMode, isLibraryDevtoolsImport, shouldStripDevtools } from "./bundler-shared.js";
1
+ import { NOOP_DEVTOOLS_CODE, NOOP_DEVTOOLS_ID, TAKUMI_NATIVE_LOADER_CODE, TAKUMI_NATIVE_LOADER_ID, copyNativeBindings, isDevelopmentMode, 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 { dirname, resolve } from "node:path";
@@ -39,6 +39,7 @@ function streamDeckReact(options = {}) {
39
39
  order: "pre",
40
40
  handler(source, importer) {
41
41
  if (shouldStripDevtools(this.meta.watchMode) && isLibraryDevtoolsImport(source, importer)) return NOOP_DEVTOOLS_ID;
42
+ if (options.takumi !== "wasm" && source === "@takumi-rs/core") return TAKUMI_NATIVE_LOADER_ID;
42
43
  return resolveFontId(source, importer);
43
44
  }
44
45
  },
@@ -46,6 +47,7 @@ function streamDeckReact(options = {}) {
46
47
  order: "pre",
47
48
  handler(id) {
48
49
  if (id === "\0streamdeck-react:noop-devtools") return NOOP_DEVTOOLS_CODE;
50
+ if (id === "\0streamdeck-react:takumi-native") return TAKUMI_NATIVE_LOADER_CODE;
49
51
  return loadFont(id);
50
52
  }
51
53
  },
@@ -5,44 +5,44 @@ import { TouchStripRoot } from "./touchstrip-root.js";
5
5
  var SEGMENT_WIDTH = 200;
6
6
  var KEY_SIZES = {
7
7
  0: {
8
- width: 72,
9
- height: 72
8
+ width: 144,
9
+ height: 144
10
10
  },
11
11
  1: {
12
- width: 80,
13
- height: 80
12
+ width: 144,
13
+ height: 144
14
14
  },
15
15
  2: {
16
- width: 96,
17
- height: 96
16
+ width: 144,
17
+ height: 144
18
18
  },
19
19
  3: {
20
- width: 72,
21
- height: 72
20
+ width: 144,
21
+ height: 144
22
22
  },
23
23
  4: {
24
- width: 72,
25
- height: 72
24
+ width: 144,
25
+ height: 144
26
26
  },
27
27
  5: {
28
- width: 72,
29
- height: 72
28
+ width: 144,
29
+ height: 144
30
30
  },
31
31
  6: {
32
- width: 72,
33
- height: 72
32
+ width: 144,
33
+ height: 144
34
34
  },
35
35
  7: {
36
36
  width: 144,
37
37
  height: 144
38
38
  },
39
39
  8: {
40
- width: 72,
41
- height: 72
40
+ width: 144,
41
+ height: 144
42
42
  },
43
43
  9: {
44
- width: 72,
45
- height: 72
44
+ width: 144,
45
+ height: 144
46
46
  },
47
47
  10: {
48
48
  width: 144,
@@ -72,8 +72,8 @@ function getCanvasInfo(deviceType, surfaceType) {
72
72
  };
73
73
  return {
74
74
  ...KEY_SIZES[deviceType] ?? {
75
- width: 72,
76
- height: 72
75
+ width: 144,
76
+ height: 144
77
77
  },
78
78
  type: "key"
79
79
  };
package/dist/types.d.ts CHANGED
@@ -73,12 +73,26 @@ export interface TouchStripLayout {
73
73
  items: TouchStripLayoutItem[];
74
74
  }
75
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";
76
78
  export interface PluginConfig {
77
79
  /** Stream Deck adapter. Defaults to physicalDevice() (Elgato SDK). */
78
80
  adapter?: StreamDeckAdapter;
79
81
  fonts: FontConfig[];
80
82
  actions: ActionDefinition[];
81
83
  wrapper?: WrapperComponent;
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;
82
96
  imageFormat?: "png" | "webp";
83
97
  caching?: boolean;
84
98
  devicePixelRatio?: number;
@@ -91,7 +105,7 @@ export interface PluginConfig {
91
105
  imageCacheMaxBytes?: number;
92
106
  /** Maximum TouchStrip raw buffer cache size in bytes. Set to 0 to disable. @default 8388608 (8 MB) */
93
107
  touchStripCacheMaxBytes?: number;
94
- /** Offload Takumi rendering to a worker thread. Set to false to disable. @default true */
108
+ /** Offload Takumi rendering to a worker thread. Set to false to disable. Automatically disabled when `takumi` is `"wasm"`. @default true */
95
109
  useWorker?: boolean;
96
110
  }
97
111
  export interface Plugin {
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.12",
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,22 +60,23 @@
60
60
  "test": "bun test ./src"
61
61
  },
62
62
  "dependencies": {
63
- "@takumi-rs/core": "^0.71.7",
64
- "@takumi-rs/helpers": "^0.71.7",
63
+ "@takumi-rs/core": "^0.73.1",
64
+ "@takumi-rs/helpers": "^0.73.1",
65
65
  "react-reconciler": "^0.33.0"
66
66
  },
67
67
  "devDependencies": {
68
- "@happy-dom/global-registrator": "^20.8.3",
68
+ "@happy-dom/global-registrator": "^20.8.4",
69
69
  "@types/react": "^19.2.14",
70
70
  "@types/react-dom": "^19.2.3",
71
71
  "@types/react-reconciler": "^0.33.0",
72
72
  "react-dom": "^19.2.4",
73
73
  "rollup": "^4.59.0",
74
- "vite": "8.0.0-beta.18",
74
+ "vite": "8.0.0",
75
75
  "vite-plugin-dts": "^4.5.4"
76
76
  },
77
77
  "peerDependencies": {
78
78
  "@elgato/streamdeck": "^2.0.2",
79
+ "@takumi-rs/wasm": "^0.71.7",
79
80
  "react": "^18.0.0 || ^19.0.0",
80
81
  "rollup": "^4.0.0",
81
82
  "typescript": "^5",
@@ -85,6 +86,9 @@
85
86
  "@elgato/streamdeck": {
86
87
  "optional": true
87
88
  },
89
+ "@takumi-rs/wasm": {
90
+ "optional": true
91
+ },
88
92
  "rollup": {
89
93
  "optional": true
90
94
  },