@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
|
@@ -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
|
-
|
|
37
|
-
|
|
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
|
|
31
|
-
pnpm dev
|
|
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.
|
|
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
|
|
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.
|