@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
package/CHANGELOG.md CHANGED
@@ -2,6 +2,96 @@
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
+
31
+ ## 0.20.0 - 2026-10-01
32
+
33
+ ### Changed
34
+
35
+ - **A slide is any frame with `slide: true` - nothing else.** The slide type keeps everything
36
+ a deck needs around the frame: the badge, the board's reading order as the deck order, slides
37
+ mode, publishing as a deck, comments and laser in play. What goes inside is the author's own
38
+ code: any layout, typeface, drawing or animation. `<Slide>` is now an optional wrapper that
39
+ fills the frame. **Breaking for decks built on it:** it no longer pads the stage, centres the
40
+ content, sets the `sl-*` type sizes, freezes animation at rest or outlines an overflow, and
41
+ the `sl-*` classes carry no styles - such a deck still plays, and needs its own styles to look
42
+ as it did.
43
+ - **The host scales the stage, not the slide.** A slide never reflows. Slides mode renders each
44
+ slide at its stage and scales the whole stage to the window, up as well as down - a projector
45
+ shows the deck at full size without the presenter picking Fill (the Slide device never went
46
+ past 100% before); Fill window now fits the same stage edge to edge, and slides mode offers no
47
+ viewport presets. A slide frame met in present or focus plays the same way - a device preset
48
+ never reflows it. A canvas node resized away from the stage shows it scaled and centred, like
49
+ a thumbnail, with comment pins mapped through the same fit; published sleep textures are
50
+ baked at the stage.
51
+ - **A deck chooses its stage.** A slide's stage is the `viewport` it declares, when the project
52
+ defines it - `viewport: 'laptop'` makes a 16:10 deck at 1280×800 - else 1280×720. The canvas,
53
+ `marver shot`, copy as image and slides mode all use it.
54
+ - **The slides guidance teaches instead of ruling.** `instructions/slides.md` drops the fixed
55
+ type roles, margins, bands, spacing scale, pacing quotas, recipe budgets and the defect
56
+ gate. In their place: how a slide plays and animates, the deck kit a strong deck is built on
57
+ (a master shell, whole-slide tones, the brand's type, hairlines and labels, a drawing
58
+ helper), ten craft habits distilled from the best decks built on marver, the method (the
59
+ answer first, a slide list that tells the argument, a `_brief.md`, an Aim / Say / Visual /
60
+ Source note per slide) and a review that squints at the contact sheet.
61
+ `reference/deck-layouts.md` becomes an idea bank, with the compositions of a strong
62
+ consulting deck described one by one. A new project's `design/slides.md` opens with a deck
63
+ look shaped the same way. Existing projects keep their `design/slides.md`; `marver init`
64
+ updates unedited shipped instructions and stages edited ones in `design/.local/latest/`.
65
+ - **Docs.** [docs/slides.md](docs/slides.md) and the README's Slides section describe the new
66
+ model (a slide is code; the host scales the stage; motion hooks; the deck kit), with a section
67
+ for decks built before 0.20. [docs/sticky-notes.md](docs/sticky-notes.md) shows a note beside a
68
+ slide as the presenter's script - Aim / Say / Visual / Source context - and that published notes
69
+ are readable by viewers.
70
+
71
+ ### Added
72
+
73
+ - **Motion hooks for any slide.** While a deck plays, the stage marks `data-sl-play` on
74
+ `<html>`, `data-sl-entered` once each slide has arrived, and `data-mv-slide` while the
75
+ mounted frame is a slide, so a slide's own CSS or JS animation runs when it arrives and the
76
+ canvas, `marver shot` and thumbnails show the finished slide. `useSlidePlay()` is exported from
77
+ `/content` for React. The `data-animate` entrance shortcuts and the `--marver-slide-tempo`
78
+ duration now work on any slide, wrapper or not, and every slide document shares one baseline
79
+ (no body margin) in every host, so its geometry never depends on what played before it.
80
+ - **`Chart` and `Img` know a slide frame without the wrapper.** Labels take the stage scale and
81
+ images skip the canvas's decoded-to-size path, so a slide scaled up on a projector stays sharp.
82
+ `Chart` also reads `--marver-slide-accent`.
83
+
84
+ ### Upgrading
85
+
86
+ - Run `npx marver init` to take the new slides guidance (unedited instruction files update in
87
+ place; edited ones are staged in `design/.local/latest/` for your agent to merge).
88
+ - A deck built on `<Slide>` and the `sl-*` classes keeps playing but loses its padding, centring
89
+ and type sizes. Ask your agent to give it a deck kit (instructions/slides.md describes one), or
90
+ stay on 0.19 until you do.
91
+ - A deck of ordinary frames becomes a slide deck by adding `slide: true` to each frame's meta -
92
+ keep its `viewport` if it was designed at one (a 1280×800 deck stays 16:10) - and setting its
93
+ publish row to `"type": "slides"`.
94
+
5
95
  ## 0.19.2 - 2026-09-09
