mockpit 3.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.
Files changed (54) hide show
  1. package/README.md +11 -23
  2. package/bin/initDesign.js +11 -16
  3. package/bin/mockpit.js +242 -52
  4. package/bin/poster.js +64 -7
  5. package/dist/mcp/server.js +48 -6
  6. package/dist/server/apiViews.d.ts +562 -557
  7. package/dist/server/apiViews.js +18 -9
  8. package/dist/server/app.js +723 -206
  9. package/dist/server/cdnInline.js +6 -4
  10. package/dist/server/codeWorker.js +2 -2
  11. package/dist/server/contrast.d.ts +4 -0
  12. package/dist/server/contrast.js +157 -0
  13. package/dist/server/decisionList.d.ts +4 -1
  14. package/dist/server/decisionList.js +43 -5
  15. package/dist/server/designGuide.d.ts +13 -6
  16. package/dist/server/designGuide.js +206 -41
  17. package/dist/server/events.d.ts +6 -0
  18. package/dist/server/feedbackBatch.d.ts +4 -0
  19. package/dist/server/feedbackBatch.js +19 -1
  20. package/dist/server/guidance.d.ts +1 -1
  21. package/dist/server/guidance.js +2 -2
  22. package/dist/server/kits.d.ts +2 -2
  23. package/dist/server/kits.js +54 -1
  24. package/dist/server/mcpHttp.d.ts +2 -0
  25. package/dist/server/mcpHttp.js +13 -1
  26. package/dist/server/mcpSpec.d.ts +27 -27
  27. package/dist/server/mcpSpec.js +78 -10
  28. package/dist/server/run.js +22 -1
  29. package/dist/server/runApi.d.ts +2 -2
  30. package/dist/server/runApi.js +9 -2
  31. package/dist/server/sqlStore.d.ts +68 -39
  32. package/dist/server/sqlStore.js +163 -26
  33. package/dist/server/surfacePage.d.ts +4 -2
  34. package/dist/server/surfacePage.js +80 -32
  35. package/dist/server/system.d.ts +234 -0
  36. package/dist/server/system.js +1508 -0
  37. package/dist/server/systemChecks.d.ts +15 -0
  38. package/dist/server/systemChecks.js +224 -0
  39. package/dist/server/systemSpecimen.d.ts +2 -0
  40. package/dist/server/systemSpecimen.js +49 -0
  41. package/dist/server/themes.d.ts +9 -7
  42. package/dist/server/themes.js +43 -90
  43. package/dist/server/types.d.ts +43 -24
  44. package/dist/server/types.js +3 -1
  45. package/guide/AGENT_SETUP.md +3 -1
  46. package/guide/packs/foundations.md +35 -0
  47. package/guide/topics/asks.md +2 -1
  48. package/guide/topics/feedback.md +5 -3
  49. package/guide/topics/html.md +2 -2
  50. package/guide/topics/http.md +3 -1
  51. package/guide/topics/system.md +157 -0
  52. package/package.json +12 -12
  53. package/skills/mockpit/SKILL.md +7 -4
  54. package/viewer/dist/index.html +9 -8
