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

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 (26) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +46 -25
  3. package/dist/agent/AGENTS.md +53 -57
  4. package/dist/agent/pages/docs/build-features/connections.md +333 -2
  5. package/dist/agent/pages/docs/build-features/custom-node-renderers.md +366 -2
  6. package/dist/agent/pages/docs/build-features/read-only-and-command-guards.md +278 -2
  7. package/dist/agent/pages/docs/build-features/save-and-load.md +264 -2
  8. package/dist/agent/pages/docs/build-features/selection-and-keyboard.md +257 -2
  9. package/dist/agent/pages/docs/build-features/theming.md +285 -2
  10. package/dist/agent/pages/docs/evaluate/how-nuxt-board-works.md +451 -2
  11. package/dist/agent/pages/docs/project/contributing.md +4 -4
  12. package/dist/agent/pages/docs/project/support-and-security.md +1 -1
  13. package/dist/agent/pages/docs/reference/events-and-errors.md +279 -2
  14. package/dist/agent/pages/docs/reference/package-overview.md +2 -2
  15. package/dist/agent/pages/docs/solutions/mind-map.md +448 -2
  16. package/dist/agent/pages/docs/solutions/nuxt-application.md +214 -2
  17. package/dist/agent/pages/docs/solutions/planning-board.md +281 -2
  18. package/dist/agent/pages/docs/solutions/read-only-viewer.md +278 -2
  19. package/dist/agent/pages/docs/solutions/workflow-builder.md +435 -2
  20. package/dist/agent/pages/docs/start-building/customize-your-first-node.md +381 -2
  21. package/dist/agent/pages/docs/start-building/installation.md +6 -14
  22. package/dist/agent/pages/docs/understand-the-system/commands-and-transactions.md +456 -2
  23. package/dist/agent/pages/docs/understand-the-system/document-and-session-state.md +348 -2
  24. package/dist/agent/pages/docs/understand-the-system/persistence-and-json-canvas.md +445 -2
  25. package/package.json +3 -2
  26. package/dist/agent/manifest.json +0 -320
@@ -13,8 +13,287 @@ source: "ginko-content"
13
13
 
14
14
  > A practical planning surface with notes, grouping, and zoom-to-fit.
15
15
 
