@marver-design/marver 0.3.1 → 0.5.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 (50) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/README.md +2 -0
  3. package/dist/auth-B36fMCM3.mjs +245 -0
  4. package/dist/{build-Ckmyci3O.mjs → build-BrCl9hJS.mjs} +56 -14
  5. package/dist/cli.mjs +15 -6
  6. package/dist/collab-CXy8gqoz.mjs +297 -0
  7. package/dist/comments-Ba8mU600.mjs +90 -0
  8. package/dist/comments-odHzYdO3.mjs +179 -0
  9. package/dist/dev-DdeU-Jst.mjs +162 -0
  10. package/dist/{init-DolP_Ld4.mjs → init-DsCUmlCW.mjs} +2 -1
  11. package/dist/{manifest-DW-T52MM.mjs → manifest-C8FODq2S.mjs} +26 -1
  12. package/dist/{plugin-Crp4CAma.mjs → plugin-BtSGAm2h.mjs} +169 -19
  13. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  14. package/dist/serve-D_KBK7Oy.mjs +431 -0
  15. package/dist/sync-CkBk-tUk.mjs +248 -0
  16. package/package.json +3 -1
  17. package/src/client/content/diagram.tsx +46 -2
  18. package/src/client/content/index.tsx +6 -2
  19. package/src/client/content/md.ts +29 -0
  20. package/src/client/content/palette.ts +6 -0
  21. package/src/client/frame-host/bridge.js +269 -3
  22. package/src/client/frame-host/serialize.ts +195 -0
  23. package/src/client/shell/App.tsx +131 -17
  24. package/src/client/shell/Comments.tsx +464 -0
  25. package/src/client/shell/Play.tsx +24 -0
  26. package/src/client/shell/canvas/Canvas.tsx +66 -29
  27. package/src/client/shell/canvas/FrameNode.tsx +130 -10
  28. package/src/client/shell/canvas/frame-registry.ts +18 -0
  29. package/src/client/shell/canvas/snapshots.ts +233 -0
  30. package/src/client/shell/comments-store.ts +180 -0
  31. package/src/client/shell/cursor-arrow-dark.svg +1 -0
  32. package/src/client/shell/cursor-arrow.svg +1 -0
  33. package/src/client/shell/hash.ts +14 -3
  34. package/src/client/shell/icons.tsx +6 -0
  35. package/src/client/shell/labels.ts +10 -0
  36. package/src/client/shell/perf.ts +92 -0
  37. package/src/client/shell/store.ts +101 -6
  38. package/src/client/shell/styles.css +196 -7
  39. package/src/client/stage/main.tsx +2 -0
  40. package/src/shared/events.ts +103 -0
  41. package/templates/AGENTS-embedded.md +13 -1
  42. package/templates/AGENTS-studio.md +13 -1
  43. package/templates/instructions/boards.md +6 -3
  44. package/templates/instructions/configure.md +28 -0
  45. package/templates/instructions/iterate.md +38 -0
  46. package/templates/instructions/publish.md +129 -0
  47. package/templates/instructions/reference/color.md +22 -1
  48. package/templates/instructions/shape.md +23 -16
  49. package/dist/dev-C2oTKuXe.mjs +0 -97
  50. package/dist/serve-OtA9Nlow.mjs +0 -207