@@ -409,9 +409,11 @@ export function createCdnInliner() {
409
409
  last = at + m[0].length;
410
410
  const raw = m[1] ?? m[2] ?? m[3] ?? m[4] ?? m[5] ?? "";
411
411
  const url = onCdn(raw, base);
412
- // A media or layer qualifier would need wrapping, so it keeps its link.
413
- const qualified = !!m[6]?.trim();
414
- if (!url || qualified || depth >= MAX_IMPORT_DEPTH) {
412
+ // A layer is wrapped as a block, which says the same; a media or
413
+ // supports qualifier would need more, so it keeps its link.
414
+ const qualifier = m[6]?.trim() ?? "";
415
+ const layer = /^layer\(\s*([A-Za-z_][\w.-]*)\s*\)$/.exec(qualifier)?.[1];
416
+ if (!url || (qualifier && !layer) || depth >= MAX_IMPORT_DEPTH) {
415
417
  imports += `${m[0]}\n`;
416
418
  continue;
417
419
  }
@@ -421,7 +423,7 @@ export function createCdnInliner() {
421
423
  continue;
422
424
  }
423
425
  imports += inlined.imports;
424
- importedBody += `${inlined.body}\n`;
426
+ importedBody += layer ? `@layer ${layer}{${inlined.body}}\n` : `${inlined.body}\n`;
425
427
  }
426
428
  rest += absolute.slice(last);
427
429
  return { imports, body: `${importedBody}${await rewriteFaces(rest, base)}` };
@@ -25,8 +25,8 @@ const PRELUDE = `(function (call, out) {
25
25
  };
26
26
  globalThis.mockpit = Object.freeze({
27
27
  guide: fn("guide"), read: fn("read"), publish: fn("publish"), ask: fn("ask"),
28
- feedback: fn("feedback"), say: fn("say"), move: fn("move"), edit: fn("edit"), export: fn("export"),
29
- upload: fn("upload"),
28
+ feedback: fn("feedback"), say: fn("say"), move: fn("move"), edit: fn("edit"), system: fn("system"),
29
+ export: fn("export"), upload: fn("upload"),
30
30
  });
31
31
  globalThis.print = print;
32
32
  globalThis.console = Object.freeze({ log: print, info: print, warn: print, error: print });
@@ -0,0 +1,4 @@
1
+ type Rgba = [r: number, g: number, b: number, a: number];
2
+ export declare function parseColor(input: string): Rgba | null;
3
+ export declare function contrastRatio(fg: string, bg: string): number | null;
4
+ export {};
@@ -0,0 +1,157 @@
1
+ // WCAG 2.2 contrast between two CSS colours, for the system's declared pairs.
2
+ // Five notations resolve: hex, rgb(), hsl(), oklch() and oklab(). Anything else
3
+ // (a var(), color-mix(), a named colour past the few below) is unresolved, and
4
+ // the caller reports the pair as unchecked rather than passing it.
5
+ // Runtime-agnostic.
6
+ const NAMED = {
7
+ white: [1, 1, 1, 1],
8
+ black: [0, 0, 0, 1],
9
+ transparent: [0, 0, 0, 0],
10
+ };
11
+ const clamp01 = (n) => Math.min(1, Math.max(0, n));
12
+ // A number, a percentage of `full`, or `none` (0).
13
+ function channel(raw, full) {
14
+ const s = raw.trim();
15
+ if (s === "none")
16
+ return 0;
17
+ const pct = /^(-?\d*\.?\d+)%$/.exec(s);
18
+ if (pct)
19
+ return (Number(pct[1]) / 100) * full;
20
+ const n = /^(-?\d*\.?\d+(?:e-?\d+)?)$/.exec(s);
21
+ return n ? Number(n[1]) : null;
22
+ }
23
+ function hue(raw) {
24
+ const m = /^(-?\d*\.?\d+)(deg|turn|rad|grad)?$/.exec(raw.trim());
25
+ if (!m)
26
+ return raw.trim() === "none" ? 0 : null;
27
+ const n = Number(m[1]);
28
+ switch (m[2]) {
29
+ case "turn":
30
+ return n * 360;
31
+ case "rad":
32
+ return (n * 180) / Math.PI;
33
+ case "grad":
34
+ return n * 0.9;
35
+ default:
36
+ return n;
37
+ }
38
+ }
39
+ // `fn(a b c / d)` or the legacy `fn(a, b, c, d)` into its parts.
40
+ function args(body) {
41
+ const [main, alpha] = body.includes("/") ? body.split("/") : [body, null];
42
+ const parts = main.includes(",")
43
+ ? main.split(",").map((s) => s.trim())
44
+ : main.trim().split(/\s+/);
45
+ if (parts.length === 4 && alpha === null && main.includes(","))
46
+ return { parts: parts.slice(0, 3), alpha: parts[3] };
47
+ if (parts.length !== 3)
48
+ return null;
49
+ return { parts, alpha: alpha?.trim() ?? null };
50
+ }
51
+ const linearToSrgb = (c) => (c <= 0.0031308 ? 12.92 * c : 1.055 * c ** (1 / 2.4) - 0.055);
52
+ function oklabToRgb(L, a, b) {
53
+ const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
54
+ const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
55
+ const s_ = L - 0.0894841775 * a - 1.291485548 * b;
56
+ const l = l_ ** 3;
57
+ const m = m_ ** 3;
58
+ const s = s_ ** 3;
59
+ return [
60
+ clamp01(linearToSrgb(4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s)),
61
+ clamp01(linearToSrgb(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s)),
62
+ clamp01(linearToSrgb(-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s)),
63
+ ];
64
+ }
65
+ function hslToRgb(h, s, l) {
66
+ const k = (n) => (n + h / 30) % 12;
67
+ const a = s * Math.min(l, 1 - l);
68
+ const f = (n) => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
69
+ return [f(0), f(8), f(4)];
70
+ }
71
+ export function parseColor(input) {
72
+ const v = input.trim().toLowerCase();
73
+ if (NAMED[v])
74
+ return NAMED[v];
75
+ const hex = /^#([0-9a-f]{3,8})$/.exec(v);
76
+ if (hex) {
77
+ let h = hex[1];
78
+ if (h.length === 3 || h.length === 4)
79
+ h = [...h].map((c) => c + c).join("");
80
+ if (h.length !== 6 && h.length !== 8)
81
+ return null;
82
+ const n = (i) => parseInt(h.slice(i, i + 2), 16) / 255;
83
+ return [n(0), n(2), n(4), h.length === 8 ? n(6) : 1];
84
+ }
85
+ const fn = /^([a-z]+)\((.*)\)$/.exec(v);
86
+ if (!fn)
87
+ return null;
88
+ const parsed = args(fn[2]);
89
+ if (!parsed)
90
+ return null;
91
+ const alpha = parsed.alpha === null ? 1 : channel(parsed.alpha, 1);
92
+ if (alpha === null)
93
+ return null;
94
+ const [p0, p1, p2] = parsed.parts;
95
+ switch (fn[1]) {
96
+ case "rgb":
97
+ case "rgba": {
98
+ const c = [channel(p0, 255), channel(p1, 255), channel(p2, 255)];
99
+ if (c.some((x) => x === null))
100
+ return null;
101
+ return [clamp01(c[0] / 255), clamp01(c[1] / 255), clamp01(c[2] / 255), clamp01(alpha)];
102
+ }
103
+ case "hsl":
104
+ case "hsla": {
105
+ const h = hue(p0);
106
+ const s = channel(p1.endsWith("%") ? p1 : `${p1}%`, 1);
107
+ const l = channel(p2.endsWith("%") ? p2 : `${p2}%`, 1);
108
+ if (h === null || s === null || l === null)
109
+ return null;
110
+ return [...hslToRgb(((h % 360) + 360) % 360, clamp01(s), clamp01(l)), clamp01(alpha)];
111
+ }
112
+ case "oklab": {
113
+ const L = channel(p0, 1);
114
+ const a = channel(p1, 0.4);
115
+ const b = channel(p2, 0.4);
116
+ if (L === null || a === null || b === null)
117
+ return null;
118
+ return [...oklabToRgb(L, a, b), clamp01(alpha)];
119
+ }
120
+ case "oklch": {
121
+ const L = channel(p0, 1);
122
+ const C = channel(p1, 0.4);
123
+ const h = hue(p2);
124
+ if (L === null || C === null || h === null)
125
+ return null;
126
+ const r = (h * Math.PI) / 180;
127
+ return [...oklabToRgb(L, C * Math.cos(r), C * Math.sin(r)), clamp01(alpha)];
128
+ }
129
+ default:
130
+ return null;
131
+ }
132
+ }
133
+ const luminance = ([r, g, b]) => {
134
+ const lin = (c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
135
+ return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
136
+ };
137
+ // A translucent colour is what it shows over what's under it.
138
+ const over = (top, under) => [
139
+ top[0] * top[3] + under[0] * (1 - top[3]),
140
+ top[1] * top[3] + under[1] * (1 - top[3]),
141
+ top[2] * top[3] + under[2] * (1 - top[3]),
142
+ 1,
143
+ ];
144
+ // The ratio, rounded to two places, or null when either colour doesn't resolve.
145
+ // A translucent background sits on white, the frame's canvas.
146
+ export function contrastRatio(fg, bg) {
147
+ const f = parseColor(fg);
148
+ const b0 = parseColor(bg);
149
+ if (!f || !b0)
150
+ return null;
151
+ const b = b0[3] < 1 ? over(b0, [1, 1, 1, 1]) : b0;
152
+ const top = f[3] < 1 ? over(f, b) : f;
153
+ const l1 = luminance(top);
154
+ const l2 = luminance(b);
155
+ const ratio = (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
156
+ return Math.round(ratio * 100) / 100;
157
+ }
@@ -1,4 +1,5 @@
1
1
  import { type DecisionSources, type Design } from "./types.ts";
2
+ import type { System } from "./system.ts";
2
3
  export interface DecisionParts {
3
4
  asked: string;
4
5
  chose?: string;
@@ -15,6 +16,7 @@ export interface DecisionLine {
15
16
  parts?: DecisionParts;
16
17
  edited?: true;
17
18
  archived?: true;
19
+ system?: true;
18
20
  at?: string;
19
21
  }
20
22
  export type DecisionEdits = Record<string, {
@@ -23,7 +25,7 @@ export type DecisionEdits = Record<string, {
23
25
  removed?: true;
24
26
  }>;
25
27
  export declare function parseDecisionEdits(raw: string | null): DecisionEdits;
26
- export declare function deriveDecisionLines(designs: Design[], sources: DecisionSources): (DecisionLine & {
28
+ export declare function deriveDecisionLines(designs: Design[], sources: DecisionSources, system?: System | null): (DecisionLine & {
27
29
  basis: string;
28
30
  })[];
29
31
  export declare function applyDecisionEdits(lines: (DecisionLine & {
@@ -32,6 +34,7 @@ export declare function applyDecisionEdits(lines: (DecisionLine & {
32
34
  export interface AgentDecisionLine {
33
35
  design: string;
34
36
  text: string;
37
+ system?: true;
35
38
  here?: true;
36
39
  edited?: true;
37
40
  archived?: true;
@@ -177,7 +177,7 @@ function standingBasis(design, ask) {
177
177
  // Every line the project's replies add up to, as derived: newest decided design
178
178
  // first, an archived design after every live one, each design's lines as it asked them
179
179
  // with what it put in use first.
180
- export function deriveDecisionLines(designs, sources) {
180
+ export function deriveDecisionLines(designs, sources, system = null) {
181
181
  const byDesign = new Map();
182
182
  for (const v of sources.variants) {
183
183
  const list = byDesign.get(v.designId);
@@ -188,7 +188,7 @@ export function deriveDecisionLines(designs, sources) {
188
188
  }
189
189
  const order = [...designs].sort((a, b) => Number(!!a.archivedAt) - Number(!!b.archivedAt) ||
190
190
  (sources.decidedAt[b.id] ?? "").localeCompare(sources.decidedAt[a.id] ?? ""));
191
- const out = [];
191
+ const out = systemLines(system);
192
192
  for (const design of order) {
193
193
  const variants = byDesign.get(design.id) ?? [];
194
194
  const base = {
@@ -234,6 +234,39 @@ export function deriveDecisionLines(designs, sources) {
234
234
  }
235
235
  return out;
236
236
  }
237
+ // The project's own decisions, first: they hold on every design. A palette line
238
+ // only once there are palettes to choose between.
239
+ function systemLines(system) {
240
+ if (!system)
241
+ return [];
242
+ const base = { design: "", title: "System", system: true };
243
+ const out = [];
244
+ const palettes = system.color.palettes;
245
+ const def = palettes.find((p) => p.status === "default");
246
+ if (def && palettes.length > 1) {
247
+ const named = (status) => palettes.filter((p) => p.status === status).map((p) => p.label || p.id);
248
+ const kept = named("kept");
249
+ const hidden = named("hidden");
250
+ const text = [
251
+ `Palette: ${def.label || def.id} by default.`,
252
+ kept.length ? ` Kept: ${kept.join(", ")}.` : "",
253
+ hidden.length ? ` Not: ${hidden.join(", ")}.` : "",
254
+ ].join("");
255
+ out.push({
256
+ ...base,
257
+ id: "system:palette",
258
+ text,
259
+ basis: JSON.stringify(palettes.map((p) => [p.id, p.status])),
260
+ });
261
+ }
262
+ for (const [path, value] of Object.entries(system.settled)) {
263
+ if (!Object.hasOwn(system.knobs, path))
264
+ continue;
265
+ const text = `Project knob ${path}: ${typeof value === "string" ? value : JSON.stringify(value)}.`;
266
+ out.push({ ...base, id: `system:knob:${path}`, text, basis: JSON.stringify(value) });
267
+ }
268
+ return out;
269
+ }
237
270
  // The hand edits laid over the derived lines. An edit made on an older answer
238
271
  // no longer applies: the user has decided again since.
239
272
  export function applyDecisionEdits(lines, edits) {
@@ -256,9 +289,12 @@ export function applyDecisionEdits(lines, edits) {
256
289
  const AGENT_LINE_CHARS = 280;
257
290
  const AGENT_LIST_CHARS = 6000;
258
291
  export function agentDecisionList(lines, here) {
259
- const ordered = here
260
- ? [...lines.filter((l) => l.design === here), ...lines.filter((l) => l.design !== here)]
261
- : lines;
292
+ // The project's own come first wherever the read is: they hold on every design.
293
+ const ordered = [
294
+ ...lines.filter((l) => l.system),
295
+ ...lines.filter((l) => !l.system && here && l.design === here),
296
+ ...lines.filter((l) => !l.system && !(here && l.design === here)),
297
+ ];
262
298
  const kept = [];
263
299
  let used = 0;
264
300
  let clipped = false;
@@ -272,6 +308,7 @@ export function agentDecisionList(lines, here) {
272
308
  kept.push({
273
309
  design: l.design,
274
310
  text,
311
+ ...(l.system ? { system: true } : {}),
275
312
  ...(l.design === here ? { here: true } : {}),
276
313
  ...(l.edited ? { edited: true } : {}),
277
314
  ...(l.archived ? { archived: true } : {}),
@@ -295,6 +332,7 @@ export function fullAgentDecisionList(lines) {
295
332
  lines: lines.map((l) => ({
296
333
  design: l.design,
297
334
  text: l.text,
335
+ ...(l.system ? { system: true } : {}),
298
336
  ...(l.edited ? { edited: true } : {}),
299
337
  ...(l.archived ? { archived: true } : {}),
300
338
  })),
@@ -1,20 +1,27 @@
1
- import type { DesignSystem } from "./types.ts";
2
- export declare const GUIDE_TOPICS: readonly ["knobs", "asks", "surfaces", "html", "feedback", "http", "scripts"];
1
+ import { type System } from "./system.ts";
2
+ export declare const GUIDE_TOPICS: readonly ["system", "knobs", "asks", "surfaces", "html", "feedback", "http", "scripts"];
3
3
  export type GuideTopic = (typeof GUIDE_TOPICS)[number];
4
4
  export declare const isGuideTopic: (id: unknown) => id is GuideTopic;
5
5
  export declare const unknownTopicMessage: (topic: string) => string;
6
- export declare function iconsLine(designSystem: DesignSystem | null): string;
6
+ export declare function iconsLine(system: System | null): string;
7
7
  /**
8
8
  * What the repo's DESIGN.md, DTCG tokens and shadcn components.json say, as
9
9
  * init stored them. Empty when the repo has none.
10
10
  */
11
- export declare function designFilesSection(designSystem: DesignSystem | null): string;
11
+ export declare function designFilesSection(system: System | null): string;
12
+ export interface ComponentLine {
13
+ slug: string;
14
+ purpose: string;
15
+ screens: number;
16
+ }
17
+ export declare function systemSection(system: System | null, components?: ComponentLine[]): string;
18
+ export declare function renderSystemTopic(contract: string, system: System | null, components?: ComponentLine[]): string;
12
19
  /**
13
20
  * The project-aware brief. `designSystem` is the stored `design:<project>` settings,
14
21
  * or null for a project that never ran `mockpit init`; `guidance` is the
15
22
  * layers above core (server/guidance.ts), absent when none is on.
16
23
  */
17
- export declare function renderBriefGuide(designSystem: DesignSystem | null, guidance?: {
24
+ export declare function renderBriefGuide(system: System | null, guidance?: {
18
25
  line: string;
19
26
  body: string;
20
- }): string;
27
+ }, components?: ComponentLine[]): string;