@marver-design/marver 0.13.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +162 -0
  2. package/README.md +44 -20
  3. package/dist/{build-BxGrHFT2.mjs → build-DfuTQZlY.mjs} +46 -6
  4. package/dist/cli.mjs +21 -7
  5. package/dist/{daemon-BChkzDqQ.mjs → daemon-DalgvoA9.mjs} +1 -1
  6. package/dist/{dev-DLwt3Brb.mjs → dev-BxCmeU_H.mjs} +14 -4
  7. package/dist/{init-BpitOqRQ.mjs → init-QKNi9gvF.mjs} +66 -2
  8. package/dist/{manifest-CS6krOTe.mjs → manifest-BzxSMoDB.mjs} +24 -6
  9. package/dist/{marver-id-gate-B2uraTHS.mjs → marver-id-gate-D6By7XHj.mjs} +1 -1
  10. package/dist/{plugin-DNc4Jpae.mjs → plugin-DJyjmQeh.mjs} +60 -10
  11. package/dist/poster-CbpzSzJu.mjs +143 -0
  12. package/dist/{serve-EjEqsiYa.mjs → serve-Bcwfpvhl.mjs} +3 -2
  13. package/dist/{shot-Cyv3GN79.mjs → shot-BWhoz6cU.mjs} +204 -57
  14. package/dist/{shot-DkkwuCZ2.mjs → shot-By1AItpD.mjs} +3 -2
  15. package/docs/live-jam.md +177 -0
  16. package/docs/publish.md +270 -0
  17. package/docs/sharing.md +333 -0
  18. package/docs/slides.md +140 -0
  19. package/package.json +3 -1
  20. package/src/client/const.ts +13 -0
  21. package/src/client/content/chart-engine.ts +33 -0
  22. package/src/client/content/chart.tsx +138 -0
  23. package/src/client/content/index.tsx +30 -6
  24. package/src/client/content/slide.tsx +238 -0
  25. package/src/client/content/video.tsx +223 -0
  26. package/src/client/frame-host/bridge.js +6 -1
  27. package/src/client/shell/App.tsx +59 -13
  28. package/src/client/shell/LockedApp.tsx +7 -2
  29. package/src/client/shell/Play.tsx +138 -24
  30. package/src/client/shell/Toolbar.tsx +12 -3
  31. package/src/client/shell/canvas/FrameNode.tsx +5 -3
  32. package/src/client/shell/hash.ts +3 -1
  33. package/src/client/shell/icons.tsx +3 -0
  34. package/src/client/shell/play-order.ts +22 -0
  35. package/src/client/shell/store.ts +80 -9
  36. package/src/client/shell/styles.css +23 -27
  37. package/src/client/stage/main.tsx +54 -3
  38. package/src/shared/utm.ts +3 -2
  39. package/templates/AGENTS-embedded.md +20 -4
  40. package/templates/AGENTS-studio.md +20 -4
  41. package/templates/instructions/boards.md +47 -5
  42. package/templates/instructions/craft.md +17 -0
  43. package/templates/instructions/iterate.md +109 -14
  44. package/templates/instructions/jam.md +18 -2
  45. package/templates/instructions/publish.md +7 -0
  46. package/templates/instructions/reference/deck-layouts.md +230 -0
  47. package/templates/instructions/reference/deck-story.md +110 -0
  48. package/templates/instructions/shape.md +15 -1
  49. package/templates/instructions/slides.md +402 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,168 @@
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
+
94
+ ## 0.14.0 - 2026-09-02
95
+
96
+ ### Added
97
+
98
+ - **Slides.** A deck is a scene of `slide: true` frames: 1280×720 on the
99
+ canvas, the slideshow badge, and a real slides mode - the standard
100
+ prototype toolbar and walker, arrows / Space / click to advance, and
101
+ **the board as the sorter** (the deck plays in the board's reading
102
+ order; drag to reorder). Publish with `"type": "slides", "open":
103
+ "slides"` plus `transition` (`fade | none`) and `chrome` (`full |
104
+ minimal | none`); the Slide stage scales and centers itself to ANY
105
+ viewport with pure CSS (fill window, phones, a resized canvas node -
106
+ author px and Tailwind scale together); viewers land straight in the
107
+ deck and the URL survives refresh. The full guide is
108
+ [docs/slides.md](docs/slides.md).
109
+ - **The `Slide` root, `Chart`, and `Video` content primitives.** One root
110
+ owns the stage (asymmetric margins - 88px sides, 44px top and bottom - so
111
+ the middle band breathes; content box 1104×632), the theme tokens
112
+ (`--marver-slide-ground/-ink/-muted` with dark twins, `-accent`, `-font`,
113
+ `-tempo` timing both entrances and morphs), fixed type roles
114
+ (`sl-display` 160, `sl-stat` 88, `sl-assertion` 56, `sl-support` 30,
115
+ `sl-body` 24, `sl-caption` 18), and a development-only overflow marker
116
+ that catches content escaping the stage, or a flex/grid child outgrowing
117
+ its parent; `Chart`
118
+ is Apache ECharts on a fixed supported surface (series bar, line, pie, scatter, radar, gauge, heatmap, funnel, treemap, sunburst, sankey, boxplot; SVG, lazy
119
+ chunk, house-themed, animation stripped at rest); `Video` is
120
+ poster-first with a glass control strip in play mode. The motion
121
+ contract: a resting slide built from the shipped primitives serializes
122
+ to the canvas's lean path - no canvas/video elements, no running
123
+ animation - so a 40-slide deck pans like 40 statics (a slide that embeds
124
+ its own canvas, video, or shadow DOM stays live, as any frame would).
125
+ - **Motion as the diff.** `view-transition-name` morphs between adjacent
126
+ slides are the animation system; build steps are sibling frames; three
127
+ one-shot entrance presets (`data-animate`) run after each transition
128
+ settles. Reduced motion flattens both. Slides mode offers a 1280×720
129
+ Slide device preset beside your devices and fill; `marver shot` renders a
130
+ slide at 1280×720 over any authored viewport; `/content` exports
131
+ `SLIDE_W` / `SLIDE_H`; `--marver-slide-pad-x/-y` set the stage margins.
132
+ - **The slides doctrine.** `marver init` ships
133
+ `design/instructions/slides.md` - assertion-first argument, the type
134
+ scale, 19 layout recipes with budgets and morph anchors, the choreography
135
+ rules, "the space IS the design" (asymmetric margins, the 85% rule, one
136
+ px spacing scale), seven silhouettes chosen before any recipe with a
137
+ storyboard step and pacing rules, and a review gate that squints the
138
+ contact sheet - with two depth references behind it:
139
+ `reference/deck-story.md` (intake, answer-first structure, the evidence
140
+ check, audience calibration, the writing) and `reference/deck-layouts.md`
141
+ (the full layout atlas grouped by job, the composition grid, content
142
+ budgets, rebuilding an existing deck, chart craft). Plus your
143
+ project-owned `design/slides.md` - now opening with a fill-in **deck
144
+ look** template (tokens, type, mark, colour meaning, numbers, voice) the
145
+ agent drafts on the first deck - which overrides the doctrine and survives
146
+ every upgrade.
147
+
148
+ ### Changed
149
+
150
+ - **The play brand pill is a powered-by link** (marver.design, standard UTM
151
+ set, `utm_content=play-brand`), sized to the pixel like the toolbar pill.
152
+ The keyboard-hint pill is gone (two conflicting rules stretched it into a
153
+ full-stage circle; the walker and toolbar already carry stepping and
154
+ comment). Name your canvas - `share: { name }` - before containerising it:
155
+ the shell's fallback is the root directory, which in a container is `app`.
156
+ - **A `slides` board plays only its `slide: true` frames.** 0.13.0 accepted
157
+ `type: "slides"` / `open: "slides"` and played them as `present`. From
158
+ 0.14.0 the deck is the board's slide frames in reading order; non-slide
159
+ frames on such a board warn at build, and a board with none fails the
160
+ build with the fix in the message (a non-slides `type` plus
161
+ `open: "present"` to keep the old behaviour, or `slide: true` on the
162
+ frames). In the app an empty deck
163
+ falls back to the prototype with a toast. `transition` / `chrome` are
164
+ refused on non-slide boards. `marver init` appends `slides-inspiration/`
165
+ to an existing `design/.gitignore`. `docs/` now ships in the package.
166
+
5
167
  ## 0.13.0 - 2026-09-01
