@marver-design/marver 0.15.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.
Files changed (36) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/README.md +2 -1
  3. package/dist/boards-BmxcT3Lc.mjs +290 -0
  4. package/dist/boards-PuVzw5Wp.mjs +62 -0
  5. package/dist/{build-DfuTQZlY.mjs → build-B4yPgFNF.mjs} +72 -29
  6. package/dist/cli.mjs +33 -13
  7. package/dist/{daemon-DalgvoA9.mjs → daemon-Bbh_jmui.mjs} +1 -1
  8. package/dist/{dev-BxCmeU_H.mjs → dev-DH2W7Ffw.mjs} +4 -4
  9. package/dist/{init-QKNi9gvF.mjs → init-B7YhcN2o.mjs} +6 -3
  10. package/dist/{manifest-BzxSMoDB.mjs → manifest-CaslQIAO.mjs} +175 -12
  11. package/dist/{plugin-DJyjmQeh.mjs → plugin-BeBGu3gH.mjs} +239 -82
  12. package/dist/{poster-CbpzSzJu.mjs → poster-CoyobbGW.mjs} +1 -1
  13. package/dist/{shot-BWhoz6cU.mjs → shot-z-d-zMzf.mjs} +2 -2
  14. package/docs/publish.md +4 -1
  15. package/package.json +1 -1
  16. package/src/client/shell/App.tsx +45 -259
  17. package/src/client/shell/BoardList.tsx +438 -0
  18. package/src/client/shell/ContextMenu.tsx +59 -0
  19. package/src/client/shell/LockedApp.tsx +2 -1
  20. package/src/client/shell/Play.tsx +1 -0
  21. package/src/client/shell/icons.tsx +6 -0
  22. package/src/client/shell/labels.ts +4 -4
  23. package/src/client/shell/store.ts +156 -51
  24. package/src/client/shell/styles.css +40 -4
  25. package/src/shared/board-tree.ts +343 -0
  26. package/templates/AGENTS-embedded.md +25 -5
  27. package/templates/AGENTS-studio.md +25 -5
  28. package/templates/instructions/boards.md +91 -2
  29. package/templates/instructions/craft.md +4 -0
  30. package/templates/instructions/discover.md +20 -3
  31. package/templates/instructions/iterate.md +5 -4
  32. package/templates/instructions/review.md +4 -0
  33. package/templates/instructions/shape.md +1 -1
  34. package/templates/instructions/welcome.md +4 -1
  35. package/templates/instructions/wireframe.md +3 -0
  36. package/dist/{comments-DHB_8BRa.mjs → comments-oYcZ3cE-.mjs} +1 -1
@@ -1,13 +1,13 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
2
  import { i as ROUTE, n as NAME, r as PKG } from "./cli.mjs";
3
+ import { a as listBoardFiles, c as BOARD_NAME, g as hash, h as validateWire, i as isRegularFile, l as FOLDERS_FILE, m as readTitle, n as checkBoardsDir, o as nodeExists, p as readDescription, s as readRegistry, t as boardFields } from "./boards-BmxcT3Lc.mjs";
3
4
  import { n as localProfile, t as isConnected } from "./profile-BjAPAJSb.mjs";
4
- import { a as writeManifest, n as hash, r as scanFrames, t as affectedFrameIds } from "./manifest-BzxSMoDB.mjs";
5
- import { copyFileSync, existsSync, linkSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, watch, writeFileSync } from "node:fs";
5
+ import { a as writeManifest, n as scanFrames, r as setSceneTitle, s as loadConfig, t as affectedFrameIds } from "./manifest-CaslQIAO.mjs";
6
+ import { copyFileSync, existsSync, linkSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, watch, writeFileSync } from "node:fs";
6
7
  import { basename, dirname, join, resolve, sep } from "node:path";
7
8
  import { fileURLToPath } from "node:url";
8
9
  import { randomBytes } from "node:crypto";
9
10
  //#region src/server/api.ts
10
- const BOARD_NAME = /^[a-z0-9][a-z0-9-]*$/;
11
11
  const BODY_LIMIT = 1e6;
12
12
  const CSRF_MAX_AGE = 2592e3;