@@ -0,0 +1,129 @@
1
+ # Publish - put the canvas on a URL, collaboration included
2
+
3
+ Publishing is three decisions, then one build and one process. A coding agent can
4
+ run the whole thing; nothing here needs a human at a dashboard except pasting env
5
+ vars if the host has no CLI.
6
+
7
+ ## The three decisions
8
+
9
+ 1. **Which boards ship, with which rights** - `design/publish.json`:
10
+
11
+ ```json
12
+ { "boards": { "release-review": "comment", "roadmap": "read" } }
13
+ ```
14
+
15
+ `read` = visible to anyone past the gate. `comment` = signed-in accounts can
16
+ also pin threads. Boards not listed do not ship at all - their frames are not
17
+ even in the bundle. `marver build` FAILS without this file (default-closed);
18
+ `--boards a,b` overrides ad hoc (grants comment), `--all-boards` ships
19
+ everything loudly.
20
+
21
+ 2. **The canvas password** - `MARVER_PASSWORD` at serve time. This is the READ
22
+ boundary: one shared password for GUESTS. Members never need it - their own
23
+ account signs them in at the gate, and invite links skip it entirely (the
24
+ token is the authorization). Rotating it therefore only affects guests.
25
+ No env var = an open canvas.
26
+
27
+ 3. **Collaboration on or off** - `MARVER_DATA_DIR` at serve time. Set it to a
28
+ path on a PERSISTENT disk and the serve grows accounts + live comments.
29
+ Leave it unset for a static, comment-free canvas. Setting it to ephemeral
30
+ container disk is the one real deploy mistake - comments would vanish on
31
+ redeploy; marver fails loudly if the dir cannot be created.
32
+
33
+ ## The serve contract (env vars, complete list)
34
+
35
+ | Var | Meaning |
36
+ |---|---|
37
+ | `PORT` | listen port (hosts inject this) |
38
+ | `MARVER_PASSWORD` | canvas password (guests' read credential); unset = open |
39
+ | `MARVER_DATA_DIR` | persistent dir for `comments/` + `auth.json`; unset = no collaboration |
40
+ | `MARVER_OWNER_EMAIL` | prints a single-use OWNER claim link in the deploy logs on first boot |
41
+ | `MARVER_TRUSTED_PROXY` | set to `1` behind a reverse proxy (Railway, Fly) so rate limits see real IPs |
42
+
43
+ ## The host contract (works on any volume-capable host)
44
+
45
+ The host does two things, and the deploy config names both. **`design/.dist` is
46
+ gitignored - it is built ON THE HOST at deploy time, never committed.**
47
+
48
+ - **build command**: `<install> && npx marver build` (respects `publish.json`,
49
+ seeds comment logs into the bundle)
50
+ - **start command**: `npx marver serve` (reads `PORT` + the env vars above)
51
+ - a **persistent volume** mounted at some path, named by `MARVER_DATA_DIR`
52
+
53
+ The `@marver-design/marver` dependency must resolve from the registry (a local
54
+ `link:`/`file:` dep cannot ride to a remote host) - a normal `^0.4.0` in
55
+ `package.json` is all it takes.
56
+
57
+ ## Railway quickstart
58
+
59
+ Commit a `railway.json` so `railway up` knows how to build and serve:
60
+
61
+ ```json
62
+ {
63
+ "$schema": "https://railway.com/railway.schema.json",
64
+ "build": { "builder": "NIXPACKS", "buildCommand": "pnpm install && npx marver build" },
65
+ "deploy": { "startCommand": "npx marver serve" }
66
+ }
67
+ ```
68
+
69
+ Then, once per service:
70
+
71
+ ```bash
72
+ railway init # or `railway link` an existing service
73
+ railway volume add --mount-path /data
74
+ railway variables --set MARVER_PASSWORD=<pw> --set MARVER_DATA_DIR=/data \
75
+ --set MARVER_OWNER_EMAIL=<owner@email> --set MARVER_TRUSTED_PROXY=1
76
+ railway up # uploads the repo; Railway runs build then start
77
+ railway logs # ← the owner claim LINK prints here, once (see below)
78
+ ```
79
+
80
+ Republishing is just `railway up` again: the server unions the seeded logs on
81
+ boot, so collected feedback is NEVER clobbered by a new build. Run ONE instance -
82
+ the event log is single-writer by design.
83
+
84
+ ## What the deployed gate offers (so you know what you're wiring)
85
+
86
+ On a collaboration canvas the gate has three doors, one credential each:
87
+ - **Guest** - the canvas password → read-only across published boards.
88
+ - **Member** - "Sign in instead" → their own email + password → read + comment.
89
+ A member session IS gate passage; they never touch the shared password again.
90
+ - **Invited** - opening an invite link → set a display name + password (+ optional
91
+ avatar) → account created, read + comment. The link skips the canvas password.
92
+
93
+ ## Wiring people up (after first deploy)
94
+
95
+ `MARVER_OWNER_EMAIL` makes the first boot print an owner bootstrap in the logs:
96
+ a browser link (`<url>/#/i/<token>`) AND the exact repo command. The token is
97
+ single-use. Claim it from the repo so this machine can mint invites:
98
+
99
+ ```bash
100
+ # 1. claim the owner account - copy the command the deploy logs printed:
101
+ marver comments connect https://canvas.example.com --invite <token-from-logs>
102
+
103
+ # 2. invite each colleague - one command, one link, no email infrastructure
104
+ marver comments invite colleague@company.com
105
+ # → prints a single invite LINK (<url>/#/i/<token>). Send it over Slack/DM
106
+ # with the canvas password. It opens straight into the claim (name +
107
+ # password + optional avatar). Single-use; 7-day expiry.
108
+
109
+ # 3. the loop is now closed
110
+ marver dev # two-way syncs comments every 30s
111
+ marver comments list --open # the agent's work queue
112
+ marver comments reply <thread> --body "..." # answer in the loop
113
+ marver comments resolve <thread> --addressed-in <frame> # close with a receipt
114
+ marver comments revoke <email> # kills the account + its sessions, mid-flight
115
+ ```
116
+
117
+ Share the canvas as `<url>/#/b/<board>` (with the canvas password), or copy-link
118
+ on any thread for a deep link straight to it - the gate carries deep links
119
+ through sign-in.
120
+
121
+ ## What lands where (so you can reason about persistence)
122
+
123
+ - `design/publish.json` - the policy, git-tracked, part of the repo.
124
+ - `<MARVER_DATA_DIR>/comments/<board>.jsonl` - the live event log, on the volume.
125
+ - `<MARVER_DATA_DIR>/auth.json` - accounts (scrypt), sessions, invites, on the volume.
126
+ - `design/comments/<board>.jsonl` - the dev-side mirror, git-tracked: feedback
127
+ has history, and the volume has an off-site replica for free.
128
+ - `design/.local/collab.json` - THIS machine's device credential. Gitignored;
129
+ never commit it.
@@ -46,8 +46,29 @@ present). Focus indicators need contrast AND a clearly visible change of appeara
46
46
  Check interactive states, text over images, and BOTH themes.
