Host
For the application embedding a canvas, not for the environments in it: the document and scene hosts, and the adapters they are given for storage, code execution and plugins.
@internote/canvas-sdk@internote/canvas-sdk/host#Components
Builds the host with createHeadlessCanvasHost — rebuilt when the document, files or assets change, since each manifest arrives after the first render — and disposes it on unmount. It renders a CanvasSceneView for sceneIndex, clamped to the last scene, and nothing when the document has no scenes. The reading view mounts CanvasSceneView directly, with its own host.
Also takes className. Any other prop is passed to the rendered element.
assetsnull while the manifest is still arriving.Record<string, string> | nulldbsession+db environments; a no-op store when absent.KeyValueStoragedocument{ body?: unknown; data?: Record<string, ChalkDataset> }environmentsEnvironmentDefinition[]filesnull while still loading; the host is rebuilt when it arrives.Record<string, string> | nullinternoteIdstringonStepChange(step: number, sceneIndex: number) => voidrenderBlankElementrenderBlankElement from @internote/canvas-library.BlankElementRendererrenderToolbar(toolbar: HostToolbarState) => ReactNoderesolveAssetUrlnull when it cannot.(fileName: string) => string | nullsceneIndexnumber0The canvas holds the scene's detail in the order the author wrote it — an environment's surface where they put one, a run of blank content (drawn by renderBlankElement) where they put that — with a hairline between every adjacent pair. A two-environment split is arranged by the scene's direction/ratio; a direction: auto split (the schema default) stacks here and goes side by side while presenting, where the canvas has the whole screen.
There is one toolbar per scene and one footer: the bar carries the system tier plus EVERY environment's declarative items, and the footer carries every environment's two bands under one chevron. Embedders draw the bar through renderToolbar with their own design system (the reading view draws the app's buttons); the built-in fallback is deliberately minimal.
Presentation is a drive mode: while presenting, the arrow keys and space move through the steps and Escape leaves. The embedder stages it (a fullscreen overlay, or this element's own fullscreen via manageFullscreen); the view only drives the steps. Mounting resets the scene's clock.
Also takes className. Any other prop is passed to the rendered element.
filesRecord<string, string> | nullhostHeadlessCanvasHostmanageFullscreenbooleantrueonPresentingChange(presenting: boolean) => voidonStepChange(step: number) => voidpresentingbooleanrenderBlankElementBlankElementRendererrenderToolbar(toolbar: HostToolbarState) => ReactNoderesolveAssetUrl(fileName: string) => string | nullscenehost.scenes.HeadlessScenestepnumber | null#Hooks
(steps: StepController, options?: StepRevisionOptions): {
revision: number;
step: number;
pulse: () => void;
}Returns a revision that bumps on every step transition and keeps bumping on animation frames for tailMs after each transition (and after mount), so over: keyframe transitions and compound time offsets (at: 3 + 1.5s) resolve smoothly on screen. The tail is a bound, not a schedule: choreography that runs longer calls pulse to restart it.
Returns revision, the current step, and pulse to restart the tail.
#Functions
(options: HeadlessHostOptions): HeadlessCanvasHostRegisters options.environments, runs the canvas semantic pass, folds attached CSVs into the datasets, then builds a StepController per scene and an EnvironmentAnimationRuntime per environment, applying continue: handoffs before each environment's initial state. The React CanvasDocumentHost renders on top of this same host; tests assert against it directly.
The document must already be compiled and refined (the wasm compiler and refineInternoteDocument run upstream; in tests, feed a captured compile or a hand-refined fixture).
(
manifest: CanvasPluginManifest,
options?: CanvasLoadOptions,
): Promise<EnvironmentDefinition>Only runtime: 'module' loads; 'worker' is reserved for a sandboxed runtime and throws.
Returns The plugin's environment definition.
direction and ratio.(scene: CompiledNode): CanvasLayoutA scene detail holds at most two canvas.<name> nodes (the semantic pass enforces the cap; this function does not); their order is primacy. Everything else is blank-canvas content, kept in source order. direction is returned as written — auto included, since only the render host knows whether it is presenting — so pass it through resolveSplitDirection at render. A missing or non-positive ratio reads as 1.
horizontalverticalauto stacks (vertical) while the canvas shares the page with the prose — the canvas column is far narrower than it is tall, and two side-by-side halves of it are unreadable — and goes side by side (horizontal) in presentation, where the canvas has the whole screen and the height is what is scarce. An explicit horizontal/vertical is honoured in both.
#Interfaces
runnable:, injected by the host.The SDK defines only this seam: a transport-agnostic service the host may provide (HeadlessHostOptions.runner, exposed as ctx.runner), as the database tier is provided through db. The SDK itself never executes anything — without a runner, runnable environments render with running unavailable.
The shape is an event stream rather than a result: runs are long relative to a frame, output interleaves stdout and stderr, and the reader can stop one mid-flight.
availabilityCodeRunAvailabilitynotifyUnavailablevoidrunPromise<void>CodeRunner to run.codestringfileNametitle:, or the language's default) — the entry file runs under it, so tracebacks read exercise.py, not a scratch name. Hosts sanitize; absent falls back host-side.stringidentityEnvironmentIdentityinputs@data datasets, files). Always undefined in v1.Record<string, unknown>languagestringsignalAbortSignaldatasets@data datasets, by id — what a data: #id binding resolves against.Record<string, ChalkDataset>files{ src } pointer and does not inline its rows (§2.3). null while the host is still loading them, which a consumer must distinguish from {} — a wait, not an absence.Record<string, string> | nullresolveAssetUrl(fileName: string) => string | nullrevisionnumberruntimeEnvironmentAnimationRuntimestepnumberblankCompiledNode[]directionenvironments.length === 2. Pass through resolveSplitDirection for the concrete axis: auto is not an axis.autohorizontalverticalenvironmentsCompiledNode[]itemsCanvasLayoutItem[]The two lists below are the same nodes sorted into their kinds, which is what the headless host and the semantic pass want: one animation runtime over all the blank content, one mounted environment per canvas node. Only this list remembers that the author wrote a table above the graph rather than below it, which is the order they meant.
ratioratio, default 1).numberkindenvironmentblanknodeCompiledNodeloadEnvironmentFromManifest imports and registers a plugin.importModuleimport() of manifest.src, for tests and hosts.(src: string) => Promise<{ default: EnvironmentDefinition }>registerEnvironmentRegistry.booleantruedatasets@data datasets, by id, for consumers that bind one.Record<string, ChalkDataset>diagnosticsSemanticPassResultdisposevoidenvironmentById#id, else its instance id.HeadlessEnvironment | undefinedfocusEnvironmentvoidsceneAtindex.HeadlessScenescenesscene in the document body, in order.HeadlessScene[]sessionSessionStoresetPresentingisPresenting on every environment's context.voidctxstep, focus and presenting current.EnvironmentContextenvironmentNamegraph, not canvas.graph).stringidentity#id when given).EnvironmentIdentitynodecanvas.<name> node.CompiledNoderuntimectx.animation.EnvironmentAnimationRuntime<EnvironmentContext>toolbarItemsToolbarItem[]createHeadlessCanvasHost mounts and the services it hands each environment.assetsnull while the manifest is still arriving. Handed to each environment's context untouched — resolution is the reading type's job.Record<string, string> | nullclock() => numberdbsession+db environments (defaults to a no-op).KeyValueStoragedocumentdata is the @data datasets key (docs/design/chalk-data-and-references.md §3): inline datasets arrive with their columns and rows, attached ones with only a src filename — their CSV is deliberately not inlined into compiled output (§2.3), so a consumer that wants the rows fetches them.{ body?: unknown; data?: Record<string, ChalkDataset> }environmentsEnvironmentDefinition[]files.geojson are both found. Attached CSVs are folded into datasets here so no consumer downstream has to know the difference; geometry is handed on as text for the type that reads it.Record<string, string> | nullinternoteIdstring'headless'interpolationsInterpolationRegistryrunnerrunnable: environments (absent = running unavailable).CodeRunnersourcestringblankRuntimeEnvironmentAnimationRuntimeenvironmentsHeadlessEnvironment[]layoutCanvasLayoutnodescene node.CompiledNodesceneIdscene-<index> when it has none.stringstepAnimationMsnumber[]stepCount=== step markers the scene body holds.numberstepRevealOnlycue reveals — content fading in or out on its anchor — indexed like stepAnimationMs.boolean[]Everything else is false: a cue.draw's strokes, an attention cue, a cue.type, a cue.parameter sweep, a !cue.to keyframe chain, an environment that moves on the scene clock of its own accord. The distinction is what a transport is FOR — a reveal has nothing to scrub through, so a short one does not earn a control (see CanvasDocumentHost).
stepsStepControllertimeAnimationMsnumberSteps partition a scene's animation, and the control is scoped to the step the reader is on: it plays, pauses and replays that step's choreography, not the whole canvas. A scene with no steps has one partition, so its control is the whole scene's transport — which is what the control used to be everywhere.
Null covers two cases, and the second is the reason this is a state rather than a duration: a step that animates nothing, and one whose whole choreography is a brief plain-cue reveal. Neither has a middle for the reader to stop in.
finishedbooleanpausedbooleanprogressnumberreplay() => voidtoggle() => voidadvance() => voidback() => voiditemsToolbarItem[]playbacknull when the current step has nothing to play.HostPlaybackState | nullpresentingbooleanstepnumberstepCountnumbertogglePresent() => voiduseStepRevision.tailMsnumber3000#Types
{
ok: true;
} | {
ok: false;
reason: string;
kind: "auth" | "unavailable";
}(node: CompiledNode, ctx: BlankRenderContext) => ReactNodeThe host handles cue wrappers itself, fading their content with the step timeline, and calls the renderer for each element inside them.
direction as the author wrote it.autohorizontalvertical