@marver-design/marver 0.2.3 → 0.3.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 ADDED
@@ -0,0 +1,46 @@
1
+ # Changelog
2
+
3
+ Notable changes to `@marver-design/marver`. Format follows [Keep a Changelog](https://keepachangelog.com); versions follow semver.
4
+
5
+ ## 0.3.0 - 2026-08-13
6
+
7
+ The co-thinking release: the canvas now holds the thinking, not just the screens.
8
+
9
+ ### Added
10
+
11
+ - **Content frames (SPEC-026).** Specs, Mermaid diagrams, and mood boards as ordinary frames beside UI frames - import `Doc`, `Row`, `Col`, `Space`, `Md`, `Diagram`, `Img` from `@marver-design/marver/content`. Works in a repo with no app at all: idea first, design second.
12
+ - `Doc` auto-sizes the frame to its content (measurement protocol; auto sizes are session-transient, manual resize and device views still win). Published canvases keep parity.
13
+ - `Diagram` is first-class Mermaid, lazily loaded - a workspace with no diagrams ships zero mermaid bytes. Source theme overrides are stripped; parse errors show an in-frame card and heal live.
14
+ - `Md` renders theme-aware markdown; `[label](goto:scene/frame)` links jump the canvas. Raw HTML is inert, images are local-only.
15
+ - `Img` shows `design/assets/` imagery with captions; `h={n}` cover-crops a mixed-aspect row to one height so it reads aligned.
16
+ - Zero-external-request boundary: URLs are rejected in diagram source, rendered SVG is sanitized, published builds copy only referenced local assets.
17
+ - **The marver diagram theme.** Full Apple system palette (12 series colors + systemGray ramp, exact HIG light/dark pairs), system font stack, accent-washed nodes by default - a plain flowchart is never gray-on-gray. Label typography rides inside the SVG (measured, not post-styled).
18
+ - **Frame intent.** Content frames declare `intent` (`diagram` | `spec` | `moodboard` | `notes`); the sidebar shows a glyph per row - every row leads with an icon, variant groups carry the flask.
19
+ - **Sidebar tells the canvas's story.** Rows and scene groups order by canvas position, not file order.
20
+ - **The onboarding fork (SPEC-025 amendment).** Both first-session paths - empty repo and existing app - now stop and ask what the highest priority is: think the idea through together on the canvas, or go straight to screens.
21
+ - **Shape & Iterate doctrine.** `instructions/shape.md` (feature-story boards: specs → lo-fi → hi-fi with graduated spacing) and `instructions/iterate.md` (fork-don't-overwrite, letter variants, the archive ritual).
22
+ - **Craft doctrine hardened.** Real assets are binding (Phosphor icons by default, actual brand logos, fetched imagery). Interactive means visibly interactive at every fidelity - cursor + hover on every clickable target, component-library gaps (shadcn on Tailwind v4 ships `cursor: default` buttons) fixed at the design-system base layer.
23
+
24
+ ### Changed
25
+
26
+ - Markdown typography moved to the HIG scale: 16px body on 1.65, tightened heading tracking, contained Notion-style tables, re-asserted list markers (Tailwind preflight strips them in host apps).
27
+
28
+ ## 0.2.4 - 2026-08-13
29
+
30
+ - Onboarding as a conversation (SPEC-025): setup flow asks what you're building, proposes a stack (a recommendation, not a requirement), hosted tour canvas as the waiting room, local canvas as the reveal. Two dogfood rounds folded in.
31
+
32
+ ## 0.2.3 - 2026-08-12
33
+
34
+ - Hardening release: codex P1s (live-JOIN adjacency, same-directory group invariant, tsx-only inference) and a P2 sweep (extractor boundaries, sceneRows dedupe, play-mode chrome fixes, extreme-zoom badge fade).
35
+
36
+ ## 0.2.2 - 2026-08-12
37
+
38
+ - Update discovery: glass pill + stdout notice + daily registry check (opt out with `MARVER_NO_UPDATE_CHECK=1`). `design/` collision guard on init.
39
+
40
+ ## 0.2.1 - 2026-08-12
41
+
42
+ - The dogfood friction release: all 23 logged friction issues triaged; bugs fixed.
43
+
44
+ ## 0.2.0 - 2026-08-11
45
+
46
+ - First public release on npm as `@marver-design/marver`, Apache-2.0. The agent-native design canvas: `design/` folder, live frames from your app's real components, boards, device sweeps, play mode, published canvases with a password gate.
package/README.md CHANGED
@@ -18,6 +18,7 @@ Then, to your agent:
18
18
  - **Boards**: one canvas on screen at a time. Agents write `design/boards/<name>.json` (a frame list is enough); switch boards at the top of the sidebar. `all-scenes` is auto-managed.
19
19
  - **Devices view**: the Devices menu (or hotkeys `1`-`5`) sizes every frame to mobile / tablet / laptop / monitor / tv to sweep your breakpoints; `0` restores your own layout exactly. Widths live in `design/config.ts`.
20
20
  - `data-goto="scene/frame"` on any element links frames into a walkable prototype.
21
+ - **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. Diagrams ship pre-themed (both modes), content frames auto-size to their content, and everything - devices, play mode, publish - works on them identically. Works in a repo with no app at all: idea first, design second.
21
22
  - **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.
22
23
  - Uninstall: delete `design/`, remove the dependency. (If `init` patched your tsconfig `exclude`, revert that one line.)
23
24
 
@@ -1,7 +1,7 @@
1
- import { r as ROUTE, t as NAME } from "./cli.mjs";
2
- import { a as loadConfig, n as scanFrames, o as detectHost } from "./manifest-mYlO_1Pj.mjs";
3
- import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-YVpBNTB3.mjs";
4
- import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
1
+ import { i as ROUTE, n as NAME } from "./cli.mjs";
2
+ import { a as loadConfig, n as scanFrames, o as detectHost } from "./manifest-DW-T52MM.mjs";
3
+ import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-Crp4CAma.mjs";
4
+ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, writeFileSync } from "node:fs";
5
5
  import { basename, dirname, join, sep } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { build } from "vite";
@@ -24,6 +24,25 @@ const posix = (p) => p.split(sep).join("/");
24
24
  function packageDir() {
25
25
  return join(dirname(fileURLToPath(import.meta.url)), "..");
26
26
  }
27
+ /** Static asset references in one module's source (SPEC-026): <Img src="..."> string
28
+ * literals plus markdown image literals INSIDE TEMPLATE STRINGS (where Md content
29
+ * lives - prose in comments never counts). Comments are stripped first so a commented
30
+ * example cannot fail the build. A computed <Img src={...}> fails CLOSED - the build
31
+ * cannot know what it resolves to, so it must not publish. */
32
+ function scanAssetRefs(src, moduleId) {
33
+ const templates = [];
34
+ const stripped = src.replace(/`(?:[^`\\]|\\[\s\S])*`/g, (t) => {
35
+ templates.push(t);
36
+ return "``";
37
+ }).replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|\s)\/\/.*$/gm, "$1");
38
+ 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
+ const out = [];
40
+ for (const m of stripped.matchAll(/<Img\b[^>]*\bsrc\s*=\s*["']([^"']+)["']/g)) out.push(m[1]);
41
+ for (const tpl of templates) for (const m of tpl.matchAll(/!\[[^\]]*\]\(([^)\s"']+)\)/g)) out.push(m[1]);
42
+ return out;
43
+ }
44
+ /** Same shape the client's assetUrl accepts: relative, inside design/assets/, no tricks. */
45
+ const isLocalAssetRef = (p) => !!p && !p.includes(":") && !p.startsWith("/") && !p.startsWith("\\") && !p.split("/").some((s) => s === ".." || s === "");
27
46
  /** Read every board file; returns name -> parsed json. Bad JSON fails the build loudly. */
28
47
  function readBoards(root) {
29
48
  const dir = join(root, "design", "boards");
@@ -208,6 +227,37 @@ async function buildSite(root, boardsFlag) {
208
227
  mkdirSync(dirname(join(outDir, f.file)), { recursive: true });
209
228
  writeFileSync(join(outDir, f.file), html);
210
229
  }
230
+ const assetsDir = join(root, "design", "assets");
231
+ const rootP = posix(root);
232
+ const moduleIds = /* @__PURE__ */ new Set();
233
+ for (const c of chunks) for (const id of c.moduleIds ?? Object.keys(c.modules ?? {})) moduleIds.add(String(id));
234
+ const refs = /* @__PURE__ */ new Set();
235
+ for (const id of moduleIds) {
236
+ const file = posix(String(id)).split("?")[0];
237
+ if (file.startsWith("\0") || !file.startsWith("/") || file.includes("/node_modules/")) continue;
238
+ if (!/\.(tsx|jsx|ts|js|mjs)$/.test(file)) continue;
239
+ let src;
240
+ try {
241
+ src = readFileSync(file, "utf8");
242
+ } catch {
243
+ continue;
244
+ }
245
+ const rel = file.startsWith(rootP + "/") ? file.slice(rootP.length + 1) : file;
246
+ for (const r of scanAssetRefs(src, rel)) refs.add(r);
247
+ }
248
+ let copiedAssets = 0;
249
+ const realAssets = existsSync(assetsDir) ? realpathSync(assetsDir) : null;
250
+ for (const r of refs) {
251
+ if (!isLocalAssetRef(r)) continue;
252
+ const srcFile = join(assetsDir, r);
253
+ if (!existsSync(srcFile)) throw new Error(`design/assets/${r} is referenced by a published frame but does not exist`);
254
+ const real = posix(realpathSync(srcFile));
255
+ if (!realAssets || !(real === posix(realAssets) || real.startsWith(posix(realAssets) + "/"))) throw new Error(`design/assets/${r} resolves outside design/assets/ (symlink) - refusing to publish it`);
256
+ const dest = join(outDir, "design", "assets", r);
257
+ mkdirSync(dirname(dest), { recursive: true });
258
+ cpSync(srcFile, dest);
259
+ copiedAssets++;
260
+ }
211
261
  let name = basename(root);
212
262
  try {
213
263
  name = JSON.parse(readFileSync(join(root, "package.json"), "utf8")).name ?? name;
@@ -237,6 +287,7 @@ async function buildSite(root, boardsFlag) {
237
287
  console.log(`\n ${NAME} build → design/.dist`);
238
288
  console.log(` boards: ${publishedNames.join(", ")}`);
239
289
  console.log(` frames: ${frames.length}${includeAll ? "" : ` of ${manifest.frames.length} (build-time filter)`}`);
290
+ if (copiedAssets) console.log(` assets: ${copiedAssets} referenced file${copiedAssets === 1 ? "" : "s"} from design/assets/ (unreferenced assets never ship)`);
240
291
  if (!includeAll && existsSync(join(root, "public"))) console.log(` note: the host public/ directory ships in full - the --boards filter covers frames, not public assets`);
241
292
  console.log(`\n serve it: npx ${NAME} serve (set MARVER_PASSWORD to gate it)\n`);
242
293
  }
package/dist/cli.mjs CHANGED
@@ -9,6 +9,13 @@ import { fileURLToPath } from "node:url";
9
9
  const NAME = "marver";
10
10
  const PKG = "@marver-design/marver";
11
11
  const ROUTE = "/__mv";
12
+ /** Content-frame natural widths (SPEC-026): Doc layout -> own-size width.
13
+ * Shared by the Doc primitive (measurement messages) and the server-side
14
+ * manifest scan (defaultSize for content frames) - one source, no drift. */
15
+ const CONTENT_WIDTH = {
16
+ document: 760,
17
+ wide: 1280
18
+ };
12
19
  //#endregion
13
20
  //#region src/cli/index.ts
14
21
  const [major, minor] = process.versions.node.split(".").map(Number);
@@ -32,14 +39,14 @@ function version() {
32
39
  }
33
40
  const cli = cac(NAME);
34
41
  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) => {
35
- const { init } = await import("./init-ClhCgn4v.mjs");
42
+ const { init } = await import("./init-DolP_Ld4.mjs");
36
43
  init(resolve(opts.root), {
37
44
  mode: opts.mode === "embedded" ? "embedded" : "studio",
38
45
  demo: opts.demo !== false
39
46
  });
40
47
  });
41
48
  cli.command("dev", "Start the canvas").option("--root <dir>", "Host repo root", { default: "." }).option("--port <port>", "Port (default 5199)").action(async (opts) => {
42
- const { dev } = await import("./dev-9-80L5i5.mjs");
49
+ const { dev } = await import("./dev-C2oTKuXe.mjs");
43
50
  let port;
44
51
  if (opts.port !== void 0) {
45
52
  const n = Number(opts.port);
@@ -49,7 +56,7 @@ cli.command("dev", "Start the canvas").option("--root <dir>", "Host repo root",
49
56
  await dev(resolve(opts.root), port);
50
57
  });
51
58
  cli.command("build", "Static export → design/.dist").option("--boards <names>", "Publish only these boards (comma-separated); the frame filter is applied at build time").option("--root <dir>", "Host repo root", { default: "." }).action(async (opts) => {
52
- const { buildSite } = await import("./build-Bqd6OEsQ.mjs");
59
+ const { buildSite } = await import("./build-Ckmyci3O.mjs");
53
60
  try {
54
61
  const boards = opts.boards === void 0 ? void 0 : typeof opts.boards === "string" ? opts.boards : "";
55
62
  await buildSite(resolve(opts.root), boards);
@@ -59,7 +66,7 @@ cli.command("build", "Static export → design/.dist").option("--boards <names>"
59
66
  }
60
67
  });
61
68
  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) => {
62
- const { serve } = await import("./serve-BvbAbWeK.mjs");
69
+ const { serve } = await import("./serve-OtA9Nlow.mjs");
63
70
  let port;
64
71
  if (opts.port !== void 0) {
65
72
  const n = Number(opts.port);
@@ -71,4 +78,4 @@ cli.help();
71
78
  cli.version(version());
72
79
  cli.parse();
73
80
  //#endregion
74
- export { PKG as n, ROUTE as r, NAME as t };
81
+ export { ROUTE as i, NAME as n, PKG as r, CONTENT_WIDTH as t };
@@ -1,6 +1,6 @@
1
- import { n as PKG, t as NAME } from "./cli.mjs";
2
- import { a as loadConfig, o as detectHost } from "./manifest-mYlO_1Pj.mjs";
3
- import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-YVpBNTB3.mjs";
1
+ import { n as NAME, r as PKG } from "./cli.mjs";
2
+ import { a as loadConfig, o as detectHost } from "./manifest-DW-T52MM.mjs";
3
+ import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-Crp4CAma.mjs";
4
4
  import { dirname, join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { createLogger, createServer, searchForWorkspaceRoot } from "vite";
@@ -79,7 +79,9 @@ async function dev(root, portFlag) {
79
79
  "react-dom",
80
80
  "react-dom/client",
81
81
  "react/jsx-runtime",
82
- "react/jsx-dev-runtime"
82
+ "react/jsx-dev-runtime",
83
+ `${PKG} > marked`,
84
+ `${PKG} > mermaid`
83
85
  ],
84
86
  entries: [join(clientDir, "frame-host", "index.html"), "design/**/*.{tsx,jsx}"]
85
87
  },
@@ -1,5 +1,5 @@
1
- import { t as NAME } from "./cli.mjs";
2
- import { i as DEFAULTS, n as scanFrames, o as detectHost, r as writeManifest, s as readJson } from "./manifest-mYlO_1Pj.mjs";
1
+ import { n as NAME } from "./cli.mjs";
2
+ import { i as DEFAULTS, n as scanFrames, o as detectHost, r as writeManifest, s as readJson } from "./manifest-DW-T52MM.mjs";
3
3
  import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { dirname, join, relative } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
@@ -98,21 +98,40 @@ function init(root, opts) {
98
98
  for (const f of readdirSync(join(instrRoot, e.name))) if (f.endsWith(".md")) writeManaged(`instructions/${e.name}/${f}`, readFileSync(join(instrRoot, e.name, f), "utf8"));
99
99
  } else if (e.name.endsWith(".md")) writeManaged(`instructions/${e.name}`, readFileSync(join(instrRoot, e.name), "utf8"));
100
100
  const setupPath = join(design, "instructions", "setup.md");
101
- const ourSetup = () => {
102
- try {
103
- const s = readFileSync(setupPath, "utf8");
104
- return s.startsWith("# Setup required") && s.includes("marver init");
105
- } catch {
106
- return false;
101
+ const setupState = () => {
102
+ if (!existsSync(setupPath)) return "absent";
103
+ const s = readFileSync(setupPath, "utf8");
104
+ if (s.startsWith(MANAGED_PREFIX)) {
105
+ const recorded = s.slice(MANAGED_PREFIX.length).split(" ")[0];
106
+ const nl = s.indexOf("\n");
107
+ return nl >= 0 && hashBody(s.slice(nl + 1)) === recorded ? "ours-pristine" : "ours-edited";
107
108
  }
109
+ return s.startsWith("# Setup required") && s.includes("marver init") ? "ours-pristine" : "foreign";
108
110
  };
111
+ const setupWas = setupState();
112
+ const appJustAppeared = !noApp(host) && (setupWas === "ours-pristine" || setupWas === "ours-edited");
109
113
  if (noApp(host)) {
110
- if (!existsSync(setupPath)) write("instructions/setup.md", SETUP_MD);
111
- } else if (existsSync(setupPath) && ourSetup()) {
114
+ if (setupWas === "ours-pristine" && !readFileSync(setupPath, "utf8").startsWith(MANAGED_PREFIX)) rmSync(setupPath);
115
+ if (setupWas !== "foreign") writeManaged("instructions/setup.md", SETUP_MD);
116
+ } else if (setupWas === "ours-pristine") {
112
117
  rmSync(setupPath);
113
118
  console.log(` - design/instructions/setup.md removed (app detected - setup complete)`);
119
+ } else if (setupWas === "ours-edited") console.log(` - design/instructions/setup.md: app detected, but you customized the file - delete it yourself when setup is done`);
120
+ const rootTsconfig = existsSync(join(root, "tsconfig.json"));
121
+ const tsconfigNow = () => rootTsconfig ? readFileSync(join(templates, "design-tsconfig.json"), "utf8").replace("{{PATHS}}", designPaths(root)) : STANDALONE_TSCONFIG;
122
+ write("tsconfig.json", tsconfigNow());
123
+ if (appJustAppeared) {
124
+ const refresh = (rel, noAppPristine, next) => {
125
+ try {
126
+ if (!noAppPristine.includes(next) && noAppPristine.includes(readFileSync(join(design, rel), "utf8"))) {
127
+ writeFileSync(join(design, rel), next);
128
+ created.push(`design/${rel} (updated for the detected app)`);
129
+ }
130
+ } catch {}
131
+ };
132
+ refresh("providers.tsx", [providersTemplate(null, null), providersTemplate(null, host.toaster, host.routerPkg)], providersTemplate(host.router, host.toaster, host.routerPkg));
133
+ refresh("tsconfig.json", [STANDALONE_TSCONFIG], tsconfigNow());
114
134
  }
115
- write("tsconfig.json", existsSync(join(root, "tsconfig.json")) ? readFileSync(join(templates, "design-tsconfig.json"), "utf8").replace("{{PATHS}}", designPaths(root)) : STANDALONE_TSCONFIG);
116
135
  write(".gitignore", ".local/\n.dist/\n");
117
136
  write("scenes/_layout.tsx", readFileSync(join(templates, "root-layout.tsx"), "utf8"));
118
137
  if (!existsSync(join(design, "boards"))) {
@@ -134,15 +153,16 @@ function init(root, opts) {
134
153
  │ No framework, no theme CSS, no component library. ${NAME} builds │
135
154
  │ frames from YOUR components - with none, designs get thrown away. │
136
155
  │ │
137
- │ Setup instructions: design/instructions/setup.md. Set up the app, │
138
- │ re-run init, and that file removes itself. AGENTS.md carries a STOP │
139
- │ so your agent does not design against components that do not exist. │
156
+ │ Setup instructions: design/instructions/setup.md. Your agent will │
157
+ │ ask what you are building, propose a stack, set it up with you, and │
158
+ │ re-run init - that file then removes itself. AGENTS.md points there │
159
+ │ so nothing gets designed against components that do not exist. │
140
160
  └───────────────────────────────────────────────────────────────────────┘`);