47
47
  Anything conveyed by color alone also needs text, shape, icon, or position.
48
48
 
49
+ ## Content-frame families (one palette, prose + diagrams)
50
+
51
+ In content frames (`Md`, `Diagram`), color-code by MEANING using the built-in family
52
+ names - never hand-roll hex or mermaid `classDef`. The same six families work in both,
53
+ so a sentence and the diagram beside it read as one color language:
54
+
55
+ - **Prose:** `:blue[the shipper's world]`, `:orange[the carrier market]`, `:purple[the
56
+ driver pool]`, `:green[...]`, `:red[...]`, `:gray[...]` inside any `Md` block.
57
+ - **Diagram nodes:** tag a node with the family - `HQ:::blue`, `Carriers:::orange`,
58
+ `Drivers:::purple`. No `classDef` needed; they're injected for you.
59
+
60
+ Pick ONE family per concept and hold it everywhere it appears (the intro word, the
61
+ diagram box, the section heading). That consistency is what lets a reader link the
62
+ picture to the prose at a glance. Reserve `gray` for the neutral/background ("the platform",
63
+ "out of scope"); use the vivid families for the actors that matter.
64
+
65
+ Node text: write labels as `Head :: gloss` - marver renders the head bold on top
66
+ with the gloss lighter and smaller below, so a box scans as label-then-detail,
67
+ never a run-on. Just the ` :: ` token; no backticks or `**` needed.
68
+
49
69
  ## Verify
50
70
 
51
71
  Every color has a stable role; attention lands on the intended action; the palette
52
72
  holds across quiet, dense, error, and empty states; both themes are composed; the
53
- result is recognizably THIS product, not a generic colorful treatment.
73
+ result is recognizably THIS product, not a generic colorful treatment. In content
74
+ frames, each concept keeps ONE family across prose and diagram.
@@ -110,27 +110,34 @@ a quadrant for prioritization, a state diagram for lifecycle logic, a sequence
110
110
  for API choreography. The syntax reference is the Mermaid docs:
111
111
  https://mermaid.js.org/intro/ - pull the one page you need, apply, return.
112
112
 
113
- **Color carries meaning - and the floor is never gray-on-gray.** The marver
114
- theme already colors every node (accent-washed fills, accent borders, both
115
- modes) - a default diagram looks designed with zero effort, so never hand-set
116
- grays "to be safe" and never re-theme (init directives are stripped anyway).
117
- Where the diagram has SEMANTICS, add them with `classDef` on top:
113
+ **Two pieces of built-in sugar make a flowchart read well with zero fiddling -
114
+ use them, don't hand-roll their raw mermaid equivalents.**
115
+
116
+ *Label hierarchy (`::`).* Write a node label as `Head :: gloss` and marver
117
+ renders the **head bold** on top with the gloss on a lighter, smaller line
118
+ below - no backticks, no `**`, no `<br>`. A box should scan as label-then-
119
+ detail, so lead with the actor and let the example follow:
118
120
 
