@marver-design/marver 0.2.4 → 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 +46 -0
- package/README.md +1 -0
- package/dist/{build-Bqd6OEsQ.mjs → build-Ckmyci3O.mjs} +55 -4
- package/dist/cli.mjs +12 -5
- package/dist/{dev-9-80L5i5.mjs → dev-C2oTKuXe.mjs} +6 -4
- package/dist/{init-Di4geblA.mjs → init-DolP_Ld4.mjs} +36 -7
- package/dist/{manifest-mYlO_1Pj.mjs → manifest-DW-T52MM.mjs} +29 -1
- package/dist/{plugin-YVpBNTB3.mjs → plugin-Crp4CAma.mjs} +2 -2
- package/dist/{serve-BvbAbWeK.mjs → serve-OtA9Nlow.mjs} +1 -1
- package/package.json +6 -2
- package/src/client/const.ts +5 -0
- package/src/client/content/diagram.tsx +96 -0
- package/src/client/content/index.tsx +196 -0
- package/src/client/content/md.ts +50 -0
- package/src/client/content/palette.ts +93 -0
- package/src/client/shell/App.tsx +50 -6
- package/src/client/shell/canvas/FrameNode.tsx +3 -1
- package/src/client/shell/icons.tsx +22 -0
- package/src/client/shell/store.ts +150 -17
- package/src/client/shell/styles.css +9 -1
- package/templates/AGENTS-embedded.md +5 -0
- package/templates/AGENTS-studio.md +5 -0
- package/templates/instructions/boards.md +6 -0
- package/templates/instructions/brand.md +3 -1
- package/templates/instructions/craft.md +49 -0
- package/templates/instructions/discover.md +2 -2
- package/templates/instructions/iterate.md +50 -0
- package/templates/instructions/shape.md +171 -0
- package/templates/instructions/welcome.md +27 -12
- package/templates/instructions/wireframe.md +8 -1
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 {
|
|
2
|
-
import { a as loadConfig, n as scanFrames, o as detectHost } from "./manifest-
|
|
3
|
-
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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 {
|
|
81
|
+
export { ROUTE as i, NAME as n, PKG as r, CONTENT_WIDTH as t };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { n as
|
|
2
|
-
import { a as loadConfig, o as detectHost } from "./manifest-
|
|
3
|
-
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-
|
|
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 {
|
|
2
|
-
import { i as DEFAULTS, n as scanFrames, o as detectHost, r as writeManifest, s as readJson } from "./manifest-
|
|
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";
|
|
@@ -248,10 +248,23 @@ searches, then propose. Recommend shadcn/ui + latest Tailwind as the default
|
|
|
248
248
|
component layer (take their current defaults) - but they are a recommendation,
|
|
249
249
|
not a requirement: if the human prefers another component library, plain CSS,
|
|
250
250
|
or their own design system, that wins. Any React + a real stylesheet works;
|
|
251
|
-
${NAME} adapts to what detection finds.
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
251
|
+
${NAME} adapts to what detection finds.
|
|
252
|
+
|
|
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):
|
|
256
|
+
|
|
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.
|
|
255
268
|
|
|
256
269
|
## 4. Hand them the tour, then scaffold
|
|
257
270
|
|
|
@@ -304,7 +317,23 @@ configure.md): frames render styled, one app component imports cleanly.
|
|
|
304
317
|
DESIGN.md comes next, as part of the first draft.
|
|
305
318
|
|
|
306
319
|
|
|
307
|
-
## 6. The
|
|
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
|
|
308
337
|
|
|
309
338
|
This is the human's first impression of the canvas AND the first draft of their
|
|
310
339
|
product - it sets the direction. Take the time to do it well:
|
|
@@ -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
|
|
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 {
|
|
2
|
-
import { n as scanFrames, r as writeManifest, t as hash } from "./manifest-
|
|
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 {
|
|
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.
|
|
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"
|
package/src/client/const.ts
CHANGED
|
@@ -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 }
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Diagram block - first-class Mermaid (SPEC-026).
|
|
3
|
+
* Mermaid is marver's dependency behind a dynamic import: a workspace with no
|
|
4
|
+
* Diagram loads no mermaid bytes. Source-level theme overrides (%%{init}%%,
|
|
5
|
+
* yaml frontmatter) are stripped so the marver palette always holds; the
|
|
6
|
+
* rendered SVG is sanitized of external references (strict securityLevel is
|
|
7
|
+
* not a no-network policy). A parse error renders an in-frame card - never a
|
|
8
|
+
* blank frame, never a tripped readiness timeout.
|
|
9
|
+
*/
|
|
10
|
+
import { useEffect, useRef, useState, type ReactNode } from 'react'
|
|
11
|
+
import { FONT_STACK, THEME_CSS, themeVars } from './palette.ts'
|
|
12
|
+
|
|
13
|
+
let uidSeq = 0
|
|
14
|
+
|
|
15
|
+
const isDark = () =>
|
|
16
|
+
document.documentElement.classList.contains('dark') || document.documentElement.dataset.theme === 'dark'
|
|
17
|
+
|
|
18
|
+
/** Strip yaml frontmatter and %%{...}%% directives - diagram source cannot re-theme itself. */
|
|
19
|
+
export function cleanSource(src: string): string {
|
|
20
|
+
return src
|
|
21
|
+
.replace(/^\s*---\r?\n[\s\S]*?\r?\n---\r?\n/, '')
|
|
22
|
+
.replace(/%%\{[\s\S]*?\}%%/g, '')
|
|
23
|
+
.trim()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Remove external URL references from rendered SVG (images, links, href attrs). */
|
|
27
|
+
export function sanitizeSvg(svg: string): string {
|
|
28
|
+
const doc = new DOMParser().parseFromString(svg, 'image/svg+xml')
|
|
29
|
+
const external = (v: string | null) => !!v && !v.trim().startsWith('#') && !v.trim().startsWith('data:')
|
|
30
|
+
for (const el of [...doc.querySelectorAll('image')]) el.remove()
|
|
31
|
+
for (const el of [...doc.querySelectorAll('script, foreignObject iframe')]) el.remove()
|
|
32
|
+
for (const el of [...doc.querySelectorAll('*')]) {
|
|
33
|
+
for (const attr of ['href', 'xlink:href']) {
|
|
34
|
+
if (external(el.getAttribute(attr))) el.removeAttribute(attr)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return new XMLSerializer().serializeToString(doc.documentElement)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function Diagram({ title, children }: { title?: string; children?: ReactNode }) {
|
|
41
|
+
const src = cleanSource(
|
|
42
|
+
typeof children === 'string' ? children : Array.isArray(children) ? children.join('') : String(children ?? ''),
|
|
43
|
+
)
|
|
44
|
+
const ref = useRef<HTMLDivElement>(null)
|
|
45
|
+
const [error, setError] = useState<string | null>(null)
|
|
46
|
+
const uid = useRef(`mv-mmd-${++uidSeq}`)
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
let live = true
|
|
50
|
+
let seq = 0
|
|
51
|
+
let renderSeq = 0
|
|
52
|
+
const render = async () => {
|
|
53
|
+
const mySeq = ++seq
|
|
54
|
+
try {
|
|
55
|
+
// the zero-external-request boundary must hold BEFORE render: mermaid's image
|
|
56
|
+
// shapes fetch their URL during render(), so post-render SVG sanitizing alone
|
|
57
|
+
// would be too late. Reject ANY URL shape - absolute (scheme://) and
|
|
58
|
+
// protocol-relative (//host) alike; neither has a place in diagram source.
|
|
59
|
+
if (/(?:\w+:)?\/\//.test(src)) throw new Error('URLs are not allowed in diagram source - use local design/assets/ images in an Img block instead')
|
|
60
|
+
const mermaid = (await import('mermaid')).default
|
|
61
|
+
if (!live || mySeq !== seq) return
|
|
62
|
+
mermaid.initialize({
|
|
63
|
+
startOnLoad: false,
|
|
64
|
+
securityLevel: 'strict',
|
|
65
|
+
theme: 'base',
|
|
66
|
+
themeVariables: themeVars(isDark()),
|
|
67
|
+
themeCSS: THEME_CSS,
|
|
68
|
+
fontFamily: FONT_STACK,
|
|
69
|
+
})
|
|
70
|
+
// unique id per render: mermaid mounts a temp element under it, and a
|
|
71
|
+
// superseded render must never collide with the one that lands
|
|
72
|
+
const { svg } = await mermaid.render(`${uid.current}-${++renderSeq}`, src)
|
|
73
|
+
if (!live || mySeq !== seq || !ref.current) return // superseded by a newer theme - discard
|
|
74
|
+
ref.current.innerHTML = sanitizeSvg(svg)
|
|
75
|
+
setError(null)
|
|
76
|
+
} catch (e) {
|
|
77
|
+
if (live && mySeq === seq) setError(String((e as Error)?.message ?? e))
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
render()
|
|
81
|
+
// the bridge mutates <html data-theme>/.dark with no React event - observe and re-render
|
|
82
|
+
const mo = new MutationObserver(render)
|
|
83
|
+
mo.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme', 'class'] })
|
|
84
|
+
return () => { live = false; mo.disconnect() }
|
|
85
|
+
}, [src])
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<figure className="mv-block mv-diagram">
|
|
89
|
+
{/* the render target stays MOUNTED through errors - a healed source or theme
|
|
90
|
+
re-render must find its ref alive to clear the card */}
|
|
91
|
+
{error && <div className="mv-diagram-err"><b>diagram error</b><span>{error}</span><span className="dim">fix the mermaid source - the frame heals live</span></div>}
|
|
92
|
+
<div className="mv-diagram-svg" ref={ref} style={error ? { display: 'none' } : undefined} />
|
|
93
|
+
{title && <figcaption>{title}</figcaption>}
|
|
94
|
+
</figure>
|
|
95
|
+
)
|
|
96
|
+
}
|