6
168
 
7
169
  ### Added
package/README.md CHANGED
@@ -6,7 +6,9 @@
6
6
 
7
7
  **The agent-native design canvas.** A `design/` folder in your repo, one command, and a canvas of live frames built from your app's real components and theme. Your coding agent designs by writing files; the tool ships no AI.
8
8
 
9
- [marver.design](https://marver.design) · [Live Jam](docs/live-jam.md) · [Deploying a canvas](docs/publish.md) · [Sharing](docs/sharing.md) · [Changelog](CHANGELOG.md) · [Contributing](CONTRIBUTING.md) · [Issues](https://github.com/TNEP4/marver/issues)
9
+ Screens, prototypes, specs, and now slide decks - all real code, all on one canvas, all shareable with people who sign in as themselves.
10
+
11
+ [marver.design](https://marver.design) · [Slides](docs/slides.md) · [Live Jam](docs/live-jam.md) · [Deploying a canvas](docs/publish.md) · [Sharing](docs/sharing.md) · [Changelog](CHANGELOG.md) · [Contributing](CONTRIBUTING.md) · [Issues](https://github.com/TNEP4/marver/issues)
10
12
 
11
13
  ## Quickstart
12
14
 
@@ -20,42 +22,59 @@ Then, to your agent:
20
22
 
21
23
  > Read design/AGENTS.md. Build an onboarding scene - welcome, form, done - mobile-first, using our components.
22
24
 
25
+ or
26
+
27
+ > Read design/AGENTS.md. Build me a 12-slide pitch deck from this brief, in our brand.
28
+
23
29
  Frames appear on the canvas the moment the files land. That's the loop.
24
30
 
25
31
  ## Why marver
26
32
 
27
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).
28
35
  - **Everything hot-reloads.** The agent writes, you watch it land - live.
29
36
  - **True viewports.** Each frame is a real iframe: drag its edge and your actual breakpoints fire.
30
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).
31
- - **Feedback without a signup wall.** Publish the canvas, invite people by email, and they sign in as themselves - one free Marver account, Google or an emailed code, and it opens every canvas you ever share with them. Or keep it entirely self-hosted behind a shared password. See [Collaboration](#collaboration).
32
- - **No AI inside.** The designer is the coding agent you already run and pay for. `init` generates the `design/AGENTS.md` contract that teaches it the whole workflow.
38
+ - **Feedback without a signup wall.** Publish the canvas and invite people by email; they sign in as themselves with one free Marver account that opens every canvas anyone ever shares with them. @-mention a reviewer, they get the mail; reply, and the thread's people hear about it. Or keep it entirely self-hosted behind a shared password. See [Collaboration](#collaboration).
39
+ - **No AI inside.** The designer is the coding agent you already run and pay for. `init` generates `design/AGENTS.md` and a `design/instructions/` method - configure, discover, wireframe, brand, build, review, slides, publish - that teaches it the whole workflow.
33
40
 
34
41
  ## The canvas
35
42
 
36
43
  - **Frames, scenes, boards.** Frames are screens, scenes group them (`design/scenes/<scene>/<frame>.tsx`), boards arrange them. Agents write `design/boards/<name>.json` (a frame list is enough); switch boards at the top of the sidebar. `all-scenes` is auto-managed. Right-click any board, scene, or frame in the sidebar to copy its path - the exact string to paste to your agent - and rename or drag-reorder boards from there too.
37
44
  - **Devices view.** Hotkeys `1`-`5` (or the Devices menu) size every frame to mobile / tablet / laptop / monitor / tv to sweep your breakpoints; `0` restores your own layout exactly. Widths live in `design/config.ts`.
38
45
  - **Prototype links.** `data-goto="scene/frame"` on any element links frames into a walkable prototype - across boards, too.
39
- - **Play mode.** Press `p`: the board becomes a full-screen, clickable walkthrough. `data-goto` links navigate, arrow keys step between frames, `[` / `]` cycle variants, `Escape` exits. Publish it and you have a shareable prototype.
40
- - **Content frames.** Specs, Mermaid diagrams, and mood boards live on the same canvas as the screens - import `Doc`, `Md`, `Diagram`, `Img` 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.
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
+ - **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.
41
50
 
42
- ## Collaboration
51
+ ## Slides
43
52
 
44
- - **Comments.** Google-Docs-style feedback pinned to actual elements. Press `c`, click a div inside a frame, write - the thread lives on that element and survives edits via a layered anchor (source semantics → structure → fuzzy text). Viewers on a published canvas comment with real names and avatars - either a Marver account they already have, or an invite-link account on that canvas alone. `marver dev` syncs the same threads into `design/comments/*.jsonl`, where your agent works the queue: `npx marver comments list --open --json` → fork a variant → `resolve --addressed-in`. Live via SSE; one deploy, no extra services.
45
- - **Laser mode.** Press `l`: every element in every frame gets depth-hued outlines plus a hover label - the fastest way to see structure. Click any element to copy its full address (frame file + CSS path) for the agent.
46
- - **Publishing.** `npx marver build` exports a static canvas (default-closed: `design/publish.json` names what ships, and whether each board is `read` or `comment`); `npx marver serve` hosts it. One deploy on Railway, Docker, or any static host - the [publishing guide](docs/publish.md) has the one-pagers.
53
+ 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.
47
54
 
48
- - **Two ways to let people in.** They are alternatives, not layers - pick one per canvas.
55
+ What makes it light on the agent side, and good on every screen:
49
56
 
50
- **Marver Sign In** (new in 0.11). Set `MARVER_ID_ISSUER=https://id.marver.design` and reviewers sign in as themselves, with Google or a six-digit code emailed to them. One free Marver account opens *every* canvas gated this way, so the second board you share costs them nothing: no new signup, no new password, no link to keep. You invite an email address and they are in.
57
+ - **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.
58
+ - **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.
59
+ - **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.)
60
+ - **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.
51
61
 