119
121
  ```
120
122
  flowchart LR
121
- A[Draft] --> B{Review?} -->|approved| C[Published]
122
- B -->|rejected| D[Archived]
123
- classDef win fill:#34C759,stroke:#248A3D,color:#fff
124
- classDef stop fill:#FF383C,stroke:#D70015,color:#fff
125
- class C win
126
- class D stop
123
+ S["Shipper :: the company that needs freight moved"]:::blue
124
+ C["Carrier :: the trucking company that hauls it"]:::orange
125
+ D["Driver :: the person behind the wheel"]:::purple
126
+ S --> C --> D
127
127
  ```
128
128
 
129
- Use the system palette (the same 12 colors the series ramp uses), a few
130
- classes at most, and never encode meaning in color ALONE - the label or shape
131
- must carry it too. Check the diagram in BOTH themes (`d`): fills flip with
132
- the theme, hand-set classDef colors do not, so pick values that hold on both
133
- grounds (the system colors do).
129
+ *Family colors (`:::name`).* Tag a node with a built-in family and it gets a
130
+ filled, on-brand color with a legible border in both themes - no `classDef`.
131
+ The SAME six names work in `Md` prose (`:blue[the shipper's world]`), so a
132
+ sentence and the diagram beside it read as one color language:
133
+ `blue orange purple green red gray`. Pick ONE family per concept and hold it
134
+ everywhere; reserve `gray` for the neutral/background actor. Never encode
135
+ meaning in color ALONE - the label carries it too.
136
+
137
+ The marver theme already accent-washes every default node (both modes), so a
138
+ plain flowchart looks designed with zero effort - never hand-set grays "to be
139
+ safe" and never re-theme (init directives are stripped anyway). Check in BOTH
140
+ themes (`d`).
134
141
 
135
142
  ## Images and mood boards
136
143
 
