@marver-design/marver 0.19.2 → 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.
Files changed (39) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.md +8 -10
  3. package/dist/{bake-kaf5kGZ7.mjs → bake-BID6mo-N.mjs} +1 -1
  4. package/dist/{boards-BmxcT3Lc.mjs → boards-BwiDAmPf.mjs} +95 -48
  5. package/dist/{boards-PuVzw5Wp.mjs → boards-DnLewfj8.mjs} +20 -11
  6. package/dist/{build-7ed5H2vT.mjs → build-C7MqQ7hq.mjs} +18 -8
  7. package/dist/cli.mjs +19 -13
  8. package/dist/{daemon-DbHvLQUL.mjs → daemon-CRZFpl6K.mjs} +1 -1
  9. package/dist/{dev-D3mP2x27.mjs → dev-BNZF4Mup.mjs} +5 -5
  10. package/dist/{init-BQYCS3EU.mjs → init-C34BY3R4.mjs} +34 -33
  11. package/dist/{manifest-B01PSyDc.mjs → manifest-mMfUhPtL.mjs} +8 -5
  12. package/dist/{plugin-DI-7NAnx.mjs → plugin-omHLCn91.mjs} +34 -26
  13. package/dist/{poster-DNh6N27C.mjs → poster-BvxiAzy1.mjs} +1 -1
  14. package/dist/{publish-bakes-Dp-ZFk3d.mjs → publish-bakes-BqzAAa3w.mjs} +8 -2
  15. package/dist/{shot-DMDvDbeP.mjs → shot-DswS4iRK.mjs} +7 -7
  16. package/docs/live-jam.md +1 -1
  17. package/docs/slides.md +89 -89
  18. package/docs/sticky-notes.md +9 -0
  19. package/package.json +1 -1
  20. package/src/client/const.ts +27 -9
  21. package/src/client/content/chart.tsx +8 -8
  22. package/src/client/content/index.tsx +5 -4
  23. package/src/client/content/slide.tsx +26 -201
  24. package/src/client/frame-host/main.tsx +10 -0
  25. package/src/client/shell/BoardList.tsx +111 -52
  26. package/src/client/shell/Comments.tsx +12 -4
  27. package/src/client/shell/Play.tsx +26 -14
  28. package/src/client/shell/Toolbar.tsx +7 -5
  29. package/src/client/shell/canvas/FrameNode.tsx +16 -2
  30. package/src/client/shell/store.ts +8 -8
  31. package/src/client/shell/styles.css +10 -9
  32. package/src/client/stage/main.tsx +56 -7
  33. package/src/shared/board-tree.ts +271 -140
  34. package/templates/AGENTS-embedded.md +3 -3
  35. package/templates/AGENTS-studio.md +3 -3
  36. package/templates/instructions/boards.md +47 -22
  37. package/templates/instructions/reference/deck-layouts.md +153 -199
  38. package/templates/instructions/reference/deck-story.md +6 -6
  39. package/templates/instructions/slides.md +275 -383
@@ -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-B01PSyDc.mjs";
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";
@@ -153,11 +153,11 @@ function init(root, opts) {
153
153
  }
154
154
  }
