mockpit 1.1.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 -6
- package/bin/mockpit.js +139 -18
- package/dist/mcp/server.js +30 -10
- package/dist/server/apiViews.d.ts +129 -25
- package/dist/server/apiViews.js +86 -23
- package/dist/server/app.d.ts +2 -1
- package/dist/server/app.js +1258 -200
- package/dist/server/appIcon.d.ts +22 -0
- package/dist/server/appIcon.js +43 -0
- package/dist/server/appIconPng.d.ts +1 -0
- package/dist/server/appIconPng.js +6 -0
- package/dist/server/askNudges.d.ts +3 -0
- package/dist/server/askNudges.js +40 -0
- package/dist/server/base64.d.ts +1 -0
- package/dist/server/base64.js +8 -0
- package/dist/server/decisions.d.ts +23 -0
- package/dist/server/decisions.js +99 -0
- package/dist/server/designGuide.js +53 -16
- 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 +28 -5
- package/dist/server/feedbackBatch.js +78 -11
- package/dist/server/fills.d.ts +41 -0
- package/dist/server/fills.js +328 -0
- package/dist/server/handoff.d.ts +6 -0
- package/dist/server/handoff.js +38 -0
- package/dist/server/index.js +15 -1
- package/dist/server/knobs.d.ts +6 -2
- package/dist/server/knobs.js +13 -4
- package/dist/server/mcpHttp.d.ts +5 -2
- package/dist/server/mcpHttp.js +14 -5
- package/dist/server/mcpSpec.d.ts +21 -13
- package/dist/server/mcpSpec.js +99 -9
- package/dist/server/postSurfaces.js +3 -12
- package/dist/server/richRender.js +3 -1
- package/dist/server/run.js +6 -2
- package/dist/server/runApi.d.ts +1 -1
- package/dist/server/runApi.js +5 -4
- package/dist/server/sqlStore.d.ts +92 -5
- package/dist/server/sqlStore.js +188 -30
- 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 +95 -10
- package/dist/server/types.js +89 -6
- package/dist/server/viewerRequest.d.ts +2 -0
- package/dist/server/viewerRequest.js +14 -0
- package/extensions/mockpit.js +1 -0
- package/guide/topics/asks.md +69 -10
- package/guide/topics/feedback.md +104 -16
- package/guide/topics/html.md +5 -1
- package/guide/topics/http.md +2 -1
- package/guide/topics/knobs.md +4 -3
- package/guide/topics/surfaces.md +32 -16
- package/package.json +4 -13
- package/skills/launch-video/SKILL.md +1 -1
- package/skills/mockpit/SKILL.md +14 -4
- package/viewer/dist/index.html +9 -9
- package/dist/server/slots.d.ts +0 -14
- package/dist/server/slots.js +0 -71
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type Mode } from "./themes.ts";
|
|
2
|
+
export declare const ICON_SVG: string;
|
|
3
|
+
export declare const ICON_SIZES: readonly [180, 192, 512];
|
|
4
|
+
export type IconSize = (typeof ICON_SIZES)[number];
|
|
5
|
+
export declare const themeColor: (mode: Mode) => string;
|
|
6
|
+
export declare function webManifest(mode: Mode, startUrl?: string): {
|
|
7
|
+
name: string;
|
|
8
|
+
short_name: string;
|
|
9
|
+
description: string;
|
|
10
|
+
id: string;
|
|
11
|
+
start_url: string;
|
|
12
|
+
scope: string;
|
|
13
|
+
display: string;
|
|
14
|
+
background_color: string;
|
|
15
|
+
theme_color: string;
|
|
16
|
+
icons: {
|
|
17
|
+
src: string;
|
|
18
|
+
sizes: string;
|
|
19
|
+
type: string;
|
|
20
|
+
purpose: string;
|
|
21
|
+
}[];
|
|
22
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// The installable app: its web app manifest and its icon. Runtime-agnostic (no
|
|
2
|
+
// `node:` imports) so the Worker serves the same bytes. The PNG renders of the
|
|
3
|
+
// icon are generated from ICON_SVG by scripts/app-icons.ts into appIconPng.ts,
|
|
4
|
+
// because iOS takes no SVG for a home-screen icon.
|
|
5
|
+
import { DIALKIT } from "./themes.js";
|
|
6
|
+
const BG = DIALKIT.dark.bg;
|
|
7
|
+
const ACCENT = DIALKIT.dark.info.text;
|
|
8
|
+
// A stage frame with a comment pin on its corner. Full-bleed background and the
|
|
9
|
+
// mark inside the central 80% circle, so the same art works as `maskable`.
|
|
10
|
+
export const ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
|
11
|
+
<rect width="512" height="512" fill="${BG}"/>
|
|
12
|
+
<rect x="126" y="142" width="260" height="196" rx="34" fill="none" stroke="${ACCENT}" stroke-width="28"/>
|
|
13
|
+
<path d="M170 206h120M170 262h76" stroke="${ACCENT}" stroke-width="24" stroke-linecap="round" opacity="0.55"/>
|
|
14
|
+
<circle cx="366" cy="338" r="58" fill="${ACCENT}" stroke="${BG}" stroke-width="22"/>
|
|
15
|
+
</svg>`;
|
|
16
|
+
export const ICON_SIZES = [180, 192, 512];
|
|
17
|
+
export const themeColor = (mode) => DIALKIT[mode].bg;
|
|
18
|
+
// Relative URLs resolve against the manifest's own URL, so the app works under
|
|
19
|
+
// any base path. `startUrl` carries `?key=` on a tokened workspace: a
|
|
20
|
+
// home-screen app may not share Safari's cookie jar, and the key is how the
|
|
21
|
+
// viewer gets its cookie back on first launch.
|
|
22
|
+
export function webManifest(mode, startUrl = "./") {
|
|
23
|
+
return {
|
|
24
|
+
name: "mockpit",
|
|
25
|
+
short_name: "mockpit",
|
|
26
|
+
description: "Answer your agent's design questions.",
|
|
27
|
+
id: "./",
|
|
28
|
+
start_url: startUrl,
|
|
29
|
+
scope: "./",
|
|
30
|
+
display: "standalone",
|
|
31
|
+
background_color: themeColor(mode),
|
|
32
|
+
theme_color: themeColor(mode),
|
|
33
|
+
icons: [
|
|
34
|
+
{ src: "icon.svg", sizes: "any", type: "image/svg+xml", purpose: "any" },
|
|
35
|
+
...ICON_SIZES.filter((s) => s !== 180).map((s) => ({
|
|
36
|
+
src: `icon-${s}.png`,
|
|
37
|
+
sizes: `${s}x${s}`,
|
|
38
|
+
type: "image/png",
|
|
39
|
+
purpose: "any maskable",
|
|
40
|
+
})),
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const ICON_PNG_BASE64: Record<number, string>;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Generated by scripts/app-icons.ts from ICON_SVG; do not edit.
|
|
2
|
+
export const ICON_PNG_BASE64 = {
|
|
3
|
+
180: "iVBORw0KGgoAAAANSUhEUgAAALQAAAC0CAIAAACyr5FlAAAKnklEQVR4nOzdfXAU9R3H8W9yyT0k95BcEhJCHgiJEXkIjTRqRAYUpDyNQBGKteq0xZbOtDqdtuPUGTt22k7/czr9wz+EtsoMijwjT/IgIGIxahRJCCQBQsjzcy6Xu9xDHlzMwDDmvktCbve3e/m8Jn/dLoTk3uz+9rd7uzEOh4MAQokmAAbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiABbiAFYMaVV+zvL7p62clDTDbEq0mJ1mk40ihc/v7vN1+vxdLe0XK68drL5+hDQpSmtPTbDFT55f9Mqs+9cbY+NoYggEveWVO858/s9ebwtpiYbiMBntCx95rajgRZqoSs6/KSXiD7hJG7QSh92a8eyqfUmJeTSxtXVceveDtW5PI2mAwWw2k2hpKXNeWPuhw5ZBE158XMqs/Kdr6j/2eFtJNPFHK4mOac+tORBnSSL4jjU+9fkfH3Im5JJoguMwmxzS3kQabRDcQfqF/PSpPcZYwQdoIuOIjo7ZsHJngj2LYIQEe/b6Fe9GRYl8g0SOOYoKfl048wUChtSHx9vW1PoVCSJyEqy48OVRrtnT29jlqhkaGiL9i4qKSnTk2K3po1l53g//UFq+hQQRFscDuats1jT5dXz+nsOnf3+19rg/0EORRRps5WY9uWzhG2aT3HjLbp0szRRX1RwmEYTt0qbnrZJfobntwlvvPVpRvTvyyqCb3bsuVu+SfsDWjovyaz6Qt5oEERNHdHRsfs4ymRXcnuZ3di/t6a2niCb9gG/vWur2NMmsk5+zVPp1kQhi4piW+URsjEVmhX3HNgb7vTQBBILufcd+JbOCdFibk7mQRBATR2rKbJmlja1f1zacpQmjtuGTxpbzMiukJReQCGLiMMvOetU1nqMJpr65RGap0ShmNkxMHPJTonVNn9EEI/8jW0xiTo6KOZQ1GIwyS6VZDZpgXO46maUGg4lEwGWCwEIcwEIcwEIcwEIcwNLuRxNGMsbastIftcanGgRNJw8LBD2u3vobE2CaTjdx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",
|
|
4
|
+
192: "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",
|
|
5
|
+
512: "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",
|
|
6
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Ask discipline on `ask`: the two habits that make the user type or re-answer
|
|
2
|
+
// what they could have seen. Pure, so the rules are tested on their own.
|
|
3
|
+
// Words that name how something looks, matched on option labels only: a
|
|
4
|
+
// question's text names the topic, not the options. Words with a common
|
|
5
|
+
// non-visual sense ("size", "background", "compact", "white") are left out, so
|
|
6
|
+
// a pill about file size or a background job is not flagged.
|
|
7
|
+
const VISUAL = /\b(colou?rs?|borders?|radius|rounded|corners?|spacing|padding|fonts?|typeface|serif|shadows?|tint|hue|opacity|red|orange|yellow|green|olive|teal|blue|navy|purple|violet|pink|gr[ae]y|\d+\s?(px|rem|em|pt))\b|#[0-9a-f]{3,8}\b/i;
|
|
8
|
+
const plain = (o) => !o.variant && !(o.set && Object.keys(o.set).length);
|
|
9
|
+
export const looksVisual = (label) => VISUAL.test(label);
|
|
10
|
+
// A Look-style ask: one whose options choose between variants.
|
|
11
|
+
const bindsVariants = (a) => a.options.some((o) => o.variant);
|
|
12
|
+
const quoted = (opts) => opts.map((o) => `"${o.label}"`).join(", ");
|
|
13
|
+
export function askNudges(added, all) {
|
|
14
|
+
const out = [];
|
|
15
|
+
for (const ask of added) {
|
|
16
|
+
const pills = ask.options.filter(plain);
|
|
17
|
+
if (!pills.length)
|
|
18
|
+
continue;
|
|
19
|
+
const visual = pills.filter((o) => looksVisual(o.label));
|
|
20
|
+
const look = ask.scope === "part" || bindsVariants(ask)
|
|
21
|
+
? undefined
|
|
22
|
+
: all.find((a) => a.id !== ask.id &&
|
|
23
|
+
a.scope !== "part" &&
|
|
24
|
+
bindsVariants(a) &&
|
|
25
|
+
(a.scope === "mock" || ask.scope === "mock" || a.state === ask.state));
|
|
26
|
+
// Only a pill that names a look or reads visual overlaps the variants; a
|
|
27
|
+
// plain question on another axis ("Drawer or Margin?") stands beside them.
|
|
28
|
+
const names = new Set(look?.options.flatMap((o) => [o.label, o.variant ?? ""].map((n) => n.toLowerCase())));
|
|
29
|
+
const reask = look
|
|
30
|
+
? pills.filter((o) => names.has(o.label.toLowerCase()) || looksVisual(o.label))
|
|
31
|
+
: [];
|
|
32
|
+
if (look && reask.length) {
|
|
33
|
+
out.push(`ask "${ask.id}": ${quoted(reask)} may re-ask what "${look.id}" decides. Drop it or use knob sets.`);
|
|
34
|
+
}
|
|
35
|
+
else if (visual.length) {
|
|
36
|
+
out.push(`ask "${ask.id}": ${quoted(visual)} look visual. Give each a knob set or variant.`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
package/dist/server/base64.d.ts
CHANGED
package/dist/server/base64.js
CHANGED
|
@@ -14,3 +14,11 @@ export function decodeBase64(b64) {
|
|
|
14
14
|
bytes[i] = bin.charCodeAt(i);
|
|
15
15
|
return bytes;
|
|
16
16
|
}
|
|
17
|
+
// bytes -> base64, in chunks so a multi-MB image never spreads into one call.
|
|
18
|
+
export function encodeBase64(bytes) {
|
|
19
|
+
let bin = "";
|
|
20
|
+
for (let i = 0; i < bytes.length; i += 0x8000) {
|
|
21
|
+
bin += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
|
|
22
|
+
}
|
|
23
|
+
return btoa(bin);
|
|
24
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type SlotInfo } from "./fills.ts";
|
|
2
|
+
import { type KnobValue, type Mock, type Post, type Reply } from "./types.ts";
|
|
3
|
+
export interface SlotDecision {
|
|
4
|
+
fill: string;
|
|
5
|
+
label?: string;
|
|
6
|
+
from?: string;
|
|
7
|
+
slots?: Record<string, SlotDecision>;
|
|
8
|
+
}
|
|
9
|
+
export interface StateDecision {
|
|
10
|
+
variant: string;
|
|
11
|
+
slots: Record<string, SlotDecision>;
|
|
12
|
+
}
|
|
13
|
+
export interface Decisions {
|
|
14
|
+
states: Record<string, StateDecision>;
|
|
15
|
+
knobs: Record<string, KnobValue>;
|
|
16
|
+
}
|
|
17
|
+
export declare function replyValues(reply: Pick<Reply, "answers" | "tuned">, mock: Pick<Mock, "asks">): Record<string, KnobValue>;
|
|
18
|
+
export declare function settledValues(replies: Pick<Reply, "answers" | "tuned">[], mock: Pick<Mock, "asks" | "knobs">, posts: (Pick<Post, "knobs"> & {
|
|
19
|
+
surfaces?: Post["surfaces"];
|
|
20
|
+
})[]): Record<string, KnobValue>;
|
|
21
|
+
export declare function chosenFills(slots: SlotInfo[], values: Record<string, KnobValue>): Record<string, string>;
|
|
22
|
+
export declare function postForState(posts: Post[], state: string | null): Post | undefined;
|
|
23
|
+
export declare function decisionsFor(mock: Mock, posts: Post[], reply: Pick<Reply, "answers" | "tuned" | "mix">): Decisions | null;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// What a reply decided, as the screen is built: per state, the look and each
|
|
2
|
+
// slot's chosen fill, with the slots inside a fill listed under it only when
|
|
3
|
+
// that fill was chosen; plus every other knob value the reply set. The agent
|
|
4
|
+
// reads "entry → skeleton → rows → shimmer" instead of reassembling it from
|
|
5
|
+
// option ids, and `export` composes the same choice into one screen.
|
|
6
|
+
// Runtime-agnostic (no node imports).
|
|
7
|
+
import { slotsInSurfaces } from "./fills.js";
|
|
8
|
+
import { checkKnobValue, knobFor } from "./knobs.js";
|
|
9
|
+
import { inUseVersion, postAtVersion, } from "./types.js";
|
|
10
|
+
// The knob values a reply carries: each chosen option's `set`, then tuning,
|
|
11
|
+
// which wins, as in the viewer's resolveKnobs.
|
|
12
|
+
export function replyValues(reply, mock) {
|
|
13
|
+
const out = {};
|
|
14
|
+
for (const ask of mock.asks) {
|
|
15
|
+
const answer = reply.answers[ask.id];
|
|
16
|
+
const ids = answer === undefined ? [] : Array.isArray(answer) ? answer : [answer];
|
|
17
|
+
for (const id of ids)
|
|
18
|
+
Object.assign(out, ask.options.find((o) => o.id === id)?.set ?? {});
|
|
19
|
+
}
|
|
20
|
+
return { ...out, ...reply.tuned };
|
|
21
|
+
}
|
|
22
|
+
// Every value the user has sent, oldest reply first so a later answer or tune
|
|
23
|
+
// replaces an earlier one. Values the knobs declared now can't hold drop out:
|
|
24
|
+
// the agent may have renamed or narrowed a knob since.
|
|
25
|
+
export function settledValues(replies, mock, posts) {
|
|
26
|
+
const all = {};
|
|
27
|
+
for (const r of replies)
|
|
28
|
+
Object.assign(all, replyValues(r, mock));
|
|
29
|
+
const out = {};
|
|
30
|
+
for (const [path, value] of Object.entries(all)) {
|
|
31
|
+
const config = knobFor(path, mock, posts);
|
|
32
|
+
const checked = config === undefined ? null : checkKnobValue(path, config, value);
|
|
33
|
+
if (checked?.ok)
|
|
34
|
+
out[path] = checked.value;
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
// The fill each slot shows under these values: a value naming one of its
|
|
39
|
+
// fills, else its default.
|
|
40
|
+
export function chosenFills(slots, values) {
|
|
41
|
+
const out = {};
|
|
42
|
+
for (const s of slots) {
|
|
43
|
+
const v = values[s.part];
|
|
44
|
+
out[s.part] = typeof v === "string" && s.fills.some((f) => f.name === v) ? v : s.default;
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
// The post a state is decided by: the accepted look at its version in use,
|
|
49
|
+
// else the open one.
|
|
50
|
+
export function postForState(posts, state) {
|
|
51
|
+
const inState = posts.filter((p) => p.state === state);
|
|
52
|
+
const post = inState.find((p) => p.status === "accepted") ??
|
|
53
|
+
inState.find((p) => p.status === "open") ??
|
|
54
|
+
inState[0];
|
|
55
|
+
return post && (postAtVersion(post, inUseVersion(post)) ?? post);
|
|
56
|
+
}
|
|
57
|
+
function tree(slots, chosen, parent, mix) {
|
|
58
|
+
const out = {};
|
|
59
|
+
for (const s of slots) {
|
|
60
|
+
const same = parent
|
|
61
|
+
? s.parent?.part === parent.part && s.parent.fill === parent.fill
|
|
62
|
+
: s.parent === null;
|
|
63
|
+
if (!same)
|
|
64
|
+
continue;
|
|
65
|
+
const fill = chosen[s.part];
|
|
66
|
+
const label = s.fills.find((f) => f.name === fill)?.label;
|
|
67
|
+
const inner = tree(slots, chosen, { part: s.part, fill }, mix);
|
|
68
|
+
out[s.part] = {
|
|
69
|
+
fill,
|
|
70
|
+
...(label ? { label } : {}),
|
|
71
|
+
...(mix[s.part] ? { from: mix[s.part] } : {}),
|
|
72
|
+
...(Object.keys(inner).length ? { slots: inner } : {}),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
// Null when the mock has no slots and the reply set no knobs: then the
|
|
78
|
+
// answered asks already say everything.
|
|
79
|
+
export function decisionsFor(mock, posts, reply) {
|
|
80
|
+
const values = replyValues(reply, mock);
|
|
81
|
+
const states = {};
|
|
82
|
+
const slotPaths = new Set();
|
|
83
|
+
for (const state of mock.states.length ? mock.states : [null]) {
|
|
84
|
+
const post = postForState(posts, state);
|
|
85
|
+
if (!post)
|
|
86
|
+
continue;
|
|
87
|
+
const slots = slotsInSurfaces(post.surfaces);
|
|
88
|
+
for (const s of slots)
|
|
89
|
+
slotPaths.add(s.part);
|
|
90
|
+
states[state ?? ""] = {
|
|
91
|
+
variant: post.variant,
|
|
92
|
+
slots: tree(slots, chosenFills(slots, values), null, reply.mix ?? {}),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const knobs = Object.fromEntries(Object.entries(values).filter(([p]) => !slotPaths.has(p)));
|
|
96
|
+
if (!slotPaths.size && !Object.keys(knobs).length)
|
|
97
|
+
return null;
|
|
98
|
+
return { states, knobs };
|
|
99
|
+
}
|
|
@@ -29,7 +29,7 @@ export function iconsLine(design) {
|
|
|
29
29
|
}
|
|
30
30
|
const TOPIC_SUMMARY = {
|
|
31
31
|
knobs: "every knob shape, how values reach html",
|
|
32
|
-
asks: "the ask JSON, scopes,
|
|
32
|
+
asks: "the ask JSON, scopes, slots (data-fill), the Look ask, multi",
|
|
33
33
|
surfaces: "markdown, diff, mermaid, code, terminal, json, image; uploads",
|
|
34
34
|
html: "the contract, finish rules, tokens, kits, Tailwind, icons",
|
|
35
35
|
feedback: "the feedback JSON, pending, delivery, export",
|
|
@@ -227,7 +227,10 @@ project › mock › state › variant › version. A project is the repo; a moc
|
|
|
227
227
|
page or component, by slug; a state is one moment of it in the user's words
|
|
228
228
|
("Writing"), omitted for a single-state mock; a variant is a parallel design of
|
|
229
229
|
a state; versions are its history. A part is an element marked \`data-part\`.
|
|
230
|
-
|
|
230
|
+
The user answers asks, tunes knobs, presses Send once; you get one reply.
|
|
231
|
+
Name a state by what the user sees on that screen ("Home, idle", "Proposal
|
|
232
|
+
shown"), never by where it is in your process ("Prototype", "v2", "Round 2"):
|
|
233
|
+
the viewer lists states as the screens of the mock.
|
|
231
234
|
|
|
232
235
|
## The loop
|
|
233
236
|
|
|
@@ -241,11 +244,12 @@ mockpit publish --mock writer --state "Writing" --variant dark --html dark.html
|
|
|
241
244
|
mockpit ask --mock writer "Which look?" --option Quiet=quiet --option Dark=dark
|
|
242
245
|
mockpit feedback # after the user says they answered
|
|
243
246
|
mockpit publish --mock writer --state "Writing" --variant dark --parts body=b.html # v2, one part
|
|
244
|
-
mockpit
|
|
247
|
+
mockpit publish --mock writer --state "Writing" --variant dark --fills bar#tabs=t.html
|
|
248
|
+
mockpit export --mock writer # the chosen html per state
|
|
245
249
|
\`\`\`
|
|
246
250
|
|
|
247
251
|
Server: \`$MOCKPIT_URL\` (default http://localhost:8228; \`mockpit serve\`).
|
|
248
|
-
MCP tools
|
|
252
|
+
MCP tools share the verb names.
|
|
249
253
|
|
|
250
254
|
## Parts
|
|
251
255
|
|
|
@@ -254,10 +258,37 @@ feedback on. The same name in two states is the same part. \`data-part-key\`
|
|
|
254
258
|
tells list items apart and keeps comments anchored across a rename. Never
|
|
255
259
|
declare geometry; the viewer measures it.
|
|
256
260
|
|
|
257
|
-
##
|
|
261
|
+
## The design is the UI
|
|
258
262
|
|
|
259
|
-
|
|
260
|
-
|
|
263
|
+
Publish the screen alone, laid out for the width it ships at: no captions,
|
|
264
|
+
side notes, labels or rationale beside it (they shrink the design on a phone).
|
|
265
|
+
Say what a variant is in \`--note "System font, thin bars; feels like a tool"\`;
|
|
266
|
+
the viewer shows it on the variant and its option. Name the widths the user
|
|
267
|
+
ships to once, \`--widths 390,1280\`, and the design is shown at each, side by
|
|
268
|
+
side; without them the viewer picks one for the screen it is on.
|
|
269
|
+
Screens the user moves between, even across mocks, are a flow: publish each
|
|
270
|
+
with \`--flow Morning --step 2\` (\`--step 0\` takes it out); the viewer walks it.
|
|
271
|
+
|
|
272
|
+
## Build from components
|
|
273
|
+
|
|
274
|
+
A screen made of reusable pieces (a button, a card, a header, a day strip) is
|
|
275
|
+
built from them: publish each piece as its own mock (\`--kind component\`), and
|
|
276
|
+
the screen (\`--kind page\`) embeds it with \`<mockpit-embed slug="card">\`.
|
|
277
|
+
Vary a piece in its own mock, not by copying it into every page: the user
|
|
278
|
+
sees every page it is in, a decision on it reaches all of them, and Home's
|
|
279
|
+
Components tab counts the screens it is in. Topic \`surfaces\` has the details.
|
|
280
|
+
|
|
281
|
+
## Ask, slot or knob
|
|
282
|
+
|
|
283
|
+
Answers drive the screen. A choice is several variants plus one ask that binds
|
|
284
|
+
them (ask it first), or a slot: one region's alternatives in one html, sharing
|
|
285
|
+
\`data-part\`, each its own \`data-fill\` (\`<nav data-part="bar" data-fill="thin">\`).
|
|
286
|
+
The viewer shows the chosen fill. A fill may hold slots, asked only while it is
|
|
287
|
+
chosen. Send publish's \`suggestedSlotAsks\` (\`set: {"bar": "tabs"}\`). A variant
|
|
288
|
+
decides all it differs on; don't re-ask it. Visual options carry a \`variant\`
|
|
289
|
+
or knob \`set\`. One render plus a control: a knob,
|
|
290
|
+
\`--knobs '{...}'\`. Declare knobs for what is worth fine-tuning (size,
|
|
291
|
+
spacing, colour, density) even unasked; they wait in Tune.
|
|
261
292
|
|
|
262
293
|
| knob | control |
|
|
263
294
|
| --- | --- |
|
|
@@ -267,25 +298,31 @@ first. One render plus a control: declare a knob with \`--knobs '{...}'\`.
|
|
|
267
298
|
| \`"trim.position": {"type": "select", "options": ["top", "bottom"]}\` | select |
|
|
268
299
|
|
|
269
300
|
Values reach the html unitless: \`calc(var(--k-body-size, 17) * 1px)\`, and as
|
|
270
|
-
\`data-k-trim-position\` on \`<html>\`.
|
|
301
|
+
\`data-k-trim-position\` on \`<html>\`.
|
|
271
302
|
|
|
272
303
|
## Feedback
|
|
273
304
|
|
|
274
305
|
\`feedback\` returns \`{feedback, pending}\`. A batch is \`{mock, reply: {asks, tuned,
|
|
275
|
-
mix, comments, decision, text}, accepted, archived}\`. In order: answers
|
|
276
|
-
structure (a variant answer already accepted it
|
|
277
|
-
|
|
278
|
-
address each comment on its part; decision
|
|
279
|
-
answers when there were no asks
|
|
306
|
+
mix, comments, reactions, decision, text}, accepted, archived}\`. In order: answers
|
|
307
|
+
decide structure (a variant answer already accepted it); tuned values become
|
|
308
|
+
defaults; mix names a part to take from another variant (if it conflicts with an
|
|
309
|
+
answer, the answer wins); address each comment and reaction on its part; decision
|
|
310
|
+
(accept, revise, drop) replaces answers when there were no asks; \`decisions\` is
|
|
311
|
+
the screen as chosen, per state: variant, then each slot's fill, nested. Each Send arrives exactly once: in a write's
|
|
280
312
|
\`feedback\`, \`mockpit feedback\` or a \`mockpit watch\` line.
|
|
281
|
-
A \`pending\` draft means the user is still answering.
|
|
282
|
-
|
|
313
|
+
A \`pending\` draft means the user is still answering. Send a nudge's
|
|
314
|
+
\`suggestedAsk\` with \`ask\`. A batch with \`resolved: true\` (no reply) means
|
|
315
|
+
the user closed the mock unanswered: its \`dismissed\` asks were not worth
|
|
316
|
+
answering. Don't ask them again, under any id; \`ask\` skips a dismissed id.
|
|
317
|
+
The viewer shows each state its own asks and only open ones; a sent answer is
|
|
318
|
+
not shown again.
|
|
283
319
|
|
|
284
320
|
## HTML
|
|
285
321
|
|
|
286
322
|
Send a body fragment: no doctype, html, head or body. Keep content in normal
|
|
287
323
|
flow: no \`position: fixed\`, no absolute layers over a fixed height; grid-stack
|
|
288
|
-
(\`grid-area: 1/1\`) to overlap.
|
|
324
|
+
(\`grid-area: 1/1\`) to overlap. A web font is decoration: the page must read
|
|
325
|
+
without it. Never hardcode a color; use the tokens below so
|
|
289
326
|
dark and light both work. External loads only from cdnjs.cloudflare.com, esm.sh,
|
|
290
327
|
cdn.jsdelivr.net, unpkg.com and Google Fonts.
|
|
291
328
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Embed } from "./types.ts";
|
|
2
|
+
export interface EmbedTag {
|
|
3
|
+
slug: string;
|
|
4
|
+
variant: string | null;
|
|
5
|
+
version: number | null;
|
|
6
|
+
}
|
|
7
|
+
export interface ResolvedEmbed {
|
|
8
|
+
variant: string;
|
|
9
|
+
version: number;
|
|
10
|
+
html: string;
|
|
11
|
+
attrs?: string;
|
|
12
|
+
stamp?: string;
|
|
13
|
+
}
|
|
14
|
+
export type EmbedResolver = (tag: EmbedTag) => Promise<ResolvedEmbed | null>;
|
|
15
|
+
export declare const MAX_EMBED_DEPTH = 6;
|
|
16
|
+
export declare function parseEmbedTags(html: string): EmbedTag[];
|
|
17
|
+
export declare const hasEmbedTags: (html: string) => boolean;
|
|
18
|
+
export interface Expanded {
|
|
19
|
+
html: string;
|
|
20
|
+
used: Embed[];
|
|
21
|
+
stamps: string[];
|
|
22
|
+
cycle: string[] | null;
|
|
23
|
+
}
|
|
24
|
+
export declare function expandHtml(html: string, resolve: EmbedResolver, stack: string[]): Promise<Expanded>;
|
|
25
|
+
export declare function embedsKey({ used, stamps }: Pick<Expanded, "used" | "stamps">): string;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Composition: `<mockpit-embed slug="…" variant="…" version="…">` tags in any
|
|
2
|
+
// mock's html are expanded server-side by inlining the referenced variant
|
|
3
|
+
// version's first html surface body, recursively, so a component can be built
|
|
4
|
+
// from components. A tag without `version` follows the version in use when the
|
|
5
|
+
// variant it resolves to is accepted, else its latest; one without `variant`
|
|
6
|
+
// follows the component's accepted variant.
|
|
7
|
+
// Publish records the versions it saw on the post (`Post.embeds`), so what a
|
|
8
|
+
// version rendered with stays answerable.
|
|
9
|
+
//
|
|
10
|
+
// Runtime-agnostic (no node imports) and string-only: the result is handed to
|
|
11
|
+
// renderHtmlPage, which serves it from /s/:id under the sandbox CSP header, so
|
|
12
|
+
// an inlined component body is exactly as contained as the page's own markup.
|
|
13
|
+
// `<mockpit-slot>`, the tag's first name, is still read: "slot" now means a
|
|
14
|
+
// part with alternative fills (fills.ts).
|
|
15
|
+
// Deep enough for page › section › component › control; past it a chain is
|
|
16
|
+
// almost certainly a mistake and costs a render each time.
|
|
17
|
+
export const MAX_EMBED_DEPTH = 6;
|
|
18
|
+
const EMBED_TAG = /<mockpit-(?:embed|slot)\b([^>]*?)(?:\/>|>\s*<\/mockpit-(?:embed|slot)\s*>|>)/gi;
|
|
19
|
+
const ATTR = /([a-z-]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+))/gi;
|
|
20
|
+
function attributes(raw) {
|
|
21
|
+
const out = {};
|
|
22
|
+
ATTR.lastIndex = 0;
|
|
23
|
+
let m;
|
|
24
|
+
while ((m = ATTR.exec(raw)))
|
|
25
|
+
out[m[1].toLowerCase()] = m[2] ?? m[3] ?? m[4] ?? "";
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
function tagOf(attrRaw) {
|
|
29
|
+
const attrs = attributes(attrRaw);
|
|
30
|
+
const slug = attrs.slug ?? "";
|
|
31
|
+
if (!slug)
|
|
32
|
+
return null;
|
|
33
|
+
const version = Number(attrs.version);
|
|
34
|
+
return {
|
|
35
|
+
slug,
|
|
36
|
+
variant: attrs.variant || null,
|
|
37
|
+
version: Number.isInteger(version) && version > 0 ? version : null,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export function parseEmbedTags(html) {
|
|
41
|
+
const tags = [];
|
|
42
|
+
EMBED_TAG.lastIndex = 0;
|
|
43
|
+
let m;
|
|
44
|
+
while ((m = EMBED_TAG.exec(html))) {
|
|
45
|
+
const tag = tagOf(m[1] ?? "");
|
|
46
|
+
if (tag)
|
|
47
|
+
tags.push(tag);
|
|
48
|
+
}
|
|
49
|
+
return tags;
|
|
50
|
+
}
|
|
51
|
+
export const hasEmbedTags = (html) => /<mockpit-(?:embed|slot)\b/i.test(html);
|
|
52
|
+
const escapeAttr = (s) => s.replace(/&/g, "&").replace(/"/g, """);
|
|
53
|
+
// Replace each tag with its resolved body, expanding that body's own tags in
|
|
54
|
+
// turn. `stack` is the chain of slugs being expanded (the root mock's first),
|
|
55
|
+
// so a component that reaches back up renders a marked placeholder instead of
|
|
56
|
+
// recursing. An unresolvable embed renders as an empty marked-up placeholder
|
|
57
|
+
// rather than vanishing, so a broken reference is visible in review instead of
|
|
58
|
+
// silently changing the layout.
|
|
59
|
+
export async function expandHtml(html, resolve, stack) {
|
|
60
|
+
const used = [];
|
|
61
|
+
const stamps = [];
|
|
62
|
+
let cycle = null;
|
|
63
|
+
const matches = [];
|
|
64
|
+
EMBED_TAG.lastIndex = 0;
|
|
65
|
+
let m;
|
|
66
|
+
while ((m = EMBED_TAG.exec(html)))
|
|
67
|
+
matches.push({ tag: tagOf(m[1] ?? "") });
|
|
68
|
+
const bodies = [];
|
|
69
|
+
for (const { tag } of matches) {
|
|
70
|
+
if (!tag) {
|
|
71
|
+
bodies.push(null);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const label = (variant, version) => `data-mockpit-embed="${escapeAttr(tag.slug)}"${variant ? ` data-mockpit-variant="${escapeAttr(variant)}"` : ""}${version == null ? "" : ` data-mockpit-version="${version}"`}`;
|
|
75
|
+
if (stack.includes(tag.slug)) {
|
|
76
|
+
cycle ??= [...stack.slice(stack.indexOf(tag.slug)), tag.slug];
|
|
77
|
+
bodies.push(`<div ${label(tag.variant, tag.version)} data-mockpit-cycle="1"></div>`);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (stack.length >= MAX_EMBED_DEPTH) {
|
|
81
|
+
bodies.push(`<div ${label(tag.variant, tag.version)} data-mockpit-too-deep="1"></div>`);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const resolved = await resolve(tag);
|
|
85
|
+
if (!resolved) {
|
|
86
|
+
bodies.push(`<div ${label(tag.variant, tag.version)} data-mockpit-missing="1"></div>`);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
used.push({
|
|
90
|
+
slug: tag.slug,
|
|
91
|
+
variant: resolved.variant,
|
|
92
|
+
version: resolved.version,
|
|
93
|
+
...(tag.version != null ? { pinned: true } : {}),
|
|
94
|
+
});
|
|
95
|
+
stamps.push(resolved.stamp ?? "");
|
|
96
|
+
const inner = await expandHtml(resolved.html, resolve, [...stack, tag.slug]);
|
|
97
|
+
used.push(...inner.used);
|
|
98
|
+
stamps.push(...inner.stamps);
|
|
99
|
+
cycle ??= inner.cycle;
|
|
100
|
+
bodies.push(`<div ${label(resolved.variant, resolved.version)}${resolved.attrs ?? ""}>${inner.html}</div>`);
|
|
101
|
+
}
|
|
102
|
+
let i = 0;
|
|
103
|
+
EMBED_TAG.lastIndex = 0;
|
|
104
|
+
const out = html.replace(EMBED_TAG, (raw) => bodies[i++] ?? raw);
|
|
105
|
+
return { html: out, used, stamps, cycle };
|
|
106
|
+
}
|
|
107
|
+
// A short, stable key for what a document's embeds resolved to: part of the
|
|
108
|
+
// render cache key and of the frame URL, so a component's new version reloads
|
|
109
|
+
// every page that shows it and a stale document is never served as current.
|
|
110
|
+
export function embedsKey({ used, stamps }) {
|
|
111
|
+
let h = 0x811c9dc5;
|
|
112
|
+
const s = used.map((e, i) => `${e.slug}:${e.variant}@${e.version}:${stamps[i] ?? ""}`).join("|");
|
|
113
|
+
for (let i = 0; i < s.length; i++) {
|
|
114
|
+
h ^= s.charCodeAt(i);
|
|
115
|
+
h = Math.imul(h, 0x01000193);
|
|
116
|
+
}
|
|
117
|
+
return (h >>> 0).toString(36);
|
|
118
|
+
}
|
package/dist/server/eventLog.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// the agent was told was pending. Shapes and ids only, never html or comment
|
|
5
5
|
// text: the store already holds the content under the same ids, and a log that
|
|
6
6
|
// copied it would be a second, unbounded store. `scripts/inspect.ts` reads it.
|
|
7
|
+
import { isViewerRequest } from "./viewerRequest.js";
|
|
7
8
|
const MAX_ERROR = 300;
|
|
8
9
|
const CLIENT = /^[\w.@/-]{1,64}$/;
|
|
9
10
|
export function clientOf(header, viewer, mcp) {
|
|
@@ -112,7 +113,7 @@ export function logMiddleware(sink) {
|
|
|
112
113
|
return async (c, next) => {
|
|
113
114
|
const start = Date.now();
|
|
114
115
|
await next();
|
|
115
|
-
const viewer = c.req.
|
|
116
|
+
const viewer = isViewerRequest(c.req.raw);
|
|
116
117
|
if (!loggable(c, viewer))
|
|
117
118
|
return;
|
|
118
119
|
try {
|