mockpit 1.2.0 → 1.3.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 +13 -8
- package/bin/mockpit.js +116 -16
- package/dist/mcp/server.js +10 -7
- package/dist/server/apiViews.d.ts +118 -17
- package/dist/server/apiViews.js +66 -8
- package/dist/server/app.d.ts +2 -1
- package/dist/server/app.js +917 -144
- package/dist/server/decisions.d.ts +23 -0
- package/dist/server/decisions.js +99 -0
- package/dist/server/designGuide.js +45 -10
- package/dist/server/embeds.d.ts +25 -0
- package/dist/server/embeds.js +118 -0
- package/dist/server/eventLog.js +2 -1
- package/dist/server/feedbackBatch.d.ts +12 -1
- package/dist/server/feedbackBatch.js +20 -3
- package/dist/server/fills.d.ts +41 -0
- package/dist/server/fills.js +328 -0
- package/dist/server/index.js +15 -1
- package/dist/server/knobs.d.ts +6 -2
- package/dist/server/knobs.js +11 -1
- package/dist/server/mcpHttp.d.ts +3 -0
- package/dist/server/mcpHttp.js +3 -0
- package/dist/server/mcpSpec.d.ts +13 -13
- package/dist/server/mcpSpec.js +73 -8
- package/dist/server/postSurfaces.js +3 -12
- package/dist/server/richRender.js +3 -1
- package/dist/server/run.js +4 -0
- package/dist/server/runApi.d.ts +1 -1
- package/dist/server/runApi.js +4 -3
- package/dist/server/sqlStore.d.ts +89 -4
- package/dist/server/sqlStore.js +149 -23
- package/dist/server/surfacePage.d.ts +6 -2
- package/dist/server/surfacePage.js +142 -10
- package/dist/server/themes.d.ts +2 -0
- package/dist/server/themes.js +23 -3
- package/dist/server/types.d.ts +73 -11
- package/dist/server/types.js +42 -2
- package/dist/server/viewerRequest.d.ts +2 -0
- package/dist/server/viewerRequest.js +14 -0
- package/guide/topics/asks.md +50 -3
- package/guide/topics/feedback.md +89 -11
- package/guide/topics/html.md +5 -1
- package/guide/topics/http.md +2 -1
- package/guide/topics/knobs.md +2 -1
- package/guide/topics/surfaces.md +24 -8
- package/package.json +4 -13
- package/skills/launch-video/SKILL.md +1 -1
- package/skills/mockpit/SKILL.md +12 -4
- package/viewer/dist/index.html +8 -8
- package/dist/server/slots.d.ts +0 -14
- package/dist/server/slots.js +0 -71
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { scanFills, slotCss } from "./fills.js";
|
|
1
2
|
import { CDN_ALLOWLIST, checkCdnUrl } from "./cdn.js";
|
|
2
3
|
import { CORE_CSS, KITS, kitAssets } from "./kits.js";
|
|
3
4
|
import { expandIcons } from "./icons.js";
|
|
@@ -63,7 +64,11 @@ const TOKENS_CSS = `
|
|
|
63
64
|
--border-radius-lg: 12px;
|
|
64
65
|
--border-radius-xl: 16px;
|
|
65
66
|
}
|
|
66
|
-
|
|
67
|
+
/* A page that scrolls inside a host's frame stops at its own ends instead of
|
|
68
|
+
handing the rest of a swipe to the host (Safari reads the root's value), and
|
|
69
|
+
a pinch on it can't zoom the whole viewer around it. */
|
|
70
|
+
html { box-sizing: border-box; scrollbar-width: none; overscroll-behavior: contain; touch-action: pan-x pan-y; }
|
|
71
|
+
body { overscroll-behavior: contain; }
|
|
67
72
|
html::-webkit-scrollbar, body::-webkit-scrollbar { display: none; }
|
|
68
73
|
*, *::before, *::after { box-sizing: inherit; }
|
|
69
74
|
body {
|
|
@@ -356,7 +361,7 @@ export const HIT_TEST_JS = `
|
|
|
356
361
|
// copy {text} window.copyToClipboard (BRIDGE_JS)
|
|
357
362
|
// switch-session {key} Cmd+Opt+ArrowUp/Down (BRIDGE_JS)
|
|
358
363
|
// hit-test-result {ref, path, text, rect} reply to hit-test (HIT_TEST_JS)
|
|
359
|
-
// parts {version, parts, height, scroll:{x,y}, viewport:{w,h}} (PARTS_JS)
|
|
364
|
+
// parts {version, parts, height, scroll:{x,y}, viewport:{w,h,scrollW}} (PARTS_JS)
|
|
360
365
|
// parts[i] = {name, label, key, box:{x,y,w,h}, visible, fixed, depth, parent, order}
|
|
361
366
|
// box is in DOCUMENT px (viewport rect + scroll), so the host draws
|
|
362
367
|
// (box − scroll) × scale, which is also right for fixed and sticky parts.
|
|
@@ -370,6 +375,23 @@ export const HIT_TEST_JS = `
|
|
|
370
375
|
// 50/150/400/1000ms timers, and on any layout-affecting change; never
|
|
371
376
|
// twice in a row with identical content.
|
|
372
377
|
// hit {ref, part, key} reply to hit; part = name|null
|
|
378
|
+
// goto {version, to} a click on [data-goto] while the host
|
|
379
|
+
// has Try on (PARTS_JS); to is the
|
|
380
|
+
// attribute as written ("next", "prev",
|
|
381
|
+
// "<mock>:<state>"), ≤200 chars. The host
|
|
382
|
+
// honours it only while Try is on, from
|
|
383
|
+
// the frame on stage at its version, and
|
|
384
|
+
// only to a screen of this project:
|
|
385
|
+
// never a URL
|
|
386
|
+
// tap {x, y} a finger's tap in document px (PARTS_JS);
|
|
387
|
+
// a drag never reports
|
|
388
|
+
// ready {version} the knob runtime is listening (KNOBS_JS,
|
|
389
|
+
// from <head>); values can be sent now
|
|
390
|
+
// pinch {phase, a:{x,y}, b:{x,y}} two fingers on the page, in viewport px
|
|
391
|
+
// zoom {x, y, dy} ⌘/Ctrl+scroll or a trackpad pinch, in viewport px
|
|
392
|
+
// (PARTS_JS); phase start|move|end. The
|
|
393
|
+
// page's own two-finger touches are
|
|
394
|
+
// cancelled: the host zooms its canvas
|
|
373
395
|
//
|
|
374
396
|
// host → frame
|
|
375
397
|
// hit-test {ref, x, y} x,y normalized 0..1 of the document box (Mark tool)
|
|
@@ -383,6 +405,9 @@ export const HIT_TEST_JS = `
|
|
|
383
405
|
// overlay, forwarded)
|
|
384
406
|
// knobs {values} {path: value}, merged into the current values
|
|
385
407
|
// and applied exactly like the `?k=` preamble
|
|
408
|
+
// try {on} Try is on or off: while on, [data-goto] elements are
|
|
409
|
+
// outlined and a click on one reports goto instead
|
|
410
|
+
// of reaching the page; a new document starts off
|
|
386
411
|
//
|
|
387
412
|
// Knobs (`/s/:id?k=`, validated by the route against the mock's declared knobs,
|
|
388
413
|
// and the `knobs` command): for each path, dots become "-" in names, and
|
|
@@ -482,7 +507,8 @@ export const PARTS_JS = `
|
|
|
482
507
|
parts: parts,
|
|
483
508
|
height: document.body ? document.body.scrollHeight : document.documentElement.scrollHeight,
|
|
484
509
|
scroll: { x: sx, y: sy },
|
|
485
|
-
|
|
510
|
+
// scrollWidth past innerWidth: the design is wider than the frame.
|
|
511
|
+
viewport: { w: innerWidth, h: innerHeight, scrollW: document.documentElement.scrollWidth },
|
|
486
512
|
};
|
|
487
513
|
}
|
|
488
514
|
var last = '';
|
|
@@ -552,12 +578,97 @@ export const PARTS_JS = `
|
|
|
552
578
|
mo.takeRecords();
|
|
553
579
|
}
|
|
554
580
|
|
|
581
|
+
// Try: the host lets the user click through the design; an element marked
|
|
582
|
+
// data-goto moves them to another screen. Only the host knows what that
|
|
583
|
+
// names, so the frame just reports it, and the click goes no further.
|
|
584
|
+
var trying = false;
|
|
585
|
+
function setTrying(on) {
|
|
586
|
+
trying = on;
|
|
587
|
+
document.documentElement.classList.toggle('mockpit-try', on);
|
|
588
|
+
mo.takeRecords();
|
|
589
|
+
}
|
|
590
|
+
window.addEventListener('click', function (e) {
|
|
591
|
+
if (!trying) return;
|
|
592
|
+
var t = e.target && e.target.closest ? e.target.closest('[data-goto]') : null;
|
|
593
|
+
if (!t) return;
|
|
594
|
+
e.preventDefault();
|
|
595
|
+
e.stopPropagation();
|
|
596
|
+
parent.postMessage({
|
|
597
|
+
__mockpit: true,
|
|
598
|
+
type: 'goto',
|
|
599
|
+
version: version,
|
|
600
|
+
to: text(t.getAttribute('data-goto')),
|
|
601
|
+
}, '*');
|
|
602
|
+
}, true);
|
|
603
|
+
|
|
604
|
+
// A finger's tap, in document px, for a host that lets touches through to the
|
|
605
|
+
// page (its canvas, where the page scrolls natively). A drag is a scroll: the
|
|
606
|
+
// browser cancels the pointer or it moves too far. A mouse clicks the host's
|
|
607
|
+
// own overlay instead, so it never reports here.
|
|
608
|
+
var tapAt = null;
|
|
609
|
+
window.addEventListener('pointerdown', function (e) {
|
|
610
|
+
tapAt = e.isPrimary && e.pointerType !== 'mouse' ? { x: e.clientX, y: e.clientY, t: Date.now() } : null;
|
|
611
|
+
}, true);
|
|
612
|
+
window.addEventListener('pointercancel', function () { tapAt = null; }, true);
|
|
613
|
+
window.addEventListener('pointerup', function (e) {
|
|
614
|
+
var a = tapAt;
|
|
615
|
+
tapAt = null;
|
|
616
|
+
if (!a || !e.isPrimary) return;
|
|
617
|
+
if (Math.abs(e.clientX - a.x) > 10 || Math.abs(e.clientY - a.y) > 10 || Date.now() - a.t > 600) return;
|
|
618
|
+
parent.postMessage({
|
|
619
|
+
__mockpit: true,
|
|
620
|
+
type: 'tap',
|
|
621
|
+
x: e.clientX + (window.scrollX || 0),
|
|
622
|
+
y: e.clientY + (window.scrollY || 0),
|
|
623
|
+
}, '*');
|
|
624
|
+
}, true);
|
|
625
|
+
|
|
626
|
+
// Two fingers are the host's: a page laid on its canvas takes one-finger
|
|
627
|
+
// scrolls itself, but a pinch zooms the canvas, not this page or the browser.
|
|
628
|
+
var pinching = false;
|
|
629
|
+
function pinch(phase, e) {
|
|
630
|
+
var t = e.touches;
|
|
631
|
+
parent.postMessage({
|
|
632
|
+
__mockpit: true,
|
|
633
|
+
type: 'pinch',
|
|
634
|
+
phase: phase,
|
|
635
|
+
a: t[0] ? { x: t[0].clientX, y: t[0].clientY } : null,
|
|
636
|
+
b: t[1] ? { x: t[1].clientX, y: t[1].clientY } : null,
|
|
637
|
+
}, '*');
|
|
638
|
+
}
|
|
639
|
+
window.addEventListener('touchstart', function (e) {
|
|
640
|
+
if (e.touches.length !== 2) return;
|
|
641
|
+
e.preventDefault();
|
|
642
|
+
pinching = true;
|
|
643
|
+
pinch('start', e);
|
|
644
|
+
}, { capture: true, passive: false });
|
|
645
|
+
window.addEventListener('touchmove', function (e) {
|
|
646
|
+
if (!pinching || e.touches.length < 2) return;
|
|
647
|
+
e.preventDefault();
|
|
648
|
+
pinch('move', e);
|
|
649
|
+
}, { capture: true, passive: false });
|
|
650
|
+
function release(e) {
|
|
651
|
+
if (!pinching || e.touches.length >= 2) return;
|
|
652
|
+
pinching = false;
|
|
653
|
+
pinch('end', e);
|
|
654
|
+
}
|
|
655
|
+
window.addEventListener('touchend', release, true);
|
|
656
|
+
window.addEventListener('touchcancel', release, true);
|
|
657
|
+
// ⌘/Ctrl+scroll and a trackpad pinch (ctrl+wheel) are the host's too: they
|
|
658
|
+
// zoom the canvas around the pointer, never this page or the browser.
|
|
659
|
+
window.addEventListener('wheel', function (e) {
|
|
660
|
+
if (!e.ctrlKey && !e.metaKey) return;
|
|
661
|
+
e.preventDefault();
|
|
662
|
+
parent.postMessage({ __mockpit: true, type: 'zoom', x: e.clientX, y: e.clientY, dy: e.deltaY }, '*');
|
|
663
|
+
}, { capture: true, passive: false });
|
|
664
|
+
|
|
555
665
|
window.addEventListener('message', function (e) {
|
|
556
666
|
if (e.source !== parent) return;
|
|
557
667
|
var d = e.data;
|
|
558
668
|
if (!d || d.__mockpit !== true) return;
|
|
559
669
|
if (d.type === 'highlight') setHighlight(Array.isArray(d.parts) ? d.parts : []);
|
|
560
670
|
else if (d.type === 'clear') setHighlight(null);
|
|
671
|
+
else if (d.type === 'try') setTrying(d.on === true);
|
|
561
672
|
else if (d.type === 'scroll') window.scrollBy(Number(d.dx) || 0, Number(d.dy) || 0);
|
|
562
673
|
else if (d.type === 'hit') {
|
|
563
674
|
var el = document.elementFromPoint(
|
|
@@ -612,9 +723,19 @@ ${KNOB_VALUE_JS}
|
|
|
612
723
|
detail: { values: JSON.parse(JSON.stringify(current)) },
|
|
613
724
|
}));
|
|
614
725
|
}
|
|
615
|
-
|
|
726
|
+
// The URL's values are already on <html>; a live command may also have come
|
|
727
|
+
// in before the body existed, so both are bound once it does.
|
|
728
|
+
function initial() {
|
|
729
|
+
var start = doc.knobs && typeof doc.knobs === 'object' ? doc.knobs : {};
|
|
730
|
+
for (var k in start) if (own(start, k) && !own(current, k)) current[k] = start[k];
|
|
731
|
+
apply(current, false);
|
|
732
|
+
}
|
|
616
733
|
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', initial);
|
|
617
734
|
else initial();
|
|
735
|
+
// Runs from <head>, before the page's own markup and anything slow it loads
|
|
736
|
+
// (a web font, a CDN stylesheet), so the host can send values at once rather
|
|
737
|
+
// than wait for the first parts report, which those can hold back.
|
|
738
|
+
if (parent !== window) parent.postMessage({ __mockpit: true, type: 'ready', version: doc.version }, '*');
|
|
618
739
|
window.addEventListener('message', function (e) {
|
|
619
740
|
if (e.source !== parent || parent === window) return;
|
|
620
741
|
var d = e.data;
|
|
@@ -680,6 +801,16 @@ const scriptJson = (v) => JSON.stringify(v)
|
|
|
680
801
|
// knobCss (quoted unless plain tokens) and then escapeHtml as the style
|
|
681
802
|
// attribute, and the JSON through scriptJson.
|
|
682
803
|
export function knobPreamble(version, knobs) {
|
|
804
|
+
const { attrs, values } = knobAttributes(knobs);
|
|
805
|
+
return {
|
|
806
|
+
htmlTag: `<html lang="en"${attrs}>`,
|
|
807
|
+
headScript: `<script>window.__mockpitDoc=${scriptJson({ version: version ?? null, knobs: values })};</script>`,
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
// Knob values as an element's `data-k-*` attributes and `--k-*` style vars, the
|
|
811
|
+
// same marks the preamble puts on <html>; an embedded component's wrapper
|
|
812
|
+
// carries its own settled values this way.
|
|
813
|
+
export function knobAttributes(knobs) {
|
|
683
814
|
const attrs = [];
|
|
684
815
|
const vars = [];
|
|
685
816
|
const values = {};
|
|
@@ -694,14 +825,11 @@ export function knobPreamble(version, knobs) {
|
|
|
694
825
|
vars.push(`--k-${name}:${css}`);
|
|
695
826
|
}
|
|
696
827
|
const style = vars.length ? ` style="${escapeHtml(vars.join(";"))}"` : "";
|
|
697
|
-
return {
|
|
698
|
-
htmlTag: `<html lang="en"${attrs.join("")}${style}>`,
|
|
699
|
-
headScript: `<script>window.__mockpitDoc=${scriptJson({ version: version ?? null, knobs: values })};</script>`,
|
|
700
|
-
};
|
|
828
|
+
return { attrs: `${attrs.join("")}${style}`, values };
|
|
701
829
|
}
|
|
702
830
|
// Our own highlight, shipped in the base stylesheet so turning it on is a class
|
|
703
831
|
// flip rather than a DOM insertion the parts observer would report.
|
|
704
|
-
const PART_HIGHLIGHT_CSS = `.mockpit-part-hl{outline:2px solid #8b7bff !important;outline-offset:2px}`;
|
|
832
|
+
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}`;
|
|
705
833
|
// Tailwind's browser build, pinned to a major (jsdelivr is already on the CDN
|
|
706
834
|
// allowlist, so this needs no CSP widening). Injected only when a project's
|
|
707
835
|
// design settings say `kit: "tailwind"`, i.e. the repo itself is a Tailwind
|
|
@@ -1074,6 +1202,9 @@ export function renderHtmlPage(doc) {
|
|
|
1074
1202
|
]),
|
|
1075
1203
|
`<style>${design.css}${colorSchemeCss(doc.mode)}</style>`,
|
|
1076
1204
|
].join("\n");
|
|
1205
|
+
// Slots (fills.ts): only the chosen fill of each shows, keyed on the knob
|
|
1206
|
+
// attribute; with no value yet, the default fill.
|
|
1207
|
+
const fillCss = slotCss(scanFills(doc.html).slots);
|
|
1077
1208
|
return `<!doctype html>
|
|
1078
1209
|
${htmlTag}
|
|
1079
1210
|
<head>
|
|
@@ -1082,7 +1213,9 @@ ${htmlTag}
|
|
|
1082
1213
|
<meta http-equiv="Content-Security-Policy" content="${buildCsp(doc.origin)}">
|
|
1083
1214
|
<title>${escapeHtml(doc.title)}</title>
|
|
1084
1215
|
${preamble.headScript}
|
|
1216
|
+
${scriptTag(doc.origin, KNOBS_PATH)}
|
|
1085
1217
|
${styles}
|
|
1218
|
+
${fillCss ? `<style>${fillCss}</style>` : ""}
|
|
1086
1219
|
${design.headScripts}
|
|
1087
1220
|
${kits.flatMap((k) => (k.script ? [`<script src="${escapeHtml(k.script)}" defer></script>`] : [])).join("\n")}
|
|
1088
1221
|
</head>
|
|
@@ -1091,7 +1224,6 @@ ${SVG_DEFS}
|
|
|
1091
1224
|
${expandIcons(doc.html, doc.resolveIcon ?? (() => null)).html}
|
|
1092
1225
|
${scriptTag(doc.origin, BRIDGE_PATH)}
|
|
1093
1226
|
${scriptTag(doc.origin, HIT_TEST_PATH)}
|
|
1094
|
-
${scriptTag(doc.origin, KNOBS_PATH)}
|
|
1095
1227
|
${scriptTag(doc.origin, PARTS_PATH)}
|
|
1096
1228
|
${kit.js ? `<script>${kit.js}</script>` : ""}
|
|
1097
1229
|
</body>
|
package/dist/server/themes.d.ts
CHANGED
package/dist/server/themes.js
CHANGED
|
@@ -21,6 +21,8 @@ function viewerVars(p) {
|
|
|
21
21
|
"border-2": p.border2,
|
|
22
22
|
accent: p.info.text,
|
|
23
23
|
"accent-bg": p.info.bg,
|
|
24
|
+
"accent-solid": p.info.solid ?? p.info.text,
|
|
25
|
+
"on-accent": p.info.onSolid ?? "#fff",
|
|
24
26
|
hover: p.hover,
|
|
25
27
|
danger: p.danger.text,
|
|
26
28
|
};
|
|
@@ -111,7 +113,14 @@ export const DIALKIT = {
|
|
|
111
113
|
border: "#e6e6e6",
|
|
112
114
|
border2: "#d6d6d6",
|
|
113
115
|
hover: "#ebebeb",
|
|
114
|
-
|
|
116
|
+
// White on #2563eb is already 5.2:1, so the fill is the accent itself.
|
|
117
|
+
info: {
|
|
118
|
+
bg: "rgba(37, 99, 235, 0.1)",
|
|
119
|
+
text: "#2563eb",
|
|
120
|
+
border: "#2563eb",
|
|
121
|
+
solid: "#2563eb",
|
|
122
|
+
onSolid: "#fff",
|
|
123
|
+
},
|
|
115
124
|
success: { bg: "rgba(43, 147, 72, 0.12)", text: "#2b9348", border: "#2b9348" },
|
|
116
125
|
warning: { bg: "#fff8e6", text: "#e8590c", border: "#f0e0b8" },
|
|
117
126
|
danger: { bg: "rgba(220, 38, 38, 0.1)", text: "#dc2626", border: "#dc2626" },
|
|
@@ -126,7 +135,14 @@ export const DIALKIT = {
|
|
|
126
135
|
border: "#242424",
|
|
127
136
|
border2: "#3a3a3a",
|
|
128
137
|
hover: "#262626",
|
|
129
|
-
|
|
138
|
+
// #8b7bff under white text is 3.3:1; the same hue, darker, is 5.0:1.
|
|
139
|
+
info: {
|
|
140
|
+
bg: "rgba(139, 123, 255, 0.16)",
|
|
141
|
+
text: "#8b7bff",
|
|
142
|
+
border: "#8b7bff",
|
|
143
|
+
solid: "#6b5ae0",
|
|
144
|
+
onSolid: "#fff",
|
|
145
|
+
},
|
|
130
146
|
success: { bg: "rgba(74, 222, 128, 0.14)", text: "#4ade80", border: "#4ade80" },
|
|
131
147
|
warning: { bg: "#2a2312", text: "#e8590c", border: "#4a3c1c" },
|
|
132
148
|
danger: { bg: "rgba(248, 113, 113, 0.14)", text: "#f87171", border: "#f87171" },
|
|
@@ -238,10 +254,14 @@ function pick(vars, names) {
|
|
|
238
254
|
return null;
|
|
239
255
|
}
|
|
240
256
|
function accentFrom(vars, bg, text, border, fallback) {
|
|
257
|
+
const ink = pick(vars, text);
|
|
241
258
|
return {
|
|
242
259
|
bg: pick(vars, bg) ?? fallback.bg,
|
|
243
|
-
text:
|
|
260
|
+
text: ink ?? fallback.text,
|
|
244
261
|
border: pick(vars, border) ?? fallback.border,
|
|
262
|
+
// The default's solid pair belongs to the default's ink; a repo's own ink
|
|
263
|
+
// fills with itself.
|
|
264
|
+
...(ink ? {} : { solid: fallback.solid, onSolid: fallback.onSolid }),
|
|
245
265
|
};
|
|
246
266
|
}
|
|
247
267
|
function paletteFrom(vars, fallback) {
|
package/dist/server/types.d.ts
CHANGED
|
@@ -197,14 +197,22 @@ export interface PadValue {
|
|
|
197
197
|
}
|
|
198
198
|
export type KnobValue = number | boolean | string | PadValue | SpringKnob | EasingKnob;
|
|
199
199
|
export type MockKind = "page" | "component";
|
|
200
|
-
export type PostStatus = "open" | "accepted" | "archived";
|
|
200
|
+
export type PostStatus = "open" | "accepted" | "archived" | "dropped";
|
|
201
|
+
export declare const isLive: (status: PostStatus) => boolean;
|
|
202
|
+
export declare const inUseVersion: (post: Pick<Post, "status" | "version" | "acceptedVersion">) => number;
|
|
203
|
+
export declare function postAtVersion(post: Post, version: number): Post | null;
|
|
204
|
+
export declare function versionAt(post: Pick<Post, "version" | "history">, n: number): number;
|
|
201
205
|
export interface AskOption {
|
|
202
206
|
id: string;
|
|
203
207
|
label: string;
|
|
204
208
|
variant?: string;
|
|
209
|
+
version?: number;
|
|
205
210
|
set?: Record<string, KnobValue>;
|
|
206
211
|
}
|
|
207
212
|
export type AskAnswer = string | string[];
|
|
213
|
+
export declare const optionState: (ask: Pick<Ask, "state">, mock: {
|
|
214
|
+
states: string[];
|
|
215
|
+
}) => string | null;
|
|
208
216
|
export interface Ask {
|
|
209
217
|
id: string;
|
|
210
218
|
text: string;
|
|
@@ -213,11 +221,13 @@ export interface Ask {
|
|
|
213
221
|
part?: string;
|
|
214
222
|
options: AskOption[];
|
|
215
223
|
multi?: boolean;
|
|
224
|
+
after?: string;
|
|
216
225
|
answer?: AskAnswer;
|
|
217
226
|
other?: string;
|
|
218
227
|
note?: string;
|
|
219
228
|
otherImages?: string[];
|
|
220
229
|
noteImages?: string[];
|
|
230
|
+
dismissed?: true;
|
|
221
231
|
at: string;
|
|
222
232
|
}
|
|
223
233
|
export declare const OTHER_ID = "other";
|
|
@@ -242,6 +252,7 @@ export interface ReplyDecision {
|
|
|
242
252
|
kind: "accept" | "revise" | "drop";
|
|
243
253
|
state: string | null;
|
|
244
254
|
variant: string;
|
|
255
|
+
version?: number;
|
|
245
256
|
}
|
|
246
257
|
export interface Draft {
|
|
247
258
|
version: number;
|
|
@@ -268,6 +279,9 @@ export interface Reply {
|
|
|
268
279
|
noteImages?: Record<string, string[]>;
|
|
269
280
|
text?: string;
|
|
270
281
|
decision?: ReplyDecision;
|
|
282
|
+
resolved?: {
|
|
283
|
+
dismissed: string[];
|
|
284
|
+
};
|
|
271
285
|
}
|
|
272
286
|
export interface Mock {
|
|
273
287
|
id: string;
|
|
@@ -278,15 +292,18 @@ export interface Mock {
|
|
|
278
292
|
states: string[];
|
|
279
293
|
asks: Ask[];
|
|
280
294
|
knobs: Knobs;
|
|
295
|
+
widths: number[];
|
|
281
296
|
draft: Draft | null;
|
|
282
297
|
sessionId: string | null;
|
|
298
|
+
resolvedAt: string | null;
|
|
283
299
|
createdAt: string;
|
|
284
300
|
updatedAt: string;
|
|
285
301
|
}
|
|
286
|
-
export interface
|
|
302
|
+
export interface Embed {
|
|
287
303
|
slug: string;
|
|
288
304
|
variant: string;
|
|
289
305
|
version: number;
|
|
306
|
+
pinned?: true;
|
|
290
307
|
}
|
|
291
308
|
export interface PostVersion {
|
|
292
309
|
version: number;
|
|
@@ -297,6 +314,10 @@ export interface PostVersion {
|
|
|
297
314
|
prompt?: string;
|
|
298
315
|
author?: string;
|
|
299
316
|
}
|
|
317
|
+
export interface ForkRef {
|
|
318
|
+
variant: string;
|
|
319
|
+
version: number;
|
|
320
|
+
}
|
|
300
321
|
export interface Post {
|
|
301
322
|
id: string;
|
|
302
323
|
sessionId: string;
|
|
@@ -311,11 +332,28 @@ export interface Post {
|
|
|
311
332
|
version: number;
|
|
312
333
|
history: PostVersion[];
|
|
313
334
|
knobs?: Knobs;
|
|
314
|
-
|
|
335
|
+
note?: string;
|
|
336
|
+
embeds: Embed[];
|
|
337
|
+
forkedFrom?: ForkRef;
|
|
338
|
+
starred?: true;
|
|
339
|
+
acceptedVersion?: number;
|
|
340
|
+
hiddenVersions?: number[];
|
|
315
341
|
from?: number;
|
|
316
342
|
prompt?: string;
|
|
317
343
|
author?: string;
|
|
318
344
|
}
|
|
345
|
+
export interface FlowStep {
|
|
346
|
+
mock: string;
|
|
347
|
+
state: string | null;
|
|
348
|
+
}
|
|
349
|
+
export interface Flow {
|
|
350
|
+
project: string;
|
|
351
|
+
name: string;
|
|
352
|
+
steps: FlowStep[];
|
|
353
|
+
createdAt: string;
|
|
354
|
+
updatedAt: string;
|
|
355
|
+
}
|
|
356
|
+
export declare const sameScreen: (a: FlowStep, b: FlowStep) => boolean;
|
|
319
357
|
export interface ProjectSummary {
|
|
320
358
|
name: string;
|
|
321
359
|
mocks: number;
|
|
@@ -470,6 +508,7 @@ export interface CreateMockInput {
|
|
|
470
508
|
kind?: MockKind;
|
|
471
509
|
states?: string[];
|
|
472
510
|
knobs?: Knobs;
|
|
511
|
+
widths?: number[];
|
|
473
512
|
sessionId?: string | null;
|
|
474
513
|
}
|
|
475
514
|
export interface UpdateMockInput {
|
|
@@ -478,7 +517,9 @@ export interface UpdateMockInput {
|
|
|
478
517
|
states?: string[];
|
|
479
518
|
asks?: Ask[];
|
|
480
519
|
knobs?: Knobs;
|
|
520
|
+
widths?: number[];
|
|
481
521
|
sessionId?: string | null;
|
|
522
|
+
resolvedAt?: string | null;
|
|
482
523
|
}
|
|
483
524
|
export interface CreatePostInput {
|
|
484
525
|
sessionId: string;
|
|
@@ -488,7 +529,9 @@ export interface CreatePostInput {
|
|
|
488
529
|
title?: string;
|
|
489
530
|
surfaces: Surface[];
|
|
490
531
|
knobs?: Knobs;
|
|
491
|
-
|
|
532
|
+
note?: string;
|
|
533
|
+
embeds?: Embed[];
|
|
534
|
+
forkedFrom?: ForkRef;
|
|
492
535
|
from?: number;
|
|
493
536
|
prompt?: string;
|
|
494
537
|
author?: string;
|
|
@@ -497,7 +540,8 @@ export interface UpdatePostInput {
|
|
|
497
540
|
title?: string;
|
|
498
541
|
surfaces?: Surface[];
|
|
499
542
|
knobs?: Knobs | null;
|
|
500
|
-
|
|
543
|
+
note?: string;
|
|
544
|
+
embeds?: Embed[];
|
|
501
545
|
from?: number;
|
|
502
546
|
prompt?: string;
|
|
503
547
|
author?: string;
|
|
@@ -528,8 +572,14 @@ export interface CommitReplyInput {
|
|
|
528
572
|
text: string;
|
|
529
573
|
payload: Reply;
|
|
530
574
|
asks: Ask[];
|
|
531
|
-
accept:
|
|
575
|
+
accept: {
|
|
576
|
+
id: string;
|
|
577
|
+
version: number;
|
|
578
|
+
}[];
|
|
532
579
|
archive: string[];
|
|
580
|
+
drop?: string[];
|
|
581
|
+
resolvedAt?: string;
|
|
582
|
+
keepDraft?: true;
|
|
533
583
|
}
|
|
534
584
|
export interface PostQuery {
|
|
535
585
|
mockId?: string;
|
|
@@ -554,6 +604,8 @@ export interface Store {
|
|
|
554
604
|
createMock(input: CreateMockInput): Promise<Mock>;
|
|
555
605
|
updateMock(id: string, patch: UpdateMockInput): Promise<Mock | null>;
|
|
556
606
|
removeMock(id: string): Promise<boolean>;
|
|
607
|
+
listFlows(project: string): Promise<Flow[]>;
|
|
608
|
+
placeFlowStep(project: string, name: string, screen: FlowStep, step: number): Promise<Flow | null>;
|
|
557
609
|
putDraft(mockId: string, draft: Draft | null): Promise<Mock | null>;
|
|
558
610
|
listPosts(query?: PostQuery): Promise<Post[]>;
|
|
559
611
|
countPostsBySession(): Promise<Map<string, number>>;
|
|
@@ -562,7 +614,9 @@ export interface Store {
|
|
|
562
614
|
createPost(input: CreatePostInput): Promise<Post | null>;
|
|
563
615
|
updatePost(id: string, patch: UpdatePostInput): Promise<Post | null>;
|
|
564
616
|
removePost(id: string): Promise<boolean>;
|
|
565
|
-
setPostStatus(id: string, status: PostStatus): Promise<Post | null>;
|
|
617
|
+
setPostStatus(id: string, status: PostStatus, acceptedVersion?: number): Promise<Post | null>;
|
|
618
|
+
setPostStarred(id: string, starred: boolean): Promise<Post | null>;
|
|
619
|
+
setVersionHidden(id: string, version: number, hidden: boolean): Promise<Post | null>;
|
|
566
620
|
listComments(query: CommentQuery): Promise<Comment[]>;
|
|
567
621
|
createComment(input: CreateCommentInput): Promise<Comment | null>;
|
|
568
622
|
removeComment(id: string): Promise<Comment | null>;
|
|
@@ -600,10 +654,18 @@ export declare function projectFromCwd(cwd: string | null | undefined): string |
|
|
|
600
654
|
export declare const htmlSurface: (html: string, kits?: unknown) => HtmlSurface;
|
|
601
655
|
export declare const askAnswered: (a: Pick<Ask, "answer" | "other" | "note">) => boolean;
|
|
602
656
|
export declare const openAsks: (mock: Pick<Mock, "asks">) => Ask[];
|
|
603
|
-
export declare function builtinAskIds(mock: Pick<Mock, "asks" | "states"
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
export declare
|
|
657
|
+
export declare function builtinAskIds(mock: Pick<Mock, "asks" | "states"> & {
|
|
658
|
+
resolvedAt?: string | null;
|
|
659
|
+
}, posts: Pick<Post, "mock" | "state" | "variant" | "status">[], mockId: string): string[];
|
|
660
|
+
export declare const builtinAskCount: (mock: Pick<Mock, "asks" | "states"> & {
|
|
661
|
+
resolvedAt?: string | null;
|
|
662
|
+
}, posts: Pick<Post, "mock" | "state" | "variant" | "status">[], mockId: string) => number;
|
|
663
|
+
export declare const openCount: (mock: Pick<Mock, "id" | "asks" | "states"> & {
|
|
664
|
+
resolvedAt?: string | null;
|
|
665
|
+
}, posts: Pick<Post, "mock" | "state" | "variant" | "status">[]) => number;
|
|
666
|
+
export declare function draftedCount(mock: Pick<Mock, "id" | "asks" | "states" | "draft"> & {
|
|
667
|
+
resolvedAt?: string | null;
|
|
668
|
+
}, posts: Pick<Post, "mock" | "state" | "variant" | "status">[]): number | null;
|
|
607
669
|
export declare const BUILTIN_ASK_ID = "variant";
|
|
608
670
|
export declare function builtinAsk(mock: Pick<Mock, "states">, posts: Pick<Post, "state" | "variant">[], id: string): Ask | undefined;
|
|
609
671
|
export declare function summarizeProjects(mocks: Mock[], sessions: Session[], posts?: Post[]): ProjectSummary[];
|
package/dist/server/types.js
CHANGED
|
@@ -51,6 +51,42 @@ export function isSurfaceKind(kind) {
|
|
|
51
51
|
export function isSandboxedSurfaceKind(kind) {
|
|
52
52
|
return isSurfaceKind(kind) && SURFACE_KIND_METADATA[kind].sandboxed;
|
|
53
53
|
}
|
|
54
|
+
// Still in the running: what the user can choose between.
|
|
55
|
+
export const isLive = (status) => status === "open" || status === "accepted";
|
|
56
|
+
// The version a variant is resolved at when nothing names one: the version in
|
|
57
|
+
// use when accepted, else its latest.
|
|
58
|
+
export const inUseVersion = (post) => post.status === "accepted" ? (post.acceptedVersion ?? post.version) : post.version;
|
|
59
|
+
// A variant as it stood at one of its versions (current or retained), or null
|
|
60
|
+
// when that version is gone.
|
|
61
|
+
export function postAtVersion(post, version) {
|
|
62
|
+
if (version === post.version)
|
|
63
|
+
return post;
|
|
64
|
+
const old = post.history.find((h) => h.version === version);
|
|
65
|
+
if (!old)
|
|
66
|
+
return null;
|
|
67
|
+
const { from: _f, prompt: _p, author: _a, ...rest } = post;
|
|
68
|
+
return {
|
|
69
|
+
...rest,
|
|
70
|
+
version: old.version,
|
|
71
|
+
title: old.title,
|
|
72
|
+
surfaces: old.surfaces,
|
|
73
|
+
updatedAt: old.at,
|
|
74
|
+
...(old.from === undefined ? {} : { from: old.from }),
|
|
75
|
+
...(old.prompt === undefined ? {} : { prompt: old.prompt }),
|
|
76
|
+
...(old.author === undefined ? {} : { author: old.author }),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
// The variant's newest version not after mock version `n` — what the stage
|
|
80
|
+
// showed when a draft or reply was bound to n (the viewer's frameVersion).
|
|
81
|
+
export function versionAt(post, n) {
|
|
82
|
+
const versions = [post.version, ...post.history.map((h) => h.version)];
|
|
83
|
+
const fit = versions.filter((v) => v <= n);
|
|
84
|
+
return fit.length ? Math.max(...fit) : Math.min(...versions);
|
|
85
|
+
}
|
|
86
|
+
// The state a version option names a version in: the ask's state, else the
|
|
87
|
+
// mock's first (`ask` accepts version options without a state only while the
|
|
88
|
+
// mock has one).
|
|
89
|
+
export const optionState = (ask, mock) => ask.state ?? mock.states[0] ?? null;
|
|
54
90
|
// The viewer's write-in option on every ask. Reserved, so a delivered
|
|
55
91
|
// `{id: "other", other: true}` choice never shadows an option the agent declared.
|
|
56
92
|
export const OTHER_ID = "other";
|
|
@@ -58,6 +94,7 @@ export const OTHER_ID = "other";
|
|
|
58
94
|
// for a reviewer on a phone who would rather not type: like, dislike, or
|
|
59
95
|
// "more like this". Rides as a part comment, so it travels in the one reply.
|
|
60
96
|
export const REACTIONS = ["up", "down", "more"];
|
|
97
|
+
export const sameScreen = (a, b) => a.mock === b.mock && a.state === b.state;
|
|
61
98
|
export const HISTORY_LIMIT = 20;
|
|
62
99
|
// "user" is the reserved trust label for genuine human comments. A session
|
|
63
100
|
// agent with that name could otherwise have its programmatic comments delivered
|
|
@@ -152,15 +189,18 @@ export const htmlSurface = (html, kits) => ({
|
|
|
152
189
|
// An open ask is one no reply has answered yet: a write-in or a note alone
|
|
153
190
|
// answers it too ("Neither, because…").
|
|
154
191
|
export const askAnswered = (a) => a.answer !== undefined || a.other !== undefined || a.note !== undefined;
|
|
155
|
-
export const openAsks = (mock) => mock.asks.filter((a) => !askAnswered(a));
|
|
192
|
+
export const openAsks = (mock) => mock.asks.filter((a) => !askAnswered(a) && !a.dismissed);
|
|
156
193
|
// How many built-in "Which one?" asks the viewer shows: one mock-wide when every
|
|
157
194
|
// state needs one and the live names line up, else one per needing state. Same
|
|
158
195
|
// rule as viewer/src/logic.ts builtinAsks, so Home, `read` and `pending` count
|
|
159
196
|
// the question the user actually sees. Answering it archives the losers, which
|
|
160
197
|
// is what makes it stop counting.
|
|
161
198
|
export function builtinAskIds(mock, posts, mockId) {
|
|
199
|
+
// Resolving closed the choice along with every other open question.
|
|
200
|
+
if (mock.resolvedAt)
|
|
201
|
+
return [];
|
|
162
202
|
const live = (state) => posts
|
|
163
|
-
.filter((p) => p.mock === mockId && p.state === state && p.status
|
|
203
|
+
.filter((p) => p.mock === mockId && p.state === state && isLive(p.status))
|
|
164
204
|
.map((p) => p.variant);
|
|
165
205
|
const needs = (state) => {
|
|
166
206
|
const names = live(state);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Whether a request comes from the trusted viewer, the page this server
|
|
2
|
+
// served: only it may act as the user (drafts, replies, user comments).
|
|
3
|
+
// Fetch Metadata says so directly, but browsers omit it on plain http away
|
|
4
|
+
// from localhost (a LAN or tailnet address), where the viewer then looked
|
|
5
|
+
// like any other client and every draft call failed. The viewer also names
|
|
6
|
+
// itself with a custom header, which a cross-origin caller cannot send: that
|
|
7
|
+
// header forces a CORS preflight, and this server answers none, so a sandboxed
|
|
8
|
+
// surface (opaque origin) or another site is refused by the browser itself.
|
|
9
|
+
export const VIEWER_CLIENT = "viewer";
|
|
10
|
+
export function isViewerRequest(request) {
|
|
11
|
+
if (request.headers.get("sec-fetch-site") === "same-origin")
|
|
12
|
+
return true;
|
|
13
|
+
return request.headers.get("x-mockpit-client") === VIEWER_CLIENT;
|
|
14
|
+
}
|