Plugin UI

A plugin can show a floating panel with your own HTML. Users can drag it by its title bar and resize it from the corner; closing it stops the plugin.

Setup

Add "ui": "ui.html" to the manifest, then open the panel from main.js:

await popcraft.showUI({ width: 280, height: 200, title: 'Rename layers' })

ui.html can be a full document or a fragment. Put styles and scripts inline — the package has no web server, so relative <script src> / <link href> files aren't loaded. Images and fonts must be inline data: URLs unless the plugin has the network permission.

Messaging

The panel and the main script can't call each other directly; they exchange messages.

In ui.html a popcraft object is available:

API
popcraft.postMessage(message)Send a message to the main script
popcraft.onmessage = (message) => {}Receive messages from the main script
popcraft.close()Close the panel (the plugin keeps running)

Messages also arrive as window message events with the payload in event.data.pluginMessage, so code written for that convention works too.

In main.js: popcraft.ui.postMessage(message) and popcraft.ui.onmessage = (message) => {}.

The panel's API calls are limited to messaging: to read or change the file, send a message to the main script and let it call the API.

Example: rename layers

A plugin with a panel: Rename Layers · 0:08
Description

A brainstorm board with its eight sticky notes selected and Rename Layers installed. Run opens the plugin’s own panel beside the canvas, a sandboxed page that reads the selection. The pattern is set to “Idea #” and Rename renames all eight layers, which the layers panel shows.

main.js

(async () => {
  await popcraft.showUI({ width: 280, height: 190, title: 'Rename layers' })
  const lastPattern = (await popcraft.storage.get('pattern')) || 'Layer #'
 
  const sendSelection = async () => {
    const selection = await popcraft.getSelection()
    popcraft.ui.postMessage({ type: 'selection', count: selection.length, pattern: lastPattern })
  }
  popcraft.on('selectionchange', sendSelection)
 
  popcraft.ui.onmessage = async (msg) => {
    if (msg.type === 'ready') return sendSelection()
    if (msg.type === 'cancel') return popcraft.closePlugin()
    if (msg.type === 'rename') {
      const selection = await popcraft.getSelection()
      let i = Number(msg.start) || 1
      for (const node of selection) {
        await popcraft.updateNode(node.id, { name: String(msg.pattern).replace(/#/g, String(i++)) })
      }
      await popcraft.storage.set('pattern', msg.pattern)
      await popcraft.notify(`Renamed ${selection.length} layer(s)`)
    }
  }
})()

ui.html

<!doctype html>
<html>
<head>
  <style>
    body { font: 12px system-ui, sans-serif; margin: 12px; color: #222; }
    label { display: block; margin-bottom: 8px; }
    input { width: 100%; box-sizing: border-box; padding: 6px; border: 1px solid #ccc; border-radius: 6px; margin-top: 4px; }
    .row { display: flex; gap: 8px; justify-content: flex-end; margin-top: 10px; }
    button { padding: 6px 12px; border-radius: 6px; border: 1px solid #ccc; background: #fff; }
    button.primary { background: #0c8ce9; color: #fff; border-color: #0c8ce9; }
  </style>
</head>
<body>
  <label>Pattern <input id="pattern" value="Layer #"></label>
  <label>Start at <input id="start" type="number" value="1"></label>
  <div id="count">No selection</div>
  <div class="row">
    <button id="cancel">Close</button>
    <button id="rename" class="primary">Rename</button>
  </div>
  <script>
    popcraft.onmessage = (msg) => {
      if (msg.type === 'selection') {
        document.getElementById('count').textContent = msg.count ? `${msg.count} layer(s) selected` : 'No selection'
      }
    }
    document.getElementById('rename').onclick = () => popcraft.postMessage({
      type: 'rename',
      pattern: document.getElementById('pattern').value,
      start: document.getElementById('start').value,
    })
    document.getElementById('cancel').onclick = () => popcraft.postMessage({ type: 'cancel' })
    popcraft.postMessage({ type: 'ready' })
  </script>
</body>
</html>

Tips

  • Send a ready message from the panel and reply with initial data — the panel may load before or after your main script sets its handler (messages are queued either way, but this keeps the flow explicit).
  • The panel background is white by default; set your own colors to match light or dark designs.
  • Keep heavy work in main.js and send results to the panel.