52
- That is the whole difference, and it is the difference between "I'll look later" and a comment actually landing on the board. It also means their real name and face ride along, so a thread is from a person rather than from an address.
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 - morphs, build steps, `Chart` and `Video`, the theme tokens.
53
63
 
54
- **A shared password** (`MARVER_PASSWORD`). Fully self-hosted, no account anywhere but your own volume, nothing about your reviewers leaves your infrastructure - and still fully supported, not a legacy path. The trade is that every canvas is an island: reviewers claim an invite link and pick a name and password *on that canvas*, and do it again for the next one. Fine for one board, a toll on the fifth.
64
+ ## Collaboration
55
65
 
56
- Either way the canvas runs on your infrastructure and stores its own comments and members. With Marver Sign In the identity service only ever tells your canvas that a verified address matched an entry on your invite list - it never sees your frames, your files, or your comments. Rights stay yours: `design/publish.json` decides which boards are readable and which are commentable, and `marver comments invite`/`revoke` decides who is on the list.
66
+ - **Comments.** Google-Docs-style feedback pinned to actual elements. Press `c`, click a div inside a frame, write - the thread lives on that element and survives edits via a layered anchor (source semantics → structure → fuzzy text). Type `@` to mention someone who can already see the canvas; they get a mail with the comment and a link that lands on the thread, and the mention pulses its pin in their canvas with an in-app notification. A reply mails the thread's other participants, throttled per person and thread; anyone can mute a canvas's mail. `marver dev` syncs the same threads into `design/comments/*.jsonl`, where your agent works the queue: `npx marver comments list --open --json` → fork a variant → `resolve --addressed-in`. Live via SSE; one deploy, no extra services.
67
+ - **Laser mode.** Press `l`: every element in every frame gets depth-hued outlines plus a hover label - the fastest way to see structure. Click any element to copy its full address (frame file + CSS path) for the agent. (On a published canvas, laser needs `"reveal": { "source": true }` in `publish.json` - source paths go opaque by default.)
68
+ - **Publishing.** `npx marver build` exports a static canvas (default-closed: `design/publish.json` names what ships, each board's ceiling - `read` or `comment` - and, since 0.12, its **type** and **landing view**: `{ "max", "type": "doc" | "slides" | "design" | "sketch" | "refs" | "mix", "open": "canvas" | "board" | "present" | "focus" | "slides", "lock": true }`); `npx marver serve` hosts it. One deploy on Railway, Docker, or any static host - the [publishing guide](docs/publish.md) has the one-pagers.
69
+ - **Sharing, person by person.** On a canvas with a persistent volume, `marver share add sam@acme.com --role comment` (or the browser's share dialog) grants one person access, `@acme.com` grants a domain, `general private|password|public` sets the floor, and a refused visitor can ask to get in - approve from the terminal. One resolver answers "what can this person do here", and `marver share explain <email>` shows its reasoning. The [sharing guide](docs/sharing.md) has the whole surface.
57
70
 
58
- *Coming next:* one home for your account - every canvas you have been invited to, every canvas you have shared, and the access each one carries, in a single list. Today the account page at `id.marver.design` shows the canvases you have approved and lets you revoke them.
71
+ - **Two ways to let people in.** They are alternatives, not layers - pick one per canvas.
72
+
73
+ **Marver Sign In.** Set `MARVER_ID_ISSUER=https://id.marver.design` and reviewers sign in as themselves, with Google or a six-digit code emailed to them. One free Marver account opens *every* canvas gated this way, so the second board you share costs them nothing: no new signup, no new password, no link to keep. Their real name and face ride along, so a thread is from a person rather than from an address. And **[app.marver.design](https://app.marver.design)** is their front door: every canvas they can reach, in one signed-in list, each row lit by a summary the canvas itself signs - the front door holds no roster and makes no access decision.
74
+
75
+ **A shared password** (`MARVER_PASSWORD`). Fully self-hosted, no account anywhere but your own volume, nothing about your reviewers leaves your infrastructure - and still fully supported. The trade is that every canvas is an island: reviewers claim an invite link and pick a name and password *on that canvas*, and do it again for the next one.
76
+
77
+ Either way the canvas runs on your infrastructure and stores its own comments and members. With Marver Sign In the identity service only ever tells your canvas that a verified address matched an entry on your list - it never sees your frames, your files, or your comments. Rights stay yours: `design/publish.json` decides which boards are readable and which are commentable, and the roster decides who is on the list.
59
78
 
60
79
  ## Live Jam
61
80
 
@@ -65,7 +84,7 @@ The trust boundary is hard: only comments written on the owner's machine trigger
65
84
 
66
85
  ## Working state
67
86
 
68
- 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.
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.
69
88
 
70
89
  ## Commands
71
90
 
@@ -75,8 +94,10 @@ The same glow, driven from the terminal. When your agent takes a request, it cre
75
94
  | `npx marver dev` / `canvas` | Start the local canvas - hot reload, comments, Live Jam armed (`--port`, default 5199) |
76
95
  | `npx marver build` | Static export → `design/.dist`; what ships comes from `design/publish.json` (default-closed) |
77
96
  | `npx marver serve` | Serve the export; `MARVER_ID_ISSUER` or `MARVER_PASSWORD` gates it, `MARVER_DATA_DIR` persists comments + accounts |
97
+ | `npx marver share …` | The roster (owner): `add <who> [--role]` · `remove` · `block` / `unblock` · `general <mode>` · `list` · `requests` · `explain <who>` · `who` |
78
98
  | `npx marver comments …` | The agent's queue: `connect <url>` · `sync` · `list` · `reply` · `resolve` · `invite <email>` · `revoke <email>` |
79
99
  | `npx marver work …` | Working glow from the terminal: `start <scene/frame …>` · `done … \| --all` · `list` |
100
+ | `npx marver shot <frame> [--scale 1-4]` | Render one frame headless and print the PNG path (needs `dev` running); 2x by default |
80
101
 
81
102
  ## Shortcuts
82
103
 
@@ -89,19 +110,22 @@ The same glow, driven from the terminal. When your agent takes a request, it cre
89
110
 
90
111
  **Board & chrome** - `t` tidy · `d` toggle light/dark for the board · `⌘\` (ctrl+\) collapse/open sidebar.
91
112
 
92
- **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).
114
+
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.
93
116
 
94
- **Modes** - `c` comment mode · `l` laser mode · `⇧C` hide/show comment pins · `⇧L` laser comment (spotlight a thread's element) · `p` play mode · `h` hide all chrome.
117
+ **In play** - `←` `→` step (Space and click advance a deck) · `[` `]` cycle variants · `d` cycles the theme · digits pick a device, the digit after your last one fills the window · `esc` exits, unless the board is locked to that view.
95
118
 
96
119
  ## Notes
97
120
 
98
121
  - **Next.js**: supported with one caveat - frames render in Vite, outside Next. `next/font` CSS variables are undefined inside frames (give font tokens a fallback chain), `next/image`/`next/link` should be plain `img`/`data-goto` in frames, and Server Components cannot run there. `init` writes the specifics into `design/AGENTS.md` when it detects Next.
99
- - **Upgrade**: `npm i -D @marver-design/marver@latest && npx marver init`. The canvas tells you when a new version is out (one anonymous registry check per day, cached in `design/.local/`; `MARVER_NO_UPDATE_CHECK=1` disables). Re-running init refreshes the managed files (AGENTS.md, `design/instructions/`) - your edits to them are detected and preserved; when both you and a release changed a file, the fresh version is staged at `design/.local/latest/` for you (or your agent) to merge. Everything else in `design/` is yours and never touched.
122
+ - **Upgrade**: `npm i -D @marver-design/marver@latest && npx marver init`. The canvas tells you when a new version is out (one anonymous registry check per day, cached in `design/.local/`; `MARVER_NO_UPDATE_CHECK=1` disables), and warns when your `design/instructions/` predate the installed package. Re-running init refreshes the managed files (AGENTS.md, `design/instructions/`) - your edits to them are detected and preserved; when both you and a release changed a file, the fresh version is staged at `design/.local/latest/` for you (or your agent) to merge. Everything else in `design/` is yours and never touched.
123
+ - **Name your canvas**: `share: { name: "Your App" }` in `design/config.ts` titles the gate, labels the brand pill, and tags every powered-by link. Unset, the gate falls back to your `package.json` name and the canvas shell to the directory name - `app` inside most containers.
100
124
  - **Uninstall**: delete `design/`, remove the dependency. (If `init` patched your tsconfig `exclude`, revert that one line.)
101
125
 
102
126
  ## Status
103
127
 
104
- Marver is a young solo side project - it works, it's dogfooded daily, and it has rough edges. The known weak spot: boards with many heavy frames (animation-rich, component-dense) can strain the canvas, especially while zooming - snapshot-based rendering helps but isn't finished. If you hit a wall, an issue with your board shape and frame count genuinely helps.
128
+ Marver is a young solo side project - it works, it's dogfooded daily, and it has rough edges. The known weak spot: boards with many heavy frames (animation-rich, component-dense) can strain the canvas, especially while zooming - snapshot-based rendering helps but isn't finished. Sharing controls who gets in and who may comment; per-board read privacy (a board some people can see and others cannot) is next. If you hit a wall, an issue with your board shape and frame count genuinely helps.
105
129
 
106
130
  ## Contributing
107
131
 
@@ -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-CS6krOTe.mjs";
3
- import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-DNc4Jpae.mjs";
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";
@@ -36,9 +36,20 @@ function scanAssetRefs(src, moduleId) {
36
36
  templates.push(t);
37
37
  return "``";
38
38
  }).replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|\s)\/\/.*$/gm, "$1");
39
- if (/<Img\b[^>]*\bsrc\s*=\s*\{/.test(stripped)) throw new Error(`${moduleId}: <Img src={...}> is computed - published builds copy only statically referenced assets. Use a string literal.`);
39
+ for (const tag of ["Img", "Video"]) if (new RegExp(`<${tag}\\b[^>]*\\b(?:src|poster)\\s*=\\s*\\{`).test(stripped)) throw new Error(`${moduleId}: <${tag}> has a computed src/poster - published builds copy only statically referenced assets. Use a string literal.`);
40
40
  const out = [];
41
41
  for (const m of stripped.matchAll(/<Img\b[^>]*\bsrc\s*=\s*["']([^"']+)["']/g)) out.push(m[1]);
42
+ for (const m of stripped.matchAll(/<Video\b[^>]*>|<Video\b[^>]*\/>/g)) {
43
+ const tag = m[0];
44
+ const srcM = /\bsrc\s*=\s*["']([^"']+)["']/.exec(tag);
45
+ const posterM = /\bposter\s*=\s*["']([^"']+)["']/.exec(tag);
46
+ if (srcM) {
47
+ const remote = /^https:\/\//.test(srcM[1]);
48
+ out.push(srcM[1]);
49
+ if (!remote && !posterM) out.push(`${srcM[1]}.poster.png`);
50
+ }
51
+ if (posterM) out.push(posterM[1]);
52
+ }
42
53
  for (const tpl of templates) for (const m of tpl.matchAll(/!\[[^\]]*\]\(([^)\s"']+)\)/g)) out.push(m[1]);
43
54
  return out;
44
55
  }
@@ -59,6 +70,12 @@ const VIEW_MODES = [
59
70
  "focus",
60
71
  "slides"
61
72
  ];
73
+ const DECK_TRANSITIONS = ["fade", "none"];
74
+ const DECK_CHROME = [
75
+ "full",
76
+ "minimal",
77
+ "none"
78
+ ];
62
79
  /** Publishing is default-CLOSED - no policy and no explicit flag = no build.
63
80
  * Boards absent from the result do not ship. */
64
81
  function resolvePolicy(root, allBoards, boardsFlag, allBoardsFlag) {
@@ -108,12 +125,17 @@ function resolvePolicy(root, allBoards, boardsFlag, allBoardsFlag) {
108
125
  const type = p.type ?? "mix";
109
126
  if (!ARTIFACT_TYPES.includes(type)) throw new Error(`design/publish.json: board "${n}" has type "${type}" - use ${ARTIFACT_TYPES.join(" | ")}`);
110
127
  if (p.open !== void 0 && !VIEW_MODES.includes(p.open)) throw new Error(`design/publish.json: board "${n}" has open "${p.open}" - use ${VIEW_MODES.join(" | ")}`);
128
+ if (p.transition !== void 0 && !DECK_TRANSITIONS.includes(p.transition)) throw new Error(`design/publish.json: board "${n}" has transition "${p.transition}" - use ${DECK_TRANSITIONS.join(" | ")}`);
129
+ if (p.chrome !== void 0 && !DECK_CHROME.includes(p.chrome)) throw new Error(`design/publish.json: board "${n}" has chrome "${p.chrome}" - use ${DECK_CHROME.join(" | ")}`);
130
+ if ((p.transition !== void 0 || p.chrome !== void 0) && type !== "slides" && p.open !== "slides") throw new Error(`design/publish.json: board "${n}" sets transition/chrome but is not a slides board - set "type": "slides" or "open": "slides", or drop them`);
111
131
  if (p.lock && p.open === void 0) throw new Error(`design/publish.json: board "${n}" sets "lock" without "open" - name the mode the lock freezes`);
112
132
  out[n] = {
113
133
  max: p.max,
114
134
  type,
115
135
  ...p.open ? { open: p.open } : {},
116
- ...p.lock ? { lock: true } : {}
136
+ ...p.lock ? { lock: true } : {},
137
+ ...p.transition ? { transition: p.transition } : {},
138
+ ...p.chrome ? { chrome: p.chrome } : {}
117
139
  };
118
140
  }
119
141
  if (policy.reveal !== void 0) {
@@ -277,12 +299,23 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds) {
277
299
  frames: pubFrames.filter((f) => f.scene === name).length
278
300
  }))
279
301
  };
302
+ for (const n of publishedNames) {
303
+ const pb = policy.boards[n];
304
+ if (pb?.type !== "slides" && pb?.open !== "slides") continue;
305
+ const ids = new Set((boards[n]?.nodes ?? []).map((x) => x.frame));
306
+ const on = pubManifest.frames.filter((f) => ids.has(f.id));
307
+ const off = on.filter((f) => !(f.kind === "tsx" && f.slide));
308
+ if (off.length === on.length) throw new Error(`design/publish.json: board "${n}" plays as slides but none of its ${on.length} frame(s) carry \`slide: true\` - add it to the frames' meta, or set "type"/"open" to "present" (0.13.0 let slides alias present; 0.14.0 plays only slide frames)`);
309
+ if (off.length) console.warn(` warning: board "${n}" plays as slides but ${off.length} frame(s) are not slides (${off.slice(0, 4).map((f) => f.id).join(", ")}${off.length > 4 ? ", …" : ""}) - they will not play`);
310
+ }
280
311
  const boardsMeta = Object.fromEntries(publishedNames.filter((n) => policy.boards[n]).map((n) => {
281
312
  const p = policy.boards[n];
282
313
  return [n, {
283
314
  type: p.type,
284
315
  ...p.open ? { open: p.open } : {},
285
- ...p.lock ? { lock: true } : {}
316
+ ...p.lock ? { lock: true } : {},
317
+ ...p.transition ? { transition: p.transition } : {},
318
+ ...p.chrome ? { chrome: p.chrome } : {}
286
319
  }];
287
320
  }));
288
321
  const lockedShell = publishedNames.length > 0 && publishedNames.every((n) => {
@@ -426,6 +459,13 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds) {
426
459
  }
427
460
  let copiedAssets = 0;
428
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
+ }
429
469
  for (const r of refs) {
430
470
  if (!isLocalAssetRef(r)) continue;
431
471
  const srcFile = join(assetsDir, r);
@@ -494,4 +534,4 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds) {
494
534
  console.log(`\n serve it: npx ${NAME} serve (set MARVER_PASSWORD to gate it)\n`);
495
535
  }
496
536
  //#endregion
497
- export { buildSite };
537
+ export { buildSite, isLocalAssetRef, scanAssetRefs };
package/dist/cli.mjs CHANGED
@@ -16,6 +16,20 @@ const CONTENT_WIDTH = {
16
16
  document: 760,
17
17
  wide: 1280
18
18
  };
19
+ /** The slide stage (v1.5): a runtime-reserved intrinsic, deliberately NOT a
20
+ * config viewport - no migration for existing projects, no deck device in
21
+ * sweeps. Dependency-neutral so server (shot) and shell (store) share it. */
22
+ const SLIDE_INTRINSIC = {
23
+ width: 1280,
24
+ height: 720
25
+ };
26
+ /** The one DEFAULT sizing rule for slide frames, shared by canvas and shot:
27
+ * `slide: true` sets the intrinsic 1280×720 stage, over any authored viewport.
28
+ * Board nodes stay resizable (the Slide root scales into whatever box it is
29
+ * given); this governs defaults, shots, and stage coordinates. */
30
+ function slideSize(frame) {
31
+ return frame.slide ? SLIDE_INTRINSIC : null;
32
+ }
19
33
  //#endregion
20
34
  //#region src/cli/index.ts
21
35
  const [major, minor] = process.versions.node.split(".").map(Number);
@@ -39,14 +53,14 @@ function version() {
39
53
  }
40
54
  const cli = cac(NAME);
41
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) => {
42
- const { init } = await import("./init-BpitOqRQ.mjs");
56
+ const { init } = await import("./init-QKNi9gvF.mjs");
43
57
  init(resolve(opts.root), {
44
58
  mode: opts.mode === "embedded" ? "embedded" : "studio",
45
59
  demo: opts.demo !== false
46
60
  });
47
61
  });
