← Getting Started

SDK Reference

Complete reference for the Polyhedral SDK — components, APIs, and theme. Looking for working code? Browse the examples.

SDK API

Sheets run inside a sandboxed iframe with React 19 and ReactDOM available as globals. The Polyhedral global provides access to data, state, dice rolling, theme variables, and the component library.

Polyhedral.sheet      // getData(), setData(), onDataChange()
Polyhedral.state      // get(), set() — ephemeral UI state
Polyhedral.dice       // roll(), roll3d(), rollToChat()
Polyhedral.storage    // upload() — image uploads to Supabase Storage
Polyhedral.events     // on(), off(), once() — live game event subscriptions
Polyhedral.components // 19 built-in React components
Polyhedral.theme      // current color tokens + onChange()
Polyhedral.meta       // instanceId, gameId, characterName, userId, isEditor
Polyhedral.useFieldData // React hook for field data with live updates

Component Library

19 pre-built components that match the Polyhedral design system. Destructure them from Polyhedral.components.

Layout

<Sheet>

Root wrapper with padding and theme.

{ children }
<Section>

Card-like grouping with optional heading.

{ title?, children }
<Row>

Horizontal flex row.

{ gap?, children }
<Column>

Vertical flex column.

{ gap?, children }
<Grid>

CSS grid layout.

{ columns, gap?, children }

Data Display

<StatBlock>

Label + value pair in a bordered box.

{ label, value }
<AbilityScore>

D&D-style ability score with modifier.

{ name, score }
<HealthBar>

Colored HP bar.

{ current, max, label? }
<ResourceTrack>

Pip-based tracker (spell slots, etc).

{ label, current, max, onToggle? }
<Badge>

Status pill. Variants: default, success, warning, danger.

{ children, variant? }
<Portrait>

Character portrait from field data URL.

{ field, size? }

Interactive

<EditableText>

Text input bound to field data.

{ field, placeholder?, label? }
<EditableNumber>

Number input bound to field data.

{ field, min?, max?, label? }
<Checkbox>

Boolean toggle bound to field data.

{ field, label }
<Select>

Dropdown select bound to field data.

{ field, label?, options }
<DiceButton>

Rolls dice on click. Default mode 'chat' animates 3D dice and posts to chat. '3d' animates without chat. 'silent' is instant math only.

{ notation, label?, mode?: 'silent'|'3d'|'chat', children? }

Typography

<Heading>

h1, h2, or h3.

{ level?, children }
<Text>

Body text. Sizes: small, default, large.

{ size?, muted?, children }
<Label>

Small uppercase label.

{ children }

Dice API

Three tiers of dice rolling, from silent math to full 3D animation with chat integration.

Polyhedral.dice.roll(notation)

Silent math — instant result, no 3D animation, no chat message. Returns {total, rolls}.

Polyhedral.dice.roll3d(notation, label?)

3D animated roll — shows dice animation on screen but does not post to chat. Returns {total, rolls}.

Polyhedral.dice.rollToChat(notation, label?)

3D animated roll + posts the result to chat. The label appears as a heading above the roll result. Returns {total, rolls}.

// Silent math — instant, no visuals
const result = await Polyhedral.dice.roll('2d6+3');

// 3D animated — dice fly across screen
const result = await Polyhedral.dice.roll3d('1d20', 'Attack Roll');

// 3D animated + posts to chat
const result = await Polyhedral.dice.rollToChat('1d20', 'Attack Roll');

// All three return { total: number, rolls: number[] }

Storage API

Upload images (portraits, icons, etc.) from within the sandbox. Files are stored in Supabase Storage and a public URL is returned.

Polyhedral.storage.upload(file)

Accepts a File instance (max 5 MB, images only). Returns a Promise<string> resolving to the public URL.

// Let the user pick a portrait image
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.onchange = async () => {
  const file = input.files[0];
  const url = await Polyhedral.storage.upload(file);
  Polyhedral.sheet.setData({ portrait: url });
};
input.click();

Events API

Subscribe to live game events from within your sheet. When someone rolls dice, activates a map, or changes an encounter, your sheet is notified in real time — no polling, no server required.

events.on(eventType, callback)    // subscribe, returns unsubscribe fn
events.off(eventType, callback)   // remove a callback
events.once(eventType, callback)  // subscribe for one event, then auto-remove

Callbacks receive a GameEvent with eventType, userId, payload, and timestamp.

// Subscribe to dice rolls
const unsub = Polyhedral.events.on('dice.rolled', (event) => {
  console.log(event.userId, 'rolled', event.payload);
});

// Listen for a single encounter change
Polyhedral.events.once('encounter.created', (event) => {
  // Update UI for new encounter
});

// Clean up when done
unsub();

