@marver-design/marver 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +4 -0
  3. package/README.md +38 -0
  4. package/dist/build-D0GnIR4G.mjs +244 -0
  5. package/dist/cli.mjs +58 -0
  6. package/dist/config-DMBEpdEN.mjs +132 -0
  7. package/dist/dev-BK4x3PBr.mjs +76 -0
  8. package/dist/init-DcOy1krf.mjs +168 -0
  9. package/dist/plugin-DB5t2WUl.mjs +437 -0
  10. package/dist/serve-BPNmWeJx.mjs +200 -0
  11. package/package.json +59 -0
  12. package/src/client/const.ts +5 -0
  13. package/src/client/frame-host/bridge.js +37 -0
  14. package/src/client/frame-host/index.html +11 -0
  15. package/src/client/frame-host/main.tsx +73 -0
  16. package/src/client/frame-host/registry.ts +40 -0
  17. package/src/client/runtime/index.ts +8 -0
  18. package/src/client/shell/App.tsx +633 -0
  19. package/src/client/shell/Play.tsx +399 -0
  20. package/src/client/shell/Tip.tsx +43 -0
  21. package/src/client/shell/canvas/Canvas.tsx +285 -0
  22. package/src/client/shell/canvas/FrameNode.tsx +194 -0
  23. package/src/client/shell/favicon/android-chrome-192x192-interactive.png +0 -0
  24. package/src/client/shell/favicon/android-chrome-192x192.png +0 -0
  25. package/src/client/shell/favicon/android-chrome-512x512-interactive.png +0 -0
  26. package/src/client/shell/favicon/android-chrome-512x512.png +0 -0
  27. package/src/client/shell/favicon/apple-touch-icon-interactive.png +0 -0
  28. package/src/client/shell/favicon/apple-touch-icon.png +0 -0
  29. package/src/client/shell/favicon/favicon-16x16-interactive.png +0 -0
  30. package/src/client/shell/favicon/favicon-16x16.png +0 -0
  31. package/src/client/shell/favicon/favicon-32x32-interactive.png +0 -0
  32. package/src/client/shell/favicon/favicon-32x32.png +0 -0
  33. package/src/client/shell/favicon/favicon.ico +0 -0
  34. package/src/client/shell/favicon/site.webmanifest +11 -0
  35. package/src/client/shell/hash.ts +67 -0
  36. package/src/client/shell/icons.tsx +53 -0
  37. package/src/client/shell/index.html +17 -0
  38. package/src/client/shell/main.tsx +9 -0
  39. package/src/client/shell/store.ts +540 -0
  40. package/src/client/shell/styles.css +508 -0
  41. package/src/client/shell/tidy.ts +24 -0
  42. package/src/client/stage/index.html +11 -0
  43. package/src/client/stage/main.tsx +160 -0
  44. package/templates/AGENTS-embedded.md +76 -0
  45. package/templates/AGENTS-studio.md +76 -0
  46. package/templates/demo/_fixtures.ts +9 -0
  47. package/templates/demo/dark.tsx +17 -0
  48. package/templates/demo/form.tsx +51 -0
  49. package/templates/demo/welcome.tsx +21 -0
  50. package/templates/design-tsconfig.json +8 -0
  51. package/templates/root-layout.tsx +7 -0
