@marver-design/marver 0.20.0 → 0.21.0
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/dist/{bake-tAb0D6Rc.mjs → bake-BID6mo-N.mjs} +1 -1
- package/dist/{boards-BmxcT3Lc.mjs → boards-BwiDAmPf.mjs} +95 -48
- package/dist/{boards-PuVzw5Wp.mjs → boards-DnLewfj8.mjs} +20 -11
- package/dist/{build-DwpNPl6Z.mjs → build-C7MqQ7hq.mjs} +18 -8
- package/dist/cli.mjs +4 -4
- package/dist/{daemon-DbHvLQUL.mjs → daemon-CRZFpl6K.mjs} +1 -1
- package/dist/{dev-BjdDP69b.mjs → dev-BNZF4Mup.mjs} +5 -5
- package/dist/{init-DKxuRxBr.mjs → init-C34BY3R4.mjs} +1 -1
- package/dist/{manifest-B01PSyDc.mjs → manifest-mMfUhPtL.mjs} +8 -5
- package/dist/{plugin-y4Ch7o_A.mjs → plugin-omHLCn91.mjs} +34 -26
- package/dist/{poster-FCv_nXyP.mjs → poster-BvxiAzy1.mjs} +1 -1
- package/dist/{publish-bakes-XH6Bac58.mjs → publish-bakes-BqzAAa3w.mjs} +2 -2
- package/dist/{shot-iw3SEcpn.mjs → shot-DswS4iRK.mjs} +2 -2
- package/package.json +1 -1
- package/src/client/shell/BoardList.tsx +111 -52
- package/src/client/shell/store.ts +5 -5
- package/src/client/shell/styles.css +10 -9
- package/src/shared/board-tree.ts +271 -140
- package/templates/AGENTS-embedded.md +3 -3
- package/templates/AGENTS-studio.md +3 -3
- package/templates/instructions/boards.md +47 -22
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
Notable changes to `@marver-design/marver`. Format follows [Keep a Changelog](https://keepachangelog.com); versions follow semver.
|
|
4
4
|
|
|
5
|
+
## 0.21.0 - 2026-10-05
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **Folders in folders.** A folder can now hold folders, one level down, so the sidebar has two
|
|
10
|
+
levels: a board sits at the root, in a folder, or in a sub-folder. Every folder move works at
|
|
11
|
+
both levels, from the sidebar and from the files: **New folder inside** on a top-level folder's
|
|
12
|
+
menu, **Move to top level** for a sub-folder, boards dragged into a sub-folder or between its
|
|
13
|
+
boards, a folder without sub-folders dragged into a top-level folder. Deleting a folder moves
|
|
14
|
+
what it held up one level, into its place - never a board lost. **Move to new folder** now
|
|
15
|
+
makes the folder at the board's own level: a sub-folder when the board sits in a folder.
|
|
16
|
+
- The files carry it with one new field: a sub-folder's entry in `design/boards/_folders.json`
|
|
17
|
+
names its `"parent"`. A board's `folder` stays the one folder it sits in directly. `npx marver
|
|
18
|
+
boards` prints the nested tree, the manifest lists each folder's `parent`, and a published
|
|
19
|
+
canvas keeps the nesting of its published boards (a folder with nothing published at any depth
|
|
20
|
+
stays out of the bundle). `instructions/boards.md` teaches agents the moves.
|
|
21
|
+
|
|
22
|
+
### Upgrading
|
|
23
|
+
|
|
24
|
+
- **The registry says `"version": 2` while any folder nests, and 1 when none does.** Marver 0.20
|
|
25
|
+
and earlier refuse a version-2 registry with an error instead of rewriting it flat, so a
|
|
26
|
+
teammate on an older version sees a clear message - upgrade the whole team before nesting.
|
|
27
|
+
A canvas tab opened before the upgrade is refused the same way once folders nest: reload it.
|
|
28
|
+
- Run `npx marver init` to take the new folders guidance (unedited instruction files update in
|
|
29
|
+
place; edited ones are staged in `design/.local/latest/`).
|
|
30
|
+
|
|
5
31
|
## 0.20.0 - 2026-10-01
|
|
6
32
|
|
|
7
33
|
### Changed
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AREA, SURFACE, pool, shotConcurrency, withBrowser } from "./shot-
|
|
1
|
+
import { AREA, SURFACE, pool, shotConcurrency, withBrowser } from "./shot-DswS4iRK.mjs";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, utimesSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join, sep } from "node:path";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
@@ -9,9 +9,11 @@ const hash = (s) => createHash("sha256").update(s).digest("hex");
|
|
|
9
9
|
/**
|
|
10
10
|
* Board folders - the pure tree shared by the sidebar, the dev API, the build and the
|
|
11
11
|
* tests. Files are the truth: a board says which folder it sits in (`folder` on the
|
|
12
|
-
* board file, ranked among its siblings by
|
|
13
|
-
* says which folders exist
|
|
14
|
-
*
|
|
12
|
+
* board file - one slug, the folder it sits in directly), ranked among its siblings by
|
|
13
|
+
* `order`, and `design/boards/_folders.json` says which folders exist, where they rank, and
|
|
14
|
+
* which folder holds which (`parent`). Two levels: a folder holds boards and folders, a
|
|
15
|
+
* folder inside a folder (a sub-folder) holds boards only. `all-scenes` never enters the
|
|
16
|
+
* tree - callers pin it last.
|
|
15
17
|
*/
|
|
16
18
|
/** The on-disk name grammar shared by boards and folders (a board name is a filename). */
|
|
17
19
|
const BOARD_NAME = /^[a-z0-9][a-z0-9-]*$/;
|
|
@@ -36,12 +38,13 @@ const folderExtras = (it) => ({
|
|
|
36
38
|
...it.description ? { description: it.description } : {}
|
|
37
39
|
});
|
|
38
40
|
/** The registry file's shape. Returns the rows, or a string naming what is wrong - a
|
|
39
|
-
* malformed registry is an ERROR the human must fix (silently reading it as empty
|
|
40
|
-
* let the next drag overwrite their folders), while a
|
|
41
|
+
* malformed registry is an ERROR the human must fix (silently reading it as empty, or
|
|
42
|
+
* flattening a broken nesting, would let the next drag overwrite their folders), while a
|
|
43
|
+
* missing file is simply no folders. */
|
|
41
44
|
function parseFolders(raw) {
|
|
42
45
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return "expected an object";
|
|
43
46
|
const { version, folders } = raw;
|
|
44
|
-
if (version !== void 0 && version !== 1) return `unsupported version ${String(version)}`;
|
|
47
|
+
if (version !== void 0 && version !== 1 && version !== 2) return `unsupported version ${String(version)}`;
|
|
45
48
|
if (!Array.isArray(folders)) return "expected a \"folders\" array";
|
|
46
49
|
const out = [];
|
|
47
50
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -51,30 +54,45 @@ function parseFolders(raw) {
|
|
|
51
54
|
if (seen.has(name)) return `folder "${name}" is listed twice`;
|
|
52
55
|
seen.add(name);
|
|
53
56
|
const o = f.order;
|
|
57
|
+
const p = f.parent;
|
|
58
|
+
if (p !== void 0 && !isBoardName(p)) return `folder "${name}" has an invalid parent - a folder name`;
|
|
54
59
|
const t = readTitle(f.title);
|
|
55
60
|
const d = readDescription(f.description);
|
|
56
61
|
out.push({
|
|
57
62
|
name,
|
|
58
63
|
...typeof o === "number" && Number.isFinite(o) ? { order: o } : {},
|
|
64
|
+
...p !== void 0 ? { parent: p } : {},
|
|
59
65
|
...t ? { title: t } : {},
|
|
60
66
|
...d ? { description: d } : {}
|
|
61
67
|
});
|
|
62
68
|
}
|
|
69
|
+
const byName = new Map(out.map((f) => [f.name, f]));
|
|
70
|
+
for (const f of out) {
|
|
71
|
+
if (f.parent === void 0) continue;
|
|
72
|
+
if (version !== 2) return `folder "${f.name}" names a parent - nested folders need "version": 2`;
|
|
73
|
+
if (f.parent === f.name) return `folder "${f.name}" cannot be its own parent`;
|
|
74
|
+
const p = byName.get(f.parent);
|
|
75
|
+
if (!p) return `folder "${f.name}" names an unknown parent "${f.parent}"`;
|
|
76
|
+
if (p.parent !== void 0) return `folder "${f.name}" would sit three levels deep - folders nest one level only`;
|
|
77
|
+
}
|
|
63
78
|
return out;
|
|
64
79
|
}
|
|
65
|
-
/** Sidebar order from the files.
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
80
|
+
/** Sidebar order from the files. At every level the boards and folders there share one
|
|
81
|
+
* sequence, ranked by `order`, then kind (board before folder), then name; unranked sorts
|
|
82
|
+
* after ranked. The root holds root boards and top-level folders (registered without a
|
|
83
|
+
* parent, or implied by a board that names an unregistered folder); a top-level folder holds
|
|
84
|
+
* its boards and its sub-folders; a sub-folder holds its boards. A folder's title and
|
|
85
|
+
* description ride on its item (they live in the registry a tree write rewrites); a board's
|
|
86
|
+
* title stays with its row - it lives in the board's own file. */
|
|
70
87
|
function buildTree(boards, folders) {
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
88
|
+
const reg = /* @__PURE__ */ new Map();
|
|
89
|
+
for (const f of folders) if (isBoardName(f.name) && !reg.has(f.name)) reg.set(f.name, f);
|
|
90
|
+
const parentOf = (n) => {
|
|
91
|
+
const p = reg.get(n)?.parent;
|
|
92
|
+
return p !== void 0 && p !== n && reg.has(p) && reg.get(p).parent === void 0 ? p : void 0;
|
|
93
|
+
};
|
|
77
94
|
const members = /* @__PURE__ */ new Map();
|
|
95
|
+
const implied = [];
|
|
78
96
|
const rootBoards = [];
|
|
79
97
|
for (const b of boards) {
|
|
80
98
|
if (!isBoardName(b.name) || b.name === "all-scenes") continue;
|
|
@@ -83,37 +101,43 @@ function buildTree(boards, folders) {
|
|
|
83
101
|
rootBoards.push(b);
|
|
84
102
|
continue;
|
|
85
103
|
}
|
|
86
|
-
if (!
|
|
104
|
+
if (!reg.has(folder) && !implied.includes(folder)) implied.push(folder);
|
|
87
105
|
const list = members.get(folder) ?? [];
|
|
88
106
|
list.push(b);
|
|
89
107
|
members.set(folder, list);
|
|
90
108
|
}
|
|
91
|
-
const
|
|
92
|
-
const
|
|
109
|
+
const sorted = (xs) => xs.sort((a, b) => rank(a.order) - rank(b.order) || (a.item.kind === b.item.kind ? 0 : a.item.kind === "board" ? -1 : 1) || a.item.name.localeCompare(b.item.name)).map((r) => r.item);
|
|
110
|
+
const boardsHere = (name) => (name === null ? rootBoards : members.get(name) ?? []).map((b) => ({
|
|
93
111
|
item: {
|
|
94
112
|
kind: "board",
|
|
95
113
|
name: b.name
|
|
96
114
|
},
|
|
97
115
|
order: b.order
|
|
98
|
-
}))
|
|
116
|
+
}));
|
|
117
|
+
const folder = (name, kids) => ({
|
|
99
118
|
item: {
|
|
100
119
|
kind: "folder",
|
|
101
120
|
name,
|
|
102
|
-
|
|
103
|
-
...
|
|
121
|
+
items: sorted(kids),
|
|
122
|
+
...folderExtras(reg.get(name) ?? {})
|
|
104
123
|
},
|
|
105
|
-
order
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
|
|
124
|
+
order: reg.get(name)?.order
|
|
125
|
+
});
|
|
126
|
+
const subsOf = (top) => [...reg.keys()].filter((n) => parentOf(n) === top);
|
|
127
|
+
const tops = [...[...reg.keys()].filter((n) => !parentOf(n)), ...implied];
|
|
128
|
+
return sorted([...boardsHere(null), ...tops.map((t) => folder(t, [...boardsHere(t), ...subsOf(t).map((s) => folder(s, boardsHere(s)))]))]);
|
|
109
129
|
}
|
|
110
|
-
/** Every board in reading order - the order the switchers and the landing pick use. */
|
|
130
|
+
/** Every board in reading order, depth-first - the order the switchers and the landing pick use. */
|
|
111
131
|
function flatten(tree) {
|
|
112
132
|
const out = [];
|
|
113
|
-
|
|
114
|
-
|
|
133
|
+
const walk = (items) => {
|
|
134
|
+
for (const it of items) if (it.kind === "board") out.push(it.name);
|
|
135
|
+
else walk(it.items);
|
|
136
|
+
};
|
|
137
|
+
walk(tree);
|
|
115
138
|
return out;
|
|
116
139
|
}
|
|
140
|
+
const wireKids = (w) => w.items ?? w.boards ?? [];
|
|
117
141
|
function validateWire(wire) {
|
|
118
142
|
if (!Array.isArray(wire)) return "invalid tree";
|
|
119
143
|
const boards = /* @__PURE__ */ new Set(), folders = /* @__PURE__ */ new Set();
|
|
@@ -123,28 +147,51 @@ function validateWire(wire) {
|
|
|
123
147
|
boards.add(n);
|
|
124
148
|
return null;
|
|
125
149
|
};
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
150
|
+
const walk = (list, depth) => {
|
|
151
|
+
for (const w of list) {
|
|
152
|
+
if (typeof w === "string") {
|
|
153
|
+
const e = board(w);
|
|
154
|
+
if (e) return e;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (!w || typeof w !== "object" || Array.isArray(w)) return "invalid tree item";
|
|
158
|
+
if (depth >= 2) return "folders nest one level only";
|
|
159
|
+
const { folder, items, boards: legacy, title, description } = w;
|
|
160
|
+
if (!isBoardName(folder)) return "invalid folder name in tree";
|
|
161
|
+
if (title !== void 0 && (typeof title !== "string" || Array.from(title).length > 120)) return "invalid folder title";
|
|
162
|
+
if (description !== void 0 && (typeof description !== "string" || description.length > 300)) return "invalid folder description";
|
|
163
|
+
if (folders.has(folder)) return `folder "${folder}" appears twice`;
|
|
164
|
+
folders.add(folder);
|
|
165
|
+
const kids = items ?? legacy;
|
|
166
|
+
if (!Array.isArray(kids)) return "invalid folder in tree";
|
|
167
|
+
if (items === void 0 && kids.some((k) => typeof k !== "string")) return "invalid folder in tree";
|
|
168
|
+
const e = walk(kids, depth + 1);
|
|
129
169
|
if (e) return e;
|
|
130
|
-
continue;
|
|
131
170
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (description !== void 0 && (typeof description !== "string" || description.length > 300)) return "invalid folder description";
|
|
137
|
-
if (folders.has(folder)) return `folder "${folder}" appears twice`;
|
|
138
|
-
folders.add(folder);
|
|
139
|
-
if (!Array.isArray(kids)) return "invalid folder in tree";
|
|
140
|
-
for (const k of kids) {
|
|
141
|
-
const e = board(k);
|
|
142
|
-
if (e) return e;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
171
|
+
return null;
|
|
172
|
+
};
|
|
173
|
+
const e = walk(wire, 0);
|
|
174
|
+
if (e) return e;
|
|
145
175
|
if (boards.size > 200 || folders.size > 50) return "tree too large";
|
|
146
176
|
return null;
|
|
147
177
|
}
|
|
178
|
+
/** Every folder with the folder it sits in (null = the root), top-level folders first in
|
|
179
|
+
* reading order, each followed by its sub-folders. */
|
|
180
|
+
function folderEntries(t) {
|
|
181
|
+
const out = [];
|
|
182
|
+
for (const it of t) {
|
|
183
|
+
if (it.kind !== "folder") continue;
|
|
184
|
+
out.push({
|
|
185
|
+
folder: it,
|
|
186
|
+
parent: null
|
|
187
|
+
});
|
|
188
|
+
for (const k of it.items) if (k.kind === "folder") out.push({
|
|
189
|
+
folder: k,
|
|
190
|
+
parent: it.name
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
148
195
|
//#endregion
|
|
149
196
|
//#region src/server/boards.ts
|
|
150
197
|
/**
|
|
@@ -287,4 +334,4 @@ function readRegistry(boardsDir) {
|
|
|
287
334
|
};
|
|
288
335
|
}
|
|
289
336
|
//#endregion
|
|
290
|
-
export { listBoardFiles as a, BOARD_NAME as c, flatten as d,
|
|
337
|
+
export { wireKids as _, listBoardFiles as a, BOARD_NAME as c, flatten as d, folderEntries as f, validateWire as g, readTitle as h, isRegularFile as i, FOLDERS_FILE as l, readDescription as m, checkBoardsDir as n, nodeExists as o, isBoardName as p, checkRealDirs as r, readRegistry as s, boardFields as t, buildTree as u, hash as v };
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { a as listBoardFiles, d as flatten,
|
|
1
|
+
import { a as listBoardFiles, d as flatten, n as checkBoardsDir, p as isBoardName, s as readRegistry, t as boardFields, u as buildTree } from "./boards-BwiDAmPf.mjs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
//#region src/cli/boards.ts
|
|
4
4
|
/**
|
|
5
5
|
* `marver boards` - the sidebar as the agent sees it: every folder and board in reading
|
|
6
|
-
* order
|
|
7
|
-
* is in them, what ranks where" before the agent writes `folder` on a board or edits
|
|
6
|
+
* order - sub-folders indented under their parent - from the files (no dev server needed). One
|
|
7
|
+
* call answers "what folders exist, what is in them, what ranks where" before the agent writes `folder` on a board or edits
|
|
8
8
|
* `design/boards/_folders.json`. `--json` gives the tree shape the shell uses.
|
|
9
9
|
*/
|
|
10
10
|
function boardsCommand(root, opts) {
|
|
@@ -43,15 +43,24 @@ function boardsCommand(root, opts) {
|
|
|
43
43
|
const r = rows.find((x) => x.name === n);
|
|
44
44
|
return `${n}${title(r?.title)}${order(n)}${desc(r?.description)}`;
|
|
45
45
|
};
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
const count = (f) => {
|
|
47
|
+
const boards = f.items.filter((k) => k.kind === "board").length, subs = f.items.length - boards;
|
|
48
|
+
return `${boards} board${boards === 1 ? "" : "s"}${subs ? `, ${subs} folder${subs === 1 ? "" : "s"}` : ""}`;
|
|
49
|
+
};
|
|
50
|
+
const print = (items, pad, parent) => {
|
|
51
|
+
for (const it of items) {
|
|
52
|
+
if (it.kind === "board") {
|
|
53
|
+
console.log(`${pad}${boardLine(it.name)}`);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const where = parent ? `sub-folder of ${parent}` : "folder";
|
|
57
|
+
const implied = reg.folders.some((f) => f.name === it.name) ? "" : ", implied by its boards - not in _folders.json";
|
|
58
|
+
console.log(`${pad}${it.name}/${title(it.title)} (${where}, ${count(it)}${implied})${desc(it.description)}`);
|
|
59
|
+
print(it.items, `${pad} `, it.name);
|
|
60
|
+
if (!it.items.length) console.log(`${pad} (empty)`);
|
|
50
61
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (!it.boards.length) console.log(" (empty)");
|
|
54
|
-
}
|
|
62
|
+
};
|
|
63
|
+
print(tree, "", null);
|
|
55
64
|
if (hasAll) console.log("all-scenes (auto, always last)");
|
|
56
65
|
const landing = flatten(tree)[0];
|
|
57
66
|
if (landing) console.log(`\nlanding board: ${landing}`);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { a as ROUTE, r as NAME } from "./cli.mjs";
|
|
2
|
-
import { a as listBoardFiles, d as flatten,
|
|
3
|
-
import { c as loadConfig, r as scanFrames, u as detectHost } from "./manifest-
|
|
4
|
-
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-
|
|
2
|
+
import { a as listBoardFiles, d as flatten, n as checkBoardsDir, p as isBoardName, s as readRegistry, t as boardFields, u as buildTree } from "./boards-BwiDAmPf.mjs";
|
|
3
|
+
import { c as loadConfig, r as scanFrames, u as detectHost } from "./manifest-mMfUhPtL.mjs";
|
|
4
|
+
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-omHLCn91.mjs";
|
|
5
5
|
import { cpSync, existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { basename, dirname, join, sep } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
@@ -339,15 +339,24 @@ function readBoards(root) {
|
|
|
339
339
|
return out;
|
|
340
340
|
}
|
|
341
341
|
/** Switcher order = the sidebar's reading order: the folder tree over the PUBLISHED boards only
|
|
342
|
-
* (a folder left with no published board drops out - its name never reaches the bundle),
|
|
342
|
+
* (a folder left with no published board at any depth drops out - its name never reaches the bundle),
|
|
343
343
|
* flattened depth-first; all-scenes always LAST (it is the expensive everything-board, never
|
|
344
344
|
* the landing). `names[0]` is where `/` opens. Folder names of published boards are structure,
|
|
345
345
|
* like board names: they ship. */
|
|
346
346
|
function publishedTree(published, allBoards, folders) {
|
|
347
|
-
const
|
|
347
|
+
const built = buildTree(published.filter((n) => n !== "all-scenes").map((n) => ({
|
|
348
348
|
name: n,
|
|
349
349
|
...boardFields(allBoards[n], isBoardName)
|
|
350
|
-
})), folders)
|
|
350
|
+
})), folders);
|
|
351
|
+
const prune = (items) => items.flatMap((it) => {
|
|
352
|
+
if (it.kind === "board") return [it];
|
|
353
|
+
const kids = prune(it.items);
|
|
354
|
+
return kids.length ? [{
|
|
355
|
+
...it,
|
|
356
|
+
items: kids
|
|
357
|
+
}] : [];
|
|
358
|
+
});
|
|
359
|
+
const tree = prune(built);
|
|
351
360
|
return {
|
|
352
361
|
tree,
|
|
353
362
|
names: [...flatten(tree), ...published.includes("all-scenes") ? ["all-scenes"] : []]
|
|
@@ -361,6 +370,7 @@ function publishedManifest(manifest, pubFrames, publishedNames, strip) {
|
|
|
361
370
|
const pubBoardSet = new Set(publishedNames);
|
|
362
371
|
const pubBoards = (manifest.boards ?? []).filter((b) => pubBoardSet.has(b.name));
|
|
363
372
|
const pubFolderSet = new Set(pubBoards.map((b) => b.folder).filter(Boolean));
|
|
373
|
+
for (const f of manifest.folders ?? []) if (f.parent && pubFolderSet.has(f.name)) pubFolderSet.add(f.parent);
|
|
364
374
|
const pubFolders = (manifest.folders ?? []).filter((f) => pubFolderSet.has(f.name));
|
|
365
375
|
return {
|
|
366
376
|
...manifest.project ? { project: manifest.project } : {},
|
|
@@ -683,7 +693,7 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds, textures =
|
|
|
683
693
|
const realAssets = existsSync(assetsDir) ? realpathSync(assetsDir) : null;
|
|
684
694
|
for (const r of refs) {
|
|
685
695
|
if (!isLocalAssetRef(r) || !r.endsWith(".poster.png") || existsSync(join(assetsDir, r))) continue;
|
|
686
|
-
const { ensurePoster } = await import("./poster-
|
|
696
|
+
const { ensurePoster } = await import("./poster-BvxiAzy1.mjs");
|
|
687
697
|
const g = await ensurePoster(assetsDir, r.slice(0, -11));
|
|
688
698
|
if (!g.ok) throw new Error(`design/assets/${r}: ${g.error}`);
|
|
689
699
|
console.log(` poster: rendered design/assets/${r} (${g.width}×${g.height})`);
|
|
@@ -750,7 +760,7 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds, textures =
|
|
|
750
760
|
}));
|
|
751
761
|
let textureLine = " textures: skipped (--no-textures)";
|
|
752
762
|
if (textures && bakeGen) {
|
|
753
|
-
const { bakePublished } = await import("./publish-bakes-
|
|
763
|
+
const { bakePublished } = await import("./publish-bakes-BqzAAa3w.mjs");
|
|
754
764
|
const pubFrameFile = new Map(pubFrames.map((f) => [f.id, f]));
|
|
755
765
|
const r = await bakePublished({
|
|
756
766
|
root,
|
package/dist/cli.mjs
CHANGED
|
@@ -78,14 +78,14 @@ const resolve$1 = (dir) => {
|
|
|
78
78
|
};
|
|
79
79
|
const cli = cac(NAME);
|
|
80
80
|
cli.command("init", "Scaffold design/ in this repo").option("--mode <mode>", "studio | embedded", { default: "studio" }).option("--no-demo", "Skip the demo scene (the demo ships unless this flag is passed)").option("--root <dir>", "Host repo root", { default: "." }).action(async (opts) => {
|
|
81
|
-
const { init } = await import("./init-
|
|
81
|
+
const { init } = await import("./init-C34BY3R4.mjs");
|
|
82
82
|
init(resolve$1(opts.root), {
|
|
83
83
|
mode: opts.mode === "embedded" ? "embedded" : "studio",
|
|
84
84
|
demo: opts.demo !== false
|
|
85
85
|
});
|
|
86
86
|
});
|
|
87
87
|
for (const [name, desc] of [["dev", "Start the local canvas (everything on: hot reload, comments, Live Jam)"], ["canvas", "Start the local canvas - same as dev"]]) cli.command(name, desc).option("--root <dir>", "Host repo root", { default: "." }).option("--port <port>", "Port (default 5199)").action(async (opts) => {
|
|
88
|
-
const { dev } = await import("./dev-
|
|
88
|
+
const { dev } = await import("./dev-BNZF4Mup.mjs");
|
|
89
89
|
let port;
|
|
90
90
|
if (opts.port !== void 0) {
|
|
91
91
|
const n = Number(opts.port);
|
|
@@ -95,7 +95,7 @@ for (const [name, desc] of [["dev", "Start the local canvas (everything on: hot
|
|
|
95
95
|
await dev(resolve$1(opts.root), port);
|
|
96
96
|
});
|
|
97
97
|
cli.command("build", "Static export → design/.dist (what ships comes from design/publish.json - publishing is default-closed)").option("--boards <names>", "Publish only these boards (comma-separated); overrides the publish policy").option("--all-boards", "Publish every board - the loud override for the default-closed policy").option("--embed-seeds", "Copy comment history INTO the web root (identifying - every event carries its author's email)").option("--no-textures", "Skip compiling the glass textures the published frames rest under (needs Chrome; a CI without one skips on its own)").option("--root <dir>", "Host repo root", { default: "." }).action(async (opts) => {
|
|
98
|
-
const { buildSite } = await import("./build-
|
|
98
|
+
const { buildSite } = await import("./build-C7MqQ7hq.mjs");
|
|
99
99
|
try {
|
|
100
100
|
const boards = opts.boards === void 0 ? void 0 : typeof opts.boards === "string" ? opts.boards : "";
|
|
101
101
|
await buildSite(resolve$1(opts.root), boards, opts.allBoards === true, opts.embedSeeds === true, opts.textures !== false && !process.env.MARVER_NO_TEXTURES);
|
|
@@ -141,7 +141,7 @@ cli.command("work <action> [...frames]", "Working state on the canvas: start <sc
|
|
|
141
141
|
}
|
|
142
142
|
});
|
|
143
143
|
cli.command("boards", "The sidebar as files: every folder and board in reading order, the landing board, the registry").option("--root <dir>", "Host repo root", { default: "." }).option("--json", "The tree as JSON ({ tree, landing, registry })").action(async (opts) => {
|
|
144
|
-
const { boardsCommand } = await import("./boards-
|
|
144
|
+
const { boardsCommand } = await import("./boards-DnLewfj8.mjs");
|
|
145
145
|
try {
|
|
146
146
|
boardsCommand(resolve$1(opts.root), opts);
|
|
147
147
|
} catch (err) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { n as replay } from "./events-B3LBn74P.mjs";
|
|
2
2
|
import { i as readLog, r as listBoards, t as appendEvents } from "./comments-DZyobpxG.mjs";
|
|
3
3
|
import { n as localProfile } from "./profile-BjAPAJSb.mjs";
|
|
4
|
-
import { a as toFrameId } from "./manifest-
|
|
4
|
+
import { a as toFrameId } from "./manifest-mMfUhPtL.mjs";
|
|
5
5
|
import { workActivity } from "./work-CLrmY-vQ.mjs";
|
|
6
6
|
import { r as deviceId, t as has } from "./ledger-Bu0BjqIe.mjs";
|
|
7
7
|
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, watch, writeFileSync, writeSync } from "node:fs";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { i as PKG, r as NAME } from "./cli.mjs";
|
|
2
|
-
import { c as loadConfig, u as detectHost } from "./manifest-
|
|
3
|
-
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-
|
|
2
|
+
import { c as loadConfig, u as detectHost } from "./manifest-mMfUhPtL.mjs";
|
|
3
|
+
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-omHLCn91.mjs";
|
|
4
4
|
import { readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
5
5
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
@@ -452,12 +452,12 @@ async function dev(root, portFlag) {
|
|
|
452
452
|
console.log(`\n ${NAME} · ${projectName} → http://localhost:${port}/\n`);
|
|
453
453
|
setTimeout(async () => {
|
|
454
454
|
try {
|
|
455
|
-
await (await import("./shot-
|
|
455
|
+
await (await import("./shot-DswS4iRK.mjs")).sweepGhosts((m) => console.log(` ${m}`));
|
|
456
456
|
} catch {}
|
|
457
457
|
}, 0);
|
|
458
458
|
try {
|
|
459
459
|
const { staleManagedInstructions } = await import("./managed-HwHVNI3h.mjs");
|
|
460
|
-
const { installedVersion } = await import("./plugin-
|
|
460
|
+
const { installedVersion } = await import("./plugin-omHLCn91.mjs").then((n) => n.i);
|
|
461
461
|
const stale = staleManagedInstructions(root);
|
|
462
462
|
if (stale.length) {
|
|
463
463
|
const shown = stale.slice(0, 4).join(", ") + (stale.length > 4 ? `, +${stale.length - 4} more` : "");
|
|
@@ -495,7 +495,7 @@ async function dev(root, portFlag) {
|
|
|
495
495
|
});
|
|
496
496
|
}
|
|
497
497
|
if (config.jam) {
|
|
498
|
-
const { startJam } = await import("./daemon-
|
|
498
|
+
const { startJam } = await import("./daemon-CRZFpl6K.mjs");
|
|
499
499
|
const jam = startJam(root, config.jam, (m) => console.log(m), (board) => server.ws.send("sh:jam-comment", { board }));
|
|
500
500
|
if (jam) {
|
|
501
501
|
const close = server.close.bind(server);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { r as NAME } from "./cli.mjs";
|
|
2
|
-
import { d as readJson, l as detectAgent, o as writeManifest, r as scanFrames, s as DEFAULTS, u as detectHost } from "./manifest-
|
|
2
|
+
import { d as readJson, l as detectAgent, o as writeManifest, r as scanFrames, s as DEFAULTS, u as detectHost } from "./manifest-mMfUhPtL.mjs";
|
|
3
3
|
import { MANAGED_PREFIX, enumerateInstructionTemplates, hashBody, managedFile, pkgDir } from "./managed-HwHVNI3h.mjs";
|
|
4
4
|
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { basename, dirname, join, relative } from "node:path";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { i as PKG, n as CONTENT_WIDTH } from "./cli.mjs";
|
|
2
|
-
import { a as listBoardFiles, d as flatten, f as
|
|
2
|
+
import { a as listBoardFiles, d as flatten, f as folderEntries, h as readTitle, m as readDescription, n as checkBoardsDir, p as isBoardName, r as checkRealDirs, s as readRegistry, t as boardFields, u as buildTree, v as hash } from "./boards-BwiDAmPf.mjs";
|
|
3
3
|
import { accessSync, constants, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { delimiter, isAbsolute, join, relative, sep } from "node:path";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
@@ -615,8 +615,9 @@ function setSceneTitle(root, scene, title) {
|
|
|
615
615
|
}
|
|
616
616
|
return null;
|
|
617
617
|
}
|
|
618
|
-
/** The sidebar as files say it is, for the manifest: folders in
|
|
619
|
-
*
|
|
618
|
+
/** The sidebar as files say it is, for the manifest: folders in reading order (a sub-folder
|
|
619
|
+
* after its parent, with `parent`), boards in reading order with the folder they sit in
|
|
620
|
+
* directly and their description. A boards dir we may not read (symlink)
|
|
620
621
|
* or a malformed registry yields nothing here - the API and the build say why; the
|
|
621
622
|
* orientation file must never fail to write. */
|
|
622
623
|
function scanBoards(root) {
|
|
@@ -629,10 +630,12 @@ function scanBoards(root) {
|
|
|
629
630
|
...boardFields(b.json, isBoardName)
|
|
630
631
|
}));
|
|
631
632
|
const tree = buildTree(rows, reg.folders);
|
|
633
|
+
const entries = folderEntries(tree);
|
|
632
634
|
const folderOf = /* @__PURE__ */ new Map();
|
|
633
|
-
for (const
|
|
634
|
-
const folders =
|
|
635
|
+
for (const { folder } of entries) for (const k of folder.items) if (k.kind === "board") folderOf.set(k.name, folder.name);
|
|
636
|
+
const folders = entries.map(({ folder: it, parent }) => ({
|
|
635
637
|
name: it.name,
|
|
638
|
+
...parent ? { parent } : {},
|
|
636
639
|
...it.title ? { title: it.title } : {},
|
|
637
640
|
...it.description ? { description: it.description } : {}
|
|
638
641
|
}));
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
2
2
|
import { a as ROUTE, i as PKG, r as NAME } from "./cli.mjs";
|
|
3
|
-
import { a as listBoardFiles, c as BOARD_NAME, g as
|
|
3
|
+
import { _ as wireKids, a as listBoardFiles, c as BOARD_NAME, g as validateWire, h as readTitle, i as isRegularFile, l as FOLDERS_FILE, m as readDescription, n as checkBoardsDir, o as nodeExists, s as readRegistry, t as boardFields, v as hash } from "./boards-BwiDAmPf.mjs";
|
|
4
4
|
import { n as localProfile, t as isConnected } from "./profile-BjAPAJSb.mjs";
|
|
5
|
-
import { c as loadConfig, i as setSceneTitle, n as isNoteFile, o as writeManifest, r as scanFrames, t as affectedFrameIds } from "./manifest-
|
|
5
|
+
import { c as loadConfig, i as setSceneTitle, n as isNoteFile, o as writeManifest, r as scanFrames, t as affectedFrameIds } from "./manifest-mMfUhPtL.mjs";
|
|
6
6
|
import { copyFileSync, existsSync, linkSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, watch, writeFileSync } from "node:fs";
|
|
7
7
|
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
@@ -261,6 +261,7 @@ function apiMiddleware(root, opts = {}) {
|
|
|
261
261
|
}
|
|
262
262
|
const reg = readRegistry(boardsDir);
|
|
263
263
|
if (reg.state === "malformed") return json(res, 422, { error: reg.error });
|
|
264
|
+
if (reg.state === "ok" && reg.folders.some((f) => f.parent) && parsed.protocol !== 2) return json(res, 422, { error: "this tab predates folders in folders - reload the canvas, then try again" });
|
|
264
265
|
if (reg.sha256 !== b.folders) return json(res, 409, {
|
|
265
266
|
error: "folders changed on disk",
|
|
266
267
|
stale: [FOLDERS_FILE]
|
|
@@ -300,23 +301,29 @@ function apiMiddleware(root, opts = {}) {
|
|
|
300
301
|
return null;
|
|
301
302
|
};
|
|
302
303
|
const folders = [];
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
304
|
+
const walk = (list, parent) => {
|
|
305
|
+
for (const [i, it] of list.entries()) {
|
|
306
|
+
if (typeof it === "string") {
|
|
307
|
+
const e = consider(it, i, parent);
|
|
308
|
+
if (e) return e;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
const title = readTitle(it.title), description = readDescription(it.description);
|
|
312
|
+
folders.push({
|
|
313
|
+
name: it.folder,
|
|
314
|
+
order: i,
|
|
315
|
+
...parent ? { parent } : {},
|
|
316
|
+
...title ? { title } : {},
|
|
317
|
+
...description ? { description } : {}
|
|
318
|
+
});
|
|
319
|
+
const e = walk(wireKids(it), it.folder);
|
|
320
|
+
if (e) return e;
|
|
319
321
|
}
|
|
322
|
+
return null;
|
|
323
|
+
};
|
|
324
|
+
{
|
|
325
|
+
const e = walk(tree, null);
|
|
326
|
+
if (e) return json(res, 422, { error: e });
|
|
320
327
|
}
|
|
321
328
|
if (stale.length) return json(res, 409, {
|
|
322
329
|
error: "boards changed on disk",
|
|
@@ -335,8 +342,9 @@ function apiMiddleware(root, opts = {}) {
|
|
|
335
342
|
mkdirSync(boardsDir, { recursive: true });
|
|
336
343
|
let foldersSha = null;
|
|
337
344
|
if (folders.length) {
|
|
345
|
+
const version = folders.some((f) => f.parent) ? 2 : 1;
|
|
338
346
|
const next = JSON.stringify({
|
|
339
|
-
version
|
|
347
|
+
version,
|
|
340
348
|
folders
|
|
341
349
|
}, null, 2) + "\n";
|
|
342
350
|
atomicWrite(foldersPath, next);
|
|
@@ -422,7 +430,7 @@ function apiMiddleware(root, opts = {}) {
|
|
|
422
430
|
};
|
|
423
431
|
const asPng = url.searchParams.get("format") === "png";
|
|
424
432
|
const origin = opts.origin?.() ?? `http://${req.headers.host ?? "localhost"}`;
|
|
425
|
-
const { shootFrame } = await import("./shot-
|
|
433
|
+
const { shootFrame } = await import("./shot-DswS4iRK.mjs");
|
|
426
434
|
const r = await shootFrame({
|
|
427
435
|
root,
|
|
428
436
|
viewports: opts.viewports ?? {},
|
|
@@ -472,7 +480,7 @@ function apiMiddleware(root, opts = {}) {
|
|
|
472
480
|
if (typeof theme !== "string" || !/^[a-z0-9-]+$/i.test(theme)) return json(res, 400, { error: "invalid theme" });
|
|
473
481
|
const scale = body.scale === void 0 ? void 0 : body.scale;
|
|
474
482
|
if (scale !== void 0 && !(Number.isInteger(scale) && scale >= 1 && scale <= 4)) return json(res, 400, { error: "invalid scale" });
|
|
475
|
-
const { resolveFrames, shootBatch } = await import("./shot-
|
|
483
|
+
const { resolveFrames, shootBatch } = await import("./shot-DswS4iRK.mjs");
|
|
476
484
|
const sel = resolveFrames(root, body);
|
|
477
485
|
if (!sel.ok) return json(res, sel.status, { error: sel.error });
|
|
478
486
|
const origin = opts.origin?.() ?? `http://${req.headers.host ?? "localhost"}`;
|
|
@@ -500,7 +508,7 @@ function apiMiddleware(root, opts = {}) {
|
|
|
500
508
|
return json(res, 400, { error: "malformed JSON" });
|
|
501
509
|
}
|
|
502
510
|
if (!body || typeof body !== "object" || !Array.isArray(body.asks) || !body.asks.length || body.asks.length > 200) return json(res, 400, { error: "asks must list 1-200 frames" });
|
|
503
|
-
const { bakeBatch, ASK_MAX } = await import("./bake-
|
|
511
|
+
const { bakeBatch, ASK_MAX } = await import("./bake-BID6mo-N.mjs");
|
|
504
512
|
let manifest = {};
|
|
505
513
|
try {
|
|
506
514
|
manifest = JSON.parse(readFileSync(join(root, "design", "manifest.json"), "utf8"));
|
|
@@ -544,8 +552,8 @@ function apiMiddleware(root, opts = {}) {
|
|
|
544
552
|
if (path === "poster" && req.method === "GET") {
|
|
545
553
|
if (!ownerGated(req)) return json(res, 403, { error: "forbidden" });
|
|
546
554
|
const src = url.searchParams.get("src") ?? "";
|
|
547
|
-
const { ensurePoster, isLocalClip } = await import("./poster-
|
|
548
|
-
const { isLocalAssetRef } = await import("./build-
|
|
555
|
+
const { ensurePoster, isLocalClip } = await import("./poster-BvxiAzy1.mjs");
|
|
556
|
+
const { isLocalAssetRef } = await import("./build-C7MqQ7hq.mjs");
|
|
549
557
|
if (!isLocalAssetRef(src) || !isLocalClip(src)) return json(res, 400, { error: "src must be a clip under design/assets/" });
|
|
550
558
|
const r = await ensurePoster(join(root, "design", "assets"), src);
|
|
551
559
|
if (!r.ok) return json(res, r.error.includes("does not exist") ? 404 : 503, { error: r.error });
|
|
@@ -1027,7 +1035,7 @@ function marverPlugin(ctx) {
|
|
|
1027
1035
|
};
|
|
1028
1036
|
const prune = () => setTimeout(async () => {
|
|
1029
1037
|
try {
|
|
1030
|
-
(await import("./bake-
|
|
1038
|
+
(await import("./bake-BID6mo-N.mjs")).pruneBakes(root, bakeGen);
|
|
1031
1039
|
} catch {}
|
|
1032
1040
|
}, 500);
|
|
1033
1041
|
prune();
|
|
@@ -1166,7 +1174,7 @@ function marverPlugin(ctx) {
|
|
|
1166
1174
|
});
|
|
1167
1175
|
return;
|
|
1168
1176
|
}
|
|
1169
|
-
const { shootFrame, shootBatch, resolveFrames } = await import("./shot-
|
|
1177
|
+
const { shootFrame, shootBatch, resolveFrames } = await import("./shot-DswS4iRK.mjs");
|
|
1170
1178
|
if (ways[0] === "frame") {
|
|
1171
1179
|
if (typeof spec.frame !== "string") {
|
|
1172
1180
|
write({
|