# Custom Block interfaces

Build and preview a real interface around your Block’s isolated code.

A package can include an optional **`view`** beside `manifest`, `code`/`recipe`,
and `tests`. This is the Block's real interface in Preview: a user edits controls,
presses its action, and sees the code's result. Packages without `view` use
StillMade's generated controls and output viewer. `manifest.ui` remains the
fallback controls schema; it is not the custom interface source.

`view` optionally declares `appearance: "original"` or `"host"` and contains
`html` (required, up to 64 KiB), `css` (optional, up to 32 KiB),
and `javascript` (optional, up to 64 KiB). These are UTF-8 byte limits. For folder
packages, use separate `src/view.html`, `src/view.css`, and `src/view.js` files
(the last two are optional). Optional theme-specific CSS uses
`src/view.themed.css`. The older `src/view.json` object format is also supported,
but never combine it with separate interface files. CLI pack, folder import,
and ZIP import reconstruct the same `view` object and run the same validators.
Use **Choose folder** on the import page to select an edited SDK package directory.
`src/run.js` remains the production function; `src/view.js` controls the interface. A standard view is a reviewed subset of standalone
controls: no framework, URL, npm module or build step runs inside it. For React
or another framework, SVG, canvas or WebGL, drag, timers and loops, use a
[frame view](/docs/build/frame-views) instead.

### Interface bridge

The host provides `StillMade` inside the sandbox:

| API | Behavior |
| --- | --- |
| `StillMade.input` | Copy of the current preview input bindings after initialization |
| `StillMade.onInput(callback)` | Receives initial input and later host input changes; returns an unsubscribe function |
| `StillMade.connectDocument(options)` | Connects a declared JSON content store to shared edits; returns status, flush and recovery methods |
| `await StillMade.previewImage(value)` | Resolves a currently authorized image to a bounded, display-only PNG copy; preserves the original run input |
| `await StillMade.run(values)` | Validates the named input bindings, runs the package in the isolated computation worker, validates outputs, and resolves to the output bindings object |

`admitPackage` and local admission include a **Shared interface contract** source
check. The lightest path for an ordinary form control is one HTML annotation:
`<textarea id="draft" data-stillmade-share="draft"></textarea>`. The host turns
that into the same checked, permission-aware binding as `bindShared`; the Block
does not implement networking, cursors, batching, conflict transport or retries.
A `connectDocument` content-store connection supplies the platform-owned
receive/publication paths for an existing structured store. Otherwise an
`onShared` subscription is required, and detected editable controls must call
`bindShared` or `updateShared`. This is a minimum static prerequisite,
not full collaboration verification. Its `runtimeVerified` remains false even
when it passes. Calls may be unreachable or fail to cover all controls; actual
two-participant acceptance remains required. Runtime safety checks for already
pinned immutable packages are separate and do not rewrite those releases.
The general `scanPackage` keeps compatibility by default; passing
`{collaboration:true}` requests this admission prerequisite. Creation and import
services use `scanAdmissionPackage` for the same mandatory source check,
including hosted Blocks. This is not yet an enforced two-user runtime acceptance
guarantee for every import route.

