@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 +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-ClhCgn4v.mjs → init-DolP_Ld4.mjs} +183 -28
- 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 +17 -2
- package/templates/AGENTS-studio.md +17 -2
- package/templates/instructions/boards.md +6 -0
- package/templates/instructions/brand.md +3 -1
- package/templates/instructions/configure.md +5 -2
- package/templates/instructions/craft.md +49 -0
- package/templates/instructions/discover.md +4 -3
- package/templates/instructions/iterate.md +50 -0
- package/templates/instructions/shape.md +171 -0
- package/templates/instructions/welcome.md +137 -0
- 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";
|
|
@@ -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
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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 (!
|
|
111
|
-
|
|
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.
|
|
138
|
-
│
|
|
139
|
-
│
|
|
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.
|
|
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 `
|
|
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
|
|
187
|
-
|
|
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
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
204
|
-
|
|
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
|
-
|
|
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
|
|
214
|
-
|
|
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
|
|
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 }
|