141
161
  console.log(`\n commit design/ - only .local/ is ignored`);
142
162
  console.log(` uninstall = delete design/, remove the ${NAME} dependency${host.tsconfigSweepsDesign ? ", revert the \"design\" line in tsconfig exclude" : ""}`);
143
163
  if (!noApp(host) && !existsSync(join(design, "DESIGN.md"))) console.log(`\n note: design/DESIGN.md (the brand doc) does not exist yet - have your agent create it from the app's tokens (instructions/brand.md, Path A) to reach the idle state.`);
144
164
  console.log(`\n next: npx ${NAME} dev (canvas on http://localhost:${DEFAULTS.port} by default)\n`);
145
- if (!noApp(host)) console.log(` then, to your agent: "Read design/AGENTS.md. Build an onboarding scene - welcome, form, done - mobile-first, using our components."\n`);
165
+ if (!noApp(host)) console.log(` then, to your agent: "Read design/AGENTS.md. This is our first session - follow design/instructions/welcome.md."\n`);
146
166
  }
147
167
  const MANAGED_PREFIX = "<!-- marver:managed ";
148
168
  const LEGACY_PREFIX = "<!-- generated by marver init";
@@ -170,7 +190,7 @@ const noApp = (host) => !host.router && !host.tailwind && !host.shadcn && !host.
170
190
  * The STOP branch fires only on the same condition that creates SETUP.md - an app
