Token导航 LogoToken导航

Agent Skill · 研究检索

streamdeck-react(streamdeck React 搜索)

用于辅助前端页面、组件、样式和交互逻辑的开发与维护。它适合让 Agent 生成或审查 React、Next.js、Vue、Tailwind、CSS 等相关代码,整理组件结构,或定位布局和性能问题。使用时需要结合项目现有设计系统、路由和构建方式,避免只生成孤立片段;涉及页面改动时,应配合本地预览和构建检查确认视觉效果。

streamdeck-react 是研究检索类 Agent Skill,可用宿主为 Codex、Claude、Cursor、Gemini CLI,通过GitHub 仓库获取,权限为需要联网;许可证信息还不明确,第三方扫描已通过,按安装量在研究检索类的 73,675 个 Skill 里排第 53,170。

总安装 539周安装 22GitHub Stars 2下载 174核验 2026-05-01

安装说明

本站只整理中文说明,不托管安装包,也不代用户安装。
GitHub
许可证
unknown

安装方式

通过对话安装

复制提示词发给支持本地命令或 Skills 的 AI 助手,先确认命令和权限,再让它执行。

请帮我安装这个 Agent Skill:streamdeck-react(streamdeck React 搜索)
来源仓库:https://github.com/fcannizzaro/streamdeck-react
仓库路径:skills/streamdeck-react
安装命令:
npx skills add https://github.com/fcannizzaro/streamdeck-react --skill streamdeck-react
安装前请先检查当前环境是否支持对应 CLI,并向我确认将要执行的命令、安装目录、联网范围和文件读写权限;确认后再执行。

命令行安装

复制命令到本机终端执行。该命令会通过 npx skills 从第三方来源获取 Skill;本站只展示命令,不托管安装包,也不自动执行。

skills.shnpx skills
npx skills add https://github.com/fcannizzaro/streamdeck-react --skill streamdeck-react

这个 Skill 是做什么的

研究检索类 Skill 主要解决「找资料、核对事实、把来源整理清楚」的问题,通常会让助手先检索再总结。

用于辅助 React、Next.js、Vue 等前端框架的开发与维护,支持组件与样式管理。支持生成或审查前端代码,整理组件结构并定位布局问题。通过 npx skills add 命令从指定 GitHub 仓库安装使用。使用时需结合项目现有设计系统和构建方式,避免生成孤立片段。

streamdeck-react 属于研究检索类 Skill,可作为该场景下的辅助能力补充。

来源仓库内路径:skills/streamdeck-react

上手步骤

4 步
  1. 1

    确认自己的宿主工具

    公开资料标注支持 Codex、Claude、Cursor、Gemini CLI;先确认自己用的是其中之一,再按对应工具的 Skills 目录放置。

  2. 2

    拿到安装命令

    可以直接复制的命令是 npx skills add https://github.com/fcannizzaro/streamdeck-react --skill streamdec…;命令里的占位符换成自己的路径或密钥再执行。

  3. 3

    安装后重启宿主

    命令执行完要完全退出宿主工具再打开,让新装的 Skill 被重新扫描到,否则列表里可能看不到。

  4. 4

    按最小权限试用

    它需要需要联网,先在测试目录或测试账号里跑,确认没问题再放到正式项目。

常见问题

6 条

streamdeck-react 怎么安装?

在终端执行 npx skills add https://github.com/fcannizzaro/streamdeck-react --skill streamdeck-react。装完完全退出宿主工具再打开,让它重新扫描本地 Skill。

streamdeck-react 支持哪些 AI 工具?

公开资料标注支持 Codex、Claude、Cursor、Gemini CLI。其他工具只要能读取本地 Skill 目录,一般也能手动放进去,但要自己确认触发方式是否一致。

streamdeck-react 需要什么权限?

权限级别是需要联网。该 Skill 可能需要联网访问来源站点、仓库或外部 API;具体网络访问范围需要结合源码和 README 复核。

streamdeck-react 是免费的吗?

公开资料没有标注明确许可证,收费和商用限制都无法确认。正式使用前请到来源仓库核对授权说明。

streamdeck-react 用的人多吗?

公开数据是累计安装 539 次,近一周安装 22 次,下载 174 次,GitHub 2 星。这些数字来自第三方目录统计,只能当作热度参考,不代表质量或安全性。

streamdeck-react 安全吗?

第三方扫描结果为通过,但这只覆盖已知的静态检查项,不能替代自己读一遍源码和安装命令。

