@marver-design/marver 0.2.0 → 0.2.2
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/README.md +3 -2
- package/dist/{build-D0GnIR4G.mjs → build-CNoXE13J.mjs} +3 -3
- package/dist/cli.mjs +23 -7
- package/dist/{dev-BK4x3PBr.mjs → dev-Blyy4jOL.mjs} +24 -5
- package/dist/init-3h9pXEzp.mjs +337 -0
- package/dist/manifest-CHmKAAtG.mjs +298 -0
- package/dist/{plugin-DB5t2WUl.mjs → plugin-DiDJA9n-.mjs} +157 -120
- package/dist/{serve-BPNmWeJx.mjs → serve-BvbAbWeK.mjs} +8 -1
- package/package.json +4 -3
- package/src/client/frame-host/bridge.js +8 -2
- package/src/client/frame-host/main.tsx +7 -1
- package/src/client/shell/App.tsx +69 -8
- package/src/client/shell/canvas/FrameNode.tsx +9 -0
- package/src/client/shell/store.ts +58 -8
- package/src/client/shell/styles.css +21 -2
- package/templates/AGENTS-embedded.md +38 -26
- package/templates/AGENTS-studio.md +38 -26
- package/templates/instructions/boards.md +38 -0
- package/templates/instructions/brand.md +60 -0
- package/templates/instructions/components.md +52 -0
- package/templates/instructions/configure.md +44 -0
- package/templates/instructions/craft.md +90 -0
- package/templates/instructions/discover.md +52 -0
- package/templates/instructions/reference/color.md +53 -0
- package/templates/instructions/reference/concepts.md +68 -0
- package/templates/instructions/reference/copy.md +57 -0
- package/templates/instructions/reference/critique.md +53 -0
- package/templates/instructions/reference/delight.md +35 -0
- package/templates/instructions/reference/layout.md +51 -0
- package/templates/instructions/reference/motion.md +66 -0
- package/templates/instructions/reference/operate.md +38 -0
- package/templates/instructions/reference/slop.md +76 -0
- package/templates/instructions/reference/states.md +48 -0
- package/templates/instructions/reference/tune.md +61 -0
- package/templates/instructions/reference/typography.md +45 -0
- package/templates/instructions/review.md +51 -0
- package/templates/instructions/wireframe.md +49 -0
- package/dist/config-DMBEpdEN.mjs +0 -132
- package/dist/init-DcOy1krf.mjs +0 -168
package/README.md
CHANGED
|
@@ -18,11 +18,12 @@ 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
|
+
- **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.
|
|
21
22
|
- Uninstall: delete `design/`, remove the dependency. (If `init` patched your tsconfig `exclude`, revert that one line.)
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
**Next.js**: supported with one caveat - frames render in Vite, outside Next. `next/font` CSS variables are undefined inside frames (give font tokens a fallback chain), `next/image`/`next/link` should be plain `img`/`data-goto` in frames, and Server Components cannot run there. `init` writes the specifics into `design/AGENTS.md` when it detects Next.
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
Implementation contracts (SPEC.md, DECISIONS.md) live in the [GitHub repo](https://github.com/TNEP4/marver).
|
|
26
27
|
|
|
27
28
|
## Shortcuts
|
|
28
29
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { r as ROUTE, t as NAME } from "./cli.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { basename, dirname, join, sep } from "node:path";
|
|
2
|
+
import { a as loadConfig, n as scanFrames, o as detectHost } from "./manifest-CHmKAAtG.mjs";
|
|
3
|
+
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-DiDJA9n-.mjs";
|
|
5
4
|
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { basename, dirname, join, sep } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { build } from "vite";
|
|
8
8
|
import react from "@vitejs/plugin-react";
|
package/dist/cli.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { cac } from "cac";
|
|
3
|
-
import {
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
4
6
|
//#region src/client/const.ts
|
|
5
7
|
/** Client-safe constants. The single source - src/cli/name.ts re-exports for node-side code.
|
|
6
8
|
* Lives under src/client because that is the only source directory shipped in the package. */
|
|
@@ -14,16 +16,30 @@ if (major < 22 || major === 22 && minor < 18) {
|
|
|
14
16
|
console.error(`${NAME} needs Node >= 22.18 (native TypeScript config loading). You have ${process.versions.node}.`);
|
|
15
17
|
process.exit(1);
|
|
16
18
|
}
|
|
19
|
+
const emitWarning = process.emitWarning.bind(process);
|
|
20
|
+
process.emitWarning = ((warning, ...rest) => {
|
|
21
|
+
const opt = rest[0];
|
|
22
|
+
if (((typeof opt === "object" && opt ? opt.code : rest[1]) ?? (warning && typeof warning === "object" ? warning.code : void 0)) === "MODULE_TYPELESS_PACKAGE_JSON") return;
|
|
23
|
+
emitWarning(warning, ...rest);
|
|
24
|
+
});
|
|
25
|
+
/** The real installed version - dist/cli.mjs lives one level under the package root. */
|
|
26
|
+
function version() {
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8")).version;
|
|
29
|
+
} catch {
|
|
30
|
+
return "0.0.0";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
17
33
|
const cli = cac(NAME);
|
|
18
|
-
cli.command("init", "Scaffold design/ in this repo").option("--mode <mode>", "studio | embedded", { default: "studio" }).option("--no-demo", "Skip the demo scene").option("--root <dir>", "Host repo root", { default: "." }).action(async (opts) => {
|
|
19
|
-
const { init } = await import("./init-
|
|
34
|
+
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-3h9pXEzp.mjs");
|
|
20
36
|
init(resolve(opts.root), {
|
|
21
37
|
mode: opts.mode === "embedded" ? "embedded" : "studio",
|
|
22
38
|
demo: opts.demo !== false
|
|
23
39
|
});
|
|
24
40
|
});
|
|
25
41
|
cli.command("dev", "Start the canvas").option("--root <dir>", "Host repo root", { default: "." }).option("--port <port>", "Port (default 5199)").action(async (opts) => {
|
|
26
|
-
const { dev } = await import("./dev-
|
|
42
|
+
const { dev } = await import("./dev-Blyy4jOL.mjs");
|
|
27
43
|
let port;
|
|
28
44
|
if (opts.port !== void 0) {
|
|
29
45
|
const n = Number(opts.port);
|
|
@@ -33,7 +49,7 @@ cli.command("dev", "Start the canvas").option("--root <dir>", "Host repo root",
|
|
|
33
49
|
await dev(resolve(opts.root), port);
|
|
34
50
|
});
|
|
35
51
|
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) => {
|
|
36
|
-
const { buildSite } = await import("./build-
|
|
52
|
+
const { buildSite } = await import("./build-CNoXE13J.mjs");
|
|
37
53
|
try {
|
|
38
54
|
const boards = opts.boards === void 0 ? void 0 : typeof opts.boards === "string" ? opts.boards : "";
|
|
39
55
|
await buildSite(resolve(opts.root), boards);
|
|
@@ -43,7 +59,7 @@ cli.command("build", "Static export → design/.dist").option("--boards <names>"
|
|
|
43
59
|
}
|
|
44
60
|
});
|
|
45
61
|
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) => {
|
|
46
|
-
const { serve } = await import("./serve-
|
|
62
|
+
const { serve } = await import("./serve-BvbAbWeK.mjs");
|
|
47
63
|
let port;
|
|
48
64
|
if (opts.port !== void 0) {
|
|
49
65
|
const n = Number(opts.port);
|
|
@@ -52,7 +68,7 @@ cli.command("serve", "Serve design/.dist (set MARVER_PASSWORD to gate it)").opti
|
|
|
52
68
|
serve(resolve(opts.root), port);
|
|
53
69
|
});
|
|
54
70
|
cli.help();
|
|
55
|
-
cli.version(
|
|
71
|
+
cli.version(version());
|
|
56
72
|
cli.parse();
|
|
57
73
|
//#endregion
|
|
58
74
|
export { PKG as n, ROUTE as r, NAME as t };
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { n as PKG, t as NAME } from "./cli.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-
|
|
2
|
+
import { a as loadConfig, o as detectHost } from "./manifest-CHmKAAtG.mjs";
|
|
3
|
+
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-DiDJA9n-.mjs";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { createServer, searchForWorkspaceRoot } from "vite";
|
|
6
|
+
import { createLogger, createServer, searchForWorkspaceRoot } from "vite";
|
|
7
7
|
import react from "@vitejs/plugin-react";
|
|
8
8
|
//#region src/server/dev.ts
|
|
9
9
|
/** packageDir = the installed marver package root (dist/cli.js lives one level down). */
|
|
@@ -19,7 +19,7 @@ async function dev(root, portFlag) {
|
|
|
19
19
|
if (host.tailwind === 4) {
|
|
20
20
|
const tw = await tailwind4Plugin(root);
|
|
21
21
|
if (tw) plugins.push(...tw);
|
|
22
|
-
else console.warn(
|
|
22
|
+
else console.warn(`[marver] tailwindcss v4 detected but @tailwindcss/vite could not be loaded - utility classes WILL be missing from frames. Fix: npm i -D @tailwindcss/vite (used only by the canvas, never by your app's build).`);
|
|
23
23
|
}
|
|
24
24
|
let css;
|
|
25
25
|
if (host.tailwind === 3) css = await tailwind3Css(root) ?? void 0;
|
|
@@ -29,10 +29,21 @@ async function dev(root, portFlag) {
|
|
|
29
29
|
config,
|
|
30
30
|
detectedThemeCss: host.themeCss
|
|
31
31
|
}));
|
|
32
|
+
const logger = createLogger("info");
|
|
33
|
+
const noise = (msg) => /Sourcemap for .*node_modules.*points to/.test(msg);
|
|
34
|
+
const warnBase = logger.warn.bind(logger);
|
|
35
|
+
const warnOnceBase = logger.warnOnce.bind(logger);
|
|
36
|
+
logger.warn = (msg, opts) => {
|
|
37
|
+
if (!noise(msg)) warnBase(msg, opts);
|
|
38
|
+
};
|
|
39
|
+
logger.warnOnce = (msg, opts) => {
|
|
40
|
+
if (!noise(msg)) warnOnceBase(msg, opts);
|
|
41
|
+
};
|
|
32
42
|
const server = await createServer({
|
|
33
43
|
configFile: false,
|
|
34
44
|
root,
|
|
35
45
|
css,
|
|
46
|
+
customLogger: logger,
|
|
36
47
|
plugins,
|
|
37
48
|
server: {
|
|
38
49
|
port: portFlag ?? config.port,
|
|
@@ -46,7 +57,15 @@ async function dev(root, portFlag) {
|
|
|
46
57
|
"**/design/manifest.json",
|
|
47
58
|
"**/design/boards/**",
|
|
48
59
|
"**/design/.local/**",
|
|
49
|
-
"**/design/.dist/**"
|
|
60
|
+
"**/design/.dist/**",
|
|
61
|
+
"**/.next/**",
|
|
62
|
+
"**/.turbo/**",
|
|
63
|
+
"**/.vercel/**",
|
|
64
|
+
"**/.output/**",
|
|
65
|
+
"**/dist/**",
|
|
66
|
+
"**/build/**",
|
|
67
|
+
"**/out/**",
|
|
68
|
+
"**/coverage/**"
|
|
50
69
|
] }
|
|
51
70
|
},
|
|
52
71
|
resolve: {
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { t as NAME } from "./cli.mjs";
|
|
2
|
+
import { i as DEFAULTS, n as scanFrames, o as detectHost, r as writeManifest } from "./manifest-CHmKAAtG.mjs";
|
|
3
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join, relative } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
//#region src/cli/init.ts
|
|
8
|
+
/** Package root = the nearest ancestor holding templates/ (one hop from dist/, two
|
|
9
|
+
* from src/cli/ - the walk serves both, and tests run init from source). */
|
|
10
|
+
const pkgDir = () => {
|
|
11
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
for (let i = 0; i < 4; i++) {
|
|
13
|
+
if (existsSync(join(dir, "templates"))) return dir;
|
|
14
|
+
dir = dirname(dir);
|
|
15
|
+
}
|
|
16
|
+
return join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
17
|
+
};
|
|
18
|
+
/** Idempotent scaffolder: never overwrites existing files; every host-repo touch prints a diff. */
|
|
19
|
+
function init(root, opts) {
|
|
20
|
+
const host = detectHost(root);
|
|
21
|
+
const design = join(root, "design");
|
|
22
|
+
const templates = join(pkgDir(), "templates");
|
|
23
|
+
const created = [];
|
|
24
|
+
const fileHas = (rel, needle) => {
|
|
25
|
+
try {
|
|
26
|
+
return readFileSync(join(design, rel), "utf8").includes(needle);
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const marverShaped = fileHas("config.ts", "marver") || fileHas("AGENTS.md", "agent contract");
|
|
32
|
+
if (existsSync(design) && !marverShaped && readdirSync(design).some((f) => !f.startsWith("."))) {
|
|
33
|
+
console.error(`
|
|
34
|
+
[${NAME}] design/ already exists in this repo and does not look like a ${NAME} workspace.
|
|
35
|
+
Refusing to merge into it - your files and ${NAME}'s would interleave, and
|
|
36
|
+
"uninstall = delete design/" would stop being safe.
|
|
37
|
+
|
|
38
|
+
Move or rename the existing design/ folder, then re-run \`npx ${NAME} init\`.
|
|
39
|
+
(If you need marver to live in a differently-named folder, say so at
|
|
40
|
+
github.com/TNEP4/marver - a --dir flag is planned.)`);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
const write = (rel, content) => {
|
|
44
|
+
const file = join(design, rel);
|
|
45
|
+
if (existsSync(file)) return;
|
|
46
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
47
|
+
writeFileSync(file, content);
|
|
48
|
+
created.push(`design/${rel}`);
|
|
49
|
+
};
|
|
50
|
+
const writeManaged = (rel, body) => {
|
|
51
|
+
const file = join(design, rel);
|
|
52
|
+
const next = managedFile(body);
|
|
53
|
+
const latest = join(design, ".local", "latest", rel);
|
|
54
|
+
if (!existsSync(file)) return write(rel, next);
|
|
55
|
+
const current = readFileSync(file, "utf8");
|
|
56
|
+
if (current === next) {
|
|
57
|
+
rmSync(latest, { force: true });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (current.startsWith(MANAGED_PREFIX)) {
|
|
61
|
+
const recorded = current.slice(MANAGED_PREFIX.length).split(" ")[0];
|
|
62
|
+
const nl = current.indexOf("\n");
|
|
63
|
+
const currentBody = nl >= 0 ? current.slice(nl + 1) : "";
|
|
64
|
+
if (nl >= 0 && hashBody(currentBody) === recorded) {
|
|
65
|
+
writeFileSync(file, next);
|
|
66
|
+
rmSync(latest, { force: true });
|
|
67
|
+
created.push(`design/${rel} (updated)`);
|
|
68
|
+
} else if (recorded !== hashBody(body)) {
|
|
69
|
+
mkdirSync(dirname(latest), { recursive: true });
|
|
70
|
+
writeFileSync(latest, body);
|
|
71
|
+
if (nl >= 0) {
|
|
72
|
+
const tmp = file + ".tmp";
|
|
73
|
+
writeFileSync(tmp, managedFile(body).split("\n")[0] + "\n" + currentBody);
|
|
74
|
+
renameSync(tmp, file);
|
|
75
|
+
}
|
|
76
|
+
console.warn(` ~ design/${rel}: you customized it and a newer version exists - your edits are untouched. Merge what you want from design/.local/latest/${rel}`);
|
|
77
|
+
}
|
|
78
|
+
} else if (current.startsWith(LEGACY_PREFIX)) {
|
|
79
|
+
writeFileSync(file, next);
|
|
80
|
+
rmSync(latest, { force: true });
|
|
81
|
+
created.push(`design/${rel} (updated)`);
|
|
82
|
+
} else if (current !== body) {
|
|
83
|
+
rmSync(latest, { force: true });
|
|
84
|
+
console.warn(` note: design/${rel} exists without a marver marker - left untouched. If you did not author it, delete it and re-run init to restore the managed version.`);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
write("config.ts", configTemplate(opts.mode));
|
|
88
|
+
if (host.themeCss) {
|
|
89
|
+
const relCss = relative(design, join(root, host.themeCss)).split("\\").join("/");
|
|
90
|
+
write("theme.css", themeWrapper(relCss, host.tailwind === 4));
|
|
91
|
+
} else console.warn(`[${NAME}] no theme CSS detected - create design/theme.css importing your app's stylesheet when you have one (or set \`theme\` in design/config.ts).`);
|
|
92
|
+
write("providers.tsx", providersTemplate(host.router, host.toaster, host.routerPkg));
|
|
93
|
+
writeManaged("AGENTS.md", readFileSync(join(templates, `AGENTS-${opts.mode}.md`), "utf8").replaceAll("{{UI_GUIDANCE}}", uiGuidance(host, noApp(host))).replace(/\{\{NEXT_NOTES\}\}\n?/, host.router === "next" ? NEXT_NOTES : ""));
|
|
94
|
+
const agentsNow = readFileSync(join(design, "AGENTS.md"), "utf8");
|
|
95
|
+
if (!agentsNow.startsWith(MANAGED_PREFIX) && !agentsNow.startsWith(LEGACY_PREFIX) && agentsNow.includes("# Design canvas - agent contract") && !agentsNow.includes("## The method (binding)")) console.warn(` note: design/AGENTS.md predates managed regeneration - if you never edited it, delete it and re-run init to get the current contract (incl. the design/instructions routing).`);
|
|
96
|
+
const instrRoot = join(templates, "instructions");
|
|
97
|
+
for (const e of readdirSync(instrRoot, { withFileTypes: true })) if (e.isDirectory()) {
|
|
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
|
+
} else if (e.name.endsWith(".md")) writeManaged(`instructions/${e.name}`, readFileSync(join(instrRoot, e.name), "utf8"));
|
|
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;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
if (noApp(host)) {
|
|
110
|
+
if (!existsSync(setupPath)) write("instructions/setup.md", SETUP_MD);
|
|
111
|
+
} else if (existsSync(setupPath) && ourSetup()) {
|
|
112
|
+
rmSync(setupPath);
|
|
113
|
+
console.log(` - design/instructions/setup.md removed (app detected - setup complete)`);
|
|
114
|
+
}
|
|
115
|
+
write("tsconfig.json", existsSync(join(root, "tsconfig.json")) ? readFileSync(join(templates, "design-tsconfig.json"), "utf8") : STANDALONE_TSCONFIG);
|
|
116
|
+
write(".gitignore", ".local/\n.dist/\n");
|
|
117
|
+
write("scenes/_layout.tsx", readFileSync(join(templates, "root-layout.tsx"), "utf8"));
|
|
118
|
+
if (!existsSync(join(design, "boards"))) {
|
|
119
|
+
mkdirSync(join(design, "boards"), { recursive: true });
|
|
120
|
+
writeFileSync(join(design, "boards", ".gitkeep"), "");
|
|
121
|
+
created.push("design/boards/");
|
|
122
|
+
}
|
|
123
|
+
if (opts.demo && !existsSync(join(design, "scenes", "demo"))) {
|
|
124
|
+
cpSync(join(templates, "demo"), join(design, "scenes", "demo"), { recursive: true });
|
|
125
|
+
created.push("design/scenes/demo/ (3 frames)");
|
|
126
|
+
}
|
|
127
|
+
if (host.tsconfigSweepsDesign) patchTsconfigExclude(root);
|
|
128
|
+
writeManifest(root, scanFrames(root));
|
|
129
|
+
console.log(`\n${NAME} initialized (${opts.mode} mode). Created:`);
|
|
130
|
+
for (const f of created) console.log(` + ${f}`);
|
|
131
|
+
if (host.router === "next") console.log(`\n note: Next.js support is partial - frames render outside Next, so next/font, next/image and Server Components do not exist inside them (details in design/AGENTS.md).`);
|
|
132
|
+
if (noApp(host)) console.warn(`
|
|
133
|
+
┌─ NO APP DETECTED ─────────────────────────────────────────────────────┐
|
|
134
|
+
│ No framework, no theme CSS, no component library. ${NAME} builds │
|
|
135
|
+
│ frames from YOUR components - with none, designs get thrown away. │
|
|
136
|
+
│ │
|
|
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. │
|
|
140
|
+
└───────────────────────────────────────────────────────────────────────┘`);
|
|
141
|
+
console.log(`\n commit design/ - only .local/ is ignored`);
|
|
142
|
+
console.log(` uninstall = delete design/, remove the ${NAME} dependency${host.tsconfigSweepsDesign ? ", revert the \"design\" line in tsconfig exclude" : ""}`);
|
|
143
|
+
console.log(`\n next: npx ${NAME} dev (canvas on http://localhost:${DEFAULTS.port} by default)\n`);
|
|
144
|
+
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`);
|
|
145
|
+
}
|
|
146
|
+
const MANAGED_PREFIX = "<!-- marver:managed ";
|
|
147
|
+
const LEGACY_PREFIX = "<!-- generated by marver init";
|
|
148
|
+
const hashBody = (s) => createHash("sha256").update(s).digest("hex");
|
|
149
|
+
/** The marker carries a hash of the generated body: edits are DETECTED, not assumed.
|
|
150
|
+
* Edit freely - init preserves edits and stages upstream updates for merging.
|
|
151
|
+
* Deleting the marker line detaches the file from updates entirely. */
|
|
152
|
+
const managedFile = (body) => `${MANAGED_PREFIX}${hashBody(body)} - edit freely: init preserves your edits and stages upstream updates at design/.local/latest/ for you to merge. Delete this line to detach this file from updates entirely. -->\n${body}`;
|
|
153
|
+
/** No framework, no theme, no component alias = nothing to build frames FROM. */
|
|
154
|
+
const noApp = (host) => !host.router && !host.tailwind && !host.shadcn && !host.themeCss;
|
|
155
|
+
/** The UI line of AGENTS.md, matched to what detection actually found (friction log #1).
|
|
156
|
+
* The STOP branch fires only on the same condition that creates SETUP.md - an app
|
|
157
|
+
* without Tailwind (plain React + CSS) gets guidance, never a dead pointer. */
|
|
158
|
+
function uiGuidance(host, isNoApp) {
|
|
159
|
+
if (isNoApp) return `STOP - this repo has no app yet. Read design/instructions/setup.md before designing anything.`;
|
|
160
|
+
if (host.shadcn) return `Use the app's UI: import from ${host.shadcn.uiAlias}; style with the app's Tailwind classes.`;
|
|
161
|
+
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/.`;
|
|
162
|
+
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/.`;
|
|
163
|
+
}
|
|
164
|
+
const SETUP_MD = `# Setup required - this repo has no app yet
|
|
165
|
+
|
|
166
|
+
> This file exists because \`${NAME} init\` ran in a repo with no framework, no theme
|
|
167
|
+
> CSS, and no component library. It disappears automatically: set up the app, re-run
|
|
168
|
+
> \`npx ${NAME} init\`, and init deletes this file and regenerates AGENTS.md against
|
|
169
|
+
> the real stack. While this file exists, DO NOT design.
|
|
170
|
+
|
|
171
|
+
${NAME} builds frames from YOUR components and YOUR theme. With none, frames become
|
|
172
|
+
hand-rolled CSS that shares nothing with the future app and cannot be promoted into
|
|
173
|
+
it later - work that gets thrown away.
|
|
174
|
+
|
|
175
|
+
## Do this first
|
|
176
|
+
|
|
177
|
+
For a web app or marketing site, the blessed stack:
|
|
178
|
+
|
|
179
|
+
\`\`\`bash
|
|
180
|
+
npx create-next-app@latest . --ts --tailwind --app --src-dir
|
|
181
|
+
npx shadcn@latest init
|
|
182
|
+
\`\`\`
|
|
183
|
+
|
|
184
|
+
Any React + CSS setup works; the point is that components and a theme EXIST.
|
|
185
|
+
|
|
186
|
+
## Then
|
|
187
|
+
|
|
188
|
+
\`\`\`bash
|
|
189
|
+
npx ${NAME} init
|
|
190
|
+
\`\`\`
|
|
191
|
+
|
|
192
|
+
init is idempotent: it fills in what it now detects (theme wrapper, providers, a
|
|
193
|
+
shadcn-aware AGENTS.md), deletes this file, and you design from real parts.
|
|
194
|
+
`;
|
|
195
|
+
/** Next.js frames render OUTSIDE Next - say concretely what that means (friction log #10/#11). */
|
|
196
|
+
const NEXT_NOTES = `- Next.js caveats (frames render in Vite, outside Next):
|
|
197
|
+
next/font does not exist here - CSS variables it injects (e.g. --font-geist-sans) are
|
|
198
|
+
undefined in frames, so give every font token a real fallback chain in the app's CSS:
|
|
199
|
+
--font-sans: var(--font-geist-sans, ui-sans-serif, system-ui, sans-serif).
|
|
200
|
+
next/image and next/link render as plain img/a via shims at best - prefer <img> and
|
|
201
|
+
data-goto in frames. Server Components and server actions cannot run: frames are
|
|
202
|
+
client components importing client components.
|
|
203
|
+
`;
|
|
204
|
+
const STANDALONE_TSCONFIG = `{
|
|
205
|
+
"compilerOptions": {
|
|
206
|
+
"target": "ES2022",
|
|
207
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
208
|
+
"module": "ESNext",
|
|
209
|
+
"moduleResolution": "bundler",
|
|
210
|
+
"jsx": "react-jsx",
|
|
211
|
+
"strict": true,
|
|
212
|
+
"skipLibCheck": true,
|
|
213
|
+
"noEmit": true
|
|
214
|
+
},
|
|
215
|
+
"include": ["."]
|
|
216
|
+
}
|
|
217
|
+
`;
|
|
218
|
+
function patchTsconfigExclude(root) {
|
|
219
|
+
const file = join(root, "tsconfig.json");
|
|
220
|
+
if (!existsSync(file)) return;
|
|
221
|
+
const raw = readFileSync(file, "utf8");
|
|
222
|
+
try {
|
|
223
|
+
let next;
|
|
224
|
+
if (/"exclude"\s*:/.test(raw)) next = raw.replace(/("exclude"\s*:\s*\[)/, "$1\"design\", ");
|
|
225
|
+
else {
|
|
226
|
+
const at = firstJsonBrace(raw);
|
|
227
|
+
if (at < 0) throw new Error("no object brace found");
|
|
228
|
+
next = raw.slice(0, at + 1) + "\n \"exclude\": [\"design\"]," + raw.slice(at + 1);
|
|
229
|
+
}
|
|
230
|
+
if (next === raw) throw new Error("no anchor matched");
|
|
231
|
+
writeFileSync(file, next);
|
|
232
|
+
console.log(`\n patched tsconfig.json (the only host file touched):`);
|
|
233
|
+
console.log(` + "design" added to "exclude" (revert this line to fully uninstall)`);
|
|
234
|
+
} catch {
|
|
235
|
+
console.warn(` could not patch tsconfig.json - add "design" to its "exclude" yourself.`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/** Index of the first `{` outside //, /* *\/ comments and strings (tsconfig is JSONC). */
|
|
239
|
+
function firstJsonBrace(src) {
|
|
240
|
+
let inLine = false, inBlock = false, inStr = false;
|
|
241
|
+
for (let i = 0; i < src.length; i++) {
|
|
242
|
+
const c = src[i], n = src[i + 1];
|
|
243
|
+
if (inLine) {
|
|
244
|
+
if (c === "\n") inLine = false;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (inBlock) {
|
|
248
|
+
if (c === "*" && n === "/") {
|
|
249
|
+
inBlock = false;
|
|
250
|
+
i++;
|
|
251
|
+
}
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (inStr) {
|
|
255
|
+
if (c === "\\") i++;
|
|
256
|
+
else if (c === "\"") inStr = false;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (c === "/" && n === "/") {
|
|
260
|
+
inLine = true;
|
|
261
|
+
i++;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (c === "/" && n === "*") {
|
|
265
|
+
inBlock = true;
|
|
266
|
+
i++;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (c === "\"") {
|
|
270
|
+
inStr = true;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (c === "{") return i;
|
|
274
|
+
}
|
|
275
|
+
return -1;
|
|
276
|
+
}
|
|
277
|
+
const configTemplate = (mode) => `// ${NAME} config - OPTIONAL. Delete this file and everything still works on defaults.
|
|
278
|
+
// Theme lives in design/theme.css (it imports your app's real stylesheet) - not here.
|
|
279
|
+
// Sharp edges (native Node TS import): erasable syntax only (no enums/namespaces),
|
|
280
|
+
// relative imports need extensions, tsconfig paths are ignored here.
|
|
281
|
+
export default {
|
|
282
|
+
mode: ${JSON.stringify(mode)},
|
|
283
|
+
// Device widths for frames and the Devices view. Rename, retune, or uncomment tv.
|
|
284
|
+
viewports: {
|
|
285
|
+
mobile: { width: 390, height: 844 },
|
|
286
|
+
tablet: { width: 768, height: 1024 },
|
|
287
|
+
laptop: { width: 1280, height: 800 },
|
|
288
|
+
monitor: { width: 1920, height: 1080 },
|
|
289
|
+
// tv: { width: 3840, height: 2160 },
|
|
290
|
+
},
|
|
291
|
+
themes: ["light", "dark"],
|
|
292
|
+
port: ${DEFAULTS.port},
|
|
293
|
+
// Canvas zoom feel: 1 = default, 1.2 = 20% faster, 0.8 = 20% slower.
|
|
294
|
+
// zoomSpeed: 1,
|
|
295
|
+
// Publishing (\`${NAME} build\` + \`${NAME} serve\`): gate identity + branding footer.
|
|
296
|
+
// name/logo default to the host package.json name and design/logo.svg (then public/).
|
|
297
|
+
// branding is the small "Powered by Marver.design" line under the gate. Marver is
|
|
298
|
+
// free, and that line is how it spreads - we'd love it if you leave it on, but it
|
|
299
|
+
// is yours to remove, no strings: share: { branding: false }.
|
|
300
|
+
// share: { name: "My App", logo: "design/logo.svg", branding: true },
|
|
301
|
+
}
|
|
302
|
+
`;
|
|
303
|
+
const themeWrapper = (relCss, v4) => `/* ${NAME} theme wrapper - imports the app's real theme; the app's own build never sees design/. */
|
|
304
|
+
@import "${relCss}";
|
|
305
|
+
${v4 ? `@source "./";\n` : ""}`;
|
|
306
|
+
function providersTemplate(router, toaster, routerPkg = "react-router-dom") {
|
|
307
|
+
const imports = [`import type { ReactNode } from 'react'`];
|
|
308
|
+
let open = "", close = "";
|
|
309
|
+
if (router === "react-router") {
|
|
310
|
+
imports.push(`import { MemoryRouter } from '${routerPkg}'`);
|
|
311
|
+
open += "<MemoryRouter>";
|
|
312
|
+
close = "</MemoryRouter>" + close;
|
|
313
|
+
}
|
|
314
|
+
let toasterEl = "";
|
|
315
|
+
if (toaster === "sonner") {
|
|
316
|
+
imports.push(`import { Toaster } from 'sonner'`);
|
|
317
|
+
toasterEl = "<Toaster />";
|
|
318
|
+
}
|
|
319
|
+
if (toaster === "react-hot-toast") {
|
|
320
|
+
imports.push(`import { Toaster } from 'react-hot-toast'`);
|
|
321
|
+
toasterEl = "<Toaster />";
|
|
322
|
+
}
|
|
323
|
+
return `// Mock contexts wrapped around every frame. Scaffolded by ${NAME} init from what it detected - yours to edit.
|
|
324
|
+
${imports.join("\n")}
|
|
325
|
+
|
|
326
|
+
export default function Providers({ children }: { children: ReactNode }) {
|
|
327
|
+
return (
|
|
328
|
+
${open || "<>"}
|
|
329
|
+
{children}
|
|
330
|
+
${toasterEl}
|
|
331
|
+
${close || "</>"}
|
|
332
|
+
)
|
|
333
|
+
}
|
|
334
|
+
`;
|
|
335
|
+
}
|
|
336
|
+
//#endregion
|
|
337
|
+
export { init };
|