@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.
- package/CHANGELOG.md +90 -0
- package/README.md +8 -10
- package/dist/{bake-kaf5kGZ7.mjs → bake-BID6mo-N.mjs} +1 -1
- package/dist/{boards-BmxcT3Lc.mjs → boards-BwiDAmPf.mjs} +95 -48
- package/dist/{boards-PuVzw5Wp.mjs → boards-DnLewfj8.mjs} +20 -11
- package/dist/{build-7ed5H2vT.mjs → build-C7MqQ7hq.mjs} +18 -8
- package/dist/cli.mjs +19 -13
- package/dist/{daemon-DbHvLQUL.mjs → daemon-CRZFpl6K.mjs} +1 -1
- package/dist/{dev-D3mP2x27.mjs → dev-BNZF4Mup.mjs} +5 -5
- package/dist/{init-BQYCS3EU.mjs → init-C34BY3R4.mjs} +34 -33
- package/dist/{manifest-B01PSyDc.mjs → manifest-mMfUhPtL.mjs} +8 -5
- package/dist/{plugin-DI-7NAnx.mjs → plugin-omHLCn91.mjs} +34 -26
- package/dist/{poster-DNh6N27C.mjs → poster-BvxiAzy1.mjs} +1 -1
- package/dist/{publish-bakes-Dp-ZFk3d.mjs → publish-bakes-BqzAAa3w.mjs} +8 -2
- package/dist/{shot-DMDvDbeP.mjs → shot-DswS4iRK.mjs} +7 -7
- package/docs/live-jam.md +1 -1
- package/docs/slides.md +89 -89
- package/docs/sticky-notes.md +9 -0
- package/package.json +1 -1
- package/src/client/const.ts +27 -9
- package/src/client/content/chart.tsx +8 -8
- package/src/client/content/index.tsx +5 -4
- package/src/client/content/slide.tsx +26 -201
- package/src/client/frame-host/main.tsx +10 -0
- package/src/client/shell/BoardList.tsx +111 -52
- package/src/client/shell/Comments.tsx +12 -4
- package/src/client/shell/Play.tsx +26 -14
- package/src/client/shell/Toolbar.tsx +7 -5
- package/src/client/shell/canvas/FrameNode.tsx +16 -2
- package/src/client/shell/store.ts +8 -8
- package/src/client/shell/styles.css +10 -9
- package/src/client/stage/main.tsx +56 -7
- package/src/shared/board-tree.ts +271 -140
- package/templates/AGENTS-embedded.md +3 -3
- package/templates/AGENTS-studio.md +3 -3
- package/templates/instructions/boards.md +47 -22
- package/templates/instructions/reference/deck-layouts.md +153 -199
- package/templates/instructions/reference/deck-story.md +6 -6
- 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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
1
|
+
import { AREA, SURFACE, pool, shotConcurrency, withBrowser } from "./shot-DswS4iRK.mjs";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, utimesSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join, sep } from "node:path";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
@@ -9,9 +9,11 @@ const hash = (s) => createHash("sha256").update(s).digest("hex");
|
|
|
9
9
|
/**
|
|
10
10
|
* Board folders - the pure tree shared by the sidebar, the dev API, the build and the
|
|
11
11
|
* tests. Files are the truth: a board says which folder it sits in (`folder` on the
|
|
12
|
-
* board file, ranked among its siblings by
|
|
13
|
-
* says which folders exist
|
|
14
|
-
*
|
|
12
|
+
* board file - one slug, the folder it sits in directly), ranked among its siblings by
|
|
13
|
+
* `order`, and `design/boards/_folders.json` says which folders exist, where they rank, and
|
|
14
|
+
* which folder holds which (`parent`). Two levels: a folder holds boards and folders, a
|
|
15
|
+
* folder inside a folder (a sub-folder) holds boards only. `all-scenes` never enters the
|
|
16
|
+
* tree - callers pin it last.
|
|
15
17
|
*/
|
|
16
18
|
/** The on-disk name grammar shared by boards and folders (a board name is a filename). */
|
|
17
19
|
const BOARD_NAME = /^[a-z0-9][a-z0-9-]*$/;
|
|
@@ -36,12 +38,13 @@ const folderExtras = (it) => ({
|
|
|
36
38
|
...it.description ? { description: it.description } : {}
|
|
37
39
|
});
|
|
38
40
|
/** The registry file's shape. Returns the rows, or a string naming what is wrong - a
|
|
39
|
-
* malformed registry is an ERROR the human must fix (silently reading it as empty
|
|
40
|
-
* let the next drag overwrite their folders), while a
|
|
41
|
+
* malformed registry is an ERROR the human must fix (silently reading it as empty, or
|
|
42
|
+
* flattening a broken nesting, would let the next drag overwrite their folders), while a
|
|
43
|
+
* missing file is simply no folders. */
|
|
41
44
|
function parseFolders(raw) {
|
|
42
45
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return "expected an object";
|
|
43
46
|
const { version, folders } = raw;
|
|
44
|
-
if (version !== void 0 && version !== 1) return `unsupported version ${String(version)}`;
|
|
47
|
+
if (version !== void 0 && version !== 1 && version !== 2) return `unsupported version ${String(version)}`;
|
|
45
48
|
if (!Array.isArray(folders)) return "expected a \"folders\" array";
|
|
46
49
|
const out = [];
|
|
47
50
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -51,30 +54,45 @@ function parseFolders(raw) {
|
|
|
51
54
|
if (seen.has(name)) return `folder "${name}" is listed twice`;
|
|
52
55
|
seen.add(name);
|
|
53
56
|
const o = f.order;
|
|
57
|
+
const p = f.parent;
|
|
58
|
+
if (p !== void 0 && !isBoardName(p)) return `folder "${name}" has an invalid parent - a folder name`;
|
|
54
59
|
const t = readTitle(f.title);
|
|
55
60
|
const d = readDescription(f.description);
|
|
56
61
|
out.push({
|
|
57
62
|
name,
|
|
58
63
|
...typeof o === "number" && Number.isFinite(o) ? { order: o } : {},
|
|
64
|
+
...p !== void 0 ? { parent: p } : {},
|
|
59
65
|
...t ? { title: t } : {},
|
|
60
66
|
...d ? { description: d } : {}
|
|
61
67
|
});
|
|
62
68
|
}
|
|
69
|
+
const byName = new Map(out.map((f) => [f.name, f]));
|
|
70
|
+
for (const f of out) {
|
|
71
|
+
if (f.parent === void 0) continue;
|
|
72
|
+
if (version !== 2) return `folder "${f.name}" names a parent - nested folders need "version": 2`;
|
|
73
|
+
if (f.parent === f.name) return `folder "${f.name}" cannot be its own parent`;
|
|
74
|
+
const p = byName.get(f.parent);
|
|
75
|
+
if (!p) return `folder "${f.name}" names an unknown parent "${f.parent}"`;
|
|
76
|
+
if (p.parent !== void 0) return `folder "${f.name}" would sit three levels deep - folders nest one level only`;
|
|
77
|
+
}
|
|
63
78
|
return out;
|
|
64
79
|
}
|
|
65
|
-
/** Sidebar order from the files.
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
80
|
+
/** Sidebar order from the files. At every level the boards and folders there share one
|
|
81
|
+
* sequence, ranked by `order`, then kind (board before folder), then name; unranked sorts
|
|
82
|
+
* after ranked. The root holds root boards and top-level folders (registered without a
|
|
83
|
+
* parent, or implied by a board that names an unregistered folder); a top-level folder holds
|
|
84
|
+
* its boards and its sub-folders; a sub-folder holds its boards. A folder's title and
|
|
85
|
+
* description ride on its item (they live in the registry a tree write rewrites); a board's
|
|
86
|
+
* title stays with its row - it lives in the board's own file. */
|
|
70
87
|
function buildTree(boards, folders) {
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
88
|
+
const reg = /* @__PURE__ */ new Map();
|
|
89
|
+
for (const f of folders) if (isBoardName(f.name) && !reg.has(f.name)) reg.set(f.name, f);
|
|
90
|
+
const parentOf = (n) => {
|
|
91
|
+
const p = reg.get(n)?.parent;
|
|
92
|
+
return p !== void 0 && p !== n && reg.has(p) && reg.get(p).parent === void 0 ? p : void 0;
|
|
93
|
+
};
|
|
77
94
|
const members = /* @__PURE__ */ new Map();
|
|
95
|
+
const implied = [];
|
|
78
96
|
const rootBoards = [];
|
|
79
97
|
for (const b of boards) {
|
|
80
98
|
if (!isBoardName(b.name) || b.name === "all-scenes") continue;
|
|
@@ -83,37 +101,43 @@ function buildTree(boards, folders) {
|
|
|
83
101
|
rootBoards.push(b);
|
|
84
102
|
continue;
|
|
85
103
|
}
|
|
86
|
-
if (!
|
|
104
|
+
if (!reg.has(folder) && !implied.includes(folder)) implied.push(folder);
|
|
87
105
|
const list = members.get(folder) ?? [];
|
|
88
106
|
list.push(b);
|
|
89
107
|
members.set(folder, list);
|
|
90
108
|
}
|
|
91
|
-
const
|
|
92
|
-
const
|
|
109
|
+
const sorted = (xs) => xs.sort((a, b) => rank(a.order) - rank(b.order) || (a.item.kind === b.item.kind ? 0 : a.item.kind === "board" ? -1 : 1) || a.item.name.localeCompare(b.item.name)).map((r) => r.item);
|
|
110
|
+
const boardsHere = (name) => (name === null ? rootBoards : members.get(name) ?? []).map((b) => ({
|
|
93
111
|
item: {
|
|
94
112
|
kind: "board",
|
|
95
113
|
name: b.name
|
|
96
114
|
},
|
|
97
115
|
order: b.order
|
|
98
|
-
}))
|
|
116
|
+
}));
|
|
117
|
+
const folder = (name, kids) => ({
|
|
99
118
|
item: {
|
|
100
119
|
kind: "folder",
|
|
101
120
|
name,
|
|
102
|
-
|
|
103
|
-
...
|
|
121
|
+
items: sorted(kids),
|
|
122
|
+
...folderExtras(reg.get(name) ?? {})
|
|
104
123
|
},
|
|
105
|
-
order
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
|
|
124
|
+
order: reg.get(name)?.order
|
|
125
|
+
});
|
|
126
|
+
const subsOf = (top) => [...reg.keys()].filter((n) => parentOf(n) === top);
|
|
127
|
+
const tops = [...[...reg.keys()].filter((n) => !parentOf(n)), ...implied];
|
|
128
|
+
return sorted([...boardsHere(null), ...tops.map((t) => folder(t, [...boardsHere(t), ...subsOf(t).map((s) => folder(s, boardsHere(s)))]))]);
|
|
109
129
|
}
|
|
110
|
-
/** Every board in reading order - the order the switchers and the landing pick use. */
|
|
130
|
+
/** Every board in reading order, depth-first - the order the switchers and the landing pick use. */
|
|
111
131
|
function flatten(tree) {
|
|
112
132
|
const out = [];
|
|
113
|
-
|
|
114
|
-
|
|
133
|
+
const walk = (items) => {
|
|
134
|
+
for (const it of items) if (it.kind === "board") out.push(it.name);
|
|
135
|
+
else walk(it.items);
|
|
136
|
+
};
|
|
137
|
+
walk(tree);
|
|
115
138
|
return out;
|
|
116
139
|
}
|
|
140
|
+
const wireKids = (w) => w.items ?? w.boards ?? [];
|
|
117
141
|
function validateWire(wire) {
|
|
118
142
|
if (!Array.isArray(wire)) return "invalid tree";
|
|
119
143
|
const boards = /* @__PURE__ */ new Set(), folders = /* @__PURE__ */ new Set();
|
|
@@ -123,28 +147,51 @@ function validateWire(wire) {
|
|
|
123
147
|
boards.add(n);
|
|
124
148
|
return null;
|
|
125
149
|
};
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
150
|
+
const walk = (list, depth) => {
|
|
151
|
+
for (const w of list) {
|
|
152
|
+
if (typeof w === "string") {
|
|
153
|
+
const e = board(w);
|
|
154
|
+
if (e) return e;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (!w || typeof w !== "object" || Array.isArray(w)) return "invalid tree item";
|
|
158
|
+
if (depth >= 2) return "folders nest one level only";
|
|
159
|
+
const { folder, items, boards: legacy, title, description } = w;
|
|
160
|
+
if (!isBoardName(folder)) return "invalid folder name in tree";
|
|
161
|
+
if (title !== void 0 && (typeof title !== "string" || Array.from(title).length > 120)) return "invalid folder title";
|
|
162
|
+
if (description !== void 0 && (typeof description !== "string" || description.length > 300)) return "invalid folder description";
|
|
163
|
+
if (folders.has(folder)) return `folder "${folder}" appears twice`;
|
|
164
|
+
folders.add(folder);
|
|
165
|
+
const kids = items ?? legacy;
|
|
166
|
+
if (!Array.isArray(kids)) return "invalid folder in tree";
|
|
167
|
+
if (items === void 0 && kids.some((k) => typeof k !== "string")) return "invalid folder in tree";
|
|
168
|
+
const e = walk(kids, depth + 1);
|
|
129
169
|
if (e) return e;
|
|
130
|
-
continue;
|
|
131
170
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (description !== void 0 && (typeof description !== "string" || description.length > 300)) return "invalid folder description";
|
|
137
|
-
if (folders.has(folder)) return `folder "${folder}" appears twice`;
|
|
138
|
-
folders.add(folder);
|
|
139
|
-
if (!Array.isArray(kids)) return "invalid folder in tree";
|
|
140
|
-
for (const k of kids) {
|
|
141
|
-
const e = board(k);
|
|
142
|
-
if (e) return e;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
171
|
+
return null;
|
|
172
|
+
};
|
|
173
|
+
const e = walk(wire, 0);
|
|
174
|
+
if (e) return e;
|
|
145
175
|
if (boards.size > 200 || folders.size > 50) return "tree too large";
|
|
146
176
|
return null;
|
|
147
177
|
}
|
|
178
|
+
/** Every folder with the folder it sits in (null = the root), top-level folders first in
|
|
179
|
+
* reading order, each followed by its sub-folders. */
|
|
180
|
+
function folderEntries(t) {
|
|
181
|
+
const out = [];
|
|
182
|
+
for (const it of t) {
|
|
183
|
+
if (it.kind !== "folder") continue;
|
|
184
|
+
out.push({
|
|
185
|
+
folder: it,
|
|
186
|
+
parent: null
|
|
187
|
+
});
|
|
188
|
+
for (const k of it.items) if (k.kind === "folder") out.push({
|
|
189
|
+
folder: k,
|
|
190
|
+
parent: it.name
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
148
195
|
//#endregion
|
|
149
196
|
//#region src/server/boards.ts
|
|
150
197
|
/**
|
|
@@ -287,4 +334,4 @@ function readRegistry(boardsDir) {
|
|
|
287
334
|
};
|
|
288
335
|
}
|
|
289
336
|
//#endregion
|
|
290
|
-
export { listBoardFiles as a, BOARD_NAME as c, flatten as d,
|
|
337
|
+
export { wireKids as _, listBoardFiles as a, BOARD_NAME as c, flatten as d, folderEntries as f, validateWire as g, readTitle as h, isRegularFile as i, FOLDERS_FILE as l, readDescription as m, checkBoardsDir as n, nodeExists as o, isBoardName as p, checkRealDirs as r, readRegistry as s, boardFields as t, buildTree as u, hash as v };
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { a as listBoardFiles, d as flatten,
|
|
1
|
+
import { a as listBoardFiles, d as flatten, n as checkBoardsDir, p as isBoardName, s as readRegistry, t as boardFields, u as buildTree } from "./boards-BwiDAmPf.mjs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
//#region src/cli/boards.ts
|
|
4
4
|
/**
|
|
5
5
|
* `marver boards` - the sidebar as the agent sees it: every folder and board in reading
|
|
6
|
-
* order
|
|
7
|
-
* is in them, what ranks where" before the agent writes `folder` on a board or edits
|
|
6
|
+
* order - sub-folders indented under their parent - from the files (no dev server needed). One
|
|
7
|
+
* call answers "what folders exist, what is in them, what ranks where" before the agent writes `folder` on a board or edits
|
|
8
8
|
* `design/boards/_folders.json`. `--json` gives the tree shape the shell uses.
|
|
9
9
|
*/
|
|
10
10
|
function boardsCommand(root, opts) {
|
|
@@ -43,15 +43,24 @@ function boardsCommand(root, opts) {
|
|
|
43
43
|
const r = rows.find((x) => x.name === n);
|
|
44
44
|
return `${n}${title(r?.title)}${order(n)}${desc(r?.description)}`;
|
|
45
45
|
};
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
const count = (f) => {
|
|
47
|
+
const boards = f.items.filter((k) => k.kind === "board").length, subs = f.items.length - boards;
|
|
48
|
+
return `${boards} board${boards === 1 ? "" : "s"}${subs ? `, ${subs} folder${subs === 1 ? "" : "s"}` : ""}`;
|
|
49
|
+
};
|
|
50
|
+
const print = (items, pad, parent) => {
|
|
51
|
+
for (const it of items) {
|
|
52
|
+
if (it.kind === "board") {
|
|
53
|
+
console.log(`${pad}${boardLine(it.name)}`);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const where = parent ? `sub-folder of ${parent}` : "folder";
|
|
57
|
+
const implied = reg.folders.some((f) => f.name === it.name) ? "" : ", implied by its boards - not in _folders.json";
|
|
58
|
+
console.log(`${pad}${it.name}/${title(it.title)} (${where}, ${count(it)}${implied})${desc(it.description)}`);
|
|
59
|
+
print(it.items, `${pad} `, it.name);
|
|
60
|
+
if (!it.items.length) console.log(`${pad} (empty)`);
|
|
50
61
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (!it.boards.length) console.log(" (empty)");
|
|
54
|
-
}
|
|
62
|
+
};
|
|
63
|
+
print(tree, "", null);
|
|
55
64
|
if (hasAll) console.log("all-scenes (auto, always last)");
|
|
56
65
|
const landing = flatten(tree)[0];
|
|
57
66
|
if (landing) console.log(`\nlanding board: ${landing}`);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { a as ROUTE, r as NAME } from "./cli.mjs";
|
|
2
|
-
import { a as listBoardFiles, d as flatten,
|
|
3
|
-
import { c as loadConfig, r as scanFrames, u as detectHost } from "./manifest-
|
|
4
|
-
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-
|
|
2
|
+
import { a as listBoardFiles, d as flatten, n as checkBoardsDir, p as isBoardName, s as readRegistry, t as boardFields, u as buildTree } from "./boards-BwiDAmPf.mjs";
|
|
3
|
+
import { c as loadConfig, r as scanFrames, u as detectHost } from "./manifest-mMfUhPtL.mjs";
|
|
4
|
+
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-omHLCn91.mjs";
|
|
5
5
|
import { cpSync, existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { basename, dirname, join, sep } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
@@ -339,15 +339,24 @@ function readBoards(root) {
|
|
|
339
339
|
return out;
|
|
340
340
|
}
|
|
341
341
|
/** Switcher order = the sidebar's reading order: the folder tree over the PUBLISHED boards only
|
|
342
|
-
* (a folder left with no published board drops out - its name never reaches the bundle),
|
|
342
|
+
* (a folder left with no published board at any depth drops out - its name never reaches the bundle),
|
|
343
343
|
* flattened depth-first; all-scenes always LAST (it is the expensive everything-board, never
|
|
344
344
|
* the landing). `names[0]` is where `/` opens. Folder names of published boards are structure,
|
|
345
345
|
* like board names: they ship. */
|
|
346
346
|
function publishedTree(published, allBoards, folders) {
|
|
347
|
-
const
|
|
347
|
+
const built = buildTree(published.filter((n) => n !== "all-scenes").map((n) => ({
|
|
348
348
|
name: n,
|
|
349
349
|
...boardFields(allBoards[n], isBoardName)
|
|
350
|
-
})), folders)
|
|
350
|
+
})), folders);
|
|
351
|
+
const prune = (items) => items.flatMap((it) => {
|
|
352
|
+
if (it.kind === "board") return [it];
|
|
353
|
+
const kids = prune(it.items);
|
|
354
|
+
return kids.length ? [{
|
|
355
|
+
...it,
|
|
356
|
+
items: kids
|
|
357
|
+
}] : [];
|
|
358
|
+
});
|
|
359
|
+
const tree = prune(built);
|
|
351
360
|
return {
|
|
352
361
|
tree,
|
|
353
362
|
names: [...flatten(tree), ...published.includes("all-scenes") ? ["all-scenes"] : []]
|
|
@@ -361,6 +370,7 @@ function publishedManifest(manifest, pubFrames, publishedNames, strip) {
|
|
|
361
370
|
const pubBoardSet = new Set(publishedNames);
|
|
362
371
|
const pubBoards = (manifest.boards ?? []).filter((b) => pubBoardSet.has(b.name));
|
|
363
372
|
const pubFolderSet = new Set(pubBoards.map((b) => b.folder).filter(Boolean));
|
|
373
|
+
for (const f of manifest.folders ?? []) if (f.parent && pubFolderSet.has(f.name)) pubFolderSet.add(f.parent);
|
|
364
374
|
const pubFolders = (manifest.folders ?? []).filter((f) => pubFolderSet.has(f.name));
|
|
365
375
|
return {
|
|
366
376
|
...manifest.project ? { project: manifest.project } : {},
|
|
@@ -683,7 +693,7 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds, textures =
|
|
|
683
693
|
const realAssets = existsSync(assetsDir) ? realpathSync(assetsDir) : null;
|
|
684
694
|
for (const r of refs) {
|
|
685
695
|
if (!isLocalAssetRef(r) || !r.endsWith(".poster.png") || existsSync(join(assetsDir, r))) continue;
|
|
686
|
-
const { ensurePoster } = await import("./poster-
|
|
696
|
+
const { ensurePoster } = await import("./poster-BvxiAzy1.mjs");
|
|
687
697
|
const g = await ensurePoster(assetsDir, r.slice(0, -11));
|
|
688
698
|
if (!g.ok) throw new Error(`design/assets/${r}: ${g.error}`);
|
|
689
699
|
console.log(` poster: rendered design/assets/${r} (${g.width}×${g.height})`);
|
|
@@ -750,7 +760,7 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds, textures =
|
|
|
750
760
|
}));
|
|
751
761
|
let textureLine = " textures: skipped (--no-textures)";
|
|
752
762
|
if (textures && bakeGen) {
|
|
753
|
-
const { bakePublished } = await import("./publish-bakes-
|
|
763
|
+
const { bakePublished } = await import("./publish-bakes-BqzAAa3w.mjs");
|
|
754
764
|
const pubFrameFile = new Map(pubFrames.map((f) => [f.id, f]));
|
|
755
765
|
const r = await bakePublished({
|
|
756
766
|
root,
|
package/dist/cli.mjs
CHANGED
|
@@ -17,19 +17,25 @@ const CONTENT_WIDTH = {
|
|
|
17
17
|
document: 760,
|
|
18
18
|
wide: 1280
|
|
19
19
|
};
|
|
20
|
-
/** The slide stage
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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
|
|
28
|
-
* `slide: true`
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
|
|
32
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
4
|
+
import { a as toFrameId } from "./manifest-mMfUhPtL.mjs";
|
|
5
5
|
import { workActivity } from "./work-CLrmY-vQ.mjs";
|
|
6
6
|
import { r as deviceId, t as has } from "./ledger-Bu0BjqIe.mjs";
|
|
7
7
|
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, watch, writeFileSync, writeSync } from "node:fs";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { i as PKG, r as NAME } from "./cli.mjs";
|
|
2
|
-
import { c as loadConfig, u as detectHost } from "./manifest-
|
|
3
|
-
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-
|
|
2
|
+
import { c as loadConfig, u as detectHost } from "./manifest-mMfUhPtL.mjs";
|
|
3
|
+
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-omHLCn91.mjs";
|
|
4
4
|
import { readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
5
5
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
@@ -452,12 +452,12 @@ async function dev(root, portFlag) {
|
|
|
452
452
|
console.log(`\n ${NAME} · ${projectName} → http://localhost:${port}/\n`);
|
|
453
453
|
setTimeout(async () => {
|
|
454
454
|
try {
|
|
455
|
-
await (await import("./shot-
|
|
455
|
+
await (await import("./shot-DswS4iRK.mjs")).sweepGhosts((m) => console.log(` ${m}`));
|
|
456
456
|
} catch {}
|
|
457
457
|
}, 0);
|
|
458
458
|
try {
|
|
459
459
|
const { staleManagedInstructions } = await import("./managed-HwHVNI3h.mjs");
|
|
460
|
-
const { installedVersion } = await import("./plugin-
|
|
460
|
+
const { installedVersion } = await import("./plugin-omHLCn91.mjs").then((n) => n.i);
|
|
461
461
|
const stale = staleManagedInstructions(root);
|
|
462
462
|
if (stale.length) {
|
|
463
463
|
const shown = stale.slice(0, 4).join(", ") + (stale.length > 4 ? `, +${stale.length - 4} more` : "");
|
|
@@ -495,7 +495,7 @@ async function dev(root, portFlag) {
|
|
|
495
495
|
});
|
|
496
496
|
}
|
|
497
497
|
if (config.jam) {
|
|
498
|
-
const { startJam } = await import("./daemon-
|
|
498
|
+
const { startJam } = await import("./daemon-CRZFpl6K.mjs");
|
|
499
499
|
const jam = startJam(root, config.jam, (m) => console.log(m), (board) => server.ws.send("sh:jam-comment", { board }));
|
|
500
500
|
if (jam) {
|
|
501
501
|
const close = server.close.bind(server);
|