13
13
  /** Read one cookie value off the request. */
@@ -85,37 +85,18 @@ function contained(target, base) {
85
85
  }
86
86
  function apiMiddleware(root, opts = {}) {
87
87
  const boardsDir = join(root, "design", "boards");
88
+ const foldersPath = join(boardsDir, FOLDERS_FILE);
88
89
  const boardPath = (name) => {
89
- if (!BOARD_NAME.test(name)) return null;
90
+ if (!validName(name)) return null;
91
+ if (checkBoardsDir(root, boardsDir)) return null;
90
92
  mkdirSync(boardsDir, { recursive: true });
91
93
  const p = resolve(boardsDir, `${name}.json`);
92
94
  return contained(p, boardsDir) ? p : null;
93
95
  };
94
96
  const validName = (n) => typeof n === "string" && n.length >= 1 && n.length <= 64 && BOARD_NAME.test(n);
95
- const underRoot = (dir) => {
96
- try {
97
- const rr = realpathSync(root);
98
- const rd = realpathSync(dir);
99
- return rd === rr || rd.startsWith(rr + sep);
100
- } catch {
101
- return false;
102
- }
103
- };
104
- const notSymlink = (p) => {
105
- try {
106
- return !existsSync(p) || !lstatSync(p).isSymbolicLink();
107
- } catch {
108
- return false;
109
- }
110
- };
111
- const nodeExists = (p) => {
112
- try {
113
- lstatSync(p);
114
- return true;
115
- } catch {
116
- return false;
117
- }
118
- };
97
+ const dirError = () => checkBoardsDir(root, boardsDir);
98
+ const notSymlink = (p) => !nodeExists(p) || isRegularFile(p);
99
+ const nodeExists$1 = nodeExists;
119
100
  try {
120
101
  for (const f of readdirSync(boardsDir)) if (f.endsWith(".tmp")) rmSync(join(boardsDir, f), { force: true });
121
102
  } catch {}
@@ -144,20 +125,24 @@ function apiMiddleware(root, opts = {}) {
144
125
  return json(res, 200, { boards: {} });
145
126
  }
146
127
  if (path === "boards" && req.method === "GET") {
128
+ const de = dirError();
129
+ if (de) return json(res, 422, { error: de });
147
130
  mkdirSync(boardsDir, { recursive: true });
148
- return json(res, 200, readdirSync(boardsDir).filter((f) => f.endsWith(".json") && !f.endsWith(".tmp")).map((f) => {
149
- const content = readFileSync(join(boardsDir, f), "utf8");
150
- let order;
151
- try {
152
- const o = JSON.parse(content)?.order;
153
- if (typeof o === "number" && Number.isFinite(o)) order = o;
154
- } catch {}
155
- return {
156
- name: f.replace(/\.json$/, ""),
157
- sha256: hash(content),
158
- order
159
- };
160
- }));
131
+ return json(res, 200, listBoardFiles(boardsDir).boards.map((b) => ({
132
+ name: b.name,
133
+ sha256: b.sha256,
134
+ ...boardFields(b.json, validName)
135
+ })));
136
+ }
137
+ if (path === "folders" && req.method === "GET") {
138
+ const de = dirError();
139
+ if (de) return json(res, 422, { error: de });
140
+ const reg = readRegistry(boardsDir);
141
+ if (reg.state === "malformed") return json(res, 422, { error: reg.error });
142
+ return json(res, 200, {
143
+ folders: reg.folders,
144
+ sha256: reg.sha256
145
+ });
161
146
  }
162
147
  if (path === "boards/rename" && req.method === "POST") {
163
148
  if (!ownerGated(req)) return json(res, 403, { error: "forbidden" });
@@ -170,29 +155,89 @@ function apiMiddleware(root, opts = {}) {
170
155
  return json(res, 400, { error: "malformed JSON" });
171
156
  }
172
157
  if (!parsed || typeof parsed !== "object") return json(res, 400, { error: "expected an object" });
173
- const { from, to } = parsed;
158
+ const { from, to: toRaw, title: titleRaw, baseHash } = parsed;
159
+ const to = toRaw === void 0 ? from : toRaw;
174
160
  if (!validName(from) || !validName(to)) return json(res, 400, { error: "invalid board name" });
175
- if (to === "all-scenes" || from === "all-scenes" || to === from) return json(res, 400, { error: "invalid rename" });
176
- if (!underRoot(boardsDir)) return json(res, 400, { error: "boards directory escapes the project" });
161
+ if (titleRaw !== void 0 && (typeof titleRaw !== "string" || Array.from(titleRaw).length > 120)) return json(res, 400, { error: "invalid title" });
162
+ if (baseHash !== void 0 && typeof baseHash !== "string") return json(res, 400, { error: "invalid baseHash" });
163
+ if (to === "all-scenes" || from === "all-scenes") return json(res, 400, { error: "invalid rename" });
164
+ if (to === from && titleRaw === void 0) return json(res, 400, { error: "invalid rename" });
165
+ {
166
+ const de = dirError();
167
+ if (de) return json(res, 400, { error: de });
168
+ }
177
169
  const fromPath = boardPath(from), toPath = boardPath(to);
178
170
  if (!fromPath || !toPath) return json(res, 400, { error: "invalid board name" });
179
171
  if (!existsSync(fromPath)) return json(res, 404, { error: `board "${from}" does not exist` });
180
172
  if (!notSymlink(fromPath)) return json(res, 400, { error: "refusing to rename a symlinked board file" });
181
- if (isConnected(root)) return json(res, 409, { error: "disconnect before renaming boards - comment sync is board-keyed" });
182
- if (existsSync(join(root, "design", "comments", `${from}.jsonl`))) return json(res, 409, { error: "rename a board before commenting on it - this board has threads" });
183
- if (nodeExists(toPath)) return json(res, 409, { error: `a board named "${to}" already exists` });
173
+ const current = readFileSync(fromPath, "utf8");
174
+ if (baseHash !== void 0 && baseHash !== hash(current)) return json(res, 409, {
175
+ error: "board changed on disk",
176
+ sha256: hash(current)
177
+ });
178
+ let obj = null;
179
+ if (titleRaw !== void 0) {
180
+ let parsedBoard;
181
+ try {
182
+ parsedBoard = JSON.parse(current);
183
+ } catch {
184
+ return json(res, 422, { error: `board "${from}" is not valid JSON - fix the file` });
185
+ }
186
+ if (!parsedBoard || typeof parsedBoard !== "object" || Array.isArray(parsedBoard)) return json(res, 422, { error: `board "${from}" is not a JSON object - fix the file` });
187
+ obj = parsedBoard;
188
+ }
189
+ let next;
190
+ if (obj) {
191
+ const title = readTitle(titleRaw);
192
+ if (title) obj.title = title;
193
+ else delete obj.title;
194
+ next = JSON.stringify(obj, null, 2) + "\n";
195
+ }
196
+ if (to !== from) {
197
+ if (isConnected(root)) return json(res, 409, { error: "disconnect before renaming boards - comment sync is board-keyed" });
198
+ if (existsSync(join(root, "design", "comments", `${from}.jsonl`))) return json(res, 409, { error: "rename a board before commenting on it - this board has threads" });
199
+ if (nodeExists$1(toPath)) return json(res, 409, { error: `a board named "${to}" already exists` });
200
+ try {
201
+ if (next !== void 0) writeFileSync(toPath, next, { flag: "wx" });
202
+ else linkSync(fromPath, toPath);
203
+ } catch (err) {
204
+ const code = err.code;
205
+ if (code === "EEXIST") return json(res, 409, { error: `a board named "${to}" already exists` });
206
+ if (next !== void 0 || code !== "EPERM" && code !== "ENOSYS" && code !== "ENOTSUP" && code !== "EOPNOTSUPP") throw err;
207
+ if (nodeExists$1(toPath)) return json(res, 409, { error: `a board named "${to}" already exists` });
208
+ renameSync(fromPath, toPath);
209
+ return json(res, 200, { name: to });
210
+ }
211
+ rmSync(fromPath);
212
+ return json(res, 200, {
213
+ name: to,
214
+ ...next !== void 0 ? { sha256: hash(next) } : {}
215
+ });
216
+ }
217
+ atomicWrite(toPath, next);
218
+ return json(res, 200, {
219
+ name: to,
220
+ sha256: hash(next)
221
+ });
222
+ }
223
+ if (path === "scenes/rename" && req.method === "POST") {
224
+ if (!ownerGated(req)) return json(res, 403, { error: "forbidden" });
225
+ const raw = await readBody(req);
226
+ if (raw == null) return json(res, 400, { error: "body too large or unreadable" });
227
+ let parsed;
184
228
  try {
185
- linkSync(fromPath, toPath);
186
- } catch (err) {
187
- const code = err.code;
188
- if (code === "EEXIST") return json(res, 409, { error: `a board named "${to}" already exists` });
189
- if (code !== "EPERM" && code !== "ENOSYS" && code !== "ENOTSUP" && code !== "EOPNOTSUPP") throw err;
190
- if (nodeExists(toPath)) return json(res, 409, { error: `a board named "${to}" already exists` });
191
- renameSync(fromPath, toPath);
192
- return json(res, 200, { name: to });
229
+ parsed = JSON.parse(raw);
230
+ } catch {
231
+ return json(res, 400, { error: "malformed JSON" });
193
232
  }
194
- rmSync(fromPath);
195
- return json(res, 200, { name: to });
233
+ if (!parsed || typeof parsed !== "object") return json(res, 400, { error: "expected an object" });
234
+ const { scene, title } = parsed;
235
+ if (typeof scene !== "string" || !scene || scene.length > 128 || /[\\/]/.test(scene) || scene === "." || scene === "..") return json(res, 400, { error: "invalid scene name" });
236
+ if (typeof title !== "string" || Array.from(title).length > 120) return json(res, 400, { error: "invalid title" });
237
+ if (!scanFrames(root).scenes.some((sc) => sc.name === scene)) return json(res, 400, { error: `scene "${scene}" does not exist (no frames under design/scenes/${scene}/)` });
238
+ const e = setSceneTitle(root, scene, readTitle(title) ?? "");
239
+ if (e) return json(res, 400, { error: e });
240
+ return json(res, 200, { ok: true });
196
241
  }
197
242
  if (path === "boards/reorder" && req.method === "POST") {
198
243
  if (!ownerGated(req)) return json(res, 403, { error: "forbidden" });
@@ -205,25 +250,105 @@ function apiMiddleware(root, opts = {}) {
205
250
  return json(res, 400, { error: "malformed JSON" });
206
251
  }
207
252
  if (!parsed || typeof parsed !== "object") return json(res, 400, { error: "expected an object" });
208
- const order = parsed.order;
209
- if (!Array.isArray(order) || order.length < 1 || order.length > 200) return json(res, 400, { error: "invalid order" });
210
- if (!order.every(validName) || order.some((n) => n === "all-scenes")) return json(res, 400, { error: "invalid board name in order" });
211
- if (new Set(order).size !== order.length) return json(res, 400, { error: "duplicate board in order" });
212
- if (!underRoot(boardsDir)) return json(res, 400, { error: "boards directory escapes the project" });
213
- for (let i = 0; i < order.length; i++) {
214
- const p = boardPath(order[i]);
215
- if (!p || !existsSync(p) || !notSymlink(p)) continue;
253
+ const { tree, base } = parsed;
254
+ const bad = validateWire(tree);
255
+ if (bad) return json(res, 400, { error: bad });
256
+ const b = base;
257
+ if (!b || typeof b !== "object" || !b.boards || typeof b.boards !== "object" || b.folders !== null && typeof b.folders !== "string") return json(res, 400, { error: "expected base hashes" });
258
+ {
259
+ const de = dirError();
260
+ if (de) return json(res, 400, { error: de });
261
+ }
262
+ const reg = readRegistry(boardsDir);
263
+ if (reg.state === "malformed") return json(res, 422, { error: reg.error });
264
+ if (reg.sha256 !== b.folders) return json(res, 409, {
265
+ error: "folders changed on disk",
266
+ stale: [FOLDERS_FILE]
267
+ });
268
+ const unseen = listBoardFiles(boardsDir).boards.map((x) => x.name).filter((n) => !(n in b.boards));
269
+ if (unseen.length) return json(res, 409, {
270
+ error: "boards changed on disk",
271
+ stale: unseen
272
+ });
273
+ const plan = [];
274
+ const stale = [];
275
+ const consider = (name, order, folder) => {
276
+ const p = boardPath(name);
277
+ if (!p || !isRegularFile(p)) {
278
+ stale.push(name);
279
+ return null;
280
+ }
281
+ const content = readFileSync(p, "utf8");
282
+ if (b.boards[name] !== hash(content)) {
283
+ stale.push(name);
284
+ return null;
285
+ }
216
286
  let obj;
217
287
  try {
218
- obj = JSON.parse(readFileSync(p, "utf8"));
288
+ obj = JSON.parse(content);
219
289
  } catch {
290
+ return `board "${name}" is not valid JSON - fix the file`;
291
+ }
292
+ if (!obj || typeof obj !== "object" || Array.isArray(obj)) return `board "${name}" is not a JSON object - fix the file`;
293
+ plan.push({
294
+ name,
295
+ path: p,
296
+ obj,
297
+ order,
298
+ folder
299
+ });
300
+ return null;
301
+ };
302
+ const folders = [];
303
+ for (const [i, it] of tree.entries()) {
304
+ if (typeof it === "string") {
305
+ const e = consider(it, i, null);
306
+ if (e) return json(res, 422, { error: e });
220
307
  continue;
221
308
  }
222
- if (!obj || typeof obj !== "object") continue;
223
- obj.order = i;
224
- atomicWrite(p, JSON.stringify(obj, null, 2) + "\n");
309
+ const title = readTitle(it.title), description = readDescription(it.description);
310
+ folders.push({
311
+ name: it.folder,
312
+ order: i,
313
+ ...title ? { title } : {},
314
+ ...description ? { description } : {}
315
+ });
316
+ for (const [j, kid] of it.boards.entries()) {
317
+ const e = consider(kid, j, it.folder);
318
+ if (e) return json(res, 422, { error: e });
319
+ }
225
320
  }
226
- return json(res, 200, { ok: true });
321
+ if (stale.length) return json(res, 409, {
322
+ error: "boards changed on disk",
323
+ stale
324
+ });
325
+ const sha256 = {};
326
+ for (const w of plan) {
327
+ if (w.obj.order === w.order && (w.folder ? w.obj.folder === w.folder : w.obj.folder === void 0)) continue;
328
+ w.obj.order = w.order;
329
+ if (w.folder) w.obj.folder = w.folder;
330
+ else delete w.obj.folder;
331
+ const next = JSON.stringify(w.obj, null, 2) + "\n";
332
+ atomicWrite(w.path, next);
333
+ sha256[w.name] = hash(next);
334
+ }
335
+ mkdirSync(boardsDir, { recursive: true });
336
+ let foldersSha = null;
337
+ if (folders.length) {
338
+ const next = JSON.stringify({
339
+ version: 1,
340
+ folders
341
+ }, null, 2) + "\n";
342
+ atomicWrite(foldersPath, next);
343
+ foldersSha = hash(next);
344
+ } else rmSync(foldersPath, { force: true });
345
+ return json(res, 200, {
346
+ ok: true,
347
+ sha256: {
348
+ boards: sha256,
349
+ folders: foldersSha
350
+ }
351
+ });
227
352
  }
228
353
  const boardMatch = /^boards\/([^/]+)$/.exec(path);
229
354
  if (boardMatch) {
@@ -268,9 +393,12 @@ function apiMiddleware(root, opts = {}) {
268
393
  }
269
394
  mkdirSync(boardsDir, { recursive: true });
270
395
  const incoming = body.board;
271
- if (incoming && typeof incoming === "object" && incoming.order === void 0 && current) try {
272
- const o = JSON.parse(current).order;
273
- if (typeof o === "number" && Number.isFinite(o)) incoming.order = o;
396
+ if (incoming && typeof incoming === "object" && current) try {
397
+ const disk = JSON.parse(current);
398
+ if (incoming.order === void 0 && typeof disk.order === "number" && Number.isFinite(disk.order)) incoming.order = disk.order;
399
+ if (incoming.folder === void 0 && validName(disk.folder)) incoming.folder = disk.folder;
400
+ if (incoming.title === void 0 && readTitle(disk.title)) incoming.title = disk.title;
401
+ if (incoming.description === void 0 && readDescription(disk.description)) incoming.description = disk.description;
274
402
  } catch {}
275
403
  const next2 = JSON.stringify(body.board, null, 2) + "\n";
276
404
  atomicWrite(p, next2);
@@ -294,7 +422,7 @@ function apiMiddleware(root, opts = {}) {
294
422
  };
295
423
  const asPng = url.searchParams.get("format") === "png";
296
424
  const origin = opts.origin?.() ?? `http://${req.headers.host ?? "localhost"}`;
297
- const { shootFrame } = await import("./shot-BWhoz6cU.mjs");
425
+ const { shootFrame } = await import("./shot-z-d-zMzf.mjs");
298
426
  const r = await shootFrame({
299
427
  root,
300
428
  viewports: opts.viewports ?? {},
@@ -328,8 +456,8 @@ function apiMiddleware(root, opts = {}) {
328
456
  if (path === "poster" && req.method === "GET") {
329
457
  if (!ownerGated(req)) return json(res, 403, { error: "forbidden" });
330
458
  const src = url.searchParams.get("src") ?? "";
331
- const { ensurePoster, isLocalClip } = await import("./poster-CbpzSzJu.mjs");
332
- const { isLocalAssetRef } = await import("./build-DfuTQZlY.mjs");
459
+ const { ensurePoster, isLocalClip } = await import("./poster-CoyobbGW.mjs");
460
+ const { isLocalAssetRef } = await import("./build-B4yPgFNF.mjs");
333
461
  if (!isLocalAssetRef(src) || !isLocalClip(src)) return json(res, 400, { error: "src must be a clip under design/assets/" });
334
462
  const r = await ensurePoster(join(root, "design", "assets"), src);
335
463
  if (!r.ok) return json(res, r.error.includes("does not exist") ? 404 : 503, { error: r.error });
@@ -611,7 +739,12 @@ const VIRTUAL_CONFIG = "virtual:sh-config";
611
739
  const VIRTUAL_DATA = "virtual:sh-data";
612
740
  function marverPlugin(ctx) {
613
741
  const { root, clientDir, config } = ctx;
614
- let manifest = scanFrames(root);
742
+ let project = {
743
+ name: config.share.name || basename(root),
744
+ description: config.description
745
+ };
746
+ const projectInfo = () => project;
747
+ let manifest = scanFrames(root, projectInfo());
615
748
  let devServer = null;
616
749
  const bootId = String(process.hrtime.bigint());
617
750
  let invRev = 0;
@@ -752,9 +885,16 @@ function marverPlugin(ctx) {
752
885
  }));
753
886
  server.middlewares.use(routesMiddleware(server, clientDir));
754
887
  devServer = server;
888
+ let framesKey = JSON.stringify(manifest.frames);
889
+ let scenesKey = JSON.stringify(manifest.scenes);
755
890
  const regen = debounce(() => {
756
- manifest = scanFrames(root);
757
- if (writeManifest(root, manifest)) server.ws.send("sh:manifest", manifest);
891
+ manifest = scanFrames(root, projectInfo());
892
+ const changed = writeManifest(root, manifest);
893
+ const key = JSON.stringify(manifest.frames), sKey = JSON.stringify(manifest.scenes);
894
+ if (changed && key !== framesKey) server.ws.send("sh:manifest", manifest);
895
+ else if (changed && sKey !== scenesKey) server.ws.send("sh:scenes", { scenes: manifest.scenes });
896
+ framesKey = key;
897
+ scenesKey = sKey;
758
898
  }, 150);
759
899
  const watched = [join(root, "design", "scenes"), join(root, "design", "components")];
760
900
  const inScope = (file) => watched.some((w) => file.startsWith(w));
@@ -791,14 +931,31 @@ function marverPlugin(ctx) {
791
931
  rescanTheme();
792
932
  }
793
933
  });
794
- server.watcher.on("change", (f) => inScope(f) && /\.(tsx|jsx)$/.test(f) && regen());
795
- writeManifest(root, scanFrames(root));
934
+ server.watcher.on("change", (f) => inScope(f) && (/\.(tsx|jsx)$/.test(f) || f.endsWith("_brief.md")) && regen());
935
+ const configFile = join(root, "design", "config.ts");
936
+ server.watcher.on("change", (f) => {
937
+ if (f !== configFile) return;
938
+ loadConfig(root).then((c) => {
939
+ project = {
940
+ name: c.share.name || basename(root),
941
+ description: c.description
942
+ };
943
+ regen();
944
+ }).catch(() => {});
945
+ });
946
+ writeManifest(root, manifest);
796
947
  const boardsDir = join(root, "design", "boards");
797
948
  try {
798
949
  mkdirSync(boardsDir, { recursive: true });
799
950
  const timers = /* @__PURE__ */ new Map();
951
+ let listTimer;
800
952
  const watcher = watch(boardsDir, (_event, file) => {
801
953
  if (!file || !file.endsWith(".json")) return;
954
+ clearTimeout(listTimer);
955
+ listTimer = setTimeout(() => {
956
+ server.ws.send("sh:boards", {});
957
+ regen();
958
+ }, 150);
802
959
  clearTimeout(timers.get(file));
803
960
  timers.set(file, setTimeout(() => {
804
961
  timers.delete(file);
@@ -849,7 +1006,7 @@ function marverPlugin(ctx) {
849
1006
  });
850
1007
  return;
851
1008
  }
852
- const { shootFrame } = await import("./shot-BWhoz6cU.mjs");
1009
+ const { shootFrame } = await import("./shot-z-d-zMzf.mjs");
853
1010
  write(await shootFrame({
854
1011
  root,
855
1012
  viewports: config.viewports,
@@ -1,4 +1,4 @@
1
- import { capture, findChrome } from "./shot-BWhoz6cU.mjs";
1
+ import { capture, findChrome } from "./shot-z-d-zMzf.mjs";
2
2
  import { constants, copyFileSync, existsSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { basename, dirname, join, sep } from "node:path";
4
4
  import { pathToFileURL } from "node:url";
@@ -95,8 +95,8 @@ async function shootFrame(opts) {
95
95
  };
96
96
  const plan = planShot(frame, viewports, size);
97
97
  if (frame.kind !== "html") try {
98
- const { scanAssetRefs } = await import("./build-DfuTQZlY.mjs");
99
- const { ensurePoster } = await import("./poster-CbpzSzJu.mjs");
98
+ const { scanAssetRefs } = await import("./build-B4yPgFNF.mjs");
99
+ const { ensurePoster } = await import("./poster-CoyobbGW.mjs");
100
100
  const refs = scanAssetRefs(readFileSync(join(root, frame.file), "utf8"), frame.file);
101
101
  for (const r of refs) if (r.endsWith(".poster.png")) await ensurePoster(join(root, "design", "assets"), r.slice(0, -11));
102
102
  } catch {}
package/docs/publish.md CHANGED
@@ -31,7 +31,10 @@ opens the same view on the published site, as long as its board and frame shippe
31
31
 
32
32
  **What ships**: the boards you list, the frames they reference, and the host `public/`
33
33
  directory in full (the `--boards` filter covers frames, not public assets). A flow you
34
- publish must have all its `data-goto` targets on a published board.
34
+ publish must have all its `data-goto` targets on a published board. Sidebar folders ship
35
+ only around published boards (a folder with nothing published never reaches the bundle),
36
+ and `title`s and `description`s ship only for published things - the project's, the
37
+ published boards', their folders', scenes' and frames'.
35
38
 
36
39
  ## Who can open your canvas
37
40
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marver-design/marver",
3
- "version": "0.15.0",
3
+ "version": "0.16.1",
4
4
  "description": "The agent-native design canvas. A design/ folder, one command, a canvas of live frames built from your repo's real components - comment @marver and your own coding agent does the work. The tool ships no AI.",
5
5
  "type": "module",
6
6
  "private": false,