@@ -1,97 +0,0 @@
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
- import { dirname, join } from "node:path";
5
- import { fileURLToPath } from "node:url";
6
- import { createLogger, 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 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
- }
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 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
- };
42
- const server = await createServer({
43
- configFile: false,
44
- root,
45
- css,
46
- customLogger: logger,
47
- plugins,
48
- server: {
49
- port: portFlag ?? config.port,
50
- strictPort: false,
51
- fs: { allow: [.../* @__PURE__ */ new Set([
52
- root,
53
- pkgDir,
54
- searchForWorkspaceRoot(root)
55
- ])] },
56
- watch: { ignored: [
57
- "**/design/manifest.json",
58
- "**/design/boards/**",
59
- "**/design/.local/**",
60
- "**/design/.dist/**",
61
- "**/.next/**",
62
- "**/.turbo/**",
63
- "**/.vercel/**",
64
- "**/.output/**",
65
- "**/dist/**",
66
- "**/build/**",
67
- "**/out/**",
68
- "**/coverage/**"
69
- ] }
70
- },
71
- resolve: {
72
- dedupe: ["react", "react-dom"],
73
- tsconfigPaths: true
74
- },
75
- optimizeDeps: {
76
- exclude: [PKG],
77
- include: [
78
- "react",
79
- "react-dom",
80
- "react-dom/client",
81
- "react/jsx-runtime",
82
- "react/jsx-dev-runtime",
83
- `${PKG} > marked`,
84
- `${PKG} > mermaid`
85
- ],
86
- entries: [join(clientDir, "frame-host", "index.html"), "design/**/*.{tsx,jsx}"]
87
- },
88
- logLevel: "info"
89
- });
90
- await server.listen();
91
- const addr = server.httpServer?.address();
92
- const port = typeof addr === "object" && addr ? addr.port : config.port;
93
- console.log(`\n ${NAME} canvas → http://localhost:${port}/\n`);
94
- return server;
95
- }
96
- //#endregion
97
- export { dev };
@@ -1,207 +0,0 @@
1
- import { n as NAME } from "./cli.mjs";
2
- import { existsSync, readFileSync, realpathSync } from "node:fs";
3
- import { extname, isAbsolute, join, relative, resolve } from "node:path";
4
- import { createHmac, randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
5
- import { createServer } from "node:http";
6
- //#region src/server/serve.ts
7
- /**
8
- * `marver serve` - a zero-dependency static server for design/.dist with an optional
9
- * password gate (SPEC-M2 §4b). No MARVER_PASSWORD → plain static serving. With it, the
10
- * bundle is never sent pre-auth: every unauthenticated request gets the gate page, auth
11
- * is a POST compare + an HMAC-signed 30-day cookie keyed off the password itself (all
12
- * instances agree, nothing stored server-side).
13
- */
14
- const MIME = {
15
- ".html": "text/html; charset=utf-8",
16
- ".js": "text/javascript",
17
- ".mjs": "text/javascript",
18
- ".css": "text/css",
19
- ".json": "application/json",
20
- ".svg": "image/svg+xml",
21
- ".png": "image/png",
22
- ".ico": "image/x-icon",
23
- ".webmanifest": "application/manifest+json",
24
- ".woff": "font/woff",
25
- ".woff2": "font/woff2",
26
- ".map": "application/json",
27
- ".txt": "text/plain"
28
- };
29
- const COOKIE = "mv_a";
30
- const MONTH = 2592e3;
31
- function serve(root, portFlag) {
32
- const dist = join(root, "design", ".dist");
33
- if (!existsSync(join(dist, "index.html"))) {
34
- console.error(`[${NAME}] design/.dist not found - run \`npx ${NAME} build\` first.`);
35
- process.exit(1);
36
- }
37
- const realDist = realpathSync(dist);
38
- let meta = {
39
- name: "Marver",
40
- branding: true
41
- };
42
- try {
43
- meta = {
44
- ...meta,
45
- ...JSON.parse(readFileSync(join(dist, "meta.json"), "utf8"))
46
- };
47
- } catch {}
48
- const password = process.env.MARVER_PASSWORD ?? "";
49
- const verifier = password ? scryptSync(password, "marver-gate", 32) : null;
50
- const cookieKey = randomBytes(32);
51
- const sign = (exp) => createHmac("sha256", cookieKey).update(`v1.${exp}`).digest("hex");
52
- const authed = (req) => {
53
- const m = /(?:^|;\s*)mv_a=(\d+)\.([0-9a-f]+)/.exec(String(req.headers.cookie ?? ""));
54
- if (!m) return false;
55
- const exp = Number(m[1]);
56
- if (!Number.isFinite(exp) || exp * 1e3 < Date.now()) return false;
57
- const want = Buffer.from(sign(exp)), got = Buffer.from(m[2]);
58
- return want.length === got.length && timingSafeEqual(want, got);
59
- };
60
- const server = createServer((req, res) => {
61
- const url = new URL(req.url ?? "/", "http://x");
62
- if (verifier) {
63
- if (req.method === "POST" && url.pathname === "/__mv/auth") {
64
- let body = "";
65
- req.on("data", (c) => {
66
- body += c;
67
- if (body.length > 1e4) req.destroy();
68
- });
69
- req.on("end", () => {
70
- const form = new URLSearchParams(body);
71
- const given = form.get("password") ?? "";
72
- if (timingSafeEqual(scryptSync(given, "marver-gate", 32), verifier)) {
73
- const exp = Math.floor(Date.now() / 1e3) + MONTH;
74
- const secure = req.headers["x-forwarded-proto"] === "https" ? "; Secure" : "";
75
- res.setHeader("set-cookie", `${COOKIE}=${exp}.${sign(exp)}; Path=/; Max-Age=${MONTH}; HttpOnly; SameSite=Lax${secure}`);
76
- const next = form.get("next") ?? "";
77
- res.statusCode = 303;
78
- res.setHeader("location", /^#\/[\w\/?&=%.,~-]*$/.test(next) ? `/${next}` : "/");
79
- return res.end();
80
- }
81
- return gate(res, meta, "Wrong password - try again");
82
- });
83
- return;
84
- }
85
- const cosmetic = url.pathname.startsWith("/__mv/favicon/") || /^\/__mv\/logo\.(svg|png)$/.test(url.pathname);
86
- if (!authed(req) && !cosmetic) return gate(res, meta);
87
- }
88
- let path;
89
- try {
90
- path = decodeURIComponent(url.pathname);
91
- } catch {
92
- res.statusCode = 400;
93
- return res.end("bad request");
94
- }
95
- if (path.endsWith("/")) path += "index.html";
96
- let file = resolve(dist, path.slice(1));
97
- try {
98
- const real = realpathSync(file);
99
- const rel = relative(realDist, real);
100
- if (rel.startsWith("..") || isAbsolute(rel)) throw new Error("outside dist");
101
- file = real;
102
- } catch {
103
- file = join(dist, "index.html");
104
- }
105
- if (!extname(file)) file = join(dist, "index.html");
106
- try {
107
- const content = readFileSync(file);
108
- res.setHeader("content-type", MIME[extname(file)] ?? "application/octet-stream");
109
- const cache = verifier ? "private, no-store" : file.startsWith(join(dist, "assets")) ? "public, max-age=31536000, immutable" : "no-store";
110
- res.setHeader("cache-control", cache);
111
- res.end(content);
112
- } catch {
113
- res.statusCode = 404;
114
- res.end("not found");
115
- }
116
- });
117
- const port = portFlag ?? (Number(process.env.PORT) || 4199);
118
- server.on("error", (err) => {
119
- if (err.code === "EADDRINUSE") {
120
- console.error(`[${NAME}] port ${port} is already in use - another \`${NAME} serve\` still running? Stop it, or pass --port <n>.`);
121
- process.exit(1);
122
- }
123
- throw err;
124
- });
125
- server.listen(port, () => {
126
- console.log(`\n ${NAME} serving design/.dist → http://localhost:${port}/`);
127
- console.log(verifier ? " gate: ON (MARVER_PASSWORD set)\n" : " gate: off - set MARVER_PASSWORD to require a password\n");
128
- });
129
- return server;
130
- }
131
- /** The Marver logo mark (ParallelogramDuo, same as the shell's sidebar). */
132
- const MARK_AT = (size) => `<svg viewBox="0 0 256 256" width="${size}" height="${size}" fill="currentColor" aria-hidden><path d="M239.29,59.28l-64.8,144a8,8,0,0,1-7.3,4.72H24a8,8,0,0,1-7.3-11.28l64.8-144A8,8,0,0,1,88.81,48H232A8,8,0,0,1,239.29,59.28Z" opacity=".1"/><path d="M245.43,47.31A15.94,15.94,0,0,0,232,40H88.81a16,16,0,0,0-14.59,9.43l-64.8,144A16,16,0,0,0,24,216H167.19a16,16,0,0,0,14.59-9.43l64.8-144A16,16,0,0,0,245.43,47.31ZM167.19,200H24L88.81,56H232Z"/></svg>`;
133
- const MARK = MARK_AT(17);
134
- const MARK_LG = MARK_AT(24);
135
- /** The gate: the canvas's own antechamber - light ground, dot grid, glass card in the
136
- * shell's exact token language. The visitor is one password away from stepping in. */
137
- function gate(res, meta, error) {
138
- const name = meta.name ? meta.name[0].toUpperCase() + meta.name.slice(1) : "Marver";
139
- const appMark = meta.logo ? `<img src="${esc(meta.logo)}" alt="" width="24" height="24" />` : MARK_LG;
140
- const title = meta.branding ? `${name} | Marver - Visualize your software` : name;
141
- const desc = meta.branding ? `${name}, shared as a live Marver canvas - real screens and prototypes, built from the codebase. Marver is the agent-native design canvas.` : `${name} - a private design canvas.`;
142
- res.statusCode = 200;
143
- res.setHeader("content-type", "text/html; charset=utf-8");
144
- res.setHeader("cache-control", "no-store");
145
- res.end(`<!doctype html>
146
- <html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
147
- <title>${esc(title)}</title>
148
- <meta name="description" content="${esc(desc)}" />
149
- <meta name="robots" content="noindex" />
150
- <meta name="theme-color" content="#e7e9ef" />
151
- <meta property="og:title" content="${esc(title)}" />
152
- <meta property="og:description" content="${esc(desc)}" />
153
- ${meta.branding ? "<meta property=\"og:site_name\" content=\"Marver\" />" : ""}
154
- <meta property="og:type" content="website" />
155
- <meta name="twitter:card" content="summary" />
156
- <link rel="icon" href="/__mv/favicon/favicon.ico" sizes="48x48" />
157
- <link rel="icon" type="image/png" sizes="32x32" href="/__mv/favicon/favicon-32x32.png" />
158
- <link rel="icon" type="image/png" sizes="16x16" href="/__mv/favicon/favicon-16x16.png" />
159
- <link rel="apple-touch-icon" href="/__mv/favicon/apple-touch-icon.png" />
160
- <style>
161
- * { box-sizing: border-box; margin: 0 }
162
- body { min-height: 100vh; display: flex; align-items: center; justify-content: center;
163
- background-color: #e7e9ef;
164
- background-image: radial-gradient(#c9cbd5 1px, transparent 1px); background-size: 20px 20px;
165
- font: 500 14px -apple-system, system-ui, sans-serif; color: #18181b;
166
- -webkit-font-smoothing: antialiased }
167
- main { display: flex; flex-direction: column; align-items: center; gap: 22px; padding: 16px }
168
- form { width: 340px; padding: 24px; border-radius: 24px; display: flex; flex-direction: column; gap: 14px;
169
- background: rgba(255, 255, 255, .64); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
170
- border: 1px solid rgba(24, 24, 27, .1);
171
- box-shadow: inset 0 1px 0 rgba(255, 255, 255, .65), 0 1px 2px rgba(24, 24, 27, .05), 0 4px 12px -6px rgba(24, 24, 27, .12) }
172
- header { display: flex; align-items: center; gap: 10px; padding: 4px 2px 4px 4px }
173
- header svg, header img { color: #0088ff; flex: none; border-radius: 6px }
174
- h1 { font-size: 17px; font-weight: 600; letter-spacing: -.01em }
175
- p { font-size: 12.5px; line-height: 1.5; color: rgba(24, 24, 27, .66); padding: 0 4px }
176
- input { height: 40px; padding: 0 16px; border-radius: 999px; border: 1px solid rgba(24, 24, 27, .14);
177
- background: #fff; color: #18181b; font: inherit; outline: none; transition: border-color .15s, box-shadow .15s }
178
- input::placeholder { color: rgba(24, 24, 27, .35) }
179
- input:focus { border-color: #0088ff; box-shadow: 0 0 0 3px rgba(0, 136, 255, .18) }
180
- button { height: 40px; border: 0; border-radius: 999px; background: #18181b; color: #fafafa;
181
- font: 600 13px -apple-system, system-ui, sans-serif; cursor: pointer; transition: background .15s }
182
- button:hover { background: #000 }
183
- .err { font-size: 12px; color: #b42318; padding: 0 4px }
184
- footer a { display: inline-flex; align-items: center; gap: 7px; font: 600 12.5px -apple-system, system-ui, sans-serif;
185
- color: rgba(24, 24, 27, .55); text-decoration: none }
186
- footer a > svg:first-of-type { width: 20px; height: 20px; color: #0088ff }
187
- footer .md, footer .up { transition: color .15s, opacity .15s }
188
- footer a:hover .md { color: #0088ff; text-decoration: underline; text-underline-offset: 3px }
189
- footer .up { opacity: .45; margin-left: -3px }
190
- footer a:hover .up { opacity: 1; color: #0088ff }
191
- </style></head>
192
- <body><main>
193
- <form method="post" action="/__mv/auth">
194
- <header>${appMark}<h1>${esc(name)}</h1></header>
195
- <p>You're one step from the canvas. This space is private - enter the password to step inside.</p>
196
- ${error ? `<div class="err">${esc(error)}</div>` : ""}
197
- <input type="password" name="password" placeholder="Password" autofocus autocomplete="current-password" />
198
- <input type="hidden" name="next" />
199
- <button type="submit">Open the canvas</button>
200
- </form>
201
- <script>document.querySelector('[name=next]').value = location.hash<\/script>
202
- ${meta.branding ? `<footer><a href="https://marver.design" target="_blank" rel="noopener">${MARK} <span>Powered by <span class="md">Marver.design</span></span> <svg class="up" viewBox="0 0 256 256" width="11" height="11" fill="currentColor" aria-hidden><path d="M200,64V168a8,8,0,0,1-16,0V83.31L69.66,197.66a8,8,0,0,1-11.32-11.32L172.69,72H88a8,8,0,0,1,0-16H192A8,8,0,0,1,200,64Z"/></svg></a></footer>` : ""}
203
- </main></body></html>`);
204
- }
205
- const esc = (s) => s.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
206
- //#endregion
207
- export { serve };