@marver-design/marver 0.14.0 → 0.15.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 +89 -0
- package/README.md +5 -3
- package/dist/{build-ByYafIhj.mjs → build-DfuTQZlY.mjs} +12 -4
- package/dist/cli.mjs +5 -5
- package/dist/{daemon-Bfucyf1o.mjs → daemon-DalgvoA9.mjs} +1 -1
- package/dist/{dev-yZMyQeUj.mjs → dev-BxCmeU_H.mjs} +14 -4
- package/dist/{init-Dvaso7YO.mjs → init-QKNi9gvF.mjs} +1 -1
- package/dist/{manifest-DvOmglFp.mjs → manifest-BzxSMoDB.mjs} +17 -6
- package/dist/{plugin-BsmG5i2X.mjs → plugin-DJyjmQeh.mjs} +42 -10
- package/dist/poster-CbpzSzJu.mjs +143 -0
- package/dist/{shot-kbR_xzJH.mjs → shot-BWhoz6cU.mjs} +197 -56
- package/dist/{shot-DkkwuCZ2.mjs → shot-By1AItpD.mjs} +3 -2
- package/docs/live-jam.md +6 -2
- package/docs/slides.md +9 -3
- package/package.json +1 -1
- package/src/client/content/chart.tsx +62 -24
- package/src/client/content/index.tsx +5 -2
- package/src/client/content/video.tsx +132 -35
- package/src/client/frame-host/bridge.js +6 -1
- package/src/client/shell/App.tsx +30 -2
- package/src/client/shell/icons.tsx +1 -0
- package/src/client/shell/store.ts +51 -1
- package/src/client/shell/styles.css +6 -0
- package/templates/AGENTS-embedded.md +19 -4
- package/templates/AGENTS-studio.md +19 -4
- package/templates/instructions/boards.md +47 -5
- package/templates/instructions/craft.md +17 -0
- package/templates/instructions/iterate.md +109 -14
- package/templates/instructions/jam.md +18 -2
- package/templates/instructions/shape.md +15 -1
- package/templates/instructions/slides.md +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,95 @@
|
|
|
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.15.0 - 2026-09-02
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **Copy frame as image.** The floating toolbar gains an images-square button
|
|
10
|
+
(right of Copy path): one click puts the selected frame on the clipboard as
|
|
11
|
+
a **2x PNG**, rendered by the dev server's headless Chrome - the same picture
|
|
12
|
+
`marver shot` gives an agent. `i` is the shortcut; `⇧i` (or shift-click)
|
|
13
|
+
renders at **4x** (a slide is 5120×2880). The icon breathes while the render
|
|
14
|
+
runs and flashes a check on success, like Copy path. Sized to the node: a
|
|
15
|
+
frame resized to Laptop copies as Laptop; content frames capture in full;
|
|
16
|
+
slides are always the 1280×720 artwork. Dev canvases only - a published
|
|
17
|
+
container has no browser - and one frame at a time.
|
|
18
|
+
- **`marver shot --scale <1-4>`** and `scale=`/`w=`/`h=`/`format=png` on
|
|
19
|
+
`/api/shot`. A frame too tall for the asked scale steps down (4 → 2 → 1)
|
|
20
|
+
inside a 16384px-per-side / 64M-pixel capture budget and reports it in
|
|
21
|
+
`note`; the file name carries the scale actually used (`…@4x.png`), so a
|
|
22
|
+
4x file always holds 4x pixels.
|
|
23
|
+
- **Render settle.** Every shot (not only content frames) now waits - bounded
|
|
24
|
+
to 3 s, and again in the final viewport of a full-height capture - for fonts,
|
|
25
|
+
in-viewport images, LOD decodes, echarts instances and mermaid diagrams before
|
|
26
|
+
capturing, so a slide with a chart or a photo is never shot half-drawn. Shot
|
|
27
|
+
file names: a default request keeps its unsuffixed name whatever scale the
|
|
28
|
+
capture settled on; an explicit scale gets `@4x`, or `@4x-as-2x` when it had
|
|
29
|
+
to step down, so nothing an agent reads moves and `@4x` never lies.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- **Three standing rules for the agent, from dogfooding** (`AGENTS.md`,
|
|
34
|
+
`boards.md`, `iterate.md`, `jam.md` - refreshed by `marver init`):
|
|
35
|
+
- **One horizontal band by default.** Every curated board carries a `layout`
|
|
36
|
+
recipe with the scenes side by side; a second band only when the agent can
|
|
37
|
+
say why the eye should move down, with a gap that reads as "below". (Without
|
|
38
|
+
a recipe the shell stacks every scene as its own row.)
|
|
39
|
+
- **Look sideways.** A pin or a pasted pointer marks where the human noticed
|
|
40
|
+
a problem, not the only place it lives: the agent checks the board's live
|
|
41
|
+
sibling frames, fixes the same defect in the same pass and names the frames,
|
|
42
|
+
or asks in-thread whether to roll it out - never one frame fixed and its
|
|
43
|
+
siblings left wrong. History (`archive/`, versions) is never a sibling.
|
|
44
|
+
- **Version the scene before a round.** A round of feedback on a reviewed
|
|
45
|
+
scene starts with a snapshot, `design/scenes/<scene>-v<N>/`, pinned as its
|
|
46
|
+
own band on the `archive` board (oldest at the top), with `data-goto`,
|
|
47
|
+
`goto:` links and `meta.of` re-pointed so the version plays on its own; then
|
|
48
|
+
the live frames are edited in place and threads answered. One snapshot per
|
|
49
|
+
round, git commits per version on offer, staging only the round's paths.
|
|
50
|
+
Rollback is a copy back; the archive is the proof of work.
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
- **Charts everywhere, not only on slides.** `Chart` now takes its ink and
|
|
55
|
+
typeface from the frame it sits in (a UI screen's Tailwind colour and font, a
|
|
56
|
+
Doc's tokens, a Slide's), sizes its type to the context (12px labels in a
|
|
57
|
+
screen or document, 18px on a slide), paints series labels in that ink with
|
|
58
|
+
no halo, follows the layout on resize (a chart no longer pins its flex/grid
|
|
59
|
+
column at mount width), and keys on the option's content so a parent
|
|
60
|
+
re-render never re-initialises it (a viewer's dataZoom or legend selection
|
|
61
|
+
survives, and a formatter edit reaches the live chart). Before this a chart
|
|
62
|
+
in a dark screen or dark spec drew dark-on-dark axis text at slide scale.
|
|
63
|
+
- **Video plays everywhere, not only on slides.** The player used to mount
|
|
64
|
+
only under the slides-mode contract, so a `Video` in a screen, a spec or a
|
|
65
|
+
published prototype was a poster forever. Now the poster is the play button
|
|
66
|
+
wherever the frame is live (interact, play, focus, published); slides keep
|
|
67
|
+
the auto-mounted player, and leaving interact mode on the canvas disarms a
|
|
68
|
+
playing clip so the frame goes still again. New: `ratio="9 / 16"` for
|
|
69
|
+
vertical clips, and `autoplay` for a muted ambient loop (an explicit choice -
|
|
70
|
+
that frame stays live on the canvas). The player's glyphs are Phosphor
|
|
71
|
+
(play, pause, speaker, corners). `shape.md` and `craft.md` teach it.
|
|
72
|
+
- **Posters render themselves.** A local clip without `poster` no longer fails
|
|
73
|
+
the build: marver renders `<clip>.poster.png` beside it from the clip's own
|
|
74
|
+
first moments (at 0.5 s, past the usual black opening) in the same headless
|
|
75
|
+
Chrome the shot renderer uses - the dev server on first sight (the frame
|
|
76
|
+
asks when its conventional poster is missing), `marver build` before assets
|
|
77
|
+
are copied, and `shot` before a frame's first capture. Posters render on
|
|
78
|
+
their own capture lane, so a frame's poster never waits behind the shot that
|
|
79
|
+
needs it. An authored poster always wins; without Chrome the build says
|
|
80
|
+
exactly which file to add.
|
|
81
|
+
- **A screen with a chart is still a screen.** Importing anything from
|
|
82
|
+
`@marver-design/marver/content` used to turn the frame into a content
|
|
83
|
+
document (spec badge, measured height, no device). Content frames are now
|
|
84
|
+
those that render `Doc`, `Md`, `Diagram` or `Img`; `Chart`, `Video`, `Row`,
|
|
85
|
+
`Col` and `Space` are shared blocks a screen or a slide uses freely. The
|
|
86
|
+
block card (padding, hairline, surface) is a document treatment: inside a
|
|
87
|
+
slide or a screen a block is bare, so charts no longer carry 32px of
|
|
88
|
+
invisible padding there. `shape.md` and `craft.md` now teach `Chart`.
|
|
89
|
+
- **Dev server behind a symlinked path.** `marver dev` on a repo reached
|
|
90
|
+
through a symlink (`/tmp` → `/private/tmp`, a linked Dropbox folder) refused
|
|
91
|
+
to serve its own frames ("outside of Vite serving allow list"); the allow
|
|
92
|
+
list now carries the realpath too.
|
|
93
|
+
|
|
5
94
|
## 0.14.0 - 2026-09-02
|
|
6
95
|
|
|
7
96
|
### Added
|
package/README.md
CHANGED
|
@@ -45,6 +45,8 @@ Frames appear on the canvas the moment the files land. That's the loop.
|
|
|
45
45
|
- **Prototype links.** `data-goto="scene/frame"` on any element links frames into a walkable prototype - across boards, too.
|
|
46
46
|
- **Five ways to view a board.** The canvas (frames on a plane), the board (the same, tidy), **present** (`p`: a full-screen clickable walkthrough - `data-goto` navigates, arrows step, `[` / `]` cycle variants, laser, comments, theme and device pickers in the toolbar), **focus** (one frame as a document - the reading preset for specs), and **slides** (a deck). A published board names its landing view; a frame deep link opens straight into it.
|
|
47
47
|
- **Content frames.** Specs, Mermaid diagrams, mood boards, and slides live on the same canvas as the screens - import `Doc`, `Md`, `Diagram`, `Img`, `Slide`, `Chart`, `Video` from `@marver-design/marver/content` and think a feature through before any pixels exist. Works in a repo with no app at all: idea first, design second.
|
|
48
|
+
- **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.
|
|
49
|
+
- **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.
|
|
48
50
|
|
|
49
51
|
## Slides
|
|
50
52
|
|
|
@@ -82,7 +84,7 @@ The trust boundary is hard: only comments written on the owner's machine trigger
|
|
|
82
84
|
|
|
83
85
|
## Working state
|
|
84
86
|
|
|
85
|
-
The same glow, driven from the terminal. When your agent takes a request, it creates the frame files first, pins them on a board, and runs `npx marver work start <scene/frame ...>` - you see the work land on the canvas in seconds, watch it shimmer while subagents build in parallel, and see it settle on `work done`. Marks self-expire, so a crashed agent never leaves a frame glowing. And `npx marver shot <scene/frame
|
|
87
|
+
The same glow, driven from the terminal. When your agent takes a request, it creates the frame files first, pins them on a board, and runs `npx marver work start <scene/frame ...>` - you see the work land on the canvas in seconds, watch it shimmer while subagents build in parallel, and see it settle on `work done`. Marks self-expire, so a crashed agent never leaves a frame glowing. And `npx marver shot <scene/frame> [--scale 4]` renders one frame headless to a PNG, so the agent can look at what it built before it says it is done - the same picture you get from the canvas's copy-as-image.
|
|
86
88
|
|
|
87
89
|
## Commands
|
|
88
90
|
|
|
@@ -95,7 +97,7 @@ The same glow, driven from the terminal. When your agent takes a request, it cre
|
|
|
95
97
|
| `npx marver share …` | The roster (owner): `add <who> [--role]` · `remove` · `block` / `unblock` · `general <mode>` · `list` · `requests` · `explain <who>` · `who` |
|
|
96
98
|
| `npx marver comments …` | The agent's queue: `connect <url>` · `sync` · `list` · `reply` · `resolve` · `invite <email>` · `revoke <email>` |
|
|
97
99
|
| `npx marver work …` | Working glow from the terminal: `start <scene/frame …>` · `done … \| --all` · `list` |
|
|
98
|
-
| `npx marver shot <frame
|
|
100
|
+
| `npx marver shot <frame> [--scale 1-4]` | Render one frame headless and print the PNG path (needs `dev` running); 2x by default |
|
|
99
101
|
|
|
100
102
|
## Shortcuts
|
|
101
103
|
|
|
@@ -108,7 +110,7 @@ The same glow, driven from the terminal. When your agent takes a request, it cre
|
|
|
108
110
|
|
|
109
111
|
**Board & chrome** - `t` tidy · `d` toggle light/dark for the board · `⌘\` (ctrl+\) collapse/open sidebar.
|
|
110
112
|
|
|
111
|
-
**Selection** - click selects · shift+click (canvas or sidebar) builds a multi-selection · `⌘A` selects every frame on the board · `⇧P` copies the selected frames' paths (board, frame, and file) · double-click enters interact mode (`esc` or click outside leaves) · drag the title bar to move, edges to resize (widths snap to devices).
|
|
113
|
+
**Selection** - click selects · shift+click (canvas or sidebar) builds a multi-selection · `⌘A` selects every frame on the board · `⇧P` copies the selected frames' paths (board, frame, and file) · `i` copies the selected frame to the clipboard as a 2x PNG (`⇧i` for 4x; also the images-square button in the floating toolbar - dev canvases only, the renderer is the dev server's headless Chrome) · double-click enters interact mode (`esc` or click outside leaves) · drag the title bar to move, edges to resize (widths snap to devices).
|
|
112
114
|
|
|
113
115
|
**Modes** - `c` comment mode · `l` laser mode · `⇧C` hide/show comment pins · `⇧L` laser comment (spotlight a thread's element) · `p` play (present, or slides on a slides board) · `h` hide all chrome.
|
|
114
116
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { i as ROUTE, n as NAME } from "./cli.mjs";
|
|
2
|
-
import { l as detectHost, r as scanFrames, s as loadConfig } from "./manifest-
|
|
3
|
-
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-
|
|
2
|
+
import { l as detectHost, r as scanFrames, s as loadConfig } from "./manifest-BzxSMoDB.mjs";
|
|
3
|
+
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-DJyjmQeh.mjs";
|
|
4
4
|
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { basename, dirname, join, sep } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
@@ -44,8 +44,9 @@ function scanAssetRefs(src, moduleId) {
|
|
|
44
44
|
const srcM = /\bsrc\s*=\s*["']([^"']+)["']/.exec(tag);
|
|
45
45
|
const posterM = /\bposter\s*=\s*["']([^"']+)["']/.exec(tag);
|
|
46
46
|
if (srcM) {
|
|
47
|
-
|
|
47
|
+
const remote = /^https:\/\//.test(srcM[1]);
|
|
48
48
|
out.push(srcM[1]);
|
|
49
|
+
if (!remote && !posterM) out.push(`${srcM[1]}.poster.png`);
|
|
49
50
|
}
|
|
50
51
|
if (posterM) out.push(posterM[1]);
|
|
51
52
|
}
|
|
@@ -458,6 +459,13 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds) {
|
|
|
458
459
|
}
|
|
459
460
|
let copiedAssets = 0;
|
|
460
461
|
const realAssets = existsSync(assetsDir) ? realpathSync(assetsDir) : null;
|
|
462
|
+
for (const r of refs) {
|
|
463
|
+
if (!isLocalAssetRef(r) || !r.endsWith(".poster.png") || existsSync(join(assetsDir, r))) continue;
|
|
464
|
+
const { ensurePoster } = await import("./poster-CbpzSzJu.mjs");
|
|
465
|
+
const g = await ensurePoster(assetsDir, r.slice(0, -11));
|
|
466
|
+
if (!g.ok) throw new Error(`design/assets/${r}: ${g.error}`);
|
|
467
|
+
console.log(` poster: rendered design/assets/${r} (${g.width}×${g.height})`);
|
|
468
|
+
}
|
|
461
469
|
for (const r of refs) {
|
|
462
470
|
if (!isLocalAssetRef(r)) continue;
|
|
463
471
|
const srcFile = join(assetsDir, r);
|
|
@@ -526,4 +534,4 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds) {
|
|
|
526
534
|
console.log(`\n serve it: npx ${NAME} serve (set MARVER_PASSWORD to gate it)\n`);
|
|
527
535
|
}
|
|
528
536
|
//#endregion
|
|
529
|
-
export { buildSite };
|
|
537
|
+
export { buildSite, isLocalAssetRef, scanAssetRefs };
|
package/dist/cli.mjs
CHANGED
|
@@ -53,14 +53,14 @@ function version() {
|
|
|
53
53
|
}
|
|
54
54
|
const cli = cac(NAME);
|
|
55
55
|
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) => {
|
|
56
|
-
const { init } = await import("./init-
|
|
56
|
+
const { init } = await import("./init-QKNi9gvF.mjs");
|
|
57
57
|
init(resolve(opts.root), {
|
|
58
58
|
mode: opts.mode === "embedded" ? "embedded" : "studio",
|
|
59
59
|
demo: opts.demo !== false
|
|
60
60
|
});
|
|
61
61
|
});
|
|
62
62
|
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) => {
|
|
63
|
-
const { dev } = await import("./dev-
|
|
63
|
+
const { dev } = await import("./dev-BxCmeU_H.mjs");
|
|
64
64
|
let port;
|
|
65
65
|
if (opts.port !== void 0) {
|
|
66
66
|
const n = Number(opts.port);
|
|
@@ -70,7 +70,7 @@ for (const [name, desc] of [["dev", "Start the local canvas (everything on: hot
|
|
|
70
70
|
await dev(resolve(opts.root), port);
|
|
71
71
|
});
|
|
72
72
|
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("--root <dir>", "Host repo root", { default: "." }).action(async (opts) => {
|
|
73
|
-
const { buildSite } = await import("./build-
|
|
73
|
+
const { buildSite } = await import("./build-DfuTQZlY.mjs");
|
|
74
74
|
try {
|
|
75
75
|
const boards = opts.boards === void 0 ? void 0 : typeof opts.boards === "string" ? opts.boards : "";
|
|
76
76
|
await buildSite(resolve(opts.root), boards, opts.allBoards === true, opts.embedSeeds === true);
|
|
@@ -115,8 +115,8 @@ cli.command("work <action> [...frames]", "Working state on the canvas: start <sc
|
|
|
115
115
|
process.exit(1);
|
|
116
116
|
}
|
|
117
117
|
});
|
|
118
|
-
cli.command("shot <frame>", "Render one frame headless and print the PNG path (needs `dev` running)").option("--root <dir>", "Host repo root", { default: "." }).option("--theme <name>", "Theme to render (default: light)").action(async (frame, opts) => {
|
|
119
|
-
const { shotCommand } = await import("./shot-
|
|
118
|
+
cli.command("shot <frame>", "Render one frame headless and print the PNG path (needs `dev` running)").option("--root <dir>", "Host repo root", { default: "." }).option("--theme <name>", "Theme to render (default: light)").option("--scale <n>", "Device pixels per CSS px, 1-4 (default: 2; 4 for print-quality stills)").action(async (frame, opts) => {
|
|
119
|
+
const { shotCommand } = await import("./shot-By1AItpD.mjs");
|
|
120
120
|
try {
|
|
121
121
|
await shotCommand(resolve(opts.root), frame, opts);
|
|
122
122
|
} 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 { i as toFrameId } from "./manifest-
|
|
4
|
+
import { i as toFrameId } from "./manifest-BzxSMoDB.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 { n as NAME, r as PKG } from "./cli.mjs";
|
|
2
|
-
import { l as detectHost, s as loadConfig } from "./manifest-
|
|
3
|
-
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-
|
|
2
|
+
import { l as detectHost, s as loadConfig } from "./manifest-BzxSMoDB.mjs";
|
|
3
|
+
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-DJyjmQeh.mjs";
|
|
4
4
|
import { realpathSync } from "node:fs";
|
|
5
5
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
@@ -18,6 +18,14 @@ const portFree = (p) => new Promise((res) => {
|
|
|
18
18
|
s.once("listening", () => s.close(() => res(true)));
|
|
19
19
|
s.listen(p, "127.0.0.1");
|
|
20
20
|
});
|
|
21
|
+
/** realpath, or the path itself when it cannot be resolved (never throws at boot). */
|
|
22
|
+
const safeReal = (p) => {
|
|
23
|
+
try {
|
|
24
|
+
return realpathSync(p);
|
|
25
|
+
} catch {
|
|
26
|
+
return p;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
21
29
|
/** C1: a deterministic per-project port in [5200,5399] derived from the root path. Two projects
|
|
22
30
|
* never collide, and the same project always lands on the same port across restarts. */
|
|
23
31
|
const projectPort = (root) => {
|
|
@@ -189,7 +197,9 @@ async function dev(root, portFlag) {
|
|
|
189
197
|
fs: {
|
|
190
198
|
allow: [.../* @__PURE__ */ new Set([
|
|
191
199
|
root,
|
|
200
|
+
safeReal(root),
|
|
192
201
|
pkgDir,
|
|
202
|
+
safeReal(pkgDir),
|
|
193
203
|
searchForWorkspaceRoot(root)
|
|
194
204
|
])],
|
|
195
205
|
deny: FS_DENY
|
|
@@ -247,7 +257,7 @@ async function dev(root, portFlag) {
|
|
|
247
257
|
console.log(`\n ${NAME} · ${projectName} → http://localhost:${port}/\n`);
|
|
248
258
|
try {
|
|
249
259
|
const { staleManagedInstructions } = await import("./managed-HwHVNI3h.mjs");
|
|
250
|
-
const { installedVersion } = await import("./plugin-
|
|
260
|
+
const { installedVersion } = await import("./plugin-DJyjmQeh.mjs").then((n) => n.i);
|
|
251
261
|
const stale = staleManagedInstructions(root);
|
|
252
262
|
if (stale.length) {
|
|
253
263
|
const shown = stale.slice(0, 4).join(", ") + (stale.length > 4 ? `, +${stale.length - 4} more` : "");
|
|
@@ -285,7 +295,7 @@ async function dev(root, portFlag) {
|
|
|
285
295
|
});
|
|
286
296
|
}
|
|
287
297
|
if (config.jam) {
|
|
288
|
-
const { startJam } = await import("./daemon-
|
|
298
|
+
const { startJam } = await import("./daemon-DalgvoA9.mjs");
|
|
289
299
|
const jam = startJam(root, config.jam, (m) => console.log(m), (board) => server.ws.send("sh:jam-comment", { board }));
|
|
290
300
|
if (jam) {
|
|
291
301
|
const close = server.close.bind(server);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as NAME } from "./cli.mjs";
|
|
2
|
-
import { a as writeManifest, c as detectAgent, l as detectHost, o as DEFAULTS, r as scanFrames, u as readJson } from "./manifest-
|
|
2
|
+
import { a as writeManifest, c as detectAgent, l as detectHost, o as DEFAULTS, r as scanFrames, u as readJson } from "./manifest-BzxSMoDB.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 { dirname, join, relative } from "node:path";
|
|
@@ -403,20 +403,31 @@ function extractMeta(src) {
|
|
|
403
403
|
if (slide !== void 0) out.slide = slide;
|
|
404
404
|
return out;
|
|
405
405
|
}
|
|
406
|
-
/** Content-frame detection: LEXICAL by convention - the frame file
|
|
407
|
-
*
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
406
|
+
/** Content-frame detection: LEXICAL by convention - the frame file itself imports
|
|
407
|
+
* PKG/content (an import specifier scan, so "<Diagram" inside a string in a UI frame can
|
|
408
|
+
* never misbadge it; barrels/re-exports are not detected - meta.intent is the taught path
|
|
409
|
+
* and always works) AND renders a DOCUMENT primitive: Doc, Md, Diagram or Img (reference
|
|
410
|
+
* imagery - a moodboard is Rows of Img). The import alone is not enough - Chart, Video, Row,
|
|
411
|
+
* Col and Space are shared blocks a UI screen or a slide uses too, and a dashboard with one
|
|
412
|
+
* <Chart> is still a screen (device height, no document measuring), never a spec. Returns
|
|
413
|
+
* the inferred intent + natural width, or null for UI frames. */
|
|
411
414
|
const CONTENT_IMPORT = new RegExp(`from\\s+['"]${PKG}/content['"]`);
|
|
412
415
|
const WIDE_DOC = /<Doc\b[^>]*\blayout\s*=\s*["']wide["']/;
|
|
413
416
|
const contentWidthOf = (src) => WIDE_DOC.test(src) ? CONTENT_WIDTH.wide : CONTENT_WIDTH.document;
|
|
417
|
+
/** The source with comments, template literals and CODE-shaped strings blanked (a JSX
|
|
418
|
+
* expression string `{'<Diagram>'}`, an attribute value), so a tag name quoted in an example
|
|
419
|
+
* or a comment never counts as a rendered element. Prose is left alone: JSX text has
|
|
420
|
+
* apostrophes ("it's"), and a blanket quote match would swallow real tags between two of them. */
|
|
421
|
+
const codeOnly = (src) => src.replace(/`(?:[^`\\]|\\[\s\S])*`/g, "``").replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1").replace(/\{\s*(?:"(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*')\s*\}/g, "{\"\"}").replace(/([=(,:[{?+|&!]|\breturn)\s*(?:"(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*')/g, "$1\"\"").replace(/([=(,:[{?+|&!]|\breturn)\s*\/(?:[^/\\\n[]|\\.|\[(?:[^\]\\\n]|\\.)*\])+\/[gimsuy]*/g, "$1/re/");
|
|
414
422
|
function contentScan(src) {
|
|
415
423
|
if (!CONTENT_IMPORT.test(src)) return null;
|
|
416
|
-
const
|
|
424
|
+
const code = codeOnly(src);
|
|
425
|
+
const count = (re) => (code.match(re) ?? []).length;
|
|
426
|
+
const docs = count(/<Doc[\s>/]/g);
|
|
417
427
|
const diagrams = count(/<Diagram[\s>/]/g);
|
|
418
428
|
const imgs = count(/<Img[\s>/]/g);
|
|
419
429
|
const mds = count(/<Md[\s>/]/g);
|
|
430
|
+
if (docs + diagrams + mds + imgs === 0) return null;
|
|
420
431
|
return {
|
|
421
432
|
intent: diagrams > 0 ? "diagram" : imgs > mds ? "moodboard" : "spec",
|
|
422
433
|
width: contentWidthOf(src)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
2
2
|
import { i as ROUTE, n as NAME, r as PKG } from "./cli.mjs";
|
|
3
3
|
import { n as localProfile, t as isConnected } from "./profile-BjAPAJSb.mjs";
|
|
4
|
-
import { a as writeManifest, n as hash, r as scanFrames, t as affectedFrameIds } from "./manifest-
|
|
4
|
+
import { a as writeManifest, n as hash, r as scanFrames, t as affectedFrameIds } from "./manifest-BzxSMoDB.mjs";
|
|
5
5
|
import { copyFileSync, existsSync, linkSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, watch, writeFileSync } from "node:fs";
|
|
6
6
|
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
@@ -283,27 +283,59 @@ function apiMiddleware(root, opts = {}) {
|
|
|
283
283
|
if (!ownerGated(req) && !(token && req.headers["x-mv-work"] === token)) return json(res, 403, { error: "forbidden" });
|
|
284
284
|
const frameId = url.searchParams.get("frame") ?? "";
|
|
285
285
|
const theme = url.searchParams.get("theme") ?? "light";
|
|
286
|
+
const scale = url.searchParams.has("scale") ? Number(url.searchParams.get("scale")) : void 0;
|
|
287
|
+
const dim = (k) => {
|
|
288
|
+
const v = Math.round(Number(url.searchParams.get(k)));
|
|
289
|
+
return url.searchParams.has(k) && Number.isFinite(v) ? v : void 0;
|
|
290
|
+
};
|
|
291
|
+
const size = {
|
|
292
|
+
w: dim("w"),
|
|
293
|
+
h: dim("h")
|
|
294
|
+
};
|
|
295
|
+
const asPng = url.searchParams.get("format") === "png";
|
|
286
296
|
const origin = opts.origin?.() ?? `http://${req.headers.host ?? "localhost"}`;
|
|
287
|
-
const { shootFrame } = await import("./shot-
|
|
297
|
+
const { shootFrame } = await import("./shot-BWhoz6cU.mjs");
|
|
288
298
|
const r = await shootFrame({
|
|
289
299
|
root,
|
|
290
300
|
viewports: opts.viewports ?? {},
|
|
291
301
|
frameId,
|
|
292
302
|
theme,
|
|
293
|
-
origin
|
|
303
|
+
origin,
|
|
304
|
+
scale,
|
|
305
|
+
size
|
|
294
306
|
});
|
|
295
|
-
if (!r.ok) return json(res, r.error.startsWith("unknown frame") ? 404 : r.error
|
|
296
|
-
|
|
307
|
+
if (!r.ok) return json(res, r.error.startsWith("unknown frame") ? 404 : r.error.startsWith("invalid ") ? 400 : 503, { error: r.error });
|
|
308
|
+
const summary = {
|
|
297
309
|
path: r.path,
|
|
298
310
|
frame: frameId,
|
|
299
311
|
theme,
|
|
300
312
|
width: r.width,
|
|
301
313
|
height: r.height,
|
|
302
314
|
scale: r.scale,
|
|
303
|
-
...r.truncated ? {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
315
|
+
...r.truncated ? { truncated: true } : {},
|
|
316
|
+
...r.note ? { note: r.note } : {}
|
|
317
|
+
};
|
|
318
|
+
if (!asPng) return json(res, 200, summary);
|
|
319
|
+
const png = readFileSync(join(root, r.path));
|
|
320
|
+
res.writeHead(200, {
|
|
321
|
+
"content-type": "image/png",
|
|
322
|
+
"content-length": png.length,
|
|
323
|
+
"cache-control": "no-store",
|
|
324
|
+
"x-mv-shot": Buffer.from(JSON.stringify(summary)).toString("base64url")
|
|
325
|
+
});
|
|
326
|
+
return res.end(png);
|
|
327
|
+
}
|
|
328
|
+
if (path === "poster" && req.method === "GET") {
|
|
329
|
+
if (!ownerGated(req)) return json(res, 403, { error: "forbidden" });
|
|
330
|
+
const src = url.searchParams.get("src") ?? "";
|
|
331
|
+
const { ensurePoster, isLocalClip } = await import("./poster-CbpzSzJu.mjs");
|
|
332
|
+
const { isLocalAssetRef } = await import("./build-DfuTQZlY.mjs");
|
|
333
|
+
if (!isLocalAssetRef(src) || !isLocalClip(src)) return json(res, 400, { error: "src must be a clip under design/assets/" });
|
|
334
|
+
const r = await ensurePoster(join(root, "design", "assets"), src);
|
|
335
|
+
if (!r.ok) return json(res, r.error.includes("does not exist") ? 404 : 503, { error: r.error });
|
|
336
|
+
return json(res, 200, {
|
|
337
|
+
path: `design/assets/${src}.poster.png`,
|
|
338
|
+
generated: r.generated
|
|
307
339
|
});
|
|
308
340
|
}
|
|
309
341
|
if (path === "me" && req.method === "GET") return json(res, 200, {
|
|
@@ -817,7 +849,7 @@ function marverPlugin(ctx) {
|
|
|
817
849
|
});
|
|
818
850
|
return;
|
|
819
851
|
}
|
|
820
|
-
const { shootFrame } = await import("./shot-
|
|
852
|
+
const { shootFrame } = await import("./shot-BWhoz6cU.mjs");
|
|
821
853
|
write(await shootFrame({
|
|
822
854
|
root,
|
|
823
855
|
viewports: config.viewports,
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { capture, findChrome } from "./shot-BWhoz6cU.mjs";
|
|
2
|
+
import { constants, copyFileSync, existsSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, join, sep } from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { randomBytes } from "node:crypto";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
//#region src/server/poster.ts
|
|
8
|
+
/**
|
|
9
|
+
* Video posters, generated: a local clip without an authored poster gets one rendered
|
|
10
|
+
* from its own first moments - the same headless Chrome the shot renderer drives, no
|
|
11
|
+
* ffmpeg. The convention is `<clip>.poster.png` beside the clip in design/assets/, and
|
|
12
|
+
* the Video primitive reaches for that name whenever `poster` is omitted, so the dev
|
|
13
|
+
* server (on demand, when the frame first asks) and `marver build` (before assets are
|
|
14
|
+
* copied - a published canvas never ships a poster-less video) produce one file both
|
|
15
|
+
* agree on. An authored poster always wins: this only runs when the file is missing.
|
|
16
|
+
*
|
|
17
|
+
* The frame is taken at min(0.5s, half the duration) - the first frame of most clips is
|
|
18
|
+
* black or a fade. The page is a file:// document holding the clip as a file:// <video>,
|
|
19
|
+
* so nothing has to be served; readiness is the video element appearing in #root once
|
|
20
|
+
* the seek lands, which is the shot renderer's own readiness rule.
|
|
21
|
+
*/
|
|
22
|
+
/** The conventional poster path for a local clip reference (relative to design/assets/). */
|
|
23
|
+
const posterNameFor = (src) => `${src}.poster.png`;
|
|
24
|
+
/** Is this a video reference the generator handles (a local design-asset clip)? The same
|
|
25
|
+
* grammar the client's assetUrl accepts: relative, forward slashes only, no dot segments. */
|
|
26
|
+
const isLocalClip = (src) => !/^https?:\/\//.test(src) && /\.(mp4|webm|mov|m4v|ogv)$/i.test(src) && !src.includes("\\") && !src.includes(":") && !src.startsWith("/") && !src.split("/").some((seg) => seg === ".." || seg === "." || seg === "");
|
|
27
|
+
/** realpath(p) is base or inside it. */
|
|
28
|
+
const inside = (p, base) => {
|
|
29
|
+
try {
|
|
30
|
+
const r = realpathSync(p);
|
|
31
|
+
return r === base || r.startsWith(base + sep);
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const PAGE = (video) => `<!doctype html><html><head><meta charset="utf-8"><style>
|
|
37
|
+
html,body{margin:0;background:#000}#root video{display:block}
|
|
38
|
+
</style></head><body><div id="root"></div><script>
|
|
39
|
+
(function(){
|
|
40
|
+
var v = document.createElement('video');
|
|
41
|
+
v.muted = true; v.playsInline = true; v.preload = 'auto'; v.src = ${JSON.stringify(video)};
|
|
42
|
+
var done = false;
|
|
43
|
+
function show(){ if (done) return; done = true;
|
|
44
|
+
var w = Math.min(1920, v.videoWidth || 1280), h = Math.round(w * ((v.videoHeight || 720) / (v.videoWidth || 1280)));
|
|
45
|
+
v.width = w; v.height = h; v.style.width = w + 'px'; v.style.height = h + 'px';
|
|
46
|
+
document.getElementById('root').appendChild(v) }
|
|
47
|
+
v.addEventListener('loadedmetadata', function(){
|
|
48
|
+
var t = Math.min(0.5, (isFinite(v.duration) ? v.duration : 1) / 2);
|
|
49
|
+
v.addEventListener('seeked', show, { once: true });
|
|
50
|
+
v.currentTime = t; });
|
|
51
|
+
v.addEventListener('error', function(){ window.__mvFrameError = 'the clip could not be decoded by this browser (' + (v.error && v.error.code) + ')' ; document.getElementById('root').appendChild(document.createElement('i')) });
|
|
52
|
+
setTimeout(function(){ if (!done && v.readyState >= 2) show() }, 4000);
|
|
53
|
+
})();
|
|
54
|
+
<\/script></body></html>`;
|
|
55
|
+
/** Render `<clip>.poster.png` for a clip under design/assets/. Idempotent: an existing
|
|
56
|
+
* poster (authored or generated) is left alone. */
|
|
57
|
+
async function ensurePoster(assetsDir, src) {
|
|
58
|
+
if (!isLocalClip(src)) return {
|
|
59
|
+
ok: false,
|
|
60
|
+
error: `not a local clip: ${src}`
|
|
61
|
+
};
|
|
62
|
+
const clip = join(assetsDir, src);
|
|
63
|
+
const out = join(assetsDir, posterNameFor(src));
|
|
64
|
+
if (existsSync(out)) return {
|
|
65
|
+
ok: true,
|
|
66
|
+
path: out,
|
|
67
|
+
width: 0,
|
|
68
|
+
height: 0,
|
|
69
|
+
generated: false
|
|
70
|
+
};
|
|
71
|
+
if (!existsSync(clip)) return {
|
|
72
|
+
ok: false,
|
|
73
|
+
error: `design/assets/${src} does not exist`
|
|
74
|
+
};
|
|
75
|
+
let realAssets;
|
|
76
|
+
try {
|
|
77
|
+
realAssets = realpathSync(assetsDir);
|
|
78
|
+
} catch {
|
|
79
|
+
return {
|
|
80
|
+
ok: false,
|
|
81
|
+
error: "design/assets/ does not exist"
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (!inside(clip, realAssets)) return {
|
|
85
|
+
ok: false,
|
|
86
|
+
error: `design/assets/${src} resolves outside design/assets/ - not rendered`
|
|
87
|
+
};
|
|
88
|
+
if (!inside(dirname(out), realAssets)) return {
|
|
89
|
+
ok: false,
|
|
90
|
+
error: `design/assets/${dirname(src)} resolves outside design/assets/ - not rendered`
|
|
91
|
+
};
|
|
92
|
+
if (!findChrome()) return {
|
|
93
|
+
ok: false,
|
|
94
|
+
error: `no Chrome/Chromium found to render a poster for ${src} - add poster="..." on the <Video>, or install Chrome`
|
|
95
|
+
};
|
|
96
|
+
const dir = mkdtempSync(join(tmpdir(), "mv-poster-"));
|
|
97
|
+
const page = join(dir, "poster.html");
|
|
98
|
+
writeFileSync(page, PAGE(pathToFileURL(realpathSync(clip)).href));
|
|
99
|
+
const tmp = join(dirname(out), `.${basename(out)}.${randomBytes(6).toString("hex")}.tmp`);
|
|
100
|
+
try {
|
|
101
|
+
const r = await capture({
|
|
102
|
+
url: pathToFileURL(page).href,
|
|
103
|
+
width: 1920,
|
|
104
|
+
height: 1080,
|
|
105
|
+
scale: 1,
|
|
106
|
+
out: tmp,
|
|
107
|
+
fullHeight: false,
|
|
108
|
+
clip: "#root video",
|
|
109
|
+
timeoutMs: 2e4
|
|
110
|
+
}, "poster");
|
|
111
|
+
if (!r.ok) return {
|
|
112
|
+
ok: false,
|
|
113
|
+
error: `could not render a poster for ${basename(src)} - ${r.error}`
|
|
114
|
+
};
|
|
115
|
+
try {
|
|
116
|
+
copyFileSync(tmp, out, constants.COPYFILE_EXCL);
|
|
117
|
+
} catch (err) {
|
|
118
|
+
if (err.code === "EEXIST") return {
|
|
119
|
+
ok: true,
|
|
120
|
+
path: out,
|
|
121
|
+
width: r.width,
|
|
122
|
+
height: r.height,
|
|
123
|
+
generated: false
|
|
124
|
+
};
|
|
125
|
+
throw err;
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
ok: true,
|
|
129
|
+
path: out,
|
|
130
|
+
width: r.width,
|
|
131
|
+
height: r.height,
|
|
132
|
+
generated: true
|
|
133
|
+
};
|
|
134
|
+
} finally {
|
|
135
|
+
rmSync(tmp, { force: true });
|
|
136
|
+
rmSync(dir, {
|
|
137
|
+
recursive: true,
|
|
138
|
+
force: true
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
//#endregion
|
|
143
|
+
export { ensurePoster, isLocalClip };
|