Addon Authoring
Create custom addons to extend Sireno Deck functionality.
Addon Structure
Section titled “Addon Structure”my-addon/├── sirenodeck.json # Manifest├── src/│ ├── index.ts # Entry point│ ├── manifest.ts # Button types and decks│ ├── frontend.tsx # React components│ └── backend.ts # Backend services└── dist/ # Compiled outputManifest (sirenodeck.json)
Section titled “Manifest (sirenodeck.json)”{ "kind": "addon", "apiVersion": 1, "name": "my-addon", "entry": "./dist/index.js"}Complete Addon Example
Section titled “Complete Addon Example”Step 1: Create Manifest
Section titled “Step 1: Create Manifest”import type { AddonManifestV1 } from "@sirenodeck/cli"
export const manifest: AddonManifestV1 = { apiVersion: 1, name: "my-addon", buttonTypes: { "my-addon:greet": { frontend: GreetButton, service: { gestureHandlers: ["tap"] as const, onTap: async (ctx) => { ctx.publish("my-addon:greeted", { name: "World" }) }, }, }, }, decks: [ { id: "my-addon:menu", name: "My Menu", icon: "icon://menu", buttons: [{ position: 0, type: "my-addon:greet" }], }, ],}Step 2: Create Frontend Component
Section titled “Step 2: Create Frontend Component”import React from "react"import type { AddonFrontendButtonProps } from "@sirenodeck/cli"
export function GreetButton({ config }: AddonFrontendButtonProps<{}>) { return ( <div> <Icon source="icon://hand" /> <Label text="Greet" /> </div> )}Step 3: Create Backend Service
Section titled “Step 3: Create Backend Service”import type { AddonButtonTypeService } from "@sirenodeck/cli"
export const GreetService: AddonButtonTypeService<{}> = { gestureHandlers: ["tap"], async onTap(ctx) { console.log("Button tapped!") ctx.methods.invalidate() },}Button Types API
Section titled “Button Types API”AddonButtonTypeDef
Section titled “AddonButtonTypeDef”interface AddonButtonTypeDef<Config> { readonly frontend: React.ComponentType<AddonFrontendButtonProps<Config>> readonly service: AddonButtonTypeService<Config>}AddonButtonTypeService
Section titled “AddonButtonTypeService”interface AddonButtonTypeService<Config> { readonly configSchema?: unknown // Zod schema for validation readonly defaultRenderIntervalMs?: number // Frontend refresh rate readonly internal?: boolean // Hidden from UI readonly full?: boolean // Takes full button area readonly gestureHandlers?: ReadonlyArray<GestureKind> readonly onMount?: ( ctx: AddonButtonServiceContext<Config>, ) => void | Promise<void> readonly onTap?: ( ctx: AddonButtonServiceContext<Config>, ) => void | Promise<void> readonly onDblTap?: ( ctx: AddonButtonServiceContext<Config>, ) => void | Promise<void> readonly onHold?: ( ctx: AddonButtonServiceContext<Config>, ) => void | Promise<void> readonly dispose?: ( ctx: AddonButtonServiceContext<Config>, ) => void | Promise<void>}AddonButtonServiceContext
Section titled “AddonButtonServiceContext”interface AddonButtonServiceContext<Config> { readonly config: Config readonly buttonId: string readonly addonName: string readonly methods: Readonly<Record<string, AddonServiceMethod>> // Namespaced addon methods readonly coreMethods: Methods // Core runtime methods readonly publish: (channel: string, data: unknown) => void readonly executor: ActionExecutor readonly signal: AbortSignal readonly store: Store}Deck Generation
Section titled “Deck Generation”Static Decks
Section titled “Static Decks”decks: [ { id: "my-addon:static", name: "Static Deck", buttons: [{ position: 0, type: "my-addon:action" }], },]Dynamic Decks
Section titled “Dynamic Decks”decks: [ { id: "my-addon:dynamic", createDeck: (ctx) => ({ name: "Generated", buttons: ctx.config.items.map((item, i) => ({ position: i, type: "my-addon:item", config: item, })), }), },]Multi-Deck Generation
Section titled “Multi-Deck Generation”decks: [ { createDecks: (ctx) => ({ "my-addon:deck1": { name: "Deck 1", buttons: [...] }, "my-addon:deck2": { name: "Deck 2", buttons: [...] } }) }]Global Services
Section titled “Global Services”Addons can provide global services that run continuously:
export const manifest: AddonManifestV1 = { apiVersion: 1, name: "my-addon", buttonTypes: { ... }, globalService: { pollers: [ { id: "my-poll", channel: "my-addon:data", intervalMs: 5000, poll: async (ctx) => ({ value: Date.now() }) } ], methods: { myMethod: (ctx, arg) => { ... } }, onLoad: async (ctx) => { ... }, onUnload: async (ctx) => { ... } }}Store API
Section titled “Store API”Persist data per-addon:
// Per-button storageconst scope = ctx.store.buttonScope(addonName, buttonId)scope.set("key", value)const value = scope.get("key")
// Per-addon storageconst scope = ctx.store.addonScope(addonName)scope.set("key", value)Publishing
Section titled “Publishing”Distribute data to frontend:
// Backendctx.publish("my-channel", { data: "value" })
// Frontendconst { data } = useAddonChannel("my-channel")Building
Section titled “Building”# Build the addonnpx tsc
# Output to dist/