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
- Reading the file ·
read - Editing the file ·
write - Styles and variables
- Export ·
export - Notifications, UI and lifecycle
- Storage
- Events ·
read - Network ·
network - Nodes
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.
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.
| Event | Payload | Fires 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:
| Field | Type | Notes |
|---|---|---|
id | string | Stable, unique in the file |
type | string | RECTANGLE, ELLIPSE, TEXT, FRAME, GROUP, VECTOR, STICKY, INSTANCE, WIDGET, … |
name | string | Layer name |
x, y | number | Position relative to the parent's top-left |
width, height | number | Size |
rotation | number | Degrees |
opacity | number | 0–1 |
visible, locked | boolean | |
parentId | string | null | |
childIds | string[] | Containers only |
fills | Paint[] | Fill layers, bottom to top |
strokes | Paint[] | Stroke paints; a stroke's thickness is its width |
cornerRadius | number | Rectangles and frames |
characters, fontSize, fontFamily, fontWeight, textAlign | Text layers | |
text | string | Sticky 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.