Getting started

This guide builds a plugin that lines the selected layers up in a row or a column with even gaps, then shows how to install, run and iterate on it.

1. Create the files

Make a folder with two files:

tidy-up/
├── manifest.json
└── main.js

manifest.json

{
  "id": "yourname.tidy-up",
  "name": "Tidy Up",
  "version": "1.0.0",
  "description": "Lines the selected layers up in a row or column, 16px apart.",
  "author": "Your Name",
  "main": "main.js",
  "permissions": ["read", "write"],
  "commands": [
    { "id": "row", "label": "In a row" },
    { "id": "column", "label": "In a column" }
  ]
}

main.js

/// <reference types="@popcraft/plugins" />
 
const GAP = 16
 
;(async () => {
  const selection = await popcraft.getSelection()
  if (selection.length < 2) {
    await popcraft.closePlugin('Select two or more layers first')
    return
  }
 
  const row = popcraft.command === 'row'
  // Keep the order they already have on the canvas.
  const sorted = [...selection].sort((a, b) => (row ? a.x - b.x : a.y - b.y))
  let at = row ? sorted[0].x : sorted[0].y
  const line = row ? Math.min(...sorted.map(n => n.y)) : Math.min(...sorted.map(n => n.x))
 
  for (const node of sorted) {
    await popcraft.updateNode(node.id, row ? { x: at, y: line } : { x: line, y: at })
    at += (row ? node.width : node.height) + GAP
  }
 
  await popcraft.closePlugin(`Tidied ${sorted.length} layers`)
})()

Things to notice:

  • Every API call is asynchronous — await them.
  • popcraft.command tells you which command the user picked.
  • Call popcraft.closePlugin() when you're done. The optional message is shown as a notification.

For editor autocomplete, install the SDK — npm i -D @popcraft/plugins — and keep that /// <reference> line at the top of main.js. VS Code and other TypeScript-aware editors pick it up with no other setup, and the types come from the same source as the runtime your plugin actually calls, so they are never out of date.

Working examples to copy from: getpopcraft/plugins.

2. Package it

PopCraft installs either format:

  • A .zip of the folder. manifest.json may be at the top level of the zip or inside a single top-level folder. Files referenced by main and ui are resolved relative to the manifest.

  • A single .json bundle, handy when you generate the plugin from a build step:

    { "manifest": { "id": "…", "name": "…", "version": "…", "main": "main.js", "permissions": [] },
      "main": "/* the contents of main.js */",
      "ui": "<!-- the contents of ui.html, if the manifest has one -->" }

On macOS or Linux:

cd tidy-up && zip -r ../tidy-up.zip . -x '.*'

Or build a JSON bundle with Node:

// pack.mjs — usage: node pack.mjs tidy-up
import { readFileSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
 
const dir = process.argv[2]
const manifest = JSON.parse(readFileSync(join(dir, 'manifest.json'), 'utf8'))
const bundle = { manifest, main: readFileSync(join(dir, manifest.main), 'utf8') }
if (manifest.ui) bundle.ui = readFileSync(join(dir, manifest.ui), 'utf8')
writeFileSync(`${manifest.id}-${manifest.version}.json`, JSON.stringify(bundle))

3. Install and run

Install a plugin from a file, then run it · 0:11
Description

A brainstorm board with its sticky notes selected. From ⋯, Manage plugins… opens the plugin manager, and Install from file… installs Arrange in Grid from its package: it is listed with its commands and the permissions it asks for. Its Grid command is clicked and the permissions are allowed; its panel asks for the gap and the columns, 4 is typed, and Arrange lays the stickies out in a tidy grid.

  1. Open any file in PopCraft.
  2. Open the main menu (top-left) → Manage plugins… → Installed → Install from file… and pick your .zip or .json.
  3. Run it from the main menu (Tidy Up: In a row) or with the command buttons in the plugin manager.
  4. The first run asks the user to approve the plugin's permissions.

4. Iterate

  • Reinstalling a plugin with the same id replaces it. If the new version asks for the same (or fewer) permissions, the earlier approval is kept; otherwise the user is asked again.
  • Each run starts the plugin fresh. Running a plugin that's already running restarts it.
  • Errors thrown in your script (including unhandled promise rejections) appear as error notifications.
  • The plugin is sandboxed, but console.log works — open your browser's developer tools and look for the plugin's frame in the console context picker.

Next steps