@marver-design/marver 0.7.0 → 0.8.1
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 +141 -3
- package/README.md +72 -16
- package/dist/{auth-B36fMCM3.mjs → auth-KQ9Aj-nB.mjs} +1 -1
- package/dist/{build-BZaPa2DS.mjs → build-BBVQRetk.mjs} +5 -5
- package/dist/cli.mjs +16 -7
- package/dist/{collab-CXy8gqoz.mjs → collab-s3k5byM1.mjs} +18 -7
- package/dist/{comments-Ba8mU600.mjs → comments-BZBKhKRO.mjs} +4 -10
- package/dist/{comments-odHzYdO3.mjs → comments-J06jqCVV.mjs} +10 -16
- package/dist/daemon-DkyNOwIt.mjs +878 -0
- package/dist/{dev-DaPQ9xA5.mjs → dev-BTAhTie-.mjs} +28 -3
- package/dist/events-BMtBvvgU.mjs +101 -0
- package/dist/{init-DsCUmlCW.mjs → init-DWdhjJD5.mjs} +28 -3
- package/dist/ledger-CbzTJrV2.mjs +64 -0
- package/dist/{manifest-C8FODq2S.mjs → manifest-B4zcDGBf.mjs} +19 -4
- package/dist/{plugin-wMY9lNf3.mjs → plugin-BdQEeTLg.mjs} +89 -41
- package/dist/profile-BkiWglVE.mjs +39 -0
- package/dist/{serve-D_KBK7Oy.mjs → serve-CZqPnj19.mjs} +32 -8
- package/dist/{sync-CkBk-tUk.mjs → sync-BJKKmy1n.mjs} +5 -103
- package/dist/work-CLrmY-vQ.mjs +97 -0
- package/dist/work-lzC-lPY0.mjs +76 -0
- package/package.json +16 -1
- package/src/client/const.ts +1 -1
- package/src/client/content/diagram.tsx +1 -1
- package/src/client/content/index.tsx +2 -2
- package/src/client/content/md.ts +1 -1
- package/src/client/content/palette.ts +2 -2
- package/src/client/frame-host/bridge.js +1 -1
- package/src/client/frame-host/inspect.js +16 -3
- package/src/client/frame-host/serialize.ts +2 -2
- package/src/client/shell/App.tsx +165 -26
- package/src/client/shell/Comments.tsx +434 -78
- package/src/client/shell/Play.tsx +12 -8
- package/src/client/shell/Toolbar.tsx +14 -2
- package/src/client/shell/canvas/Canvas.tsx +1 -1
- package/src/client/shell/canvas/FrameNode.tsx +47 -15
- package/src/client/shell/canvas/snapshots.ts +3 -3
- package/src/client/shell/comments-store.ts +147 -33
- package/src/client/shell/hash.ts +2 -2
- package/src/client/shell/icons.tsx +5 -1
- package/src/client/shell/keys.ts +39 -0
- package/src/client/shell/mentions.ts +18 -0
- package/src/client/shell/perf.ts +1 -1
- package/src/client/shell/store.ts +127 -39
- package/src/client/shell/styles.css +303 -30
- package/src/client/shell/tidy.ts +5 -5
- package/src/client/stage/main.tsx +2 -2
- package/src/shared/events.ts +23 -6
- package/src/shared/utm.ts +22 -0
- package/templates/AGENTS-embedded.md +49 -1
- package/templates/AGENTS-studio.md +49 -1
- package/templates/instructions/configure.md +15 -0
- package/templates/instructions/jam.md +85 -0
- package/templates/instructions/publish.md +1 -1
- package/dist/rolldown-runtime-D7D4PA-g.mjs +0 -13
|
@@ -1,101 +1,10 @@
|
|
|
1
|
-
import { t as
|
|
2
|
-
import {
|
|
1
|
+
import { t as diffEvents } from "./events-BMtBvvgU.mjs";
|
|
2
|
+
import { appendEvents, listBoards, readLog } from "./comments-BZBKhKRO.mjs";
|
|
3
3
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
//#region src/shared/events.ts
|
|
6
|
-
/** Events the other side lacks, by id - the sync payload in either direction. */
|
|
7
|
-
function diffEvents(mine, theirIds) {
|
|
8
|
-
const have = new Set(theirIds);
|
|
9
|
-
return mine.filter((e) => !have.has(e.id));
|
|
10
|
-
}
|
|
11
|
-
/** Replay a board's events into current thread state. Deterministic: replay order is
|
|
12
|
-
* (ts, id) so two stores holding the same event SET always derive the same state.
|
|
13
|
-
* TWO passes - creates first, then everything else - so a reply whose author's clock
|
|
14
|
-
* ran ahead of the creator's still lands instead of being dropped forever. */
|
|
15
|
-
function replay(events) {
|
|
16
|
-
const ordered = [...events].sort((a, b) => a.ts - b.ts || (a.id < b.id ? -1 : 1));
|
|
17
|
-
const threads = /* @__PURE__ */ new Map();
|
|
18
|
-
for (const ev of ordered) {
|
|
19
|
-
if (ev.type !== "create") continue;
|
|
20
|
-
if (!ev.commentId || threads.has(ev.commentId)) continue;
|
|
21
|
-
threads.set(ev.commentId, {
|
|
22
|
-
id: ev.commentId,
|
|
23
|
-
board: ev.board,
|
|
24
|
-
nodeKey: ev.nodeKey,
|
|
25
|
-
frame: ev.frame,
|
|
26
|
-
anchor: ev.anchor,
|
|
27
|
-
author: ev.author,
|
|
28
|
-
body: ev.body,
|
|
29
|
-
ts: ev.ts,
|
|
30
|
-
resolved: false,
|
|
31
|
-
replies: [],
|
|
32
|
-
reactions: {}
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
for (const ev of ordered) switch (ev.type) {
|
|
36
|
-
case "reply": {
|
|
37
|
-
const t = ev.parentId ? threads.get(ev.parentId) : void 0;
|
|
38
|
-
if (!t || !ev.commentId || t.replies.some((r) => r.id === ev.commentId)) break;
|
|
39
|
-
t.replies.push({
|
|
40
|
-
id: ev.commentId,
|
|
41
|
-
author: ev.author,
|
|
42
|
-
body: ev.body,
|
|
43
|
-
ts: ev.ts
|
|
44
|
-
});
|
|
45
|
-
break;
|
|
46
|
-
}
|
|
47
|
-
case "edit": {
|
|
48
|
-
if (!ev.commentId || ev.body === void 0) break;
|
|
49
|
-
const t = threads.get(ev.commentId);
|
|
50
|
-
if (t) {
|
|
51
|
-
t.body = ev.body;
|
|
52
|
-
break;
|
|
53
|
-
}
|
|
54
|
-
for (const th of threads.values()) {
|
|
55
|
-
const r = th.replies.find((r) => r.id === ev.commentId);
|
|
56
|
-
if (r) {
|
|
57
|
-
r.body = ev.body;
|
|
58
|
-
break;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
break;
|
|
62
|
-
}
|
|
63
|
-
case "resolve": {
|
|
64
|
-
const t = ev.commentId ? threads.get(ev.commentId) : void 0;
|
|
65
|
-
if (t) {
|
|
66
|
-
t.resolved = true;
|
|
67
|
-
if (ev.addressedIn) t.addressedIn = ev.addressedIn;
|
|
68
|
-
}
|
|
69
|
-
break;
|
|
70
|
-
}
|
|
71
|
-
case "reopen": {
|
|
72
|
-
const t = ev.commentId ? threads.get(ev.commentId) : void 0;
|
|
73
|
-
if (t) {
|
|
74
|
-
t.resolved = false;
|
|
75
|
-
t.addressedIn = void 0;
|
|
76
|
-
}
|
|
77
|
-
break;
|
|
78
|
-
}
|
|
79
|
-
case "react": {
|
|
80
|
-
const t = ev.commentId ? threads.get(ev.commentId) : void 0;
|
|
81
|
-
const who = ev.author?.email;
|
|
82
|
-
if (!t || !who || !ev.emoji) break;
|
|
83
|
-
const users = t.reactions[ev.emoji] ??= [];
|
|
84
|
-
const at = users.indexOf(who);
|
|
85
|
-
if (at === -1) users.push(who);
|
|
86
|
-
else {
|
|
87
|
-
users.splice(at, 1);
|
|
88
|
-
if (!users.length) delete t.reactions[ev.emoji];
|
|
89
|
-
}
|
|
90
|
-
break;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return [...threads.values()];
|
|
94
|
-
}
|
|
95
|
-
//#endregion
|
|
96
5
|
//#region src/server/sync.ts
|
|
97
6
|
/**
|
|
98
|
-
* Dev ↔ published comment sync
|
|
7
|
+
* Dev ↔ published comment sync - one merge rule, run from the dev side.
|
|
99
8
|
*
|
|
100
9
|
* The published serve is the canonical home; the repo's design/comments/ is the
|
|
101
10
|
* mirror the agent reads. Each exchange is: pull remote events → union into local
|
|
@@ -105,13 +14,6 @@ function replay(events) {
|
|
|
105
14
|
* Credentials: design/.local/collab.json {url, token} written by `comments connect`.
|
|
106
15
|
* The token is a server session held by the DEV PROCESS only - pages never see it.
|
|
107
16
|
*/
|
|
108
|
-
var sync_exports = /* @__PURE__ */ __exportAll({
|
|
109
|
-
connect: () => connect,
|
|
110
|
-
connectClaim: () => connectClaim,
|
|
111
|
-
loadCollab: () => loadCollab,
|
|
112
|
-
saveCollab: () => saveCollab,
|
|
113
|
-
syncOnce: () => syncOnce
|
|
114
|
-
});
|
|
115
17
|
const collabFile = (root) => join(root, "design", ".local", "collab.json");
|
|
116
18
|
function loadCollab(root) {
|
|
117
19
|
try {
|
|
@@ -148,7 +50,7 @@ async function syncOnce(root, collab) {
|
|
|
148
50
|
const pulled = appendEvents(dir, board, remote).length;
|
|
149
51
|
let pushed = 0;
|
|
150
52
|
if (rights[board] === "comment") {
|
|
151
|
-
const missing = diffEvents(readLog(dir, board), remote.map((e) => e.id));
|
|
53
|
+
const missing = diffEvents(readLog(dir, board), remote.map((e) => e.id)).filter((e) => !e.agent);
|
|
152
54
|
for (let i = 0; i < missing.length; i += 100) {
|
|
153
55
|
const r = await fetch(`${base}/__mv/api/comments/${board}`, {
|
|
154
56
|
method: "POST",
|
|
@@ -245,4 +147,4 @@ async function connectClaim(root, url, invite, profile, canvasPassword) {
|
|
|
245
147
|
});
|
|
246
148
|
}
|
|
247
149
|
//#endregion
|
|
248
|
-
export {
|
|
150
|
+
export { connect, connectClaim, loadCollab, syncOnce };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
//#region src/server/jam/activity.ts
|
|
5
|
+
function createActivity(ttlMs = 9e4) {
|
|
6
|
+
const m = /* @__PURE__ */ new Map();
|
|
7
|
+
const cbs = [];
|
|
8
|
+
const frames = () => [...new Set([...m.keys()].map((k) => k.slice(k.indexOf("|") + 1)))];
|
|
9
|
+
const emit = () => {
|
|
10
|
+
const f = frames();
|
|
11
|
+
for (const cb of cbs) cb(f);
|
|
12
|
+
};
|
|
13
|
+
return {
|
|
14
|
+
mark(frame, ttl, src = "cli") {
|
|
15
|
+
if (!frame) return;
|
|
16
|
+
m.set(`${src}|${frame}`, Date.now() + (ttl ?? ttlMs));
|
|
17
|
+
emit();
|
|
18
|
+
},
|
|
19
|
+
clear(frame, src = "cli") {
|
|
20
|
+
if (m.delete(`${src}|${frame}`)) emit();
|
|
21
|
+
},
|
|
22
|
+
clearAll(src = "cli") {
|
|
23
|
+
let changed = false;
|
|
24
|
+
for (const k of [...m.keys()]) if (k.startsWith(`${src}|`)) {
|
|
25
|
+
m.delete(k);
|
|
26
|
+
changed = true;
|
|
27
|
+
}
|
|
28
|
+
if (changed) emit();
|
|
29
|
+
},
|
|
30
|
+
active: frames,
|
|
31
|
+
sweep() {
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
let changed = false;
|
|
34
|
+
for (const [k, until] of m) if (until <= now) {
|
|
35
|
+
m.delete(k);
|
|
36
|
+
changed = true;
|
|
37
|
+
}
|
|
38
|
+
if (changed) emit();
|
|
39
|
+
},
|
|
40
|
+
onChange(fn) {
|
|
41
|
+
cbs.push(fn);
|
|
42
|
+
return () => {
|
|
43
|
+
const i = cbs.indexOf(fn);
|
|
44
|
+
if (i !== -1) cbs.splice(i, 1);
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/server/work.ts
|
|
51
|
+
/**
|
|
52
|
+
* The working-state rail for CHAT-driven agents (the jam daemon has its own in-process
|
|
53
|
+
* path into the same set). A coding agent that just accepted a request creates the frame
|
|
54
|
+
* files first, then `npx marver work start <frame...>` - the canvas shows the live
|
|
55
|
+
* working shimmer before a single component exists. `marver work done` clears it.
|
|
56
|
+
*
|
|
57
|
+
* Transport: the dev server writes design/.local/dev.json ({port, token}) at boot; the
|
|
58
|
+
* CLI reads it to find the server and authenticate. The token makes the endpoint
|
|
59
|
+
* unreachable from a drive-by browser page (which cannot read local files), while any
|
|
60
|
+
* process that can read the repo - the owner's own tools - is trusted by definition.
|
|
61
|
+
* Presence itself NEVER touches disk (activity.ts).
|
|
62
|
+
*/
|
|
63
|
+
/** One per process - the dev server, the jam daemon, and the API all share it. */
|
|
64
|
+
const workActivity = createActivity();
|
|
65
|
+
/** The longest a CLI mark may glow unrefreshed - a forgotten `done` self-heals. */
|
|
66
|
+
const WORK_TTL_MAX = 18e5;
|
|
67
|
+
const WORK_TTL_DEFAULT = 6e5;
|
|
68
|
+
const infoPath = (root) => join(root, "design", ".local", "dev.json");
|
|
69
|
+
/** Written once per boot (dev.ts), removed on close - the CLI's discovery + credential. */
|
|
70
|
+
function writeDevInfo(root, port) {
|
|
71
|
+
const token = randomBytes(24).toString("base64url");
|
|
72
|
+
mkdirSync(join(root, "design", ".local"), { recursive: true });
|
|
73
|
+
writeFileSync(infoPath(root), JSON.stringify({
|
|
74
|
+
port,
|
|
75
|
+
token,
|
|
76
|
+
ts: Date.now()
|
|
77
|
+
}, null, 2) + "\n");
|
|
78
|
+
return token;
|
|
79
|
+
}
|
|
80
|
+
function removeDevInfo(root) {
|
|
81
|
+
try {
|
|
82
|
+
rmSync(infoPath(root), { force: true });
|
|
83
|
+
} catch {}
|
|
84
|
+
}
|
|
85
|
+
function readDevInfo(root) {
|
|
86
|
+
try {
|
|
87
|
+
const v = JSON.parse(readFileSync(infoPath(root), "utf8"));
|
|
88
|
+
return typeof v?.port === "number" && typeof v?.token === "string" ? {
|
|
89
|
+
port: v.port,
|
|
90
|
+
token: v.token
|
|
91
|
+
} : null;
|
|
92
|
+
} catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
//#endregion
|
|
97
|
+
export { WORK_TTL_DEFAULT, WORK_TTL_MAX, readDevInfo, removeDevInfo, workActivity, writeDevInfo };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { n as NAME } from "./cli.mjs";
|
|
2
|
+
import { WORK_TTL_DEFAULT, WORK_TTL_MAX, readDevInfo } from "./work-CLrmY-vQ.mjs";
|
|
3
|
+
//#region src/cli/work.ts
|
|
4
|
+
/**
|
|
5
|
+
* `marver work` - the coding agent's hand on the canvas working state.
|
|
6
|
+
*
|
|
7
|
+
* work start <scene/frame ...> [--ttl <minutes>] mark frames actively working
|
|
8
|
+
* work done <scene/frame ...> | --all clear the glow
|
|
9
|
+
* work list what is glowing right now
|
|
10
|
+
*
|
|
11
|
+
* The intended choreography (taught in design/AGENTS.md): on accepting a request,
|
|
12
|
+
* create the frame FILES first (name, meta, a minimal skeleton), pin them on the
|
|
13
|
+
* right board, `work start` them - the human sees the request land on the canvas
|
|
14
|
+
* within seconds - build (independent frames in parallel, one subagent each), then
|
|
15
|
+
* `work done`. Marks self-expire (default 10 min, max 30) so a crashed agent can
|
|
16
|
+
* never leave a frame glowing forever.
|
|
17
|
+
*/
|
|
18
|
+
async function workCommand(root, action, frames, opts) {
|
|
19
|
+
const info = readDevInfo(root);
|
|
20
|
+
if (!info) throw new Error(`\`${NAME} dev\` is not running in this repo (design/.local/dev.json not found) - start it first.`);
|
|
21
|
+
const call = async (method, body) => {
|
|
22
|
+
let res;
|
|
23
|
+
try {
|
|
24
|
+
res = await fetch(`http://localhost:${info.port}/__mv/api/work`, {
|
|
25
|
+
method,
|
|
26
|
+
headers: {
|
|
27
|
+
"content-type": "application/json",
|
|
28
|
+
"x-mv-work": info.token
|
|
29
|
+
},
|
|
30
|
+
...body === void 0 ? {} : { body: JSON.stringify(body) }
|
|
31
|
+
});
|
|
32
|
+
} catch {
|
|
33
|
+
throw new Error(`could not reach \`${NAME} dev\` on port ${info.port} - is it still running?`);
|
|
34
|
+
}
|
|
35
|
+
const data = await res.json().catch(() => ({}));
|
|
36
|
+
if (res.status === 403) throw new Error(`the dev server refused the token - it likely restarted; try again (design/.local/dev.json is re-read each run)`);
|
|
37
|
+
if (!res.ok) throw new Error(String(data?.error ?? `work ${action} failed (${res.status})`));
|
|
38
|
+
if (!Array.isArray(data?.frames)) throw new Error(`port ${info.port} did not answer like \`${NAME} dev\` - is it still running?`);
|
|
39
|
+
return data;
|
|
40
|
+
};
|
|
41
|
+
switch (action) {
|
|
42
|
+
case "start": {
|
|
43
|
+
if (!frames.length) throw new Error("name the frames: work start <scene/frame ...>");
|
|
44
|
+
const min = Number(opts.ttl);
|
|
45
|
+
const ttlMs = Number.isFinite(min) && min > 0 ? Math.min(Math.max(min * 6e4, 1e4), WORK_TTL_MAX) : WORK_TTL_DEFAULT;
|
|
46
|
+
const { frames: active } = await call("POST", {
|
|
47
|
+
frames,
|
|
48
|
+
on: true,
|
|
49
|
+
ttlMs
|
|
50
|
+
});
|
|
51
|
+
const pretty = ttlMs >= 6e4 ? `${Math.round(ttlMs / 6e4)} min` : `${Math.round(ttlMs / 1e3)} s`;
|
|
52
|
+
console.log(`working: ${active.join(", ")} (auto-expires in ${pretty} - re-run to extend, \`work done\` to clear)`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
case "done": {
|
|
56
|
+
if (!frames.length && !opts.all) throw new Error("name the frames (or --all): work done <scene/frame ...>");
|
|
57
|
+
const { frames: active } = await call("POST", opts.all ? {
|
|
58
|
+
on: false,
|
|
59
|
+
all: true
|
|
60
|
+
} : {
|
|
61
|
+
frames,
|
|
62
|
+
on: false
|
|
63
|
+
});
|
|
64
|
+
console.log(active.length ? `still working: ${active.join(", ")}` : "nothing working - all clear");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
case "list": {
|
|
68
|
+
const { frames: active } = await call("GET");
|
|
69
|
+
console.log(active.length ? active.join("\n") : "nothing working");
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
default: throw new Error(`unknown action "${action}" - use start · done · list`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
76
|
+
export { workCommand };
|
package/package.json
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marver-design/marver",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "The agent-native design canvas. A design/ folder, one command, a canvas of live frames built from your repo's real components. The tool ships no AI - your coding agent is the designer.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
7
7
|
"license": "Apache-2.0",
|
|
8
|
+
"author": "Nic Touron (https://github.com/TNEP4)",
|
|
9
|
+
"keywords": [
|
|
10
|
+
"design",
|
|
11
|
+
"canvas",
|
|
12
|
+
"agent",
|
|
13
|
+
"prototyping",
|
|
14
|
+
"design-tool",
|
|
15
|
+
"react",
|
|
16
|
+
"vite",
|
|
17
|
+
"tailwind"
|
|
18
|
+
],
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/TNEP4/marver/issues"
|
|
21
|
+
},
|
|
8
22
|
"bin": {
|
|
9
23
|
"marver": "dist/cli.mjs"
|
|
10
24
|
},
|
|
@@ -55,6 +69,7 @@
|
|
|
55
69
|
"engines": {
|
|
56
70
|
"node": ">=22.18.0"
|
|
57
71
|
},
|
|
72
|
+
"packageManager": "pnpm@11.0.8",
|
|
58
73
|
"homepage": "https://marver.design",
|
|
59
74
|
"repository": {
|
|
60
75
|
"type": "git",
|
package/src/client/const.ts
CHANGED
|
@@ -4,7 +4,7 @@ export const NAME = 'marver'
|
|
|
4
4
|
export const PKG = '@marver-design/marver' // registry identity; bin stays `marver`
|
|
5
5
|
export const ROUTE = '/__mv'
|
|
6
6
|
|
|
7
|
-
/** Content-frame natural widths
|
|
7
|
+
/** Content-frame natural widths: Doc layout -> own-size width.
|
|
8
8
|
* Shared by the Doc primitive (measurement messages) and the server-side
|
|
9
9
|
* manifest scan (defaultSize for content frames) - one source, no drift. */
|
|
10
10
|
export const CONTENT_WIDTH: Record<string, number> = { document: 760, wide: 1280 }
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The Diagram block - first-class Mermaid
|
|
2
|
+
* The Diagram block - first-class Mermaid.
|
|
3
3
|
* Mermaid is marver's dependency behind a dynamic import: a workspace with no
|
|
4
4
|
* Diagram loads no mermaid bytes. Source-level theme overrides (%%{init}%%,
|
|
5
5
|
* yaml frontmatter) are stripped so the marver palette always holds; the
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Content-frame block primitives
|
|
2
|
+
* Content-frame block primitives: Doc, Row, Col, Space, Md, Diagram, Img.
|
|
3
3
|
* One vocabulary at two scales: boards arrange frames in rows/columns with space
|
|
4
4
|
* units; these primitives do the same for blocks INSIDE a frame - as CSS flex,
|
|
5
|
-
* deliberately NOT the
|
|
5
|
+
* deliberately NOT the board lane-flow grammar (shared vocabulary, not semantics).
|
|
6
6
|
*
|
|
7
7
|
* Doc owns the measurement protocol: it reports its content height so the shell
|
|
8
8
|
* can give the frame a natural size (the shell alone owns node dimensions).
|
package/src/client/content/md.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Markdown rendering for the Md block
|
|
2
|
+
* Markdown rendering for the Md block.
|
|
3
3
|
* Raw HTML is inert (escaped, never parsed). Links and images have SEPARATE
|
|
4
4
|
* policies: links may be goto:/http(s)/mailto (external ones open a new tab,
|
|
5
5
|
* never navigate the iframe); images are local-only - design/assets/ paths.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The marver diagram identity
|
|
2
|
+
* The marver diagram identity: the full Apple system palette (HIG),
|
|
3
3
|
* every value the HIG's own light/dark pair - never inverted. Structure comes
|
|
4
4
|
* from the systemGray ramp, emphasis from blue+purple, series diversity from
|
|
5
5
|
* the 12-color set.
|
|
@@ -43,7 +43,7 @@ export function themeVars(dark: boolean): Record<string, string> {
|
|
|
43
43
|
const purple = dark ? ACCENT.purple.dark : ACCENT.purple.light
|
|
44
44
|
// Nodes are ACCENTED by default - a plain flowchart only ever touches
|
|
45
45
|
// primaryColor, so a gray primary meant every default diagram rendered
|
|
46
|
-
// gray-on-gray
|
|
46
|
+
// gray-on-gray. Structure (lines, clusters,
|
|
47
47
|
// labels) stays grayscale; the shapes carry the marver blue.
|
|
48
48
|
const blueWash = dark ? '#123A5C' : '#E4F0FF'
|
|
49
49
|
const purpleWash = dark ? '#3A1440' : '#F8E4FC'
|
|
@@ -12,7 +12,7 @@ const isHtmlFrame = new URL(import.meta.url).searchParams.get('html') === '1'
|
|
|
12
12
|
const post = (msg) => { if (window.parent !== window) window.parent.postMessage(msg, location.origin) }
|
|
13
13
|
const id = new URLSearchParams(location.search).get('id') ?? location.pathname
|
|
14
14
|
|
|
15
|
-
//
|
|
15
|
+
// The shell serialises this frame's DOM (same origin) for the lean facade. Open shadow roots
|
|
16
16
|
// are walkable, but a CLOSED root is invisible after the fact - flag it at creation so the serialiser
|
|
17
17
|
// degrades the frame (keeps it live) instead of shipping a lean copy missing its shadow content.
|
|
18
18
|
const _attachShadow = Element.prototype.attachShadow
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Shared frame-inspect controller
|
|
1
|
+
// Shared frame-inspect controller.
|
|
2
2
|
//
|
|
3
3
|
// Laser outlines, comment-pick, anchor capture/resolve, and a persistent element
|
|
4
4
|
// lock ("highlight") - the review machinery shared by the canvas frame bridge
|
|
@@ -52,6 +52,9 @@ const DEPTH_HUE = (() => {
|
|
|
52
52
|
|
|
53
53
|
export function createInspect({ post, getId, onModeChange }) {
|
|
54
54
|
let laserOn = false, pickOn = false
|
|
55
|
+
// quiet pick (⇧L laser comment off): clicks still anchor comments, but NO hover outline,
|
|
56
|
+
// label, or lock lighting inside the artwork - the shell decides, the frame obeys
|
|
57
|
+
let pickQuiet = false
|
|
55
58
|
let hoverEl = null, labelEl = null
|
|
56
59
|
let lockedEl = null, highlightAnchor = null // the persistent outline (#4 lock / #5 open)
|
|
57
60
|
let copiedTimer = null, copySeq = 0, copyEl = null
|
|
@@ -172,11 +175,19 @@ body [data-mv-hover] { outline: 2px solid hsl(var(--mv-hue) 95% 45%); outline-of
|
|
|
172
175
|
label.style.top = y + 'px'
|
|
173
176
|
}
|
|
174
177
|
|
|
178
|
+
// the pointer LEAVING this frame takes its hover lighting with it: each frame is its own
|
|
179
|
+
// document, so once the cursor is gone no mousemove ever fires here again - without this,
|
|
180
|
+
// the last hovered outline + label fossilize on every frame the cursor crossed.
|
|
181
|
+
// (relatedTarget null = left the window/iframe, not just moved between elements)
|
|
182
|
+
on(document, 'mouseout', (e) => { if (!e.relatedTarget) clearHover() })
|
|
183
|
+
on(window, 'blur', clearHover)
|
|
184
|
+
|
|
175
185
|
// hover-follow: the label chases the cursor while a mode is on - BUT a locked
|
|
176
186
|
// element (#4 composing / #5 thread open) freezes it: the lit element is the one
|
|
177
187
|
// being discussed, not whatever the pointer grazes.
|
|
178
188
|
on(document, 'mousemove', (e) => {
|
|
179
189
|
if (!modeActive() || lockedEl) return
|
|
190
|
+
if (pickOn && pickQuiet) return // quiet pick: no hover visuals, clicks still land
|
|
180
191
|
const el = e.target instanceof Element && e.target.closest('body *:not(#mv-laser-label)')
|
|
181
192
|
if (!el || el === hoverEl) { if (labelEl && hoverEl) place(labelEl, e); return }
|
|
182
193
|
clearHover()
|
|
@@ -245,7 +256,8 @@ body [data-mv-hover] { outline: 2px solid hsl(var(--mv-hue) 95% 45%); outline-of
|
|
|
245
256
|
if (pickOn) {
|
|
246
257
|
// #4: lock the chosen element the instant it is picked - no round-trip flicker.
|
|
247
258
|
// The shell re-confirms via sh:highlight-anchor once the draft is staged.
|
|
248
|
-
|
|
259
|
+
// Quiet pick (⇧L off) anchors WITHOUT lighting the element.
|
|
260
|
+
if (!pickQuiet) applyLock(el, hueOf(el))
|
|
249
261
|
clearHover()
|
|
250
262
|
post({ type: 'sh:picked', id: getId(), anchor: anchorBundle(el, e) })
|
|
251
263
|
} else {
|
|
@@ -299,7 +311,8 @@ body [data-mv-hover] { outline: 2px solid hsl(var(--mv-hue) 95% 45%); outline-of
|
|
|
299
311
|
if (!m || typeof m !== 'object') return
|
|
300
312
|
if (m.type === 'sh:laser') { laserOn = !!m.on; applyModes(); onModeChange?.(laserOn, pickOn) }
|
|
301
313
|
else if (m.type === 'sh:pick') {
|
|
302
|
-
pickOn = !!m.on; applyModes()
|
|
314
|
+
pickOn = !!m.on; pickQuiet = !!m.quiet; applyModes()
|
|
315
|
+
if (pickQuiet) clearHover() // a hover lit before the toggle landed must not linger
|
|
303
316
|
// leaving comment mode drops a provisional self-lock the shell never confirmed;
|
|
304
317
|
// a shell-owned highlight (highlightAnchor set, e.g. #5) survives
|
|
305
318
|
if (!pickOn && !highlightAnchor) clearLock()
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// The DOM-snapshot serializer. Turns a live, same-origin frame Document into a
|
|
2
2
|
// self-contained STATIC html string: post-render DOM + full inlined CSS (styleSheets +
|
|
3
3
|
// adoptedStyleSheets), JS stripped. Rendered in a `sandbox="allow-same-origin"` (NO allow-scripts)
|
|
4
4
|
// iframe srcdoc, it reflows / theme-flips / device-sweeps with the browser's own layout engine and
|
|
@@ -19,7 +19,7 @@ export interface SerializeResult {
|
|
|
19
19
|
|
|
20
20
|
const STRIP_TAGS = new Set(['SCRIPT', 'NOSCRIPT'])
|
|
21
21
|
|
|
22
|
-
/** Absolutize relative url() in a sheet's cssText against that sheet's href (
|
|
22
|
+
/** Absolutize relative url() in a sheet's cssText against that sheet's href (consolidating
|
|
23
23
|
* rules into one <style> moves the url() base from each sheet to the srcdoc base). */
|
|
24
24
|
function absolutizeUrls(css: string, sheetHref: string | null): string {
|
|
25
25
|
if (!sheetHref) return css
|