mockpit 2.0.0 → 3.0.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/README.md +60 -212
- package/bin/demoData.js +2 -2
- package/bin/initDesign.js +6 -6
- package/bin/mockpit.js +407 -187
- package/bin/poster.js +377 -0
- package/dist/mcp/server.js +80 -24
- package/dist/server/answerAdditions.d.ts +7 -0
- package/dist/server/answerAdditions.js +76 -0
- package/dist/server/apiViews.d.ts +289 -52
- package/dist/server/apiViews.js +189 -81
- package/dist/server/app.d.ts +7 -2
- package/dist/server/app.js +2845 -832
- package/dist/server/appIcon.js +15 -5
- package/dist/server/appIconPng.js +3 -3
- package/dist/server/askNudges.js +1 -1
- package/dist/server/cdnInline.d.ts +8 -0
- package/dist/server/cdnInline.js +638 -0
- package/dist/server/codeWorker.js +1 -1
- package/dist/server/decisionList.d.ts +45 -0
- package/dist/server/decisionList.js +302 -0
- package/dist/server/decisions.d.ts +4 -4
- package/dist/server/decisions.js +9 -9
- package/dist/server/designGuide.d.ts +5 -5
- package/dist/server/designGuide.js +128 -67
- package/dist/server/embeds.d.ts +3 -1
- package/dist/server/embeds.js +31 -6
- package/dist/server/eventLog.d.ts +2 -2
- package/dist/server/eventLog.js +11 -11
- package/dist/server/events.d.ts +9 -6
- package/dist/server/feedbackBatch.d.ts +35 -13
- package/dist/server/feedbackBatch.js +111 -47
- package/dist/server/fills.d.ts +4 -0
- package/dist/server/fills.js +55 -1
- package/dist/server/handoff.d.ts +4 -1
- package/dist/server/handoff.js +8 -2
- package/dist/server/icons.js +1 -1
- package/dist/server/index.js +39 -10
- package/dist/server/jump.d.ts +21 -0
- package/dist/server/jump.js +175 -0
- package/dist/server/knobs.d.ts +3 -3
- package/dist/server/knobs.js +6 -6
- package/dist/server/mcpHttp.d.ts +5 -1
- package/dist/server/mcpHttp.js +16 -11
- package/dist/server/mcpSpec.js +118 -46
- package/dist/server/parts.js +9 -1
- package/dist/server/run.d.ts +1 -1
- package/dist/server/run.js +25 -16
- package/dist/server/runApi.d.ts +3 -3
- package/dist/server/runApi.js +25 -19
- package/dist/server/sqlStore.d.ts +112 -96
- package/dist/server/sqlStore.js +906 -172
- package/dist/server/standings.d.ts +25 -0
- package/dist/server/standings.js +113 -0
- package/dist/server/surfacePage.d.ts +6 -9
- package/dist/server/surfacePage.js +361 -86
- package/dist/server/types.d.ts +226 -72
- package/dist/server/types.js +73 -78
- package/extensions/mockpit.js +89 -72
- package/guide/AGENT_SETUP.md +13 -1
- package/guide/packs/design-craft.md +2 -3
- package/guide/topics/asks.md +106 -62
- package/guide/topics/feedback.md +126 -119
- package/guide/topics/html.md +17 -24
- package/guide/topics/http.md +41 -41
- package/guide/topics/knobs.md +7 -10
- package/guide/topics/scripts.md +22 -33
- package/guide/topics/surfaces.md +39 -48
- package/opencode/mockpit.js +120 -0
- package/opencode/package.json +8 -0
- package/package.json +4 -3
- package/skills/launch-video/SKILL.md +11 -11
- package/skills/mockpit/SKILL.md +26 -22
- package/viewer/dist/index.html +9 -9
|
@@ -2,6 +2,7 @@ import { scanFills, slotCss } from "./fills.js";
|
|
|
2
2
|
import { CDN_ALLOWLIST, checkCdnUrl } from "./cdn.js";
|
|
3
3
|
import { CORE_CSS, KITS, kitAssets } from "./kits.js";
|
|
4
4
|
import { expandIcons } from "./icons.js";
|
|
5
|
+
import { PARAGRAPH_TEXT_MAX, PARAGRAPHS_MAX } from "./types.js";
|
|
5
6
|
import { schemeCss, themeById, tokenThemeCss, viewerThemeCss, } from "./themes.js";
|
|
6
7
|
// The kit's two custom SVG accent ramps (teal, coral) aren't in the theme
|
|
7
8
|
// palette, so they carry their own light/dark values. Like the theme tokens
|
|
@@ -34,17 +35,11 @@ const cdns = CDN_ALLOWLIST.join(" ");
|
|
|
34
35
|
// (served at <origin>/a/:id) embed by URL. It is needed because the iframe runs
|
|
35
36
|
// at an opaque origin (sandbox without allow-same-origin), so `'self'` matches
|
|
36
37
|
// nothing, and a local http origin isn't covered by the `https:` source.
|
|
37
|
-
// `${origin}/asset/` is the fixed, content-hashed path the bridge script and the
|
|
38
|
-
// static stylesheets are served from (see registerAsset). It is deliberately a
|
|
39
|
-
// single directory of server-authored, workspace-data-free files: nothing an
|
|
40
|
-
// agent or a user can write is ever reachable under it, so widening script-src
|
|
41
|
-
// to it grants the sandbox no capability beyond running our own bridge.
|
|
42
38
|
function buildCsp(origin) {
|
|
43
|
-
const assets = `${origin}${STATIC_ASSET_PREFIX}`;
|
|
44
39
|
return [
|
|
45
40
|
`default-src 'none'`,
|
|
46
|
-
`script-src 'unsafe-inline' ${
|
|
47
|
-
`style-src 'unsafe-inline' ${
|
|
41
|
+
`script-src 'unsafe-inline' ${cdns}`,
|
|
42
|
+
`style-src 'unsafe-inline' ${cdns}`,
|
|
48
43
|
`font-src ${cdns} data:`,
|
|
49
44
|
`img-src https: data: blob: ${origin}`,
|
|
50
45
|
`connect-src ${cdns}`,
|
|
@@ -264,6 +259,140 @@ if (window.ResizeObserver) {
|
|
|
264
259
|
if (document.body) window.__ssRO.observe(document.body);
|
|
265
260
|
}
|
|
266
261
|
`;
|
|
262
|
+
// Freeze: a frame shown small (a wall, a grid, a cover, an option's picture) stops animating
|
|
263
|
+
// once its page has settled, so a screenful of them costs nothing while they're looked at.
|
|
264
|
+
// Opt-in by the fragment `#small`, so it's the same document and the same cache entry as the
|
|
265
|
+
// page on Stage, which never has it. It runs first in <head>, ahead of the page's own
|
|
266
|
+
// scripts, and asks nothing of them. Pinned down with pages built to trip it.
|
|
267
|
+
//
|
|
268
|
+
// The danger is a picture that freezes half-drawn, so stopping is the last thing that happens
|
|
269
|
+
// to a page, and each way of drawing is handled for what it is:
|
|
270
|
+
//
|
|
271
|
+
// - Settled: the page has loaded, and for QUIET_MS the DOM hasn't changed, no animation
|
|
272
|
+
// with an end is still running (its delay counts), and no frame has been asked for.
|
|
273
|
+
// An animation that loops forever doesn't hold this up: it never finishes.
|
|
274
|
+
// - Or CAP_MS have gone by. A frame loop can't be told apart from a page drawing in slowly
|
|
275
|
+
// (it may draw on a canvas or in a shadow root, neither of which the DOM check sees), so
|
|
276
|
+
// every loop gets that long, and the cap is where its cost ends. A draw-in longer than
|
|
277
|
+
// the cap is the one case that freezes part way.
|
|
278
|
+
// - Time here is time the frame was rendered: this clock ticks on animation frames, which a
|
|
279
|
+
// hidden or offscreen frame isn't given. A page that hasn't been seen hasn't had its
|
|
280
|
+
// chance to draw, so it doesn't get frozen before it has.
|
|
281
|
+
// - At the freeze, what has an end is finished rather than paused, since its end is what
|
|
282
|
+
// the page means to show. What loops is paused back at its start, its resting state in
|
|
283
|
+
// nearly every loop, and never midway through a fade. New animations get the same.
|
|
284
|
+
// - Frames stop. A frame loop's next frame is held and never handed out. A timer or event
|
|
285
|
+
// asking for a frame is the page doing something new, not a loop, so it thaws the page
|
|
286
|
+
// and settling starts over, within RUN_MAX_MS of rendered time in all, so a timer can't
|
|
287
|
+
// keep it awake. Timers themselves keep running: drawing by timer completes however late.
|
|
288
|
+
// Past that allowance only HELD_MAX requests are kept for a later thaw: a page asking by
|
|
289
|
+
// timer would otherwise pile them up for as long as it stays open.
|
|
290
|
+
// - New knob values always thaw, with a new allowance: the picture must follow them.
|
|
291
|
+
export const FREEZE_JS = `
|
|
292
|
+
(function () {
|
|
293
|
+
if (location.hash !== '#small' || !document.getAnimations) return;
|
|
294
|
+
var QUIET_MS = 800, CAP_MS = 10000, LOAD_CAP_MS = 15000, MAX_STEP_MS = 100, RUN_MAX_MS = 30000;
|
|
295
|
+
var HELD_MAX = 32;
|
|
296
|
+
var raf = window.requestAnimationFrame.bind(window);
|
|
297
|
+
var caf = window.cancelAnimationFrame.bind(window);
|
|
298
|
+
var asked = new Map(), nextId = 0;
|
|
299
|
+
var frozen = false, loaded = false;
|
|
300
|
+
var run = 0, last = 0, settleFrom = 0, active = 0, runUntil = RUN_MAX_MS;
|
|
301
|
+
var paused = [], sweepQueued = false;
|
|
302
|
+
window.addEventListener('load', function () { loaded = true; settleFrom = run; active = run; });
|
|
303
|
+
|
|
304
|
+
function fire(id, t) {
|
|
305
|
+
var entry = asked.get(id);
|
|
306
|
+
if (!entry) return;
|
|
307
|
+
entry.native = 0;
|
|
308
|
+
if (frozen) return;
|
|
309
|
+
asked.delete(id);
|
|
310
|
+
entry.cb(t);
|
|
311
|
+
}
|
|
312
|
+
function queue(id, entry) {
|
|
313
|
+
entry.native = raf(function (t) { fire(id, t); });
|
|
314
|
+
}
|
|
315
|
+
window.requestAnimationFrame = function (cb) {
|
|
316
|
+
if (frozen && run < runUntil) thaw();
|
|
317
|
+
var id = ++nextId, entry = { cb: cb, native: 0 };
|
|
318
|
+
if (frozen && asked.size >= HELD_MAX) return id;
|
|
319
|
+
asked.set(id, entry);
|
|
320
|
+
active = run;
|
|
321
|
+
if (!frozen) queue(id, entry);
|
|
322
|
+
return id;
|
|
323
|
+
};
|
|
324
|
+
window.cancelAnimationFrame = function (id) {
|
|
325
|
+
var entry = asked.get(id);
|
|
326
|
+
if (!entry) return;
|
|
327
|
+
if (entry.native) caf(entry.native);
|
|
328
|
+
asked.delete(id);
|
|
329
|
+
};
|
|
330
|
+
if (window.webkitRequestAnimationFrame) {
|
|
331
|
+
window.webkitRequestAnimationFrame = window.requestAnimationFrame;
|
|
332
|
+
window.webkitCancelAnimationFrame = window.cancelAnimationFrame;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function own(a) { return a.timeline === document.timeline && a.effect; }
|
|
336
|
+
function ends(a) { return isFinite(a.effect.getComputedTiming().endTime); }
|
|
337
|
+
function stillDrawing() {
|
|
338
|
+
return document.getAnimations().some(function (a) {
|
|
339
|
+
return own(a) && a.playState === 'running' && ends(a);
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
function sweep() {
|
|
343
|
+
sweepQueued = false;
|
|
344
|
+
if (!frozen) return;
|
|
345
|
+
document.getAnimations().forEach(function (a) {
|
|
346
|
+
if (!own(a)) return;
|
|
347
|
+
try {
|
|
348
|
+
if (ends(a)) a.finish();
|
|
349
|
+
else if (a.playState !== 'paused') { a.pause(); a.currentTime = 0; paused.push(a); }
|
|
350
|
+
} catch (e) {}
|
|
351
|
+
});
|
|
352
|
+
Array.prototype.forEach.call(document.querySelectorAll('svg'), function (svg) {
|
|
353
|
+
if (svg.pauseAnimations) svg.pauseAnimations();
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
function queueSweep() {
|
|
357
|
+
if (frozen && !sweepQueued) { sweepQueued = true; setTimeout(sweep, 0); }
|
|
358
|
+
}
|
|
359
|
+
function freeze() { frozen = true; sweep(); }
|
|
360
|
+
function thaw() {
|
|
361
|
+
frozen = false;
|
|
362
|
+
active = settleFrom = run;
|
|
363
|
+
paused.splice(0).forEach(function (a) { try { a.play(); } catch (e) {} });
|
|
364
|
+
Array.prototype.forEach.call(document.querySelectorAll('svg'), function (svg) {
|
|
365
|
+
if (svg.unpauseAnimations) svg.unpauseAnimations();
|
|
366
|
+
});
|
|
367
|
+
asked.forEach(function (entry, id) { if (!entry.native) queue(id, entry); });
|
|
368
|
+
last = 0;
|
|
369
|
+
raf(tick);
|
|
370
|
+
}
|
|
371
|
+
function settled() {
|
|
372
|
+
return run - active >= QUIET_MS && !stillDrawing();
|
|
373
|
+
}
|
|
374
|
+
function tick(t) {
|
|
375
|
+
if (frozen) return;
|
|
376
|
+
run += last ? Math.min(t - last, MAX_STEP_MS) : 0;
|
|
377
|
+
last = t;
|
|
378
|
+
if (loaded ? settled() || run - settleFrom >= CAP_MS : run >= LOAD_CAP_MS) freeze();
|
|
379
|
+
else raf(tick);
|
|
380
|
+
}
|
|
381
|
+
new MutationObserver(function () {
|
|
382
|
+
active = run;
|
|
383
|
+
queueSweep();
|
|
384
|
+
}).observe(document, { subtree: true, childList: true, attributes: true, characterData: true });
|
|
385
|
+
document.addEventListener('animationstart', queueSweep, true);
|
|
386
|
+
document.addEventListener('transitionstart', queueSweep, true);
|
|
387
|
+
window.addEventListener('message', function (e) {
|
|
388
|
+
var d = e.data;
|
|
389
|
+
if (e.source !== window.parent || !d || d.__mockpit !== true || d.type !== 'knobs') return;
|
|
390
|
+
runUntil = run + RUN_MAX_MS;
|
|
391
|
+
if (frozen) thaw();
|
|
392
|
+
});
|
|
393
|
+
raf(tick);
|
|
394
|
+
})();
|
|
395
|
+
`;
|
|
267
396
|
// Hit test: the viewer's marker overlay lives in the trusted origin ABOVE the
|
|
268
397
|
// sandboxed frame, so when the operator drops a pin it knows the normalized
|
|
269
398
|
// point but nothing about what sits under it. This answers that question over
|
|
@@ -378,7 +507,7 @@ export const HIT_TEST_JS = `
|
|
|
378
507
|
// goto {version, to} a click on [data-goto] while the host
|
|
379
508
|
// has Try on (PARTS_JS); to is the
|
|
380
509
|
// attribute as written ("next", "prev",
|
|
381
|
-
// "<
|
|
510
|
+
// "<design>:<state>"), ≤200 chars. The host
|
|
382
511
|
// honours it only while Try is on, from
|
|
383
512
|
// the frame on stage at its version, and
|
|
384
513
|
// only to a screen of this project:
|
|
@@ -387,6 +516,14 @@ export const HIT_TEST_JS = `
|
|
|
387
516
|
// a drag never reports
|
|
388
517
|
// ready {version} the knob runtime is listening (KNOBS_JS,
|
|
389
518
|
// from <head>); values can be sent now
|
|
519
|
+
// paragraphs {version, blocks, width} a written surface's blocks (PARAGRAPHS_JS),
|
|
520
|
+
// only in a document opened with #read;
|
|
521
|
+
// blocks[i] = {i, kind, text, box:{x,y,w,h}}: place in the document, tag, clipped
|
|
522
|
+
// text, document px. Sent on load, on 50/150/400/1000ms timers and on any layout
|
|
523
|
+
// change; never twice alike. `version` as on a parts report.
|
|
524
|
+
// paragraph-tap {version, i} a click or tap on block i, which only opens
|
|
525
|
+
// the host's comment field; the host reads
|
|
526
|
+
// the text from its own last report
|
|
390
527
|
// pinch {phase, a:{x,y}, b:{x,y}} two fingers on the page, in viewport px
|
|
391
528
|
// zoom {x, y, dy} ⌘/Ctrl+scroll or a trackpad pinch, in viewport px
|
|
392
529
|
// (PARTS_JS); phase start|move|end. The
|
|
@@ -408,8 +545,10 @@ export const HIT_TEST_JS = `
|
|
|
408
545
|
// try {on} Try is on or off: while on, [data-goto] elements are
|
|
409
546
|
// outlined and a click on one reports goto instead
|
|
410
547
|
// of reaching the page; a new document starts off
|
|
548
|
+
// paragraph-marks {marked:[i], on:i|null} tint these blocks of a #read document: the
|
|
549
|
+
// ones holding comments, and the one being commented on
|
|
411
550
|
//
|
|
412
|
-
// Knobs (`/s/:id?k=`, validated by the route against the
|
|
551
|
+
// Knobs (`/s/:id?k=`, validated by the route against the design's declared knobs,
|
|
413
552
|
// and the `knobs` command): for each path, dots become "-" in names, and
|
|
414
553
|
// --k-<path> on <html style>: numbers as-is, booleans 1/0, strings
|
|
415
554
|
// raw when they are plain CSS tokens, else a quoted CSS
|
|
@@ -687,6 +826,161 @@ export const PARTS_JS = `
|
|
|
687
826
|
});
|
|
688
827
|
})();
|
|
689
828
|
`;
|
|
829
|
+
// A written surface read as a document. Identity and geometry leave the frame as
|
|
830
|
+
// data, the way parts do; the trusted viewer draws its pins from those numbers and
|
|
831
|
+
// shows the text as text nodes. Opt-in by the fragment `#read`, so the same document
|
|
832
|
+
// pictured on a card or stacked on the stage stays inert: a fragment reaches no
|
|
833
|
+
// server and no render cache, and it is read before first paint, so the reading type
|
|
834
|
+
// never flashes. A rich document is not scrollable in this mode: the host sizes the
|
|
835
|
+
// frame to it and the reader is the page's one scroll.
|
|
836
|
+
const READ_CSS = `
|
|
837
|
+
html.mockpit-read,html.mockpit-read body{overflow:hidden}
|
|
838
|
+
html.mockpit-read body{display:flow-root;box-sizing:border-box;padding:2px 28px 6px;font-size:16px;line-height:1.65;text-wrap:pretty}
|
|
839
|
+
html.mockpit-read :is(p,ul,ol,blockquote,table){margin:.75em 0}
|
|
840
|
+
html.mockpit-read table{display:block;max-width:100%;overflow-x:auto}
|
|
841
|
+
html.mockpit-read pre{max-height:none}
|
|
842
|
+
html.mockpit-read [data-mp]{border-radius:6px;cursor:pointer;transition:background-color .12s ease-out}
|
|
843
|
+
html.mockpit-read .mockpit-p-hov{background-color:var(--hover);box-shadow:0 0 0 4px var(--hover)}
|
|
844
|
+
html.mockpit-read .mockpit-p-mark{--t:color-mix(in srgb,var(--accent) 12%,transparent);background-color:var(--t);box-shadow:0 0 0 4px var(--t)}
|
|
845
|
+
html.mockpit-read .mockpit-p-on{--t:color-mix(in srgb,var(--accent) 24%,transparent);background-color:var(--t);box-shadow:0 0 0 4px var(--t)}
|
|
846
|
+
`;
|
|
847
|
+
export const PARAGRAPHS_JS = `
|
|
848
|
+
(function () {
|
|
849
|
+
if (window.parent === window || location.hash !== '#read') return;
|
|
850
|
+
var MAX_BLOCKS = ${PARAGRAPHS_MAX};
|
|
851
|
+
var MAX_TEXT = ${PARAGRAPH_TEXT_MAX};
|
|
852
|
+
// Outermost blocks only: a paragraph in a quote or a list item belongs to it. A
|
|
853
|
+
// list inside a list item is the one exception, so a sub-item can be commented on.
|
|
854
|
+
var BLOCKS = 'h1,h2,h3,h4,h5,h6,p,li,blockquote,pre,table,figure,dl';
|
|
855
|
+
var html = document.documentElement;
|
|
856
|
+
var version = Number(html.getAttribute('data-mockpit-version')) || null;
|
|
857
|
+
html.classList.add('mockpit-read');
|
|
858
|
+
var sheet = document.createElement('style');
|
|
859
|
+
sheet.textContent = ${JSON.stringify(READ_CSS)};
|
|
860
|
+
document.head.appendChild(sheet);
|
|
861
|
+
|
|
862
|
+
function clean(t) { return String(t || '').replace(/\\s+/g, ' ').trim().slice(0, MAX_TEXT); }
|
|
863
|
+
function textOf(el) {
|
|
864
|
+
var from = el;
|
|
865
|
+
if (el.tagName === 'LI' && el.querySelector('ul,ol')) {
|
|
866
|
+
from = el.cloneNode(true);
|
|
867
|
+
var inner = from.querySelectorAll('ul,ol');
|
|
868
|
+
for (var i = 0; i < inner.length; i++) inner[i].remove();
|
|
869
|
+
}
|
|
870
|
+
var t = clean(from.textContent);
|
|
871
|
+
if (t) return t;
|
|
872
|
+
var pic = el.querySelector('img');
|
|
873
|
+
return pic ? clean(pic.getAttribute('alt')) || 'image' : '';
|
|
874
|
+
}
|
|
875
|
+
function found() {
|
|
876
|
+
// A mermaid page: the whole diagram is one block, once it has drawn.
|
|
877
|
+
var diagram = document.getElementById('m');
|
|
878
|
+
if (diagram) return diagram.firstChild ? [{ el: diagram, kind: 'diagram' }] : [];
|
|
879
|
+
var all = document.querySelectorAll(BLOCKS);
|
|
880
|
+
var out = [];
|
|
881
|
+
for (var i = 0; i < all.length && out.length < MAX_BLOCKS; i++) {
|
|
882
|
+
var el = all[i];
|
|
883
|
+
var up = el.parentElement && el.parentElement.closest(BLOCKS);
|
|
884
|
+
if (up && !(el.tagName === 'LI' && up.tagName === 'LI')) continue;
|
|
885
|
+
out.push({ el: el, kind: el.tagName.toLowerCase() });
|
|
886
|
+
}
|
|
887
|
+
return out.length || !document.body ? out : [{ el: document.body, kind: 'document' }];
|
|
888
|
+
}
|
|
889
|
+
function collect() {
|
|
890
|
+
var sx = window.scrollX || 0, sy = window.scrollY || 0;
|
|
891
|
+
var list = found();
|
|
892
|
+
var blocks = [];
|
|
893
|
+
for (var i = 0; i < list.length; i++) {
|
|
894
|
+
var el = list[i].el;
|
|
895
|
+
var t = textOf(el);
|
|
896
|
+
if (!t) continue;
|
|
897
|
+
el.setAttribute('data-mp', String(blocks.length));
|
|
898
|
+
var r = el.getBoundingClientRect();
|
|
899
|
+
blocks.push({
|
|
900
|
+
i: blocks.length,
|
|
901
|
+
kind: list[i].kind,
|
|
902
|
+
text: t,
|
|
903
|
+
box: { x: r.left + sx, y: r.top + sy, w: r.width, h: r.height },
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
return { version: version, blocks: blocks, width: innerWidth };
|
|
907
|
+
}
|
|
908
|
+
var last = '';
|
|
909
|
+
var timer = 0;
|
|
910
|
+
function schedule() {
|
|
911
|
+
if (timer) return;
|
|
912
|
+
timer = setTimeout(function () {
|
|
913
|
+
timer = 0;
|
|
914
|
+
var m = collect();
|
|
915
|
+
var key = JSON.stringify(m);
|
|
916
|
+
if (key === last) return;
|
|
917
|
+
last = key;
|
|
918
|
+
m.__mockpit = true;
|
|
919
|
+
m.type = 'paragraphs';
|
|
920
|
+
parent.postMessage(m, '*');
|
|
921
|
+
}, 0);
|
|
922
|
+
}
|
|
923
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
924
|
+
var ro = new ResizeObserver(schedule);
|
|
925
|
+
ro.observe(html);
|
|
926
|
+
if (document.body) ro.observe(document.body);
|
|
927
|
+
}
|
|
928
|
+
// No attributes: this script writes those, and only content arriving moves blocks.
|
|
929
|
+
new MutationObserver(schedule).observe(html, { subtree: true, childList: true, characterData: true });
|
|
930
|
+
window.addEventListener('resize', schedule);
|
|
931
|
+
if (document.fonts && document.fonts.ready) document.fonts.ready.then(schedule);
|
|
932
|
+
document.addEventListener('load', schedule, true);
|
|
933
|
+
function settle() {
|
|
934
|
+
schedule();
|
|
935
|
+
[50, 150, 400, 1000].forEach(function (ms) { setTimeout(schedule, ms); });
|
|
936
|
+
}
|
|
937
|
+
if (document.readyState === 'complete') settle();
|
|
938
|
+
else window.addEventListener('load', settle);
|
|
939
|
+
|
|
940
|
+
var hovered = null;
|
|
941
|
+
function hover(el) {
|
|
942
|
+
if (el === hovered) return;
|
|
943
|
+
if (hovered) hovered.classList.remove('mockpit-p-hov');
|
|
944
|
+
hovered = el;
|
|
945
|
+
if (el) el.classList.add('mockpit-p-hov');
|
|
946
|
+
}
|
|
947
|
+
document.addEventListener('pointerover', function (e) {
|
|
948
|
+
if (e.pointerType !== 'mouse') return;
|
|
949
|
+
hover(e.target && e.target.closest ? e.target.closest('[data-mp]') : null);
|
|
950
|
+
}, true);
|
|
951
|
+
html.addEventListener('pointerleave', function () { hover(null); });
|
|
952
|
+
document.addEventListener('click', function (e) {
|
|
953
|
+
var t = e.target;
|
|
954
|
+
if (!t || !t.closest || t.closest('a[href],button,summary,input,select,textarea,label')) return;
|
|
955
|
+
// Selecting text to copy it is reading, not commenting.
|
|
956
|
+
var picked = window.getSelection && window.getSelection();
|
|
957
|
+
if (picked && !picked.isCollapsed) return;
|
|
958
|
+
var block = t.closest('[data-mp]');
|
|
959
|
+
if (!block) return;
|
|
960
|
+
parent.postMessage({
|
|
961
|
+
__mockpit: true,
|
|
962
|
+
type: 'paragraph-tap',
|
|
963
|
+
version: version,
|
|
964
|
+
i: Number(block.getAttribute('data-mp')),
|
|
965
|
+
}, '*');
|
|
966
|
+
});
|
|
967
|
+
function tint(cls, list) {
|
|
968
|
+
var on = document.querySelectorAll('.' + cls);
|
|
969
|
+
for (var i = 0; i < on.length; i++) on[i].classList.remove(cls);
|
|
970
|
+
for (var k = 0; k < list.length && k < MAX_BLOCKS; k++) {
|
|
971
|
+
var el = document.querySelector('[data-mp="' + Number(list[k]) + '"]');
|
|
972
|
+
if (el) el.classList.add(cls);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
window.addEventListener('message', function (e) {
|
|
976
|
+
if (e.source !== parent) return;
|
|
977
|
+
var d = e.data;
|
|
978
|
+
if (!d || d.__mockpit !== true || d.type !== 'paragraph-marks') return;
|
|
979
|
+
tint('mockpit-p-mark', Array.isArray(d.marked) ? d.marked : []);
|
|
980
|
+
tint('mockpit-p-on', d.on === null || d.on === undefined ? [] : [d.on]);
|
|
981
|
+
});
|
|
982
|
+
})();
|
|
983
|
+
`;
|
|
690
984
|
// Knobs: the `?k=` values are already on <html> (see knobPreamble); this binds
|
|
691
985
|
// text, announces them, and applies the host's live `knobs` commands.
|
|
692
986
|
export const KNOBS_JS = `
|
|
@@ -840,55 +1134,36 @@ const TAILWIND_CDN = "https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4";
|
|
|
840
1134
|
// declares them per set and draws in currentColor.
|
|
841
1135
|
const ICON_CSS = `.icon{width:1em;height:1em;flex:none;vertical-align:-0.125em}`;
|
|
842
1136
|
// ---------------------------------------------------------------------------
|
|
843
|
-
//
|
|
1137
|
+
// Mockpit's own scripts and stylesheets, inlined in every surface document
|
|
844
1138
|
// ---------------------------------------------------------------------------
|
|
845
1139
|
//
|
|
846
|
-
//
|
|
847
|
-
//
|
|
848
|
-
//
|
|
849
|
-
//
|
|
850
|
-
//
|
|
1140
|
+
// These were once served from content-hashed URLs so a browser would fetch each
|
|
1141
|
+
// file once. It doesn't: a frame's subresources are partitioned by its opaque
|
|
1142
|
+
// origin, so in Chrome nothing a frame fetches is cached or shared. Measured with
|
|
1143
|
+
// 8 frames, every file was fetched 8 times on first load and on every reload,
|
|
1144
|
+
// while the document itself came from disk cache. Bytes in the document cost
|
|
1145
|
+
// nothing past that document's one fetch, so that's where they live.
|
|
851
1146
|
//
|
|
852
|
-
//
|
|
853
|
-
//
|
|
854
|
-
//
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
const
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
// Every path is registered at module load, so a document served from a
|
|
869
|
-
// long-lived cache after a restart always finds the asset it references.
|
|
870
|
-
export const STATIC_ASSET_PREFIX = "/asset/";
|
|
871
|
-
const STATIC_ASSETS = new Map();
|
|
872
|
-
function registerAsset(name, ext, body) {
|
|
873
|
-
const path = `${STATIC_ASSET_PREFIX}${name}.${fnv1a(body)}.${ext}`;
|
|
874
|
-
STATIC_ASSETS.set(path, {
|
|
875
|
-
path,
|
|
876
|
-
contentType: ext === "js" ? "text/javascript; charset=utf-8" : "text/css; charset=utf-8",
|
|
877
|
-
body,
|
|
878
|
-
});
|
|
879
|
-
return path;
|
|
880
|
-
}
|
|
881
|
-
export const staticAsset = (path) => STATIC_ASSETS.get(path) ?? null;
|
|
882
|
-
const BRIDGE_PATH = registerAsset("bridge", "js", BRIDGE_JS);
|
|
883
|
-
const HIT_TEST_PATH = registerAsset("hit-test", "js", HIT_TEST_JS);
|
|
884
|
-
const PARTS_PATH = registerAsset("parts", "js", PARTS_JS);
|
|
885
|
-
const KNOBS_PATH = registerAsset("knobs", "js", KNOBS_JS);
|
|
1147
|
+
// Comments and indentation in these scripts are for this file's reader; every
|
|
1148
|
+
// frame would pay for them, so they're dropped once at load. Safe because no
|
|
1149
|
+
// script here holds a multi-line string.
|
|
1150
|
+
const compactJs = (js) => js
|
|
1151
|
+
.split("\n")
|
|
1152
|
+
.map((line) => line.trim())
|
|
1153
|
+
.filter((line) => line && !line.startsWith("//"))
|
|
1154
|
+
.join("\n");
|
|
1155
|
+
const inlineScript = (js) => `<script>${compactJs(js)}</script>`;
|
|
1156
|
+
const inlineStyle = (css) => `<style>${css}</style>`;
|
|
1157
|
+
const FREEZE_TAG = inlineScript(FREEZE_JS);
|
|
1158
|
+
const BRIDGE_TAG = inlineScript(BRIDGE_JS);
|
|
1159
|
+
const HIT_TEST_TAG = inlineScript(HIT_TEST_JS);
|
|
1160
|
+
const PARTS_TAG = inlineScript(PARTS_JS);
|
|
1161
|
+
const KNOBS_TAG = inlineScript(KNOBS_JS);
|
|
1162
|
+
const PARAGRAPHS_TAG = inlineScript(PARAGRAPHS_JS);
|
|
886
1163
|
// The base html-surface stylesheet: static design tokens + the surface kit.
|
|
887
|
-
const
|
|
888
|
-
const
|
|
889
|
-
const
|
|
890
|
-
const scriptTag = (origin, path) => `<script src="${origin}${path}"></script>`;
|
|
891
|
-
const styleTag = (origin, path) => `<link rel="stylesheet" href="${origin}${path}">`;
|
|
1164
|
+
const BASE_CSS_TAG = inlineStyle(`${TOKENS_CSS}${KIT_CSS}${PART_HIGHLIGHT_CSS}${ICON_CSS}`);
|
|
1165
|
+
const KIT_CORE_TAG = inlineStyle(CORE_CSS);
|
|
1166
|
+
const versionAttr = (version) => version === undefined ? "" : ` data-mockpit-version="${version}"`;
|
|
892
1167
|
// The same resolution kitAssets does (known ids, first occurrence wins), but
|
|
893
1168
|
// yielding one stylesheet per kit instead of one concatenated string. Project
|
|
894
1169
|
// kit URLs are re-checked here as well as on write: a stored setting is the
|
|
@@ -904,7 +1179,7 @@ function resolveKits(ids, projectKits = []) {
|
|
|
904
1179
|
const kit = KITS.find((k) => k.id === id);
|
|
905
1180
|
if (kit) {
|
|
906
1181
|
seen.add(id);
|
|
907
|
-
chosen.push({ id,
|
|
1182
|
+
chosen.push({ id, css: kit.css, href: kit.href, script: kit.script });
|
|
908
1183
|
continue;
|
|
909
1184
|
}
|
|
910
1185
|
const own = projectKits.find((k) => k.id === id);
|
|
@@ -919,19 +1194,19 @@ function resolveKits(ids, projectKits = []) {
|
|
|
919
1194
|
}
|
|
920
1195
|
return chosen;
|
|
921
1196
|
}
|
|
922
|
-
// Everything a project's
|
|
1197
|
+
// Everything a project's DesignSystem contributes to one html-surface doc.
|
|
923
1198
|
// Kept in one place so the ordering rule is visible: the project's own tokens
|
|
924
1199
|
// land AFTER mockpit's, because a repo that declares `--radius` or a brand
|
|
925
1200
|
// color should win inside its own project's surfaces.
|
|
926
|
-
function designAssets(
|
|
927
|
-
if (!
|
|
1201
|
+
function designAssets(designSystem, mode) {
|
|
1202
|
+
if (!designSystem)
|
|
928
1203
|
return { css: "", kits: [], projectKits: [], headScripts: "", dark: false };
|
|
929
|
-
const tailwindCss =
|
|
930
|
-
const raw = tailwindCss ? "" : (
|
|
1204
|
+
const tailwindCss = designSystem.kit === "tailwind" ? (designSystem.tailwindCss?.trim() ?? "") : "";
|
|
1205
|
+
const raw = tailwindCss ? "" : (designSystem.cssVars?.trim() ?? "");
|
|
931
1206
|
// `cssVars` is stored as the repo's raw block; accept either the full
|
|
932
1207
|
// `:root{…}` text or a bare declaration list.
|
|
933
1208
|
const vars = raw ? (raw.includes("{") ? raw : `:root{${raw}}`) : "";
|
|
934
|
-
const kit =
|
|
1209
|
+
const kit = designSystem.kit;
|
|
935
1210
|
// The browser build reads its input from the style tags present when its
|
|
936
1211
|
// script runs, so the stylesheet goes first. Its compiled output already
|
|
937
1212
|
// carries the repo's `:root` block, hence no `cssVars` beside it.
|
|
@@ -941,7 +1216,7 @@ function designAssets(design, mode) {
|
|
|
941
1216
|
return {
|
|
942
1217
|
css: vars,
|
|
943
1218
|
kits: kit && kit !== "tailwind" && kit !== "none" ? [kit] : [],
|
|
944
|
-
projectKits: Array.isArray(
|
|
1219
|
+
projectKits: Array.isArray(designSystem.projectKits) ? designSystem.projectKits : [],
|
|
945
1220
|
headScripts: tailwind,
|
|
946
1221
|
// Repos theme dark through a `.dark` ancestor (shadcn's `.dark {…}` block,
|
|
947
1222
|
// `@custom-variant dark (&:is(.dark *))`), so the frame wears the class.
|
|
@@ -963,9 +1238,7 @@ export const escapeHtml = (s) => s.replace(/&/g, "&").replace(/</g, "<").
|
|
|
963
1238
|
function buildRichCsp(origin) {
|
|
964
1239
|
return [
|
|
965
1240
|
`default-src 'none'`,
|
|
966
|
-
|
|
967
|
-
// script lives there. Still no connect-src.
|
|
968
|
-
`script-src 'unsafe-inline' ${origin}${STATIC_ASSET_PREFIX}`,
|
|
1241
|
+
`script-src 'unsafe-inline'`,
|
|
969
1242
|
`style-src 'unsafe-inline'`,
|
|
970
1243
|
`img-src https: data: blob: ${origin}`,
|
|
971
1244
|
`font-src data:`,
|
|
@@ -990,7 +1263,7 @@ function buildRichCsp(origin) {
|
|
|
990
1263
|
export function renderSandboxedPart(doc) {
|
|
991
1264
|
const theme = typeof doc.theme === "string" || doc.theme == null ? themeById(doc.theme) : doc.theme;
|
|
992
1265
|
return `<!doctype html>
|
|
993
|
-
<html lang="en">
|
|
1266
|
+
<html lang="en"${versionAttr(doc.version)}>
|
|
994
1267
|
<head>
|
|
995
1268
|
<meta charset="utf-8">
|
|
996
1269
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
@@ -1004,7 +1277,8 @@ export function renderSandboxedPart(doc) {
|
|
|
1004
1277
|
</head>
|
|
1005
1278
|
<body>
|
|
1006
1279
|
${doc.body}
|
|
1007
|
-
${
|
|
1280
|
+
${BRIDGE_TAG}
|
|
1281
|
+
${PARAGRAPHS_TAG}
|
|
1008
1282
|
</body>
|
|
1009
1283
|
</html>`;
|
|
1010
1284
|
}
|
|
@@ -1161,7 +1435,7 @@ try {
|
|
|
1161
1435
|
el.appendChild(pre);
|
|
1162
1436
|
}`;
|
|
1163
1437
|
return `<!doctype html>
|
|
1164
|
-
<html lang="en">
|
|
1438
|
+
<html lang="en"${versionAttr(doc.version)}>
|
|
1165
1439
|
<head>
|
|
1166
1440
|
<meta charset="utf-8">
|
|
1167
1441
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
@@ -1172,35 +1446,35 @@ try {
|
|
|
1172
1446
|
<body>
|
|
1173
1447
|
<div id="m"></div>
|
|
1174
1448
|
<script type="module">${loader}</script>
|
|
1175
|
-
${
|
|
1449
|
+
${BRIDGE_TAG}
|
|
1450
|
+
${PARAGRAPHS_TAG}
|
|
1176
1451
|
</body>
|
|
1177
1452
|
</html>`;
|
|
1178
1453
|
}
|
|
1179
1454
|
export function renderHtmlPage(doc) {
|
|
1180
1455
|
const theme = typeof doc.theme === "string" || doc.theme == null ? themeById(doc.theme) : doc.theme;
|
|
1181
|
-
const
|
|
1456
|
+
const designSystem = designAssets(doc.designSystem, doc.mode);
|
|
1182
1457
|
const preamble = knobPreamble(doc.version, doc.knobs);
|
|
1183
|
-
const htmlTag =
|
|
1458
|
+
const htmlTag = designSystem.dark
|
|
1184
1459
|
? preamble.htmlTag.replace("<html ", '<html class="dark" ')
|
|
1185
1460
|
: preamble.htmlTag;
|
|
1186
1461
|
// The project kit is appended, so with both present its components win over
|
|
1187
1462
|
// a surface-requested kit's same-named classes.
|
|
1188
|
-
const kitIds = [...(doc.kits ?? []), ...
|
|
1189
|
-
const kits = resolveKits(kitIds,
|
|
1463
|
+
const kitIds = [...(doc.kits ?? []), ...designSystem.kits];
|
|
1464
|
+
const kits = resolveKits(kitIds, designSystem.projectKits);
|
|
1190
1465
|
const kit = kitAssets(kitIds);
|
|
1191
|
-
// Document order
|
|
1192
|
-
//
|
|
1193
|
-
// accents, kit(s), then the project's own vars (which must win last).
|
|
1466
|
+
// Document order decides the cascade: theme tokens, base, kit accents, kit(s),
|
|
1467
|
+
// then the project's own vars (which must win last).
|
|
1194
1468
|
const styles = [
|
|
1195
1469
|
`<style>${tokenThemeCss(theme, doc.mode)}</style>`,
|
|
1196
|
-
|
|
1470
|
+
BASE_CSS_TAG,
|
|
1197
1471
|
`<style>${kitAccentCss(doc.mode)}</style>`,
|
|
1198
|
-
...(kits.some((k) => !k.href) ? [
|
|
1472
|
+
...(kits.some((k) => !k.href) ? [KIT_CORE_TAG] : []),
|
|
1199
1473
|
...kits.flatMap((k) => [
|
|
1200
1474
|
...(k.href ? [`<link rel="stylesheet" href="${escapeHtml(k.href)}">`] : []),
|
|
1201
|
-
...(k.
|
|
1475
|
+
...(k.css ? [inlineStyle(k.css)] : []),
|
|
1202
1476
|
]),
|
|
1203
|
-
`<style>${
|
|
1477
|
+
`<style>${designSystem.css}${colorSchemeCss(doc.mode)}</style>`,
|
|
1204
1478
|
].join("\n");
|
|
1205
1479
|
// Slots (fills.ts): only the chosen fill of each shows, keyed on the knob
|
|
1206
1480
|
// attribute; with no value yet, the default fill.
|
|
@@ -1212,19 +1486,20 @@ ${htmlTag}
|
|
|
1212
1486
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1213
1487
|
<meta http-equiv="Content-Security-Policy" content="${buildCsp(doc.origin)}">
|
|
1214
1488
|
<title>${escapeHtml(doc.title)}</title>
|
|
1489
|
+
${FREEZE_TAG}
|
|
1215
1490
|
${preamble.headScript}
|
|
1216
|
-
${
|
|
1491
|
+
${KNOBS_TAG}
|
|
1217
1492
|
${styles}
|
|
1218
1493
|
${fillCss ? `<style>${fillCss}</style>` : ""}
|
|
1219
|
-
${
|
|
1494
|
+
${designSystem.headScripts}
|
|
1220
1495
|
${kits.flatMap((k) => (k.script ? [`<script src="${escapeHtml(k.script)}" defer></script>`] : [])).join("\n")}
|
|
1221
1496
|
</head>
|
|
1222
1497
|
<body>
|
|
1223
1498
|
${SVG_DEFS}
|
|
1224
1499
|
${expandIcons(doc.html, doc.resolveIcon ?? (() => null)).html}
|
|
1225
|
-
${
|
|
1226
|
-
${
|
|
1227
|
-
${
|
|
1500
|
+
${BRIDGE_TAG}
|
|
1501
|
+
${HIT_TEST_TAG}
|
|
1502
|
+
${PARTS_TAG}
|
|
1228
1503
|
${kit.js ? `<script>${kit.js}</script>` : ""}
|
|
1229
1504
|
</body>
|
|
1230
1505
|
</html>`;
|