[]previous and next page

Plugins and marketplaces

Package shapes, tools and actions as plugins, and share artwork as data-only spec shapes that are safe to accept from anyone.

There are two kinds of plugins, for two different jobs.

Code plugins add shapes, tools and actions to your app. You write them, you ship them in your bundle.

Manifest plugins are pure data: shape specs and library items. They're safe to load from a marketplace, from another user, or from a file, because nothing in them runs.

Code plugins#

ts
import { createEditor, type CanvasFlowPlugin } from 'canvasflow'

const diagramKit: CanvasFlowPlugin = {
  name: 'diagram-kit',
  shapes: [Badge],
  tools: [StampTool],
  actions: [{ id: 'insert-badge', label: 'Insert badge', run: (editor) => editor.setTool('stamp') }],
  setup(editor) {
    const off = editor.on('shape:dblclick', ({ shape }) => console.log(shape.id))
    return off // cleanup when the plugin is removed
  },
}

const editor = createEditor({ plugins: [diagramKit] })
// or later: const remove = editor.use(diagramKit)

Spec shapes#

A spec is a shape described entirely by data: SVG paths in a viewBox, plus which parts follow the shape's styles.

server-rack.tsts
import type { ShapeSpec } from 'canvasflow'

export const serverRack: ShapeSpec = {
  id: 'acme.server-rack',
  version: 1,
  name: 'Server rack',
  viewBox: [0, 0, 80, 100],
  paths: [
    { d: 'M8 4h64a4 4 0 0 1 4 4v84a4 4 0 0 1-4 4H8a4 4 0 0 1-4-4V8a4 4 0 0 1 4-4Z', fill: 'style', stroke: 'style', strokeWidth: 'style', sketch: true },
    { d: 'M14 24h52M14 48h52M14 72h52', stroke: 'style', strokeWidth: 'style', dash: true },
  ],
  label: { x: 6, y: 80, w: 68, h: 16 },
  defaultSize: { w: 80, h: 100 },
  tags: ['network', 'infrastructure'],
}

For each path, fill and stroke can be 'style' (follow the shape's fill or stroke color), 'none', a color token or a hex color. strokeWidth: 'style' follows the stroke-width style, dash follows the dash style, and sketch applies sloppiness.

Specs are validated on the way in: path data is parsed, colors are checked against an allow-list, sizes are capped. No URLs, no scripts, no HTML.

They travel with the document#

When a spec shape is placed, the spec itself is stored in the document as an asset. Someone who opens the board without the plugin still sees the shape exactly, and can copy it. That's what makes specs safe and convenient for marketplaces.

Manifests#

A manifest bundles specs and library items under one id and version:

ts
import { installPlugin, validatePluginManifest, type PluginManifest } from 'canvasflow'

const manifest: PluginManifest = {
  id: 'acme.network-icons',
  name: 'Network icons',
  version: '1.2.0',
  author: 'Acme',
  shapes: [serverRack],
}

const check = validatePluginManifest(manifest)
if (!check.ok) throw new Error(check.errors.join('\n'))

const uninstall = installPlugin(editor, manifest)

Installing adds a tool for each shape (spec:<id>) and entries in the catalog, which is what a library panel lists:

ts
import { getCatalog, placeSpec } from 'canvasflow'

getCatalog(editor) // installed specs and library items
placeSpec(editor, 'acme.server-rack', { x: 200, y: 200 }) // drop one on the page

Make a shape from a drawing#

People can turn what they drew into a reusable spec:

ts
import { createSpecFromShapes } from 'canvasflow'

const spec = createSpecFromShapes(editor, editor.getSelectedIds(), {
  id: 'me.logo',
  name: 'My logo',
  bindColors: true, // the main color follows the shape's color style
})

That's the whole flow behind a marketplace: draw, turn into a spec, publish the manifest, install it elsewhere. Kashkol's marketplace is built exactly this way, with the server running validatePluginManifest before anything is published.

Drag from your own library panel#

On the web, make any element draggable onto the canvas:

ts
import { DRAG_MIME } from 'canvasflow/dom'

item.draggable = true
item.addEventListener('dragstart', (e) => {
  e.dataTransfer!.setData(DRAG_MIME, JSON.stringify({ kind: 'spec', id: 'acme.server-rack' }))
})