值得选的地方和要先想清楚的地方

值得选的地方

  • 同时适配 4 种工具,换客户端不用重新找替代品。
  • 第三方扫描已通过,常见的高风险行为没有命中。

先想清楚的地方

  • 许可证没有明确标注,能不能商用、能不能再分发都不确定。
  • 第三方目录整理,版本更新和行为细节以来源仓库为准。

适不适合你

更适合

  • 已经在用 Codex,想直接扩展它能力的人。
  • 需要处理研究检索类任务、又不想每次都手写提示词的人。

不太适合

  • 需要明确授权才能用到公司项目里的场景。
  • 需要厂商级技术支持和 SLA 的生产系统。

研究检索类 Skill 对比

6 个同类
Skill总安装GitHub 星权限许可证扫描
find-skills

寻找技能 · Codex、Claude

31,199,81316,486操作浏览器MIT有提醒
self-improving-agent

自我改进 Agent · OpenClaw

16,464,0023,396接触敏感数据MIT-0通过
soultrace

灵魂追踪 · Codex、Claude

7,588,94412需要联网未明确有提醒
self-improving

自我完善 · OpenClaw

6,829,2841,046需要联网MIT-0通过
microsoft-foundry

微软代工厂 · Codex、Claude

6,614,144764调用外部服务未明确有提醒
streamdeck-react

streamdeck React 搜索 · 本页

5392需要联网未明确通过

按安装量取同分类前几名;数字来自第三方目录统计,只作热度参考,不代表质量结论。

SKILL.md 原文

streamdeck-react

A custom React renderer that turns JSX into rendered images for Elgato Stream Deck hardware. Each action instance gets its own isolated React root with full hooks, state, and lifecycle support.

When to Use This Skill

Use when the user is:

  • Creating or modifying a Stream Deck plugin that uses @fcannizzaro/streamdeck-react
  • Asking about rendering React components on Stream Deck keys or dials, or handling touch input
  • Working with @elgato/streamdeck SDK in a React-based plugin
  • Implementing a custom adapter for web simulation or testing
  • Setting up Vite bundling for a Stream Deck plugin with native Takumi bindings
  • Scaffolding a brand new plugin and needs a sensible project template or starter example

Architecture (5-Stage Pipeline)

React Tree --> Reconciler --> VNode Tree --> Takumi --> Adapter --> setImage/setFeedback
(JSX+Hooks)   (host config)   (plain JS)   (JSX->PNG)  (bridge)    (hardware/simulator)
  1. Your components render standard React with hooks and state.
  2. A custom react-reconciler manages the fiber tree.
  3. On commit, host nodes form a virtual tree of {type, props, children} with back-pointers for dirty propagation.
  4. The Takumi renderer converts the tree to a PNG/WebP image buffer via a direct VNode-to-Takumi bypass (skips createElement + fromJsx).
  5. The adapter pushes the image to the backend via action.setImage() or action.setFeedback(). The default physicalDevice() adapter wraps the Elgato SDK; custom adapters handle it differently.

4-Phase Skip Hierarchy

Every render passes through a multi-tier skip hierarchy to avoid redundant work:

Phase 1: Dirty-flag check (O(1)) → skip if no VNode mutated
Phase 2: Merkle hash → Image cache lookup → skip if hash matches cached render
Phase 3: Takumi render (main thread or worker) → rasterize
Phase 4: xxHash output dedup → skip hardware push if identical to last frame

Two entry points: renderToDataUri (keys/dials → base64 data URI) and renderToRaw (TouchStrip → raw RGBA Buffer).

Flush Coordinator

When multiple roots request flushes in the same tick, the FlushCoordinator batches them via microtask and processes in priority order:

  • Priority 0 (animating) → 1 (interactive) → 2 (normal) → 3 (idle)
  • Sequential execution ensures higher-priority roots get first access to the USB bus.

Root Recycling Pool

When actions disappear (profile switch, page navigation), the root is suspended rather than destroyed — the fiber tree stays alive. When the same action type reappears, the root is resumed with new context data, avoiding expensive fiber root creation.

  • Pool key: ${actionUUID}:${canvasType} — ensures component and surface compatibility.
  • Reduces profile switch latency from ~160-480ms to ~32-96ms on a 32-key device.
  • LRU eviction with configurable max size (default 16 entries).

Context Provider Tree

Every action root uses 6 context providers (stable outermost, volatile innermost):

