@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,441 @@ source: "ginko-content"
|
|
|
13
13
|
|
|
14
14
|
> Render step-style cards with custom renderers, connections, and undo/redo.
|
|
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/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
|
|