16
- > Component omitted: `basic-board-demo`.
17
- > This page contains an interactive or site-specific block that has no agent markdown serializer yet.
16
+ ## Live example source
17
+
18
+ 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.
19
+
20
+ ### app/components/demos/BasicBoardDemo.vue
21
+
22
+ ````vue
23
+ <script setup lang="ts">
24
+ import { onMounted, ref } from 'vue'
25
+ import { createBoardEngine } from '@lupinum/board-core'
26
+ import { createDemoDocument } from '../../utils/demoDocument'
27
+
28
+ const engine = createBoardEngine({
29
+ grid: { size: 20, majorEvery: 5, snap: true, pattern: 'line' },
30
+ })
31
+ const nodeCount = ref(0)
32
+
33
+ function syncNodeCount() {
34
+ nodeCount.value = engine.getState().nodes.size
35
+ }
36
+
37
+ function seed() {
38
+ engine.loadDocument(
39
+ createDemoDocument({
40
+ camera: { x: -80, y: -20, z: 1 },
41
+ grid: engine.getGridSettings(),
42
+ nodes: [
43
+ {
44
+ id: 'group-1',
45
+ type: 'group',
46
+ x: 32,
47
+ y: 32,
48
+ width: 896,
49
+ height: 306,
50
+ text: 'Node',
51
+ zIndex: 0,
52
+ locked: false,
53
+ visible: true,
54
+ },
55
+ {
56
+ id: 'research',
57
+ type: 'text',
58
+ x: 60,
59
+ y: 60,
60
+ width: 240,
61
+ height: 100,
62
+ text: 'Node',
63
+ zIndex: 1,
64
+ locked: false,
65
+ visible: true,
66
+ parentId: 'group-1',
67
+ },
68
+ {
69
+ id: 'prototype',
70
+ type: 'text',
71
+ x: 370,
72
+ y: 200,
73
+ width: 260,
74
+ height: 110,
75
+ text: 'Node',
76
+ zIndex: 2,
77
+ locked: false,
78
+ visible: true,
79
+ parentId: 'group-1',
80
+ },
81
+ {
82
+ id: 'review',
83
+ type: 'text',
84
+ x: 680,
85
+ y: 80,
86
+ width: 220,
87
+ height: 100,
88
+ text: 'Node',
89
+ zIndex: 3,
90
+ locked: false,
91
+ visible: true,
92
+ parentId: 'group-1',
93
+ },
94
+ ],
95
+ selection: [],
96
+ nextZIndex: 4,
97
+ }),
98
+ { mode: 'replace' },
99
+ )
100
+ syncNodeCount()
101
+ }
102
+
103
+ function addNode() {
104
+ const index = nodeCount.value - 4
105
+ engine.createNode({
106
+ type: 'text',
107
+ x: 180 + (index % 3) * 120,
108
+ y: 140 + (index % 2) * 100,
109
+ width: 220,
110
+ height: 100,
111
+ text: `Planning note ${index + 1}`,
112
+ })
113
+ syncNodeCount()
114
+ }
115
+
116
+ function wrapSelectionInGroup() {
117
+ const selected = engine.getSelection()
118
+ if (selected.length === 0) {
119
+ return
120
+ }
121
+ const nodes = Array.from(engine.getState().nodes.values()).filter((node) =>
122
+ selected.includes(node.id),
123
+ )
124
+ if (nodes.length === 0) {
125
+ return
126
+ }
127
+ const minX = Math.min(...nodes.map((node) => node.x))
128
+ const minY = Math.min(...nodes.map((node) => node.y))
129
+ const maxX = Math.max(...nodes.map((node) => node.x + node.width))
130
+ const maxY = Math.max(...nodes.map((node) => node.y + node.height))
131
+ const group = engine.createNode({
132
+ type: 'group',
133
+ x: minX - 28,
134
+ y: minY - 28,
135
+ width: maxX - minX + 56,
136
+ height: maxY - minY + 56,
137
+ label: 'Group',
138
+ select: false,
139
+ })
140
+ engine.sendToBack(group.id)
141
+ for (const node of nodes) {
142
+ engine.updateNode(node.id, { parentId: group.id })
143
+ }
144
+ engine.select(group.id)
145
+ }
146
+
147
+ onMounted(async () => {
148
+ seed()
149
+ await engine.zoomToFit(72, false)
150
+ })
151
+ </script>
152
+
153
+ <template>
154
+ <div
155
+ class="demo-frame"
156
+ data-testid="basic-board-demo"
157
+ :data-node-count="nodeCount"
158
+ >
159
+ <div class="demo-toolbar">
160
+ <button class="demo-danger" @click="seed">Reset</button>
161
+ <button class="demo-primary" @click="addNode">Add note</button>
162
+ <button @click="wrapSelectionInGroup">Group selection</button>
163
+ <button @click="engine.zoomToFit(72, false)">Zoom to fit</button>
164
+ </div>
165
+
166
+ <BoardRoot :engine="engine" style="height: 360px" />
167
+ </div>
168
+ </template>
169
+ ````
170
+
171
+ ### app/utils/demoDocument.ts
172
+
173
+ ````ts
174
+ import type {
175
+ BoardNode,
176
+ BoardNodeMetadata,
177
+ Camera,
178
+ GridSettings,
179
+ JsonCanvasDocument,
180
+ JsonCanvasNode,
181
+ NodeId,
182
+ } from '@lupinum/board-core'
183
+
184
+ type DemoNode = Omit<
185
+ Pick<
186
+ BoardNode,
187
+ | 'id'
188
+ | 'type'
189
+ | 'x'
190
+ | 'y'
191
+ | 'width'
192
+ | 'height'
193
+ | 'color'
194
+ | 'text'
195
+ | 'file'
196
+ | 'subpath'
197
+ | 'url'
198
+ | 'label'
199
+ | 'background'
200
+ | 'backgroundStyle'
201
+ | 'zIndex'
202
+ | 'locked'
203
+ | 'visible'
204
+ | 'parentId'
205
+ >,
206
+ 'id' | 'parentId'
207
+ > & {
208
+ id: NodeId | string
209
+ parentId?: NodeId | string
210
+ }
211
+
212
+ export interface DemoDocumentInput {
213
+ nodes: readonly DemoNode[]
214
+ camera?: Camera
215
+ grid?: GridSettings
216
+ selection?: readonly (NodeId | string)[]
217
+ nextZIndex?: number
218
+ }
219
+
220
+ function toJsonCanvasNode(node: DemoNode): JsonCanvasNode {
221
+ const base = {
222
+ id: node.id as NodeId,
223
+ type: node.type,
224
+ x: node.x,
225
+ y: node.y,
226
+ width: node.width,
227
+ height: node.height,
228
+ ...(node.color ? { color: node.color } : {}),
229
+ }
230
+
231
+ switch (node.type) {
232
+ case 'file':
233
+ return {
234
+ ...base,
235
+ type: 'file',
236
+ file: node.file ?? '',
237
+ ...(node.subpath ? { subpath: node.subpath } : {}),
238
+ }
239
+ case 'link':
240
+ return {
241
+ ...base,
242
+ type: 'link',
243
+ url: node.url ?? '',
244
+ }
245
+ case 'group':
246
+ return {
247
+ ...base,
248
+ type: 'group',
249
+ ...((node.label ?? node.text)
250
+ ? { label: node.label ?? node.text }
251
+ : {}),
252
+ ...(node.background ? { background: node.background } : {}),
253
+ ...(node.backgroundStyle
254
+ ? { backgroundStyle: node.backgroundStyle }
255
+ : {}),
256
+ }
257
+ case 'text':
258
+ default:
259
+ return {
260
+ ...base,
261
+ type: 'text',
262
+ text: node.text ?? '',
263
+ }
264
+ }
265
+ }
266
+
267
+ function toNodeMetadata(node: DemoNode): BoardNodeMetadata {
268
+ return {
269
+ zIndex: node.zIndex,
270
+ locked: node.locked,
271
+ visible: node.visible,
272
+ ...(node.parentId ? { parentId: node.parentId as NodeId } : {}),
273
+ }
274
+ }
275
+
276
+ export function createDemoDocument(
277
+ input: DemoDocumentInput,
278
+ ): JsonCanvasDocument {
279
+ return {
280
+ nodes: input.nodes.map(toJsonCanvasNode),
281
+ 'x-lupinum-board': {
282
+ ...(input.camera ? { camera: input.camera } : {}),
283
+ ...(input.grid ? { grid: input.grid } : {}),
284
+ ...(input.selection
285
+ ? { selection: input.selection.map((id) => id as NodeId) }
286
+ : {}),
287
+ nextZIndex:
288
+ input.nextZIndex ??
289
+ input.nodes.reduce((max, node) => Math.max(max, node.zIndex), 0) + 1,
290
+ nodes: Object.fromEntries(
291
+ input.nodes.map((node) => [node.id, toNodeMetadata(node)]),
292
+ ),
293
+ },
294
+ }
295
+ }
296
+ ````
18
297
 