CoordinatorContext.Provider    ← plugin-level coordinator (null if not enabled)
  ThemeContext.Provider          ← plugin-level theme (CSS variables + setter)
    RootContext.Provider          ← merged: action + device + canvas + streamDeck (immutable)
      EventBusContext.Provider    ← per-root EventBus (new instance on resume)
        GlobalSettingsContext.Provider  ← plugin-wide settings
          SettingsContext.Provider      ← per-action settings
            PluginWrapper / ActionWrapper / <UserComponent />

CoordinatorContext and ThemeContext are plugin-level singletons that rarely or never change. RootContext merges ActionInfo, DeviceInfo, CanvasInfo, and StreamDeckAccess into a single provider, eliminating 3 fiber nodes per root. For 32 active roots, this saves 96 provider fiber nodes.

Code-First Manifest Generation

The Vite bundler plugin auto-generates manifest.json from code:

  • Action metadata is defined in defineAction({info: {name, icon,...}}) — the bundler plugin auto-extracts it from the module graph at build time via AST analysis.
  • Plugin metadata (uuid, name, author, description, icon, version) is provided via the manifest option in the bundler plugin config.
  • Controllers are auto-derived from key/dial/touchStrip presence on each action.
  • Defaults are applied for OS, Nodejs, SDKVersion, Software, CodePath, Category, States.
  • Actions with info.disabled: true are excluded from the manifest but still work at runtime.
  • The manifest is written to the .sdPlugin directory during writeBundle.
  • Skips write if content unchanged (avoids unnecessary recompilation in watch mode).

No hand-written manifest.json is needed.

Each visible action instance on the hardware gets its own isolated React root. No shared state between roots unless you use the built-in Action Coordinator (coordinator: true), an external store (Zustand, Jotai), or the wrapper API.

Native Module Lazy Loading

Native .node binaries (Takumi and any user-registered nativeModules) are lazy-loaded by default — downloaded from npm on first plugin startup and cached on disk.

Build-Time Pipeline
  1. The streamDeckReact() Vite plugin resolves the installed version of each native module from its package.json.
  2. A self-contained virtual ESM module is generated for each, embedding the version, npm scope, and platform-to-binding map.
  3. The bundler's resolveId hook replaces all imports of the native module with the virtual loader. No user code changes needed.

Version resolution uses three strategies in order: createRequire from project root, from library location (hoisted packages), and direct node_modules walk (for packages with restricted exports maps). If all fail, that module falls back to copy mode with a build-time warning.

Runtime Flow
Read .native-versions.json manifest
  ↓
existsSync(nodePath) && cachedVersion === VERSION?
  ├── YES → require() cached .node file (fast path, ~1ms)
  └── NO  → fetch npm tarball → gunzipSync → inline tar parse
            → writeFileSync .node to disk
            → update .native-versions.json
            → require()

The .node file is written next to the bundle output (import.meta.url-relative) and persists across restarts.

Version Manifest (.native-versions.json)

Tracks cached binary versions for cache invalidation on dependency upgrades:

{ "core.darwin-arm64.node": "0.73.1" }

When the baked-in VERSION (from build time) differs from the manifest entry, the binary is re-downloaded. Each native module only reads/writes its own key — concurrent module evaluation is safe.

Tarball Extraction

The loader includes a minimal inline tar parser (no external dependency). npm tarballs use 512-byte headers (filename at offset 0, size at offset 124 in octal). The parser scans sequentially until it finds the target .node file.

Copy Mode Alternative

Set nativeBindings: "copy" for air-gapped/offline environments. Requires platform packages installed and targets specified. .node files are copied from node_modules during writeBundle. Missing bindings are warnings in dev, errors in production.

Custom Native Modules

Register additional NAPI-RS packages via nativeModules on streamDeckReact(). Each entry gets the same lazy/copy treatment. Validation at build time catches empty exports/bindings, duplicate specifiers, filename collisions, and Takumi conflicts. See references/bundling.md for configuration details.

Adapter Layer

The adapter layer abstracts the @elgato/streamdeck SDK behind a pluggable StreamDeckAdapter interface. This makes the SDK an optional peer dependency and enables alternative backends (web simulator, test harness).

  • physicalDevice() is the default adapter wrapping the real Elgato SDK. It is the only module that value-imports from @elgato/streamdeck.
  • Pass a custom adapter via createPlugin({adapter: myAdapter()}).
  • All hooks (useOpenUrl, useSwitchProfile, useSendToPI, useShowAlert, useShowOk, useTitle, useDialHint) route through the adapter.
  • AdapterActionHandle is a flat interface unifying Key/Dial/Action. Inapplicable methods (e.g., setImage on dial) no-op.
  • See references/adapter.md for full interface definitions and custom adapter example.

