@marver-design/marver 0.10.0 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -0
- package/README.md +2 -2
- package/dist/{build-Dd91jjIg.mjs → build-zUf_kwKp.mjs} +1 -1
- package/dist/cli.mjs +3 -3
- package/dist/{daemon-B_8BmmSJ.mjs → daemon-BpKBJZee.mjs} +1 -1
- package/dist/{dev-Dv0GlYQq.mjs → dev-D2PG6igI.mjs} +12 -2
- package/dist/{init-D2EQEbV6.mjs → init-DwV0_d9G.mjs} +7 -28
- package/dist/{ledger-BgA7nQoH.mjs → ledger-Bu0BjqIe.mjs} +1 -12
- package/dist/managed-HwHVNI3h.mjs +94 -0
- package/dist/{plugin-CgLVYTR-.mjs → plugin-C1sDewZ3.mjs} +107 -6
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/dist/{shot-BRgPaFCs.mjs → shot-D7I0MCLu.mjs} +139 -12
- package/package.json +1 -1
- package/src/client/content/img-lod.ts +6 -0
- package/src/client/frame-host/bridge.js +3 -1
- package/src/client/frame-host/main.tsx +4 -1
- package/src/client/shell/App.tsx +276 -31
- package/src/client/shell/Play.tsx +5 -4
- package/src/client/shell/canvas/FrameNode.tsx +45 -6
- package/src/client/shell/canvas/ready-watch.ts +28 -0
- package/src/client/shell/store.ts +83 -8
- package/src/client/shell/styles.css +33 -0
- package/templates/AGENTS-embedded.md +11 -4
- package/templates/AGENTS-studio.md +11 -4
- package/templates/instructions/boards.md +3 -1
- package/templates/instructions/jam.md +9 -0
|
@@ -34,6 +34,29 @@ function findChrome() {
|
|
|
34
34
|
return CHROMES.find((p) => existsSync(p)) ?? null;
|
|
35
35
|
}
|
|
36
36
|
const slug = (frameId) => frameId.replace(/\//g, "--");
|
|
37
|
+
const clamp = (n, lo, hi) => Math.min(hi, Math.max(lo, n));
|
|
38
|
+
function planShot(frame, viewports) {
|
|
39
|
+
const cw = typeof frame.contentWidth === "number" && Number.isFinite(frame.contentWidth) && frame.contentWidth > 0 ? frame.contentWidth : void 0;
|
|
40
|
+
const vpObj = frame.viewport ? viewports[frame.viewport] : void 0;
|
|
41
|
+
const fallback = viewports.mobile ?? {
|
|
42
|
+
width: 390,
|
|
43
|
+
height: 844
|
|
44
|
+
};
|
|
45
|
+
if (cw) {
|
|
46
|
+
const width = clamp(vpObj?.width ?? cw, 320, 1600);
|
|
47
|
+
return {
|
|
48
|
+
width,
|
|
49
|
+
initialHeight: Math.round(width * .75),
|
|
50
|
+
fullHeight: true
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const vp = vpObj ?? fallback;
|
|
54
|
+
return {
|
|
55
|
+
width: vp.width,
|
|
56
|
+
initialHeight: vp.height,
|
|
57
|
+
fullHeight: false
|
|
58
|
+
};
|
|
59
|
+
}
|
|
37
60
|
/** Resolve a frame from the manifest and screenshot it - the shared core behind BOTH
|
|
38
61
|
* transports (the HTTP /api/shot endpoint and the file-drop inbox), so they validate and
|
|
39
62
|
* name output identically. `origin` is the dev server's own base URL. */
|
|
@@ -52,24 +75,27 @@ async function shootFrame(opts) {
|
|
|
52
75
|
ok: false,
|
|
53
76
|
error: `unknown frame "${frameId}" - ids are in design/manifest.json`
|
|
54
77
|
};
|
|
55
|
-
const
|
|
56
|
-
width: 390,
|
|
57
|
-
height: 844
|
|
58
|
-
};
|
|
78
|
+
const plan = planShot(frame, viewports);
|
|
59
79
|
const shotsDir = join(root, "design", ".local", "shots");
|
|
60
80
|
mkdirSync(shotsDir, { recursive: true });
|
|
61
81
|
const rel = `design/.local/shots/${slug(frameId)}--${theme}.png`;
|
|
62
82
|
const result = await capture({
|
|
63
83
|
url: frame.kind === "html" ? `${origin}/${frame.file}?theme=${encodeURIComponent(theme)}` : `${origin}${ROUTE}/frame/?id=${encodeURIComponent(frameId)}&theme=${encodeURIComponent(theme)}`,
|
|
64
|
-
width:
|
|
65
|
-
height:
|
|
66
|
-
out: join(root, rel)
|
|
84
|
+
width: plan.width,
|
|
85
|
+
height: plan.initialHeight,
|
|
86
|
+
out: join(root, rel),
|
|
87
|
+
fullHeight: plan.fullHeight
|
|
67
88
|
});
|
|
68
89
|
return result.ok ? {
|
|
69
90
|
ok: true,
|
|
70
91
|
path: rel,
|
|
71
|
-
width:
|
|
72
|
-
height:
|
|
92
|
+
width: result.width,
|
|
93
|
+
height: result.height,
|
|
94
|
+
scale: result.scale,
|
|
95
|
+
...result.truncated ? {
|
|
96
|
+
truncated: true,
|
|
97
|
+
note: result.note
|
|
98
|
+
} : {}
|
|
73
99
|
} : result;
|
|
74
100
|
}
|
|
75
101
|
/** One capture at a time: shots are seconds apart at most, and a Chrome per concurrent
|
|
@@ -80,7 +106,7 @@ function capture(req) {
|
|
|
80
106
|
chain = run;
|
|
81
107
|
return run;
|
|
82
108
|
}
|
|
83
|
-
async function captureNow({ url, width, height, out, timeoutMs = 3e4 }) {
|
|
109
|
+
async function captureNow({ url, width, height, out, fullHeight = false, timeoutMs = 3e4 }) {
|
|
84
110
|
const bin = findChrome();
|
|
85
111
|
if (!bin) return {
|
|
86
112
|
ok: false,
|
|
@@ -172,6 +198,7 @@ async function captureNow({ url, width, height, out, timeoutMs = 3e4 }) {
|
|
|
172
198
|
rej(e);
|
|
173
199
|
}
|
|
174
200
|
});
|
|
201
|
+
const sendD = (method, params, ms = 1500) => Promise.race([send(method, params, sessionId), new Promise((_, rej) => setTimeout(() => rej(/* @__PURE__ */ new Error(`${method} timed out`)), ms))]);
|
|
175
202
|
watchdog = setTimeout(() => {
|
|
176
203
|
try {
|
|
177
204
|
chrome.kill("SIGKILL");
|
|
@@ -226,9 +253,109 @@ async function captureNow({ url, width, height, out, timeoutMs = 3e4 }) {
|
|
|
226
253
|
ok: false,
|
|
227
254
|
error: `the frame rendered an error - ${frameError}`
|
|
228
255
|
};
|
|
229
|
-
|
|
256
|
+
let capW = width, capH = height, scale = 2, truncated = false, note = "";
|
|
257
|
+
if (fullHeight) {
|
|
258
|
+
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
259
|
+
const measureH = async () => {
|
|
260
|
+
const v = (await sendD("Runtime.evaluate", {
|
|
261
|
+
expression: `Math.ceil((document.querySelector('.mv-doc')||document.getElementById('root')||document.body).getBoundingClientRect().height)`,
|
|
262
|
+
returnByValue: true
|
|
263
|
+
}).catch(() => null))?.result?.value;
|
|
264
|
+
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
265
|
+
};
|
|
266
|
+
const lodIdle = async () => {
|
|
267
|
+
const v = (await sendD("Runtime.evaluate", {
|
|
268
|
+
expression: `(typeof window.__mvLodBusy==='function')?window.__mvLodBusy():0`,
|
|
269
|
+
returnByValue: true
|
|
270
|
+
}).catch(() => null))?.result?.value;
|
|
271
|
+
return typeof v === "number" ? v === 0 : true;
|
|
272
|
+
};
|
|
273
|
+
let measured = 0;
|
|
274
|
+
const growFit = async (dsf, capCss, deadline) => {
|
|
275
|
+
let h = Math.max(height, 2e3), prev = -1;
|
|
276
|
+
for (let i = 0; i < 8 && Date.now() < deadline; i++) {
|
|
277
|
+
await sendD("Emulation.setDeviceMetricsOverride", {
|
|
278
|
+
width,
|
|
279
|
+
height: Math.min(h, capCss),
|
|
280
|
+
deviceScaleFactor: dsf,
|
|
281
|
+
mobile: false
|
|
282
|
+
}).catch(() => {});
|
|
283
|
+
if (Date.now() >= deadline) break;
|
|
284
|
+
await sendD("Runtime.evaluate", {
|
|
285
|
+
expression: "document.fonts.ready.then(()=>true)",
|
|
286
|
+
awaitPromise: true,
|
|
287
|
+
returnByValue: true
|
|
288
|
+
}).catch(() => null);
|
|
289
|
+
if (Date.now() >= deadline) break;
|
|
290
|
+
for (let j = 0; j < 10 && Date.now() < deadline && !await lodIdle(); j++) await wait(100);
|
|
291
|
+
if (Date.now() >= deadline) break;
|
|
292
|
+
await wait(150);
|
|
293
|
+
if (Date.now() >= deadline) break;
|
|
294
|
+
const m = await measureH();
|
|
295
|
+
if (m > 0) measured = m;
|
|
296
|
+
const capped = Math.min(h, capCss);
|
|
297
|
+
if (capped >= capCss) break;
|
|
298
|
+
if (m > 0 && m <= capped && m === prev) break;
|
|
299
|
+
prev = m;
|
|
300
|
+
h = Math.max(m, capped);
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
const CAP2 = 8192, CAP1 = 16384;
|
|
304
|
+
const settleDeadline = Date.now() + 6e3;
|
|
305
|
+
await growFit(2, CAP2, settleDeadline);
|
|
306
|
+
if (measured > CAP2) {
|
|
307
|
+
scale = 1;
|
|
308
|
+
await growFit(1, CAP1, settleDeadline);
|
|
309
|
+
}
|
|
310
|
+
const cap = scale === 2 ? CAP2 : CAP1;
|
|
311
|
+
capW = width;
|
|
312
|
+
if (measured > cap) {
|
|
313
|
+
capH = cap;
|
|
314
|
+
truncated = true;
|
|
315
|
+
note = `frame is ${measured}px tall; captured the top ${cap}px - split it or reduce its height`;
|
|
316
|
+
} else capH = clamp(measured || height, 80, cap);
|
|
317
|
+
await sendD("Emulation.setDeviceMetricsOverride", {
|
|
318
|
+
width: capW,
|
|
319
|
+
height: capH,
|
|
320
|
+
deviceScaleFactor: scale,
|
|
321
|
+
mobile: false
|
|
322
|
+
}, 5e3);
|
|
323
|
+
if (Date.now() < settleDeadline) {
|
|
324
|
+
await sendD("Runtime.evaluate", {
|
|
325
|
+
expression: `window.postMessage({type:'sh:camera',moving:false,scale:1}, location.origin)`,
|
|
326
|
+
returnByValue: true
|
|
327
|
+
}).catch(() => null);
|
|
328
|
+
await wait(300);
|
|
329
|
+
for (let j = 0; j < 20 && Date.now() < settleDeadline && !await lodIdle(); j++) await wait(100);
|
|
330
|
+
await sendD("Runtime.evaluate", {
|
|
331
|
+
expression: "document.fonts.ready.then(()=>true)",
|
|
332
|
+
awaitPromise: true,
|
|
333
|
+
returnByValue: true
|
|
334
|
+
}).catch(() => null);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
const shot = await sendD("Page.captureScreenshot", {
|
|
338
|
+
format: "png",
|
|
339
|
+
captureBeyondViewport: true,
|
|
340
|
+
clip: {
|
|
341
|
+
x: 0,
|
|
342
|
+
y: 0,
|
|
343
|
+
width: capW,
|
|
344
|
+
height: capH,
|
|
345
|
+
scale: 1
|
|
346
|
+
}
|
|
347
|
+
}, 15e3);
|
|
230
348
|
writeFileSync(out, Buffer.from(String(shot.data), "base64"));
|
|
231
|
-
return {
|
|
349
|
+
return {
|
|
350
|
+
ok: true,
|
|
351
|
+
width: capW,
|
|
352
|
+
height: capH,
|
|
353
|
+
scale,
|
|
354
|
+
...truncated ? {
|
|
355
|
+
truncated: true,
|
|
356
|
+
note
|
|
357
|
+
} : {}
|
|
358
|
+
};
|
|
232
359
|
} catch (err) {
|
|
233
360
|
return {
|
|
234
361
|
ok: false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marver-design/marver",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.2",
|
|
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 - comment @marver and your own coding agent does the work. The tool ships no AI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -35,6 +35,12 @@ const schedule = (job: () => Promise<void>): void => {
|
|
|
35
35
|
pump()
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
// Decode-idle signal for the headless shot: 0 means no decode is running or queued. The shot
|
|
39
|
+
// (src/server/shot.ts) polls this to know each image's first decode has settled - which pins
|
|
40
|
+
// its aspect-ratio and so stabilizes the frame's measured height - before it measures and
|
|
41
|
+
// captures. Harmless and 0 when there are no LOD images (nothing ever decodes).
|
|
42
|
+
if (typeof window !== 'undefined') (window as { __mvLodBusy?: () => number }).__mvLodBusy = () => active + q.length
|
|
43
|
+
|
|
38
44
|
interface Item { canvas: HTMLCanvasElement; src: string; bucket: number; token: number }
|
|
39
45
|
const items = new Set<Item>()
|
|
40
46
|
let scale = 0.2 // overview default until the shell primes the settled scale on frame-ready
|
|
@@ -73,7 +73,9 @@ window.addEventListener('message', (e) => {
|
|
|
73
73
|
})
|
|
74
74
|
|
|
75
75
|
if (isHtmlFrame) {
|
|
76
|
-
|
|
76
|
+
// stamp the URL revision (see main.tsx) so the shell can drop a ready from a superseded document
|
|
77
|
+
const gen = new URLSearchParams(location.search).get('r') ?? ''
|
|
78
|
+
const ready = () => post({ type: 'sh:ready', id, gen })
|
|
77
79
|
document.readyState === 'loading' ? addEventListener('DOMContentLoaded', ready) : ready()
|
|
78
80
|
}
|
|
79
81
|
|
|
@@ -77,7 +77,10 @@ async function boot() {
|
|
|
77
77
|
for (const W of wrappers.reverse()) if (W != null) tree = createElement(W, null, tree)
|
|
78
78
|
|
|
79
79
|
createRoot(document.getElementById('root')!).render(createElement(Boundary, null, tree))
|
|
80
|
-
|
|
80
|
+
// stamp the URL revision so the shell can drop a ready queued by a superseded document (one it
|
|
81
|
+
// auto-renavigated past) - a WindowProxy survives navigation, so a stale ready could otherwise
|
|
82
|
+
// mark a reloading frame ready. Mirrors the sh:measure generation guard.
|
|
83
|
+
post({ type: 'sh:ready', id, gen: params.get('r') ?? '', meta: frameMod.meta && typeof frameMod.meta === 'object' ? frameMod.meta : undefined })
|
|
81
84
|
} catch (err) {
|
|
82
85
|
fail((err as Error).message)
|
|
83
86
|
}
|