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