Project Setup

New Plugin

For greenfield projects, prefer the scaffolder first:

npm create streamdeck-react@latest

It asks for the plugin UUID, author, platforms, native targets, starter example, and whether to use React Compiler, then generates a working project.

To use React Compiler via CLI flag:

npm create streamdeck-react@latest --react-compiler true

React Compiler automatically memoizes components at build time, preventing unnecessary re-renders. This is especially beneficial in this environment because every re-render triggers an expensive rasterization pipeline (VNode tree -> Takumi layout -> Rust PNG render -> hardware).

If the user wants to build it manually, use this structure:

A minimal plugin project needs:

my-plugin/
  src/
    plugin.ts           # Entry point
    actions/
      my-action.tsx     # Action component + defineAction with info
  com.example.my-plugin.sdPlugin/
    bin/                # Vite output goes here
    imgs/               # Action and plugin icons
  vite.config.ts
  package.json
  tsconfig.json

Dependencies

# Runtime
npm install @fcannizzaro/streamdeck-react react

# Runtime support used by the Stream Deck SDK
npm install ws

# Build tooling
npm install -D vite@8.0.0 @vitejs/plugin-react@6.0.1

# Build tooling (with React Compiler -- add on top of base)
# npm install -D @rolldown/plugin-babel @babel/core babel-plugin-react-compiler

# Build tooling (with Tailwind v4 CSS -- add on top of base)
# npm install -D @tailwindcss/vite

# Types (if using TypeScript)
npm install -D @types/react

Native Takumi bindings are lazy-loaded by default — they are downloaded from npm on first plugin startup and cached on disk. No platform-specific @takumi-rs/core-* packages need to be installed. Only the main @takumi-rs/core package is required (included as a dependency of @fcannizzaro/streamdeck-react).

To opt out of lazy loading and copy binaries from node_modules at build time instead, set nativeBindings: "copy" on streamDeckReact() and install the matching @takumi-rs/core-* packages. See references/bundling.md for the full platform matrix.

package.json

Must use "type": "module". Example:

{
  "type": "module",
  "scripts": {
    "build": "vite build",
    "dev": "vite build --watch"
  }
}

tsconfig.json

