@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
@@ -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
- > Component omitted: `json-import-export-demo`.
19
- > This page contains an interactive or site-specific block that has no agent markdown serializer yet.
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
- > Component omitted: `interaction-state-viz`.
19
- > This page contains an interactive or site-specific block that has no agent markdown serializer yet.
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