171
191
  * without Tailwind (plain React + CSS) gets guidance, never a dead pointer. */
172
192
  function uiGuidance(host, isNoApp) {
173
- if (isNoApp) return `STOP - this repo has no app yet. Read design/instructions/setup.md before designing anything.`;
193
+ if (isNoApp) return `Setup required - this repo has no app yet: read design/instructions/setup.md and follow it before designing anything.`;
174
194
  if (host.shadcn) return `Use the app's UI: import from ${host.shadcn.uiAlias}; style with the app's Tailwind classes.`;
175
195
  if (host.tailwind) return `Style with the app's Tailwind classes and design tokens; there is no detected component library - extract shared pieces into design/components/.`;
176
196
  return `Use the app's existing components and stylesheets (import them directly); there is no Tailwind or component library detected - extract shared pieces into design/components/.`;
@@ -183,14 +203,83 @@ const SETUP_MD = `# Setup required - this repo has no app yet
183
203
  > the real stack. While this file exists, DO NOT design.
184
204
 
185
205
  ${NAME} builds frames from YOUR components and YOUR theme. With none, frames become
186
- hand-rolled CSS that shares nothing with the future app and cannot be promoted into
187
- it later - work that gets thrown away.
206
+ hand-rolled CSS that shares nothing with the future app - throwaway work. So the
207
+ first session sets up the stack - TOGETHER with the human. The stack is their
208
+ decision; your job is a good recommendation and a smooth setup. Narrate every step
209
+ in one plain line as you go - and tell the story, not the machinery: this file is
210
+ stage directions, never read it aloud to the human ("setup.md says...", "step 2
211
+ requires..."). Voice rules and the structured-question guidance live in
212
+ instructions/welcome.md - read that section before you say anything.
213
+
214
+ ## 1. Greet and explain
215
+
216
+ Tell the human the repo is empty and that this is a perfect starting point. Then
217
+ the pitch, ~4 sentences in your own words (source: instructions/welcome.md): we design
218
+ in real code; the theme, components, and screens made while designing ARE the
219
+ app's building blocks; by the time the design is agreed most of the UI work
220
+ exists, and building the product means plugging functionality in; the goal is
221
+ alignment on look and feel across themes and devices first.
222
+
223
+ ## 2. Ask what they are building - STOP
224
+
225
+ Two questions, one message (use the harness's structured question tool if it
226
+ has one):
227
+
228
+ 1. "In a sentence or two - what are we building?"
229
+ 2. "Any intuition for the look? Colors, mood, UI style - a sentence like
230
+ 'minimalist, glass UI, witty copy' steers everything. 'Surprise me' is a
231
+ fine answer."
232
+
233
+ Then STOP: no further tool calls, end your turn, resume only after the human
234
+ replies. (The one exception: the human explicitly asked for unattended
235
+ execution - then assume something reasonable, mark it UNCONFIRMED, surface it
236
+ first.)
237
+
238
+ ## 3. Propose the stack - STOP
239
+
240
+ From their answer, recommend a framework with one line of reasoning each:
241
+
242
+ - Marketing site, content, SEO -> latest Next.js.
243
+ - App-like, interactive, client-heavy -> latest React Router.
244
+ - Their answer points somewhere else? Recommend that instead, and say why.
245
+
246
+ If you can search the web, verify current major versions first - one or two
247
+ searches, then propose. Recommend shadcn/ui + latest Tailwind as the default
248
+ component layer (take their current defaults) - but they are a recommendation,
249
+ not a requirement: if the human prefers another component library, plain CSS,
250
+ or their own design system, that wins. Any React + a real stylesheet works;
251
+ ${NAME} adapts to what detection finds.
188
252
 
189
- ## Do this first
253
+ Ask the FORK in the same message - it decides everything after the scaffold,
254
+ so it belongs here, before any tunnel. Two questions through the harness's
255
+ structured question tool when it has one (short labels, one-line trade-offs):
190
256
 
191
- For a web app or marketing site, the blessed stack. NOTE: create-next-app refuses a
192
- non-empty directory (this repo already holds design/ and a package.json), so
193
- scaffold into a temp dir and merge:
257
+ 1. The stack - closing with "aligned, or tell me what you'd rather use".
258
+ 2. How do you want to start?
259
+ - **Think it through together first** - we co-develop the idea, the
260
+ workflow, the specs, and the mood/inspiration on the canvas before any
261
+ pixels. What goes on each screen, what stays out, what the intent is.
262
+ - **Build me something to react to** - I go heads-down and come back with
263
+ a first draft of ~4 screens you can play with, and we iterate from there.
264
+
265
+ Works for any kind of product - a marketing site co-thinks its story and
266
+ inspiration the same way an app co-thinks its workflow. Then STOP: no further
267
+ tool calls, end your turn, wait for both answers.
268
+
269
+ ## 4. Hand them the tour, then scaffold
270
+
271
+ The setup and first draft take real minutes; the human should spend them
272
+ learning the canvas, not watching a terminal. The moment the stack is agreed,
273
+ send them to the ${NAME} tour - a published canvas we host, built to be
274
+ explored: https://tour.marver.design - password \`welcome\`. Tell them it teaches
275
+ selection, devices, themes, variants, and play mode from inside the canvas,
276
+ and that it ends by sending them back to check on you. (Unreachable from this
277
+ machine? Say so and skip it - never stall on it.) Then get to work.
278
+
279
+ NOTE: scaffolders refuse non-empty directories (this repo already holds design/
280
+ and a package.json - the one you likely created with \`npm init -y\` to install
281
+ ${NAME} into), so scaffold into a temp dir and merge. The Next.js lane - adapt
282
+ the same shape to whatever stack was agreed:
194
283
 
195
284
  \`\`\`bash
196
285
  npx create-next-app@latest app-scaffold --ts --tailwind --app --src-dir --yes
@@ -200,18 +289,84 @@ npx create-next-app@latest app-scaffold --ts --tailwind --app --src-dir --yes
200
289
  npx shadcn@latest init
201
290
  \`\`\`
202
291
 
203
- shadcn's flags change between versions - if a flag errors or it prompts despite
204
- --yes, answer the prompts with its defaults. Any React + CSS setup works; the
205
- point is that components and a theme EXIST.
292
+ Scaffolder flags drift between versions (create-next-app and shadcn both) - if
293
+ a flag errors or a prompt appears despite --yes, accept the tool's defaults.
206
294
 
207
- ## Then
295
+ Known scaffold bug if shadcn was agreed (hit on every create-next-app + shadcn
296
+ run so far): shadcn's
297
+ init rewrites the theme CSS's \`@theme inline\` block and leaves
298
+ \`--font-sans: var(--font-sans)\` - self-referential, resolves to nothing, and
299
+ the app silently renders in the browser's default font. After shadcn init,
300
+ open the theme CSS and bind every font token to a variable that actually
301
+ exists (e.g. \`--font-sans: var(--font-geist-sans)\` under Next); check
302
+ --font-heading and friends for the same circularity.
303
+
304
+ Then START the dev server and
305
+ confirm the starter page renders before moving on. Unsure about the stack's
306
+ conventions? Fetch its docs.
307
+
308
+ ## 5. Re-run init
208
309
 
209
310
  \`\`\`bash
210
311
  npx ${NAME} init
211
312
  \`\`\`
212
313
 
213
- init is idempotent: it fills in what it now detects (theme wrapper, providers, a
214
- shadcn-aware AGENTS.md), deletes this file, and you design from real parts.
314
+ init is idempotent: it detects the real stack, deletes this file, and
315
+ regenerates AGENTS.md against reality. Verify the wiring (instructions/
316
+ configure.md): frames render styled, one app component imports cleanly.
317
+ DESIGN.md comes next, as part of the first draft.
318
+
319
+
320
+ ## 6. The path they chose at the fork
321
+
322
+ **Chose "think it together"?** No tunnel - the canvas becomes the shared
323
+ thinking surface (instructions/shape.md is the guide). Start
324
+ \`npx ${NAME} dev\`, then seed a feature-story board from what step 2 taught
325
+ you: an intent content frame with their answer restated as problem/goal/
326
+ non-goals, a first-guess workflow diagram (mermaid - your draft of THEIR flow,
327
+ made to be corrected), and a mood frame - fetch real inspiration and brand
328
+ references when you can search (craft.md "Real assets"). Reveal it EARLY with
329
+ the board deep link and iterate together: what belongs on each screen, what
330
+ stays out, what the intent of each step is. Wireframes and the hi-fi draft
331
+ come after the story is agreed - and by then the brief writes itself. Delete
332
+ the generic demo scene once your frames are in.
333
+
334
+ **Chose "build me something"?** The impressive first draft, below.
335
+
336
+ ## The first draft - make it impressive, then tour
337
+
338
+ This is the human's first impression of the canvas AND the first draft of their
339
+ product - it sets the direction. Take the time to do it well:
340
+
341
+ - If you can search the web, spend a few minutes understanding the domain from
342
+ step 2's answer; write design/DESIGN.md. The human's look intuition from
343
+ step 2 is the north star - honor it literally. If they said "surprise me",
344
+ commit to a direction and name it in one sentence at the reveal.
345
+ - Build ~4 frames of THEIR product - not lorem, not filler. Hold them to the
346
+ craft bar: instructions/craft.md and instructions/reference/slop.md are
347
+ binding here. Responsive, working in BOTH themes, linked with data-goto so
348
+ play mode flows.
349
+ - UNDERWHELMING IS THE FAILURE MODE. A restrained concept executed thinly
350
+ reads as a wireframe, however careful the type. Every frame needs presence -
351
+ scale, contrast, color, one real visual moment - and the human should feel
352
+ the direction before they read a word. Write the copy like it ships:
353
+ specific, confident, witty where the brand allows, never placeholder. This
354
+ first draft is the product's first impression AND ${NAME}'s - go above and
355
+ beyond.
356
+ - Delete the generic demo scene (design/scenes/demo/) once your frames are in -
357
+ it exists to show YOU the file shapes, not to impress anyone.
358
+ - Create a curated board for them (instructions/boards.md) containing every
359
+ frame the flow visits.
360
+ - Offer a divergence: "want a variant of <frame> exploring a different
361
+ direction?" - one a-/b- pair teaches the variant workflow better than any
362
+ explanation.
363
+
364
+ This first draft skips the written-brief ceremony (the human just told you what
365
+ they are building) but never the quality bar. Then THE REVEAL: start
366
+ \`npx ${NAME} dev\` and give the guided tour from instructions/welcome.md -
367
+ by now the human has played with the hosted tour, so keep it short and let
368
+ their own product carry it. End with the deep link using the PRINTED port -
369
+ \`http://localhost:<port>/#/b/<board>\`, never the bare root URL.
215
370
  `;
216
371
  /** Next.js frames render OUTSIDE Next - say concretely what that means (friction log #10/#11). */
217
372
  const NEXT_NOTES = `- Next.js caveats (frames render in Vite, outside Next):
@@ -1,3 +1,4 @@
1
+ import { r as PKG, t as CONTENT_WIDTH } from "./cli.mjs";
1
2
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
2
3
  import { join, relative, sep } from "node:path";
3
4
  import { pathToFileURL } from "node:url";
@@ -218,8 +219,29 @@ function extractMeta(src) {
218
219
  if (of) out.of = of;
219
220
  const variant = pick("variant");
220
221
  if (variant) out.variant = variant;
222
+ const intent = pick("intent");
223
+ if (intent) out.intent = intent;
221
224
  return out;
222
225
  }
226
+ /** Content-frame detection (SPEC-026): LEXICAL by convention - the frame file
227
+ * itself imports PKG/content (an import specifier scan, so "<Diagram" inside a
228
+ * string in a UI frame can never misbadge it; barrels/re-exports are not
229
+ * detected - meta.intent is the taught path and always works). Returns the
230
+ * inferred intent + natural width, or null for UI frames. */
231
+ const CONTENT_IMPORT = new RegExp(`from\\s+['"]${PKG}/content['"]`);
232
+ const WIDE_DOC = /<Doc\b[^>]*\blayout\s*=\s*["']wide["']/;
233
+ const contentWidthOf = (src) => WIDE_DOC.test(src) ? CONTENT_WIDTH.wide : CONTENT_WIDTH.document;
234
+ function contentScan(src) {
235
+ if (!CONTENT_IMPORT.test(src)) return null;
236
+ const count = (re) => (src.match(re) ?? []).length;
237
+ const diagrams = count(/<Diagram[\s>/]/g);
238
+ const imgs = count(/<Img[\s>/]/g);
239
+ const mds = count(/<Md[\s>/]/g);
240
+ return {
241
+ intent: diagrams > 0 ? "diagram" : imgs > mds ? "moodboard" : "spec",
242
+ width: contentWidthOf(src)
243
+ };
244
+ }
223
245
  /** id = path relative to design/, extension dropped, `scenes/` prefix dropped. Always `/`-separated. */
224
246
  function toFrameId(designRelPath) {
225
247
  const noExt = designRelPath.split(sep).join("/").replace(FRAME_EXT, "");
@@ -256,12 +278,18 @@ function scanFrames(root) {
256
278
  scene
257
279
  };
258
280
  if (kind === "tsx") {
259
- const meta = extractMeta(readFileSync(abs, "utf8"));
281
+ const src = readFileSync(abs, "utf8");
282
+ const meta = extractMeta(src);
260
283
  if (meta.title) entry.title = meta.title;
261
284
  if (meta.viewport) entry.viewport = meta.viewport;
262
285
  if (meta.theme) entry.theme = meta.theme;
263
286
  if (meta.of) entry.variantGroup = meta.of;
264
287
  if (meta.variant) entry.variant = meta.variant;
288
+ const content = contentScan(src);
289
+ if (content || meta.intent) {
290
+ entry.intent = meta.intent ?? content.intent;
291
+ entry.contentWidth = content?.width ?? contentWidthOf(src);
292
+ }
265
293
  }
266
294
  frames.push(entry);
267
295
  }
@@ -1,5 +1,5 @@
1
- import { n as PKG, r as ROUTE, t as NAME } from "./cli.mjs";
2
- import { n as scanFrames, r as writeManifest, t as hash } from "./manifest-mYlO_1Pj.mjs";
1
+ import { i as ROUTE, n as NAME, r as PKG } from "./cli.mjs";
2
+ import { n as scanFrames, r as writeManifest, t as hash } from "./manifest-DW-T52MM.mjs";
3
3
  import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, watch, writeFileSync } from "node:fs";
4
4
  import { dirname, join, resolve, sep } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
@@ -1,4 +1,4 @@
1
- import { t as NAME } from "./cli.mjs";
1
+ import { n as NAME } from "./cli.mjs";
2
2
  import { existsSync, readFileSync, realpathSync } from "node:fs";
3
3
  import { extname, isAbsolute, join, relative, resolve } from "node:path";
4
4
  import { createHmac, randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marver-design/marver",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "description": "The agent-native design canvas. A design/ folder, one command, a canvas of live frames built from your repo's real components. The tool ships no AI - your coding agent is the designer.",
5
5
  "type": "module",
6
6
  "private": false,
@@ -14,10 +14,12 @@
14
14
  "templates",
15
15
  "README.md",
16
16
  "LICENSE",
17
- "NOTICE"
17
+ "NOTICE",
18
+ "CHANGELOG.md"
18
19
  ],
19
20
  "exports": {
20
21
  "./runtime": "./src/client/runtime/index.ts",
22
+ "./content": "./src/client/content/index.tsx",
21
23
  "./package.json": "./package.json"
22
24
  },
23
25
  "scripts": {
@@ -30,6 +32,8 @@
30
32
  "@tailwindcss/vite": "^4.0.0",
31
33
  "@vitejs/plugin-react": "^6.0.0",
32
34
  "cac": "^7.0.0",
35
+ "marked": "^16.0.0",
36
+ "mermaid": "^11.6.0",
33
37
  "react-zoom-pan-pinch": "^3.6.1",
34
38
  "vite": "^8.0.0",
35
39
  "zustand": "^5.0.0"
@@ -3,3 +3,8 @@
3
3
  export const NAME = 'marver'
4
4
  export const PKG = '@marver-design/marver' // registry identity; bin stays `marver`
5
5
  export const ROUTE = '/__mv'
6
+
7
+ /** Content-frame natural widths (SPEC-026): Doc layout -> own-size width.
8
+ * Shared by the Doc primitive (measurement messages) and the server-side
9
+ * manifest scan (defaultSize for content frames) - one source, no drift. */
10
+ export const CONTENT_WIDTH: Record<string, number> = { document: 760, wide: 1280 }