{
  "compilerOptions": {
    "lib": ["ESNext"],
    "target": "ESNext",
    "module": "Preserve",
    "jsx": "react-jsx",
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "noEmit": true,
    "strict": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}

Core Workflow

Step 1: Define Actions

// src/actions/counter.tsx
import { useState } from "react";
import { defineAction, useKeyDown, useKeyUp, cn } from "@fcannizzaro/streamdeck-react";

function CounterKey() {
  const [count, setCount] = useState(0);
  const [pressed, setPressed] = useState(false);

  useKeyDown(() => {
    setCount((c) => c + 1);
    setPressed(true);
  });

  useKeyUp(() => setPressed(false));

  return (
    <div
      className={cn(
        "flex flex-col items-center justify-center w-full h-full gap-1",
        pressed ? "bg-[#2563eb]" : "bg-[#0f172a]",
      )}
    >
      <span className="text-white/70 text-[12px] font-medium">COUNT</span>
      <span className="text-white text-[36px] font-bold">{count}</span>
    </div>
  );
}

export const counterAction = defineAction({
  uuid: "com.example.my-plugin.counter",
  key: CounterKey,
  info: {
    name: "Counter",
    icon: "imgs/actions/counter",
  },
});

Step 2: Create the Plugin Entry

// src/plugin.ts
import { createPlugin, googleFont } from "@fcannizzaro/streamdeck-react";
import { counterAction } from "./actions/counter.tsx";

const inter = await googleFont("Inter");

const plugin = createPlugin({
  fonts: [inter],
  actions: [counterAction],
});

await plugin.connect();

Step 3: Configure Vite

Default setup (Oxc transforms):

// vite.config.ts
import { builtinModules } from "node:module";
import { resolve } from "node:path";
import { defineConfig, esmExternalRequirePlugin } from "vite";
import react from "@vitejs/plugin-react";
import { streamDeckReact } from "@fcannizzaro/streamdeck-react/vite";

const PLUGIN_DIR = "com.example.my-plugin.sdPlugin";
const builtins = builtinModules.flatMap((m) => [m, `node:${m}`]);

export default defineConfig({
  resolve: {
    conditions: ["node"],
  },
  plugins: [
    esmExternalRequirePlugin({ external: builtins }),
    react(),
    streamDeckReact({
      uuid: "com.example.my-plugin",
      manifest: {
        uuid: "com.example.my-plugin",
        name: "My Plugin",
        author: "Your Name",
        description: "A Stream Deck plugin built with React.",
        icon: "imgs/plugin-icon",
        version: "0.0.0.1",
      },
    }),
  ],
  build: {
    target: "node20",
    outDir: resolve(PLUGIN_DIR, "bin"),
    emptyOutDir: false,
    sourcemap: true,
    minify: false,
    lib: {
      entry: resolve("src/plugin.ts"),
      formats: ["es"],
      fileName: () => "plugin.mjs",
    },
    rolldownOptions: {
      output: {
        codeSplitting: false,
      },
    },
  },
});

With React Compiler (add Babel on top):

// vite.config.ts
import { builtinModules } from "node:module";
import { resolve } from "node:path";
import { defineConfig, esmExternalRequirePlugin } from "vite";
import react, { reactCompilerPreset } from "@vitejs/plugin-react";
import babel from "@rolldown/plugin-babel";
import { streamDeckReact } from "@fcannizzaro/streamdeck-react/vite";

const PLUGIN_DIR = "com.example.my-plugin.sdPlugin";
const builtins = builtinModules.flatMap((m) => [m, `node:${m}`]);

export default defineConfig({
  resolve: {
    conditions: ["node"],
  },
  plugins: [
    esmExternalRequirePlugin({ external: builtins }),
    react(),
    // @ts-expect-error — @rolldown/plugin-babel types incorrectly mark inherited babel fields as required
    await babel({
      presets: [reactCompilerPreset()],
    }),
    streamDeckReact({
      uuid: "com.example.my-plugin",
      manifest: {
        uuid: "com.example.my-plugin",
        name: "My Plugin",
        author: "Your Name",
        description: "A Stream Deck plugin built with React.",
        icon: "imgs/plugin-icon",
        version: "0.0.0.1",
      },
    }),
  ],
  build: {
    target: "node20",
    outDir: resolve(PLUGIN_DIR, "bin"),
    emptyOutDir: false,
    sourcemap: true,
    minify: false,
    lib: {
      entry: resolve("src/plugin.ts"),
      formats: ["es"],
      fileName: () => "plugin.mjs",
    },
    rolldownOptions: {
      output: {
        codeSplitting: false,
      },
    },
  },
});

Native bindings are lazy-loaded by default — they are downloaded from npm on first plugin startup and cached on disk. No targets option is needed.

manifest.json is auto-generated. You do not need to write or maintain it by hand. Action metadata is extracted from defineAction({info}) calls at build time.

Step 4: Build and Verify

The manifest.json is auto-generated during the build. Action metadata comes from defineAction({info}) calls, and plugin metadata from the bundler plugin's manifest option.

Critical: The uuid in each defineAction() must start with the plugin UUID prefix (e.g., "com.example.my-plugin.").

Step 5: Dev

npx vite build --watch

Install the .sdPlugin folder in the Stream Deck app.

If your package.json has a dev script configured, you can also just run bun dev (or npm run dev / pnpm dev).

Hook Quick Reference

CategoryHooksPurpose
EventsuseKeyDown, useKeyUpKey press/release
EventsuseDialRotate, useDialDown, useDialUpEncoder rotation/press
EventsuseTouchTapTouch strip tap
EventsuseDialHintSet encoder trigger descriptions
GesturesuseTapSingle tap (auto-delayed when useDoubleTap is active)
GesturesuseLongPressKey held for configurable duration (default 500ms)
GesturesuseDoubleTapTwo rapid taps within configurable window (default 250ms)
SettingsuseSettings, useGlobalSettingsBidirectional settings sync
LifecycleuseWillAppear, useWillDisappearAction mount/unmount
ContextuseDevice, useAction, useCanvasDevice/action/canvas metadata
ContextuseStreamDeckAdapter and action handle
SizeuseSizePercentage-based and proportional size calculations
CoordinatoruseChannelCross-action shared state via named channels
CoordinatoruseActionPresenceLive snapshot of visible action instances
CoordinatoruseCoordinatorRaw coordinator instance (escape hatch)
ThemeuseThemeRead/write CSS theme variables at runtime
SDKuseOpenUrl, useSwitchProfileSystem actions
SDKuseSendToPI, usePropertyInspectorPI communication
SDKuseShowAlert, useShowOk, useTitleKey overlays
UtilityuseInterval, useTimeout, usePreviousTimers and helpers
UtilityuseTickAnimation frame loop
AnimationuseSpring, useTweenPhysics and easing-based value animation
AnimationSpringPresets, EasingsBuilt-in spring presets and easing functions

See references/hooks.md for full signatures and usage.

Component Quick Reference

ComponentElementPurpose
BoxdivFlex container with shorthand props (center, padding, gap, direction)
TextspanText with shorthand props (size, color, weight, align, font)
ImageimgImage with required width/height, optional fit
IconsvgSingle SVG path icon with path, size, color
ProgressBardivHorizontal progress bar with value/max
CircularGaugesvgRing/arc gauge with value/max/size/strokeWidth
ErrorBoundary--Catches errors, renders fallback

All components are optional convenience wrappers. Raw div, span, img, svg elements work directly.

See references/components.md for full props tables.

Styling

Prefer Tailwind classes for all static styling. Use inline style only for dynamic values computed at runtime.

  1. Tailwind classes via className — the primary styling approach. Resolved by Takumi at render time (no CSS build step): <div className="flex items-center justify-center w-full h-full bg-[#1a1a1a]"> <span className="text-white text-[18px] font-bold">Ready</span> </div>
  2. cn() utility for conditional classes (like clsx). tw() is a deprecated alias: <div className={cn("flex items-center justify-center w-full h-full", pressed? "bg-blue-600": "bg-[#0f172a]",)} > <span className={cn("text-[28px] font-bold", pressed? "text-white": "text-white/70")}> {count} </span> </div>
  3. Inline style — only for dynamic values that can't be known at write-time: ` // ✅ Dynamic values: animation outputs, size.scale(), computed colors <span className="text-white font-bold" style={{fontSize: size.scale(24)}}>OK</span> <div className="flex items-center justify-center w-full h-full" style={{backgroundColor: hsl(${hue}, 60%, 25%)}} /> // ❌ Avoid: inline styles for static layout <div style={{display: "flex", width: "100%", height: "100%", background: "#1a1a1a"}}> `

Layout rules:

  • Prefer flexbox layout (flex, flex-col, items-center, justify-center, gap-*) over absolute positioning.
  • Use w-full h-full for full-size containers, not style={{width: "100%", height: "100%"}}.
  • Use Tailwind's arbitrary value syntax for one-off values: bg-[#1a1a2e], text-[14px], p-[6px], rounded-[12px].
  • Use position: absolute only when elements truly need to overlap (rare — most layouts work with flexbox).

State Management Decision Guide

NeedSolution
Simple per-action stateuseState / useReducer
Persist per-action settings across reloadsuseSettings<T>()
Plugin-wide shared configuseGlobalSettings<T>()
Simple cross-action state (built-in)useChannel() via Action Coordinator (coordinator: true)
Know which actions are visibleuseActionPresence() via Action Coordinator
Shared state across actions (no provider needed)Zustand store in module scope
Shared state with provider patternJotai/React Context via wrapper on createPlugin or defineAction
Complex derived state / middlewareZustand or Jotai

Action Coordinator

Built-in cross-action communication, opt-in via createPlugin({coordinator: true}):

const plugin = createPlugin({
  coordinator: true,
  fonts: [...],
  actions: [...],
});

Channels

Named publish/subscribe channels with latest-value semantics:

// In a "play/pause" action:
const [state, setState] = useChannel<"playing" | "paused">("playback", "paused");
useKeyDown(() => setState(state === "playing" ? "paused" : "playing"));

// In a "now playing" action (reads same channel):
const [state] = useChannel<"playing" | "paused">("playback", "paused");
  • Scoped re-renders — only subscribers of the changed channel re-render.
  • Sticky values — new subscribers receive the current value immediately.
  • Referential skip — updates are skipped when === identity matches.

Presence Tracking

const presence = useActionPresence();
const volumeActions = presence.byUuid("com.example.plugin.volume");
const totalVisible = presence.count;

Imperative Access

const coordinator = useCoordinator();
coordinator.setChannelValue("volume", 50);

CSS Theme System

Centralized design tokens injected as CSS custom properties:

import { defineTheme, mergeThemes } from "@fcannizzaro/streamdeck-react";

const theme = defineTheme({
  colors: { primary: "#4CAF50", surface: "#1a1a2e" },
  spacing: { sm: "4px", md: "8px", lg: "16px" },
  fontSize: { body: "14px", heading: "24px" },
});

createPlugin({ theme, ... });

Tokens are mapped to CSS variables: colors.primary → --color-primary.

Use in components via Tailwind arbitrary values:

<div className="bg-[var(--color-surface)]">
  <span className="text-[var(--color-primary)]">Themed</span>
</div>

Dynamic Theme Switching

const [variables, setTheme] = useTheme();
useKeyDown(() => setTheme(darkTheme)); // All roots re-render with new variables

Merging Themes

const merged = mergeThemes(baseTheme, darkOverride);
// Later themes override earlier ones for the same variable name

Tailwind v4 CSS Support

For full Tailwind v4 support including @theme blocks, custom utilities, and standard CSS, use the stylesheets option in createPlugin():

// Install: npm install -D @tailwindcss/vite
// vite.config.ts: add tailwindcss() to plugins

// theme.css — @import "tailwindcss"; @theme { --color-primary: #4CAF50; }
import stylesheet from "./theme.css?inline";

const plugin = createPlugin({
  stylesheets: [stylesheet],
  fonts: [await googleFont("Inter")],
  actions: [...],
});

With stylesheets, Tailwind v4 @theme tokens become first-class utility classes:

// With stylesheets: bg-primary instead of bg-[var(--color-primary)]
<div className="bg-primary text-white">Themed</div>

defineTheme() and stylesheets can be used together. defineTheme() supports runtime switching via useTheme(), while stylesheets provides first-class Tailwind v4 integration for build-time themes.

Size Calculation Utility

Percentage-based and proportional size helpers for responsive layouts:

import { calcSize, useSize } from "@fcannizzaro/streamdeck-react";

// Standalone (no React context):
const s = calcSize(144, 144);
s.w(50); // 72 (50% of width)
s.scale(16); // 16 (proportional to 144px reference)

// Hook (reads canvas dimensions from context):
function MyKey() {
  const size = useSize();
  return <span style={{ fontSize: size.scale(24) }}>{size.square ? "Key" : "Dial"}</span>;
}

SizeHelper Methods

MethodDescription
w(pct)Percentage of canvas width, rounded
h(pct)Percentage of canvas height, rounded
minP(pct)Percentage of min(width, height), rounded
maxP(pct)Percentage of max(width, height), rounded
scale(px)Scale a base pixel value proportionally (ref: 144px)

Properties: width, height, min, max, square, landscape, portrait, aspectRatio.

Encoder / Dial Actions

For Stream Deck+ encoders, provide a dial component in defineAction. If omitted, the key component is used as fallback on encoder slots.

export const volumeAction = defineAction({
  uuid: "com.example.my-plugin.volume",
  key: VolumeKey,
  dial: VolumeDial,
  info: {
    name: "Volume",
    icon: "imgs/actions/volume",
    encoder: {
      layout: "$A0",
      triggerDescription: {
        rotate: "Adjust volume",
        push: "Mute / Unmute",
      },
    },
  },
});

The info.encoder block tells the Stream Deck UI about dial interactions. Controllers are auto-derived: if dial or touchStrip is present, ["Encoder"] is used; if key is also present, ["Keypad", "Encoder"].

For touch interaction on Stream Deck+, use useTouchTap() inside the mounted action root. Treat touch as input handling, not as a separate primary rendering surface.

Critical Gotchas

  1. Fonts are mandatory -- the renderer cannot access system fonts. Use googleFont("Inter") to download TTF fonts from Google Fonts (cached to .google-fonts/ on disk). Alternatively, load font files manually via readFile. Supported formats depend on the backend: native-binding supports .ttf, .otf, .woff, .woff2; WASM mode only supports .ttf and .otf.
  2. plugin.connect() must be called last -- after createPlugin() and all setup.
  3. UUID prefix -- every action uuid in defineAction() must start with the plugin UUID prefix (e.g., "com.example.my-plugin."). The manifest is auto-generated from these.
  4. Native bindings are lazy-loaded by default -- the plugin downloads the platform-specific .node binary from npm on first startup and caches it on disk. No targets option or @takumi-rs/core-* platform packages are needed. Set nativeBindings: "copy" to revert to the old behavior of copying from node_modules. Additional native modules can be registered via the nativeModules option on streamDeckReact() — each entry gets the same lazy/copy treatment as the built-in Takumi binding. See references/bundling.md for configuration details.
  5. Install ws -- required by the Stream Deck SDK runtime. When using the WASM backend (takumi: "wasm"), install @takumi-rs/wasm instead and native binding packages are not needed.
  6. No animated images -- each setImage call is a static frame. Use useTick for manual animation loops, or the higher-level useSpring and useTween hooks for physics-based and easing-based animation.
  7. WASM backend limitations -- takumi: "wasm" is available for environments where native addons can't load (WebContainers, browsers). It force-disables worker threads and does not support WOFF/WOFF2 fonts (use TTF/OTF only). Pass takumi: "wasm" to both createPlugin() and streamDeckReact() to skip native binary copying at build time.
  8. Design for 72x72 minimum -- smallest key size. Use useCanvas() to adapt to larger devices.
  9. Use simple layouts -- this is not a browser DOM. Stick to flexbox layouts via Tailwind classes (flex, flex-col, items-center, gap-*), fixed sizes, and simple elements (div, span, img, svg, p). Avoid absolute positioning unless elements truly need to overlap.
  10. Animation FPS -- Stream Deck hardware refreshes at max 30Hz. The useTick, useSpring, and useTween hooks default to 30fps (clamped). Design animations accordingly.

Verification Checklist

When scaffolding or modifying a @fcannizzaro/streamdeck-react plugin, verify:

  • @fcannizzaro/streamdeck-react and react are in dependencies
  • ws is installed for the Stream Deck SDK runtime
  • package.json has "type": "module"
  • tsconfig.json has "jsx": "react-jsx"
  • At least one font is loaded via googleFont() or manual readFile and passed to createPlugin()
  • Every defineAction() has info: {name, icon} for manifest generation
  • Every defineAction() UUID starts with the plugin UUID prefix
  • vite.config.ts includes streamDeckReact({manifest: {uuid, name, author,...}})
  • Encoder actions have info.encoder with layout and triggerDescription
  • plugin.connect() is called after createPlugin()
  • Build completes without errors: npx vite build
  • manifest.json is auto-generated in the .sdPlugin directory after build
  • If React Compiler is enabled: output bundle contains react.memo_cache_sentinel (proof compiler is active)

DevTools

A browser-based inspector for debugging plugins during development. When enabled, the plugin starts an HTTP + SSE (Server-Sent Events) server on localhost (port range 39400-39499) and the browser UI auto-discovers running plugins by scanning that range.

Enabling

const plugin = createPlugin({
  devtools: true, // starts the devtools server (port derived from plugin UUID)
  fonts: [
    // ...your fonts
  ],
  actions: [
    /* ... */
  ],
});

Opening the DevTools

Panels

PanelDescription
ConsoleIntercepted console.log/warn/error/info/debug output
NetworkIntercepted fetch requests and responses
ElementsVNode tree inspector with element highlighting on the physical device
PreviewLive rendered images for every active action and touch bar
EventsEventBus emissions (keyDown, dialRotate, touchTap, etc.)
PerformanceRender pipeline metrics: flush counts, skip rates, cache stats, render timing

Key Details

  • Element highlighting -- hover a node in the Elements tree to highlight it with a cyan overlay on the Stream Deck hardware.
  • Multi-plugin support -- discovers and switches between multiple running plugins.
  • Automatic production stripping -- all devtools code, the ws dependency, and instrumentation hooks are removed from the bundle when NODE_ENV=production (non-watch builds). Zero overhead in release builds.

Detailed References

平台分布

Codex

39.02%
按下载量换算68

Claude

28.2%
按下载量换算49

Cursor

18.52%
按下载量换算32

Gemini CLI

10.91%
按下载量换算19

安全审计

Gen Agent Trust Hub

通过

Socket

通过

Snyk

通过

权限和风险

需要联网

该 Skill 可能需要联网访问来源站点、仓库或外部 API;具体网络访问范围需要结合源码和 README 复核。

安装前确认

本站仅展示第三方公开信息,不托管安装包,不提供自动安装或运行环境。安装前应自行审查源码、依赖和命令行为。目前只有一个公开来源,建议同时核对源仓库。

继续浏览同类 Skills