19
298
  ## What happens
20
299
 
@@ -13,8 +13,284 @@ source: "ginko-content"
13
13
 
14
14
  > Display a board for viewing without allowing edits by using a command guard.
15
15
 
16
- > Component omitted: `read-only-toggle-demo`.
17
- > This page contains an interactive or site-specific block that has no agent markdown serializer yet.
16
+ ## Live example source
17
+
18
+ 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.
19
+
20
+ ### app/components/demos/ReadOnlyToggleDemo.vue
21
+
22
+ ````vue
23
+ <script setup lang="ts">
24
+ import { onMounted, ref, watch } from 'vue'
25
+ import { createBoardEngine } from '@lupinum/board-core'
26
+ import { createDemoDocument } from '../../utils/demoDocument'
27
+
28
+ const engine = createBoardEngine({
29
+ grid: { size: 20, majorEvery: 5, snap: true, pattern: 'dot' },
30
+ })
31
+
32
+ const isReadOnly = ref(true)
33
+ const blockedCount = ref(0)
34
+ let unsubscribeReadOnly: (() => void) | null = null
35
+
36
+ const blockedCommands = new Set([
37
+ 'beginNodeDrag',
38
+ 'beginResize',
39
+ 'beginTextEdit',
40
+ 'createNode',
41
+ 'updateNode',
42
+ 'deleteNode',
43
+ 'moveNode',
44
+ 'resizeNode',
45
+ 'deleteSelected',
46
+ 'duplicateNodes',
47
+ 'translateSelectedNodes',
48
+ 'sendToBack',
49
+ 'lockNode',
50
+ 'unlockNode',
51
+ 'commitTextEdit',
52
+ 'copySelected',
53
+ 'pasteClipboard',
54
+ ])
55
+
56
+ function seed() {
57
+ engine.loadDocument(
58
+ createDemoDocument({
59
+ camera: { x: 0, y: 0, z: 1 },
60
+ grid: engine.getGridSettings(),
61
+ nodes: [
62
+ {
63
+ id: 'ro-a',
64
+ type: 'text',
65
+ x: 80,
66
+ y: 90,
67
+ width: 220,
68
+ height: 100,
69
+ text: 'Node',
70
+ zIndex: 1,
71
+ locked: false,
72
+ visible: true,
73
+ },
74
+ {
75
+ id: 'ro-b',
76
+ type: 'text',
77
+ x: 380,
78
+ y: 190,
79
+ width: 220,
80
+ height: 100,
81
+ text: 'Node',
82
+ zIndex: 2,
83
+ locked: false,
84
+ visible: true,
85
+ },
86
+ ],
87
+ selection: [],
88
+ nextZIndex: 3,
89
+ }),
90
+ { mode: 'replace' },
91
+ )
92
+ }
93
+
94
+ function applyReadOnly() {
95
+ unsubscribeReadOnly?.()
96
+ unsubscribeReadOnly = null
97
+
98
+ if (!isReadOnly.value) {
99
+ return
100
+ }
101
+
102
+ unsubscribeReadOnly = engine.addCommandGuard(({ name }) => {
103
+ if (blockedCommands.has(name)) {
104
+ blockedCount.value += 1
105
+ return 'Board is read-only.'
106
+ }
107
+ return true
108
+ })
109
+ }
110
+
111
+ watch(isReadOnly, applyReadOnly)
112
+
113
+ onMounted(async () => {
114
+ seed()
115
+ applyReadOnly()
116
+ await engine.zoomToFit(60, false)
117
+ })
118
+ </script>
119
+
120
+ <template>
121
+ <div class="demo-frame">
122
+ <div class="demo-toolbar">
123
+ <button class="demo-danger" @click="seed">Reset</button>
124
+ <button class="demo-primary" @click="isReadOnly = !isReadOnly">
125
+ {{ isReadOnly ? 'Switch to edit mode' : 'Switch to read-only' }}
126
+ </button>
127
+ <span class="demo-toolbar-note">
128
+ {{
129
+ isReadOnly
130
+ ? 'Mutations blocked — try dragging or deleting.'
131
+ : 'Mutations allowed.'
132
+ }}
133
+ </span>
134
+ </div>
135
+
136
+ <div class="grid gap-0 lg:grid-cols-[minmax(0,1fr)_280px]">
137
+ <BoardRoot :engine="engine" style="height: 360px" />
138
+
139
+ <div
140
+ class="border-t border-default bg-elevated p-4 lg:border-t-0 lg:border-l"
141
+ >
142
+ <div
143
+ class="inline-flex rounded-full px-3 py-2 text-sm font-semibold"
144
+ :class="
145
+ isReadOnly
146
+ ? 'bg-amber-50 text-amber-700'
147
+ : 'bg-emerald-50 text-emerald-700'
148
+ "
149
+ >
150
+ {{ isReadOnly ? 'Read-only' : 'Editable' }}
151
+ </div>
152
+ <div class="mt-5 rounded-md border border-default bg-default p-4">
153
+ <p
154
+ class="text-xs font-semibold uppercase tracking-[0.28em] text-dimmed"
155
+ >
156
+ Blocked attempts
157
+ </p>
158
+ <p class="mt-2 text-2xl font-bold tracking-tight text-highlighted">
159
+ {{ blockedCount }}
160
+ </p>
161
+ </div>
162
+ </div>
163
+ </div>
164
+ </div>
165
+ </template>
166
+ ````
167
+
168
+ ### app/utils/demoDocument.ts
169
+
170
+ ````ts
171
+ import type {
172
+ BoardNode,
173
+ BoardNodeMetadata,
174
+ Camera,
175
+ GridSettings,
176
+ JsonCanvasDocument,
177
+ JsonCanvasNode,
178
+ NodeId,
179
+ } from '@lupinum/board-core'
180
+
181
+ type DemoNode = Omit<
182
+ Pick<
183
+ BoardNode,
184
+ | 'id'
185
+ | 'type'
186
+ | 'x'
187
+ | 'y'
188
+ | 'width'
189
+ | 'height'
190
+ | 'color'
191
+ | 'text'
192
+ | 'file'
193
+ | 'subpath'
194
+ | 'url'
195
+ | 'label'
196
+ | 'background'
197
+ | 'backgroundStyle'
198
+ | 'zIndex'
199
+ | 'locked'
200
+ | 'visible'
201
+ | 'parentId'
202
+ >,
203
+ 'id' | 'parentId'
204
+ > & {
205
+ id: NodeId | string
206
+ parentId?: NodeId | string
207
+ }
208
+
209
+ export interface DemoDocumentInput {
210
+ nodes: readonly DemoNode[]
211
+ camera?: Camera
212
+ grid?: GridSettings
213
+ selection?: readonly (NodeId | string)[]
214
+ nextZIndex?: number
215
+ }
216
+
217
+ function toJsonCanvasNode(node: DemoNode): JsonCanvasNode {
218
+ const base = {
219
+ id: node.id as NodeId,
220
+ type: node.type,
221
+ x: node.x,
222
+ y: node.y,
223
+ width: node.width,
224
+ height: node.height,
225
+ ...(node.color ? { color: node.color } : {}),
226
+ }
227
+
228
+ switch (node.type) {
229
+ case 'file':
230
+ return {
231
+ ...base,
232
+ type: 'file',
233
+ file: node.file ?? '',
234
+ ...(node.subpath ? { subpath: node.subpath } : {}),
235
+ }
236
+ case 'link':
237
+ return {
238
+ ...base,
239
+ type: 'link',
240
+ url: node.url ?? '',
241
+ }
242
+ case 'group':
243
+ return {
244
+ ...base,
245
+ type: 'group',
246
+ ...((node.label ?? node.text)
247
+ ? { label: node.label ?? node.text }
248
+ : {}),
249
+ ...(node.background ? { background: node.background } : {}),
250
+ ...(node.backgroundStyle
251
+ ? { backgroundStyle: node.backgroundStyle }
252
+ : {}),
253
+ }
254
+ case 'text':
255
+ default:
256
+ return {
257
+ ...base,
258
+ type: 'text',
259
+ text: node.text ?? '',
260
+ }
261
+ }
262
+ }
263
+
264
+ function toNodeMetadata(node: DemoNode): BoardNodeMetadata {
265
+ return {
266
+ zIndex: node.zIndex,
267
+ locked: node.locked,
268
+ visible: node.visible,
269
+ ...(node.parentId ? { parentId: node.parentId as NodeId } : {}),
270
+ }
271
+ }
272
+
273
+ export function createDemoDocument(
274
+ input: DemoDocumentInput,
275
+ ): JsonCanvasDocument {
276
+ return {
277
+ nodes: input.nodes.map(toJsonCanvasNode),
278
+ 'x-lupinum-board': {
279
+ ...(input.camera ? { camera: input.camera } : {}),
280
+ ...(input.grid ? { grid: input.grid } : {}),
281
+ ...(input.selection
282
+ ? { selection: input.selection.map((id) => id as NodeId) }
283
+ : {}),
284
+ nextZIndex:
285
+ input.nextZIndex ??
286
+ input.nodes.reduce((max, node) => Math.max(max, node.zIndex), 0) + 1,
287
+ nodes: Object.fromEntries(
288
+ input.nodes.map((node) => [node.id, toNodeMetadata(node)]),
289
+ ),
290
+ },
291
+ }
292
+ }
293
+ ````
18
294
 
19
295
  ## What happens
20
296