@lupinum/board-core 1.0.0-beta.2 → 1.0.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +24 -0
  2. package/dist/agent/AGENTS.md +63 -0
  3. package/dist/agent/manifest.json +320 -0
  4. package/dist/agent/pages/docs/build-features/connections.md +241 -0
  5. package/dist/agent/pages/docs/build-features/custom-node-renderers.md +219 -0
  6. package/dist/agent/pages/docs/build-features/groups-and-nesting.md +172 -0
  7. package/dist/agent/pages/docs/build-features/performance.md +76 -0
  8. package/dist/agent/pages/docs/build-features/read-only-and-command-guards.md +52 -0
  9. package/dist/agent/pages/docs/build-features/save-and-load.md +142 -0
  10. package/dist/agent/pages/docs/build-features/selection-and-keyboard.md +145 -0
  11. package/dist/agent/pages/docs/build-features/ssr-and-deterministic-state.md +67 -0
  12. package/dist/agent/pages/docs/build-features/theming.md +102 -0
  13. package/dist/agent/pages/docs/build-features/undo-and-redo.md +124 -0
  14. package/dist/agent/pages/docs/evaluate/design-decisions.md +44 -0
  15. package/dist/agent/pages/docs/evaluate/how-nuxt-board-works.md +41 -0
  16. package/dist/agent/pages/docs/evaluate/why-nuxt-board.md +61 -0
  17. package/dist/agent/pages/docs/project/contributing.md +101 -0
  18. package/dist/agent/pages/docs/project/support-and-security.md +40 -0
  19. package/dist/agent/pages/docs/reference/board-core-types.md +82 -0
  20. package/dist/agent/pages/docs/reference/board-core.md +261 -0
  21. package/dist/agent/pages/docs/reference/connections.md +531 -0
  22. package/dist/agent/pages/docs/reference/events-and-errors.md +158 -0
  23. package/dist/agent/pages/docs/reference/glossary.md +58 -0
  24. package/dist/agent/pages/docs/reference/history.md +193 -0
  25. package/dist/agent/pages/docs/reference/minimap.md +157 -0
  26. package/dist/agent/pages/docs/reference/nuxt-board.md +148 -0
  27. package/dist/agent/pages/docs/reference/package-overview.md +58 -0
  28. package/dist/agent/pages/docs/reference/vue-board.md +424 -0
  29. package/dist/agent/pages/docs/reference/vue-composables.md +293 -0
  30. package/dist/agent/pages/docs/solutions/mind-map.md +104 -0
  31. package/dist/agent/pages/docs/solutions/nuxt-application.md +47 -0
  32. package/dist/agent/pages/docs/solutions/planning-board.md +51 -0
  33. package/dist/agent/pages/docs/solutions/read-only-viewer.md +61 -0
  34. package/dist/agent/pages/docs/solutions/workflow-builder.md +124 -0
  35. package/dist/agent/pages/docs/start-building/add-connections-and-history.md +42 -0
  36. package/dist/agent/pages/docs/start-building/customize-your-first-node.md +38 -0
  37. package/dist/agent/pages/docs/start-building/installation.md +76 -0
  38. package/dist/agent/pages/docs/start-building/your-first-board.md +52 -0
  39. package/dist/agent/pages/docs/understand-the-system/camera-and-coordinates.md +22 -0
  40. package/dist/agent/pages/docs/understand-the-system/commands-and-transactions.md +31 -0
  41. package/dist/agent/pages/docs/understand-the-system/document-and-session-state.md +25 -0
  42. package/dist/agent/pages/docs/understand-the-system/nodes-and-hierarchy.md +24 -0
  43. package/dist/agent/pages/docs/understand-the-system/packages-and-plugins.md +29 -0
  44. package/dist/agent/pages/docs/understand-the-system/persistence-and-json-canvas.md +25 -0
  45. package/dist/agent/pages/docs/understand-the-system/rendering-and-interaction.md +22 -0
  46. package/dist/agent/pages/docs/understand-the-system/the-engine.md +28 -0
  47. package/dist/agent/pages/docs.md +18 -0
  48. package/dist/engine/transaction.d.ts +2 -2
  49. package/dist/index.js +18 -7
  50. package/dist/types.d.ts +2 -2
  51. package/package.json +3 -2
