@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,441 @@ source: "ginko-content"
13
13
 
14
14
  > Render step-style cards with custom renderers, connections, and undo/redo.
15
15
 
16
- > Component omitted: `workflow-renderer-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/WorkflowRendererDemo.vue
21
+
22
+ ````vue
23
+ <script setup lang="ts">
24
+ import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
25
+ import { asNodeId, createBoardEngine } 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 WorkflowStepNode from './WorkflowStepNode.vue'
31
+
32
+ type StepStatus = 'pending' | 'active' | 'done'
33
+ interface WorkflowStep {
34
+ id: ReturnType<typeof asNodeId>
35
+ status: StepStatus
36
+ label: string
37
+ summary: string
38
+ }
39
+
40
+ const engine = createBoardEngine({
41
+ grid: { size: 24, majorEvery: 4, snap: true, pattern: 'line' },
42
+ plugins: [historyPlugin(), connectionsPlugin({ routing: 'step' })],
43
+ })
44
+
45
+ const CAPTURE_ID = asNodeId('capture')
46
+ const QUALIFY_ID = asNodeId('qualify')
47
+ const HANDOFF_ID = asNodeId('handoff')
48
+
49
+ function createWorkflowSteps(): WorkflowStep[] {
50
+ return [
51
+ {
52
+ id: CAPTURE_ID,
53
+ status: 'done',
54
+ label: 'Capture lead',
55
+ summary: 'Collect the request and normalize inputs.',
56
+ },
57
+ {
58
+ id: QUALIFY_ID,
59
+ status: 'active',
60
+ label: 'Qualify',
61
+ summary: 'Score, tag, and route the lead.',
62
+ },
63
+ {
64
+ id: HANDOFF_ID,
65
+ status: 'pending',
66
+ label: 'Handoff',
67
+ summary: 'Create the downstream record and alert the owner.',
68
+ },
69
+ ]
70
+ }
71
+
72
+ const workflowSteps = ref<WorkflowStep[]>(createWorkflowSteps())
73
+ const stepsByNodeId = computed(
74
+ () => new Map(workflowSteps.value.map((step) => [step.id, step])),
75
+ )
76
+ const historyState = ref(engine.plugins.history.getState())
77
+
78
+ function syncHistoryState() {
79
+ historyState.value = engine.plugins.history.getState()
80
+ }
81
+
82
+ const unsubscribeHistory = [
83
+ engine.on('history:push', syncHistoryState),
84
+ engine.on('history:undo', syncHistoryState),
85
+ engine.on('history:redo', syncHistoryState),
86
+ engine.on('history:clear', syncHistoryState),
87
+ ]
88
+
89
+ function seed() {
90
+ workflowSteps.value = createWorkflowSteps()
91
+
92
+ engine.loadDocument(
93
+ createDemoDocument({
94
+ camera: { x: -40, y: -30, z: 1 },
95
+ grid: engine.getGridSettings(),
96
+ selection: [],
97
+ nextZIndex: 4,
98
+ nodes: [
99
+ {
100
+ id: CAPTURE_ID,
101
+ type: 'text',
102
+ x: 40,
103
+ y: 110,
104
+ width: 220,
105
+ height: 130,
106
+ text: '',
107
+ zIndex: 1,
108
+ locked: false,
109
+ visible: true,
110
+ },
111
+ {
112
+ id: QUALIFY_ID,
113
+ type: 'text',
114
+ x: 340,
115
+ y: 110,
116
+ width: 220,
117
+ height: 130,
118
+ text: '',
119
+ zIndex: 2,
120
+ locked: false,
121
+ visible: true,
122
+ },
123
+ {
124
+ id: HANDOFF_ID,
125
+ type: 'text',
126
+ x: 640,
127
+ y: 110,
128
+ width: 220,
129
+ height: 130,
130
+ text: '',
131
+ zIndex: 3,
132
+ locked: false,
133
+ visible: true,
134
+ },
135
+ ],
136
+ }),
137
+ { mode: 'replace' },
138
+ )
139
+
140
+ for (const edge of engine.plugins.connections.getEdges()) {
141
+ engine.plugins.connections.deleteEdge(edge.id)
142
+ }
143
+
144
+ engine.plugins.connections.createEdge({
145
+ from: CAPTURE_ID,
146
+ to: QUALIFY_ID,
147
+ label: 'validated',
148
+ data: {},
149
+ })
150
+ engine.plugins.connections.createEdge({
151
+ from: QUALIFY_ID,
152
+ to: HANDOFF_ID,
153
+ label: 'approved',
154
+ data: {},
155
+ })
156
+ engine.plugins.history.clear()
157
+ syncHistoryState()
158
+ }
159
+
160
+ function nextStatus(current: StepStatus): StepStatus {
161
+ if (current === 'pending') return 'active'
162
+ if (current === 'active') return 'done'
163
+ return 'pending'
164
+ }
165
+
166
+ function advanceSelected() {
167
+ const target = engine.getSelection()[0] ?? QUALIFY_ID
168
+ workflowSteps.value = workflowSteps.value.map((step) =>
169
+ step.id === target ? { ...step, status: nextStatus(step.status) } : step,
170
+ )
171
+ }
172
+
173
+ onMounted(async () => {
174
+ seed()
175
+ await engine.zoomToFit(80, false)
176
+ })
177
+
178
+ onBeforeUnmount(() => {
179
+ for (const unsubscribe of unsubscribeHistory) {
180
+ unsubscribe()
181
+ }
182
+ })
183
+ </script>
184
+
185
+ <template>
186
+ <div class="demo-frame">
187
+ <div class="demo-toolbar">
188
+ <button class="demo-danger" @click="seed">Reset</button>
189
+ <button class="demo-primary" @click="advanceSelected">
190
+ Cycle selected step
191
+ </button>
192
+ <button
193
+ :disabled="historyState.undoDepth === 0"
194
+ @click="engine.plugins.history.undo()"
195
+ >
196
+ Undo
197
+ </button>
198
+ <button
199
+ :disabled="historyState.redoDepth === 0"
200
+ @click="engine.plugins.history.redo()"
201
+ >
202
+ Redo
203
+ </button>
204
+ <span class="demo-history-badge ml-auto"
205
+ >{{ historyState.undoDepth }} undo /
206
+ {{ historyState.redoDepth }} redo</span
207
+ >
208
+ </div>
209
+
210
+ <BoardRoot :engine="engine" style="height: 400px">
211
+ <template #node:text="{ node, selected }">
212
+ <WorkflowStepNode
213
+ :step="stepsByNodeId.get(node.id)"
214
+ :selected="selected"
215
+ />
216
+ </template>
217
+ <BoardConnectionLayer routing="step" />
218
+ </BoardRoot>
219
+ </div>
220
+ </template>
221
+ ````
222
+
223
+ ### app/utils/demoDocument.ts
224
+
225
+ ````ts
226
+ import type {
227
+ BoardNode,
228
+ BoardNodeMetadata,
229
+ Camera,
230
+ GridSettings,
231
+ JsonCanvasDocument,
232
+ JsonCanvasNode,
233
+ NodeId,
234
+ } from '@lupinum/board-core'
235
+
236
+ type DemoNode = Omit<
237
+ Pick<
238
+ BoardNode,
239
+ | 'id'
240
+ | 'type'
241
+ | 'x'
242
+ | 'y'
243
+ | 'width'
244
+ | 'height'
245
+ | 'color'
246
+ | 'text'
247
+ | 'file'
248
+ | 'subpath'
249
+ | 'url'
250
+ | 'label'
251
+ | 'background'
252
+ | 'backgroundStyle'
253
+ | 'zIndex'
254
+ | 'locked'
255
+ | 'visible'
256
+ | 'parentId'
257
+ >,
258
+ 'id' | 'parentId'
259
+ > & {
260
+ id: NodeId | string
261
+ parentId?: NodeId | string
262
+ }
263
+
264
+ export interface DemoDocumentInput {
265
+ nodes: readonly DemoNode[]
266
+ camera?: Camera
267
+ grid?: GridSettings
268
+ selection?: readonly (NodeId | string)[]
269
+ nextZIndex?: number
270
+ }
271
+
272
+ function toJsonCanvasNode(node: DemoNode): JsonCanvasNode {
273
+ const base = {
274
+ id: node.id as NodeId,
275
+ type: node.type,
276
+ x: node.x,
277
+ y: node.y,
278
+ width: node.width,
279
+ height: node.height,
280
+ ...(node.color ? { color: node.color } : {}),
281
+ }
282
+
283
+ switch (node.type) {
284
+ case 'file':
285
+ return {
286
+ ...base,
287
+ type: 'file',
288
+ file: node.file ?? '',
289
+ ...(node.subpath ? { subpath: node.subpath } : {}),
290
+ }
291
+ case 'link':
292
+ return {
293
+ ...base,
294
+ type: 'link',
295
+ url: node.url ?? '',
296
+ }
297
+ case 'group':
298
+ return {
299
+ ...base,
300
+ type: 'group',
301
+ ...((node.label ?? node.text)
302
+ ? { label: node.label ?? node.text }
303
+ : {}),
304
+ ...(node.background ? { background: node.background } : {}),
305
+ ...(node.backgroundStyle
306
+ ? { backgroundStyle: node.backgroundStyle }
307
+ : {}),
308
+ }
309
+ case 'text':
310
+ default:
311
+ return {
312
+ ...base,
313
+ type: 'text',
314
+ text: node.text ?? '',
315
+ }
316
+ }
317
+ }
318
+
319
+ function toNodeMetadata(node: DemoNode): BoardNodeMetadata {
320
+ return {
321
+ zIndex: node.zIndex,
322
+ locked: node.locked,
323
+ visible: node.visible,
324
+ ...(node.parentId ? { parentId: node.parentId as NodeId } : {}),
325
+ }
326
+ }
327
+
328
+ export function createDemoDocument(
329
+ input: DemoDocumentInput,
330
+ ): JsonCanvasDocument {
331
+ return {
332
+ nodes: input.nodes.map(toJsonCanvasNode),
333
+ 'x-lupinum-board': {
334
+ ...(input.camera ? { camera: input.camera } : {}),
335
+ ...(input.grid ? { grid: input.grid } : {}),
336
+ ...(input.selection
337
+ ? { selection: input.selection.map((id) => id as NodeId) }
338
+ : {}),
339
+ nextZIndex:
340
+ input.nextZIndex ??
341
+ input.nodes.reduce((max, node) => Math.max(max, node.zIndex), 0) + 1,
342
+ nodes: Object.fromEntries(
343
+ input.nodes.map((node) => [node.id, toNodeMetadata(node)]),
344
+ ),
345
+ },
346
+ }
347
+ }
348
+ ````
349
+
350
+ ### app/components/demos/WorkflowStepNode.vue
351
+
352
+ ````vue
353
+ <script setup lang="ts">
354
+ import { computed } from 'vue'
355
+
356
+ interface WorkflowStep {
357
+ status: 'pending' | 'active' | 'done'
358
+ label: string
359
+ summary: string
360
+ }
361
+
362
+ const props = defineProps<{
363
+ step?: WorkflowStep
364
+ selected: boolean
365
+ }>()
366
+
367
+ const data = computed(() => {
368
+ return (
369
+ props.step ?? {
370
+ status: 'pending',
371
+ label: 'Missing step',
372
+ summary: 'No workflow record matches this board node.',
373
+ }
374
+ )
375
+ })
376
+ const tone = computed(() => {
377
+ const status = data.value.status
378
+ if (status === 'done') {
379
+ return {
380
+ badge: 'bg-emerald-100 text-emerald-800 ring-1 ring-emerald-300/60',
381
+ border: '#10b981',
382
+ surface: `linear-gradient(135deg,
383
+ color-mix(in srgb, #10b981 14%, white),
384
+ color-mix(in srgb, #10b981 4%, white) 60%,
385
+ color-mix(in srgb, #10b981 10%, white))`,
386
+ glow: 'inset 0 1px 0 0 rgba(16,185,129,0.18), 0 18px 60px rgba(15,23,42,0.08), 0 1px 3px rgba(16,185,129,0.10)',
387
+ }
388
+ }
389
+ if (status === 'active') {
390
+ return {
391
+ badge: 'bg-sky-100 text-sky-800 ring-1 ring-sky-300/60',
392
+ border: '#0ea5e9',
393
+ surface: `linear-gradient(135deg,
394
+ color-mix(in srgb, #0ea5e9 14%, white),
395
+ color-mix(in srgb, #0ea5e9 4%, white) 60%,
396
+ color-mix(in srgb, #0ea5e9 10%, white))`,
397
+ glow: 'inset 0 1px 0 0 rgba(14,165,233,0.18), 0 18px 60px rgba(15,23,42,0.08), 0 1px 3px rgba(14,165,233,0.10)',
398
+ }
399
+ }
400
+ return {
401
+ badge: 'bg-slate-100 text-slate-600 ring-1 ring-slate-200',
402
+ border: '#94a3b8',
403
+ surface: `linear-gradient(135deg,
404
+ color-mix(in srgb, #94a3b8 8%, white),
405
+ white 60%,
406
+ color-mix(in srgb, #94a3b8 6%, white))`,
407
+ glow: 'inset 0 1px 0 0 rgba(148,163,184,0.14), 0 18px 60px rgba(15,23,42,0.06)',
408
+ }
409
+ })
410
+ </script>
411
+
412
+ <template>
413
+ <div
414
+ class="h-full rounded-md border p-4"
415
+ :class="[selected ? 'ring-2 ring-primary' : 'border-default']"
416
+ :style="{
417
+ borderLeft: `5px solid ${tone.border}`,
418
+ background: tone.surface,
419
+ boxShadow: tone.glow,
420
+ }"
421
+ >
422
+ <div class="flex h-full flex-col justify-between gap-3">
423
+ <div class="flex items-center justify-between gap-3">
424
+ <span
425
+ class="flex items-center gap-1.5 text-[0.65rem] font-semibold uppercase tracking-[0.28em] text-dimmed"
426
+ >
427
+ <span
428
+ class="inline-block size-1.5 rounded-full"
429
+ :style="{ background: tone.border }"
430
+ ></span>
431
+ Step
432
+ </span>
433
+ <span
434
+ class="rounded-full px-2.5 py-1 text-[0.7rem] font-semibold tracking-wide"
435
+ :class="tone.badge"
436
+ >{{ data.status }}</span
437
+ >
438
+ </div>
439
+ <div>
440
+ <h3 class="text-lg font-semibold tracking-tight text-highlighted">
441
+ {{ data.label }}
442
+ </h3>
443
+ <p class="mt-2 text-sm leading-6 text-muted">
444
+ {{ data.summary }}
445
+ </p>
446
+ </div>
447
+ </div>
448
+ </div>
449
+ </template>
450
+ ````
18
451
 
19
452
  ## What happens
20
453