@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,287 @@ source: "ginko-content"
|
|
|
13
13
|
|
|
14
14
|
> A practical planning surface with notes, grouping, and zoom-to-fit.
|
|
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/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
|
-
|
|
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/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
|
|