48
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) => {
49
- const { dev } = await import("./dev-DLwt3Brb.mjs");
63
+ const { dev } = await import("./dev-BxCmeU_H.mjs");
50
64
  let port;
51
65
  if (opts.port !== void 0) {
52
66
  const n = Number(opts.port);
@@ -56,7 +70,7 @@ for (const [name, desc] of [["dev", "Start the local canvas (everything on: hot
56
70
  await dev(resolve(opts.root), port);
57
71
  });
58
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) => {
59
- const { buildSite } = await import("./build-BxGrHFT2.mjs");
73
+ const { buildSite } = await import("./build-DfuTQZlY.mjs");
60
74
  try {
61
75
  const boards = opts.boards === void 0 ? void 0 : typeof opts.boards === "string" ? opts.boards : "";
62
76
  await buildSite(resolve(opts.root), boards, opts.allBoards === true, opts.embedSeeds === true);
@@ -66,7 +80,7 @@ cli.command("build", "Static export → design/.dist (what ships comes from desi
66
80
  }
67
81
  });
68
82
  cli.command("serve", "Serve design/.dist (set MARVER_PASSWORD to gate it)").option("--root <dir>", "Host repo root", { default: "." }).option("--port <port>", "Port (default $PORT or 4199)").action(async (opts) => {
69
- const { serve } = await import("./serve-EjEqsiYa.mjs");
83
+ const { serve } = await import("./serve-Bcwfpvhl.mjs");
70
84
  let port;
71
85
  if (opts.port !== void 0) {
72
86
  const n = Number(opts.port);
@@ -101,8 +115,8 @@ cli.command("work <action> [...frames]", "Working state on the canvas: start <sc
101
115
  process.exit(1);
102
116
  }
103
117
  });
104
- 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) => {
105
- const { shotCommand } = await import("./shot-DkkwuCZ2.mjs");
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");
106
120
  try {
107
121
  await shotCommand(resolve(opts.root), frame, opts);
108
122
  } catch (err) {
@@ -114,4 +128,4 @@ cli.help();
114
128
  cli.version(version());
115
129
  cli.parse();
116
130
  //#endregion
117
- export { ROUTE as i, NAME as n, PKG as r, CONTENT_WIDTH as t };
131
+ export { slideSize as a, ROUTE as i, NAME as n, PKG as r, CONTENT_WIDTH as t };
@@ -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-CS6krOTe.mjs";
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-CS6krOTe.mjs";
3
- import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-DNc4Jpae.mjs";
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-DNc4Jpae.mjs").then((n) => n.i);
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-BChkzDqQ.mjs");
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);