Plugin API reference

Your main script gets a global popcraft object. Every method returns a Promise; a call that fails (bad arguments, missing permission, unknown node) rejects with an Error whose message explains why.

try {
  await popcraft.updateNode(id, { name: 'Hero' })
} catch (err) {
  await popcraft.notify(err.message, { error: true })
}

Context

popcraft.command

string | null — the id of the command the user chose (from the manifest's commands, or "run" when there are none). It's "widget" when PopCraft started the plugin to handle a widget click.

Reading the file

All of these need the read permission.

getSelection(): Promise<Node[]>

The selected layers, in selection order, as full node objects.

getNode(id: string): Promise<Node | null>

One node by id, or null if it doesn't exist.

findNodes(query?: { type?: string; name?: string }): Promise<Node[]>

Every layer on the current page (at any depth) matching the query. type must match exactly ('TEXT'); name matches any layer whose name contains the string (case-sensitive). With no query, returns every layer on the page.

const buttons = await popcraft.findNodes({ type: 'FRAME', name: 'Button' })

getCurrentPage(): Promise<{ id: string; name: string; childIds: string[] } | null>

The page being viewed and the ids of its top-level layers.

A plugin using the API: Pop Palette · 0:06
Description

A launch post with its headline selected and Pop Palette installed. Run calls the plugin’s main script, which writes to the file through the popcraft API: a pop-art palette appears as colour styles under Local colors, in one undo step.

Editing the file

All of these need the write permission.

createNode(type: string, props?: Partial<Node>): Promise<string>

Creates a layer on the current page (or under props.parentId), selects it, and returns its id. type is one of RECTANGLE, ELLIPSE, POLYGON, STAR, LINE, ARROW, FRAME, SECTION, TEXT, STICKY, VECTOR, IMAGE (case-insensitive). For IMAGE, pass imageUrl as a data:image/… URL (interned as media:// by the host). Optional parentId nests the layer under another node (same as createImage).

createPage(name?: string): Promise<string> · write

Adds a page, switches to it, and returns its id.

setCurrentPage(pageId: string): Promise<void> · read

Switches the editor to another page.

createImage(opts): Promise<string> · write

Creates an IMAGE layer from dataUrl or bytes (+ optional mimeType, geometry, name, parentId).

createDocument(opts?: { name?: string }): Promise<{ key?: string; docId: string; name: string }> · write

Creates a new cloud/local file through the same gate as New File, then navigates to it (closes the plugin session).

getImportMode(): Promise<'new' | 'into-current' | null>

getImportFile(): Promise<{ name: string; dataUrl: string; byteLength: number } | null>

When the host launched the plugin as a document importer, these return the pending mode and file bytes.

const id = await popcraft.createNode('RECTANGLE', {
  name: 'Card', x: 100, y: 100, width: 240, height: 160, cornerRadius: 12,
  fills: [{ type: 'SOLID', visible: true, opacity: 1, color: { r: 1, g: 1, b: 1, a: 1 } }],
})
 
const label = await popcraft.createNode('TEXT', {
  x: 120, y: 120, characters: 'Hello', fontSize: 24, fontFamily: 'Inter', fontWeight: 600,
})

updateNode(id: string, props: Partial<Node>): Promise<void>

Merges props into the node. id, type, parentId, childIds and mainComponentId are ignored — structure is changed with the dedicated methods.

deleteNode(id: string): Promise<void>

Deletes a layer (and its children).

setText(id: string, characters: string): Promise<void>

Replaces the text of a TEXT or STICKY layer.

setTextPath(opts): Promise<unknown> · write

Puts TEXT layers on a path, or tunes one they are on. opts.ids defaults to the selection; mode is 'PATH' (follow pathId, another layer's outline, live), 'ARC', 'CIRCLE' or 'WAVE' (the text's own geometry) or 'NONE' (detach). The other options match the inspector: bend, radius, amplitude, frequency, offset and endOffset (the brackets, as fractions of the path), flip, align ('LEFT' | 'CENTER' | 'RIGHT' | 'JUSTIFY'), placement ('BASELINE' | 'ASCENDER' | 'DESCENDER' | 'CENTER'), baselineShift, spacing, effect ('RAINBOW' | 'SKEW' | 'RIBBON' | 'STAIR' | 'GRAVITY') and overflow ('CLIP' | 'VISIBLE' | 'SHRINK'). Only the options you pass change.

await popcraft.setTextPath({ ids: [labelId], mode: 'PATH', pathId: circleId, align: 'CENTER', offset: 0.5 })

flowTextIntoShape(opts: { id?: string; shapeId?: string; inset?: number; fit?: 'NONE' | 'SHRINK' | 'FIT' }): Promise<unknown> · write

Area type: flows a TEXT layer inside a closed shape, inset px from its outline. Linked, so editing the shape reflows the text.

detachText(opts?: { ids?: string[] }): Promise<unknown> · write

Takes TEXT layers off their path or out of their shape. Pass a path or shape id to free every text linked to it.

setTextFit(opts: { ids?: string[]; fit: 'NONE' | 'SHRINK' | 'FIT' }): Promise<unknown> · write

Auto-sizes the type of TEXT layers to their box, shape or path: FIT fills it, SHRINK only shrinks text that overflows. The stored font sizes stay as set.

setSelection(ids: string[]): Promise<void>

Selects the given layers. Ids that don't exist are skipped.

Styles and variables

getStyles(): Promise<Style[]> · read

The file's local styles (paint, text, effect and grid).

createPaintStyle(name: string, paints: Paint[]): Promise<string> · write

Creates a paint (color) style and returns its id.

await popcraft.createPaintStyle('Brand/Primary', [
  { type: 'SOLID', visible: true, opacity: 1, color: { r: 0.05, g: 0.55, b: 0.91, a: 1 } },
])

getVariables(): Promise<{ collections: VariableCollection[]; variables: Variable[] }> · read

The file's variable collections (with their modes) and variables (with valuesByMode).

setVariableValue(variableId: string, modeId: string, value: unknown): Promise<void> · write

Sets a variable's value for one mode. Colors are { r, g, b, a } (0–1), numbers are numbers, strings are strings, booleans are booleans.

Export

exportNode(id: string, options?: { format?: 'PNG' | 'JPG' | 'WEBP' | 'SVG' | 'PDF'; scale?: number }): Promise<Uint8Array | string> · export

Renders a layer exactly as PopCraft draws it. Raster formats resolve to the file bytes (Uint8Array); SVG resolves to the SVG markup (string); PDF resolves to the PDF bytes (Uint8Array). format defaults to PNG; scale defaults to 1 and is clamped to 0.1–8. PDF ignores scale and is always 300 dpi, with the print page's bleed and no crop marks.

const [node] = await popcraft.getSelection()
const png = await popcraft.exportNode(node.id, { format: 'PNG', scale: 2 })
popcraft.ui.postMessage({ type: 'image', bytes: png })

Notifications, UI and lifecycle

No permission needed.

notify(message: string, options?: { error?: boolean }): Promise<void>

Shows a notification in the editor. Error notifications stay on screen longer.

showUI(options?: { width?: number; height?: number; title?: string; visible?: boolean }): Promise<void>

Opens the plugin's panel with the manifest's ui HTML. Width is clamped to 160–900 px (default 320), height to 120–900 px (default 420); title defaults to the plugin name. Calling it again updates the panel. See Plugin UI.

hideUI(): Promise<void>

Closes the panel. The plugin keeps running.

ui.postMessage(message: any): void · ui.onmessage · ui.on(handler)

Send a message to the panel, and receive messages from it. Messages must be structured-cloneable (plain objects, arrays, strings, numbers, Uint8Array, …). Messages the panel sends before you set a handler are queued and delivered when you do.

closePlugin(message?: string): Promise<void>

Stops the plugin and closes its panel. The optional message is shown as a notification.

A plugin also stops when the user closes its panel, disables or uninstalls it, or runs it again.

Storage

No permission needed. Each plugin gets private key–value storage on the user's device. It persists across runs and files, works offline, and is deleted when the plugin is uninstalled. Values must be JSON-serializable.

storage.get(key: string): Promise<any>

Resolves to the stored value, or undefined.

storage.set(key: string, value: any): Promise<void>

const count = ((await popcraft.storage.get('runs')) ?? 0) + 1
await popcraft.storage.set('runs', count)

Events

Events are only delivered to plugins with the read permission.

EventPayloadFires when
selectionchange{ ids: string[] }The selection changes
pagechange{ id: string }The user switches page
documentchange{}The file changes (edits by anyone, batched to at most one event per 150 ms)
const onSelection = ({ ids }) => popcraft.ui.postMessage({ type: 'count', count: ids.length })
popcraft.on('selectionchange', onSelection)
// later
popcraft.off('selectionchange', onSelection)

A plugin that listens for events keeps running until it calls closePlugin() or the user closes it.

Network

Without the network permission, all network access from the main script and the panel is blocked by a content security policy. With it, use fetch as normal:

const res = await fetch('https://api.example.com/palette')
const palette = await res.json()

Requests come from an opaque origin, so the server must allow cross-origin requests (Access-Control-Allow-Origin: *). Plugins can't send PopCraft cookies or credentials.

The sandbox cannot open a browser tab. openExternal(url) asks the host to open an https URL, or http on localhost. Anything else is rejected. It needs the network permission.

await popcraft.openExternal('https://example.com/checkout')

Nodes

Nodes are plain JSON objects. Read a node with getNode to see every field it has; the common ones are:

FieldTypeNotes
idstringStable, unique in the file
typestringRECTANGLE, ELLIPSE, TEXT, FRAME, GROUP, VECTOR, STICKY, INSTANCE, WIDGET, …
namestringLayer name
x, ynumberPosition relative to the parent's top-left
width, heightnumberSize
rotationnumberDegrees
opacitynumber0–1
visible, lockedboolean
parentIdstring | null
childIdsstring[]Containers only
fillsPaint[]Fill layers, bottom to top
strokesPaint[]Stroke paints; a stroke's thickness is its width
cornerRadiusnumberRectangles and frames
characters, fontSize, fontFamily, fontWeight, textAlignText layers
textstringSticky notes

Colors are { r, g, b, a } with every channel from 0 to 1.

Paints are objects with a type. A solid paint:

{ type: 'SOLID', visible: true, opacity: 1, color: { r: 1, g: 0.4, b: 0, a: 1 } }

Gradient and image paints use the same shape PopCraft stores — read an existing layer's fills and modify them rather than building them from scratch.