@lupinum/board-core 1.0.0-beta.3 → 1.0.0-beta.5

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 +572 -0
  5. package/dist/agent/pages/docs/build-features/custom-node-renderers.md +583 -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 +328 -0
  9. package/dist/agent/pages/docs/build-features/save-and-load.md +404 -0
  10. package/dist/agent/pages/docs/build-features/selection-and-keyboard.md +400 -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 +385 -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 +490 -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 +435 -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 +550 -0
  31. package/dist/agent/pages/docs/solutions/nuxt-application.md +259 -0
  32. package/dist/agent/pages/docs/solutions/planning-board.md +330 -0
  33. package/dist/agent/pages/docs/solutions/read-only-viewer.md +337 -0
  34. package/dist/agent/pages/docs/solutions/workflow-builder.md +557 -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 +417 -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 +485 -0
  41. package/dist/agent/pages/docs/understand-the-system/document-and-session-state.md +371 -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 +468 -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,572 @@
1
+ ---
2
+ title: "Connections"
3
+ description: "Add edges between nodes with the connections plugin — anchors, routing styles, and the SVG connection layer."
4
+ url: "https://nuxt-board.lupinum.com/docs/build-features/connections"
5
+ route: "/docs/build-features/connections"
6
+ locale: "en"
7
+ section: "Documentation"
8
+ collection: "docs"
9
+ source: "ginko-content"
10
+ ---
11
+
12
+ # Connections
13
+
14
+ > Add edges between nodes with the connections plugin — anchors, routing styles, and the SVG connection layer.
15
+
16
+ Connections draw edges between nodes. Install the plugin, create edges, and the connection layer renders them as SVG paths with five routing styles.
17
+
18
+ ## Live example source
19
+
20
+ Use the installation guide to register @lupinum/nuxt-board. These are the actual documentation demo files. Keep their paths under app/. The module auto-imports Board components; install the optional packages named in imports. For the local content components, use components: [{ path: "~/components/content", pathPrefix: false }, "~/components"] in nuxt.config.ts, as this site does, or import them explicitly. createDemoDocument is a local helper, not a package export. DocsLab and the inspector components provide presentation only.
21
+
22
+ ### app/components/demos/ConnectionsBoardDemo.vue
23
+
24
+ ````vue
25
+ <script setup lang="ts">
26
+ import { computed, onMounted, ref } from 'vue'
27
+ import { asNodeId, createBoardEngine } from '@lupinum/board-core'
28
+ import { createDemoDocument } from '../../utils/demoDocument'
29
+ import {
30
+ connectionsPlugin,
31
+ type ConnectionRouting,
32
+ } from '@lupinum/board-connections'
33
+ import { BoardConnectionLayer } from '@lupinum/board-connections/vue'
34
+ import { historyPlugin } from '@lupinum/board-history'
35
+ import { BoardMinimap } from '@lupinum/vue-board/minimap'
36
+
37
+ const engine = createBoardEngine({
38
+ grid: { size: 20, majorEvery: 5, snap: true, pattern: 'dot' },
39
+ plugins: [historyPlugin(), connectionsPlugin()],
40
+ })
41
+
42
+ const historyState = computed(() => engine.plugins.history.getState())
43
+ const routing = ref<ConnectionRouting>('bezier')
44
+ const INPUT_ID = asNodeId('input')
45
+ const PARSE_ID = asNodeId('parse')
46
+ const SCORE_ID = asNodeId('score')
47
+ const OUTPUT_ID = asNodeId('output')
48
+ let shuffleStep = 0
49
+
50
+ function seed() {
51
+ engine.loadDocument(
52
+ createDemoDocument({
53
+ camera: { x: -80, y: -40, z: 1 },
54
+ grid: engine.getGridSettings(),
55
+ nodes: [
56
+ {
57
+ id: 'input',
58
+ type: 'text',
59
+ x: 80,
60
+ y: 150,
61
+ width: 180,
62
+ height: 96,
63
+ text: 'Collect input',
64
+ zIndex: 1,
65
+ locked: false,
66
+ visible: true,
67
+ },
68
+ {
69
+ id: 'parse',
70
+ type: 'text',
71
+ x: 340,
72
+ y: 80,
73
+ width: 200,
74
+ height: 96,
75
+ text: 'Normalize data',
76
+ zIndex: 2,
77
+ locked: false,
78
+ visible: true,
79
+ },
80
+ {
81
+ id: 'score',
82
+ type: 'text',
83
+ x: 340,
84
+ y: 240,
85
+ width: 200,
86
+ height: 96,
87
+ text: 'Score result',
88
+ zIndex: 3,
89
+ locked: false,
90
+ visible: true,
91
+ },
92
+ {
93
+ id: 'output',
94
+ type: 'text',
95
+ x: 650,
96
+ y: 150,
97
+ width: 180,
98
+ height: 96,
99
+ text: 'Publish output',
100
+ zIndex: 4,
101
+ locked: false,
102
+ visible: true,
103
+ },
104
+ ],
105
+ selection: [],
106
+ nextZIndex: 5,
107
+ }),
108
+ { mode: 'replace' },
109
+ )
110
+ const connections = engine.plugins.connections
111
+ for (const edge of connections.getEdges()) {
112
+ connections.deleteEdge(edge.id)
113
+ }
114
+ connections.createEdge({
115
+ from: INPUT_ID,
116
+ to: PARSE_ID,
117
+ label: 'clean',
118
+ data: {},
119
+ })
120
+ connections.createEdge({
121
+ from: INPUT_ID,
122
+ to: SCORE_ID,
123
+ label: 'rank',
124
+ data: {},
125
+ })
126
+ connections.createEdge({
127
+ from: PARSE_ID,
128
+ to: OUTPUT_ID,
129
+ label: 'emit',
130
+ data: {},
131
+ })
132
+ connections.createEdge({
133
+ from: SCORE_ID,
134
+ to: OUTPUT_ID,
135
+ label: 'merge',
136
+ data: {},
137
+ })
138
+ }
139
+
140
+ function shuffle() {
141
+ shuffleStep = (shuffleStep + 1) % 3
142
+ engine.updateNode(PARSE_ID, {
143
+ x: 320 + shuffleStep * 28,
144
+ y: 60 + shuffleStep * 22,
145
+ })
146
+ engine.updateNode(SCORE_ID, {
147
+ x: 360 - shuffleStep * 20,
148
+ y: 220 + shuffleStep * 18,
149
+ })
150
+ engine.updateNode(OUTPUT_ID, {
151
+ x: 620 + shuffleStep * 26,
152
+ y: 120 + shuffleStep * 24,
153
+ })
154
+ }
155
+
156
+ onMounted(async () => {
157
+ seed()
158
+ await engine.zoomToFit(84, false)
159
+ })
160
+ </script>
161
+
162
+ <template>
163
+ <div class="demo-frame">
164
+ <div class="demo-toolbar">
165
+ <button class="demo-danger" @click="seed">Reset</button>
166
+ <button @click="shuffle">Shuffle</button>
167
+ <span class="demo-toggle-group">
168
+ <button
169
+ :class="{ active: routing === 'bezier' }"
170
+ @click="routing = 'bezier'"
171
+ >
172
+ Bezier
173
+ </button>
174
+ <button
175
+ :class="{ active: routing === 'smooth-step' }"
176
+ @click="routing = 'smooth-step'"
177
+ >
178
+ Smooth
179
+ </button>
180
+ <button
181
+ :class="{ active: routing === 'step' }"
182
+ @click="routing = 'step'"
183
+ >
184
+ Step
185
+ </button>
186
+ <button
187
+ :class="{ active: routing === 'straight' }"
188
+ @click="routing = 'straight'"
189
+ >
190
+ Straight
191
+ </button>
192
+ </span>
193
+ <button
194
+ :disabled="!engine.plugins.history.canUndo()"
195
+ @click="engine.plugins.history.undo()"
196
+ >
197
+ Undo
198
+ </button>
199
+ <button
200
+ :disabled="!engine.plugins.history.canRedo()"
201
+ @click="engine.plugins.history.redo()"
202
+ >
203
+ Redo
204
+ </button>
205
+ <span class="demo-history-badge ml-auto"
206
+ >{{ historyState.undoDepth }} undo /
207
+ {{ historyState.redoDepth }} redo</span
208
+ >
209
+ </div>
210
+
211
+ <div class="relative">
212
+ <BoardRoot :engine="engine" style="height: 360px">
213
+ <BoardConnectionLayer :routing="routing" />
214
+ </BoardRoot>
215
+ <div
216
+ class="absolute right-4 bottom-4 rounded-md border border-default bg-elevated/90 p-2 shadow-lg backdrop-blur"
217
+ >
218
+ <BoardMinimap :engine="engine" :width="180" :height="110" />
219
+ </div>
220
+ </div>
221
+ </div>
222
+ </template>
223
+ ````
224
+
225
+ ### app/utils/demoDocument.ts
226
+
227
+ ````ts
228
+ import type {
229
+ BoardNode,
230
+ BoardNodeMetadata,
231
+ Camera,
232
+ GridSettings,
233
+ JsonCanvasDocument,
234
+ JsonCanvasNode,
235
+ NodeId,
236
+ } from '@lupinum/board-core'
237
+
238
+ type DemoNode = Omit<
239
+ Pick<
240
+ BoardNode,
241
+ | 'id'
242
+ | 'type'
243
+ | 'x'
244
+ | 'y'
245
+ | 'width'
246
+ | 'height'
247
+ | 'color'
248
+ | 'text'
249
+ | 'file'
250
+ | 'subpath'
251
+ | 'url'
252
+ | 'label'
253
+ | 'background'
254
+ | 'backgroundStyle'
255
+ | 'zIndex'
256
+ | 'locked'
257
+ | 'visible'
258
+ | 'parentId'
259
+ >,
260
+ 'id' | 'parentId'
261
+ > & {
262
+ id: NodeId | string
263
+ parentId?: NodeId | string
264
+ }
265
+
266
+ export interface DemoDocumentInput {
267
+ nodes: readonly DemoNode[]
268
+ camera?: Camera
269
+ grid?: GridSettings
270
+ selection?: readonly (NodeId | string)[]
271
+ nextZIndex?: number
272
+ }
273
+
274
+ function toJsonCanvasNode(node: DemoNode): JsonCanvasNode {
275
+ const base = {
276
+ id: node.id as NodeId,
277
+ type: node.type,
278
+ x: node.x,
279
+ y: node.y,
280
+ width: node.width,
281
+ height: node.height,
282
+ ...(node.color ? { color: node.color } : {}),
283
+ }
284
+
285
+ switch (node.type) {
286
+ case 'file':
287
+ return {
288
+ ...base,
289
+ type: 'file',
290
+ file: node.file ?? '',
291
+ ...(node.subpath ? { subpath: node.subpath } : {}),
292
+ }
293
+ case 'link':
294
+ return {
295
+ ...base,
296
+ type: 'link',
297
+ url: node.url ?? '',
298
+ }
299
+ case 'group':
300
+ return {
301
+ ...base,
302
+ type: 'group',
303
+ ...((node.label ?? node.text)
304
+ ? { label: node.label ?? node.text }
305
+ : {}),
306
+ ...(node.background ? { background: node.background } : {}),
307
+ ...(node.backgroundStyle
308
+ ? { backgroundStyle: node.backgroundStyle }
309
+ : {}),
310
+ }
311
+ case 'text':
312
+ default:
313
+ return {
314
+ ...base,
315
+ type: 'text',
316
+ text: node.text ?? '',
317
+ }
318
+ }
319
+ }
320
+
321
+ function toNodeMetadata(node: DemoNode): BoardNodeMetadata {
322
+ return {
323
+ zIndex: node.zIndex,
324
+ locked: node.locked,
325
+ visible: node.visible,
326
+ ...(node.parentId ? { parentId: node.parentId as NodeId } : {}),
327
+ }
328
+ }
329
+
330
+ export function createDemoDocument(
331
+ input: DemoDocumentInput,
332
+ ): JsonCanvasDocument {
333
+ return {
334
+ nodes: input.nodes.map(toJsonCanvasNode),
335
+ 'x-lupinum-board': {
336
+ ...(input.camera ? { camera: input.camera } : {}),
337
+ ...(input.grid ? { grid: input.grid } : {}),
338
+ ...(input.selection
339
+ ? { selection: input.selection.map((id) => id as NodeId) }
340
+ : {}),
341
+ nextZIndex:
342
+ input.nextZIndex ??
343
+ input.nodes.reduce((max, node) => Math.max(max, node.zIndex), 0) + 1,
344
+ nodes: Object.fromEntries(
345
+ input.nodes.map((node) => [node.id, toNodeMetadata(node)]),
346
+ ),
347
+ },
348
+ }
349
+ }
350
+ ````
351
+
352
+ ## Setup
353
+
354
+ Install the package when your board needs edges:
355
+
356
+ ```bash
357
+ pnpm add @lupinum/board-connections
358
+ ```
359
+
360
+ Then install the connections plugin during engine creation and render the SVG
361
+ layer under `BoardRoot`:
362
+
363
+ ```ts
364
+ import { createBoardEngine } from '@lupinum/board-core'
365
+ import { connectionsPlugin } from '@lupinum/board-connections'
366
+ import { BoardConnectionLayer } from '@lupinum/board-connections/vue'
367
+
368
+ const engine = createBoardEngine({
369
+ plugins: [connectionsPlugin({ routing: 'bezier' })],
370
+ })
371
+ ```
372
+
373
+ ```vue
374
+ <template>
375
+ <BoardRoot :engine="engine" style="height: 100vh">
376
+ <BoardConnectionLayer />
377
+ </BoardRoot>
378
+ </template>
379
+ ```
380
+
381
+ Both pieces are required: the plugin owns edge state, and
382
+ `BoardConnectionLayer` renders and edits that state.
383
+
384
+ ---
385
+
386
+ ## Creating edges
387
+
388
+ Create edges after the endpoint nodes exist:
389
+
390
+ ```ts
391
+ const edge = engine.plugins.connections.createEdge({
392
+ from: sourceNodeId,
393
+ to: targetNodeId,
394
+ label: 'depends on',
395
+ data: {},
396
+ })
397
+ ```
398
+
399
+ For a complete minimal scene:
400
+
401
+ ```ts
402
+ const source = engine.createNode({
403
+ type: 'text',
404
+ x: 80,
405
+ y: 100,
406
+ text: 'Source',
407
+ })
408
+
409
+ const target = engine.createNode({
410
+ type: 'text',
411
+ x: 420,
412
+ y: 100,
413
+ text: 'Target',
414
+ })
415
+
416
+ engine.plugins.connections.createEdge({
417
+ from: source.id,
418
+ to: target.id,
419
+ label: 'depends on',
420
+ data: {},
421
+ })
422
+ ```
423
+
424
+ ### With anchors
425
+
426
+ Specify where the edge attaches on each node:
427
+
428
+ ```ts
429
+ engine.plugins.connections.createEdge({
430
+ from: nodeA,
431
+ to: nodeB,
432
+ fromAnchor: { side: 'right', offset: 0.5 }, // middle of right edge
433
+ toAnchor: { side: 'left', offset: 0.5 }, // middle of left edge
434
+ data: {},
435
+ })
436
+ ```
437
+
438
+ Anchor `offset` is 0–1 along the side (0.5 = center). If omitted, the connection layer auto-picks the best side from node geometry, keeps it stable while nodes move, and attaches at the center of that side.
439
+
440
+ Use non-center offsets for precise manual anchors:
441
+
442
+ ```ts
443
+ engine.plugins.connections.createEdge({
444
+ from: nodeA,
445
+ to: nodeB,
446
+ fromAnchor: { side: 'bottom', offset: 0.2 },
447
+ toAnchor: { side: 'top', offset: 0.8 },
448
+ data: {},
449
+ })
450
+ ```
451
+
452
+ ---
453
+
454
+ ## Querying edges
455
+
456
+ ```ts
457
+ // All edges
458
+ const edges = engine.plugins.connections.getEdges()
459
+
460
+ // Edges from a specific node
461
+ const outgoing = engine.plugins.connections.getEdgesFrom(nodeId)
462
+
463
+ // Edges to a specific node
464
+ const incoming = engine.plugins.connections.getEdgesTo(nodeId)
465
+
466
+ // Edges between two nodes
467
+ const between = engine.plugins.connections.getEdgesBetween(nodeA, nodeB)
468
+
469
+ // A specific edge
470
+ const edge = engine.plugins.connections.getEdge(edgeId)
471
+ ```
472
+
473
+ ---
474
+
475
+ ## Updating edges
476
+
477
+ Reconnect or restyle an existing edge with `updateEdge()`:
478
+
479
+ ```ts
480
+ engine.plugins.connections.updateEdge(edgeId, {
481
+ to: otherNodeId,
482
+ toAnchor: undefined, // clear explicit anchor so the new node side auto-resolves
483
+ })
484
+ ```
485
+
486
+ The connection layer uses the same API internally when you drag an endpoint handle to another node.
487
+
488
+ ---
489
+
490
+ ## Deleting edges
491
+
492
+ ```ts
493
+ engine.plugins.connections.deleteEdge(edgeId)
494
+ ```
495
+
496
+ <note>
497
+ Edges are also automatically deleted when either endpoint node is removed.
498
+ </note>
499
+
500
+ ---
501
+
502
+ ## Rendering with BoardConnectionLayer
503
+
504
+ Render `BoardConnectionLayer` anywhere under `BoardRoot`. It uses the board context, teleports its SVG layer to the root element, and applies the camera transform itself:
505
+
506
+ ```vue
507
+ <script setup lang="ts">
508
+ import { BoardRoot } from '@lupinum/vue-board'
509
+ import { BoardConnectionLayer } from '@lupinum/board-connections/vue'
510
+ </script>
511
+
512
+ <template>
513
+ <BoardRoot :engine="engine" style="height: 100vh">
514
+ <BoardConnectionLayer />
515
+ </BoardRoot>
516
+ </template>
517
+ ```
518
+
519
+ Hover a card edge to reveal a filled connection handle, then drag from it to another card to create a new edge. Dropping a new connection on empty space cancels by default; pass `createNodeForConnection` if your app wants empty-drop creation to create a node and connect it. The callback runs synchronously inside the same board batch as edge creation. A rejected edge rolls back node changes made through that engine; it cannot roll back external effects from the callback. A callback that returns `null` keeps its existing cancellation behavior. Hover an edge or select it to reveal draggable endpoint handles for reconnecting existing edges; dropping a reconnect on empty space cancels it.
520
+
521
+ UI-created edges use automatic endpoints by default. The edge stores no `fromAnchor` or `toAnchor`, so the connection layer keeps choosing the best side as nodes move. Dragging an existing endpoint onto a node side locks that endpoint to the exact side offset under the pointer. Select the edge and use the reset anchor actions to clear manual anchors back to auto. Set `endpointMode="manual"` on `BoardConnectionLayer`, or `connectionsPlugin({ endpointMode: 'manual' })`, when newly created edges should also lock to the side points the user chose.
522
+
523
+ ### Routing styles
524
+
525
+ Toggle the demo above to compare routing styles. Set the routing on `BoardConnectionLayer`:
526
+
527
+ ```vue
528
+ <BoardConnectionLayer routing="step" />
529
+ ```
530
+
531
+ | Style | Purpose |
532
+ | --- | --- |
533
+ | `bezier` (default) | Smooth cubic bezier curve. Best for organic-looking graphs. |
534
+ | `smooth-step` | Rounded orthogonal connector. Clean and readable. |
535
+ | `step` | Right-angle stepped path. Good for pipeline and workflow layouts. |
536
+ | `straight` | Direct straight line. Simple and lightweight. |
537
+ | `arc` | Curved arc route for sketch-style or hand-drawn edge rendering. |
538
+
539
+ ### Custom edge rendering
540
+
541
+ Use the `edge` slot for full control over edge appearance:
542
+
543
+ ```vue
544
+ <BoardConnectionLayer>
545
+ <template #edge="{ edge, route }">
546
+ <path :d="route.path" stroke="#3b82f6" stroke-width="2" fill="none" />
547
+ <text :x="route.labelPoint.x" :y="route.labelPoint.y" text-anchor="middle">
548
+ {{ edge.label }}
549
+ </text>
550
+ </template>
551
+ </BoardConnectionLayer>
552
+ ```
553
+
554
+ ---
555
+
556
+ ## Events
557
+
558
+ ```ts
559
+ engine.on('edge:created', (edge) => {
560
+ console.log('Edge created:', edge.from, '→', edge.to)
561
+ })
562
+
563
+ engine.on('edge:updated', (edge, prev) => {
564
+ console.log('Edge moved from', prev.to, 'to', edge.to)
565
+ })
566
+
567
+ engine.on('edge:deleted', (edgeId) => {
568
+ console.log('Edge deleted:', edgeId)
569
+ })
570
+ ```
571
+
572
+ ---