@@ -0,0 +1,531 @@
1
+ ---
2
+ title: "@lupinum/board-connections"
3
+ description: "Edge and connection management plugin with routing, anchors, and an SVG connection layer."
4
+ url: "https://nuxt-board.lupinum.com/docs/reference/connections"
5
+ route: "/docs/reference/connections"
6
+ locale: "en"
7
+ section: "Documentation"
8
+ collection: "docs"
9
+ source: "ginko-content"
10
+ ---
11
+
12
+ # @lupinum/board-connections
13
+
14
+ > Edge and connection management plugin with routing, anchors, and an SVG connection layer.
15
+
16
+ ## Install
17
+
18
+ <code-group>
19
+ ```bash [pnpm]
20
+ pnpm add @lupinum/board-connections @lupinum/board-core @lupinum/vue-board
21
+ ```
22
+
23
+ ```bash [npm]
24
+ npm install @lupinum/board-connections @lupinum/board-core @lupinum/vue-board
25
+ ```
26
+ </code-group>
27
+
28
+ ## connectionsPlugin
29
+
30
+ Creates the connections plugin. Install it during engine creation via `createBoardEngine({ plugins })`.
31
+
32
+ ```ts
33
+ const plugin = connectionsPlugin({ routing: 'bezier' })
34
+ ```
35
+
36
+ ### Options
37
+
38
+ | Name | Type | Default | Description |
39
+ | --- | --- | --- | --- |
40
+ | `routing` | `ConnectionRouting` | `'bezier'` | Default edge routing style. |
41
+ | `endpointMode` | `'auto' \| 'manual'` | `'auto'` | Whether UI-created endpoints adapt or lock to side anchors. |
42
+ | `defaultArrow` | `'none' \| 'start' \| 'end' \| 'both'` | `'end'` | Default arrowhead placement. |
43
+
44
+ ```ts
45
+ import { createBoardEngine } from '@lupinum/board-core'
46
+ import { connectionsPlugin } from '@lupinum/board-connections'
47
+
48
+ const engine = createBoardEngine({
49
+ plugins: [connectionsPlugin({ routing: 'bezier' })],
50
+ })
51
+ ```
52
+
53
+ ---
54
+
55
+ ## Plugin API
56
+
57
+ After installing the plugin, the connections API is available on `engine.plugins.connections`.
58
+
59
+ ### createEdge
60
+
61
+ Creates a new edge between two nodes. Returns the created edge.
62
+
63
+ ```ts
64
+ createEdge<T>(input: {
65
+ id?: EdgeId
66
+ from: NodeId
67
+ to: NodeId
68
+ fromAnchor?: AnchorPosition
69
+ toAnchor?: AnchorPosition
70
+ fromEnd?: EdgeEnd
71
+ toEnd?: EdgeEnd
72
+ label?: string
73
+ color?: string
74
+ data: T
75
+ zIndex?: number
76
+ }): BoardEdge<T>
77
+ ```
78
+
79
+ ```ts
80
+ const edge = engine.plugins.connections.createEdge({
81
+ from: nodeA,
82
+ to: nodeB,
83
+ label: 'depends on',
84
+ color: '#0f766e',
85
+ data: {},
86
+ })
87
+ ```
88
+
89
+ ### deleteEdge
90
+
91
+ Removes an edge by ID.
92
+
93
+ ```ts
94
+ deleteEdge(id: EdgeId): void
95
+ ```
96
+
97
+ ### updateEdge
98
+
99
+ Updates an existing edge in place. This is the API used by endpoint reconnect interactions.
100
+
101
+ ```ts
102
+ updateEdge<T>(id: EdgeId, patch: BoardEdgePatch<T>): BoardEdge<T>
103
+ ```
104
+
105
+ ```ts
106
+ engine.plugins.connections.updateEdge(edge.id, {
107
+ to: anotherNodeId,
108
+ toAnchor: undefined,
109
+ })
110
+ ```
111
+
112
+ ### getEdge
113
+
114
+ Returns a single edge by ID.
115
+
116
+ ```ts
117
+ getEdge(id: EdgeId): BoardEdge | undefined
118
+ ```
119
+
120
+ ### getEdges
121
+
122
+ Returns all edges.
123
+
124
+ ```ts
125
+ getEdges(): BoardEdge[]
126
+ ```
127
+
128
+ ### getEdgesFrom
129
+
130
+ Returns all edges originating from a node.
131
+
132
+ ```ts
133
+ getEdgesFrom(id: NodeId): BoardEdge[]
134
+ ```
135
+
136
+ ### getEdgesTo
137
+
138
+ Returns all edges pointing to a node.
139
+
140
+ ```ts
141
+ getEdgesTo(id: NodeId): BoardEdge[]
142
+ ```
143
+
144
+ ### getEdgesBetween
145
+
146
+ Returns all directed edges from `from` to `to`.
147
+
148
+ ```ts
149
+ getEdgesBetween(from: NodeId, to: NodeId): BoardEdge[]
150
+ ```
151
+
152
+ ---
153
+
154
+ ## BoardConnectionLayer
155
+
156
+ A Vue component that renders edges as SVG paths inside a `BoardRoot`.
157
+
158
+ ```ts
159
+ import { BoardConnectionLayer } from '@lupinum/board-connections/vue'
160
+ ```
161
+
162
+ ### Props
163
+
164
+ | Name | Type | Default | Description |
165
+ | --- | --- | --- | --- |
166
+ | `routing` | `ConnectionRouting \| undefined` | plugin default | Routing style for all edges. |
167
+ | `endpointMode` | `ConnectionEndpointMode \| undefined` | plugin default | UI endpoint behavior: auto side resolution or manual side locking. |
168
+ | `createNodeForConnection` | `(ctx: CreateNodeForConnectionContext) => BoardNode \| null` | `null` | Optional host policy for creating a node when a new connection is dropped on empty space. |
169
+
170
+ ### Slots
171
+
172
+ #### edge
173
+
174
+ Custom edge rendering. If not provided, edges render as SVG `<path>` elements.
175
+
176
+ | Prop | Type | Description |
177
+ | --- | --- | --- |
178
+ | `edge` | `BoardEdge` | The edge data. |
179
+ | `source` | `ResolvedConnectionEndpoint` | Resolved source endpoint metadata. |
180
+ | `target` | `ResolvedConnectionEndpoint` | Resolved target endpoint metadata. |
181
+ | `route` | `ConnectionRoute` | Routed geometry, bounds, label point, and path string. |
182
+
183
+ ```vue
184
+ <BoardConnectionLayer routing="smooth-step">
185
+ <template #edge="{ edge, route }">
186
+ <path :d="route.path" stroke="blue" stroke-width="2" fill="none" />
187
+ <text :x="route.labelPoint.x" :y="route.labelPoint.y">{{ edge.label }}</text>
188
+ </template>
189
+ </BoardConnectionLayer>
190
+ ```
191
+
192
+ ### Usage
193
+
194
+ Render `BoardConnectionLayer` under `BoardRoot`. It teleports the SVG layer to the board root and applies the camera transform itself:
195
+
196
+ ```vue
197
+ <BoardRoot :engine="engine">
198
+ <BoardConnectionLayer />
199
+ </BoardRoot>
200
+ ```
201
+
202
+ `BoardConnectionLayer` handles both connection creation and reconnect. Hover a card edge to reveal a filled midpoint handle and drag it to another card to create a new edge. Hover or select an existing edge to reveal reconnect handles; dragging either end previews the route live and commits via `updateEdge()` when dropped on another node. Dropping a new connection on empty space cancels unless `createNodeForConnection` returns a node for the layer to connect.
203
+
204
+ By default, UI-created edges use `endpointMode: 'auto'`: they do not store `fromAnchor` or `toAnchor`, so each endpoint resolves to the best node side as nodes move. Dragging an existing endpoint onto a node side stores that endpoint as `{ side, offset }`, where `offset` is the exact point under the pointer. Use `endpointMode: 'manual'` when newly created edges should also lock to the dragged side offsets. Selected manual edges expose reset actions that clear one or both anchors back to auto.
205
+
206
+ ---
207
+
208
+ ## Utility functions
209
+
210
+ ### resolveAnchorPoint
211
+
212
+ Resolves an anchor position to a world-space point on a node.
213
+
214
+ ```ts
215
+ function resolveAnchorPoint(
216
+ node: Pick<BoardNode, 'x' | 'y' | 'width' | 'height'>,
217
+ anchor: AnchorPosition,
218
+ ): Point
219
+ ```
220
+
221
+ ### resolveAutoAnchorSide
222
+
223
+ Chooses the best side for an auto-routed endpoint and uses a deadband to reduce flicker near diagonals. Auto-routed endpoints then attach at the center of that side.
224
+
225
+ ```ts
226
+ function resolveAutoAnchorSide(
227
+ source: Pick<BoardNode, 'x' | 'y' | 'width' | 'height'>,
228
+ target: Pick<BoardNode, 'x' | 'y' | 'width' | 'height'>,
229
+ role: 'source' | 'target',
230
+ previousSide?: AnchorSide,
231
+ ): AnchorSide
232
+ ```
233
+
234
+ ### resolveConnectionEndpoint
235
+
236
+ Resolves one edge endpoint to a node side, normalized side offset, and world-space point. Explicit anchors are preserved; automatic endpoints choose the best side from the paired node.
237
+
238
+ ```ts
239
+ function resolveConnectionEndpoint(
240
+ edge: BoardEdge,
241
+ node: Pick<BoardNode, 'id' | 'x' | 'y' | 'width' | 'height'>,
242
+ otherNode: Pick<BoardNode, 'id' | 'x' | 'y' | 'width' | 'height'>,
243
+ role: 'source' | 'target',
244
+ previousSide?: AnchorSide,
245
+ ): ResolvedConnectionEndpoint
246
+ ```
247
+
248
+ ### buildConnectionRoute
249
+
250
+ Builds a routed connection path from fully resolved source and target endpoints.
251
+
252
+ ```ts
253
+ function buildConnectionRoute(input: {
254
+ source: ResolvedConnectionEndpoint
255
+ target: ResolvedConnectionEndpoint
256
+ routing?: ConnectionRouting
257
+ }): ConnectionRoute
258
+ ```
259
+
260
+ | Routing | Description |
261
+ | --- | --- |
262
+ | `'bezier'` | Smooth cubic bezier curve (default). |
263
+ | `'smooth-step'` | Rounded orthogonal connector. |
264
+ | `'step'` | Right-angle stepped path. |
265
+ | `'straight'` | Direct straight line. |
266
+ | `'arc'` | Curved arc route. |
267
+
268
+ ### buildArcRoute
269
+
270
+ Builds a curved arc route for hand-drawn or sketch-style edge rendering.
271
+
272
+ ```ts
273
+ function buildArcRoute(
274
+ source: ResolvedConnectionEndpoint,
275
+ target: ResolvedConnectionEndpoint,
276
+ options?: ArcOptions,
277
+ ): ConnectionRoute
278
+ ```
279
+
280
+ ### Edge color helpers
281
+
282
+ The package exports preset helpers for edge UI:
283
+
284
+ ```ts
285
+ import {
286
+ EDGE_COLOR_PRESETS,
287
+ colorForPreset,
288
+ presetForColor,
289
+ resolvePresetColor,
290
+ } from '@lupinum/board-connections'
291
+ ```
292
+
293
+ Use these helpers when a toolbar stores edge colors as presets but the renderer needs a CSS color string.
294
+
295
+ ### resolveFloatingEndpoint
296
+
297
+ Builds a temporary endpoint around a free pointer position for reconnect previews.
298
+
299
+ ```ts
300
+ function resolveFloatingEndpoint(
301
+ point: Point,
302
+ otherPoint: Point,
303
+ role: 'source' | 'target',
304
+ previousSide?: AnchorSide,
305
+ ): ResolvedConnectionEndpoint
306
+ ```
307
+
308
+ ### resolveEdgeRenderState
309
+
310
+ Resolves source/target endpoints and the routed path in one step.
311
+
312
+ ```ts
313
+ function resolveEdgeRenderState(
314
+ edge: BoardEdge,
315
+ sourceNode: Pick<BoardNode, 'id' | 'x' | 'y' | 'width' | 'height'>,
316
+ targetNode: Pick<BoardNode, 'id' | 'x' | 'y' | 'width' | 'height'>,
317
+ options?: {
318
+ routing?: ConnectionRouting
319
+ previousSourceSide?: AnchorSide
320
+ previousTargetSide?: AnchorSide
321
+ },
322
+ ): {
323
+ source: ResolvedConnectionEndpoint
324
+ target: ResolvedConnectionEndpoint
325
+ route: ConnectionRoute
326
+ }
327
+ ```
328
+
329
+ ### getVisibleEdges
330
+
331
+ Returns edges whose routed path bounds intersect the given viewport bounds.
332
+
333
+ ```ts
334
+ function getVisibleEdges(
335
+ engine: BoardEngine,
336
+ bounds: Bounds,
337
+ routing?: ConnectionRouting,
338
+ ): BoardEdge[]
339
+ ```
340
+
341
+ ---
342
+
343
+ ## Types
344
+
345
+ ### BoardEdge
346
+
347
+ ```ts
348
+ interface BoardEdge<T = Record<string, unknown>> {
349
+ id: EdgeId
350
+ from: NodeId
351
+ to: NodeId
352
+ fromAnchor?: AnchorPosition // where the edge attaches on the source node
353
+ toAnchor?: AnchorPosition // where the edge attaches on the target node
354
+ fromEnd?: EdgeEnd
355
+ toEnd?: EdgeEnd
356
+ label?: string
357
+ color?: string
358
+ data: T // custom edge payload
359
+ zIndex: number
360
+ }
361
+ ```
362
+
363
+ ### BoardEdgePatch
364
+
365
+ ```ts
366
+ interface BoardEdgePatch<T = Record<string, unknown>> {
367
+ from?: NodeId
368
+ to?: NodeId
369
+ fromAnchor?: AnchorPosition
370
+ toAnchor?: AnchorPosition
371
+ fromEnd?: EdgeEnd
372
+ toEnd?: EdgeEnd
373
+ label?: string
374
+ color?: string
375
+ data?: T
376
+ }
377
+ ```
378
+
379
+ ### AnchorPosition
380
+
381
+ ```ts
382
+ interface AnchorPosition {
383
+ side: AnchorSide // 'top' | 'right' | 'bottom' | 'left'
384
+ offset: number // 0–1 position along the side (0.5 = center)
385
+ }
386
+ ```
387
+
388
+ When `fromAnchor` / `toAnchor` are omitted, the connection layer resolves the side automatically and uses `offset = 0.5`.
389
+
390
+ Set an anchor to `undefined` with `updateEdge()` to reset that endpoint to automatic side resolution:
391
+
392
+ ```ts
393
+ engine.plugins.connections.updateEdge(edgeId, {
394
+ fromAnchor: undefined,
395
+ })
396
+ ```
397
+
398
+ ### AnchorSide
399
+
400
+ ```ts
401
+ type AnchorSide = 'top' | 'right' | 'bottom' | 'left'
402
+ ```
403
+
404
+ ### ConnectionRouting
405
+
406
+ ```ts
407
+ type ConnectionRouting = 'bezier' | 'smooth-step' | 'step' | 'straight' | 'arc'
408
+ ```
409
+
410
+ ### ConnectionEndpointMode
411
+
412
+ ```ts
413
+ type ConnectionEndpointMode = 'auto' | 'manual'
414
+ ```
415
+
416
+ ### EdgeEnd
417
+
418
+ ```ts
419
+ type EdgeEnd = 'none' | 'arrow'
420
+ ```
421
+
422
+ ### ConnectionConfig
423
+
424
+ Resolved defaults installed by `connectionsPlugin()` and returned by `engine.plugins.connections.getConfig()`.
425
+
426
+ ```ts
427
+ interface ConnectionConfig {
428
+ routing: ConnectionRouting
429
+ endpointMode: ConnectionEndpointMode
430
+ defaultArrow: 'none' | 'start' | 'end' | 'both'
431
+ }
432
+ ```
433
+
434
+ ### CreateNodeForConnectionContext
435
+
436
+ Context passed to `BoardConnectionLayer` when a host opts into creating a node from an empty connection drop.
437
+
438
+ ```ts
439
+ interface CreateNodeForConnectionContext {
440
+ sourceNodeId: NodeId
441
+ sourceSide: AnchorSide
442
+ pointerWorld: Point
443
+ candidateAnchor: AnchorPosition | null
444
+ }
445
+ ```
446
+
447
+ ### ResolvedConnectionEndpoint
448
+
449
+ ```ts
450
+ interface ResolvedConnectionEndpoint {
451
+ nodeId: NodeId
452
+ node: Pick<BoardNode, 'id' | 'x' | 'y' | 'width' | 'height'>
453
+ side: AnchorSide
454
+ offset: number
455
+ point: Point
456
+ kind: 'explicit' | 'auto'
457
+ }
458
+ ```
459
+
460
+ ### ConnectionRoute
461
+
462
+ ```ts
463
+ interface ConnectionRoute {
464
+ routing: ConnectionRouting
465
+ path: string
466
+ labelPoint: Point
467
+ bounds: Bounds
468
+ waypoints: Point[]
469
+ segments: ConnectionRouteSegment[]
470
+ }
471
+ ```
472
+
473
+ ### ConnectionRouteSegment
474
+
475
+ A routed connection is made of path segments used by custom edge renderers.
476
+
477
+ ```ts
478
+ type ConnectionRouteSegment =
479
+ | { type: 'line'; from: Point; to: Point }
480
+ | {
481
+ type: 'cubic'
482
+ from: Point
483
+ control1: Point
484
+ control2: Point
485
+ to: Point
486
+ }
487
+ ```
488
+
489
+ ### ConnectionsApi
490
+
491
+ Installed by `connectionsPlugin()` on `engine.plugins.connections`.
492
+
493
+ ```ts
494
+ interface ConnectionsApi {
495
+ createEdge<T>(input: CreateEdgeInput<T>): BoardEdge<T>
496
+ updateEdge<T>(id: EdgeId, patch: BoardEdgePatch<T>): BoardEdge<T>
497
+ deleteEdge(id: EdgeId): void
498
+ getEdge(id: EdgeId): BoardEdge | undefined
499
+ getEdges(): BoardEdge[]
500
+ getEdgesFrom(id: NodeId): BoardEdge[]
501
+ getEdgesTo(id: NodeId): BoardEdge[]
502
+ getEdgesBetween(from: NodeId, to: NodeId): BoardEdge[]
503
+ getConfig(): ConnectionConfig
504
+ }
505
+ ```
506
+
507
+ ---
508
+
509
+ ## Events
510
+
511
+ The connections plugin adds these events to `BoardEventMap`:
512
+
513
+ | Event | Handler | Description |
514
+ | --- | --- | --- |
515
+ | `edge:created` | `(edge: BoardEdge) => void` | An edge was created. |
516
+ | `edge:updated` | `(edge: BoardEdge, prev: BoardEdge) => void` | An edge was updated or reconnected. |
517
+ | `edge:deleted` | `(edgeId: EdgeId) => void` | An edge was removed. |
518
+
519
+ ```ts
520
+ engine.on('edge:created', (edge) => {
521
+ console.log('New edge:', edge.from, '->', edge.to)
522
+ })
523
+
524
+ engine.on('edge:updated', (edge, prev) => {
525
+ console.log('Moved edge:', prev.id, prev.to, '->', edge.to)
526
+ })
527
+ ```
528
+
529
+ Edges are automatically deleted when either endpoint node is deleted.
530
+
531
+ ---
@@ -0,0 +1,158 @@
1
+ ---
2
+ title: "Events and subscriptions"
3
+ description: "React to state changes with the event system and subscribables."
4
+ url: "https://nuxt-board.lupinum.com/docs/reference/events-and-errors"
5
+ route: "/docs/reference/events-and-errors"
6
+ locale: "en"
7
+ section: "Documentation"
8
+ collection: "docs"
9
+ source: "ginko-content"
10
+ ---
11
+
12
+ # Events and subscriptions
13
+
14
+ > React to state changes with the event system and subscribables.
15
+
16
+ Events tell you what happened. Subscribables tell you what state looks like now. Use events to react to discrete actions, use subscribables to keep your UI in sync.
17
+
18
+ > Component omitted: `event-logger`.
19
+ > This page contains an interactive or site-specific block that has no agent markdown serializer yet.
20
+
21
+ ## Events
22
+
23
+ Register a listener with `engine.on()`. It returns an unsubscribe function:
24
+
25
+ ```ts
26
+ const unsub = engine.on('node:created', (node) => {
27
+ console.log('Created', node.id)
28
+ })
29
+
30
+ // Later...
31
+ unsub()
32
+ ```
33
+
34
+ ### Event catalog
35
+
36
+ <accordion>
37
+ <accordion-item title="Lifecycle">
38
+ - `destroy` — engine destroyed
39
+ </accordion-item>
40
+
41
+ <accordion-item title="Node events">
42
+ - `node:created(node)` — a node was added
43
+ - `node:updated(node, previousNode)` — a node's data or geometry changed
44
+ - `node:deleted(nodeId, previousNode)` — a node was removed
45
+ - `node:moved(node, delta)` — a node's position changed
46
+ - `node:resized(node, previousBounds)` — a node's dimensions changed
47
+ </accordion-item>
48
+
49
+ <accordion-item title="Selection and interaction">
50
+ - `selection:change(selectedIds, previousIds)` — selection changed
51
+ - `interaction:start(state)` — an interaction began (drag, resize, pan, etc.)
52
+ - `interaction:update(state)` — ongoing interaction position updated (high-frequency)
53
+ - `interaction:end(state)` — interaction finished
54
+ </accordion-item>
55
+
56
+ <accordion-item title="Camera">
57
+ - `camera:change(camera, previousCamera)` — camera position or zoom changed (high-frequency during pan/zoom)
58
+ - `viewport:change(size, previousSize)` — the measured board root size changed
59
+ </accordion-item>
60
+
61
+ <accordion-item title="Guarded commands">
62
+ - `command:before(commandName, args, metadata)` — a validated command has succeeded and is publishing its lifecycle boundary
63
+ - `command:after(commandName, args, duration, metadata)` — a command finished executing
64
+ - `command:blocked(commandName, args, metadata)` — a command was blocked by a guard
65
+ - `validation:failed(failure)` — commit validation rejected an invalid candidate state
66
+ </accordion-item>
67
+
68
+ <accordion-item title="Feature events">
69
+ Internal features add their own namespaced events:
70
+
71
+ - **Connections:** `edge:created(edge)`, `edge:updated(edge, prev)`, `edge:deleted(edgeId)`
72
+ - **History:** `history:push(entry)`, `history:undo(entry | null)`, `history:redo(entry | null)`, `history:clear()`
73
+ </accordion-item>
74
+ </accordion>
75
+
76
+ ## Subscribables
77
+
78
+ Six subscribables give you reactive access to engine state. Each fires independently — a camera change does not trigger a `$nodes` notification:
79
+
80
+ <tabs>
81
+ <tab label="$nodes" icon="i-lucide-square">
82
+ ```ts
83
+ engine.$nodes.subscribe((nodes) => {
84
+ console.log('Node count:', nodes.size)
85
+ })
86
+ ```
87
+
88
+ Fires when any node is created, updated, moved, resized, or deleted.
89
+ </tab>
90
+
91
+ <tab label="$camera" icon="i-lucide-move">
92
+ ```ts
93
+ engine.$camera.subscribe((camera) => {
94
+ console.log(`Zoom: ${camera.z.toFixed(2)}`)
95
+ })
96
+ ```
97
+
98
+ Fires on every pan and zoom change. High-frequency during interaction.
99
+ </tab>
100
+
101
+ <tab label="$selection" icon="i-lucide-check-square">
102
+ ```ts
103
+ engine.$selection.subscribe((ids) => {
104
+ console.log('Selected:', ids.size)
105
+ })
106
+ ```
107
+
108
+ Fires when the selection set changes.
109
+ </tab>
110
+
111
+ <tab label="$grid" icon="i-lucide-grid-3x3">
112
+ ```ts
113
+ engine.$grid.subscribe((grid) => {
114
+ console.log('Grid size:', grid.size)
115
+ })
116
+ ```
117
+
118
+ Fires when snapping or grid settings change.
119
+ </tab>
120
+
121
+ <tab label="$interaction" icon="i-lucide-hand">
122
+ ```ts
123
+ engine.$interaction.subscribe((state) => {
124
+ console.log('Mode:', state.mode)
125
+ })
126
+ ```
127
+
128
+ Fires when the interaction state machine transitions.
129
+ </tab>
130
+
131
+ <tab label="$snapGuides" icon="i-lucide-ruler">
132
+ ```ts
133
+ engine.$snapGuides.subscribe((guides) => {
134
+ console.log('Active guides:', guides.length)
135
+ })
136
+ ```
137
+
138
+ Fires when snap alignment guides appear or disappear.
139
+ </tab>
140
+ </tabs>
141
+
142
+ <tip>
143
+ In Vue, prefer the composables (`useBoardCamera()`, `useBoardNodes()`, `useBoardSelection()`) over raw subscribables. They handle reactivity automatically.
144
+ </tip>
145
+
146
+ <note>
147
+ Inside `engine.batch()`, entity events and subscribable notifications publish only after the outer batch commits. Command lifecycle events describe the outer `batch` boundary. A blocked command and a validation failure remain observable failure telemetry.
148
+ </note>
149
+
150
+ ## Error taxonomy
151
+
152
+ - `BoardInputError`: malformed or invalid boundary input.
153
+ - `BoardNotFoundError`: requested entity does not exist.
154
+ - `BoardConflictError`: an ID, plugin name, or other unique identity conflicts.
155
+ - `CommandBlockedError`: a command guard rejected the operation.
156
+ - `BoardDestroyedError`: a command targeted a destroyed engine.
157
+
158
+ Listener, subscriber, and finalized commit-effect errors are reported through `onUnhandledError` after commit. They do not reverse committed state.