155
155
  write("slides.md", [
156
- "# This project's slide layouts and house rules",
156
+ "# This project's deck look and house habits",
157
157
  "",
158
- "The agent reads BOTH the shipped doctrine (instructions/slides.md) and this",
159
- "file - and this file WINS where they disagree. Add your own layouts, house",
160
- "rules, and banned moves here; it is yours and marver never touches it.",
158
+ "The agent reads BOTH the shipped guide (instructions/slides.md) and this",
159
+ "file - and this file WINS where they disagree. Add your own deck look,",
160
+ "compositions and habits here; it is yours and marver never touches it.",
161
161
  "",
162
162
  "To grow it from examples: drop decks (PPTX, PDF, screenshots) into",
163
163
  "design/slides-inspiration/ and ask the agent to \"study my inspiration\" -",
@@ -166,44 +166,45 @@ function init(root, opts) {
166
166
  "## The deck look",
167
167
  "",
168
168
  "How a deck wears this brand. The agent drafts it from DESIGN.md and",
169
- "theme.css on the first deck (a reviewed edit, building on provisionally",
170
- "with the theme's tokens); anything it cannot settle stays `TBD` for you.",
171
- "Each line shows the shape with an example - replace the example, keep the",
172
- "shape.",
169
+ "theme.css on the first deck (a reviewed edit); anything it cannot settle",
170
+ "stays `TBD` for you. Each line shows the shape with an example - replace",
171
+ "the example, keep the shape.",
173
172
  "",
174
- "- **Tokens** (`--marver-slide-*` in theme.css, read by the Slide root):",
175
- " ground / ink / muted, each with a `-dark` twin; accent, one value for",
176
- " both themes. e.g. ground = the page background, ink = the heading",
177
- " colour, accent = the primary button.",
178
- "- **Type** (`--marver-slide-font`): one family, the theme's; the type",
179
- " roles carry their weights. e.g. Inter.",
180
- "- **The mark**: which asset, where, how big. e.g. wordmark SVG, bottom-left",
181
- " of the cover and the closing only, 120px wide - never on content slides.",
182
- "- **Colour meaning** (charts, cards, badges): hue = category, fixed across",
183
- " the deck. e.g. accent = us, slate = competitors, muted = the baseline;",
184
- " green / amber / red reserved for status.",
185
- "- **Backgrounds allowed**: e.g. the flat ground; a theme gradient on covers",
186
- " and sections only; no photos behind text without a scrim.",
187
- "- **Imagery**: e.g. product screenshots on a device-less frame, real people,",
188
- " no stock; illustrations in the product's line style only. Evidentiary",
189
- " images (a report fragment, a log line, a reasoning trace, a real",
190
- " screen) are evidence objects, not decoration - hero material when the",
191
- " message warrants it.",
192
- "- **Tempo** (`--marver-slide-tempo`): e.g. 350ms - one value, every deck.",
173
+ "- **Stage**: e.g. 1280×720 (the default); or `viewport: 'laptop'` on every",
174
+ " slide for a 16:10 deck.",
175
+ "- **The master**: what every content slide wears. e.g. the mark top left,",
176
+ " \"Prepared for <client> · Confidential · <month year>\" top right in the",
177
+ " muted colour; the cover and the closing step outside it.",
178
+ "- **Tones**: whole-slide colour sets. e.g. paper (#F1F0EA on #151616) for",
179
+ " working slides, ink for statements and turns, the brand blue once, for",
180
+ " the proof.",
181
+ "- **Type**: family, weights, tracking, the few sizes. e.g. the brand sans at",
182
+ " 400 for headlines and text, tight tracking on headlines; a display, a",
183
+ " heading, a body and a small label size - hierarchy by size and colour.",
184
+ "- **The mark**: which asset, where, how big. e.g. the lockup in the master,",
185
+ " paired with the client's mark on the cover.",
186
+ "- **Imagery**: e.g. the client's own photography, full- or half-bleed under",
187
+ " a uniform scrim; product screenshots on a device-less frame; no stock.",
188
+ "- **Drawings**: e.g. native SVG line drawings on a shared dotted grid, ink",
189
+ " with one accent per slide, light and dark versions.",
190
+ "- **Colour meaning**: e.g. accent = the point of the slide; dark red = the",
191
+ " problem loop; green = the outcome; never decoration.",
192
+ "- **Motion** (`--marver-slide-tempo` in theme.css): e.g. 350ms morphs;",
193
+ " drawings trace in when a slide arrives; nothing loops.",
193
194
  "- **Numbers**: e.g. $ and k / M (lowercase k), fiscal years as FY26,",
194
195
  " negatives in brackets, one decimal on percentages.",
195
196
  "- **Voice**: three words the deck sounds like, and the words it never",
196
197
  " uses. e.g. direct, warm, specific; never \"leverage\", \"seamless\", \"journey\".",
197
198
  "- **Terminology**: user-facing word → never-shown internal word.",
198
199
  " e.g. \"Comments\" → \"Enrichment\".",
199
- "- **End card**: yes / no. e.g. yes - the mark on the ground, no text; the",
200
- " ask lives on the slide before it.",
200
+ "- **End card**: e.g. a full-bleed photograph, the mark and a contact; the",
201
+ " ask lives on the slide before.",
201
202
  "",
202
- "## Layouts",
203
+ "## Compositions",
203
204
  "",
204
- "(none yet - the shipped recipe list and atlas apply)",
205
+ "(none yet - the shipped idea bank in instructions/reference/deck-layouts.md applies)",
205
206
  "",
206
- "## House rules",
207
+ "## House habits",
207
208
  "",
208
209
  "(none yet)",
209
210
  ""
@@ -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 isBoardName, g as hash, m as readTitle, n as checkBoardsDir, p as readDescription, r as checkRealDirs, s as readRegistry, t as boardFields, u as buildTree } from "./boards-BmxcT3Lc.mjs";
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 root order, boards in
619
- * reading order with their folder and description. A boards dir we may not read (symlink)
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 it of tree) if (it.kind === "folder") for (const b of it.boards) folderOf.set(b, it.name);
634
- const folders = tree.filter((it) => it.kind === "folder").map((it) => ({
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 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
+ 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-B01PSyDc.mjs";
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
- 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 });
307
- continue;
308
- }
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 });
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: 1,
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-DMDvDbeP.mjs");
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-DMDvDbeP.mjs");
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-kaf5kGZ7.mjs");
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-DNh6N27C.mjs");
548
- const { isLocalAssetRef } = await import("./build-7ed5H2vT.mjs");
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-kaf5kGZ7.mjs")).pruneBakes(root, bakeGen);
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-DMDvDbeP.mjs");
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({
@@ -1,4 +1,4 @@
1
- import { capture, t as findChrome } from "./shot-DMDvDbeP.mjs";
1
+ import { capture, t as findChrome } from "./shot-DswS4iRK.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";
@@ -1,5 +1,6 @@
1
- import { planShot, t as findChrome } from "./shot-DMDvDbeP.mjs";
2
- import { ASK_MAX, bakeBatch } from "./bake-kaf5kGZ7.mjs";
1
+ import { o as slideSize } from "./cli.mjs";
2
+ import { planShot, t as findChrome } from "./shot-DswS4iRK.mjs";
3
+ import { ASK_MAX, bakeBatch } from "./bake-BID6mo-N.mjs";
3
4
  import { MIME } from "./serve-z5qtj_wJ.mjs";
4
5
  import { copyFileSync, lstatSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
5
6
  import { extname, isAbsolute, join, relative, resolve } from "node:path";
@@ -43,6 +44,11 @@ function publishedAsks(boards, themes, frames, viewports, allScenes = false) {
43
44
  }
44
45
  };
45
46
  const size = (f, n) => {
47
+ const sl = slideSize(f, viewports);
48
+ if (sl) return {
49
+ w: sl.width,
50
+ h: sl.height
51
+ };
46
52
  const nw = n && typeof n.w === "number" && n.w > 0 ? n.w : void 0, nh = n && typeof n.h === "number" && n.h > 0 ? n.h : void 0;
47
53
  const p = planShot(f, viewports, {});
48
54
  if (p.fullHeight && !nh) return null;
@@ -283,16 +283,16 @@ function planShot(frame, viewports, override = {}) {
283
283
  const cw = num(frame.contentWidth);
284
284
  const vpObj = frame.viewport ? viewports[frame.viewport] : void 0;
285
285
  const ow = num(override.w), oh = num(override.h);
286
- const sl = slideSize(frame);
286
+ const fallback = viewports.mobile ?? {
287
+ width: 390,
288
+ height: 844
289
+ };
290
+ const sl = slideSize(frame, viewports);
287
291
  if (sl) return {
288
292
  width: sl.width,
289
293
  initialHeight: sl.height,
290
294
  fullHeight: false
291
295
  };
292
- const fallback = viewports.mobile ?? {
293
- width: 390,
294
- height: 844
295
- };
296
296
  if (cw) {
297
297
  const width = clamp(ow ?? vpObj?.width ?? cw, 320, 1600);
298
298
  return {
@@ -337,8 +337,8 @@ async function shootFrame(opts) {
337
337
  };
338
338
  const plan = planShot(frame, viewports, size);
339
339
  if (frame.kind !== "html") try {
340
- const { scanAssetRefs } = await import("./build-7ed5H2vT.mjs");
341
- const { ensurePoster } = await import("./poster-DNh6N27C.mjs");
340
+ const { scanAssetRefs } = await import("./build-C7MqQ7hq.mjs");
341
+ const { ensurePoster } = await import("./poster-BvxiAzy1.mjs");
342
342
  const refs = scanAssetRefs(readFileSync(join(root, frame.file), "utf8"), frame.file);
343
343
  for (const r of refs) if (r.endsWith(".poster.png")) await ensurePoster(join(root, "design", "assets"), r.slice(0, -11));
344
344
  } catch {}
package/docs/live-jam.md CHANGED
@@ -105,7 +105,7 @@ obvious one:
105
105
  scale}` for humans and shell-ful agents - the same renderer, one line. A batch is ONE
106
106
  operation: one headless browser, `MARVER_SHOT_CONCURRENCY` frames at a time inside it
107
107
  (default up to 6, sized to the machine), so a scene costs about what a frame does. Default
108
- 2x; `--scale 4` for a print-quality still (a slide comes back 5120×2880). A frame too tall
108
+ 2x; `--scale 4` for a print-quality still (a 1280×720 slide comes back 5120×2880). A frame too tall
109
109
  for the asked scale steps down and says so in `note`; the file name carries the scale
110
110
  actually used (`…@4x.png`). A frame that ran out of settle budget still ships, marked
111
111
  `unsettled` with a note.
package/docs/slides.md CHANGED
@@ -3,67 +3,63 @@
3
3
  A slide is an ordinary frame with `slide: true`:
4
4
 
5
5
  ```tsx
6
- import { Slide } from '@marver-design/marver/content'
7
- export const meta = { title: 'Cover', slide: true }
6
+ export const meta = { title: 'Onboarding time halved', slide: true }
8
7
  export default () => (
9
- <Slide>
10
- <h1 className="sl-assertion">Churn halved after onboarding v2</h1>
11
- </Slide>
8
+ <main className="deck deck-ink">
9
+ <h1>Onboarding time halved in one quarter.</h1>
10
+ </main>
12
11
  )
13
12
  ```
14
13
 
15
- It renders 1280×720 on the canvas, wears the slide badge, and everything
16
- you know - comments, lasers, variants, promotion, Live Jam - keeps working.
17
- **The stage fits every screen**: you author at exactly 1280×720, and the
18
- slide scales and centers itself to whatever viewport plays it - fill window,
19
- a laptop, a viewer's phone - author px, Tailwind classes, and charts all
20
- scale together, so the composition you approved is the composition everyone
21
- sees. One scene = one deck; numbered files
22
- (`01-cover.tsx`) are the authoring order; **the board's reading order is the
23
- played order** - drag slides around the canvas to reorder the deck.
24
-
25
- ## Why it stays light for the agent
26
-
27
- There is no slide component library to learn. `Slide` is the ONE primitive:
28
- it owns the 1280×720 stage, the asymmetric margins, six fixed type roles
29
- (`sl-display` 160 · `sl-stat` 88 · `sl-assertion` 56 · `sl-support` 30 ·
30
- `sl-body` 24 · `sl-caption` 18), your theme's tokens, and the motion
31
- contract. Everything inside it is your project's own markup, classes, and
32
- components - the same ones the app ships - so a slide is built the way a
33
- screen is built, and an approved slide can be promoted like one.
34
-
35
- Looking good at every size costs the agent nothing extra: the fit is pure
36
- CSS on the root (a resized canvas node, a phone, a projector all get the
37
- same composition, scaled), so the doctrine forbids `vw`/`vh` and media
38
- queries inside a slide and asks for flex/grid in the stage's own
39
- proportions. A dev-only overflow marker outlines a slide whose content escapes the
40
- stage, or whose flex/grid child outgrows its parent - the agent sees the
41
- defect on the canvas, and the rule is always "cut or split, never shrink the type".
42
-
43
- The craft lives in prose, not code. `marver init` ships
44
- `design/instructions/slides.md` - the doctrine: assertion-first argument,
45
- the type roles, **the space IS the design** (three bands, the 85% rule, one
46
- px spacing scale), **seven silhouettes chosen before any recipe** (statement
47
- / hero / split / grid / stream / field / bookend) with a storyboard step
48
- and pacing rules so a deck never reads as one repeated shape, 19 core
49
- recipes with budgets and morph anchors, the choreography rules, and a
50
- review gate that squints the contact sheet. Two depth references sit
51
- beside it: `instructions/reference/deck-story.md` (intake, answer-first
52
- structure, the evidence check, audience calibration, the words) and
53
- `instructions/reference/deck-layouts.md` (the full layout atlas by job, the
54
- grid, content budgets, rebuilding an existing deck, chart craft). Your own
55
- **deck look** (tokens, type, the mark, colour meaning, numbers, voice - a
56
- fill-in template the agent drafts on the first deck), layouts, and house
57
- rules live in `design/slides.md`, which overrides the doctrine and which
58
- marver never overwrites.
14
+ That is the whole contract. Everything inside is your code - your
15
+ project's components and classes, any layout, any typeface, any image,
16
+ any SVG drawing, any CSS or JS animation. marver adds what a deck needs
17
+ around it: a stage, a player, and a place on the board.
18
+
19
+ - **The stage.** A slide renders at its stage size: 1280×720 by default,
20
+ or the `viewport` it declares (`viewport: 'laptop'` gives a 16:10 deck at
21
+ 1280×800). On the canvas it is a frame like any other - comments, laser,
22
+ variants, Live Jam - with a slide badge.
23
+ - **The deck.** One scene is one deck; numbered files (`01-cover.tsx`) are
24
+ the authoring order, and **the board's reading order is the played
25
+ order** - drag slides around the canvas to reorder the deck.
26
+ - **The host scales, not the slide.** A slide never reflows. Slides mode
27
+ renders each slide at its stage and scales the whole stage to the screen -
28
+ up on a projector, down on a laptop or a phone - and a canvas node resized
29
+ smaller shows the same stage, scaled, like a thumbnail. The composition you
30
+ approved is the one everyone sees, without breakpoints.
31
+ - **Notes.** `<slide>.note.md` beside the frame is its
32
+ [sticky note](sticky-notes.md): the aim, the talk track, the visual
33
+ intent, the sources. Notes ship with a published canvas, so keep anything
34
+ the audience must not read out of them.
35
+
36
+ ## The guidance the agent reads
37
+
38
+ `marver init` ships `design/instructions/slides.md` - not a rulebook, a
39
+ guide: how a slide plays and animates, the **deck kit** a strong deck is
40
+ built on (a master shell, whole-slide tones, the brand's type, hairlines
41
+ and labels, a drawing helper), the craft that makes a deck look made
42
+ rather than typed (the brand's own voice, one idea per slide, structure
43
+ with space instead of boxes, real images used big, a drawing system of its
44
+ own, pacing, honest numbers), the method (the answer first, a slide list
45
+ that tells the argument, a `_brief.md`, notes per slide), and a review that
46
+ squints at the contact sheet. Two references sit beside it:
47
+ `instructions/reference/deck-story.md` (intake, answer-first structure, the
48
+ evidence check, the words) and `instructions/reference/deck-layouts.md` (an
49
+ idea bank of compositions, rebuilding an existing deck, chart craft).
50
+
51
+ Your own **deck look** - master, tones, type, mark, imagery, drawing style,
52
+ colour meaning, voice - lives in `design/slides.md`, which the agent drafts
53
+ from your brand on the first deck, which overrides the shipped guide, and
54
+ which marver never overwrites.
59
55
 
60
56
  ## Playing and publishing a deck
61
57
 
62
58
  Press `p` on a board whose publish row says slides and you get slides mode:
63
- the 16:9 stage with the standard prototype toolbars (with `chrome: full`,
64
- the default) - arrows / Space / click to advance, `d` cycles the theme,
65
- devices including a 1280×720 Slide preset and fill window.
66
- Publish it with:
59
+ the stage scaled to the window, the standard prototype toolbars (with
60
+ `chrome: full`, the default), arrows / Space / click to advance, `d` cycles
61
+ the theme, and two views of the stage: Slide (fit to the window, room for
62
+ the chrome) and Fill window (edge to edge). Publish it with:
67
63
 
68
64
  ```json
69
65
  { "boards": { "pitch": { "max": "comment", "type": "slides",
@@ -72,8 +68,8 @@ Publish it with:
72
68
 
73
69
  - `transition`: `fade` (default) or `none`.
74
70
  - `chrome`: `full` (default - the standard prototype chrome: the top-right
75
- toolbar with comment, laser, theme, and devices including fill, plus the
76
- bottom-left walker; a locked deck-only share also carries the brand pill),
71
+ toolbar with comment, laser, theme, and devices, plus the bottom-left
72
+ walker; a locked deck-only share also carries the brand pill),
77
73
  `minimal` (a slim progress strip, comments, the canvas door when the
78
74
  board is not locked, and a pending-update control), or `none` (bare
79
75
  stage).
@@ -83,35 +79,44 @@ Publish it with:
83
79
 
84
80
  Viewers land straight in the deck; the URL survives refresh and back.
85
81
 
86
- ## Motion - the diff is the animation
82
+ ## Motion
87
83
 
88
- A resting slide is STILL - that is a contract, not a hope: charts render
89
- final-state SVG, videos are posters (no `<video>` element exists), and the
90
- `Slide` root suspends every CSS animation and transition under it at rest.
91
- (Your own `<canvas>`, `<video>`, or JS-driven motion is outside the contract
92
- and stays live, as in any frame.) Motion happens in slides
93
- mode, one-shot:
84
+ Motion is yours to write. While a deck plays, the stage puts
85
+ `data-sl-play` on `<html>` for the whole show, and `data-sl-entered` once
86
+ each slide has arrived (removed at each swap, set again when the new slide
87
+ settles); `useSlidePlay()` from `/content` is the playing flag in React. Key
88
+ your animations off them, so the canvas, `marver shot` and thumbnails show
89
+ the finished slide:
90
+
91
+ ```css
92
+ :root[data-sl-entered] .route { animation: draw 900ms ease-out both }
93
+ @keyframes draw { from { stroke-dashoffset: 600 } to { stroke-dashoffset: 0 } }
94
+ ```
95
+
96
+ On the canvas, a resting frame's animations are paused anyway (see sleep in
97
+ the README). marver adds three things on top:
94
98
 
95
99
  - **Morphs**: give the same `view-transition-name` to an element on two
96
- adjacent slides and it travels/grows between them. This is the house move.
97
- - **Build steps**: progressive disclosure is sibling frames (`03a-`, `03b-`)
98
- sharing morph names - every step visible and commentable on the board.
99
- - **Entrances**: `data-animate="fade-up | fade | scale-in"` +
100
- `data-animate-delay="0-3"`, run once after the transition settles. Never
101
- on an element that carries a morph name.
100
+ adjacent slides and it travels or resizes between them.
101
+ - **Build steps**: progressive disclosure is sibling frames (`03a-`,
102
+ `03b-`) sharing morph names - every step visible and commentable on the
103
+ board.
104
+ - **Entrance shortcuts**: `data-animate="fade-up | fade | scale-in"` +
105
+ `data-animate-delay="1-3"`, run once after a slide arrives. Never on an
106
+ element that carries a morph name.
102
107
 
103
- `prefers-reduced-motion` flattens marver's own motion - the morphs between
104
- slides and the entrance presets.
108
+ `--marver-slide-tempo` in your theme (default 350ms) times the morphs and
109
+ the shortcuts. `prefers-reduced-motion` flattens both.
105
110
 
106
111
  ## Charts and video
107
112
 
108
113
  - `<Chart option={...} h={420} />` - an Apache ECharts option, on a
109
114
  fixed supported surface: series bar, line, pie, scatter, radar, gauge, heatmap, funnel, treemap, sunburst, sankey, boxplot; components grid, polar, radar, tooltip, legend, title, dataset (+ transform), markLine, markPoint, markArea, visualMap, dataZoom. Anything outside
110
115
  it is dropped by ECharts without an error, so stay inside. marver
111
- supplies the house theme (colours, type, tooltip) from your
112
- `design/theme.css` tokens, strips animation at rest, and lets any
113
- styling you pass override the theme - so pass data and structure only.
114
- SVG-rendered, in a lazy chunk chart-free canvases never download.
116
+ supplies the house theme from the slide's own ink and typeface (and
117
+ `--marver-slide-accent`), sizes labels for a stage, keeps the chart
118
+ still at rest and plays its entrance in slides mode. SVG-rendered, in a
119
+ lazy chunk chart-free canvases never download.
115
120
  - `<Video src="intro.mp4" poster="intro.jpg" />` - the poster is the frame
116
121
  at rest. Omit it on a local clip and marver renders one from the clip's own
117
122
  first moments (`intro.mp4.poster.png` beside it - the dev server on first
@@ -122,19 +127,14 @@ slides and the entrance presets.
122
127
  `ratio="9 / 16"` for a vertical clip; `autoplay` for a muted ambient loop
123
128
  (that frame then stays live on the canvas). Remote https direct files work
124
129
  too.
130
+ - Images: `Img` for a framed asset, or a plain `<img>` when the slide needs
131
+ full control. Slides play scaled up, so use files at least 2x the size
132
+ they show.
133
+
134
+ ## Decks built before 0.20
125
135
 
126
- ## Theme tokens
127
-
128
- The `Slide` root reads `--marver-slide-ground / -ink / -muted` (each with a
129
- `-dark` variant), `--marver-slide-accent` (one value, both themes),
130
- `--marver-slide-font`, and `--marver-slide-tempo` (one duration that times
131
- both the entrances and the morphs between slides) from your theme and falls
132
- back to the house palette. The stage is 1280×720 (`SLIDE_W` / `SLIDE_H`, exported from `/content`)
133
- with asymmetric margins - 88px sides, 44px top and bottom, overridable in
134
- px via `--marver-slide-pad-x` / `--marver-slide-pad-y` - leaving a 1104×632
135
- content box. Morphs between slides are progressive enhancement: where
136
- `document.startViewTransition` is missing, slides crossfade at the tempo.
137
- Type roles, fixed: `sl-display` (160px, the one
138
- oversize - a hero number, a section numeral), `sl-stat` (88px, a row of
139
- figures), `sl-assertion` (56px),
140
- `sl-support` (30px), `sl-body` (24px), `sl-caption` (18px).
136
+ Earlier decks wrap each slide in `<Slide>` and use its `sl-*` type classes.
137
+ `<Slide>` still exists, as an optional wrapper that fills the frame - but it
138
+ no longer pads the stage, centres content, sizes type or freezes animation,
139
+ and the `sl-*` classes carry no styles. Such a deck still plays; give it its
140
+ own styles (a deck kit, as the guide describes) to restore the look.
@@ -44,6 +44,15 @@ own theme.
44
44
  - Comments: in comment mode (`C`) click any element of a note; the pin sits on the note, follows a
45
45
  fold onto the tab, and the thread card opens beside the column.
46
46
 
47
+ ## On a slide
48
+
49
+ Beside a slide (`slide: true`), the note is the presenter's script and the next agent's memory,
50
+ and the [slides guide](slides.md) teaches agents to write it in four short parts - **Aim** (what the
51
+ slide must do in the argument), **Say** (the talk track), **Visual** (what the image or drawing
52
+ carries) and **Source context** (where the facts come from, and their limits) - so the slide itself
53
+ can stay sparse. A note ships with a published canvas like any other: keep anything the audience
54
+ must not read out of it.
55
+
47
56
  ## What it is not
48
57
 
49
58
  A note is an aside, a screen's worth of reading at most. Specs, flows and mood boards stay content
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marver-design/marver",
3
- "version": "0.19.2",
3
+ "version": "0.21.0",
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,