Available Events

dice.rolled              // notation, total, individual rolls
encounter.created        // encounter data
encounter.updated        // updated encounter data
encounter.deleted        // deleted encounter id
map.activated            // map shown to players
map.deactivated          // map hidden from players
table.rolled             // rollable table result

GameEvent Shape

interface GameEvent {
  eventType: string;   // e.g. 'dice.rolled'
  userId: string;      // who triggered the event
  payload: object;     // event-specific data
  timestamp: string;   // ISO 8601
}

useFieldData Hook

A React hook that reads a single field from field_data and re-renders the component when it changes. Cleaner than calling getData() + onDataChange() manually.

const hp = Polyhedral.useFieldData('hp', 0);
const name = Polyhedral.useFieldData('name', 'Unnamed');

// Equivalent to:
// const [hp, setHp] = useState(Polyhedral.sheet.getData().hp ?? 0);
// useEffect(() => Polyhedral.sheet.onDataChange(d => setHp(d.hp ?? 0)), []);

Theme

CSS custom properties are set on :root as HSL values. Use them with hsl(var(--name)) for consistent styling. Theme values update live when the user toggles dark/light mode — the SDK pushes new values to the iframe automatically and updates the CSS variables on :root.

--background       --foreground
--raised           --card / --card-foreground
--primary          --primary-foreground
--muted            --muted-foreground
--hover
--destructive      --destructive-foreground
--warning          --warning-foreground
--border           --ring
--radius           --radius-inner

Legacy aliases (still available):
--secondary / --secondary-foreground  → mapped to muted
--accent / --accent-foreground        → mapped to muted
--input                               → mapped to border

If you use inline styles or JavaScript-driven colors, use Polyhedral.theme to read current values and theme.onChange() to react to mode switches:

// Read current theme values
const bg = Polyhedral.theme.background; // HSL string

// React to dark/light mode changes
const unsub = Polyhedral.theme.onChange((theme) => {
  document.body.style.color = `hsl(${theme.foreground})`;
});

// Clean up when done
unsub();

Field Schema

A template's field_schema declares what data fields the sheet expects. It's optional — interactive components work without it — but defining one enables default values when creating instances and better tooling support.

Each field has a key (matching the field prop on components), a type, and optional label and default.

field_schema: [
  { key: "name",      type: "text",    label: "Character Name", default: "" },
  { key: "hp",        type: "number",  label: "Hit Points",     default: 10 },
  { key: "level",     type: "number",  label: "Level",          default: 1 },
  { key: "inspired",  type: "boolean", label: "Inspired",       default: false },
  { key: "portrait",  type: "image",   label: "Portrait" }
]

Supported Types

How It Works

External Packages

Import npm packages via esm.sh. The sandbox CSP allows scripts from esm.sh and cdn.jsdelivr.net.

import confetti from 'https://esm.sh/canvas-confetti@1';

// Fire confetti on a nat 20 (3D animated + posts to chat)
const result = await Polyhedral.dice.rollToChat('1d20', 'Attack Roll');
if (result.total === 20) {
  confetti({ particleCount: 100, spread: 70 });
}

Constraints

Sheets run in a sandboxed iframe with restricted permissions for security:

CLI Commands

The Polyhedral CLI provides all the tools you need to build, validate, and deploy custom sheets.

polyhedral docs

Print the full SDK reference to stdout — types, components, examples, and theme variables.

polyhedral list

List your saved templates.

polyhedral pull [id]

Download a template to a local file.

polyhedral push

Validate, upload, and publish a new version.

polyhedral validate [file]

Validate JSX syntax locally. Reports line and column on error.

polyhedral dev

Watch file, auto-push on save, and open live preview.

polyhedral init [dir]

Scaffold a new template project.

External Builds

External builds let you use any framework (Svelte, Vue, plain HTML) instead of the in-app JSX editor. You provide a complete HTML document and the SDK is auto-injected into the <head> at render time.

The Polyhedral global is available just like in JSX builds, but since you're writing vanilla JS (or compiling to it), you'll use the imperative API instead of React components:

// Read a field value
const name = Polyhedral.getField('name');

// Write a field value (merges into field_data)
Polyhedral.setField('hp', 25);

// Listen for field changes
Polyhedral.onFieldChange((data) => {
  document.getElementById('hp').textContent = data.hp;
});

// Dice rolling works the same way
const result = await Polyhedral.dice.rollToChat('1d20', 'Attack');

// Theme tokens are available as CSS variables
// color: hsl(var(--foreground));

Use the Polyhedral CLI to set up an external build project. Run polyhedral init and select "external" as the build type, then use polyhedral dev for live preview and polyhedral push to deploy.

← Getting Started · Examples · CLI Guide · Webhooks