@@ -0,0 +1,132 @@
1
+ import { join } from "node:path";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { pathToFileURL } from "node:url";
4
+ //#region src/server/detect.ts
5
+ function readJson(file) {
6
+ try {
7
+ return JSON.parse(stripJsonComments(readFileSync(file, "utf8")));
8
+ } catch {
9
+ return null;
10
+ }
11
+ }
12
+ function stripJsonComments(s) {
13
+ return s.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1").replace(/,\s*([}\]])/g, "$1");
14
+ }
15
+ function detectHost(root) {
16
+ const pkg = readJson(join(root, "package.json")) ?? {};
17
+ const deps = {
18
+ ...pkg.dependencies,
19
+ ...pkg.devDependencies
20
+ };
21
+ const twRange = deps["tailwindcss"];
22
+ const tailwind = !twRange ? null : /(^|[^\d])4\./.test(twRange) || twRange.startsWith("^4") || twRange.startsWith("~4") ? 4 : 3;
23
+ const router = deps["next"] ? "next" : deps["react-router"] || deps["react-router-dom"] ? "react-router" : null;
24
+ const routerPkg = deps["react-router-dom"] ? "react-router-dom" : "react-router";
25
+ const toaster = deps["sonner"] ? "sonner" : deps["react-hot-toast"] ? "react-hot-toast" : null;
26
+ let shadcn = null;
27
+ const comp = readJson(join(root, "components.json"));
28
+ if (comp?.tailwind?.css) shadcn = {
29
+ themeCss: comp.tailwind.css,
30
+ uiAlias: comp.aliases?.ui ?? "@/components/ui"
31
+ };
32
+ const themeCss = shadcn?.themeCss && existsSync(join(root, shadcn.themeCss)) ? shadcn.themeCss : firstExisting(root, [
33
+ "src/index.css",
34
+ "src/styles/theme.css",
35
+ "src/styles/globals.css",
36
+ "src/app/globals.css",
37
+ "app/globals.css",
38
+ "src/globals.css",
39
+ "src/style.css",
40
+ "styles/globals.css"
41
+ ]);
42
+ const ts = readJson(join(root, "tsconfig.json"));
43
+ const include = ts?.include;
44
+ const exclude = ts?.exclude ?? [];
45
+ const sweeps = ts != null && !exclude.some((e) => e === "design" || e.startsWith("design/")) && (!include || include.some((i) => i === "." || i === "**/*" || i.startsWith("design")));
46
+ return {
47
+ tailwind,
48
+ router,
49
+ routerPkg,
50
+ toaster,
51
+ shadcn,
52
+ themeCss,
53
+ tsconfigSweepsDesign: !!sweeps
54
+ };
55
+ }
56
+ function firstExisting(root, candidates) {
57
+ for (const c of candidates) if (existsSync(join(root, c))) return c;
58
+ return null;
59
+ }
60
+ //#endregion
61
+ //#region src/server/config.ts
62
+ const DEFAULTS = {
63
+ mode: "studio",
64
+ theme: null,
65
+ viewports: {
66
+ mobile: {
67
+ width: 390,
68
+ height: 844
69
+ },
70
+ tablet: {
71
+ width: 768,
72
+ height: 1024
73
+ },
74
+ laptop: {
75
+ width: 1280,
76
+ height: 800
77
+ },
78
+ monitor: {
79
+ width: 1920,
80
+ height: 1080
81
+ }
82
+ },
83
+ themes: ["light", "dark"],
84
+ port: 5199,
85
+ zoomSpeed: 1,
86
+ share: { branding: true }
87
+ };
88
+ /** Load design/config.ts via native TS import (Node >= 22.18). Missing or broken fields fall back to defaults. */
89
+ async function loadConfig(root) {
90
+ const file = join(root, "design", "config.ts");
91
+ if (!existsSync(file)) return { ...DEFAULTS };
92
+ try {
93
+ const user = (await import(`${pathToFileURL(file).href}?t=${Date.now()}`)).default ?? {};
94
+ return {
95
+ ...DEFAULTS,
96
+ ...user,
97
+ viewports: validViewports(user.viewports) ?? DEFAULTS.viewports,
98
+ themes: Array.isArray(user.themes) && user.themes.length ? user.themes.map(String) : DEFAULTS.themes,
99
+ port: validPort(user.port) ?? DEFAULTS.port,
100
+ zoomSpeed: validZoom(user.zoomSpeed) ?? DEFAULTS.zoomSpeed,
101
+ share: {
102
+ branding: user.share?.branding !== false,
103
+ name: typeof user.share?.name === "string" ? user.share.name : void 0,
104
+ logo: typeof user.share?.logo === "string" ? user.share.logo : void 0
105
+ }
106
+ };
107
+ } catch (err) {
108
+ console.error(`[marver] design/config.ts failed to load, using defaults:\n ${err.message}`);
109
+ return { ...DEFAULTS };
110
+ }
111
+ }
112
+ const validDim = (n) => typeof n === "number" && Number.isFinite(n) && n >= 1 && n <= 2e4;
113
+ function validPort(n) {
114
+ return typeof n === "number" && Number.isInteger(n) && n > 0 && n < 65536 ? n : null;
115
+ }
116
+ function validZoom(n) {
117
+ return typeof n === "number" && Number.isFinite(n) && n >= .1 && n <= 10 ? n : null;
118
+ }
119
+ function validViewports(v) {
120
+ if (!v || typeof v !== "object") return null;
121
+ const out = {};
122
+ for (const [k, val] of Object.entries(v)) {
123
+ const vp = val;
124
+ if (validDim(vp?.width) && validDim(vp?.height)) out[k] = {
125
+ width: Math.round(vp.width),
126
+ height: Math.round(vp.height)
127
+ };
128
+ }
129
+ return Object.keys(out).length ? out : null;
130
+ }
131
+ //#endregion
132
+ export { loadConfig as n, detectHost as r, DEFAULTS as t };
@@ -0,0 +1,76 @@
1
+ import { n as PKG, t as NAME } from "./cli.mjs";
2
+ import { n as loadConfig, r as detectHost } from "./config-DMBEpdEN.mjs";
3
+ import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-DB5t2WUl.mjs";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { createServer, searchForWorkspaceRoot } from "vite";
7
+ import react from "@vitejs/plugin-react";
8
+ //#region src/server/dev.ts
9
+ /** packageDir = the installed marver package root (dist/cli.js lives one level down). */
10
+ function packageDir() {
11
+ return join(dirname(fileURLToPath(import.meta.url)), "..");
12
+ }
13
+ async function dev(root, portFlag) {
14
+ const config = await loadConfig(root);
15
+ const host = detectHost(root);
16
+ const pkgDir = packageDir();
17
+ const clientDir = join(pkgDir, "src", "client");
18
+ const plugins = [react()];
19
+ if (host.tailwind === 4) {
20
+ const tw = await tailwind4Plugin(root);
21
+ if (tw) plugins.push(...tw);
22
+ else console.warn("[marver] tailwindcss v4 detected but @tailwindcss/vite not found in the host - theme classes may be missing.");
23
+ }
24
+ let css;
25
+ if (host.tailwind === 3) css = await tailwind3Css(root) ?? void 0;
26
+ plugins.push(marverPlugin({
27
+ root,
28
+ clientDir,
29
+ config,
30
+ detectedThemeCss: host.themeCss
31
+ }));
32
+ const server = await createServer({
33
+ configFile: false,
34
+ root,
35
+ css,
36
+ plugins,
37
+ server: {
38
+ port: portFlag ?? config.port,
39
+ strictPort: false,
40
+ fs: { allow: [.../* @__PURE__ */ new Set([
41
+ root,
42
+ pkgDir,
43
+ searchForWorkspaceRoot(root)
44
+ ])] },
45
+ watch: { ignored: [
46
+ "**/design/manifest.json",
47
+ "**/design/boards/**",
48
+ "**/design/.local/**",
49
+ "**/design/.dist/**"
50
+ ] }
51
+ },
52
+ resolve: {
53
+ dedupe: ["react", "react-dom"],
54
+ tsconfigPaths: true
55
+ },
56
+ optimizeDeps: {
57
+ exclude: [PKG],
58
+ include: [
59
+ "react",
60
+ "react-dom",
61
+ "react-dom/client",
62
+ "react/jsx-runtime",
63
+ "react/jsx-dev-runtime"
64
+ ],
65
+ entries: [join(clientDir, "frame-host", "index.html"), "design/**/*.{tsx,jsx}"]
66
+ },
67
+ logLevel: "info"
68
+ });
69
+ await server.listen();
70
+ const addr = server.httpServer?.address();
71
+ const port = typeof addr === "object" && addr ? addr.port : config.port;
72
+ console.log(`\n ${NAME} canvas → http://localhost:${port}/\n`);
73
+ return server;
74
+ }
75
+ //#endregion
76
+ export { dev };
@@ -0,0 +1,168 @@
1
+ import { t as NAME } from "./cli.mjs";
2
+ import { r as detectHost, t as DEFAULTS } from "./config-DMBEpdEN.mjs";
3
+ import { dirname, join, relative } from "node:path";
4
+ import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
+ import { fileURLToPath } from "node:url";
6
+ //#region src/cli/init.ts
7
+ const pkgDir = () => join(dirname(fileURLToPath(import.meta.url)), "..");
8
+ /** Idempotent scaffolder: never overwrites existing files; every host-repo touch prints a diff. */
9
+ function init(root, opts) {
10
+ const host = detectHost(root);
11
+ const design = join(root, "design");
12
+ const templates = join(pkgDir(), "templates");
13
+ const created = [];
14
+ const write = (rel, content) => {
15
+ const file = join(design, rel);
16
+ if (existsSync(file)) return;
17
+ mkdirSync(dirname(file), { recursive: true });
18
+ writeFileSync(file, content);
19
+ created.push(`design/${rel}`);
20
+ };
21
+ write("config.ts", configTemplate(opts.mode));
22
+ if (host.themeCss) {
23
+ const relCss = relative(design, join(root, host.themeCss)).split("\\").join("/");
24
+ write("theme.css", themeWrapper(relCss, host.tailwind === 4));
25
+ } else console.warn(`[${NAME}] no theme CSS detected - set \`theme\` in design/config.ts when you have one.`);
26
+ write("providers.tsx", providersTemplate(host.router, host.toaster, host.routerPkg));
27
+ write("AGENTS.md", readFileSync(join(templates, `AGENTS-${opts.mode}.md`), "utf8").replaceAll("{{UI_ALIAS}}", host.shadcn?.uiAlias ?? "@/components/ui"));
28
+ write("tsconfig.json", readFileSync(join(templates, "design-tsconfig.json"), "utf8"));
29
+ write(".gitignore", ".local/\n.dist/\n");
30
+ write("scenes/_layout.tsx", readFileSync(join(templates, "root-layout.tsx"), "utf8"));
31
+ if (!existsSync(join(design, "boards"))) {
32
+ mkdirSync(join(design, "boards"), { recursive: true });
33
+ writeFileSync(join(design, "boards", ".gitkeep"), "");
34
+ created.push("design/boards/");
35
+ }
36
+ if (opts.demo && !existsSync(join(design, "scenes", "demo"))) {
37
+ cpSync(join(templates, "demo"), join(design, "scenes", "demo"), { recursive: true });
38
+ created.push("design/scenes/demo/ (3 frames)");
39
+ }
40
+ if (host.tsconfigSweepsDesign) patchTsconfigExclude(root);
41
+ console.log(`\n${NAME} initialized (${opts.mode} mode). Created:`);
42
+ for (const f of created) console.log(` + ${f}`);
43
+ if (host.router === "next") console.log(`\n note: Next.js support is partial until M3 - HTML frames and next-free components work today.`);
44
+ console.log(`\n commit design/ - only .local/ is ignored`);
45
+ console.log(` uninstall = delete design/, remove the ${NAME} dependency${host.tsconfigSweepsDesign ? ", revert the \"design\" line in tsconfig exclude" : ""}`);
46
+ console.log(`\n next: npx ${NAME} dev (canvas on http://localhost:${DEFAULTS.port} by default)\n`);
47
+ console.log(` then, to your agent: "Read design/AGENTS.md. Build an onboarding scene - welcome, form, done - mobile-first, using our components."\n`);
48
+ }
49
+ function patchTsconfigExclude(root) {
50
+ const file = join(root, "tsconfig.json");
51
+ if (!existsSync(file)) return;
52
+ const raw = readFileSync(file, "utf8");
53
+ try {
54
+ let next;
55
+ if (/"exclude"\s*:/.test(raw)) next = raw.replace(/("exclude"\s*:\s*\[)/, "$1\"design\", ");
56
+ else {
57
+ const at = firstJsonBrace(raw);
58
+ if (at < 0) throw new Error("no object brace found");
59
+ next = raw.slice(0, at + 1) + "\n \"exclude\": [\"design\"]," + raw.slice(at + 1);
60
+ }
61
+ if (next === raw) throw new Error("no anchor matched");
62
+ writeFileSync(file, next);
63
+ console.log(`\n patched tsconfig.json (the only host file touched):`);
64
+ console.log(` + "design" added to "exclude" (revert this line to fully uninstall)`);
65
+ } catch {
66
+ console.warn(` could not patch tsconfig.json - add "design" to its "exclude" yourself.`);
67
+ }
68
+ }
69
+ /** Index of the first `{` outside //, /* *\/ comments and strings (tsconfig is JSONC). */
70
+ function firstJsonBrace(src) {
71
+ let inLine = false, inBlock = false, inStr = false;
72
+ for (let i = 0; i < src.length; i++) {
73
+ const c = src[i], n = src[i + 1];
74
+ if (inLine) {
75
+ if (c === "\n") inLine = false;
76
+ continue;
77
+ }
78
+ if (inBlock) {
79
+ if (c === "*" && n === "/") {
80
+ inBlock = false;
81
+ i++;
82
+ }
83
+ continue;
84
+ }
85
+ if (inStr) {
86
+ if (c === "\\") i++;
87
+ else if (c === "\"") inStr = false;
88
+ continue;
89
+ }
90
+ if (c === "/" && n === "/") {
91
+ inLine = true;
92
+ i++;
93
+ continue;
94
+ }
95
+ if (c === "/" && n === "*") {
96
+ inBlock = true;
97
+ i++;
98
+ continue;
99
+ }
100
+ if (c === "\"") {
101
+ inStr = true;
102
+ continue;
103
+ }
104
+ if (c === "{") return i;
105
+ }
106
+ return -1;
107
+ }
108
+ const configTemplate = (mode) => `// ${NAME} config - OPTIONAL. Delete this file and everything still works on defaults.
109
+ // Theme lives in design/theme.css (it imports your app's real stylesheet) - not here.
110
+ // Sharp edges (native Node TS import): erasable syntax only (no enums/namespaces),
111
+ // relative imports need extensions, tsconfig paths are ignored here.
112
+ export default {
113
+ mode: ${JSON.stringify(mode)},
114
+ // Device widths for frames and the Devices view. Rename, retune, or uncomment tv.
115
+ viewports: {
116
+ mobile: { width: 390, height: 844 },
117
+ tablet: { width: 768, height: 1024 },
118
+ laptop: { width: 1280, height: 800 },
119
+ monitor: { width: 1920, height: 1080 },
120
+ // tv: { width: 3840, height: 2160 },
121
+ },
122
+ themes: ["light", "dark"],
123
+ port: ${DEFAULTS.port},
124
+ // Canvas zoom feel: 1 = default, 1.2 = 20% faster, 0.8 = 20% slower.
125
+ // zoomSpeed: 1,
126
+ // Publishing (\`${NAME} build\` + \`${NAME} serve\`): gate identity + branding footer.
127
+ // name/logo default to the host package.json name and design/logo.svg (then public/).
128
+ // branding is the small "Powered by Marver.design" line under the gate. Marver is
129
+ // free, and that line is how it spreads - we'd love it if you leave it on, but it
130
+ // is yours to remove, no strings: share: { branding: false }.
131
+ // share: { name: "My App", logo: "design/logo.svg", branding: true },
132
+ }
133
+ `;
134
+ const themeWrapper = (relCss, v4) => `/* ${NAME} theme wrapper - imports the app's real theme; the app's own build never sees design/. */
135
+ @import "${relCss}";
136
+ ${v4 ? `@source "./";\n` : ""}`;
137
+ function providersTemplate(router, toaster, routerPkg = "react-router-dom") {
138
+ const imports = [`import type { ReactNode } from 'react'`];
139
+ let open = "", close = "";
140
+ if (router === "react-router") {
141
+ imports.push(`import { MemoryRouter } from '${routerPkg}'`);
142
+ open += "<MemoryRouter>";
143
+ close = "</MemoryRouter>" + close;
144
+ }
145
+ let toasterEl = "";
146
+ if (toaster === "sonner") {
147
+ imports.push(`import { Toaster } from 'sonner'`);
148
+ toasterEl = "<Toaster />";
149
+ }
150
+ if (toaster === "react-hot-toast") {
151
+ imports.push(`import { Toaster } from 'react-hot-toast'`);
152
+ toasterEl = "<Toaster />";
153
+ }
154
+ return `// Mock contexts wrapped around every frame. Scaffolded by ${NAME} init from what it detected - yours to edit.
155
+ ${imports.join("\n")}
156
+
157
+ export default function Providers({ children }: { children: ReactNode }) {
158
+ return (
159
+ ${open || "<>"}
160
+ {children}
161
+ ${toasterEl}
162
+ ${close || "</>"}
163
+ )
164
+ }
165
+ `;
166
+ }
167
+ //#endregion
168
+ export { init };