@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.
- package/CHANGELOG.md +9 -0
- package/README.md +46 -25
- package/dist/agent/AGENTS.md +53 -57
- package/dist/agent/pages/docs/build-features/connections.md +333 -2
- package/dist/agent/pages/docs/build-features/custom-node-renderers.md +366 -2
- package/dist/agent/pages/docs/build-features/read-only-and-command-guards.md +278 -2
- package/dist/agent/pages/docs/build-features/save-and-load.md +264 -2
- package/dist/agent/pages/docs/build-features/selection-and-keyboard.md +257 -2
- package/dist/agent/pages/docs/build-features/theming.md +285 -2
- package/dist/agent/pages/docs/evaluate/how-nuxt-board-works.md +451 -2
- package/dist/agent/pages/docs/project/contributing.md +4 -4
- package/dist/agent/pages/docs/project/support-and-security.md +1 -1
- package/dist/agent/pages/docs/reference/events-and-errors.md +279 -2
- package/dist/agent/pages/docs/reference/package-overview.md +2 -2
- package/dist/agent/pages/docs/solutions/mind-map.md +448 -2
- package/dist/agent/pages/docs/solutions/nuxt-application.md +214 -2
- package/dist/agent/pages/docs/solutions/planning-board.md +281 -2
- package/dist/agent/pages/docs/solutions/read-only-viewer.md +278 -2
- package/dist/agent/pages/docs/solutions/workflow-builder.md +435 -2
- package/dist/agent/pages/docs/start-building/customize-your-first-node.md +381 -2
- package/dist/agent/pages/docs/start-building/installation.md +6 -14
- package/dist/agent/pages/docs/understand-the-system/commands-and-transactions.md +456 -2
- package/dist/agent/pages/docs/understand-the-system/document-and-session-state.md +348 -2
- package/dist/agent/pages/docs/understand-the-system/persistence-and-json-canvas.md +445 -2
- package/package.json +3 -2
- package/dist/agent/manifest.json +0 -320
|
@@ -13,8 +13,454 @@ source: "ginko-content"
|
|
|
13
13
|
|
|
14
14
|
> Spatial topic hierarchy with connections, custom renderers, and branch-from-selection.
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
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/MindMapDemo.vue
|
|
21
|
+
|
|
22
|
+
````vue
|
|
23
|
+
<script setup lang="ts">
|
|
24
|
+
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
|
25
|
+
import { asNodeId, createBoardEngine, type NodeId } from '@lupinum/board-core'
|
|
26
|
+
import { createDemoDocument } from '../../utils/demoDocument'
|
|
27
|
+
import { connectionsPlugin } from '@lupinum/board-connections'
|
|
28
|
+
import { BoardConnectionLayer } from '@lupinum/board-connections/vue'
|
|
29
|
+
import { historyPlugin } from '@lupinum/board-history'
|
|
30
|
+
import MindMapTopicNode from './MindMapTopicNode.vue'
|
|
31
|
+
|
|
32
|
+
const engine = createBoardEngine({
|
|
33
|
+
grid: { size: 20, majorEvery: 5, snap: true, pattern: 'dot' },
|
|
34
|
+
plugins: [historyPlugin(), connectionsPlugin({ routing: 'bezier' })],
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const selection = ref<NodeId[]>([])
|
|
38
|
+
const edgesVersion = ref(0)
|
|
39
|
+
const historyState = ref(engine.plugins.history.getState())
|
|
40
|
+
const ROOT_ID = asNodeId('root')
|
|
41
|
+
const ENG_ID = asNodeId('eng')
|
|
42
|
+
const DESIGN_ID = asNodeId('design')
|
|
43
|
+
const GROWTH_ID = asNodeId('growth')
|
|
44
|
+
const API_ID = asNodeId('api')
|
|
45
|
+
|
|
46
|
+
function syncHistoryState() {
|
|
47
|
+
historyState.value = engine.plugins.history.getState()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function refreshEdges() {
|
|
51
|
+
edgesVersion.value++
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const unsubscribeEngine = [
|
|
55
|
+
engine.on('selection:change', (next) => {
|
|
56
|
+
selection.value = next
|
|
57
|
+
}),
|
|
58
|
+
engine.on('edge:created', refreshEdges),
|
|
59
|
+
engine.on('edge:updated', refreshEdges),
|
|
60
|
+
engine.on('edge:deleted', refreshEdges),
|
|
61
|
+
engine.on('history:push', syncHistoryState),
|
|
62
|
+
engine.on('history:undo', syncHistoryState),
|
|
63
|
+
engine.on('history:redo', syncHistoryState),
|
|
64
|
+
engine.on('history:clear', syncHistoryState),
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
const topicDepthsById = computed(() => {
|
|
68
|
+
void edgesVersion.value
|
|
69
|
+
const depths = new Map<NodeId, number>([[ROOT_ID, 0]])
|
|
70
|
+
const queue = [ROOT_ID]
|
|
71
|
+
|
|
72
|
+
while (queue.length > 0) {
|
|
73
|
+
const current = queue.shift()!
|
|
74
|
+
const depth = depths.get(current) ?? 0
|
|
75
|
+
for (const edge of engine.plugins.connections.getEdgesFrom(current)) {
|
|
76
|
+
if (depths.has(edge.to)) {
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
depths.set(edge.to, depth + 1)
|
|
80
|
+
queue.push(edge.to)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return depths
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
function seed() {
|
|
88
|
+
engine.loadDocument(
|
|
89
|
+
createDemoDocument({
|
|
90
|
+
camera: { x: -60, y: -40, z: 1 },
|
|
91
|
+
grid: engine.getGridSettings(),
|
|
92
|
+
selection: [],
|
|
93
|
+
nextZIndex: 6,
|
|
94
|
+
nodes: [
|
|
95
|
+
{
|
|
96
|
+
id: ROOT_ID,
|
|
97
|
+
type: 'text',
|
|
98
|
+
x: 300,
|
|
99
|
+
y: 160,
|
|
100
|
+
width: 220,
|
|
101
|
+
height: 110,
|
|
102
|
+
text: 'Product roadmap\nQ3 priorities',
|
|
103
|
+
zIndex: 1,
|
|
104
|
+
locked: false,
|
|
105
|
+
visible: true,
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: ENG_ID,
|
|
109
|
+
type: 'text',
|
|
110
|
+
x: 40,
|
|
111
|
+
y: 40,
|
|
112
|
+
width: 200,
|
|
113
|
+
height: 100,
|
|
114
|
+
text: 'Engineering\nAPI, canvas, exports',
|
|
115
|
+
zIndex: 2,
|
|
116
|
+
locked: false,
|
|
117
|
+
visible: true,
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: DESIGN_ID,
|
|
121
|
+
type: 'text',
|
|
122
|
+
x: 40,
|
|
123
|
+
y: 290,
|
|
124
|
+
width: 200,
|
|
125
|
+
height: 100,
|
|
126
|
+
text: 'Design\nInteraction polish',
|
|
127
|
+
zIndex: 3,
|
|
128
|
+
locked: false,
|
|
129
|
+
visible: true,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: GROWTH_ID,
|
|
133
|
+
type: 'text',
|
|
134
|
+
x: 610,
|
|
135
|
+
y: 40,
|
|
136
|
+
width: 200,
|
|
137
|
+
height: 100,
|
|
138
|
+
text: 'Growth\nActivation paths',
|
|
139
|
+
zIndex: 4,
|
|
140
|
+
locked: false,
|
|
141
|
+
visible: true,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
id: API_ID,
|
|
145
|
+
type: 'text',
|
|
146
|
+
x: 610,
|
|
147
|
+
y: 290,
|
|
148
|
+
width: 200,
|
|
149
|
+
height: 90,
|
|
150
|
+
text: 'Public API\nKeep it boring',
|
|
151
|
+
zIndex: 5,
|
|
152
|
+
locked: false,
|
|
153
|
+
visible: true,
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
}),
|
|
157
|
+
{ mode: 'replace' },
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
const conn = engine.plugins.connections
|
|
161
|
+
for (const edge of conn.getEdges()) conn.deleteEdge(edge.id)
|
|
162
|
+
conn.createEdge({ from: ROOT_ID, to: ENG_ID, data: {} })
|
|
163
|
+
conn.createEdge({ from: ROOT_ID, to: DESIGN_ID, data: {} })
|
|
164
|
+
conn.createEdge({ from: ROOT_ID, to: GROWTH_ID, data: {} })
|
|
165
|
+
conn.createEdge({ from: ENG_ID, to: API_ID, data: {} })
|
|
166
|
+
engine.plugins.history.clear()
|
|
167
|
+
refreshEdges()
|
|
168
|
+
syncHistoryState()
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let branchCount = 0
|
|
172
|
+
function addBranch() {
|
|
173
|
+
branchCount++
|
|
174
|
+
const selected = selection.value[0]
|
|
175
|
+
const parent = selected ?? ROOT_ID
|
|
176
|
+
const parentNode = engine.getNode(parent)
|
|
177
|
+
if (!parentNode) return
|
|
178
|
+
|
|
179
|
+
const newNode = engine.createNode({
|
|
180
|
+
type: 'text',
|
|
181
|
+
x: parentNode.x + 260 + (branchCount % 2) * 32,
|
|
182
|
+
y: parentNode.y + ((branchCount % 3) - 1) * 60,
|
|
183
|
+
width: 200,
|
|
184
|
+
height: 90,
|
|
185
|
+
text: `New topic ${branchCount}`,
|
|
186
|
+
select: true,
|
|
187
|
+
})
|
|
188
|
+
engine.plugins.connections.createEdge({
|
|
189
|
+
from: parent,
|
|
190
|
+
to: newNode.id,
|
|
191
|
+
data: {},
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
onMounted(async () => {
|
|
196
|
+
seed()
|
|
197
|
+
await engine.zoomToFit(72, false)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
onBeforeUnmount(() => {
|
|
201
|
+
for (const unsubscribe of unsubscribeEngine) {
|
|
202
|
+
unsubscribe()
|
|
203
|
+
}
|
|
204
|
+
})
|
|
205
|
+
</script>
|
|
206
|
+
|
|
207
|
+
<template>
|
|
208
|
+
<div class="demo-frame">
|
|
209
|
+
<div class="demo-toolbar">
|
|
210
|
+
<button class="demo-danger" @click="seed">Reset</button>
|
|
211
|
+
<button class="demo-primary" @click="addBranch">Add branch</button>
|
|
212
|
+
<button @click="engine.zoomToFit(72, false)">Zoom to fit</button>
|
|
213
|
+
<button
|
|
214
|
+
:disabled="historyState.undoDepth === 0"
|
|
215
|
+
@click="engine.plugins.history.undo()"
|
|
216
|
+
>
|
|
217
|
+
Undo
|
|
218
|
+
</button>
|
|
219
|
+
<button
|
|
220
|
+
:disabled="historyState.redoDepth === 0"
|
|
221
|
+
@click="engine.plugins.history.redo()"
|
|
222
|
+
>
|
|
223
|
+
Redo
|
|
224
|
+
</button>
|
|
225
|
+
<span class="demo-toolbar-note ml-auto"
|
|
226
|
+
>Select a topic, then add a branch from it.</span
|
|
227
|
+
>
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
<BoardRoot :engine="engine" style="height: 420px">
|
|
231
|
+
<template #node:text="{ node, selected }">
|
|
232
|
+
<MindMapTopicNode
|
|
233
|
+
:node="node"
|
|
234
|
+
:selected="selected"
|
|
235
|
+
:depth="topicDepthsById.get(node.id) ?? 2"
|
|
236
|
+
/>
|
|
237
|
+
</template>
|
|
238
|
+
<BoardConnectionLayer routing="bezier" />
|
|
239
|
+
</BoardRoot>
|
|
240
|
+
</div>
|
|
241
|
+
</template>
|
|
242
|
+
````
|
|
243
|
+
|
|
244
|
+
### app/utils/demoDocument.ts
|
|
245
|
+
|
|
246
|
+
````ts
|
|
247
|
+
import type {
|
|
248
|
+
BoardNode,
|
|
249
|
+
BoardNodeMetadata,
|
|
250
|
+
Camera,
|
|
251
|
+
GridSettings,
|
|
252
|
+
JsonCanvasDocument,
|
|
253
|
+
JsonCanvasNode,
|
|
254
|
+
NodeId,
|
|
255
|
+
} from '@lupinum/board-core'
|
|
256
|
+
|
|
257
|
+
type DemoNode = Omit<
|
|
258
|
+
Pick<
|
|
259
|
+
BoardNode,
|
|
260
|
+
| 'id'
|
|
261
|
+
| 'type'
|
|
262
|
+
| 'x'
|
|
263
|
+
| 'y'
|
|
264
|
+
| 'width'
|
|
265
|
+
| 'height'
|
|
266
|
+
| 'color'
|
|
267
|
+
| 'text'
|
|
268
|
+
| 'file'
|
|
269
|
+
| 'subpath'
|
|
270
|
+
| 'url'
|
|
271
|
+
| 'label'
|
|
272
|
+
| 'background'
|
|
273
|
+
| 'backgroundStyle'
|
|
274
|
+
| 'zIndex'
|
|
275
|
+
| 'locked'
|
|
276
|
+
| 'visible'
|
|
277
|
+
| 'parentId'
|
|
278
|
+
>,
|
|
279
|
+
'id' | 'parentId'
|
|
280
|
+
> & {
|
|
281
|
+
id: NodeId | string
|
|
282
|
+
parentId?: NodeId | string
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export interface DemoDocumentInput {
|
|
286
|
+
nodes: readonly DemoNode[]
|
|
287
|
+
camera?: Camera
|
|
288
|
+
grid?: GridSettings
|
|
289
|
+
selection?: readonly (NodeId | string)[]
|
|
290
|
+
nextZIndex?: number
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function toJsonCanvasNode(node: DemoNode): JsonCanvasNode {
|
|
294
|
+
const base = {
|
|
295
|
+
id: node.id as NodeId,
|
|
296
|
+
type: node.type,
|
|
297
|
+
x: node.x,
|
|
298
|
+
y: node.y,
|
|
299
|
+
width: node.width,
|
|
300
|
+
height: node.height,
|
|
301
|
+
...(node.color ? { color: node.color } : {}),
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
switch (node.type) {
|
|
305
|
+
case 'file':
|
|
306
|
+
return {
|
|
307
|
+
...base,
|
|
308
|
+
type: 'file',
|
|
309
|
+
file: node.file ?? '',
|
|
310
|
+
...(node.subpath ? { subpath: node.subpath } : {}),
|
|
311
|
+
}
|
|
312
|
+
case 'link':
|
|
313
|
+
return {
|
|
314
|
+
...base,
|
|
315
|
+
type: 'link',
|
|
316
|
+
url: node.url ?? '',
|
|
317
|
+
}
|
|
318
|
+
case 'group':
|
|
319
|
+
return {
|
|
320
|
+
...base,
|
|
321
|
+
type: 'group',
|
|
322
|
+
...((node.label ?? node.text)
|
|
323
|
+
? { label: node.label ?? node.text }
|
|
324
|
+
: {}),
|
|
325
|
+
...(node.background ? { background: node.background } : {}),
|
|
326
|
+
...(node.backgroundStyle
|
|
327
|
+
? { backgroundStyle: node.backgroundStyle }
|
|
328
|
+
: {}),
|
|
329
|
+
}
|
|
330
|
+
case 'text':
|
|
331
|
+
default:
|
|
332
|
+
return {
|
|
333
|
+
...base,
|
|
334
|
+
type: 'text',
|
|
335
|
+
text: node.text ?? '',
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function toNodeMetadata(node: DemoNode): BoardNodeMetadata {
|
|
341
|
+
return {
|
|
342
|
+
zIndex: node.zIndex,
|
|
343
|
+
locked: node.locked,
|
|
344
|
+
visible: node.visible,
|
|
345
|
+
...(node.parentId ? { parentId: node.parentId as NodeId } : {}),
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export function createDemoDocument(
|
|
350
|
+
input: DemoDocumentInput,
|
|
351
|
+
): JsonCanvasDocument {
|
|
352
|
+
return {
|
|
353
|
+
nodes: input.nodes.map(toJsonCanvasNode),
|
|
354
|
+
'x-lupinum-board': {
|
|
355
|
+
...(input.camera ? { camera: input.camera } : {}),
|
|
356
|
+
...(input.grid ? { grid: input.grid } : {}),
|
|
357
|
+
...(input.selection
|
|
358
|
+
? { selection: input.selection.map((id) => id as NodeId) }
|
|
359
|
+
: {}),
|
|
360
|
+
nextZIndex:
|
|
361
|
+
input.nextZIndex ??
|
|
362
|
+
input.nodes.reduce((max, node) => Math.max(max, node.zIndex), 0) + 1,
|
|
363
|
+
nodes: Object.fromEntries(
|
|
364
|
+
input.nodes.map((node) => [node.id, toNodeMetadata(node)]),
|
|
365
|
+
),
|
|
366
|
+
},
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
````
|
|
370
|
+
|
|
371
|
+
### app/components/demos/MindMapTopicNode.vue
|
|
372
|
+
|
|
373
|
+
````vue
|
|
374
|
+
<script setup lang="ts">
|
|
375
|
+
import { computed } from 'vue'
|
|
376
|
+
import type { BoardNode } from '@lupinum/board-core'
|
|
377
|
+
|
|
378
|
+
const props = defineProps<{
|
|
379
|
+
node: BoardNode
|
|
380
|
+
selected: boolean
|
|
381
|
+
depth: number
|
|
382
|
+
}>()
|
|
383
|
+
|
|
384
|
+
const data = computed(() => {
|
|
385
|
+
const [title = 'Topic', detail = ''] = props.node.text?.split('\n') ?? []
|
|
386
|
+
return { title, detail }
|
|
387
|
+
})
|
|
388
|
+
const depth = computed(() => props.depth)
|
|
389
|
+
|
|
390
|
+
const style = computed(() => {
|
|
391
|
+
if (depth.value === 0) {
|
|
392
|
+
return {
|
|
393
|
+
surface: `linear-gradient(135deg,
|
|
394
|
+
color-mix(in srgb, #14b8a6 16%, white),
|
|
395
|
+
color-mix(in srgb, #14b8a6 4%, white) 55%,
|
|
396
|
+
color-mix(in srgb, #14b8a6 12%, white))`,
|
|
397
|
+
glow: 'inset 0 1px 0 0 rgba(20,184,166,0.20), 0 14px 48px rgba(15,23,42,0.08), 0 1px 3px rgba(20,184,166,0.08)',
|
|
398
|
+
border: '#14b8a6',
|
|
399
|
+
text: 'text-teal-950',
|
|
400
|
+
label: 'ROOT',
|
|
401
|
+
labelColor: 'text-teal-600',
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (depth.value === 1) {
|
|
405
|
+
return {
|
|
406
|
+
surface: `linear-gradient(135deg,
|
|
407
|
+
color-mix(in srgb, #0ea5e9 14%, white),
|
|
408
|
+
color-mix(in srgb, #0ea5e9 3%, white) 55%,
|
|
409
|
+
color-mix(in srgb, #0ea5e9 10%, white))`,
|
|
410
|
+
glow: 'inset 0 1px 0 0 rgba(14,165,233,0.18), 0 14px 48px rgba(15,23,42,0.07), 0 1px 3px rgba(14,165,233,0.08)',
|
|
411
|
+
border: '#0ea5e9',
|
|
412
|
+
text: 'text-sky-950',
|
|
413
|
+
label: 'BRANCH',
|
|
414
|
+
labelColor: 'text-sky-500',
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return {
|
|
418
|
+
surface: `linear-gradient(135deg,
|
|
419
|
+
color-mix(in srgb, #94a3b8 10%, white),
|
|
420
|
+
white 55%,
|
|
421
|
+
color-mix(in srgb, #94a3b8 6%, white))`,
|
|
422
|
+
glow: 'inset 0 1px 0 0 rgba(148,163,184,0.14), 0 10px 36px rgba(15,23,42,0.05)',
|
|
423
|
+
border: '#94a3b8',
|
|
424
|
+
text: 'text-default',
|
|
425
|
+
label: 'LEAF',
|
|
426
|
+
labelColor: 'text-dimmed',
|
|
427
|
+
}
|
|
428
|
+
})
|
|
429
|
+
</script>
|
|
430
|
+
|
|
431
|
+
<template>
|
|
432
|
+
<div
|
|
433
|
+
class="h-full rounded-md border p-4"
|
|
434
|
+
:class="[selected ? 'ring-2 ring-primary' : 'border-default']"
|
|
435
|
+
:style="{
|
|
436
|
+
borderLeft: `4px solid ${style.border}`,
|
|
437
|
+
background: style.surface,
|
|
438
|
+
boxShadow: style.glow,
|
|
439
|
+
}"
|
|
440
|
+
>
|
|
441
|
+
<div class="flex h-full flex-col justify-between gap-2">
|
|
442
|
+
<span
|
|
443
|
+
class="flex items-center gap-1.5 text-[0.6rem] font-semibold uppercase tracking-[0.28em]"
|
|
444
|
+
:class="style.labelColor"
|
|
445
|
+
>
|
|
446
|
+
<span
|
|
447
|
+
class="inline-block size-1.5 rounded-full"
|
|
448
|
+
:style="{ background: style.border }"
|
|
449
|
+
></span>
|
|
450
|
+
{{ style.label }}
|
|
451
|
+
</span>
|
|
452
|
+
<div>
|
|
453
|
+
<h3 class="text-base font-semibold tracking-tight" :class="style.text">
|
|
454
|
+
{{ data.title }}
|
|
455
|
+
</h3>
|
|
456
|
+
<p v-if="data.detail" class="mt-1.5 text-sm leading-5 text-dimmed">
|
|
457
|
+
{{ data.detail }}
|
|
458
|
+
</p>
|
|
459
|
+
</div>
|
|
460
|
+
</div>
|
|
461
|
+
</div>
|
|
462
|
+
</template>
|
|
463
|
+
````
|
|
18
464
|
|
|
19
465
|
## What happens
|
|
20
466
|
|
|
@@ -13,8 +13,220 @@ source: "ginko-content"
|
|
|
13
13
|
|
|
14
14
|
> Use board components and composables without manual imports via the Nuxt module.
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
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/NuxtAutoImportsDemo.vue
|
|
21
|
+
|
|
22
|
+
````vue
|
|
23
|
+
<script setup lang="ts">
|
|
24
|
+
import { onMounted } from 'vue'
|
|
25
|
+
import { createBoardEngine, createDemoDocument } from '#imports'
|
|
26
|
+
|
|
27
|
+
// The generated #imports entry verifies the helpers are registered by @lupinum/nuxt-board.
|
|
28
|
+
const engine = createBoardEngine({
|
|
29
|
+
grid: { size: 20, majorEvery: 5, snap: true, pattern: 'dot' },
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
function seed() {
|
|
33
|
+
engine.loadDocument(
|
|
34
|
+
createDemoDocument({
|
|
35
|
+
camera: { x: 0, y: 0, z: 1 },
|
|
36
|
+
grid: engine.getGridSettings(),
|
|
37
|
+
nodes: [
|
|
38
|
+
{
|
|
39
|
+
id: 'nuxt',
|
|
40
|
+
type: 'text',
|
|
41
|
+
x: 100,
|
|
42
|
+
y: 100,
|
|
43
|
+
width: 240,
|
|
44
|
+
height: 100,
|
|
45
|
+
text: 'Node',
|
|
46
|
+
zIndex: 1,
|
|
47
|
+
locked: false,
|
|
48
|
+
visible: true,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 'board',
|
|
52
|
+
type: 'text',
|
|
53
|
+
x: 420,
|
|
54
|
+
y: 180,
|
|
55
|
+
width: 240,
|
|
56
|
+
height: 100,
|
|
57
|
+
text: 'Node',
|
|
58
|
+
zIndex: 2,
|
|
59
|
+
locked: false,
|
|
60
|
+
visible: true,
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
selection: [],
|
|
64
|
+
nextZIndex: 3,
|
|
65
|
+
}),
|
|
66
|
+
{ mode: 'replace' },
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
seed()
|
|
71
|
+
|
|
72
|
+
onMounted(async () => {
|
|
73
|
+
await engine.zoomToFit(84, false)
|
|
74
|
+
})
|
|
75
|
+
</script>
|
|
76
|
+
|
|
77
|
+
<template>
|
|
78
|
+
<div class="demo-frame">
|
|
79
|
+
<div class="demo-toolbar">
|
|
80
|
+
<button class="demo-danger" @click="seed">Reset</button>
|
|
81
|
+
<button
|
|
82
|
+
class="demo-primary"
|
|
83
|
+
@click="
|
|
84
|
+
engine.createNode({
|
|
85
|
+
type: 'text',
|
|
86
|
+
x: 260,
|
|
87
|
+
y: 80,
|
|
88
|
+
width: 220,
|
|
89
|
+
height: 100,
|
|
90
|
+
text: 'Node',
|
|
91
|
+
})
|
|
92
|
+
"
|
|
93
|
+
>
|
|
94
|
+
Add node
|
|
95
|
+
</button>
|
|
96
|
+
<button @click="engine.zoomToFit(84, false)">Zoom to fit</button>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<BoardRoot :engine="engine" style="height: 320px" />
|
|
100
|
+
</div>
|
|
101
|
+
</template>
|
|
102
|
+
````
|
|
103
|
+
|
|
104
|
+
### app/utils/demoDocument.ts
|
|
105
|
+
|
|
106
|
+
````ts
|
|
107
|
+
import type {
|
|
108
|
+
BoardNode,
|
|
109
|
+
BoardNodeMetadata,
|
|
110
|
+
Camera,
|
|
111
|
+
GridSettings,
|
|
112
|
+
JsonCanvasDocument,
|
|
113
|
+
JsonCanvasNode,
|
|
114
|
+
NodeId,
|
|
115
|
+
} from '@lupinum/board-core'
|
|
116
|
+
|
|
117
|
+
type DemoNode = Omit<
|
|
118
|
+
Pick<
|
|
119
|
+
BoardNode,
|
|
120
|
+
| 'id'
|
|
121
|
+
| 'type'
|
|
122
|
+
| 'x'
|
|
123
|
+
| 'y'
|
|
124
|
+
| 'width'
|
|
125
|
+
| 'height'
|
|
126
|
+
| 'color'
|
|
127
|
+
| 'text'
|
|
128
|
+
| 'file'
|
|
129
|
+
| 'subpath'
|
|
130
|
+
| 'url'
|
|
131
|
+
| 'label'
|
|
132
|
+
| 'background'
|
|
133
|
+
| 'backgroundStyle'
|
|
134
|
+
| 'zIndex'
|
|
135
|
+
| 'locked'
|
|
136
|
+
| 'visible'
|
|
137
|
+
| 'parentId'
|
|
138
|
+
>,
|
|
139
|
+
'id' | 'parentId'
|
|
140
|
+
> & {
|
|
141
|
+
id: NodeId | string
|
|
142
|
+
parentId?: NodeId | string
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface DemoDocumentInput {
|
|
146
|
+
nodes: readonly DemoNode[]
|
|
147
|
+
camera?: Camera
|
|
148
|
+
grid?: GridSettings
|
|
149
|
+
selection?: readonly (NodeId | string)[]
|
|
150
|
+
nextZIndex?: number
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function toJsonCanvasNode(node: DemoNode): JsonCanvasNode {
|
|
154
|
+
const base = {
|
|
155
|
+
id: node.id as NodeId,
|
|
156
|
+
type: node.type,
|
|
157
|
+
x: node.x,
|
|
158
|
+
y: node.y,
|
|
159
|
+
width: node.width,
|
|
160
|
+
height: node.height,
|
|
161
|
+
...(node.color ? { color: node.color } : {}),
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
switch (node.type) {
|
|
165
|
+
case 'file':
|
|
166
|
+
return {
|
|
167
|
+
...base,
|
|
168
|
+
type: 'file',
|
|
169
|
+
file: node.file ?? '',
|
|
170
|
+
...(node.subpath ? { subpath: node.subpath } : {}),
|
|
171
|
+
}
|
|
172
|
+
case 'link':
|
|
173
|
+
return {
|
|
174
|
+
...base,
|
|
175
|
+
type: 'link',
|
|
176
|
+
url: node.url ?? '',
|
|
177
|
+
}
|
|
178
|
+
case 'group':
|
|
179
|
+
return {
|
|
180
|
+
...base,
|
|
181
|
+
type: 'group',
|
|
182
|
+
...((node.label ?? node.text)
|
|
183
|
+
? { label: node.label ?? node.text }
|
|
184
|
+
: {}),
|
|
185
|
+
...(node.background ? { background: node.background } : {}),
|
|
186
|
+
...(node.backgroundStyle
|
|
187
|
+
? { backgroundStyle: node.backgroundStyle }
|
|
188
|
+
: {}),
|
|
189
|
+
}
|
|
190
|
+
case 'text':
|
|
191
|
+
default:
|
|
192
|
+
return {
|
|
193
|
+
...base,
|
|
194
|
+
type: 'text',
|
|
195
|
+
text: node.text ?? '',
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function toNodeMetadata(node: DemoNode): BoardNodeMetadata {
|
|
201
|
+
return {
|
|
202
|
+
zIndex: node.zIndex,
|
|
203
|
+
locked: node.locked,
|
|
204
|
+
visible: node.visible,
|
|
205
|
+
...(node.parentId ? { parentId: node.parentId as NodeId } : {}),
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function createDemoDocument(
|
|
210
|
+
input: DemoDocumentInput,
|
|
211
|
+
): JsonCanvasDocument {
|
|
212
|
+
return {
|
|
213
|
+
nodes: input.nodes.map(toJsonCanvasNode),
|
|
214
|
+
'x-lupinum-board': {
|
|
215
|
+
...(input.camera ? { camera: input.camera } : {}),
|
|
216
|
+
...(input.grid ? { grid: input.grid } : {}),
|
|
217
|
+
...(input.selection
|
|
218
|
+
? { selection: input.selection.map((id) => id as NodeId) }
|
|
219
|
+
: {}),
|
|
220
|
+
nextZIndex:
|
|
221
|
+
input.nextZIndex ??
|
|
222
|
+
input.nodes.reduce((max, node) => Math.max(max, node.zIndex), 0) + 1,
|
|
223
|
+
nodes: Object.fromEntries(
|
|
224
|
+
input.nodes.map((node) => [node.id, toNodeMetadata(node)]),
|
|
225
|
+
),
|
|
226
|
+
},
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
````
|
|
18
230
|
|
|
19
231
|
## What happens
|
|
20
232
|
|