@design-canvas/toolbox
v0.4.28
Published
A composable plugin ecosystem for developer tools — floating toolbar, element inspector, design tokens, annotations, and more.
Downloads
6,317
Maintainers
Readme
Design Canvas
A composable plugin ecosystem for developer tools. Drop it into any project, toggle tools on/off from a floating toolbar, and let plugins share services like notifications, storage, element picking, GitHub issues, screenshots, and more — all without a server or database.
Quick Start
1. Install
npm install @design-canvas/toolbox2. Initialize project
Run the init command to scaffold config files and copy AI skills into your project:
npx design-canvas initThis creates:
.github/skills/dc-*— AI agent skills for Copilot / Claude.designcanvas.json— default plugin configuration.checks/registry.json— empty checks registry
After upgrading Design Canvas, re-run with --force to update skills to the latest version:
npx design-canvas init --forceConfig files (
.designcanvas.json,.checks/registry.json) are never overwritten — only skills are refreshed.
3. Add the Vite plugin
// vite.config.ts
import { designCanvas } from '@design-canvas/toolbox/loader/vite';
export default defineConfig({
plugins: [designCanvas()],
});That's it. The Vite plugin auto-injects the loader into your HTML, mounts the toolbar, and boots all configured plugins. Your app code stays untouched.
4. Configure plugins
Create a .designcanvas.json in your project root:
{
// Which plugins to load
"plugins": {
"notes": { "enabled": true },
"inspector": { "enabled": true },
"checks": { "enabled": true },
"style-editor": { "enabled": true },
"storybook-bridge": { "enabled": true },
// Per-plugin panel positioning (optional)
"my-plugin": {
"enabled": true,
"panel": {
"defaultPosition": "top-right", // "center" | "top-left" | "top-right" | "bottom-left" | "bottom-right" | { "x": 100, "y": 200 }
"defaultDock": "right", // "left" | "right" | "bottom" (overrides plugin default)
"defaultSize": { "width": 400, "height": 600 }
}
}
},
// Toolbar appearance
"toolbar": {
"position": "right", // "left" | "right" | "top" | "bottom"
"theme": "auto" // "light" | "dark" | "auto"
}
}JSONC comments are supported. The config file is watched — changes trigger a full page reload automatically.
Panel Position Priority
When a plugin panel opens, position is resolved in this order:
- localStorage — if the user previously dragged/resized the panel, that position is restored
.designcanvas.json— thepanel.defaultPositionfor that plugin (if configured)- Center — falls back to centering the panel on screen
Alternative: Manual initialization
If you don't use Vite or want more control:
import { init } from '@design-canvas/toolbox/loader';
await init({
toolbar: { position: 'bottom', theme: 'dark' },
});Architecture
┌──────────────────────────────────────────────┐
│ Host App (any repo) │
├──────────────────────────────────────────────┤
│ Floating Toolbar │
│ [ ⬡ ] [ 📝 ] [ 🔍 ] [ 🎨 ] [ ✓ ] [ 📖 ] │
├──────────────────────────────────────────────┤
│ Plugins (each independently toggled) │
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ Notes │ │Inspector │ │ Style Editor │ │
│ │ 🟢 on │ │ 🔴 off │ │ 🟢 on │ │
│ └──────────┘ └──────────┘ └──────────────┘ │
├──────────────────────────────────────────────┤
│ Shared Services Layer │
│ notify · errors · storage · log · commands │
│ clipboard · elementPicker · github · │
│ screenshot · notes · copilot · ... │
├──────────────────────────────────────────────┤
│ Runtime Kernel (core) │
│ lifecycle · event bus · service registry │
└──────────────────────────────────────────────┘The system has three layers:
- Runtime Kernel — manages plugin lifecycle, an event bus for inter-plugin communication, and a service registry.
- Shared Services — global capabilities (notifications, storage, element picking, GitHub, etc.) that any plugin can use without knowing the implementation.
- Plugins — self-contained tools that render into scoped DOM containers, communicate through events, and consume services.
Plugin Lifecycle
Every plugin goes through a state machine:
idle ──► loading ──► active ◄──► suspended
│
▼
error| State | Description |
| --- | --- |
| idle | Registered but never activated. No DOM, no listeners. |
| loading | activate() has been called, waiting for async setup. |
| active | Running — DOM mounted, listeners attached, UI visible. |
| suspended | UI torn down, but in-memory state is preserved. Re-activating is instant. |
| error | Activation failed. Plugin is inert until retried. |
Key: Suspending is NOT unloading. The plugin keeps its data (annotations, unsaved edits, etc.) and restores instantly when re-activated.
Creating a Custom Plugin
A plugin is a single object that implements PluginDefinition. Here's a minimal example:
import type { PluginDefinition, PluginContext } from '@design-canvas/toolbox/core';
export const myPlugin: PluginDefinition = {
meta: {
id: 'my-plugin',
name: 'My Plugin',
icon: '🚀', // emoji, SVG string, or URL
description: 'Does cool things',
version: '1.0.0',
panel: {
defaultSize: { width: 320, height: 400 },
minSize: { width: 280, height: 200 },
resizable: true,
showStatusBar: true,
defaultDock: 'right', // 'left' | 'right' | 'bottom'
defaultPosition: 'center', // 'center' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | { x, y }
},
},
activate(ctx: PluginContext) {
// ctx.mountPoint is your scoped DOM container
ctx.mountPoint.innerHTML = `
<div style="padding: 16px;">
<h3>Hello from My Plugin!</h3>
<button id="my-btn">Click me</button>
</div>
`;
const btn = ctx.mountPoint.querySelector('#my-btn')!;
btn.addEventListener('click', () => {
ctx.services.notify.toast('Button clicked!', 'success');
});
},
suspend() {
// Optional: tear down UI while keeping in-memory state.
// Called when the user toggles the plugin off.
},
deactivate() {
// Optional: full cleanup — release everything.
// Called when the plugin is unregistered.
},
getAPI() {
// Optional: return a public API other plugins can call
// via ctx.getPlugin('my-plugin')
return {
doSomething() { /* ... */ },
};
},
getHandoffContext() {
// Optional: contribute data to a handoff payload
return { notes: ['...'] };
},
};Plugin Definition Reference
interface PluginDefinition {
meta: {
id: string; // Unique identifier (kebab-case)
name: string; // Display name in toolbar
icon: string; // SVG string, emoji, or URL
description?: string;
version?: string;
panel?: {
defaultSize?: { width: number; height: number };
minSize?: { width: number; height: number };
resizable?: boolean;
showStatusBar?: boolean;
defaultDock?: 'left' | 'right' | 'bottom';
};
};
skills?: SkillDefinition[]; // AI agent skills (see below)
activate(ctx: PluginContext, config?): void | Promise<void>;
suspend?(): void;
deactivate?(): void;
getAPI?(): unknown;
getHandoffContext?(): unknown;
}Plugin Context
When activate() is called, the runtime passes a PluginContext with everything your plugin needs:
interface PluginContext {
mountPoint: HTMLElement; // Scoped DOM container for your UI
panel: PanelHandle; // Control the panel (open, close, dock, resize)
services: ServiceMap; // All shared services
isDark: boolean; // Current theme (for styling)
emit(type: string, payload?: unknown): void; // Publish event
on(type: string, handler): Disposable; // Subscribe to events
getPlugin(id: string): unknown | null; // Access another plugin's API
storage: PluginStorage; // Per-plugin scoped persistence
badge: BadgeHandle; // Set a badge count on your toolbar icon
}Mount Point
ctx.mountPoint is a <div> scoped to your plugin. Render your entire UI here — plain DOM, or wire up any framework (React, Svelte, etc.).
Panel Control
ctx.panel.open(); // Show the panel
ctx.panel.close(); // Hide it
ctx.panel.collapse(); // Minimize to title bar
ctx.panel.expand(); // Restore from collapsed
ctx.panel.dock('right'); // Dock to an edge
ctx.panel.undock(); // Float freely
ctx.panel.setPreferredSize(400, 500); // Set dimensions
ctx.panel.setStatus('3 items'); // Status bar text
ctx.panel.setStatusActions([ // Status bar buttons
{ label: 'Export', icon: '📤', onClick: () => exportData() },
]);Scoped Storage
Each plugin gets its own namespaced key-value store (backed by localStorage under dc:<pluginId>:<key>):
ctx.storage.set('lastFilter', 'unresolved');
const filter = ctx.storage.get<string>('lastFilter'); // 'unresolved'
ctx.storage.delete('lastFilter');Badge
Show a count on your toolbar icon:
ctx.badge.set(5); // Shows "5" badge
ctx.badge.clear(); // Removes badgeInter-Plugin Communication
Plugins communicate via a shared event bus:
// Plugin A: publish
ctx.emit('element:selected', { selector: '.header', tagName: 'div' });
// Plugin B: subscribe
const sub = ctx.on('element:selected', (event) => {
console.log(event.source); // 'plugin-a'
console.log(event.payload); // { selector: '.header', tagName: 'div' }
console.log(event.timestamp);
});
// Later: unsubscribe
sub.dispose();Subscribe to all events with ctx.on('*', handler).
Accessing Other Plugins
const notesAPI = ctx.getPlugin('notes');
if (notesAPI) {
notesAPI.addNote('This element needs attention');
}This returns whatever getAPI() returns from the target plugin (or null if it's not active).
Shared Services
Every plugin has access to the full service layer via ctx.services. Here's what's available:
Notify — ctx.services.notify
User-facing notifications.
ctx.services.notify.toast('Saved!', 'success'); // 'info' | 'success' | 'warning' | 'error'
const confirmed = await ctx.services.notify.confirm('Delete this item?'); // true/false
const progress = ctx.services.notify.progress('Exporting...');
progress.update(50); // percent
progress.done();Service Unavailable Banners
When a plugin depends on an optional service (GitHub, Copilot, Notes sync, etc.), use serviceUnavailable() instead of a bare toast. It renders a rich notification with the service's setup guide and the list of features the user is missing:
if (!ctx.services.github.available) {
ctx.services.notify.serviceUnavailable({
service: 'github',
pluginName: 'My Plugin',
features: ['Create issues from annotations', 'Assign collaborators'],
});
return;
}The banner automatically looks up the named service's setupGuide and renders inline setup instructions (e.g. "Install gh CLI", "Run npx design-canvas serve"). It auto-dismisses after 10 seconds or on click.
All server-dependent services expose a consistent available / setupGuide pair:
| Service | available | setupGuide |
|---------|-------------|---------------|
| github | ✅ | ✅ |
| copilot | ✅ | ✅ |
| screenshot | ✅ | ✅ |
| notes | ✅ | ✅ |
| design | ✅ | ✅ |
Errors — ctx.services.errors
Centralized error capture and display.
ctx.services.errors.report({
source: 'my-plugin',
message: 'Failed to load data',
severity: 'error', // 'warning' | 'error' | 'fatal'
error: err, // original Error object
context: { url: '/api/data' },
});
const allErrors = ctx.services.errors.getAll();
ctx.services.errors.clear();
// Listen for errors from any source
ctx.services.errors.onError((entry) => {
console.log(`${entry.source}: ${entry.message}`);
});Storage — ctx.services.storage
Global key-value persistence (localStorage). For plugin-scoped storage, use ctx.storage instead.
ctx.services.storage.set('global-setting', value);
const val = ctx.services.storage.get<string>('global-setting');
ctx.services.storage.delete('global-setting');Log — ctx.services.log
Structured logging.
ctx.services.log.debug('Detailed info', extraData);
ctx.services.log.info('Plugin initialized');
ctx.services.log.warn('Deprecated usage');
ctx.services.log.error('Something broke', err);Commands — ctx.services.commands
Register keyboard shortcuts.
const disposable = ctx.services.commands.register(
'my-plugin:search',
'Ctrl+Shift+F',
() => openSearch(),
);
// Execute a registered command
ctx.services.commands.execute('my-plugin:search');
// List all registered commands
const all = ctx.services.commands.list(); // [{ id, shortcut }]Clipboard — ctx.services.clipboard
await ctx.services.clipboard.copy('Hello, clipboard!');
const text = await ctx.services.clipboard.read();Element Picker — ctx.services.elementPicker
A shared DOM element selection tool. When a user needs to point at an element — for inspecting, annotating, measuring — use this service. All plugins share the same hover/highlight/click interaction.
// One-shot pick
const result = await ctx.services.elementPicker.pick({
prompt: 'Select an element to inspect',
highlightColor: '#0078d4',
});
if (result) {
console.log(result.element); // HTMLElement
console.log(result.selector); // CSS selector string
console.log(result.displayPath); // 'div.container > section > h1'
console.log(result.rect); // DOMRect
console.log(result.computedStyles); // key CSS properties
}
// Continuous pick mode (keeps picking until cancelled)
ctx.services.elementPicker.pick({
continuous: true,
prompt: 'Click elements to inspect — Esc to stop',
});
// Listen for pick events
ctx.services.elementPicker.onPick((event) => {
// event.type: 'start' | 'hover' | 'select' | 'deselect' | 'cancel'
if (event.type === 'select') {
console.log('Selected:', event.element, event.selector);
}
});
// Cancel picking
ctx.services.elementPicker.cancel();
// Check state
ctx.services.elementPicker.isPicking; // boolean
ctx.services.elementPicker.requestedBy; // plugin ID or nullGitHub — ctx.services.github
GitHub integration (issues, users, collaborators). Requires a dev server with a GitHub token.
if (ctx.services.github.available) {
// User info
const user = await ctx.services.github.getUser();
console.log(user?.login, user?.avatarUrl);
// Collaborators
const collabs = await ctx.services.github.getCollaborators();
// Issues
const issue = await ctx.services.github.createIssue({
title: 'Fix padding on header',
body: '## Description\nPadding is 8px, should be 16px.',
labels: ['design', 'css'],
assignees: ['username'],
});
console.log(issue.url); // https://github.com/owner/repo/issues/42
const issues = await ctx.services.github.listIssues();
await ctx.services.github.updateIssueState(42, 'closed');
// Assign GitHub Copilot to an issue
await ctx.services.github.assignCopilot(42);
} else {
console.log(ctx.services.github.setupGuide); // instructions to enable
}Screenshot — ctx.services.screenshot
Capture screenshots of DOM elements (requires the dev server with Playwright).
if (ctx.services.screenshot.available) {
const dataUrl = await ctx.services.screenshot.capture('.my-component');
if (dataUrl) {
img.src = dataUrl; // base64 PNG
}
}Notes — ctx.services.notes
Shared annotation persistence (synced via the dev server). Falls back to localStorage-only when the server is unavailable. Any plugin can create annotations programmatically.
if (!ctx.services.notes.available) {
console.log(ctx.services.notes.setupGuide); // instructions to enable sync
}
const notes = await ctx.services.notes.list();
await ctx.services.notes.save({
id: crypto.randomUUID(),
text: 'Padding needs to match the design spec',
url: location.pathname,
selector: '.hero-section',
elementLabel: 'Hero Section',
timestamp: Date.now(),
updatedAt: Date.now(),
source: 'my-plugin', // identifies the creating plugin
severity: 'warning', // 'info' | 'warning' | 'error' (optional)
status: 'open', // 'open' | 'dismissed' | 'closed' (optional, default: 'open')
author: { login: 'octocat', name: 'Mona' }, // optional
});
await ctx.services.notes.remove(noteId);
// Real-time sync — fires when notes change (polling or other tabs)
ctx.services.notes.onRemoteChange((updatedNotes) => {
refreshUI(updatedNotes);
});Creating Annotations from a Plugin
Plugins can act as annotation sources by setting the source field. This allows filtering annotations by origin in the Annotations panel:
// In your plugin's activate():
async function annotate(selector: string, message: string, severity?: 'info' | 'warning' | 'error') {
await ctx.services.notes.save({
id: crypto.randomUUID(),
text: message,
url: location.pathname,
selector,
elementLabel: document.querySelector(selector)?.tagName.toLowerCase(),
timestamp: Date.now(),
updatedAt: Date.now(),
source: ctx.meta.id, // your plugin's ID — shows in the Source filter
severity,
});
}
// Usage
annotate('.nav-link', 'Color contrast ratio is 3.2:1, needs 4.5:1', 'error');
annotate('#hero img', 'Missing alt text', 'warning');SharedNote Interface
interface SharedNote {
id: string;
text: string;
url: string; // page pathname
selector?: string; // CSS selector of the annotated element
elementLabel?: string; // human-readable element description
screenshotDataUrl?: string; // base64 PNG thumbnail
timestamp: number;
updatedAt: number;
author?: { login: string; name: string | null; avatarUrl?: string };
issue?: { number: number; url: string };
source?: string; // plugin ID that created the note
severity?: 'info' | 'warning' | 'error';
status?: 'open' | 'dismissed' | 'closed';
}Copilot — ctx.services.copilot
AI-powered assistance (requires the dev server with a Copilot endpoint).
if (ctx.services.copilot.available) {
// Free-form chat
const response = await ctx.services.copilot.ask([
{ role: 'system', content: 'You are a CSS expert.' },
{ role: 'user', content: 'How do I center a div?' },
]);
// Shorthand helpers
const explanation = await ctx.services.copilot.explain('grid-template-columns: repeat(auto-fill, minmax(200px, 1fr))');
const suggestion = await ctx.services.copilot.suggest('A responsive card layout with gap');
}Issue Templates — ctx.services.issueTemplates
Format structured data into GitHub issue bodies.
const { title, body, labels } = ctx.services.issueTemplates.formatStyleChange(
[{ selector: '.btn', property: 'padding', oldValue: '8px', newValue: '16px' }],
{ framework: 'Tailwind', pageUrl: location.href },
);
// → title: "Style change: .btn padding"
// → body: formatted Markdown with before/after table
// → labels: ['design-canvas', 'style-change']Bundling AI Skills with Plugins
Plugins can bundle skill definitions that get installed as .github/skills/dc-* files. These teach AI agents (GitHub Copilot, Claude, etc.) how to use your plugin.
export const myPlugin: PluginDefinition = {
meta: { id: 'my-plugin', name: 'My Plugin', icon: '🚀' },
skills: [
{
id: 'dc-my-action',
name: 'My Custom Action',
description: 'Use when the user asks to do X. Triggers on "do X", "run X".',
instructions: `## Workflow
1. Open the My Plugin panel from the toolbar
2. Click the action button
3. Review the output
## Capabilities
- Does X, Y, and Z
- Supports A and B`,
},
],
activate(ctx) { /* ... */ },
};Skills are automatically installed to the project's .github/skills/ directory when the plugin loads (via the dev server).
Publishing a Plugin as an npm Package
The loader resolves plugins by ID. After checking built-in plugins, it tries npm packages with these naming conventions:
@design-canvas/plugin-<id> (scoped)
design-canvas-plugin-<id> (unscoped)Your package should export the plugin definition:
// package: @design-canvas/plugin-my-tool
import type { PluginDefinition } from '@design-canvas/toolbox/core';
export const plugin: PluginDefinition = {
meta: { id: 'my-tool', name: 'My Tool', icon: '🔧' },
activate(ctx) { /* ... */ },
};
// Also works: export default, or export { myToolPlugin }Users then add it to their config:
{
"plugins": {
"my-tool": { "enabled": true }
}
}Packages
| Package | Path | Description |
| --- | --- | --- |
| @design-canvas/toolbox/core | packages/core/ | Runtime kernel, event bus, types, built-in service implementations |
| @design-canvas/toolbox/loader | packages/loader/ | Config reader, plugin resolver, auto-init entry point |
| @design-canvas/toolbox/loader/vite | packages/loader/src/vite.ts | Vite plugin for zero-config dev integration |
| @design-canvas/toolbox/toolbar | packages/toolbar/ | Floating toolbar UI with plugin icon toggles |
| @design-canvas/toolbox/panel | packages/panel/ | Draggable, dockable, resizable panel for plugin content |
| @design-canvas/toolbox/server | packages/server/ | Dev server middleware (GitHub proxy, screenshots, notes sync, Copilot, checks) |
Built-in Plugins
| Plugin | ID | Description |
| --- | --- | --- |
| Annotations | annotate | Annotate page elements with notes, resolve/close workflow, severity levels, pin to selectors, file as GitHub issues (system — always visible) |
| AI Settings | llm-settings | View/configure LLM providers, agent CLIs, edit modes, and install status (system — always active) |
Dev Server
The server package (@design-canvas/server) provides backend services that plugins rely on. When using the Vite plugin, it's embedded automatically. For standalone use:
npx design-canvas serve --port 4460Server Routes
| Route | Purpose |
| --- | --- |
| /__dc/gh/* | GitHub API proxy (issues, users, repos) |
| /__dc/notes | Notes CRUD + real-time sync (SSE) |
| /__dc/screenshot | Playwright-based element screenshot capture |
| /__dc/llm/* | LLM chat, status, and completions (multi-provider) |
| /__dc/llm/pr/* | AI-powered PR generation (preview, commit, revert) |
| /__dc/llm/delegate/* | Delegate tasks to GitHub Copilot Coding Agent |
| /__dc/llm/agent/* | Local agent CLI execution (Copilot CLI, Claude Code, Codex) |
| /__dc/llm/settings/* | LLM settings status and config updates |
| /__dc/checks | Project check runner |
| /__dc/skills | Skill file installer |
Environment Variables
| Variable | Purpose |
| --- | --- |
| GITHUB_TOKEN | GitHub Personal Access Token for issue/repo operations |
| DC_REPO_OWNER | GitHub repository owner |
| DC_REPO_NAME | GitHub repository name |
Config Reference
Full .designcanvas.json schema:
interface DesignCanvasConfig {
$schema?: string;
plugins?: Record<string, {
enabled?: boolean; // default: true
config?: Record<string, unknown>; // plugin-specific config
}>;
toolbar?: {
position?: 'left' | 'right' | 'top' | 'bottom'; // default: 'right'
theme?: 'light' | 'dark' | 'auto'; // default: 'auto'
};
services?: Record<string, {
provider?: string;
[key: string]: unknown;
}>;
framework?: {
id?: string; // e.g. 'tailwind', 'bootstrap'
configPath?: string;
};
design?: {
source?: string; // path to DESIGN.md or design token source
};
access?: {
roles?: Record<string, string[]>;
};
}Example: Full Custom Plugin
Here's a complete plugin that uses multiple services:
import type { PluginDefinition, PluginContext } from '@design-canvas/toolbox/core';
export const accessibilityPlugin: PluginDefinition = {
meta: {
id: 'a11y-checker',
name: 'Accessibility',
icon: '♿',
description: 'Check elements for accessibility issues',
panel: {
defaultSize: { width: 360, height: 480 },
showStatusBar: true,
},
},
activate(ctx: PluginContext) {
let issues: Array<{ element: HTMLElement; message: string }> = [];
// Register a keyboard shortcut
ctx.services.commands.register('a11y:scan', 'Ctrl+Shift+A', () => scanPage());
// Listen for element selections from other plugins
ctx.on('element:selected', (event) => {
const { selector } = event.payload as { selector: string };
checkElement(document.querySelector(selector) as HTMLElement);
});
function scanPage() {
issues = [];
// Check all images for alt text
document.querySelectorAll('img').forEach((img) => {
if (!img.alt) {
issues.push({ element: img, message: 'Missing alt attribute' });
}
});
// Check buttons for accessible names
document.querySelectorAll('button').forEach((btn) => {
if (!btn.textContent?.trim() && !btn.getAttribute('aria-label')) {
issues.push({ element: btn, message: 'Button has no accessible name' });
}
});
// Update badge and status
ctx.badge.set(issues.length);
ctx.panel.setStatus(`${issues.length} issue${issues.length !== 1 ? 's' : ''}`);
render();
if (issues.length === 0) {
ctx.services.notify.toast('No accessibility issues found!', 'success');
}
}
function checkElement(el: HTMLElement) {
if (!el) return;
const tag = el.tagName.toLowerCase();
if (tag === 'img' && !el.getAttribute('alt')) {
ctx.services.notify.toast(`Image missing alt text`, 'warning');
}
}
function render() {
const dark = ctx.isDark;
ctx.mountPoint.innerHTML = `
<div style="padding: 16px; font-family: system-ui; color: ${dark ? '#e0e0e0' : '#1a1a1a'}">
<button id="scan-btn" style="
padding: 8px 16px; border-radius: 6px; border: none; cursor: pointer;
background: #0078d4; color: #fff; font-size: 13px; margin-bottom: 12px;
">Scan Page</button>
${issues.length === 0
? '<p style="color: #888; font-size: 13px;">Click "Scan Page" to check for issues.</p>'
: issues.map((issue, i) => `
<div style="
padding: 8px 12px; margin-bottom: 8px; border-radius: 6px;
background: ${dark ? 'rgba(255,107,107,0.1)' : 'rgba(209,52,56,0.06)'};
font-size: 13px; border-left: 3px solid #d13438;
">
<strong>${issue.element.tagName.toLowerCase()}</strong>: ${issue.message}
</div>
`).join('')
}
</div>
`;
ctx.mountPoint.querySelector('#scan-btn')?.addEventListener('click', scanPage);
}
render();
// Set up status bar action
ctx.panel.setStatusActions([
{ label: 'Scan', icon: '🔄', onClick: scanPage },
]);
},
deactivate() {
// cleanup if needed
},
getAPI() {
return {
getIssueCount: () => 0,
};
},
};Publishing to npm
The package is published automatically on every push to main via GitHub Actions — but only when the version in package.json changes. Pushes that don't bump the version are no-ops.
How it works
- The workflow runs
npm ci→npm run build - Compares the local
package.jsonversion against the latest on npm - Publishes with npm provenance for supply-chain transparency
Setup
The workflow requires an NPM_TOKEN secret in the repository:
- Create an Automation token at npmjs.com → Access Tokens
- Add it as a repo secret: Settings → Secrets and variables → Actions → New repository secret → name it
NPM_TOKEN
Or via the CLI:
echo "<your-token>" | gh secret set NPM_TOKENReleasing a new version
npm version patch # or minor / major
git push origin mainThe workflow picks up the version bump and publishes automatically.
License
MIT
