@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
|
@@ -15,8 +15,270 @@ source: "ginko-content"
|
|
|
15
15
|
|
|
16
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
17
|
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
## Live example source
|
|
19
|
+
|
|
20
|
+
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.
|
|
21
|
+
|
|
22
|
+
### app/components/demos/JsonImportExportDemo.vue
|
|
23
|
+
|
|
24
|
+
````vue
|
|
25
|
+
<script setup lang="ts">
|
|
26
|
+
import { onMounted, ref } from 'vue'
|
|
27
|
+
import { asNodeId, createBoardEngine } from '@lupinum/board-core'
|
|
28
|
+
import { createDemoDocument } from '../../utils/demoDocument'
|
|
29
|
+
import { connectionsPlugin } from '@lupinum/board-connections'
|
|
30
|
+
import { BoardConnectionLayer } from '@lupinum/board-connections/vue'
|
|
31
|
+
|
|
32
|
+
const engine = createBoardEngine({
|
|
33
|
+
grid: { size: 18, majorEvery: 6, snap: true, pattern: 'line' },
|
|
34
|
+
plugins: [connectionsPlugin()],
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const payload = ref('')
|
|
38
|
+
const formatMode = ref<'pretty' | 'compact'>('pretty')
|
|
39
|
+
const SOURCE_ID = asNodeId('source')
|
|
40
|
+
const TARGET_ID = asNodeId('target')
|
|
41
|
+
|
|
42
|
+
function exportDocument() {
|
|
43
|
+
const raw = engine.exportDocument()
|
|
44
|
+
if (formatMode.value === 'compact') {
|
|
45
|
+
return JSON.stringify(raw)
|
|
46
|
+
}
|
|
47
|
+
return JSON.stringify(raw, null, 2)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function seed() {
|
|
51
|
+
engine.loadDocument(
|
|
52
|
+
createDemoDocument({
|
|
53
|
+
camera: { x: 0, y: 0, z: 1 },
|
|
54
|
+
grid: engine.getGridSettings(),
|
|
55
|
+
selection: [],
|
|
56
|
+
nextZIndex: 3,
|
|
57
|
+
nodes: [
|
|
58
|
+
{
|
|
59
|
+
id: SOURCE_ID,
|
|
60
|
+
type: 'text',
|
|
61
|
+
x: 80,
|
|
62
|
+
y: 110,
|
|
63
|
+
width: 220,
|
|
64
|
+
height: 100,
|
|
65
|
+
text: 'Export me\nto JSON Canvas',
|
|
66
|
+
zIndex: 1,
|
|
67
|
+
locked: false,
|
|
68
|
+
visible: true,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: TARGET_ID,
|
|
72
|
+
type: 'text',
|
|
73
|
+
x: 420,
|
|
74
|
+
y: 110,
|
|
75
|
+
width: 220,
|
|
76
|
+
height: 100,
|
|
77
|
+
text: 'Edit the JSON\nthen import back',
|
|
78
|
+
zIndex: 2,
|
|
79
|
+
locked: false,
|
|
80
|
+
visible: true,
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
}),
|
|
84
|
+
{ mode: 'replace' },
|
|
85
|
+
)
|
|
86
|
+
for (const edge of engine.plugins.connections.getEdges()) {
|
|
87
|
+
engine.plugins.connections.deleteEdge(edge.id)
|
|
88
|
+
}
|
|
89
|
+
engine.plugins.connections.createEdge({
|
|
90
|
+
from: SOURCE_ID,
|
|
91
|
+
to: TARGET_ID,
|
|
92
|
+
label: 'json-canvas',
|
|
93
|
+
data: {},
|
|
94
|
+
})
|
|
95
|
+
payload.value = exportDocument()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function exportBoard() {
|
|
99
|
+
payload.value = exportDocument()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function setFormatMode(nextMode: 'pretty' | 'compact') {
|
|
103
|
+
formatMode.value = nextMode
|
|
104
|
+
exportBoard()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function importBoard() {
|
|
108
|
+
if (!payload.value.trim()) return
|
|
109
|
+
engine.loadDocument(JSON.parse(payload.value), { mode: 'replace' })
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
onMounted(async () => {
|
|
113
|
+
seed()
|
|
114
|
+
await engine.zoomToFit(72, false)
|
|
115
|
+
})
|
|
116
|
+
</script>
|
|
117
|
+
|
|
118
|
+
<template>
|
|
119
|
+
<div class="demo-frame">
|
|
120
|
+
<div class="demo-toolbar">
|
|
121
|
+
<button class="demo-danger" @click="seed">Reset</button>
|
|
122
|
+
<button class="demo-primary" @click="exportBoard">
|
|
123
|
+
Export JSON Canvas
|
|
124
|
+
</button>
|
|
125
|
+
<button @click="importBoard">Import payload</button>
|
|
126
|
+
<span class="demo-toggle-group">
|
|
127
|
+
<button
|
|
128
|
+
:class="{ active: formatMode === 'pretty' }"
|
|
129
|
+
@click="setFormatMode('pretty')"
|
|
130
|
+
>
|
|
131
|
+
Pretty
|
|
132
|
+
</button>
|
|
133
|
+
<button
|
|
134
|
+
:class="{ active: formatMode === 'compact' }"
|
|
135
|
+
@click="setFormatMode('compact')"
|
|
136
|
+
>
|
|
137
|
+
Compact
|
|
138
|
+
</button>
|
|
139
|
+
</span>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<div class="grid gap-0 md:grid-cols-[minmax(0,1.2fr)_minmax(0,0.8fr)]">
|
|
143
|
+
<BoardRoot :engine="engine" style="height: 360px">
|
|
144
|
+
<BoardConnectionLayer />
|
|
145
|
+
</BoardRoot>
|
|
146
|
+
<textarea
|
|
147
|
+
v-model="payload"
|
|
148
|
+
class="min-h-[360px] resize-none border-0 border-l border-default bg-inverted p-4 font-mono text-xs leading-6 text-primary outline-none"
|
|
149
|
+
spellcheck="false"
|
|
150
|
+
/>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
</template>
|
|
154
|
+
````
|
|
155
|
+
|
|
156
|
+
### app/utils/demoDocument.ts
|
|
157
|
+
|
|
158
|
+
````ts
|
|
159
|
+
import type {
|
|
160
|
+
BoardNode,
|
|
161
|
+
BoardNodeMetadata,
|
|
162
|
+
Camera,
|
|
163
|
+
GridSettings,
|
|
164
|
+
JsonCanvasDocument,
|
|
165
|
+
JsonCanvasNode,
|
|
166
|
+
NodeId,
|
|
167
|
+
} from '@lupinum/board-core'
|
|
168
|
+
|
|
169
|
+
type DemoNode = Omit<
|
|
170
|
+
Pick<
|
|
171
|
+
BoardNode,
|
|
172
|
+
| 'id'
|
|
173
|
+
| 'type'
|
|
174
|
+
| 'x'
|
|
175
|
+
| 'y'
|
|
176
|
+
| 'width'
|
|
177
|
+
| 'height'
|
|
178
|
+
| 'color'
|
|
179
|
+
| 'text'
|
|
180
|
+
| 'file'
|
|
181
|
+
| 'subpath'
|
|
182
|
+
| 'url'
|
|
183
|
+
| 'label'
|
|
184
|
+
| 'background'
|
|
185
|
+
| 'backgroundStyle'
|
|
186
|
+
| 'zIndex'
|
|
187
|
+
| 'locked'
|
|
188
|
+
| 'visible'
|
|
189
|
+
| 'parentId'
|
|
190
|
+
>,
|
|
191
|
+
'id' | 'parentId'
|
|
192
|
+
> & {
|
|
193
|
+
id: NodeId | string
|
|
194
|
+
parentId?: NodeId | string
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export interface DemoDocumentInput {
|
|
198
|
+
nodes: readonly DemoNode[]
|
|
199
|
+
camera?: Camera
|
|
200
|
+
grid?: GridSettings
|
|
201
|
+
selection?: readonly (NodeId | string)[]
|
|
202
|
+
nextZIndex?: number
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function toJsonCanvasNode(node: DemoNode): JsonCanvasNode {
|
|
206
|
+
const base = {
|
|
207
|
+
id: node.id as NodeId,
|
|
208
|
+
type: node.type,
|
|
209
|
+
x: node.x,
|
|
210
|
+
y: node.y,
|
|
211
|
+
width: node.width,
|
|
212
|
+
height: node.height,
|
|
213
|
+
...(node.color ? { color: node.color } : {}),
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
switch (node.type) {
|
|
217
|
+
case 'file':
|
|
218
|
+
return {
|
|
219
|
+
...base,
|
|
220
|
+
type: 'file',
|
|
221
|
+
file: node.file ?? '',
|
|
222
|
+
...(node.subpath ? { subpath: node.subpath } : {}),
|
|
223
|
+
}
|
|
224
|
+
case 'link':
|
|
225
|
+
return {
|
|
226
|
+
...base,
|
|
227
|
+
type: 'link',
|
|
228
|
+
url: node.url ?? '',
|
|
229
|
+
}
|
|
230
|
+
case 'group':
|
|
231
|
+
return {
|
|
232
|
+
...base,
|
|
233
|
+
type: 'group',
|
|
234
|
+
...((node.label ?? node.text)
|
|
235
|
+
? { label: node.label ?? node.text }
|
|
236
|
+
: {}),
|
|
237
|
+
...(node.background ? { background: node.background } : {}),
|
|
238
|
+
...(node.backgroundStyle
|
|
239
|
+
? { backgroundStyle: node.backgroundStyle }
|
|
240
|
+
: {}),
|
|
241
|
+
}
|
|
242
|
+
case 'text':
|
|
243
|
+
default:
|
|
244
|
+
return {
|
|
245
|
+
...base,
|
|
246
|
+
type: 'text',
|
|
247
|
+
text: node.text ?? '',
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function toNodeMetadata(node: DemoNode): BoardNodeMetadata {
|
|
253
|
+
return {
|
|
254
|
+
zIndex: node.zIndex,
|
|
255
|
+
locked: node.locked,
|
|
256
|
+
visible: node.visible,
|
|
257
|
+
...(node.parentId ? { parentId: node.parentId as NodeId } : {}),
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function createDemoDocument(
|
|
262
|
+
input: DemoDocumentInput,
|
|
263
|
+
): JsonCanvasDocument {
|
|
264
|
+
return {
|
|
265
|
+
nodes: input.nodes.map(toJsonCanvasNode),
|
|
266
|
+
'x-lupinum-board': {
|
|
267
|
+
...(input.camera ? { camera: input.camera } : {}),
|
|
268
|
+
...(input.grid ? { grid: input.grid } : {}),
|
|
269
|
+
...(input.selection
|
|
270
|
+
? { selection: input.selection.map((id) => id as NodeId) }
|
|
271
|
+
: {}),
|
|
272
|
+
nextZIndex:
|
|
273
|
+
input.nextZIndex ??
|
|
274
|
+
input.nodes.reduce((max, node) => Math.max(max, node.zIndex), 0) + 1,
|
|
275
|
+
nodes: Object.fromEntries(
|
|
276
|
+
input.nodes.map((node) => [node.id, toNodeMetadata(node)]),
|
|
277
|
+
),
|
|
278
|
+
},
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
````
|
|
20
282
|
|
|
21
283
|
## What to persist
|
|
22
284
|
|
|
@@ -15,8 +15,263 @@ source: "ginko-content"
|
|
|
15
15
|
|
|
16
16
|
Selection and interaction are how users communicate with the board. Select nodes to act on them, drag to move, resize from handles, and box-select on empty canvas.
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
## Live example source
|
|
19
|
+
|
|
20
|
+
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.
|
|
21
|
+
|
|
22
|
+
### app/components/demos/InteractionStateViz.vue
|
|
23
|
+
|
|
24
|
+
````vue
|
|
25
|
+
<script setup lang="ts">
|
|
26
|
+
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
|
27
|
+
import { createBoardEngine } from '@lupinum/board-core'
|
|
28
|
+
import { createDemoDocument } from '../../utils/demoDocument'
|
|
29
|
+
|
|
30
|
+
const engine = createBoardEngine({
|
|
31
|
+
grid: { size: 20, majorEvery: 5, snap: true, pattern: 'cross' },
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const mode = ref(engine.$interaction.get().mode)
|
|
35
|
+
const selection = ref(engine.getSelection())
|
|
36
|
+
let unsubscribeInteraction: (() => void) | null = null
|
|
37
|
+
let unsubscribeSelection: (() => void) | null = null
|
|
38
|
+
|
|
39
|
+
const statusTone = computed(() => {
|
|
40
|
+
if (mode.value === 'idle') return 'bg-accented text-default'
|
|
41
|
+
if (mode.value.includes('drag') || mode.value.includes('resize'))
|
|
42
|
+
return 'bg-sky-50 text-sky-700'
|
|
43
|
+
if (mode.value.includes('pan')) return 'bg-amber-50 text-amber-700'
|
|
44
|
+
return 'bg-emerald-50 text-emerald-700'
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
function seed() {
|
|
48
|
+
engine.loadDocument(
|
|
49
|
+
createDemoDocument({
|
|
50
|
+
camera: { x: 0, y: 0, z: 1 },
|
|
51
|
+
grid: engine.getGridSettings(),
|
|
52
|
+
nodes: [
|
|
53
|
+
{
|
|
54
|
+
id: 'state-a',
|
|
55
|
+
type: 'text',
|
|
56
|
+
x: 70,
|
|
57
|
+
y: 80,
|
|
58
|
+
width: 220,
|
|
59
|
+
height: 100,
|
|
60
|
+
text: 'Node',
|
|
61
|
+
zIndex: 1,
|
|
62
|
+
locked: false,
|
|
63
|
+
visible: true,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'state-b',
|
|
67
|
+
type: 'text',
|
|
68
|
+
x: 360,
|
|
69
|
+
y: 180,
|
|
70
|
+
width: 220,
|
|
71
|
+
height: 100,
|
|
72
|
+
text: 'Node',
|
|
73
|
+
zIndex: 2,
|
|
74
|
+
locked: false,
|
|
75
|
+
visible: true,
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
selection: [],
|
|
79
|
+
nextZIndex: 3,
|
|
80
|
+
}),
|
|
81
|
+
{ mode: 'replace' },
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function reset() {
|
|
86
|
+
seed()
|
|
87
|
+
selection.value = []
|
|
88
|
+
await engine.zoomToFit(60, false)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
onMounted(() => {
|
|
92
|
+
unsubscribeInteraction = engine.$interaction.subscribe((interaction) => {
|
|
93
|
+
mode.value = interaction.mode
|
|
94
|
+
})
|
|
95
|
+
unsubscribeSelection = engine.on('selection:change', (next) => {
|
|
96
|
+
selection.value = next
|
|
97
|
+
})
|
|
98
|
+
void reset()
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
onBeforeUnmount(() => {
|
|
102
|
+
unsubscribeInteraction?.()
|
|
103
|
+
unsubscribeSelection?.()
|
|
104
|
+
})
|
|
105
|
+
</script>
|
|
106
|
+
|
|
107
|
+
<template>
|
|
108
|
+
<div class="demo-frame">
|
|
109
|
+
<div class="demo-toolbar">
|
|
110
|
+
<button class="demo-danger" @click="reset">Reset</button>
|
|
111
|
+
<span class="demo-toolbar-note"
|
|
112
|
+
>Interact with the board and watch the mode change in the sidebar.</span
|
|
113
|
+
>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<div class="grid gap-0 lg:grid-cols-[minmax(0,1fr)_280px]">
|
|
117
|
+
<BoardRoot :engine="engine" style="height: 360px" />
|
|
118
|
+
|
|
119
|
+
<div
|
|
120
|
+
class="border-t border-default bg-elevated p-4 lg:border-t-0 lg:border-l"
|
|
121
|
+
>
|
|
122
|
+
<p class="text-sm font-semibold text-highlighted">
|
|
123
|
+
Current interaction
|
|
124
|
+
</p>
|
|
125
|
+
<div
|
|
126
|
+
class="mt-4 inline-flex rounded-full px-3 py-2 text-sm font-semibold"
|
|
127
|
+
:class="statusTone"
|
|
128
|
+
>
|
|
129
|
+
{{ mode }}
|
|
130
|
+
</div>
|
|
131
|
+
<div class="mt-5 rounded-md border border-default bg-default p-4">
|
|
132
|
+
<p
|
|
133
|
+
class="text-xs font-semibold uppercase tracking-[0.28em] text-dimmed"
|
|
134
|
+
>
|
|
135
|
+
Selection
|
|
136
|
+
</p>
|
|
137
|
+
<p class="mt-2 text-sm text-default">
|
|
138
|
+
{{
|
|
139
|
+
selection.length ? selection.join(', ') : 'Nothing selected yet.'
|
|
140
|
+
}}
|
|
141
|
+
</p>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
</template>
|
|
147
|
+
````
|
|
148
|
+
|
|
149
|
+
### app/utils/demoDocument.ts
|
|
150
|
+
|
|
151
|
+
````ts
|
|
152
|
+
import type {
|
|
153
|
+
BoardNode,
|
|
154
|
+
BoardNodeMetadata,
|
|
155
|
+
Camera,
|
|
156
|
+
GridSettings,
|
|
157
|
+
JsonCanvasDocument,
|
|
158
|
+
JsonCanvasNode,
|
|
159
|
+
NodeId,
|
|
160
|
+
} from '@lupinum/board-core'
|
|
161
|
+
|
|
162
|
+
type DemoNode = Omit<
|
|
163
|
+
Pick<
|
|
164
|
+
BoardNode,
|
|
165
|
+
| 'id'
|
|
166
|
+
| 'type'
|
|
167
|
+
| 'x'
|
|
168
|
+
| 'y'
|
|
169
|
+
| 'width'
|
|
170
|
+
| 'height'
|
|
171
|
+
| 'color'
|
|
172
|
+
| 'text'
|
|
173
|
+
| 'file'
|
|
174
|
+
| 'subpath'
|
|
175
|
+
| 'url'
|
|
176
|
+
| 'label'
|
|
177
|
+
| 'background'
|
|
178
|
+
| 'backgroundStyle'
|
|
179
|
+
| 'zIndex'
|
|
180
|
+
| 'locked'
|
|
181
|
+
| 'visible'
|
|
182
|
+
| 'parentId'
|
|
183
|
+
>,
|
|
184
|
+
'id' | 'parentId'
|
|
185
|
+
> & {
|
|
186
|
+
id: NodeId | string
|
|
187
|
+
parentId?: NodeId | string
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export interface DemoDocumentInput {
|
|
191
|
+
nodes: readonly DemoNode[]
|
|
192
|
+
camera?: Camera
|
|
193
|
+
grid?: GridSettings
|
|
194
|
+
selection?: readonly (NodeId | string)[]
|
|
195
|
+
nextZIndex?: number
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function toJsonCanvasNode(node: DemoNode): JsonCanvasNode {
|
|
199
|
+
const base = {
|
|
200
|
+
id: node.id as NodeId,
|
|
201
|
+
type: node.type,
|
|
202
|
+
x: node.x,
|
|
203
|
+
y: node.y,
|
|
204
|
+
width: node.width,
|
|
205
|
+
height: node.height,
|
|
206
|
+
...(node.color ? { color: node.color } : {}),
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
switch (node.type) {
|
|
210
|
+
case 'file':
|
|
211
|
+
return {
|
|
212
|
+
...base,
|
|
213
|
+
type: 'file',
|
|
214
|
+
file: node.file ?? '',
|
|
215
|
+
...(node.subpath ? { subpath: node.subpath } : {}),
|
|
216
|
+
}
|
|
217
|
+
case 'link':
|
|
218
|
+
return {
|
|
219
|
+
...base,
|
|
220
|
+
type: 'link',
|
|
221
|
+
url: node.url ?? '',
|
|
222
|
+
}
|
|
223
|
+
case 'group':
|
|
224
|
+
return {
|
|
225
|
+
...base,
|
|
226
|
+
type: 'group',
|
|
227
|
+
...((node.label ?? node.text)
|
|
228
|
+
? { label: node.label ?? node.text }
|
|
229
|
+
: {}),
|
|
230
|
+
...(node.background ? { background: node.background } : {}),
|
|
231
|
+
...(node.backgroundStyle
|
|
232
|
+
? { backgroundStyle: node.backgroundStyle }
|
|
233
|
+
: {}),
|
|
234
|
+
}
|
|
235
|
+
case 'text':
|
|
236
|
+
default:
|
|
237
|
+
return {
|
|
238
|
+
...base,
|
|
239
|
+
type: 'text',
|
|
240
|
+
text: node.text ?? '',
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function toNodeMetadata(node: DemoNode): BoardNodeMetadata {
|
|
246
|
+
return {
|
|
247
|
+
zIndex: node.zIndex,
|
|
248
|
+
locked: node.locked,
|
|
249
|
+
visible: node.visible,
|
|
250
|
+
...(node.parentId ? { parentId: node.parentId as NodeId } : {}),
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function createDemoDocument(
|
|
255
|
+
input: DemoDocumentInput,
|
|
256
|
+
): JsonCanvasDocument {
|
|
257
|
+
return {
|
|
258
|
+
nodes: input.nodes.map(toJsonCanvasNode),
|
|
259
|
+
'x-lupinum-board': {
|
|
260
|
+
...(input.camera ? { camera: input.camera } : {}),
|
|
261
|
+
...(input.grid ? { grid: input.grid } : {}),
|
|
262
|
+
...(input.selection
|
|
263
|
+
? { selection: input.selection.map((id) => id as NodeId) }
|
|
264
|
+
: {}),
|
|
265
|
+
nextZIndex:
|
|
266
|
+
input.nextZIndex ??
|
|
267
|
+
input.nodes.reduce((max, node) => Math.max(max, node.zIndex), 0) + 1,
|
|
268
|
+
nodes: Object.fromEntries(
|
|
269
|
+
input.nodes.map((node) => [node.id, toNodeMetadata(node)]),
|
|
270
|
+
),
|
|
271
|
+
},
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
````
|
|
20
275
|
|
|
21
276
|
## Selecting nodes
|
|
22
277
|
|