@marver-design/marver 0.16.0 → 0.16.1
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 +26 -0
- package/README.md +2 -2
- package/dist/{boards-BdG1TJwU.mjs → boards-BmxcT3Lc.mjs} +32 -9
- package/dist/{boards-6hKVW42a.mjs → boards-PuVzw5Wp.mjs} +9 -4
- package/dist/{build-Ct0iMoSi.mjs → build-B4yPgFNF.mjs} +12 -8
- package/dist/cli.mjs +25 -14
- package/dist/{daemon-Cb4JjpgL.mjs → daemon-Bbh_jmui.mjs} +1 -1
- package/dist/{dev-CdcIuhZJ.mjs → dev-DH2W7Ffw.mjs} +4 -4
- package/dist/{init-BQIpIpIv.mjs → init-B7YhcN2o.mjs} +1 -1
- package/dist/{manifest-CpbsqQ_v.mjs → manifest-CaslQIAO.mjs} +112 -15
- package/dist/{plugin-BXyezwfN.mjs → plugin-BeBGu3gH.mjs} +85 -23
- package/dist/{poster-DOY7pax8.mjs → poster-CoyobbGW.mjs} +1 -1
- package/dist/{shot-CwmHO5T4.mjs → shot-z-d-zMzf.mjs} +2 -2
- package/docs/publish.md +2 -2
- package/package.json +1 -1
- package/src/client/shell/App.tsx +42 -14
- package/src/client/shell/BoardList.tsx +88 -58
- package/src/client/shell/LockedApp.tsx +2 -1
- package/src/client/shell/Play.tsx +1 -0
- package/src/client/shell/labels.ts +4 -4
- package/src/client/shell/store.ts +76 -29
- package/src/client/shell/styles.css +5 -1
- package/src/shared/board-tree.ts +104 -46
- package/templates/AGENTS-embedded.md +9 -2
- package/templates/AGENTS-studio.md +9 -2
- package/templates/instructions/boards.md +27 -9
- package/templates/instructions/discover.md +13 -1
package/src/shared/board-tree.ts
CHANGED
|
@@ -17,11 +17,11 @@ export const FOLDERS_FILE = '_folders.json'
|
|
|
17
17
|
* off-grammar name are not - every lister (dev API, build, watcher) shares this rule. */
|
|
18
18
|
export const isBoardFile = (f: string): boolean => f.endsWith('.json') && isBoardName(f.slice(0, -5))
|
|
19
19
|
|
|
20
|
-
export type Folder = { kind: 'folder'; name: string; boards: string[]; description?: string }
|
|
20
|
+
export type Folder = { kind: 'folder'; name: string; boards: string[]; title?: string; description?: string }
|
|
21
21
|
export type TreeItem = { kind: 'board'; name: string } | Folder
|
|
22
22
|
|
|
23
|
-
export interface BoardRow { name: string; order?: number; folder?: string }
|
|
24
|
-
export interface FolderRow { name: string; order?: number; description?: string }
|
|
23
|
+
export interface BoardRow { name: string; order?: number; folder?: string; title?: string }
|
|
24
|
+
export interface FolderRow { name: string; order?: number; title?: string; description?: string }
|
|
25
25
|
|
|
26
26
|
/** A description off a file: one sentence, trimmed, capped - absent when empty or not a string. */
|
|
27
27
|
export const DESCRIPTION_MAX = 300
|
|
@@ -31,7 +31,24 @@ export const readDescription = (v: unknown): string | undefined => {
|
|
|
31
31
|
return s || undefined
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/** A title off a file - what humans see, free text: any casing, punctuation, emoji. Control
|
|
35
|
+
* characters dropped, whitespace collapsed, capped in code points (never half a surrogate
|
|
36
|
+
* pair). Absent when empty or not a string; the display then falls back to the slug. */
|
|
37
|
+
export const TITLE_MAX = 120
|
|
38
|
+
export const readTitle = (v: unknown): string | undefined => {
|
|
39
|
+
if (typeof v !== 'string') return undefined
|
|
40
|
+
// eslint-disable-next-line no-control-regex
|
|
41
|
+
const s = Array.from(v.replace(/[\u0000-\u001f\u007f]/g, ' ').trim().replace(/\s+/g, ' ')).slice(0, TITLE_MAX).join('').trim()
|
|
42
|
+
return s || undefined
|
|
43
|
+
}
|
|
44
|
+
/** Title Case off a slug - the display when no title is set ("old-stuff" → "Old Stuff"). */
|
|
45
|
+
export const humanize = (s: string): string => s.replace(/-/g, ' ').replace(/(^|\s)\S/g, (c) => c.toUpperCase())
|
|
46
|
+
/** What a board, folder or scene is called on screen: its title, else its humanized slug. */
|
|
47
|
+
export const labelOf = (name: string, title?: string): string => title ?? humanize(name)
|
|
48
|
+
|
|
34
49
|
const rank = (o: number | undefined) => (typeof o === 'number' && Number.isFinite(o) ? o : Infinity)
|
|
50
|
+
/** A folder's title and description, present only when set. */
|
|
51
|
+
const folderExtras = (it: { title?: string; description?: string }) => ({ ...(it.title ? { title: it.title } : {}), ...(it.description ? { description: it.description } : {}) })
|
|
35
52
|
|
|
36
53
|
/** The registry file's shape. Returns the rows, or a string naming what is wrong - a
|
|
37
54
|
* malformed registry is an ERROR the human must fix (silently reading it as empty would
|
|
@@ -49,19 +66,22 @@ export function parseFolders(raw: unknown): FolderRow[] | string {
|
|
|
49
66
|
if (seen.has(name)) return `folder "${name}" is listed twice`
|
|
50
67
|
seen.add(name)
|
|
51
68
|
const o = (f as { order?: unknown }).order
|
|
69
|
+
const t = readTitle((f as { title?: unknown }).title)
|
|
52
70
|
const d = readDescription((f as { description?: unknown }).description)
|
|
53
|
-
out.push({ name, ...(typeof o === 'number' && Number.isFinite(o) ? { order: o } : {}), ...(d ? { description: d } : {}) })
|
|
71
|
+
out.push({ name, ...(typeof o === 'number' && Number.isFinite(o) ? { order: o } : {}), ...(t ? { title: t } : {}), ...(d ? { description: d } : {}) })
|
|
54
72
|
}
|
|
55
73
|
return out
|
|
56
74
|
}
|
|
57
75
|
|
|
58
76
|
/** Sidebar order from the files. Root: boards with no folder + every folder (registered
|
|
59
77
|
* or implied by a board), ranked by `order` then kind (board before folder) then name.
|
|
60
|
-
* Inside a folder: its boards by `order` then name. Unranked sorts after ranked.
|
|
78
|
+
* Inside a folder: its boards by `order` then name. Unranked sorts after ranked. A folder's
|
|
79
|
+
* title and description ride on its item (they live in the registry a tree write rewrites);
|
|
80
|
+
* a board's title stays with its row - it lives in the board's own file. */
|
|
61
81
|
export function buildTree(boards: BoardRow[], folders: FolderRow[]): TreeItem[] {
|
|
62
82
|
const folderOrder = new Map<string, number | undefined>()
|
|
63
|
-
const
|
|
64
|
-
for (const f of folders) if (isBoardName(f.name) && !folderOrder.has(f.name)) { folderOrder.set(f.name, f.order);
|
|
83
|
+
const folderMeta = new Map<string, { title?: string; description?: string }>()
|
|
84
|
+
for (const f of folders) if (isBoardName(f.name) && !folderOrder.has(f.name)) { folderOrder.set(f.name, f.order); folderMeta.set(f.name, folderExtras(f)) }
|
|
65
85
|
const members = new Map<string, BoardRow[]>()
|
|
66
86
|
const rootBoards: BoardRow[] = []
|
|
67
87
|
for (const b of boards) {
|
|
@@ -78,7 +98,7 @@ export function buildTree(boards: BoardRow[], folders: FolderRow[]): TreeItem[]
|
|
|
78
98
|
const root: Root[] = [
|
|
79
99
|
...rootBoards.map((b) => ({ item: { kind: 'board', name: b.name } as TreeItem, order: b.order })),
|
|
80
100
|
...[...folderOrder].map(([name, order]) => ({
|
|
81
|
-
item: { kind: 'folder', name, boards: (members.get(name) ?? []).sort(byRank).map((b) => b.name), ...(
|
|
101
|
+
item: { kind: 'folder', name, boards: (members.get(name) ?? []).sort(byRank).map((b) => b.name), ...(folderMeta.get(name) ?? {}) } as TreeItem,
|
|
82
102
|
order,
|
|
83
103
|
})),
|
|
84
104
|
]
|
|
@@ -94,12 +114,14 @@ export function flatten(tree: TreeItem[]): string[] {
|
|
|
94
114
|
}
|
|
95
115
|
|
|
96
116
|
/** The wire shape of a tree write (`POST boards/reorder`): plain strings for root boards,
|
|
97
|
-
* `{ folder, boards }` for folders - what the sidebar posts and what the server validates.
|
|
98
|
-
|
|
117
|
+
* `{ folder, boards }` for folders - what the sidebar posts and what the server validates.
|
|
118
|
+
* A folder's title and description ride with it (they live in the registry the write
|
|
119
|
+
* rewrites); a board's title lives in its own file and never rides the tree. */
|
|
120
|
+
export type WireItem = string | { folder: string; boards: string[]; title?: string; description?: string }
|
|
99
121
|
export const toWire = (tree: TreeItem[]): WireItem[] =>
|
|
100
|
-
tree.map((it) => (it.kind === 'board' ? it.name : { folder: it.name, boards: [...it.boards], ...(it
|
|
122
|
+
tree.map((it) => (it.kind === 'board' ? it.name : { folder: it.name, boards: [...it.boards], ...folderExtras(it) }))
|
|
101
123
|
export const fromWire = (wire: WireItem[]): TreeItem[] =>
|
|
102
|
-
wire.map((w) => (typeof w === 'string' ? { kind: 'board', name: w } : { kind: 'folder', name: w.folder, boards: [...w.boards], ...(w
|
|
124
|
+
wire.map((w) => (typeof w === 'string' ? { kind: 'board', name: w } : { kind: 'folder', name: w.folder, boards: [...w.boards], ...folderExtras(w) }))
|
|
103
125
|
|
|
104
126
|
/** Validate a wire tree off the network. Returns the error, or null when it is sound:
|
|
105
127
|
* every name on-grammar, `all-scenes` nowhere, no board twice, no folder twice, no
|
|
@@ -118,8 +140,9 @@ export function validateWire(wire: unknown): string | null {
|
|
|
118
140
|
for (const w of wire) {
|
|
119
141
|
if (typeof w === 'string') { const e = board(w); if (e) return e; continue }
|
|
120
142
|
if (!w || typeof w !== 'object' || Array.isArray(w)) return 'invalid tree item'
|
|
121
|
-
const { folder, boards: kids, description } = w as { folder?: unknown; boards?: unknown; description?: unknown }
|
|
143
|
+
const { folder, boards: kids, title, description } = w as { folder?: unknown; boards?: unknown; title?: unknown; description?: unknown }
|
|
122
144
|
if (!isBoardName(folder)) return 'invalid folder name in tree'
|
|
145
|
+
if (title !== undefined && (typeof title !== 'string' || Array.from(title).length > TITLE_MAX)) return 'invalid folder title'
|
|
123
146
|
if (description !== undefined && (typeof description !== 'string' || description.length > DESCRIPTION_MAX)) return 'invalid folder description'
|
|
124
147
|
if (folders.has(folder)) return `folder "${folder}" appears twice`
|
|
125
148
|
folders.add(folder)
|
|
@@ -163,35 +186,59 @@ export function takeBoard(t: TreeItem[], board: string): { list: string | null;
|
|
|
163
186
|
export type Drag = { kind: TreeItem['kind']; name: string }
|
|
164
187
|
export type Drop = { list: string | null; index: number } | { into: string }
|
|
165
188
|
|
|
166
|
-
/**
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
gutter: boolean // pointer left of a child row's indent (the root gutter = outdent)
|
|
173
|
-
}
|
|
189
|
+
/** A row the sidebar rendered, measured - the list as the human sees it, top to bottom,
|
|
190
|
+
* the pinned `all-scenes` row included (it is the root's end). `open` is the folder's
|
|
191
|
+
* disclosure; `left` is the row's left edge, from which the child indent is measured. */
|
|
192
|
+
export interface Row { kind: TreeItem['kind']; name: string; parent: string | null; open?: boolean; top: number; bottom: number; left: number }
|
|
193
|
+
/** Px a board row indents inside a folder; the seams inside draw from there too. */
|
|
194
|
+
export const INDENT = 28
|
|
174
195
|
|
|
175
|
-
/** The drop target for a
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
196
|
+
/** The drop target for a pointer at (x, y) over the rendered rows - never null inside the
|
|
197
|
+
* list (the ends clamp), so a release anywhere over the sidebar lands somewhere the seam
|
|
198
|
+
* showed. Boards: the middle band of a folder header (a closed one: its lower band too) is
|
|
199
|
+
* INTO it; otherwise the nearest gap by row midlines, read from the row above it: after a
|
|
200
|
+
* root board or a closed folder = root; after an open folder's header, or between two of
|
|
201
|
+
* its boards = inside; after a folder's LAST board (or under an open empty folder) the gap
|
|
202
|
+
* is shared between the folder and the root, and the row under the pointer decides: still
|
|
203
|
+
* over that last board (that header), at or right of the indent the seams draw at = inside;
|
|
204
|
+
* over the root row below it, or in the gutter = root, after the folder. Folders: root gaps
|
|
205
|
+
* only, each root item one block. */
|
|
206
|
+
export function resolveDrop(t: TreeItem[], d: Drag, rows: Row[], x: number, y: number): Drop | null {
|
|
207
|
+
const mid = (r: { top: number; bottom: number }) => (r.top + r.bottom) / 2
|
|
208
|
+
if (d.kind === 'folder') {
|
|
209
|
+
const blocks: { top: number; bottom: number }[] = []
|
|
210
|
+
for (const r of rows) {
|
|
211
|
+
if (r.parent && blocks.length) { blocks[blocks.length - 1]!.bottom = r.bottom; continue }
|
|
212
|
+
blocks.push({ top: r.top, bottom: r.bottom })
|
|
213
|
+
}
|
|
214
|
+
let g = 0
|
|
215
|
+
for (const b of blocks) if (y >= mid(b)) g++
|
|
216
|
+
return { list: null, index: Math.min(g, t.length) }
|
|
217
|
+
}
|
|
218
|
+
for (const r of rows) {
|
|
219
|
+
if (r.kind !== 'folder' || y < r.top || y >= r.bottom) continue
|
|
220
|
+
const f = (y - r.top) / (r.bottom - r.top)
|
|
221
|
+
if (f >= 0.25 && (f < 0.75 || !r.open)) return { into: r.name }
|
|
184
222
|
}
|
|
185
|
-
|
|
186
|
-
if (
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
223
|
+
let g = 0
|
|
224
|
+
for (const r of rows) if (y >= mid(r)) g++
|
|
225
|
+
const above = rows[g - 1], below = rows[g]
|
|
226
|
+
if (!above) return { list: null, index: 0 }
|
|
227
|
+
if (above.name === 'all-scenes') return { list: null, index: t.length }
|
|
228
|
+
const inside = (folder: string, index: number, alt: Drop): Drop => (y < above.bottom && x >= above.left + INDENT ? { list: folder, index } : alt)
|
|
229
|
+
if (above.kind === 'board') {
|
|
230
|
+
if (!above.parent) { const i = rootIndex(t, 'board', above.name); return i < 0 ? null : { list: null, index: i + 1 } }
|
|
231
|
+
const fi = rootIndex(t, 'folder', above.parent)
|
|
232
|
+
const i = folderIn(t, above.parent)?.boards.indexOf(above.name) ?? -1
|
|
233
|
+
if (fi < 0 || i < 0) return null
|
|
234
|
+
if (below?.kind === 'board' && below.parent === above.parent) return { list: above.parent, index: i + 1 }
|
|
235
|
+
return inside(above.parent, i + 1, { list: null, index: fi + 1 })
|
|
192
236
|
}
|
|
193
|
-
const
|
|
194
|
-
|
|
237
|
+
const fi = rootIndex(t, 'folder', above.name)
|
|
238
|
+
if (fi < 0) return null
|
|
239
|
+
if (!above.open) return { list: null, index: fi + 1 }
|
|
240
|
+
if (below?.kind === 'board' && below.parent === above.name) return { list: above.name, index: 0 }
|
|
241
|
+
return inside(above.name, 0, { list: null, index: fi + 1 })
|
|
195
242
|
}
|
|
196
243
|
|
|
197
244
|
/** A target that would leave the item where it is: nothing to show, nothing to drop. Into
|
|
@@ -248,25 +295,36 @@ export function moveBoard(tree: TreeItem[], board: string, folder: string | null
|
|
|
248
295
|
}
|
|
249
296
|
|
|
250
297
|
/** A new folder at root `index`, holding `board` (pulled from wherever it sat) when given. */
|
|
251
|
-
export function createFolder(tree: TreeItem[], name: string, index: number, board?: string): TreeItem[] | null {
|
|
298
|
+
export function createFolder(tree: TreeItem[], name: string, index: number, board?: string, title?: string): TreeItem[] | null {
|
|
252
299
|
if (foldersIn(tree).includes(name)) return null
|
|
253
300
|
const next = cloneTree(tree)
|
|
254
301
|
const boards: string[] = []
|
|
255
302
|
if (board) { if (!takeBoard(next, board)) return null; boards.push(board) }
|
|
256
|
-
next.splice(Math.min(index, next.length), 0, { kind: 'folder', name, boards })
|
|
303
|
+
next.splice(Math.min(index, next.length), 0, { kind: 'folder', name, boards, ...(title ? { title } : {}) })
|
|
257
304
|
return next
|
|
258
305
|
}
|
|
259
306
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
307
|
+
/** Retitle a folder: what humans see (''= clear, back to the Title-Cased slug). The slug is
|
|
308
|
+
* its identity - `folder:` on every member board, the registry key - and never changes here. */
|
|
309
|
+
export function retitleFolder(tree: TreeItem[], name: string, title: string): TreeItem[] | null {
|
|
263
310
|
const next = cloneTree(tree)
|
|
264
|
-
const f = folderIn(next,
|
|
311
|
+
const f = folderIn(next, name)
|
|
265
312
|
if (!f) return null
|
|
266
|
-
f.
|
|
313
|
+
if (title) f.title = title; else delete f.title
|
|
267
314
|
return next
|
|
268
315
|
}
|
|
269
316
|
|
|
317
|
+
/** The slug a NEW folder gets from the title the human typed: `slugify(title)`, then `-2`,
|
|
318
|
+
* `-3`, ... past a taken one; `fallback` (and `fallback-2`, ...) when nothing survives
|
|
319
|
+
* slugifying ("🚀"). Slugs are minted once - a rename changes the title, never the slug. */
|
|
320
|
+
export function slugFor(title: string, taken: string[], fallback = 'folder'): string {
|
|
321
|
+
const s = slugify(title)
|
|
322
|
+
if (s && !taken.includes(s)) return s
|
|
323
|
+
const base = s || fallback
|
|
324
|
+
if (!s && !taken.includes(base)) return base
|
|
325
|
+
for (let i = 2; ; i++) { const c = `${base.slice(0, NAME_MAX - 1 - String(i).length)}-${i}`; if (!taken.includes(c)) return c }
|
|
326
|
+
}
|
|
327
|
+
|
|
270
328
|
/** Folders organise, never own: deleting one puts its boards back at the root, in its slot, in order. */
|
|
271
329
|
export function deleteFolder(tree: TreeItem[], name: string): TreeItem[] | null {
|
|
272
330
|
const next = cloneTree(tree)
|
|
@@ -133,8 +133,15 @@ Report where the request came from: chat requests get chat replies; only comment
|
|
|
133
133
|
## Orientation
|
|
134
134
|
- design/manifest.json is the canvas with its purpose: the project (name, description),
|
|
135
135
|
every folder and board in sidebar order, every scene and frame - each with its
|
|
136
|
-
`description` when one was written
|
|
137
|
-
fresh. `npx marver boards` prints the
|
|
136
|
+
`description` when one was written, and its `title` when the human (or you) named it.
|
|
137
|
+
Read it before exploring; `marver dev` keeps it fresh. `npx marver boards` prints the
|
|
138
|
+
boards part as a tree.
|
|
139
|
+
- **Names vs titles.** A board's file name, a folder's `name`, a scene's directory are
|
|
140
|
+
IDENTITIES: slugs you address, that publish.json, URLs and comment threads hold, and
|
|
141
|
+
that never move on a rename. `title` is what humans see - free text (casing,
|
|
142
|
+
punctuation, emoji) on the board JSON, the `_folders.json` entry, the brief's front
|
|
143
|
+
matter; frames have `meta.title`. When the human says "the Checkout A/B board", the
|
|
144
|
+
manifest maps that title to its slug. Rename a slug only when asked, as one refactor.
|
|
138
145
|
- **Descriptions.** Every object takes one optional `description`: one sentence, what it
|
|
139
146
|
is for and its state when that is not obvious (≤ ~160 chars). Project: `description`
|
|
140
147
|
in design/config.ts. Board: `"description"` in its JSON. Folder: on its `_folders.json`
|
|
@@ -132,8 +132,15 @@ Report where the request came from: chat requests get chat replies; only comment
|
|
|
132
132
|
## Orientation
|
|
133
133
|
- design/manifest.json is the canvas with its purpose: the project (name, description),
|
|
134
134
|
every folder and board in sidebar order, every scene and frame - each with its
|
|
135
|
-
`description` when one was written
|
|
136
|
-
fresh. `npx marver boards` prints the
|
|
135
|
+
`description` when one was written, and its `title` when the human (or you) named it.
|
|
136
|
+
Read it before exploring; `marver dev` keeps it fresh. `npx marver boards` prints the
|
|
137
|
+
boards part as a tree.
|
|
138
|
+
- **Names vs titles.** A board's file name, a folder's `name`, a scene's directory are
|
|
139
|
+
IDENTITIES: slugs you address, that publish.json, URLs and comment threads hold, and
|
|
140
|
+
that never move on a rename. `title` is what humans see - free text (casing,
|
|
141
|
+
punctuation, emoji) on the board JSON, the `_folders.json` entry, the brief's front
|
|
142
|
+
matter; frames have `meta.title`. When the human says "the Checkout A/B board", the
|
|
143
|
+
manifest maps that title to its slug. Rename a slug only when asked, as one refactor.
|
|
137
144
|
- **Descriptions.** Every object takes one optional `description`: one sentence, what it
|
|
138
145
|
is for and its state when that is not obvious (≤ ~160 chars). Project: `description`
|
|
139
146
|
in design/config.ts. Board: `"description"` in its JSON. Folder: on its `_folders.json`
|
|
@@ -10,10 +10,20 @@ viewport and lays it out:
|
|
|
10
10
|
|
|
11
11
|
```json
|
|
12
12
|
{ "version": 1, "name": "checkout-compare", "order": 1, "auto": false,
|
|
13
|
+
"title": "Checkout A/B",
|
|
13
14
|
"description": "Cart step, direction A vs B side by side - B is the current favourite",
|
|
14
15
|
"nodes": [ { "frame": "checkout-a/cart" }, { "frame": "checkout-b/cart" } ] }
|
|
15
16
|
```
|
|
16
17
|
|
|
18
|
+
- **The file name is the board's identity** - what you, `publish.json`, URLs and
|
|
19
|
+
comment threads address (`board: checkout-compare`). It is a slug
|
|
20
|
+
(`^[a-z0-9][a-z0-9-]*$`) and never moves on a rename.
|
|
21
|
+
- `title` - what humans SEE: free text, any casing, punctuation, emoji ("MVP", "UI",
|
|
22
|
+
"Checkout (v2) 🛒"). Optional: without one the sidebar Title-Cases the slug
|
|
23
|
+
(`checkout-compare` → "Checkout Compare"), which is fine for most boards. Write a
|
|
24
|
+
title when the slug would read wrong (`mvp` → "Mvp") or when the human names it.
|
|
25
|
+
The human's Rename in the sidebar edits the title only; the manifest carries both, so
|
|
26
|
+
"the Checkout A/B board" resolves to `checkout-compare`.
|
|
17
27
|
- `description` - one sentence on what the board is for and where it stands. It is
|
|
18
28
|
how a later session (or the human's next agent) knows this board without opening
|
|
19
29
|
it; it lands in design/manifest.json. Write it at creation, keep it true.
|
|
@@ -28,8 +38,9 @@ viewport and lays it out:
|
|
|
28
38
|
orienting board (an overview or the primary flow) - never a giant one. Boards
|
|
29
39
|
without an `order` sort after the ranked ones, by name. Set `order` deliberately on
|
|
30
40
|
every curated board; it is the first impression. The human can also drag-reorder boards
|
|
31
|
-
in the sidebar (which rewrites `order`),
|
|
32
|
-
boards into folders (below) - so your ranking is a starting
|
|
41
|
+
in the sidebar (which rewrites `order`), retitle one from its right-click menu (which
|
|
42
|
+
writes `title`), and file boards into folders (below) - so your ranking is a starting
|
|
43
|
+
point they may adjust.
|
|
33
44
|
- `auto: false` boards show exactly their list. `all-scenes` is auto-managed (it holds
|
|
34
45
|
EVERY frame, so it is the heavy one) and always sinks to the BOTTOM of the switcher -
|
|
35
46
|
never the landing board, and never write its file.
|
|
@@ -64,12 +75,14 @@ Files are the truth, and two files carry it:
|
|
|
64
75
|
|
|
65
76
|
```json
|
|
66
77
|
{ "version": 1, "folders": [
|
|
67
|
-
{ "name": "research", "order": 1, "description": "The thinking behind the live boards - specs, flows, references" },
|
|
78
|
+
{ "name": "research", "order": 1, "title": "R&D", "description": "The thinking behind the live boards - specs, flows, references" },
|
|
68
79
|
{ "name": "archive", "order": 3, "description": "Retired directions and scene versions, oldest first" } ] }
|
|
69
80
|
```
|
|
70
81
|
|
|
71
|
-
A folder's `
|
|
72
|
-
|
|
82
|
+
A folder's `name` is its slug - the identity its boards point at with `folder`; its
|
|
83
|
+
`title` (optional, free text) is what humans see, exactly as on a board; its
|
|
84
|
+
`description` says what belongs in it - the next session files boards right without
|
|
85
|
+
asking.
|
|
73
86
|
|
|
74
87
|
It exists so an EMPTY folder can exist and so a folder has a rank at the root.
|
|
75
88
|
A folder a board names but the registry lacks is still real (it sorts after the
|
|
@@ -91,8 +104,12 @@ The moves, each a file edit, so the files always agree:
|
|
|
91
104
|
an `order` among the top-level items.
|
|
92
105
|
- **Rank** folders and boards: `order` on the board (among its siblings) and on the
|
|
93
106
|
registry entry (among the top-level items). Renumber the siblings you touch.
|
|
94
|
-
- **
|
|
95
|
-
|
|
107
|
+
- **Retitle** a folder (or a board): set `title` on the registry entry (on the board
|
|
108
|
+
file). **Rename a slug** - a folder's `name`, a board's file name - only when asked,
|
|
109
|
+
and as one refactor: a folder slug is on every member's `folder` field (rewrite them
|
|
110
|
+
all, AND the registry entry - a registry rename alone leaves the members in the old,
|
|
111
|
+
implied folder); a board file name is in `publish.json`, in its comment threads and in
|
|
112
|
+
every path anyone copied. A title does what a rename usually wanted.
|
|
96
113
|
- **Delete** a folder: remove `folder` from every member, then its registry entry.
|
|
97
114
|
Folders organise, never own: deleting one never deletes a board.
|
|
98
115
|
- The **landing board** is the first board in sidebar order, folders included -
|
|
@@ -105,8 +122,9 @@ last. Propose the grouping in one sentence and do it; keep folder names short an
|
|
|
105
122
|
plain.
|
|
106
123
|
|
|
107
124
|
The human does all of this too - from the sidebar: New folder (right-click the Boards
|
|
108
|
-
header, or its `+`), Rename
|
|
109
|
-
into and out of folders, folders among
|
|
125
|
+
header, or its `+`), Rename (the title - slugs never move from the sidebar), Delete
|
|
126
|
+
folder, "Move to …" on a board, and DRAG: boards into and out of folders, folders among
|
|
127
|
+
boards. Each drag rewrites `order` (and
|
|
110
128
|
`folder`) on the boards it touches and the registry - the shell owns those fields
|
|
111
129
|
while the canvas is open, exactly as it owns `order`; write membership and new
|
|
112
130
|
folders freely, and never rewrite an arrangement the human just made. The shell
|
|
@@ -45,7 +45,19 @@ e.g. `# Checkout - the buyer's path from cart to receipt (v2, after the pricing
|
|
|
45
45
|
That line lands in `design/manifest.json` as the scene's `description`, so a later
|
|
46
46
|
session reads it without opening the brief; keep it true as the scene moves on (a
|
|
47
47
|
scene that only needs a gist - a version snapshot, an archive - gets a one-line brief).
|
|
48
|
-
|
|
48
|
+
A scene's directory is its identity (every frame id starts with it); what humans SEE
|
|
49
|
+
for it is `title` in the brief's YAML front matter - optional, free text - the sidebar
|
|
50
|
+
Title-Cases the directory otherwise (`checkout` → "Checkout", but `mvp` → "Mvp"):
|
|
51
|
+
|
|
52
|
+
```md
|
|
53
|
+
---
|
|
54
|
+
title: "Checkout (v2)"
|
|
55
|
+
---
|
|
56
|
+
# Checkout - the buyer's path from cart to receipt (v2, after the pricing pivot)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The human's Rename on a scene writes exactly that line; leave the block alone and
|
|
60
|
+
never rename the directory for a title. Show the brief. Get the nod.
|
|
49
61
|
|
|
50
62
|
**Unattended?** When the human is away or has said "don't ask", the interview and
|
|
51
63
|
the nod convert to obligations, not blockers: answer the five questions yourself
|