@lupinum/vue-board
Vue 3 components for rendering interactive boards.
Install
pnpm add @lupinum/vue-board @lupinum/board-coreBoardRoot
The root component that renders the board. It subscribes to the engine, handles all pointer/keyboard events, and provides context to child components and composables.
Props
| Name | Type | Default | Description |
|---|---|---|---|
engine | BoardEngine | Auto-created | The board engine instance. If omitted, a default engine is created internally. |
cullMargin | number | 200 | World-space margin around the viewport for node culling. Nodes outside this margin are not rendered. |
grid | boolean | BoardGridOptions | true | Grid configuration. true uses engine defaults, false hides the grid, or pass an object for fine control. |
selectionToolbar | boolean | true | Show the bundled selection toolbar. |
snapGuides | boolean | true | Show alignment guides during drag and resize. |
boxSelect | boolean | true | Show the bundled box-selection overlay. |
renderers | BoardRendererRegistry | {} | Maps supported JSON Canvas node types to Vue components for custom rendering. |
fallbackRenderer | Component | null | null | Fallback renderer used when no type-specific renderer or slot matches. |
Treat the engine instance as stable after mount. To replace board contents, keep the same engine and load the new document through engine commands such as loadDocument.
Emits
| Event | Payload | Description |
|---|---|---|
ready | engine: BoardEngine | Fired after the root element is mounted and the viewport is measured. |
Slots
default
The default slot renders after the board viewport, on top of the canvas. It receives the engine, runtime state, and debug state.
<BoardRoot :engine="engine">
<template #default="{ engine, state, debugState }">
<MyToolbar :engine="engine" />
<MyMinimap />
</template>
</BoardRoot>| Prop | Type | Description |
|---|---|---|
engine | BoardEngine | The engine instance. |
state | BoardState | Current immutable runtime state. |
debugState | object | Debug info: camera, grid, selection, interaction, visibleNodeCount, last 20 trace entries. |
viewport
Renders inside the viewport transform layer (world-space). Use this for custom overlays that should move with the canvas.
<BoardRoot :engine="engine">
<template #viewport>
<MyWorldOverlay />
</template>
</BoardRoot>node
Fallback slot for node content when no type-specific slot or renderer matches.
<BoardRoot :engine="engine">
<template #node="{ node, selected, editing, beginEdit, commitText }">
<div>{{ node.text }}</div>
</template>
</BoardRoot>| Prop | Type | Description |
|---|---|---|
node | BoardNode | The node being rendered. |
selected | boolean | Whether the node is selected. |
editing | boolean | Whether the node is in text editing mode. |
beginEdit | () => void | Start editing for a text node; otherwise no-op. |
commitText | (text: string) => void | Commit a text-node edit; otherwise no-op. |
node:{type}
Type-specific named slot. Takes precedence over the node slot and renderer registry.
<BoardRoot :engine="engine">
<template #node:file="{ node, selected }">
<img :src="node.file" :alt="node.label" />
</template>
<template #node:text="{ node }">
<p>{{ node.text }}</p>
</template>
</BoardRoot>handle
Custom resize handle slot. Rendered for each of the 8 compass handles when a node is selected.
<BoardRoot :engine="engine">
<template #handle="{ node, handle }">
<div class="my-handle" :data-resize="handle">●</div>
</template>
</BoardRoot>box-select
Custom box selection overlay.
<BoardRoot :engine="engine">
<template #box-select="{ bounds }">
<div class="my-box-select" :style="boxStyle(bounds)" />
</template>
</BoardRoot>CSS custom properties
Set these on .board-root to theme the entire board:
| Property | Default | Description |
|---|---|---|
--board-bg | #fbfbfd | Board UI background token. |
--board-canvas-bg | #f5f6fa | Canvas background color. |
--board-fg | #14161f | Board foreground / text color. |
--board-node-bg | #fff | Node background color. |
--board-node-border | #e4e5ee | Node border color. |
--board-node-color | unset | Resolved border color for a node with node.color. |
--board-node-tint | unset | Resolved tinted background for a node with node.color. |
--board-node-color-soft | unset | Softer resolved color for group borders and subtle UI surfaces. |
--board-node-color-ring | unset | Resolved selection outline color for a node with node.color. |
--board-accent | #6366e8 | Accent color (snap guides, selections). |
--board-edge-color | rgba(91, 96, 117, 0.72) | Default edge color. |
--board-handle-shadow | rgba(9, 14, 28, 0.1) | Shadow on resize handles. |
--board-zoom | (auto) | Set automatically by BoardViewport to the current zoom level. |
/* Dark theme example */
.board-root {
--board-bg: #1e293b;
--board-fg: #f1f5f9;
--board-node-bg: #334155;
--board-node-border: rgba(241, 245, 249, 0.15);
--board-accent: #22d3ee;
}Level of detail (LOD)
BoardRoot automatically adjusts rendering based on screen-space size:
| LOD | Condition | Rendering |
|---|---|---|
full | Selected, or screen size >= 96px | Full node content, handles, custom renderers. |
simple | Screen size 6–96px | Minimal placeholder that preserves color and group shape. |
hidden | Screen size < 6px | Not rendered at all. |
Keyboard shortcuts
BoardRoot handles these keyboard shortcuts out of the box:
| Key | Action |
|---|---|
Escape | Clear selection and end interaction. |
Delete / Backspace | Delete selected nodes. |
Enter | Begin text editing on the selected text node. |
Ctrl/Cmd + A | Select all visible nodes. |
Ctrl/Cmd + D | Duplicate selected nodes. |
Ctrl/Cmd + C | Copy selected nodes. |
Ctrl/Cmd + V | Paste from clipboard. |
Ctrl/Cmd + 0 | Zoom to 100%. |
Ctrl/Cmd + 1 | Zoom to fit all nodes. |
Ctrl/Cmd + Z | Undo (requires history plugin). |
Ctrl/Cmd + Shift + Z / Ctrl/Cmd + Y | Redo (requires history plugin). |
Arrow keys | Nudge selected nodes by grid step. |
Shift + Arrow keys | Nudge by major grid step. |
Alt/Option + drag | Duplicate the current selection and drag the clone. |
Shift + drag | Lock dragging to the dominant axis. |
Space while dragging/resizing | Temporarily bypass snapping while the pointer is active. |
Space + drag from idle | Pan the canvas. |
Middle mouse drag | Pan the canvas. |
Scroll wheel | Pan the canvas. |
Shift + scroll | Pan horizontally. |
Ctrl/Cmd + scroll | Zoom at cursor. |
Space + scroll | Zoom at cursor. |
Double-click empty area | Create a new text node. |
Double-click text node | Begin text editing. |
Pinch (touch) | Zoom in/out. |
BoardSelectionToolbar
Floating toolbar rendered by BoardRoot for selected nodes. It is also exported for custom board shells.
The toolbar appears above the selected node bounds when the board is idle. It hides during node drag, resize, and text editing.
Built-in actions:
| Action | Behavior |
|---|---|
| Remove | Deletes unlocked selected nodes. |
| Set colour | Opens the preset palette and applies the chosen color to every unlocked selected node. |
| Zoom to selection | Fits the selected nodes in the viewport. |
| Edit | Starts text editing for a single selected text node. |
Palette options apply BoardColorPreset IDs, which are one supported form of BoardNode.color.
Node color helpers
@lupinum/vue-board exports the shared color helpers for custom renderers.
import {
BOARD_COLOR_PRESETS,
colorForPreset,
resolveNodeColorStyle,
} from '@lupinum/vue-board'Use resolveNodeColorStyle(node.color) when custom wrappers need the same CSS variables as the built-in renderer.
<template>
<article class="task-card" :style="resolveNodeColorStyle(node.color)">
{{ node.text }}
</article>
</template>resolveNodeColorStyle() returns an empty style object when the node has no color.
BoardViewport
Applies the camera transform (scale + translate) to child content. All children render in world-space coordinates. Used internally by BoardRoot.
CSS
.board-viewport {
position: absolute;
inset: 0;
transform-origin: 0 0;
will-change: transform;
}BoardNode
Renders a single node with selection state, editing support, resize handles, and slot-based content.
Props
| Name | Type | Default | Description |
|---|---|---|---|
node | BoardNode | required | The node data to render. |
selected | boolean | required | Whether the node is currently selected. |
editing | boolean | required | Whether the node is in text editing mode. |
customRenderer | boolean | undefined | Hint that a custom renderer is provided. Falls back to slot detection when omitted. |
Slots
default
Custom node content. When provided, replaces the built-in text display.
| Prop | Type | Description |
|---|---|---|
node | BoardNode | The node data. |
selected | boolean | Selection state. |
editing | boolean | Editing state. |
beginEdit | () => void | Start editing for a text node; otherwise no-op. |
commitText | (text: string) => void | Commit a text-node edit; otherwise no-op. |
handle
Custom resize handle. Rendered for each of the 8 compass directions when the node is selected and not editing or locked.
| Prop | Type | Description |
|---|---|---|
node | BoardNode | The node data. |
handle | ResizeHandle | Which handle ('n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw'). |
CSS classes
| Class | Condition | Description |
|---|---|---|
.board-node | Always | Base node styling. |
.is-selected | selected === true | Selection border and ring styling. |
.is-editing | editing === true | Text editing mode. |
.is-locked | node.locked === true | Locked state hook for custom styling. |
.is-group | node.type === 'group' | Group frame styling. |
Built-in text editing
When no custom renderer is provided and the node type is 'text', BoardNode renders:
- A
<textarea>wheneditingis true (auto-focused, auto-selected) - A
<div>with the content text when not editing
The textarea commits on blur, Cmd/Ctrl + Enter, and cancels on Escape.
BoardNodeHandle
Renders a single resize handle square at the appropriate position on a node.
Props
| Name | Type | Description |
|---|---|---|
handle | ResizeHandle | Which handle to render ('n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw'). |
The default handle uses an 18px hit target and a 7px visible dot, both scaled by 1 / zoom. It uses data-resize attributes for pointer event detection.
BoardGrid
Renders the grid pattern as CSS background gradients. Supports dot, line, cross, and none patterns. Automatically adjusts opacity based on zoom level.
The grid fades at edges when fadeEdges is enabled (via CSS mask-image).
BoardBoxSelect
Renders the box selection rectangle during a box-select interaction. The default overlay uses a solid blue window style for contain-mode selection and a dashed teal crossing style for intersecting selection.
Slots
default
Custom box selection rendering.
| Prop | Type | Description |
|---|---|---|
bounds | Bounds | Screen-space bounds of the selection rectangle. |
mode | 'window' | 'crossing' | Active box-select mode for custom styling. |
BoardSnapGuides
Renders alignment snap guides during drag and resize operations. Converts world-space guide data to screen-space lines using the current camera transform.
- Vertical guides (x-axis) render as 1px wide lines in
--board-snap-guide-color, falling back to--board-accent. - Horizontal guides (y-axis) render as 1px tall lines in
--board-snap-guide-color, falling back to--board-accent.
Types
BoardGridOptions
Presentation-only grid options passed to the grid prop on BoardRoot. Configure size, snapping, thresholds, and pattern through createBoardEngine({ grid }) or engine.updateGridSettings() so rendering and interaction use one canonical value.
interface BoardGridOptions {
visible?: boolean // show/hide the grid (default: true)
minorOpacity?: number // minor grid line opacity (default: 0.14)
majorOpacity?: number // major grid line opacity (default: 0.18)
fadeEdges?: boolean // fade grid at viewport edges (default: true)
}BoardRendererRegistry
Maps supported JSON Canvas node types to Vue components.
type BoardRendererRegistry = Partial<Record<JsonCanvasNodeType, Component>>import TextCard from './TextCard.vue'
import GroupCard from './GroupCard.vue'
const renderers: BoardRendererRegistry = {
text: TextCard,
group: GroupCard,
}