@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,219 @@
1
+ ---
2
+ title: "Custom renderers"
3
+ description: "Register custom Vue components for supported JSON Canvas node types while keeping board interactions intact."
4
+ url: "https://nuxt-board.lupinum.com/docs/build-features/custom-node-renderers"
5
+ route: "/docs/build-features/custom-node-renderers"
6
+ locale: "en"
7
+ section: "Documentation"
8
+ collection: "docs"
9
+ source: "ginko-content"
10
+ ---
11
+
12
+ # Custom renderers
13
+
14
+ > Register custom Vue components for supported JSON Canvas node types while keeping board interactions intact.
15
+
16
+ Custom renderers make nodes look like your product, not the default text box. The board shell still handles positioning, selection, resize handles, and z-ordering — your renderer only draws content.
17
+
18
+ > Component omitted: `renderer-board-demo`.
19
+ > This page contains an interactive or site-specific block that has no agent markdown serializer yet.
20
+
21
+ ## Choose the data shape first
22
+
23
+ Custom renderers do not create new node types or a second node schema. They read
24
+ the same `BoardNode` records as the default renderer.
25
+
26
+ | Need | Use |
27
+ | --- | --- |
28
+ | Editable text cards | `type: 'text'` with `node.text`. |
29
+ | File previews | `type: 'file'` with `node.file` and optional `node.subpath`. |
30
+ | Link previews | `type: 'link'` with `node.url`. |
31
+ | Visual containers | `type: 'group'` with `node.label`, `node.background`, and `node.backgroundStyle`. |
32
+ | Product state that is richer than JSON Canvas | Choose one canonical app model, then derive board nodes from it. |
33
+
34
+ Do not put the same product field in two places. If a workflow record owns
35
+ status and assignee, keep that record as the source of truth and rebuild the
36
+ matching board node fields from it. If the board document is the source of
37
+ truth, keep the data in supported JSON Canvas fields.
38
+
39
+ For app-owned records, use the board node ID as the join key and pass the app
40
+ record through a scoped slot:
41
+
42
+ ```vue
43
+ <script setup lang="ts">
44
+ import { computed, ref } from 'vue'
45
+ import { asNodeId, createBoardEngine } from '@lupinum/board-core'
46
+
47
+ const tasks = ref([{ id: 'task-1', title: 'Qualify lead', status: 'active' }])
48
+ const tasksById = computed(
49
+ () => new Map(tasks.value.map((task) => [task.id, task])),
50
+ )
51
+
52
+ const engine = createBoardEngine()
53
+ engine.createNode({
54
+ id: asNodeId('task-1'),
55
+ type: 'text',
56
+ text: '',
57
+ })
58
+ </script>
59
+
60
+ <template>
61
+ <BoardRoot :engine="engine">
62
+ <template #node:text="{ node, selected }">
63
+ <TaskCard :task="tasksById.get(node.id)" :selected="selected" />
64
+ </template>
65
+ </BoardRoot>
66
+ </template>
67
+ ```
68
+
69
+ Here the task owns `title` and `status`; the board owns position, size,
70
+ selection, and camera state. If the task changes, the renderer updates from the
71
+ task record. If the user drags the card, the engine updates the board layout.
72
+
73
+ ## The renderer registry
74
+
75
+ Map supported node types to Vue components via the `renderers` prop. The first release supports the JSON Canvas node types: `text`, `file`, `link`, and `group`.
76
+
77
+ ```vue
78
+ <script setup lang="ts">
79
+ import { createBoardEngine } from '@lupinum/board-core'
80
+ import type { BoardRendererRegistry } from '@lupinum/vue-board'
81
+ import TextCard from './TextCard.vue'
82
+
83
+ const engine = createBoardEngine()
84
+
85
+ const renderers: BoardRendererRegistry = {
86
+ text: TextCard,
87
+ }
88
+ </script>
89
+
90
+ <template>
91
+ <BoardRoot :engine="engine" :renderers="renderers" style="height: 100vh" />
92
+ </template>
93
+ ```
94
+
95
+ Text nodes without a renderer fall through to the built-in text renderer. Other supported node types fall back to a simple label unless you provide a renderer, slot, or `fallbackRenderer`.
96
+
97
+ ## Writing a custom renderer
98
+
99
+ A renderer is a standard Vue component that receives props from the board:
100
+
101
+ ```vue
102
+ <!-- TextCard.vue -->
103
+ <script setup lang="ts">
104
+ import type { BoardNode } from '@lupinum/board-core'
105
+
106
+ defineProps<{
107
+ node: BoardNode
108
+ selected: boolean
109
+ editing: boolean
110
+ beginEdit: () => void
111
+ commitText: (text: string) => void
112
+ }>()
113
+ </script>
114
+
115
+ <template>
116
+ <div class="w-full h-full">
117
+ {{ node.text }}
118
+ </div>
119
+ </template>
120
+ ```
121
+
122
+ ### Renderer props
123
+
124
+ | Prop | Type | Purpose |
125
+ | --- | --- | --- |
126
+ | `node` | `BoardNode` | The node record: position, size, type, JSON Canvas fields, and metadata. |
127
+ | `selected` | `boolean` | Whether the node is currently selected. |
128
+ | `editing` | `boolean` | Whether this text node is in text editing mode. Always false for other node types. |
129
+ | `beginEdit` | `() => void` | Enter text editing mode for a text node; otherwise a no-op. |
130
+ | `commitText` | `(text: string) => void` | Commit a text node edit; otherwise a no-op. |
131
+
132
+ Your renderer fills the node's bounding box. Use `width: 100%` and `height: 100%`.
133
+
134
+ <tip>
135
+ Use `useBoardEngine()` inside your renderer to access the engine and call commands.
136
+ </tip>
137
+
138
+ ### Editing from a renderer
139
+
140
+ The bundled editing callbacks are deliberately text-only. File, link, and group renderers should keep custom UI state locally and persist supported fields with `engine.updateNode()`.
141
+
142
+ Do not mutate the `node` prop. Call engine commands or the renderer callbacks:
143
+
144
+ ```vue
145
+ <script setup lang="ts">
146
+ import type { BoardNode } from '@lupinum/board-core'
147
+
148
+ const props = defineProps<{
149
+ node: BoardNode
150
+ editing: boolean
151
+ beginEdit: () => void
152
+ commitText: (text: string) => void
153
+ }>()
154
+ </script>
155
+
156
+ <template>
157
+ <button v-if="!editing" type="button" @click="beginEdit">Edit</button>
158
+ <textarea
159
+ v-else
160
+ :value="props.node.text"
161
+ data-editor="true"
162
+ @change="commitText(($event.target as HTMLTextAreaElement).value)"
163
+ />
164
+ </template>
165
+ ```
166
+
167
+ <warning>
168
+ Use real form controls or `contenteditable` elements for keyboard editing, and mark custom editor controls with `data-editor="true"` so board pointer and double-click handlers pass through them.
169
+ </warning>
170
+
171
+ ## Slot-based rendering
172
+
173
+ You can also use named slots on `BoardRoot` instead of (or in addition to) the registry:
174
+
175
+ ```vue
176
+ <BoardRoot :engine="engine">
177
+ <template #node:text="{ node, selected }">
178
+ <article :class="{ selected }">
179
+ {{ node.text }}
180
+ </article>
181
+ </template>
182
+ </BoardRoot>
183
+ ```
184
+
185
+ ### Resolution order
186
+
187
+ 1. `#node:{type}` named slot (highest priority)
188
+ 2. `#node` fallback slot
189
+ 3. `renderers[type]` from the registry
190
+ 4. `fallbackRenderer` prop
191
+ 5. Built-in text renderer (lowest priority)
192
+
193
+ ## Level of detail (LOD)
194
+
195
+ `BoardRoot` assigns one of three LOD levels based on screen-space size:
196
+
197
+ | LOD | Condition | What renders |
198
+ | --- | --- | --- |
199
+ | `full` | Selected, or >= 96px | Your custom renderer. |
200
+ | `simple` | 6–96px | Minimal placeholder with stripe pattern. |
201
+ | `hidden` | < 6px | Nothing (unmounted from DOM). |
202
+
203
+ Selected nodes always render at `full` LOD regardless of size.
204
+
205
+ <collapsible>
206
+ ### Custom resize handles
207
+
208
+ Override resize handle appearance with the `handle` slot:
209
+
210
+ ```vue
211
+ <BoardRoot :engine="engine">
212
+ <template #handle="{ handle }">
213
+ <div class="my-custom-handle" :data-resize="handle" />
214
+ </template>
215
+ </BoardRoot>
216
+ ```
217
+
218
+ The `data-resize` attribute is required — `BoardRoot` uses it to identify which handle was clicked.
219
+ </collapsible>
@@ -0,0 +1,172 @@
1
+ ---
2
+ title: "Group nodes"
3
+ description: "Create groups, assign children, capture nodes by dragging, and keep z-order correct."
4
+ url: "https://nuxt-board.lupinum.com/docs/build-features/groups-and-nesting"
5
+ route: "/docs/build-features/groups-and-nesting"
6
+ locale: "en"
7
+ section: "Documentation"
8
+ collection: "docs"
9
+ source: "ginko-content"
10
+ ---
11
+
12
+ # Group nodes
13
+
14
+ > Create groups, assign children, capture nodes by dragging, and keep z-order correct.
15
+
16
+ Groups are board nodes with `type: 'group'`. Children point at a group through `parentId`, and the engine moves the whole subtree when the group moves.
17
+
18
+ ## Create a group
19
+
20
+ A group stores its display name in `label`. Its visual color lives on the top-level `color` field.
21
+
22
+ ```ts
23
+ const group = engine.createNode({
24
+ type: 'group',
25
+ x: 50,
26
+ y: 50,
27
+ width: 520,
28
+ height: 360,
29
+ color: '6',
30
+ label: 'Group',
31
+ })
32
+ ```
33
+
34
+ The default Vue renderer uses the color for the group border, tinted fill, and label chip.
35
+
36
+ ## Assign children
37
+
38
+ Set `parentId` when creating or updating a node:
39
+
40
+ ```ts
41
+ const card = engine.createNode({
42
+ type: 'text',
43
+ x: 90,
44
+ y: 110,
45
+ width: 260,
46
+ height: 140,
47
+ parentId: group.id,
48
+ text: 'Node',
49
+ })
50
+
51
+ engine.updateNode(card.id, { parentId: undefined })
52
+ ```
53
+
54
+ Clearing `parentId` removes the node from the group.
55
+
56
+ ## Capture nodes by dragging a group
57
+
58
+ When a group is dragged over visible, unlocked nodes, the engine checks final node bounds. A node that is fully contained by the moved group becomes a child of that group.
59
+
60
+ This is the behavior users expect from spatial tools: drop a group over cards, then move the group again and the captured cards follow.
61
+
62
+ ```ts
63
+ engine.select([group.id])
64
+ engine.translateSelectedNodes(320, 0)
65
+ ```
66
+
67
+ `translateSelectedNodes()` uses the same hierarchy logic as pointer dragging.
68
+
69
+ ## Wrap the current selection
70
+
71
+ Use selection helpers to create a group around selected nodes:
72
+
73
+ ```ts
74
+ import { getSelectionBounds, getSelectionNodes } from '@lupinum/board-core'
75
+
76
+ function wrapSelectionInGroup(engine: BoardEngine) {
77
+ const selected = getSelectionNodes(engine)
78
+ const bounds = getSelectionBounds(engine)
79
+
80
+ if (selected.length === 0 || !bounds) {
81
+ return
82
+ }
83
+
84
+ const padding = 24
85
+ const group = engine.createNode({
86
+ type: 'group',
87
+ x: bounds.minX - padding,
88
+ y: bounds.minY - padding,
89
+ width: bounds.maxX - bounds.minX + padding * 2,
90
+ height: bounds.maxY - bounds.minY + padding * 2,
91
+ color: '5',
92
+ label: 'Group',
93
+ select: false,
94
+ })
95
+
96
+ for (const node of selected) {
97
+ engine.updateNode(node.id, { parentId: group.id })
98
+ }
99
+
100
+ engine.sendToBack(group.id)
101
+ engine.select([group.id, ...selected.map((node) => node.id)])
102
+ }
103
+ ```
104
+
105
+ The important steps are creating the group, assigning `parentId`, and sending
106
+ the group behind its children.
107
+
108
+ ## Keep groups behind children
109
+
110
+ Groups should render behind their children. Send the group behind once; the
111
+ engine maintains descendant ordering as hierarchy changes:
112
+
113
+ ```ts
114
+ engine.sendToBack(group.id)
115
+ ```
116
+
117
+ The engine also keeps descendants above their group during drag and front/back commands.
118
+
119
+ ## Nested groups
120
+
121
+ Nested groups work through the same `parentId` field:
122
+
123
+ ```ts
124
+ const outer = engine.createNode({
125
+ type: 'group',
126
+ width: 600,
127
+ height: 420,
128
+ label: 'Group',
129
+ })
130
+
131
+ const inner = engine.createNode({
132
+ type: 'group',
133
+ x: 80,
134
+ y: 80,
135
+ width: 320,
136
+ height: 220,
137
+ parentId: outer.id,
138
+ label: 'Group',
139
+ })
140
+ ```
141
+
142
+ When a node could belong to multiple groups, containment helpers choose the smallest visible group that fully contains the node bounds.
143
+
144
+ ## Custom group rendering
145
+
146
+ Custom renderers receive the same node record as other node types. Read group title from `node.label` and color from `node.color`.
147
+
148
+ ```vue [MyGroupRenderer.vue]
149
+ <script setup lang="ts">
150
+ import type { BoardNode } from '@lupinum/board-core'
151
+
152
+ const props = defineProps<{
153
+ node: BoardNode
154
+ selected: boolean
155
+ }>()
156
+ </script>
157
+
158
+ <template>
159
+ <div class="group-shell" :class="{ 'is-selected': selected }">
160
+ <span>{{ props.node.label ?? 'Untitled group' }}</span>
161
+ </div>
162
+ </template>
163
+ ```
164
+
165
+ Use CSS variables from the board shell when you want your renderer to match the built-in color system:
166
+
167
+ ```css
168
+ .group-shell {
169
+ border: 2px solid var(--board-node-color-soft, var(--board-group-border));
170
+ background: var(--board-node-tint, var(--board-group-bg));
171
+ }
172
+ ```
@@ -0,0 +1,76 @@
1
+ ---
2
+ title: "Performance"
3
+ description: "Viewport culling, level of detail, batching, and rendering habits for large boards."
4
+ url: "https://nuxt-board.lupinum.com/docs/build-features/performance"
5
+ route: "/docs/build-features/performance"
6
+ locale: "en"
7
+ section: "Documentation"
8
+ collection: "docs"
9
+ source: "ginko-content"
10
+ ---
11
+
12
+ # Performance
13
+
14
+ > Viewport culling, level of detail, batching, and rendering habits for large boards.
15
+
16
+ Nuxt Board keeps large boards usable with viewport culling, level of detail, requestAnimationFrame pointer updates, and granular subscriptions. Your code still needs to batch bulk operations and avoid deep-reactive engine wrappers.
17
+
18
+ ## Viewport culling
19
+
20
+ `BoardRoot` only renders nodes within the viewport plus a configurable margin. Nodes outside are completely unmounted from the DOM:
21
+
22
+ ```vue
23
+ <BoardRoot :engine="engine" :cull-margin="200" />
24
+ ```
25
+
26
+ Increase `cull-margin` if nodes appear too late during fast panning. Decrease it when DOM count matters more than early rendering.
27
+
28
+ ## Level of detail (LOD)
29
+
30
+ | LOD | Condition | What renders |
31
+ | --- | --- | --- |
32
+ | `full` | Selected, or >= 96px on screen | Full custom renderer. |
33
+ | `simple` | 6–96px | Lightweight placeholder that preserves node color and group shape. |
34
+ | `hidden` | < 6px | Nothing (unmounted). |
35
+
36
+ Selected nodes always render at `full` detail. Zoomed-out boards keep colored borders and fills so users can still scan structure without mounting every custom renderer.
37
+
38
+ ## Batching
39
+
40
+ Use `engine.batch()` when creating many nodes at once:
41
+
42
+ ```ts
43
+ engine.batch(() => {
44
+ for (let i = 0; i < 100; i++) {
45
+ engine.createNode({
46
+ type: 'text',
47
+ x: i * 260,
48
+ y: 0,
49
+ text: `Node ${i}`,
50
+ })
51
+ }
52
+ // One notification instead of 100
53
+ })
54
+ ```
55
+
56
+ ## Best practices
57
+
58
+ Do not wrap the engine in `reactive()`. The engine manages its own reactivity through subscribables. Use `shallowRef` if you need to store the engine in Vue state.
59
+
60
+ Prefer composables such as `useBoardCamera()` and `useBoardNodes()` over manual subscriptions. They keep component dependencies narrow.
61
+
62
+ Keep node updates immutable. Pass a fresh patch to `engine.updateNode()` instead of mutating objects returned from snapshots.
63
+
64
+ Batch bulk operations with `engine.batch()` so subscribers receive one coherent update.
65
+
66
+ <collapsible>
67
+ ### RAF throttling
68
+
69
+ `BoardRoot` throttles pointer projection updates to one internal adapter call per animation frame. Applications do not drive the pointer state machine directly.
70
+ </collapsible>
71
+
72
+ <collapsible>
73
+ ### Subscribable granularity
74
+
75
+ The five subscribables (`$camera`, `$nodes`, `$selection`, `$interaction`, `$snapGuides`) fire independently. A camera change does not trigger a `$nodes` notification. This prevents cascading re-renders in Vue's reactivity system.
76
+ </collapsible>
@@ -0,0 +1,52 @@
1
+ ---
2
+ title: "First-party features and command guards"
3
+ description: "Install first-party packages and block commands with command guards."
4
+ url: "https://nuxt-board.lupinum.com/docs/build-features/read-only-and-command-guards"
5
+ route: "/docs/build-features/read-only-and-command-guards"
6
+ locale: "en"
7
+ section: "Documentation"
8
+ collection: "docs"
9
+ source: "ginko-content"
10
+ ---
11
+
12
+ # First-party features and command guards
13
+
14
+ > Install first-party packages and block commands with command guards.
15
+
16
+ First-party features are package infrastructure for this workspace. They are how
17
+ `@lupinum/board-history` and `@lupinum/board-connections` attach state, events,
18
+ and persistence to the core engine.
19
+
20
+ ```ts
21
+ import { createBoardEngine } from '@lupinum/board-core'
22
+ import { connectionsPlugin } from '@lupinum/board-connections'
23
+ import { historyPlugin } from '@lupinum/board-history'
24
+
25
+ const engine = createBoardEngine({
26
+ plugins: [historyPlugin(), connectionsPlugin({ routing: 'step' })],
27
+ })
28
+ ```
29
+
30
+ This is not a third-party plugin surface. The supported consumer API is the
31
+ package API exposed by each first-party package, such as `engine.plugins.history` and
32
+ `engine.plugins.connections`.
33
+
34
+ > Component omitted: `read-only-toggle-demo`.
35
+ > This page contains an interactive or site-specific block that has no agent markdown serializer yet.
36
+
37
+ ## Command guards
38
+
39
+ `addCommandGuard` registers a command guard. Use it when an app needs read-only
40
+ mode, audit logging, or one concrete product policy.
41
+
42
+ ```ts
43
+ engine.addCommandGuard(({ name }) =>
44
+ name === 'deleteSelected' ? 'Deleting nodes is disabled.' : true,
45
+ )
46
+ ```
47
+
48
+ <warning>
49
+ If a guard returns a reason, the board state does not change and the command
50
+ throws `CommandBlockedError` with that reason. Listen to `command:blocked` when
51
+ the UI also needs a general blocked-command signal.
52
+ </warning>
@@ -0,0 +1,142 @@
1
+ ---
2
+ title: "Serialization"
3
+ description: "Export and import boards with the engine's JSON Canvas document API."
4
+ url: "https://nuxt-board.lupinum.com/docs/build-features/save-and-load"
5
+ route: "/docs/build-features/save-and-load"
6
+ locale: "en"
7
+ section: "Documentation"
8
+ collection: "docs"
9
+ source: "ginko-content"
10
+ ---
11
+
12
+ # Serialization
13
+
14
+ > Export and import boards with the engine's JSON Canvas document API.
15
+
16
+ Save and load boards as JSON Canvas documents. Runtime snapshots are for rendering and tests; persisted documents go through `engine.exportDocument()` and `engine.loadDocument()`.
17
+
18
+ > Component omitted: `json-import-export-demo`.
19
+ > This page contains an interactive or site-specific block that has no agent markdown serializer yet.
20
+
21
+ ## What to persist
22
+
23
+ Persist the object returned by `engine.exportDocument()`. Serialize it when your storage requires a string. Do not persist
24
+ `getState()`, Vue refs, or DOM state.
25
+
26
+ | Data | Persisted by `exportDocument()` |
27
+ | --- | --- |
28
+ | JSON Canvas nodes | Yes |
29
+ | JSON Canvas edges | Yes, when connections are installed |
30
+ | Camera and grid settings | Yes, under `x-lupinum-board` |
31
+ | Selection | Yes, under `x-lupinum-board` |
32
+ | Z-order, lock state, visibility | Yes, under `x-lupinum-board` |
33
+ | Active pointer/text interaction | No |
34
+ | Runtime snap guides | No |
35
+
36
+ ## Quick export/import
37
+
38
+ The engine exports the canonical persisted document shape:
39
+
40
+ ```ts
41
+ // Export
42
+ const document = engine.exportDocument()
43
+ localStorage.setItem('board', JSON.stringify(document))
44
+
45
+ // Import
46
+ const stored = localStorage.getItem('board')
47
+ if (stored) {
48
+ engine.loadDocument(JSON.parse(stored), { mode: 'replace' })
49
+ }
50
+ ```
51
+
52
+ `loadDocument()` validates and clones imported data before it changes the board.
53
+ To keep imports safe on the browser main thread, one document supports up to
54
+ 10,000 nodes, 20,000 edges, 10,000 selected node IDs, 64 nested JSON levels,
55
+ 250,000 JSON values, and 8 million string characters. Also limit the raw byte
56
+ size before calling `JSON.parse()` when documents come from an untrusted source.
57
+
58
+ ---
59
+
60
+ ## Connections
61
+
62
+ ```ts
63
+ const engine = createBoardEngine({
64
+ plugins: [connectionsPlugin()],
65
+ })
66
+
67
+ const document = engine.exportDocument()
68
+ engine.loadDocument(document, { mode: 'replace' })
69
+ ```
70
+
71
+ Install the same first-party plugins before importing documents that use them.
72
+ When the connections plugin is installed, it owns edge export and import. Core
73
+ validates the JSON Canvas document and passes plugin-owned fields to installed
74
+ plugins; core does not persist hidden connection state by itself.
75
+
76
+ ### Node colors
77
+
78
+ Node colors are exported as top-level JSON Canvas node `color` fields:
79
+
80
+ ```json
81
+ {
82
+ "id": "card-1",
83
+ "type": "text",
84
+ "x": 80,
85
+ "y": 80,
86
+ "width": 260,
87
+ "height": 140,
88
+ "color": "5",
89
+ "text": "Draft release notes"
90
+ }
91
+ ```
92
+
93
+ Import accepts valid preset IDs and six-digit hex colors into `BoardNode.color`. Edge colors stay on JSON Canvas edge records owned by the connections package.
94
+
95
+ ---
96
+
97
+ ## The x-lupinum-board extension
98
+
99
+ Board-specific metadata that has no JSON Canvas top-level field is stored under `x-lupinum-board`. Imports still accept the legacy `x-vue-board` key from 0.1 documents. Exports always use the new key.
100
+
101
+ <collapsible>
102
+ ### Document structure
103
+
104
+ ```json
105
+ {
106
+ "nodes": [...],
107
+ "edges": [...],
108
+ "x-lupinum-board": {
109
+ "camera": { "x": 0, "y": 0, "z": 1 },
110
+ "grid": { "size": 20, "majorEvery": 5, "snap": true, "pattern": "line" },
111
+ "nextZIndex": 5,
112
+ "nodes": {
113
+ "node-1": { "zIndex": 1, "locked": false, "visible": true },
114
+ "node-2": { "zIndex": 2, "parentId": "group-1" }
115
+ }
116
+ }
117
+ }
118
+ ```
119
+ </collapsible>
120
+
121
+ ---
122
+
123
+ ## Import modes
124
+
125
+ ```ts
126
+ // Replace all state
127
+ engine.loadDocument(document, { mode: 'replace' })
128
+
129
+ // Merge into existing board
130
+ engine.loadDocument(document, { mode: 'merge' })
131
+ ```
132
+
133
+ | Mode | Use when |
134
+ | --- | --- |
135
+ | `'replace'` | Loading a saved board document or resetting the whole scene. |
136
+ | `'merge'` | Importing another document into the current board without clearing it. |
137
+
138
+ `loadDocument()` validates the document. Invalid node fields, invalid node colors,
139
+ missing edge endpoints, or unsupported plugin-owned edge data fail the import
140
+ instead of silently creating a partial board.
141
+
142
+ ---