6
96
 
7
97
  ### Fixed
package/README.md CHANGED
@@ -31,7 +31,7 @@ Frames appear on the canvas the moment the files land. That's the loop.
31
31
  ## Why marver
32
32
 
33
33
  - **Frames are real code.** Plain TSX/HTML files rendered from your repo's actual components and theme - zero imports from this package required. An approved design promotes into the app by moving a file, not by re-implementing a picture.
34
- - **Decks are real code too.** A slide is a frame with `slide: true`. One `Slide` primitive, your own markup inside it, a doctrine that teaches the agent to argue rather than decorate - and a stage that scales itself to any screen without the agent writing a single breakpoint. See [Slides](#slides).
34
+ - **Decks are real code too.** A slide is a frame with `slide: true` - nothing else. Any layout, typeface, drawing or animation the browser can render, a guide that teaches the agent to argue and to wear your brand rather than decorate, and a player that scales the stage to any screen without the agent writing a single breakpoint. See [Slides](#slides).
35
35
  - **Everything hot-reloads.** The agent writes, you watch it land - live.
36
36
  - **True viewports.** Each frame is a real iframe: drag its edge and your actual breakpoints fire.
37
37
  - **Your agent answers on the canvas.** Tag `@marver` in a comment and it picks up the job, edits the real source, and replies in the thread - no wiring, on by default. See [Live Jam](#live-jam).
@@ -48,20 +48,18 @@ Frames appear on the canvas the moment the files land. That's the loop.
48
48
  - **Sticky notes.** A markdown file beside a frame (`cart.note.md`) or a scene (`_note.md`) becomes a yellow note left of the frame on the canvas - what it is for, how two variations differ, an open question. Markdown, `goto:` links to frames, hand-drawn Mermaid; readers comment on a note's text like on a frame's, fold it to its corner, hide them all with `n`. The layout makes room for it, beside the frame and below; published canvases carry them.
49
49
  - **Hi-fi at rest.** A frame at rest is its own live document, asleep: animations paused and every `backdrop-filter` element painted with a certified texture of its filtered backdrop (compiled by the dev server's headless Chrome), so a board of 30 glass screens pans like 30 statics and wakes pixel-identical on interact. Glass inside glass, blend modes and frames whose paint depends on random data or the clock stay live; `?awake=1` keeps every frame live for comparison.
50
50
  - **Charts and video in any frame.** `Chart` (Apache ECharts, SVG, still at rest) inherits the ink, typeface and accent of whatever frame it sits in - a Tailwind dashboard, a dark spec, a slide - sizes its type to the context and follows the layout on resize. `Video` is poster-first everywhere: click to play wherever the frame is live, `autoplay` for an ambient loop, `ratio` for vertical clips; omit the poster and marver renders one from the clip. A screen with a chart or a clip is still a screen.
51
- - **Copy as image.** Select a frame, press `i` - a 2x PNG of it lands on the clipboard, rendered by the same headless Chrome that serves `marver shot`; `⇧i` for 4x (a slide is 5120×2880). Paste into Slack, a doc, or a chat with your agent.
51
+ - **Copy as image.** Select a frame, press `i` - a 2x PNG of it lands on the clipboard, rendered by the same headless Chrome that serves `marver shot`; `⇧i` for 4x (a 1280×720 slide is 5120×2880, whatever size its node has). Paste into Slack, a doc, or a chat with your agent.
52
52
 
53
53
  ## Slides
54
54
 
55
- A deck is a scene of `slide: true` frames. On the canvas they are 1280×720 frames like any other - comment on them, laser them, fork variants, drag them to reorder the deck (the board's reading order is the play order). Press `p` on a slides board and you get slides mode: the 16:9 stage, arrows / Space / click to advance, `d` cycles the theme, devices including fill window, morphs between slides where the agent named the same element twice.
55
+ A deck is a scene of `slide: true` frames. On the canvas they are frames like any other - comment on them, laser them, fork variants, drag them to reorder the deck (the board's reading order is the play order). Press `p` on a slides board and you get slides mode: arrows / Space / click to advance, `d` cycles the theme, morphs between slides where the agent named the same element twice.
56
56
 
57
- What makes it light on the agent side, and good on every screen:
57
+ - **A slide is just code.** No component library, no wrapper, no fixed type scale: the agent builds each slide from your project's own components and styles - an intricate diagram, a full-bleed photograph, an SVG drawing that animates in - the way it builds a screen.
58
+ - **A stage, and a player that scales it.** A slide renders at 1280×720, or at the `viewport` it declares (a 16:10 deck at 1280×800). Slides mode scales the whole stage to the screen - up on a projector, down on a phone - so the composition you approved is the one everyone sees.
59
+ - **Still at rest, alive in play.** On the canvas a resting slide is asleep like any frame; while the deck plays, the stage marks `data-sl-play` and `data-sl-entered` on the document so the agent's own animations run when a slide arrives, and `view-transition-name` morphs carry elements from one slide to the next.
60
+ - **Craft, not rules.** `init` ships `design/instructions/slides.md`: the deck kit a strong deck is built on (a master, whole-slide tones, the brand's type, a drawing system), the habits that make a deck look made rather than typed, the method (the answer first, a slide list that tells the argument, notes that carry the talk track), an idea bank of compositions, and your own project-owned `design/slides.md` with a **deck look** the agent drafts from your brand.
58
61
 
59
- - **One primitive, no component library.** `Slide` owns the stage, the margins, six fixed type roles, your theme's tokens, and the motion contract. Everything inside is your project's own markup and components - a slide is built the way a screen is built.
60
- - **The fit is pure CSS.** The agent authors at exactly 1280×720; the root scales and centers itself to a phone, a laptop, a projector, or a resized canvas node. No `vw`, no media queries, no breakpoints - the composition you approved is the composition everyone sees, and a dev-only overflow marker outlines a slide whose content escapes the stage, or whose flex/grid child outgrows its parent.
61
- - **Still at rest.** Charts are final-state SVG in a lazy chunk, videos are posters, and every CSS animation under `Slide` is suspended until slides mode plays - so a 40-slide deck of the shipped primitives pans like 40 statics on the canvas. (Your own `<canvas>`, `<video>`, or JS-driven motion stays live, as in any frame.)
62
- - **A doctrine, not a template.** `init` ships `design/instructions/slides.md`: assertion-first argument, the space (three bands, the 85% rule, one spacing scale), seven silhouettes chosen before any recipe so a deck never reads as one repeated shape, 19 recipes with budgets, choreography rules, and a review gate - plus two depth references and your own project-owned `design/slides.md` with a fill-in **deck look** the agent drafts from your brand.
63
-
64
- Publish a deck with `{ "boards": { "pitch": { "max": "read", "type": "slides", "open": "slides", "lock": true } } }` in `design/publish.json` and the link opens straight into the deck, read-only, with no canvas behind it. The [slides guide](docs/slides.md) has the rest - morphs, build steps, `Chart` and `Video`, the theme tokens.
62
+ Publish a deck with `{ "boards": { "pitch": { "max": "read", "type": "slides", "open": "slides", "lock": true } } }` in `design/publish.json` and the link opens straight into the deck, read-only, with no canvas behind it. The [slides guide](docs/slides.md) has the rest - motion, build steps, `Chart` and `Video`, notes.
65
63
 
66
64
  ## Collaboration
67
65
 
@@ -1,4 +1,4 @@
1
- import { AREA, SURFACE, pool, shotConcurrency, withBrowser } from "./shot-DMDvDbeP.mjs";
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 `order`), and `design/boards/_folders.json`
13
- * says which folders exist and where they rank at the root. One level only: folders
14
- * hold boards, never folders. `all-scenes` never enters the tree - callers pin it last.
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 would
40
- * let the next drag overwrite their folders), while a missing file is simply no folders. */
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. Root: boards with no folder + every folder (registered
66
- * or implied by a board), ranked by `order` then kind (board before folder) then name.
67
- * Inside a folder: its boards by `order` then name. Unranked sorts after ranked. A folder's
68
- * title and description ride on its item (they live in the registry a tree write rewrites);
69
- * a board's title stays with its row - it lives in the board's own file. */
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 folderOrder = /* @__PURE__ */ new Map();
72
- const folderMeta = /* @__PURE__ */ new Map();
73
- for (const f of folders) if (isBoardName(f.name) && !folderOrder.has(f.name)) {
74
- folderOrder.set(f.name, f.order);
75
- folderMeta.set(f.name, folderExtras(f));
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 (!folderOrder.has(folder)) folderOrder.set(folder, void 0);
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 byRank = (a, b) => rank(a.order) - rank(b.order) || a.name.localeCompare(b.name);
92
- const root = [...rootBoards.map((b) => ({
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
- })), ...[...folderOrder].map(([name, order]) => ({
116
+ }));
117
+ const folder = (name, kids) => ({
99
118
  item: {
100
119
  kind: "folder",
101
120
  name,
102
- boards: (members.get(name) ?? []).sort(byRank).map((b) => b.name),
103
- ...folderMeta.get(name) ?? {}
121
+ items: sorted(kids),
122
+ ...folderExtras(reg.get(name) ?? {})
104
123
  },
105
- order
106
- }))];
107
- root.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));
108
- return root.map((r) => r.item);
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
- for (const it of tree) if (it.kind === "board") out.push(it.name);
114
- else out.push(...it.boards);
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
- for (const w of wire) {
127
- if (typeof w === "string") {
128
- const e = board(w);
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
- if (!w || typeof w !== "object" || Array.isArray(w)) return "invalid tree item";
133
- const { folder, boards: kids, title, description } = w;
134
- if (!isBoardName(folder)) return "invalid folder name in tree";
135
- if (title !== void 0 && (typeof title !== "string" || Array.from(title).length > 120)) return "invalid folder title";
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, isBoardName as f, hash as g, validateWire as h, isRegularFile as i, FOLDERS_FILE as l, readTitle as m, checkBoardsDir as n, nodeExists as o, readDescription as p, checkRealDirs as r, readRegistry as s, boardFields as t, buildTree as u };
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, f as isBoardName, n as checkBoardsDir, s as readRegistry, t as boardFields, u as buildTree } from "./boards-BmxcT3Lc.mjs";
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, from the files (no dev server needed). One call answers "what folders exist, what
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
- for (const it of tree) {
47
- if (it.kind === "board") {
48
- console.log(boardLine(it.name));
49
- continue;
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
- console.log(`${it.name}/${title(it.title)} (folder, ${it.boards.length} board${it.boards.length === 1 ? "" : "s"}${reg.folders.some((f) => f.name === it.name) ? "" : ", implied by its boards - not in _folders.json"})${desc(it.description)}`);
52
- for (const b of it.boards) console.log(` ${boardLine(b)}`);
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, f as isBoardName, n as checkBoardsDir, s as readRegistry, t as boardFields, u as buildTree } from "./boards-BmxcT3Lc.mjs";
3
- import { c as loadConfig, r as scanFrames, u as detectHost } from "./manifest-B01PSyDc.mjs";
4
- import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-DI-7NAnx.mjs";
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 tree = buildTree(published.filter((n) => n !== "all-scenes").map((n) => ({
347
+ const built = buildTree(published.filter((n) => n !== "all-scenes").map((n) => ({
348
348
  name: n,
349
349
  ...boardFields(allBoards[n], isBoardName)
350
- })), folders).filter((it) => it.kind === "board" || it.boards.length > 0);
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-DNh6N27C.mjs");
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-Dp-ZFk3d.mjs");
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
@@ -17,19 +17,25 @@ const CONTENT_WIDTH = {
17
17
  document: 760,
18
18
  wide: 1280
19
19
  };
20
- /** The slide stage (v1.5): a runtime-reserved intrinsic, deliberately NOT a
21
- * config viewport - no migration for existing projects, no deck device in
22
- * sweeps. Dependency-neutral so server (shot) and shell (store) share it. */
20
+ /** The default slide stage: 16:9 at 1280×720, deliberately NOT a config viewport - no
21
+ * migration for existing projects, no deck device in sweeps. Dependency-neutral so
22
+ * server (shot) and shell (store, play) share it. */
23
23
  const SLIDE_INTRINSIC = {
24
24
  width: 1280,
25
25
  height: 720
26
26
  };
27
- /** The one DEFAULT sizing rule for slide frames, shared by canvas and shot:
28
- * `slide: true` sets the intrinsic 1280×720 stage, over any authored viewport.
29
- * Board nodes stay resizable (the Slide root scales into whatever box it is
30
- * given); this governs defaults, shots, and stage coordinates. */
31
- function slideSize(frame) {
32
- return frame.slide ? SLIDE_INTRINSIC : null;
27
+ /** The one sizing rule for slide frames, shared by canvas, shot and slides mode: a
28
+ * `slide: true` frame's stage is its declared viewport when the project defines one
29
+ * (a 16:10 deck authored at `laptop` stays 1280×800), else the 1280×720 default. A
30
+ * slide is an ordinary frame at that size - slides mode scales the whole stage to the
31
+ * screen, so the frame never has to. */
32
+ function slideSize(frame, viewports = {}) {
33
+ if (!frame.slide) return null;
34
+ const vp = frame.viewport ? viewports[frame.viewport] : void 0;
35
+ return vp ? {
36
+ width: vp.width,
37
+ height: vp.height
38
+ } : SLIDE_INTRINSIC;
33
39
  }
34
40
  //#endregion
35
41
  //#region src/cli/name.ts
@@ -72,14 +78,14 @@ const resolve$1 = (dir) => {
72
78
  };
73
79
  const cli = cac(NAME);
74
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) => {
75
- const { init } = await import("./init-BQYCS3EU.mjs");
81
+ const { init } = await import("./init-C34BY3R4.mjs");
76
82
  init(resolve$1(opts.root), {
77
83
  mode: opts.mode === "embedded" ? "embedded" : "studio",
78
84
  demo: opts.demo !== false
79
85
  });
80
86
  });
81
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) => {
82
- const { dev } = await import("./dev-D3mP2x27.mjs");
88
+ const { dev } = await import("./dev-BNZF4Mup.mjs");
83
89
  let port;
84
90
  if (opts.port !== void 0) {
85
91
  const n = Number(opts.port);
@@ -89,7 +95,7 @@ for (const [name, desc] of [["dev", "Start the local canvas (everything on: hot
89
95
  await dev(resolve$1(opts.root), port);
90
96
  });
91
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) => {
92
- const { buildSite } = await import("./build-7ed5H2vT.mjs");
98
+ const { buildSite } = await import("./build-C7MqQ7hq.mjs");
93
99
  try {
94
100
  const boards = opts.boards === void 0 ? void 0 : typeof opts.boards === "string" ? opts.boards : "";
95
101
  await buildSite(resolve$1(opts.root), boards, opts.allBoards === true, opts.embedSeeds === true, opts.textures !== false && !process.env.MARVER_NO_TEXTURES);
@@ -135,7 +141,7 @@ cli.command("work <action> [...frames]", "Working state on the canvas: start <sc
135
141
  }
136
142
  });
137
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) => {
138
- const { boardsCommand } = await import("./boards-PuVzw5Wp.mjs");
144
+ const { boardsCommand } = await import("./boards-DnLewfj8.mjs");
139
145
  try {
140
146
  boardsCommand(resolve$1(opts.root), opts);
141
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-B01PSyDc.mjs";
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-B01PSyDc.mjs";
3
- import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-DI-7NAnx.mjs";
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-DMDvDbeP.mjs")).sweepGhosts((m) => console.log(` ${m}`));
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-DI-7NAnx.mjs").then((n) => n.i);
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-DbHvLQUL.mjs");
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);