StillMade AIDeveloper docs
Browse documentation · SDK 0.1.0

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 instead.

Interface bridge

The host provides StillMade inside the sandbox:

APIBehavior
StillMade.inputCopy 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:

use it inside projects as well as in Original design previews.

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 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.