@marver-design/marver 0.3.1 → 0.4.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 +89 -0
- package/README.md +2 -0
- package/dist/auth-B36fMCM3.mjs +245 -0
- package/dist/{build-Ckmyci3O.mjs → build-p3xmXU3b.mjs} +52 -12
- package/dist/cli.mjs +15 -6
- package/dist/collab-CXy8gqoz.mjs +297 -0
- package/dist/comments-Ba8mU600.mjs +90 -0
- package/dist/comments-odHzYdO3.mjs +179 -0
- package/dist/{dev-C2oTKuXe.mjs → dev-DZi1yRhn.mjs} +17 -1
- package/dist/{init-DolP_Ld4.mjs → init-Ck8z-HiD.mjs} +1 -0
- package/dist/{plugin-Crp4CAma.mjs → plugin-CtQqO5ZZ.mjs} +114 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/dist/serve-D_KBK7Oy.mjs +431 -0
- package/dist/sync-CkBk-tUk.mjs +248 -0
- package/package.json +2 -1
- package/src/client/frame-host/bridge.js +231 -0
- package/src/client/shell/App.tsx +73 -7
- package/src/client/shell/Comments.tsx +463 -0
- package/src/client/shell/canvas/FrameNode.tsx +32 -2
- package/src/client/shell/comments-store.ts +180 -0
- package/src/client/shell/hash.ts +14 -3
- package/src/client/shell/icons.tsx +6 -0
- package/src/client/shell/store.ts +4 -1
- package/src/client/shell/styles.css +159 -0
- package/src/shared/events.ts +103 -0
- package/templates/AGENTS-embedded.md +13 -1
- package/templates/AGENTS-studio.md +13 -1
- package/templates/instructions/boards.md +6 -3
- package/templates/instructions/configure.md +28 -0
- package/templates/instructions/iterate.md +38 -0
- package/templates/instructions/publish.md +129 -0
- package/dist/serve-OtA9Nlow.mjs +0 -207
|
@@ -39,6 +39,34 @@ canvas, the tour - that flow lives in instructions/welcome.md, run it alongside.
|
|
|
39
39
|
several half-brands - document the one the app actually ships in DESIGN.md and
|
|
40
40
|
name the others as legacy.
|
|
41
41
|
|
|
42
|
+
## Working with teammates (branches, merges, a second engineer)
|
|
43
|
+
|
|
44
|
+
The whole `design/` folder is git-tracked - boards, scenes, comment logs, the
|
|
45
|
+
publish policy, these instructions. Only `design/.local/` (this machine's
|
|
46
|
+
connect credential) and `design/.dist/` (built on the host) are ignored. So a
|
|
47
|
+
teammate gets the exact canvas by pulling the branch: `pnpm install` →
|
|
48
|
+
`npx marver dev`. Nothing else to sync.
|
|
49
|
+
|
|
50
|
+
How the three kinds of design state merge across branches:
|
|
51
|
+
|
|
52
|
+
- **Comment logs (`design/comments/*.jsonl`) merge themselves.** They're
|
|
53
|
+
append-only and keyed by event id, and init writes a `merge=union` git
|
|
54
|
+
attribute for them - two branches that both collected feedback union
|
|
55
|
+
cleanly, no conflict. Even a hand-botched merge self-heals: replay dedupes
|
|
56
|
+
by id. This is the point of the event-log design - multiplayer comments
|
|
57
|
+
Just Work through plain git.
|
|
58
|
+
- **Scenes (`design/scenes/**.tsx`) merge like any code** - they're React
|
|
59
|
+
components. Standard review, standard conflicts.
|
|
60
|
+
- **Boards (`design/boards/*.json`) are the one friction** - they're node
|
|
61
|
+
positions, so two people rearranging the same board conflict on x/y. It's
|
|
62
|
+
cosmetic: take either side and re-run Tidy, or give features their own
|
|
63
|
+
boards so layouts don't overlap. Never let a boards conflict block a merge.
|
|
64
|
+
|
|
65
|
+
If collaboration is deployed, each engineer runs `marver comments connect
|
|
66
|
+
<url>` ONCE with their own account (the owner invites them) to get the live
|
|
67
|
+
cloud sync on top of git. Git carries the committed comments; `connect` adds
|
|
68
|
+
the real-time stream from published viewers.
|
|
69
|
+
|
|
42
70
|
## When it breaks mid-project
|
|
43
71
|
|
|
44
72
|
Frames suddenly unstyled → the theme import path moved: fix `design/theme.css`.
|
|
@@ -48,3 +48,41 @@ The human picks a direction; then, in one pass:
|
|
|
48
48
|
Archived frames are history, not options: never link them from live flows,
|
|
49
49
|
never count them as current design. They exist so "what did we try for the
|
|
50
50
|
editor?" has a visual answer.
|
|
51
|
+
|
|
52
|
+
## Comments are your work queue
|
|
53
|
+
|
|
54
|
+
When collaboration is on, humans pin comments to specific elements inside
|
|
55
|
+
frames. Those threads are addressed to YOU as much as to the designer - treat
|
|
56
|
+
the open list as a queue:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npx marver comments list --open --json # what needs you (anchors included)
|
|
60
|
+
npx marver comments reply <thread> --body "…"
|
|
61
|
+
npx marver comments resolve <thread> --addressed-in <scene/frame>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The discipline:
|
|
65
|
+
|
|
66
|
+
1. **Read the anchor before the words.** Each thread carries the element it
|
|
67
|
+
points at - tag, quote, source hint, position. "Too cramped" pinned to a
|
|
68
|
+
button is a different task than "too cramped" on the whole frame.
|
|
69
|
+
2. **Fork, don't overwrite.** Address feedback by creating a NEW variant of the
|
|
70
|
+
frame (the letter convention above) and iterating there. The commented
|
|
71
|
+
frame stays as the before; your variant is the after.
|
|
72
|
+
3. **Resolve with the receipt.** `--addressed-in <the-new-variant>` records
|
|
73
|
+
WHICH frame answered the feedback - the thread becomes an auditable link
|
|
74
|
+
from complaint to fix. Reply first when the change deserves a sentence of
|
|
75
|
+
explanation; resolve silently only for trivial mechanical fixes.
|
|
76
|
+
4. **Never resolve what you didn't address.** Disagree? Reply with your
|
|
77
|
+
reasoning and leave the thread open - the human closes debates, you close
|
|
78
|
+
completed work.
|
|
79
|
+
5. **A frame with open comments is load-bearing - never delete, rename, or
|
|
80
|
+
gut it.** Its threads are anchored to elements INSIDE it; restructure the
|
|
81
|
+
frame and the anchors strand (a dead anchor parks the pin at the frame
|
|
82
|
+
edge; deleting the whole frame strands the thread off-canvas entirely -
|
|
83
|
+
never lost from the log, but invisible until the frame returns). So: fork
|
|
84
|
+
the variant and iterate THERE, leave the commented frame untouched as the
|
|
85
|
+
before, and only once you `resolve --addressed-in <variant>` its threads
|
|
86
|
+
may it move to `archive/`. Resolve first, restructure second - never the
|
|
87
|
+
reverse. Check `comments list --board <b>` (no `--open`) to see resolved
|
|
88
|
+
threads too; the full history lives in the append-only log and in git.
|
|
@@ -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.
|
package/dist/serve-OtA9Nlow.mjs
DELETED
|
@@ -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 };
|