Project workspaces also expose `StillMade.onShared(callback)` and
`StillMade.getShared()`. The snapshot contains `state` (shared interface JSON),
`outputs` (the latest saved results, including another collaborator's generation),
`memory` (declared Block runtime memory), and `canEdit`. Use
`StillMade.updateShared({field: value})` to queue a shared interface edit. It
checks edit permission and per-field preconditions; the host sync status reports
durable acknowledgement. In saved Step workspaces, the promise resolves only
after an acknowledged shared save, so `run` waits for pending edits to reach
storage. Builder previews remain local. The `{queued:true}` result does not
itself expose a server receipt or establish durability in every host.

The unchanged original photo-editor folder has local behavioral acceptance:
independent owner/editor/viewer browsers share edits and saved image outputs,
reopen them, wait for a delayed save, preserve independent concurrent fields and
reject stale field preconditions. Actual isolated producer/editor/consumer
execution verifies its input/output handoff. Fixture identities and storage
transport are used; authors still need to exercise their own complete journey.
Saved declared outputs can be previewed with the current package pin and exact
host media receipt; a shared URL alone does not grant image access.
The host's existing guarded Step/Home exits also await pending direct
`updateShared` writes, including dependent promise continuations in the current
task. Rejected fields remain unconfirmed until an acknowledged replacement for
those fields; unrelated successful edits do not clear them. This internal
barrier is not exposed to Block code, does not cover every navigation route,
and does not wait for future provider results or arbitrary private callbacks.
The state is limited to 384 KiB of UTF-8 JSON per placement. Shared edit requests,
including before-values, are capped at 800,000 bytes; in-flight and queued requests
together are capped at 2 MiB. The project still has its existing 1 MB operation
envelope and 4 MB live-document limits. These limits are cumulative, not an
unlimited quota per Block. Use named fields and commit text
drafts on blur or a short trailing debounce; do not send an entire document on
every pointer movement. Observe `onShared` to render remote changes and results.
Conflicting edits must be reconciled with the latest snapshot, not silently
overwritten. Builder previews simulate this state locally, without network sharing.

For a structured object, opt into checked nested merging with
`StillMade.updateShared({plan: editedPlan}, {plan: {exists: true, value: basePlan}}, {merge: true})`.
`basePlan` must be the exact snapshot used to construct `editedPlan`, not a newer
snapshot read just before sending. Unchanged nested fields are retained from the
current shared state. Arrays of objects with distinct string `id` fields merge
by identity; independent edits and additions can coexist. Overlapping edits,
deleting an item someone changed, or incompatible reorderings reject the entire
patch. Scalars, non-keyed arrays and normal calls without `merge` stay strict.
This is not character-level text merging. Structured merges still obey the
384 KiB state limit and 2,000-operation limit per structured field. Keep edits
bounded and retain a rejected local draft for explicit resolution. Initializing
an absent field is a strict write; do not assume concurrent initializations merge.

For a structured editor, `StillMade.scheduleSharedEdit('plan', callback)` replaces
the pending callback for that name, running after 150 ms without another call or
at 500 ms maximum wait. Capture the original edit base once, keep the local draft,
and serialize/publish in the callback. Do not recapture a newer remote base for
an old edit. There are at most 32 pending names; closing the interface cancels
them unless the host first completes an explicit flush. Step-header navigation
now requests that flush and waits for the bound controls and scheduled callbacks.
Return the write promise from a scheduled callback so it can be awaited. This is
not a server save acknowledgement or a durable queue. Keep a pending-edit
indicator and block Review until your writes settle; surface errors and retain
the draft. Check permission again before publishing. Do not schedule paid runs
or replay user actions from remote updates.

For a large object document, `StillMade.diffDocument(base, draft)` returns checked
field/identity operations and `StillMade.applyDocument(base, operations)` returns
a new document or throws on a stale precondition. Both are pure local helpers:
they do not save anything, mutate the supplied object, grant permissions, or run
a Block. They use the same operation implementation as project collaboration.
Each object document is limited to 4 MiB of UTF-8 JSON; operations are limited to
384 KiB and 2,000 entries. Reorder operations carry IDs instead of copies of the
entire collection, retaining concurrently added items and supporting replay.
Unchanged executable source, runtime memory and nested interface state do not
appear in a field edit. Actual insertion/deletion/replacement values still count
toward all limits. Paths and collection identities remain validated.

`StillMade.indexById(items)` returns a detached `Map` for a JSON collection with
distinct nonempty string IDs (at most 10,000 items and 4 MiB). Changing the map or
its values cannot mutate the input. Use this bounded helper for identity lookup
instead of repeatedly scanning large collections in instrumented view callbacks.

These helpers let a workspace keep a bounded edit overlay rather than copying a
large source document into `state`. Publishing, overlay merging, original edit
bases, conflict recovery and `onShared` rendering are still the Block's job.
Never rebase an old edit by substituting newer before-values, and never treat a
successful local apply as a server acknowledgement. Call document helpers in a
scheduled edit, not on every pointer event. The helpers alone do not make an
existing local-only interface collaborative.

### Connect an existing content store once

`StillMade.connectDocument` owns shared subscriptions, checked edit overlays,
150 ms quiet / 500 ms maximum batching, in-flight rebasing and conflict status.
If two participants initialize an empty shared field together, a rejected
initializer waits up to five seconds for the actual winning snapshot before
rebasing its original edit intent. It does not invent a base or blindly retry;
conflicts, permission loss, disposal and missing-snapshot timeouts preserve the
local draft and require recovery.
Keep the Block's existing synchronous JSON store and renderer:

```js
const shared = StillMade.connectDocument({
  key: 'boardDraft',
  source: initialBoard,
  collections: [
    {path: ['strip'], key: 'panelId'},
    {path: ['strip', '*', 'elements'], key: 'id'},
  ],
  read: () => draft,
  replace: value => { draft = value; draw(); },
});
// Add this to the existing content-change callback, not every individual control:
const changed = () => shared.changed();
// Optional: subscribe: notify => existingStore.subscribe(notify)
// replaces explicit changed() calls for stores with a change subscription.
StillMade.onInput(input => shared.replaceSource(input.board));
shared.onStatus(status => { save.disabled = !status.canEdit || !!status.error; });
```

`replace` must synchronously replace content and redraw without saving, running
a Block or dispatching other side effects. Local selection, playback, preview
URLs, credentials and device state do not belong in the shared document. Source
refreshes use `replaceSource`; a user reset/undo is a content change and must
notify `changed`. Inputs, raw-text drafts and delayed result handlers that bypass
the store still require adaptation. Private variables are never discovered or
automatically synchronized.

Collection declarations address original document fields. `*` traverses an
explicitly declared parent collection. Declared IDs must be distinct nonempty
strings; missing IDs do not fall back to array indexes. `panelId`, `shotId`, `id`
and other safe declared keys preserve their original saved shapes. Renaming an
identity is a checked removal/insertion. The adapter shares small overlays, not
copies of unchanged documents. Existing 4 MiB document, 384 KiB shared-state and
2,000-edit limits still apply; there are at most eight connected documents in a
view, each using a different shared field.

For legacy data whose accepted contract permits missing, duplicate or otherwise
unusable IDs, opt in explicitly with
`{path:['scenes'],key:'shotId',fallback:'atomic'}`. Use that same declaration for
every participant; never select a different codec based on the first loaded
document. The codec uses a canonical tagged representation: usable identities
still support small item-level edits, while legacy collections remain exact
checked atomic values. It never invents IDs or silently normalizes saved data.
Atomic collection changes can conflict or exceed the shared-state limit on large
documents; they are not a substitute for author-defined stable identities.
Changes between source representations keep the ordinary explicit
source-conflict/reset rules. Strict declarations retain their original encoding.

`getStatus()` and `onStatus(callback)` expose `phase`, `canEdit`, `dirty`,
`pending`, `sourceConflict` and `error`; `onStatus` returns an unsubscribe function. The host also
shows pending changes or the current adapter error. `await shared.flush()` waits
for local transport acknowledgement, **not a durable server receipt**. The
existing guarded workspace exits flush registered document connections before
the shared-write barrier. Before a run that depends on the final draft, await
`shared.flush()` and then read the current content. Incoming shared results do
not replay a run; use `onShared` to display results/progress separately.

Conflicting drafts stay local and visible. `await shared.retry()` retries against
the preserved edit baseline. `await shared.discard()` acknowledges an exact
no-op for this shared field before discarding its local draft, clearing that
field's rejected-write ledger. A racing teammate edit, a changed local draft,
pending writes or readonly/closed access refuse recovery rather than silently
erase content or claim a successful exit. Other failed fields still block exit.
`dispose()` refuses dirty, pending or failed connections; flush/discard first.
This adapter is not character-level text merging and does not establish runtime
coverage for arbitrary imported interfaces. No existing immutable Block release
is rewritten merely by exposing this API.

If a project input changes so an existing shared overlay no longer applies,
`replaceSource` retains the last valid source and remembers the incoming source
without publishing it. Source errors appear through adapter status, not a second
generic `onInput` exception that would outlive recovery. Ordinary retry/discard refuse to silently use that stale
source. Present a deliberate review/confirmation action before calling
`await shared.resetSharedSource({discardSharedEdits:true})`: **this clears the
shared draft for everyone**, not just the caller. The reset uses an exact current
field precondition and adopts the remembered source only after acknowledgement.
If a teammate acknowledges a compatible reset, the controller can adopt its
remembered incoming source without needing another input event. It rebases only
unconfirmed local intent, never reinstates the obsolete shared draft. A local
edit that conflicts with the reset remains visible and requires explicit discard;
ordinary retry cannot republish the stale shared content.
Readonly access, pending writes, racing teammate edits, newer local edits or a
newer incoming source prevent that adoption and preserve the local draft for
fresh review. An acknowledged reset may already have cleared the old shared
overlay when a subsequent local/source change is detected; the adapter reports
that failure and requires a new explicit confirmation. Reset never runs a Block.

For an ordinary static editable control, prefer
`data-stillmade-share="title"` on the element and give it a unique `id`. No
authored JavaScript adapter is needed. For a dynamically created or replaced
control, call `StillMade.bindShared('title','title-input')` once after its element
exists. Mark a genuinely device-only playback/filter control with
`data-stillmade-local`; admission rejects an unexplained static control when the
interface does not use a structured `updateShared` or `connectDocument` adapter.
Both shared forms use the same host-owned binding and share a text, numeric,
checkbox, or select value with a 150 ms trailing debounce, a 500 ms maximum
batch wait during continuous typing, and a blur flush. Unchanged/duplicate native
events do not publish or restart the delay. An edit already in flight must settle
before the next write; network and persistence latency are separate. It
observes remote changes, respects read-only access, and returns an unsubscribe
function. It never dispatches synthetic input/click events or runs a Block in
response to a teammate's edit. Derive non-DOM state and render saved outputs
through `onShared`; private variables are not synchronized by binding a control.
Do not bind credentials, file pickers, transient selection, or generation buttons.
If a bound control is replaced dynamically, unsubscribe before binding its replacement.
Text composition (IME) stays local until it ends; the debounce and maximum-wait
timer do not publish unfinished preedit. Incoming shared values/defaults cannot
overwrite the composing control. Its final edit retains the original field
precondition, so a same-field teammate edit still conflicts instead of being
silently overwritten. Workspace exit and `StillMade.run` reject while composition
is unfinished. If editing access changes during composition, the final draft
stays local even if access returns; duplicate native input events cannot submit
it. A genuinely changed later value may retry against the original checked base.
This does not provide character-level text merging or a general save-before-run
guarantee.
Use `StillMade.setSharedDefaults({title: initialTitle})` when input-derived
defaults change. This updates defaults only for existing bound fields; it does
not publish an edit, override a saved shared value, or replace a dirty/in-flight
draft. Do not assign to a bound control's value during remote rendering. Read
its current value for an explicit run so a preserved draft is not replaced by
an unrelated teammate's update. Keep slider/input elements mounted while a
gesture is active; update labels without rebuilding those controls.
Conflicting drafts stay visible with a validation error; do not automatically
overwrite the teammate's version. Dispose and rebind after the user chooses to
discard a conflicting draft. Builder previews use local-only shared state
(`localOnly: true`); this is not evidence of network collaboration.

`updateShared` queues up to 32 waiting updates and serializes their per-field
preconditions. A conflict rejects dependent queued updates instead of silently
rebasing them. An optional second argument supplies explicit field preconditions
captured when a draft began: `{title: {exists: true, value: 'original'}}`.
An acknowledgement timeout fails closed until the Block is reopened. This
protects against blindly retrying an edit whose acceptance is unknown.

Cursor forwarding is installed by the host in all admitted sandbox views and
does not require package code. Arbitrary private JavaScript variables and DOM
mutations are **not** shared state. When adapting a repository, move persistent
interface values onto this contract; importing a repository alone cannot make
its private state collaborative. The transport supports authenticated pushed
invalidations when configured, with polling fallback; concurrent
edits to the same text field can conflict; this is not character-level CRDT editing.

Always use `onInput` to initialize controls; the interface may load before its
initial values arrive. Catch run errors and display them in the interface. Disable
the run control while a request is pending. One run can be active; the host limits
the view to 30 requests per minute, 4 MiB and 500,000 JSON nodes per message, and
15 seconds for local execution. Host-confirmed ComfyUI, text and speech requests
allow up to 6 minutes; image generation allows up to 15 minutes for confirmation
and its bounded fixture/sample sequence. These are interface wait budgets, not
permission to retry or extend provider deadlines. Raw RGBA messages must fit both
message budgets (roughly 124,000 pixels after reserving nodes for other inputs);
use smaller preview images.
The existing computation limits (including the JavaScript 500 ms guest deadline)
still apply. Reset discards the interface and aborts its pending computation.
Closing Preview disposes the frame and aborts outstanding work.

For a port named `prompt`, initialize its control from the host input callback:

```js
const inputField = document.getElementById('prompt');
StillMade.onInput(input => { inputField.value = input.prompt || ''; });
```

Inside the async run button handler, call
`await StillMade.run({prompt: inputField.value})` and display its output.
Use `inputField` or `promptInput` for local variables; `prompt` is also a browser
API name and cannot be a local identifier. Its data-property exception applies
only to the single, unshadowed and unreassigned parameter of a direct inline
`StillMade.onInput` callback; declare that parameter name only once in the
interface source. Use ordinary `.prompt` access there, rather than
computed/destructured access or a property on an unrelated object. Object literal
`{prompt: value}` keys are supported. Browser dialogs remain unavailable.

Use the DOM for interaction and rendering: `document.getElementById`,
`querySelector`, `addEventListener`, `textContent`, `value`, `checked`, `disabled`,
`hidden`, and canvas are supported. Declare elements in `html`; show/hide existing
elements or draw into a declared canvas. Dynamic element creation, HTML insertion,
reflection, computed dynamic property lookups, module imports, browser navigation,
account APIs, parent/window access, and storage are rejected. Property access such
as `result.text` and `image.data[0]` is supported; use array methods for iteration.
Synchronous loops, function declarations, directly recursive helpers, timers, and
microtask queues are rejected. Every callback/helper also checks a private task
budget (10,000 calls or 50 ms between checks), including indirect recursion. This
guard cannot interrupt a single native browser operation, so it is not a
substitute for the computation worker. Use small native array operations such as `map`/`forEach` for
rendering; heavy work belongs in the isolated Block runtime.
Use `.textContent` for user text. Tags are ordinary controls, text, layout, media,
and canvas; no script, iframe, SVG, link, form, or document metadata tags. Markup
must use complete tags without HTML comments, inline event handlers, or navigation
attributes. JavaScript belongs in `view.javascript`, not the HTML string.

The frame has an opaque origin and `sandbox="allow-scripts"`, without same-origin,
popups, forms, downloads, or top navigation permissions. CSP denies connections,
workers, frames, external scripts, and resources. Image and media sources must be
local `data:` or `blob:` URLs. Styles cannot import resources. No token, account,
project mutation API, filesystem, or parent DOM handle is passed into the frame.
Only copied inputs, explicit run results, and authorized image display copies cross the bridge. Static checks
are an additional admission gate; they do not replace this browser boundary or
the separate computation sandbox. Keep the UI responsive: move computation into
`code` or `recipe`; DOM JavaScript is not the CPU-metered computation runtime.

A file input can read a user-selected image with `FileReader.readAsDataURL`, load
it into an existing `<img>`, and draw it into a declared `<canvas>`. Use
`getImageData` to pass `{width,height,data:Array.from(pixels.data)}` to an image
input, staying within the SDK image and message budgets. Nothing is uploaded by
the view. Use the host's file controls when you do not need a custom picker.

The host supplies StillMade's shared Light, Dark and White theme variables and basic controls.

For optional StillMade appearance, inherit its typography or use
`var(--font-body)` for controls and `var(--font-display)` with weight 200 and
italic style for display headings. Use shared spacing, radius and color tokens
such as `var(--bg)`, `var(--fg)`, `var(--fg-muted)`, `var(--border-tok)`, and
`var(--r-3)`. Original appearance may keep its own typography and colors within
the existing sandbox; importing external fonts or resources remains unavailable.

Use a clear input, primary action, and visible result. Preview runs
synthetic samples or the user's explicitly chosen input; it does not commit to a
production project. SDK fixture tests prove code behavior and input/output
compatibility. They do not claim to test browser interaction: review the actual
custom interface in Preview before **Confirm Import**.

### Appearance compatibility

StillMade has three modes: **Light** (cream), **Dark** (ink), and **White**
(neutral white surfaces with black text). Generated controls inherit the app mode.
A custom interface may keep its original design by explicitly setting
`view.appearance: "original"`. The surrounding StillMade app keeps its own theme.
Omitting this field, or setting it to `"host"`, preserves the existing requirement
to provide theme-compatible colors. Existing package sources and installed pins
are never rewritten to change appearance.

A minimal original-style view is:

```json
{
  "appearance": "original",
  "html": "<output id=\"result\"></output>",
  "css": "body{background:#14213d;color:#fff}",
  "javascript": "StillMade.onShared(shared=>{document.getElementById('result').textContent=shared.outputs.text||'';});"
}
```

Keep this object in `src/view.json` when using the explicit appearance field;
do not combine it with split `src/view.html`/CSS/JavaScript files. Folder, archive
and GitHub SDK-folder imports retain the field, as do source editing and export.

The stylesheets have separate purposes:

- `view.css` (up to 32 KiB) retains the original design. Original-style packages
  use it inside projects as well as in Original design previews.
- `view.themedCss` (optional, up to 32 KiB) is a **complete replacement stylesheet**
  for authors who also want StillMade appearance. Include the same layout and
  responsive rules using host tokens. Preview offers this mode only when the
  complete view passes the host-appearance check.

For example, original CSS can use `body { background:#14213d; color:#fff; }`.
Its optional themed stylesheet uses `body { background:var(--bg); color:var(--fg); }`.
Move fixed inline colors into the stylesheets when offering both appearances.
Never change image/video/canvas pixels to match the interface palette.

For StillMade appearance, use `var(--bg)`, `var(--bg-elev-1)`, `var(--bg-elev-2)`,
`var(--fg)`, `var(--fg-muted)`, `var(--border-tok)`, `var(--signal)`, and
`var(--shadow-1)`. The SDK exports `APPEARANCE_TOKENS`. Inherit host typography
or use `var(--font-body)` and `var(--font-display)`. Do not redefine host tokens
or use raw palette tokens such as `--cream-100` in theme-aware declarations.
Original appearance does not require these color or typography conventions.

**The appearance declaration does not widen the interface runtime.** Existing
HTML/JavaScript syntax restrictions, CSS restrictions on escapes, filters and
color blending, resource-loading bans, size limits, shared-state requirements,
permissions, CSP and typed input/output validation still apply. Use CSS classes
for interface changes; dynamic element-style mutation remains unavailable.
Static original colors are allowed only with the explicit original mode;
any optional themed stylesheet still needs valid semantic colors.

`scanPackage`, `admitPackage`, and CLI validation check the declared appearance
alongside all other admission gates. A styling pass does not prove working UI,
accessibility or full repository compatibility. Review the actual interface on
its declared devices and, when supplied, each themed mode. This option preserves
styling within the existing restricted view format; it does not load framework
bundles, arbitrary React applications, remote fonts or external scripts.

### Display an authorized image

Use **`await StillMade.previewImage(value)`** for an image selected in
StillMade, received from a connected Block, or present in explicitly granted
project context. The host also supports its registered sample pixels, local
uploads, and verified run outputs. A well-formed reference alone does not grant
access: a changed URL, version, role, unrelated asset, or expired preview session
is rejected. Catch and display the error; do not fall back to fetching the URL.

The result is exactly `{url, width, height}`. Its URL is a PNG data URL
that can be assigned to a declared `<img>`. It is a display copy, at most
512 pixels on either edge, with aspect ratio preserved. It does not generate,
upload, save, select, or replace an asset. Continue to pass the **original**
image value to `StillMade.run`; processing uses the original input within
the runtime's own limits. Do not use the display URL as a processing input.

Image display allows one pending request, up to 60 requests per minute, and a
15-second deadline. It is independent of the pending computation request, so an
input can be displayed while its Block runs. Both directions retain the 4 MiB
and 500,000-node message limits. Input changes, source/session reset, or closing
the workspace cancel unfinished display requests. Use a local revision counter
in asynchronous UI code so a late result cannot repaint an earlier selection.

StillMade's host image controls remain available with a custom interface.
A project lets the user select a compatible authorized image or upload one;
connected/context bindings remain read-only. A standalone preview keeps uploaded
files local to that preview. Host uploads accept PNG, JPEG, WebP or GIF up to
12 MB and one megapixel; larger uploads are rejected with a request to resize
a copy. Existing authorized project references can be displayed up to 16
megapixels, but the selected runtime still applies its processing limit. No
automatic processing resize occurs. The host validates permissions and exact media
identity before resolving bytes; no account token, native file handle, or remote
fetch capability enters the iframe.

This `view` example assumes one image input and one image output, both
named `image`. It shows the actual selected input and processed result
while retaining their original typed values:

```json
{
  "html": "<figure><img id=\"source\" alt=\"Selected input\" hidden><figcaption>Selected image</figcaption></figure><button id=\"run\">Run block</button><figure><img id=\"result\" alt=\"Processed result\" hidden></figure><p id=\"status\" role=\"status\"></p>",
  "javascript": "const source=document.getElementById('source'), result=document.getElementById('result'), button=document.getElementById('run'), status=document.getElementById('status');\nlet current={}, revision=0;\nStillMade.onInput(input=>{\n  current=input;\n  const started=++revision;\n  source.hidden=true;\n  result.hidden=true;\n  if(!input.image)return;\n  StillMade.previewImage(input.image).then(preview=>{\n    if(started!==revision)return;\n    source.src=preview.url;\n    source.width=preview.width;\n    source.height=preview.height;\n    source.hidden=false;\n    status.textContent='';\n  }).catch(error=>{if(started===revision)status.textContent=error.message});\n});\nbutton.addEventListener('click',async()=>{\n  const started=revision;\n  button.disabled=true;\n  try{\n    const output=await StillMade.run(current);\n    if(started!==revision)return;\n    const preview=await StillMade.previewImage(output.image);\n    if(started!==revision)return;\n    result.src=preview.url;\n    result.width=preview.width;\n    result.height=preview.height;\n    result.hidden=false;\n    status.textContent='Finished';\n  }catch(error){if(started===revision)status.textContent=error.message}\n  finally{button.disabled=false}\n});"
}
```

### Complete custom interface example

Download [custom-interface.stillmade.json](/block-sdk/examples/custom-interface.stillmade.json)
for the complete tested text-cleanup package. Its `view` is:

```json
{
  "html": "<h1>Clean text</h1><label for=\"source\">Your text</label><textarea id=\"source\"></textarea><p><button id=\"run\">Clean text</button></p><h2>Result</h2><output id=\"result\" aria-live=\"polite\">Your result appears here.</output>",
  "css": "output { display:block; white-space:pre-wrap; padding:16px; border:1px solid var(--border-tok); border-radius:var(--r-3); }",
  "javascript": "const source=document.getElementById('source'), button=document.getElementById('run'), result=document.getElementById('result'); StillMade.onInput(input=>{source.value=input.text||''}); button.addEventListener('click',async()=>{button.disabled=true;try{const output=await StillMade.run({text:source.value});result.textContent=output.text}catch(error){result.textContent=error.message}finally{button.disabled=false}});"
}
```

### Dynamic lists and controls

Use `StillMade.render(containerId, nodes)` to replace the children of an existing
container with structured controls. Use `StillMade.onAction(callback)` for their
click, input, and change events; it returns an unsubscribe function. This supports
variable-length lists without raw HTML injection or direct DOM creation.

For example, with `<div id="choices"></div><output id="selection"></output>`:

```js
StillMade.onInput(input => {
  StillMade.render('choices', (input.shots || []).map(shot => ({
    tag: 'button', text: shot.name || shot.id, value: shot.id, action: 'select'
  })));
});
StillMade.onAction(event => {
  document.getElementById('selection').textContent = event.value;
});
```

Each node has `tag`, optional `text` or `children`, and optional `id`, `className`,
`label` (accessible name), `action`, `value`, `type`, `checked`, `disabled`,
`placeholder`, `min`, `max`, or `step`. Unknown fields are rejected. Supported tags
are div, section, article, header, footer, p, span, strong, em, small, h1–h4,
label, button, input, textarea, select, option, ul, ol, li, table, thead, tbody,
tr, th, td, details, summary, output, and progress. Input types are text, number,
range, checkbox, radio, and color. Use a div, section, article, main, aside, ul,
ol, or tbody as the existing root. Style nodes with classes in your view CSS.

An action belongs to a button, input, textarea, or select. Its callback receives
`{action, value, checked, event}`. Buttons emit click; checkbox, radio, and select
emit change; other fields emit input. Stable IDs preserve matching controls,
focus and selection during reconciliation. Keep edited values in your declared
content store, not only in the DOM. Text inputs emit one committed action at the
end of IME composition, not intermediate preedit or duplicate final input events.
Sibling redraws preserve active composition. If the same control's declared value
changes remotely during composition, the renderer retains local text and blocks
dispatch/exit rather than applying it against a silently advanced model base.
Copy the retained text before reopening the Block to review the shared version;
there is no automatic merge or retry. Disabling a composing control also retains
its draft and blocks run/exit after access returns. Removed or rebound controls
cannot commit their old composition to a different action; removed-control draft
recovery is not provided. Call `StillMade.run` explicitly
to execute production code. Rendering
does not mutate project context or bypass execution permissions.

Limits: 1,000 nodes per render, 12 child levels, 256 KiB of serialized descriptors,
5,000 total frame elements, 16 action listeners, 10,000 characters per text/value,
120 per ID/action and 240 per class/name/placeholder. IDs must be unique. Invalid
renders leave the previous controls intact. Text is always literal; scripts,
inline handlers, links, file inputs, network attributes, and raw HTML are rejected.


### Audio and video playback

`await StillMade.previewMedia(reference)` opens an authorized `audio` or `video`
reference as a temporary playback copy and returns `{kind, url, mimeType, bytes}`.
Assign its URL to an existing `<audio controls>` or `<video controls>` element.
The host requires an exact reference from this Block’s bound input, an explicitly
selected project asset, permitted project context, or a host-recorded output.
A URL alone is insufficient. Preview has host file pickers for audio/video inputs;
project Steps can select existing project assets or receive connected media.

For `<audio id="player" controls></audio><p id="status"></p>`:

```js
const player = document.getElementById('player');
const status = document.getElementById('status');
let revision = 0, activeUrl = '';
StillMade.onInput(input => {
  const started = ++revision;
  player.pause(); player.src = '';
  if (activeUrl) StillMade.releaseMedia(activeUrl);
  activeUrl = '';
  if (!input.audio) { status.textContent = 'Choose audio above.'; return; }
  status.textContent = 'Opening audio…';
  StillMade.previewMedia(input.audio).then(preview => {
    if (started !== revision) { StillMade.releaseMedia(preview.url); return; }
    activeUrl = preview.url;
    player.src = preview.url;
    status.textContent = 'Ready to play.';
  }).catch(error => {
    if (started === revision) status.textContent = error.message;
  });
});
```

`StillMade.releaseMedia(url)` revokes a playback copy. Release old copies before
loading another: at most two can remain active. Input changes and closing/resetting
the interface revoke all copies and cancel pending reads automatically. Requests
share the image-preview queue and its limit of 60 requests per minute; only one
image/audio/video read can run at a time. Keep the original reference for production
runs; temporary URLs must never be saved as project assets or returned as outputs.
`await StillMade.useOutput(outputs)` selects complete saved media references as the Block outputs without dispatching a generation run. This is available in a saved project workspace only. Every output must match the manifest, and every media identity must come from the current authorized Block input or the current trusted desktop recording session. The host rejects changed URLs, unknown assets, and non-media outputs.


Files must be at most 24 MB and load within 15 seconds. Supported containers include
MP4, WebM, Ogg, QuickTime, MP3, AAC, WAV, and FLAC; actual playback depends on browser
codec support. Remote assets need CORS support and an appropriate media content type.
The host omits credentials and rejects redirects. Network access remains disabled
inside the iframe; larger media should use the existing native Editor workspace.
