@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
@@ -33,8 +33,457 @@ application intent
33
33
  3. **Renderers own presentation.** A renderer receives a node record. It does not create a competing state model.
34
34
  4. **Packages own named capabilities.** Core owns nodes; connections owns edges; history owns undo stacks; Nuxt owns registration.
35
35
 
36
- > Component omitted: `engine-command-lab`.
37
- > This page contains an interactive or site-specific block that has no agent markdown serializer yet.
36
+ ## Live example source
37
+
38
+ 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.
39
+
40
+ ### app/components/demos/EngineCommandLab.vue
41
+
42
+ ````vue
43
+ <script setup lang="ts">
44
+ import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
45
+ import { asNodeId, createBoardEngine } from '@lupinum/board-core'
46
+ import { createDemoDocument } from '../../utils/demoDocument'
47
+
48
+ const CARD_ID = asNodeId('review-card')
49
+ const engine = createBoardEngine({ grid: { size: 20, snap: true } })
50
+ const revision = ref(0)
51
+ const events = ref<{ label: string; detail?: string; tone?: 'success' }[]>([])
52
+ const before = ref<Record<string, unknown>>({})
53
+ const after = ref<Record<string, unknown>>({})
54
+ const disposers: (() => void)[] = []
55
+
56
+ function nodeSummary() {
57
+ const node = engine.getState().nodes.get(CARD_ID)
58
+ return node
59
+ ? { id: node.id, text: node.text, x: node.x, y: node.y, color: node.color }
60
+ : { id: CARD_ID, deleted: true }
61
+ }
62
+
63
+ const state = computed(() => {
64
+ void revision.value
65
+ return {
66
+ nodes: engine.getState().nodes.size,
67
+ selection: engine.getSelection(),
68
+ card: nodeSummary(),
69
+ }
70
+ })
71
+
72
+ function seed() {
73
+ engine.loadDocument(
74
+ createDemoDocument({
75
+ camera: { x: 0, y: 0, z: 1 },
76
+ grid: engine.getGridSettings(),
77
+ selection: [CARD_ID],
78
+ nextZIndex: 3,
79
+ nodes: [
80
+ {
81
+ id: CARD_ID,
82
+ type: 'text',
83
+ x: 90,
84
+ y: 80,
85
+ width: 240,
86
+ height: 120,
87
+ text: 'Review onboarding',
88
+ color: '5',
89
+ zIndex: 1,
90
+ locked: false,
91
+ visible: true,
92
+ },
93
+ {
94
+ id: asNodeId('release-card'),
95
+ type: 'text',
96
+ x: 400,
97
+ y: 180,
98
+ width: 240,
99
+ height: 120,
100
+ text: 'Prepare release notes',
101
+ color: '4',
102
+ zIndex: 2,
103
+ locked: false,
104
+ visible: true,
105
+ },
106
+ ],
107
+ }),
108
+ { mode: 'replace' },
109
+ )
110
+ events.value = []
111
+ before.value = nodeSummary()
112
+ after.value = nodeSummary()
113
+ revision.value++
114
+ }
115
+
116
+ function rename() {
117
+ before.value = nodeSummary()
118
+ events.value = []
119
+ engine.updateNode(CARD_ID, {
120
+ text: 'Review onboarding · approved',
121
+ color: '4',
122
+ })
123
+ after.value = nodeSummary()
124
+ revision.value++
125
+ }
126
+
127
+ function move() {
128
+ before.value = nodeSummary()
129
+ events.value = []
130
+ engine.moveNode(CARD_ID, 80, 40)
131
+ after.value = nodeSummary()
132
+ revision.value++
133
+ }
134
+
135
+ onMounted(async () => {
136
+ disposers.push(
137
+ engine.on('command:before', (name) =>
138
+ events.value.push({ label: 'command:before', detail: name }),
139
+ ),
140
+ engine.on('node:updated', (node) =>
141
+ events.value.push({ label: 'node:updated', detail: node.id }),
142
+ ),
143
+ engine.on('command:after', (name) =>
144
+ events.value.push({
145
+ label: 'command:after',
146
+ detail: name,
147
+ tone: 'success',
148
+ }),
149
+ ),
150
+ )
151
+ seed()
152
+ await engine.zoomToFit(64, false)
153
+ })
154
+
155
+ onBeforeUnmount(() => disposers.forEach((dispose) => dispose()))
156
+ </script>
157
+
158
+ <template>
159
+ <DocsLab
160
+ title="Command inspector"
161
+ description="Run one command and inspect the state change and publication order."
162
+ instructions="Rename or move the selected card. The command commits before public events appear."
163
+ >
164
+ <template #controls>
165
+ <button type="button" @click="seed">Reset</button>
166
+ <button type="button" data-primary="true" @click="rename">
167
+ Rename card
168
+ </button>
169
+ <button type="button" @click="move">Move card</button>
170
+ </template>
171
+ <BoardRoot :engine="engine" style="height: 360px" />
172
+ <template #inspect>
173
+ <StateInspector title="Engine state" :value="state" />
174
+ </template>
175
+ <template #timeline>
176
+ <EventTimeline :entries="events" />
177
+ <DocumentDiff :before="before" :after="after" />
178
+ </template>
179
+ <template #code>
180
+ <CodePreview
181
+ code="engine.updateNode(card.id, {\n text: 'Review onboarding · approved',\n color: '4',\n})"
182
+ />
183
+ </template>
184
+ </DocsLab>
185
+ </template>
186
+ ````
187
+
188
+ ### app/utils/demoDocument.ts
189
+
190
+ ````ts
191
+ import type {
192
+ BoardNode,
193
+ BoardNodeMetadata,
194
+ Camera,
195
+ GridSettings,
196
+ JsonCanvasDocument,
197
+ JsonCanvasNode,
198
+ NodeId,
199
+ } from '@lupinum/board-core'
200
+
201
+ type DemoNode = Omit<
202
+ Pick<
203
+ BoardNode,
204
+ | 'id'
205
+ | 'type'
206
+ | 'x'
207
+ | 'y'
208
+ | 'width'
209
+ | 'height'
210
+ | 'color'
211
+ | 'text'
212
+ | 'file'
213
+ | 'subpath'
214
+ | 'url'
215
+ | 'label'
216
+ | 'background'
217
+ | 'backgroundStyle'
218
+ | 'zIndex'
219
+ | 'locked'
220
+ | 'visible'
221
+ | 'parentId'
222
+ >,
223
+ 'id' | 'parentId'
224
+ > & {
225
+ id: NodeId | string
226
+ parentId?: NodeId | string
227
+ }
228
+
229
+ export interface DemoDocumentInput {
230
+ nodes: readonly DemoNode[]
231
+ camera?: Camera
232
+ grid?: GridSettings
233
+ selection?: readonly (NodeId | string)[]
234
+ nextZIndex?: number
235
+ }
236
+
237
+ function toJsonCanvasNode(node: DemoNode): JsonCanvasNode {
238
+ const base = {
239
+ id: node.id as NodeId,
240
+ type: node.type,
241
+ x: node.x,
242
+ y: node.y,
243
+ width: node.width,
244
+ height: node.height,
245
+ ...(node.color ? { color: node.color } : {}),
246
+ }
247
+
248
+ switch (node.type) {
249
+ case 'file':
250
+ return {
251
+ ...base,
252
+ type: 'file',
253
+ file: node.file ?? '',
254
+ ...(node.subpath ? { subpath: node.subpath } : {}),
255
+ }
256
+ case 'link':
257
+ return {
258
+ ...base,
259
+ type: 'link',
260
+ url: node.url ?? '',
261
+ }
262
+ case 'group':
263
+ return {
264
+ ...base,
265
+ type: 'group',
266
+ ...((node.label ?? node.text)
267
+ ? { label: node.label ?? node.text }
268
+ : {}),
269
+ ...(node.background ? { background: node.background } : {}),
270
+ ...(node.backgroundStyle
271
+ ? { backgroundStyle: node.backgroundStyle }
272
+ : {}),
273
+ }
274
+ case 'text':
275
+ default:
276
+ return {
277
+ ...base,
278
+ type: 'text',
279
+ text: node.text ?? '',
280
+ }
281
+ }
282
+ }
283
+
284
+ function toNodeMetadata(node: DemoNode): BoardNodeMetadata {
285
+ return {
286
+ zIndex: node.zIndex,
287
+ locked: node.locked,
288
+ visible: node.visible,
289
+ ...(node.parentId ? { parentId: node.parentId as NodeId } : {}),
290
+ }
291
+ }
292
+
293
+ export function createDemoDocument(
294
+ input: DemoDocumentInput,
295
+ ): JsonCanvasDocument {
296
+ return {
297
+ nodes: input.nodes.map(toJsonCanvasNode),
298
+ 'x-lupinum-board': {
299
+ ...(input.camera ? { camera: input.camera } : {}),
300
+ ...(input.grid ? { grid: input.grid } : {}),
301
+ ...(input.selection
302
+ ? { selection: input.selection.map((id) => id as NodeId) }
303
+ : {}),
304
+ nextZIndex:
305
+ input.nextZIndex ??
306
+ input.nodes.reduce((max, node) => Math.max(max, node.zIndex), 0) + 1,
307
+ nodes: Object.fromEntries(
308
+ input.nodes.map((node) => [node.id, toNodeMetadata(node)]),
309
+ ),
310
+ },
311
+ }
312
+ }
313
+ ````
314
+
315
+ ### app/components/content/DocsLab.vue
316
+
317
+ ````vue
318
+ <script setup lang="ts">
319
+ withDefaults(
320
+ defineProps<{
321
+ title: string
322
+ description: string
323
+ instructions?: string
324
+ }>(),
325
+ { instructions: undefined },
326
+ )
327
+ </script>
328
+
329
+ <template>
330
+ <section class="docs-lab" :aria-label="title">
331
+ <header class="docs-lab__header">
332
+ <div>
333
+ <h3>{{ title }}</h3>
334
+ <p>{{ description }}</p>
335
+ </div>
336
+ <div v-if="$slots.controls" class="docs-lab__controls">
337
+ <slot name="controls" />
338
+ </div>
339
+ </header>
340
+
341
+ <p v-if="instructions" class="docs-lab__instructions">
342
+ <span>Try this</span>
343
+ {{ instructions }}
344
+ </p>
345
+
346
+ <div class="docs-lab__workspace">
347
+ <div class="docs-lab__stage">
348
+ <slot />
349
+ </div>
350
+ <aside
351
+ v-if="$slots.inspect"
352
+ class="docs-lab__inspect"
353
+ aria-label="Live inspector"
354
+ >
355
+ <slot name="inspect" />
356
+ </aside>
357
+ </div>
358
+
359
+ <div v-if="$slots.timeline" class="docs-lab__timeline">
360
+ <slot name="timeline" />
361
+ </div>
362
+ <div v-if="$slots.code" class="docs-lab__code">
363
+ <slot name="code" />
364
+ </div>
365
+ </section>
366
+ </template>
367
+ ````
368
+
369
+ ### app/components/content/StateInspector.vue
370
+
371
+ ````vue
372
+ <script setup lang="ts">
373
+ import { computed } from 'vue'
374
+
375
+ const props = withDefaults(
376
+ defineProps<{
377
+ title?: string
378
+ value: unknown
379
+ empty?: string
380
+ }>(),
381
+ { title: 'State', empty: 'No value' },
382
+ )
383
+
384
+ const formatted = computed(() => {
385
+ if (props.value === undefined || props.value === null) return props.empty
386
+ return typeof props.value === 'string'
387
+ ? props.value
388
+ : JSON.stringify(props.value, null, 2)
389
+ })
390
+ </script>
391
+
392
+ <template>
393
+ <div class="state-inspector">
394
+ <p class="state-inspector__title">{{ title }}</p>
395
+ <pre aria-live="polite">{{ formatted }}</pre>
396
+ </div>
397
+ </template>
398
+ ````
399
+
400
+ ### app/components/content/CodePreview.vue
401
+
402
+ ````vue
403
+ <script setup lang="ts">
404
+ import { ref } from 'vue'
405
+
406
+ const props = withDefaults(defineProps<{ code: string; label?: string }>(), {
407
+ label: 'Relevant code',
408
+ })
409
+ const copied = ref(false)
410
+
411
+ async function copy() {
412
+ await navigator.clipboard.writeText(props.code)
413
+ copied.value = true
414
+ window.setTimeout(() => (copied.value = false), 1400)
415
+ }
416
+ </script>
417
+
418
+ <template>
419
+ <div class="code-preview">
420
+ <div>
421
+ <span>{{ label }}</span>
422
+ <button type="button" @click="copy">
423
+ {{ copied ? 'Copied' : 'Copy' }}
424
+ </button>
425
+ </div>
426
+ <pre><code>{{ code }}</code></pre>
427
+ </div>
428
+ </template>
429
+ ````
430
+
431
+ ### app/components/content/EventTimeline.vue
432
+
433
+ ````vue
434
+ <script setup lang="ts">
435
+ export interface TimelineEntry {
436
+ label: string
437
+ detail?: string
438
+ tone?: 'default' | 'success' | 'danger'
439
+ }
440
+
441
+ defineProps<{ entries: TimelineEntry[] }>()
442
+ </script>
443
+
444
+ <template>
445
+ <div class="event-timeline" aria-live="polite">
446
+ <p v-if="entries.length === 0" class="event-timeline__empty">
447
+ Run an action to inspect its publication order.
448
+ </p>
449
+ <ol v-else>
450
+ <li
451
+ v-for="(entry, index) in entries"
452
+ :key="`${index}-${entry.label}`"
453
+ :data-tone="entry.tone"
454
+ >
455
+ <span>{{ String(index + 1).padStart(2, '0') }}</span>
456
+ <strong>{{ entry.label }}</strong>
457
+ <code v-if="entry.detail">{{ entry.detail }}</code>
458
+ </li>
459
+ </ol>
460
+ </div>
461
+ </template>
462
+ ````
463
+
464
+ ### app/components/content/DocumentDiff.vue
465
+
466
+ ````vue
467
+ <script setup lang="ts">
468
+ defineProps<{
469
+ before: Record<string, unknown>
470
+ after: Record<string, unknown>
471
+ }>()
472
+ </script>
473
+
474
+ <template>
475
+ <div class="document-diff">
476
+ <div>
477
+ <p>Before</p>
478
+ <pre>{{ JSON.stringify(before, null, 2) }}</pre>
479
+ </div>
480
+ <div>
481
+ <p>After</p>
482
+ <pre>{{ JSON.stringify(after, null, 2) }}</pre>
483
+ </div>
484
+ </div>
485
+ </template>
486
+ ````
38
487
 
