@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.
- package/README.md +24 -0
- package/dist/agent/AGENTS.md +63 -0
- package/dist/agent/manifest.json +320 -0
- package/dist/agent/pages/docs/build-features/connections.md +241 -0
- package/dist/agent/pages/docs/build-features/custom-node-renderers.md +219 -0
- package/dist/agent/pages/docs/build-features/groups-and-nesting.md +172 -0
- package/dist/agent/pages/docs/build-features/performance.md +76 -0
- package/dist/agent/pages/docs/build-features/read-only-and-command-guards.md +52 -0
- package/dist/agent/pages/docs/build-features/save-and-load.md +142 -0
- package/dist/agent/pages/docs/build-features/selection-and-keyboard.md +145 -0
- package/dist/agent/pages/docs/build-features/ssr-and-deterministic-state.md +67 -0
- package/dist/agent/pages/docs/build-features/theming.md +102 -0
- package/dist/agent/pages/docs/build-features/undo-and-redo.md +124 -0
- package/dist/agent/pages/docs/evaluate/design-decisions.md +44 -0
- package/dist/agent/pages/docs/evaluate/how-nuxt-board-works.md +41 -0
- package/dist/agent/pages/docs/evaluate/why-nuxt-board.md +61 -0
- package/dist/agent/pages/docs/project/contributing.md +101 -0
- package/dist/agent/pages/docs/project/support-and-security.md +40 -0
- package/dist/agent/pages/docs/reference/board-core-types.md +82 -0
- package/dist/agent/pages/docs/reference/board-core.md +261 -0
- package/dist/agent/pages/docs/reference/connections.md +531 -0
- package/dist/agent/pages/docs/reference/events-and-errors.md +158 -0
- package/dist/agent/pages/docs/reference/glossary.md +58 -0
- package/dist/agent/pages/docs/reference/history.md +193 -0
- package/dist/agent/pages/docs/reference/minimap.md +157 -0
- package/dist/agent/pages/docs/reference/nuxt-board.md +148 -0
- package/dist/agent/pages/docs/reference/package-overview.md +58 -0
- package/dist/agent/pages/docs/reference/vue-board.md +424 -0
- package/dist/agent/pages/docs/reference/vue-composables.md +293 -0
- package/dist/agent/pages/docs/solutions/mind-map.md +104 -0
- package/dist/agent/pages/docs/solutions/nuxt-application.md +47 -0
- package/dist/agent/pages/docs/solutions/planning-board.md +51 -0
- package/dist/agent/pages/docs/solutions/read-only-viewer.md +61 -0
- package/dist/agent/pages/docs/solutions/workflow-builder.md +124 -0
- package/dist/agent/pages/docs/start-building/add-connections-and-history.md +42 -0
- package/dist/agent/pages/docs/start-building/customize-your-first-node.md +38 -0
- package/dist/agent/pages/docs/start-building/installation.md +76 -0
- package/dist/agent/pages/docs/start-building/your-first-board.md +52 -0
- package/dist/agent/pages/docs/understand-the-system/camera-and-coordinates.md +22 -0
- package/dist/agent/pages/docs/understand-the-system/commands-and-transactions.md +31 -0
- package/dist/agent/pages/docs/understand-the-system/document-and-session-state.md +25 -0
- package/dist/agent/pages/docs/understand-the-system/nodes-and-hierarchy.md +24 -0
- package/dist/agent/pages/docs/understand-the-system/packages-and-plugins.md +29 -0
- package/dist/agent/pages/docs/understand-the-system/persistence-and-json-canvas.md +25 -0
- package/dist/agent/pages/docs/understand-the-system/rendering-and-interaction.md +22 -0
- package/dist/agent/pages/docs/understand-the-system/the-engine.md +28 -0
- package/dist/agent/pages/docs.md +18 -0
- package/dist/engine/transaction.d.ts +2 -2
- package/dist/index.js +18 -7
- package/dist/types.d.ts +2 -2
- 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
|
+
---
|