Creating StickerNest Widgets
This skill guides you through creating widgets for StickerNest. Widgets follow the Lego Brick Model - they are composable, interchangeable units with standard connectors.
Core Philosophy: Widgets Are Lego Bricks
Widgets don't store data. Widgets display data.
A widget is a functional unit that:
- •Pulls data from any compatible source (native schemas, Notion, Obsidian, etc.)
- •Displays/edits that data according to its logic
- •Communicates via a standard event protocol
- •Can be themed/skinned without changing functionality
- •Is interchangeable with any widget of the same functional type
The Five Widget Layers
Every widget has these conceptual layers:
┌─────────────────────────────────────────────────────────────┐ │ WIDGET LAYERS │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 1. THEME/SKIN LAYER │ │ Visual appearance only. Uses CSS tokens. │ │ Swappable without changing functionality. │ │ │ │ 2. LOGIC LAYER │ │ Core functionality. Hardcoded per widget type. │ │ I/O definitions, event handling, capabilities. │ │ │ │ 3. STATE LAYER │ │ UI state only (not data). Filter, scroll, tab. │ │ Persistence options: ephemeral/session/user/canvas. │ │ │ │ 4. DATA BINDING LAYER │ │ Connects to any compatible data source. │ │ Field mappings, filters, transforms. │ │ │ │ 5. PIPELINE I/O LAYER │ │ Standard event protocol connectors. │ │ Inputs accepted, outputs produced. │ │ │ └─────────────────────────────────────────────────────────────┘
Widget Types
1. View Widgets (Display Data)
Connect to data sources and display them. Examples: Wiki View, Table View, Card Grid.
2. Interactive Widgets (User Input)
Handle user interactions and emit events. Examples: Button, Form, Slider.
3. Container Widgets (Group Others)
Host child widgets with hierarchy. Examples: Room, Panel, Group.
4. AI Widgets (Intelligent Agents)
Have personality, memory, and capabilities. See AI Widget section.
5. Bridge Widgets (External Data)
Connect to external services (Notion, Obsidian, etc.).
Quick Start: Creating an Inline Widget
Step 1: Create the Widget File
Create src/widgets/builtin/MyWidget.ts:
import type { WidgetManifest } from '../../types/manifest';
import type { BuiltinWidget } from './index';
export const MyWidgetManifest: WidgetManifest = {
id: 'stickernest.my-widget', // MUST be lowercase with hyphens
name: 'My Widget',
version: '1.0.0',
kind: 'display', // See widget kinds below
entry: 'index.html',
description: 'Brief description for AI context',
author: 'StickerNest',
tags: ['category', 'feature'],
// === DATA BINDING ===
dataBinding: {
// What shape of data can this widget display?
requiredFields: [
{ role: 'title', types: ['text'], description: 'Main title' },
],
optionalFields: [
{ role: 'image', types: ['asset_ref', 'url'], description: 'Preview image' },
{ role: 'tags', types: ['text[]'], description: 'Categorization' },
],
},
// === PIPELINE I/O ===
inputs: {
'data.set': { type: 'object', description: 'Set display data' },
'filter.apply': { type: 'object', description: 'Apply filter' },
},
outputs: {
'item.selected': { type: 'object', description: 'Item was selected' },
'data.changed': { type: 'object', description: 'Data was modified' },
},
// === STATE PERSISTENCE ===
state: {
persistence: 'user', // 'ephemeral' | 'session' | 'user' | 'canvas'
},
// === CAPABILITIES ===
capabilities: {
draggable: true,
resizable: true,
rotatable: true,
},
// === SIZE ===
size: {
width: 200,
height: 150,
minWidth: 100,
minHeight: 80,
scaleMode: 'scale',
},
};
export const MyWidgetHTML = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
/* === THEME LAYER: Use CSS tokens for all styling === */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
width: 100%;
height: 100%;
overflow: hidden;
font-family: var(--sn-font-family, -apple-system, sans-serif);
}
.container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background: var(--sn-bg-primary, #0f0f19);
color: var(--sn-text-primary, #e2e8f0);
padding: var(--sn-spacing-md, 12px);
border-radius: var(--sn-radius-md, 6px);
}
.title {
font-size: var(--sn-font-size-lg, 1.125rem);
font-weight: var(--sn-font-weight-bold, 600);
color: var(--sn-text-primary, #e2e8f0);
}
.content {
flex: 1;
color: var(--sn-text-secondary, #94a3b8);
}
</style>
</head>
<body>
<div class="container" id="container">
<div class="title" id="title"></div>
<div class="content" id="content"></div>
</div>
<script>
(function() {
const API = window.WidgetAPI;
// === STATE LAYER ===
let uiState = {
filter: null,
selectedItem: null,
};
// === DATA BINDING LAYER ===
let boundData = null;
let fieldMappings = {};
// Initialize from saved state and data binding
API.onMount(function(context) {
// Restore UI state
uiState = context.state || uiState;
// Get data binding configuration
fieldMappings = context.dataBinding?.mappings || {};
// Initial render
render();
API.log('MyWidget mounted');
});
// Handle data binding updates
API.onDataChange(function(data) {
boundData = data;
render();
API.emitOutput('data.changed', { count: data?.length || 0 });
});
// Handle pipeline inputs
API.onInput('filter.apply', function(filter) {
uiState.filter = filter;
API.setState({ filter });
render();
});
// Handle UI state changes from external sources
API.onStateChange(function(newState) {
uiState = { ...uiState, ...newState };
render();
});
// === LOGIC LAYER: Core widget functionality ===
function render() {
if (!boundData) return;
// Map fields using the binding configuration
const title = getField(boundData, fieldMappings.title);
const content = getField(boundData, fieldMappings.content);
document.getElementById('title').textContent = title || '';
document.getElementById('content').textContent = content || '';
}
function getField(data, mapping) {
if (!mapping || !data) return null;
return data[mapping.sourceField];
}
// Handle user interactions
document.getElementById('container').addEventListener('click', function(e) {
if (uiState.selectedItem) {
API.emitOutput('item.selected', { item: uiState.selectedItem });
}
});
// Cleanup
API.onDestroy(function() {
API.log('MyWidget destroyed');
});
})();
</script>
</body>
</html>
`;
export const MyWidget: BuiltinWidget = {
manifest: MyWidgetManifest,
html: MyWidgetHTML,
};
Step 2: Register the Widget
Add to src/widgets/builtin/index.ts:
import { MyWidget } from './MyWidget';
export const BUILTIN_WIDGETS: Record<string, BuiltinWidget> = {
// ... existing widgets
'stickernest.my-widget': MyWidget,
};
export { MyWidget };
Widget Manifest Reference
Required Fields
| Field | Type | Description |
|---|---|---|
id | string | Unique ID: namespace.widget-name (lowercase, hyphens) |
name | string | Display name for UI |
version | string | Semantic version (X.Y.Z) |
kind | WidgetKind | Widget category |
entry | string | Entry file, usually index.html |
inputs | Record | Input port definitions |
outputs | Record | Output port definitions |
capabilities | object | { draggable, resizable, rotatable } |
Widget Classification (v3.1)
Widgets are classified using 3 orthogonal axes:
WidgetDomain (what the widget is FOR)
| Domain | Use Case |
|---|---|
text | Text display, editing, formatting |
media | Images, video, audio players |
time | Timers, clocks, countdowns |
data | Data transforms, charts, storage |
layout | Containers, grids, organization |
ai | AI generation, pipelines, assistants |
canvas | Canvas tools, backgrounds, filters |
social | Social embeds, feeds, collaboration |
spatial | VR, AR, 3D positioning |
games | Game widgets, interactive entertainment |
WidgetBadge (how the widget BEHAVES)
| Badge | Use Case |
|---|---|
input | Accepts data/signals from pipeline |
output | Emits data/signals to pipeline |
control | User interaction (buttons, sliders) |
display | Passive visualization |
generator | Creates/generates content |
system | Infrastructure/internal |
WidgetTechnology (how the widget RENDERS)
| Technology | Use Case |
|---|---|
2d | Standard 2D DOM/CSS rendering |
3d | WebGL/Three.js 3D content |
audio | Audio-focused widget |
video | Video-focused widget |
hybrid | Mixed rendering modes |
Deprecated: Widget Kinds
The following kinds are deprecated but still supported:
| Kind | Use Case |
|---|---|
display | → Use domain + badge: 'display' |
interactive | → Use domain + badge: 'control' |
container | → Use domain: 'layout' |
view | → Use domain: 'data' + badge: 'display' |
ai | → Use domain: 'ai' |
bridge | → Use domain based on bridged service |
2d | → Use technology: '2d' |
3d | → Use technology: '3d' |
audio | → Use technology: 'audio' |
video | → Use technology: 'video' |
Data Binding Configuration
dataBinding: {
// Fields the widget REQUIRES
requiredFields: [
{ role: 'title', types: ['text'], description: 'The main title' },
{ role: 'content', types: ['text', 'richtext'], description: 'Body content' },
],
// Fields the widget CAN USE but doesn't require
optionalFields: [
{ role: 'image', types: ['asset_ref', 'url'], description: 'Preview image' },
{ role: 'tags', types: ['text[]'], description: 'Tags for filtering' },
{ role: 'date', types: ['date', 'datetime'], description: 'Timestamp' },
],
// What features this widget supports when bound to data
features: ['search', 'filter', 'sort', 'edit_inline'],
}
State Persistence Configuration
state: {
// Where UI state is saved
persistence: 'user', // Options:
// 'ephemeral' - resets on page load
// 'session' - saved for this visit
// 'user' - saved per user (their preference)
// 'canvas' - saved for this canvas (all users see same)
}
WidgetAPI Reference
Lifecycle
API.onMount(callback) // Called when widget loads API.onDestroy(callback) // Called before widget unloads
Data Binding
API.onDataChange(callback) // Called when bound data changes API.updateData(changes) // Push changes back to data source API.getFieldMapping(role) // Get field mapping for a role
State Management
API.setState({ key: value }) // Save UI state
API.onStateChange(callback) // Listen for state changes
Pipeline I/O
API.onInput('portId', callback) // Listen for pipeline inputs
API.emitOutput('portId', data) // Emit to pipeline outputs
Events (Broadcast)
API.emit('eventName', payload) // Emit broadcast event
API.on('eventName', callback) // Listen for broadcast events
Theme Tokens
Always use CSS variables for styling. This enables theme/skin swapping:
/* Backgrounds */ --sn-bg-primary: #0f0f19; --sn-bg-secondary: #1a1a2e; --sn-bg-tertiary: #252538; /* Text */ --sn-text-primary: #e2e8f0; --sn-text-secondary: #94a3b8; /* Accents */ --sn-accent-primary: #8b5cf6; --sn-success: #22c55e; --sn-error: #ef4444; --sn-warning: #f59e0b; /* Spacing */ --sn-spacing-xs: 4px; --sn-spacing-sm: 8px; --sn-spacing-md: 12px; --sn-spacing-lg: 16px; /* Typography */ --sn-font-family: -apple-system, BlinkMacSystemFont, sans-serif; --sn-font-size-sm: 0.875rem; --sn-font-size-md: 1rem; --sn-font-size-lg: 1.125rem; /* Borders */ --sn-border-primary: rgba(139, 92, 246, 0.2); --sn-radius-sm: 4px; --sn-radius-md: 6px; --sn-radius-lg: 8px;
Event Protocol (Standard Connectors)
All widgets must follow the event naming convention:
// Event format: domain:action or widget:id:action
"item:selected" // Generic item selection
"filter:changed" // Filter was modified
"data:updated" // Data was updated
"widget:mywidget:ready" // Widget-specific event
// Standard event payloads include:
{
source: 'widget-instance-id',
timestamp: Date.now(),
payload: { /* event-specific data */ }
}
Widget Interchangeability
Widgets of the same functional type should be interchangeable. To ensure this:
- •Use standard event names -
item:selected, notmywidget:itemClicked - •Declare data requirements - So any compatible data source works
- •Use theme tokens - So appearance can be swapped via skins
- •Keep logic generic - An "inventory widget" works with any item-shaped data
Validation Checklist
Before registering a new widget:
- •
idis lowercase with hyphens (e.g.,stickernest.my-widget) - •
versionfollows semver (X.Y.Z) - • All required manifest fields present
- •
dataBindingdeclares required and optional fields - • Input/output ports use standard naming
- • All styling uses CSS variables (no hardcoded colors)
- • State persistence mode is specified
- • Uses
window.WidgetAPIfor all communication - • Has
onMountandonDestroylifecycle handlers - • Events follow naming conventions
- • Added to
BUILTIN_WIDGETSregistry
Reference Files
- •Vision document:
docs/architecture/GAME_ENGINE_CANVAS_VISION.md - •Manifest types:
src/types/manifest.ts - •Widget registry:
src/widgets/builtin/index.ts - •Protocol docs:
Docs/WIDGET-PROTOCOL-SPEC.md - •Theme tokens: See
theming-with-tokensskill - •Pipeline I/O: See
connecting-widget-pipelinesskill