39
488
  Run a command in the lab. The document changes before queued public events publish. Subscribers never receive an intermediate candidate.
40
489
 
@@ -17,7 +17,7 @@ Contributions are welcome when they keep the library easier to use, test, and ma
17
17
 
18
18
  ## Set up the repository
19
19
 
20
- The canonical contribution policy is [CONTRIBUTING.md](https://github.com/lupinum-dev/nuxt-board/blob/main/CONTRIBUTING.md). From the repository root:
20
+ The canonical contribution policy is [CONTRIBUTING.md](https://github.com/lupinum-dev/nuxt-board/blob/main/.github/CONTRIBUTING.md). From the repository root:
21
21
 
22
22
  ```bash
23
23
  corepack enable
@@ -27,8 +27,8 @@ pnpm install
27
27
  Start the playground or docs app:
28
28
 
29
29
  ```bash
30
- pnpm dev:playground
31
- pnpm dev:docs
30
+ pnpm dev
31
+ pnpm docs:dev
32
32
  ```
33
33
 
34
34
  The playground is the fastest place to verify interaction changes. The docs app uses the workspace packages, so examples exercise the same code consumers install.
@@ -41,7 +41,7 @@ Run the narrowest relevant check while you work, then the repository gate before
41
41
  pnpm verify
42
42
  ```
43
43
 
44
- Use `pnpm test:e2e` for browser interaction or screenshot changes. Maintainers use `pnpm release:verify` only on a release candidate.
44
+ Use `pnpm test:e2e` for browser interaction or screenshot changes.
45
45
 
46
46
  ## Where docs live
47
47
 
@@ -37,4 +37,4 @@ email `info@lupinum.com`. Do not open a public issue with exploit details.
37
37
 
38
38
  Include the affected package, the suspected impact, and reproduction steps if you can share them safely.
39
39
 
40
- The root [`SECURITY.md`](https://github.com/lupinum-dev/nuxt-board/blob/main/SECURITY.md) is the source of truth for the current reporting policy.
40
+ The [`SECURITY.md`](https://github.com/lupinum-dev/nuxt-board/blob/main/.github/SECURITY.md) is the source of truth for the current reporting policy.