Documentation Index
Fetch the complete documentation index at: https://empty-ad9a3406.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
This guide walks you through building a complete plugin from scratch.
Prerequisites
- A modern browser
- A text editor
- Blank Board running locally (
npx serve)
Step 1: Create the File
plugins/
└── my-plugin/
└── plugin.js
// plugins/my-plugin/plugin.js
export const meta = {
id: 'my-plugin',
name: 'My First Plugin',
version: '0.1.0',
compat: '>=3.3.0'
};
export function setup(api) {
// Your code here
}
export function teardown() {
// Cleanup (optional but recommended)
}
| Field | Required | Description |
|---|
id | ✅ | Unique identifier. Use kebab-case |
name | ✅ | Display name in Plugin Manager |
version | ✅ | Semantic version |
compat | ❌ | Core version compatibility range |
Step 3: Build Your UI
Use the managed container for standard plugins:
export function setup(api) {
const container = api.container;
container.innerHTML = `
<div style="padding: 20px; background: white; border-radius: 12px;">
<h3 style="margin: 0 0 10px 0;">My Plugin</h3>
<p>Hello from my first plugin!</p>
</div>
`;
// Container is already draggable and resizable!
}
Or create your own element for more control:
export function setup(api) {
const box = document.createElement('div');
box.className = 'plugin-box';
box.style.cssText = `
left: 100px;
top: 100px;
width: 300px;
padding: 20px;
background: white;
border-radius: 12px;
`;
box.innerHTML = `
<h3 style="margin:0 0 10px 0;">My Plugin</h3>
<p>Hello from my first plugin!</p>
`;
api.boardEl.appendChild(box);
api.makeDraggable(box);
api.makeResizable(box);
}
Step 4: Add Storage
Use plugin-scoped storage for clean key management:
let currentApi = null;
export function setup(api) {
currentApi = api;
const container = api.container;
// Restore saved position
const pos = api.storage.getForPlugin(meta.id, 'pos');
if (pos) {
container.style.left = pos.left + 'px';
container.style.top = pos.top + 'px';
}
container.innerHTML = `
<div style="padding: 20px; background: white; border-radius: 12px;">
<p>Drag me — I remember my position!</p>
</div>
`;
// Save position on drag end
api.bus.on('plugin:dragend', ({ el }) => {
if (el.dataset.pluginId === meta.id) {
api.storage.setForPlugin(meta.id, 'pos', {
left: parseInt(el.style.left),
top: parseInt(el.style.top)
});
}
});
}
Step 5: Add Event Communication
let currentApi = null;
export function setup(api) {
currentApi = api;
const container = api.container;
let count = api.storage.getForPlugin(meta.id, 'count') || 0;
container.innerHTML = `
<div style="padding: 24px; text-align: center; background: white; border-radius: 12px;">
<div style="font-size: 14px; color: #888;">Counter</div>
<div id="count" style="font-size: 48px; font-weight: 700; margin: 16px 0;">${count}</div>
<button id="inc" style="padding: 10px 24px; font-size: 18px; cursor: pointer;">+</button>
</div>
`;
container.querySelector('#inc').addEventListener('click', () => {
count++;
container.querySelector('#count').textContent = count;
api.storage.setForPlugin(meta.id, 'count', count);
api.bus.emit('counter:changed', { value: count });
});
}
Step 6: Inject Scoped CSS
export function setup(api) {
api.injectCSS(meta.id, `
.my-counter {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
padding: 24px;
border-radius: 16px;
text-align: center;
}
.my-counter button {
background: rgba(255,255,255,0.2);
border: none;
color: white;
padding: 10px 24px;
border-radius: 8px;
font-size: 18px;
cursor: pointer;
}
.my-counter button:hover {
background: rgba(255,255,255,0.3);
}
`);
const container = api.container;
container.innerHTML = `
<div class="my-counter">
<div style="font-size: 14px; opacity: 0.8;">Counter</div>
<div id="count" style="font-size: 48px; font-weight: 700; margin: 16px 0;">0</div>
<button id="increment">+</button>
</div>
`;
}
Step 7: Handle Cleanup
let currentApi = null;
export function setup(api) {
currentApi = api;
// ... setup code ...
}
export function teardown() {
currentApi?.removeCSS(meta.id);
console.log('Plugin cleaned up');
}
Step 8: Show Notifications
export function setup(api) {
const container = api.container;
container.innerHTML = `
<div style="padding: 20px; text-align: center;">
<button id="notify">Notify me!</button>
</div>
`;
container.querySelector('#notify').addEventListener('click', () => {
api.notify('Button clicked!', 'success', 3000);
});
}
Step 9: Install and Test
Option A: Manual Install
- Right-click the board → Plugin Manager
- Click Install via URL
- Enter ID and URL:
http://localhost:3000/plugins/my-plugin/plugin.js
- Click Install
Option B: Debug in Console
// In browser DevTools:
await window.blankBoard.api.installPlugin(
'my-plugin',
'http://localhost:3000/plugins/my-plugin/plugin.js',
'My Plugin'
);
Debugging
Open browser DevTools:
// Access the core API
window.blankBoard.api.registry.getAll();
window.blankBoard.api.version; // → "4.0.0"
// Emit test events
window.blankBoard.bus.emit('test', { hello: true });
// Check storage
window.blankBoard.api.storage.list();
// Reload a plugin
await window.blankBoard.api.reloadPlugin('my-plugin');
Complete Example
Here’s a full plugin using all the patterns above:
export const meta = {
id: 'my-awesome-plugin',
name: 'My Awesome Plugin',
version: '1.0.0',
compat: '>=3.3.0'
};
let currentApi = null;
export function setup(api) {
currentApi = api;
// Inject scoped styles
api.injectCSS(meta.id, `
.maw-widget {
background: #1a1a2e;
color: #eee;
padding: 24px;
border-radius: 16px;
font-family: system-ui, sans-serif;
}
.maw-btn {
background: #7c6fff;
color: white;
border: none;
padding: 8px 16px;
border-radius: 8px;
cursor: pointer;
margin-top: 12px;
}
`);
// Restore state
const count = api.storage.getForPlugin(meta.id, 'count') || 0;
const pos = api.storage.getForPlugin(meta.id, 'pos');
// Container
const container = api.container;
if (pos) {
container.style.left = pos.left + 'px';
container.style.top = pos.top + 'px';
}
container.innerHTML = `
<div class="maw-widget">
<h3 style="margin: 0 0 8px 0;">Awesome Plugin</h3>
<div>Count: <span id="maw-count">${count}</span></div>
<button class="maw-btn" id="maw-inc">+ Increment</button>
</div>
`;
container.querySelector('#maw-inc').addEventListener('click', () => {
const el = container.querySelector('#maw-count');
const newCount = parseInt(el.textContent) + 1;
el.textContent = newCount;
api.storage.setForPlugin(meta.id, 'count', newCount);
api.bus.emit('counter:changed', { value: newCount });
api.notify(`Count: ${newCount}`, 'info', 1500);
});
// Save position on drag
api.bus.on('plugin:dragend', ({ el }) => {
if (el.dataset.pluginId === meta.id) {
api.storage.setForPlugin(meta.id, 'pos', {
left: parseInt(el.style.left),
top: parseInt(el.style.top)
});
}
});
}
export function teardown() {
currentApi?.removeCSS(meta.id);
}
Persisting Data
Deep dive into storage patterns
Plugin Communication
Events, hooks, and patterns