mockpit 1.2.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +69 -48
  2. package/bin/demoData.js +3 -3
  3. package/bin/mockpit.js +426 -51
  4. package/dist/mcp/server.js +45 -7
  5. package/dist/server/apiViews.d.ts +407 -21
  6. package/dist/server/apiViews.js +110 -11
  7. package/dist/server/app.d.ts +5 -1
  8. package/dist/server/app.js +1454 -192
  9. package/dist/server/codeWorker.js +2 -1
  10. package/dist/server/decisions.d.ts +23 -0
  11. package/dist/server/decisions.js +100 -0
  12. package/dist/server/designGuide.d.ts +6 -2
  13. package/dist/server/designGuide.js +96 -27
  14. package/dist/server/embeds.d.ts +26 -0
  15. package/dist/server/embeds.js +144 -0
  16. package/dist/server/eventLog.js +2 -1
  17. package/dist/server/feedbackBatch.d.ts +15 -4
  18. package/dist/server/feedbackBatch.js +31 -7
  19. package/dist/server/fills.d.ts +41 -0
  20. package/dist/server/fills.js +328 -0
  21. package/dist/server/guidance.d.ts +23 -0
  22. package/dist/server/guidance.js +71 -0
  23. package/dist/server/index.js +25 -2
  24. package/dist/server/knobs.d.ts +6 -2
  25. package/dist/server/knobs.js +11 -1
  26. package/dist/server/mcpHttp.d.ts +6 -0
  27. package/dist/server/mcpHttp.js +35 -0
  28. package/dist/server/mcpSpec.d.ts +14 -13
  29. package/dist/server/mcpSpec.js +133 -18
  30. package/dist/server/postSurfaces.js +6 -12
  31. package/dist/server/richRender.js +3 -1
  32. package/dist/server/run.js +10 -1
  33. package/dist/server/runApi.d.ts +3 -3
  34. package/dist/server/runApi.js +18 -13
  35. package/dist/server/sqlStore.d.ts +97 -5
  36. package/dist/server/sqlStore.js +367 -27
  37. package/dist/server/surfacePage.d.ts +6 -2
  38. package/dist/server/surfacePage.js +142 -10
  39. package/dist/server/themes.d.ts +2 -0
  40. package/dist/server/themes.js +23 -3
  41. package/dist/server/types.d.ts +107 -16
  42. package/dist/server/types.js +73 -10
  43. package/dist/server/viewerRequest.d.ts +2 -0
  44. package/dist/server/viewerRequest.js +14 -0
  45. package/extensions/mockpit.js +179 -16
  46. package/guide/packs/design-craft.md +35 -0
  47. package/guide/topics/asks.md +82 -31
  48. package/guide/topics/feedback.md +151 -43
  49. package/guide/topics/html.md +8 -10
  50. package/guide/topics/http.md +8 -4
  51. package/guide/topics/knobs.md +4 -3
  52. package/guide/topics/scripts.md +2 -2
  53. package/guide/topics/surfaces.md +45 -11
  54. package/package.json +3 -15
  55. package/skills/launch-video/SKILL.md +2 -2
  56. package/skills/mockpit/SKILL.md +23 -7
  57. package/viewer/dist/index.html +9 -8
  58. package/dist/server/slots.d.ts +0 -14
  59. package/dist/server/slots.js +0 -71
@@ -28,10 +28,10 @@ const feedbackGuideline =
28
28
  // The never-block loop, in the words of every other tier: nothing waits for
29
29
  // the user, so the agent ends its turn and reads feedback when told.
30
30
  const loopGuidelines = [
31
- "Publish each variant with mockpit_publish (name states in the user's words; mark parts with data-part); publish again to revise.",
32
- "A choice is several variants plus one ask that binds them: mockpit_ask with options bound to the variants. One render plus a control: declare a knob.",
31
+ "Publish each variant with mockpit_publish (name states in the user's words; declare parts with data-part); publish again to revise.",
32
+ "A choice is several variants plus one question that binds them: mockpit_ask with options bound to the variants. One render plus a control: declare a knob.",
33
33
  "Tell the user in one line where to look, then end your turn. The question lives in the mock, never in chat. Never poll.",
34
- "When the user says they answered, call mockpit_feedback. Every write also returns feedback; read it.",
34
+ "A reply sent from the viewer arrives on its own as a mockpit message; act on it. If the user says they answered and none came, call mockpit_feedback. Every write also returns feedback; read it.",
35
35
  ];
36
36
 
37
37
  const surfaceSchema = {
@@ -209,18 +209,47 @@ async function requestText(path) {
209
209
  return text;
210
210
  }
211
211
 
212
+ // A poster may name an image file: it goes up as an asset, and the publish
213
+ // carries its id. Anything else is already an asset id.
214
+ async function posterId(value, ctx) {
215
+ if (typeof value !== "string" || !value) return undefined;
216
+ const file = resolve(ctx.cwd, value);
217
+ let data;
218
+ try {
219
+ data = await readFile(file);
220
+ } catch {
221
+ return value;
222
+ }
223
+ const ext = file.split(".").pop()?.toLowerCase();
224
+ const asset = await requestJson("/api/assets", {
225
+ method: "POST",
226
+ body: JSON.stringify({
227
+ data: data.toString("base64"),
228
+ contentType: ext === "jpg" || ext === "jpeg" ? "image/jpeg" : `image/${ext}`,
229
+ filename: file.split(/[\\/]/).pop(),
230
+ }),
231
+ });
232
+ return asset.id;
233
+ }
234
+
212
235
  function rememberSession(state, sessionId) {
213
236
  if (typeof sessionId === "string" && sessionId) state.sessionId = sessionId;
214
237
  }
215
238
 
216
- // `feedback` is one batch per mock: the user's reply (answers, mix, tuned
217
- // knob values, part comments) plus any plain comments. Rendered one line per
218
- // thing the user said, so nothing is summarized away.
239
+ // `feedback` is one batch per mock: the user's reply (answers, borrowed parts,
240
+ // tuned knob values, part comments, verdict) plus any thread messages.
241
+ // Rendered one line per thing the user said, so nothing is summarized away.
219
242
  function feedbackLines(feedback) {
220
243
  const lines = [];
221
244
  for (const batch of feedback ?? []) {
222
245
  const prefix = batch.mock ? `${batch.mock}: ` : "";
223
246
  if (batch.handoff) lines.push(`- ${prefix}${batch.handoff}`);
247
+ if (batch.closed) {
248
+ const asks = (batch.dismissed ?? []).map((d) => d.text || d.ask).join("; ");
249
+ lines.push(
250
+ `- ${prefix}closed by the user${asks ? `, unanswered: ${asks}; don't ask again` : ""}`,
251
+ );
252
+ }
224
253
  const reply = batch.reply;
225
254
  if (reply) {
226
255
  for (const a of reply.asks ?? []) {
@@ -228,21 +257,35 @@ function feedbackLines(feedback) {
228
257
  lines.push(`- ${prefix}${a.text || a.ask} → ${chosen || "(no pick)"}`);
229
258
  if (a.note) lines.push(` note: ${a.note}`);
230
259
  }
231
- for (const [part, variant] of Object.entries(reply.mix ?? {})) {
232
- lines.push(`- ${prefix}use ${variant}'s ${part}`);
260
+ for (const [part, variant] of Object.entries(reply.borrowed ?? {})) {
261
+ lines.push(`- ${prefix}borrow ${part} from ${variant}`);
233
262
  }
234
263
  for (const [path, value] of Object.entries(reply.tuned ?? {})) {
235
264
  lines.push(`- ${prefix}tuned ${path} = ${JSON.stringify(value)}`);
236
265
  }
237
266
  for (const c of reply.comments ?? []) {
267
+ if (c.borrow) {
268
+ lines.push(
269
+ `- ${prefix}borrow ${c.borrow.part ? `${c.borrow.part} ` : ""}from ${c.borrow.from} into ${c.variant}@${c.version}${c.text ? `: ${c.text}` : ""}`,
270
+ );
271
+ continue;
272
+ }
238
273
  lines.push(
239
274
  `- ${prefix}${c.part ? `[${c.part}${c.state ? ` · ${c.state}` : ""}] ` : ""}${c.text}`,
240
275
  );
241
276
  }
242
- if (reply.decision) lines.push(`- ${prefix}${reply.decision.kind} ${reply.decision.variant}`);
277
+ if (reply.verdict) lines.push(`- ${prefix}${reply.verdict.kind} ${reply.verdict.variant}`);
243
278
  if (reply.text) lines.push(`- ${prefix}${reply.text}`);
244
279
  }
245
- for (const c of batch.comments ?? []) lines.push(`- ${prefix}${c.text}`);
280
+ for (const c of batch.messages ?? []) lines.push(`- ${prefix}${c.text}`);
281
+ if (batch.edited) {
282
+ const e = batch.edited;
283
+ const moved = [
284
+ e.kind ? `now a ${e.kind}` : "",
285
+ e.area !== undefined ? (e.area ? `area ${e.area}` : "no area") : "",
286
+ ].filter(Boolean);
287
+ lines.push(`- ${prefix}the user moved it from Home (${moved.join(", ")})`);
288
+ }
246
289
  }
247
290
  return lines;
248
291
  }
@@ -269,6 +312,42 @@ function feedbackSummary(feedback) {
269
312
  return `\n\nUser feedback delivered with this result:\n${lines.join("\n")}`;
270
313
  }
271
314
 
315
+ const pause = (ms, signal) =>
316
+ new Promise((done) => {
317
+ const timer = setTimeout(done, ms);
318
+ signal.addEventListener("abort", () => (clearTimeout(timer), done()), { once: true });
319
+ });
320
+
321
+ // The user's reply wakes the idle agent, as the Claude Code plugin's monitor
322
+ // does. Reading feedback advances the server's cursor, so the message carries
323
+ // the whole reply: a nudge to call mockpit_feedback would come back empty.
324
+ // followUp waits for a busy agent to finish rather than cutting into its work.
325
+ async function watchReplies(pi, state, signal) {
326
+ while (!signal.aborted) {
327
+ if (!state.sessionId) {
328
+ await pause(2000, signal);
329
+ continue;
330
+ }
331
+ try {
332
+ const query = new URLSearchParams({ session: state.sessionId, author: "user", wait: "60" });
333
+ const result = await requestJson(`/api/comments?${query}`, { signal });
334
+ const lines = feedbackLines(result.feedback);
335
+ if (!lines.length || signal.aborted) continue;
336
+ pi.sendMessage(
337
+ {
338
+ customType: "mockpit-reply",
339
+ content: `The user replied in mockpit:\n${lines.join("\n")}`,
340
+ display: true,
341
+ details: { feedback: result.feedback },
342
+ },
343
+ { triggerTurn: true, deliverAs: "followUp" },
344
+ );
345
+ } catch {
346
+ await pause(2000, signal);
347
+ }
348
+ }
349
+ }
350
+
272
351
  function reconstructSession(ctx) {
273
352
  let sessionId = process.env.MOCKPIT_SESSION || undefined;
274
353
  for (const entry of ctx.sessionManager.getBranch()) {
@@ -286,9 +365,17 @@ function reconstructSession(ctx) {
286
365
 
287
366
  export default function mockpitExtension(pi) {
288
367
  const state = { sessionId: process.env.MOCKPIT_SESSION || undefined };
368
+ let watching;
289
369
 
290
370
  pi.on("session_start", (_event, ctx) => {
291
371
  state.sessionId = reconstructSession(ctx);
372
+ watching?.abort();
373
+ watching = undefined;
374
+ // print and json runs end after one prompt; nobody is there to wake.
375
+ if (ctx.mode === "tui" || ctx.mode === "rpc") {
376
+ watching = new AbortController();
377
+ void watchReplies(pi, state, watching.signal);
378
+ }
292
379
  ctx.ui.setStatus(
293
380
  "mockpit",
294
381
  state.sessionId
@@ -297,6 +384,12 @@ export default function mockpitExtension(pi) {
297
384
  );
298
385
  });
299
386
 
387
+ // After shutdown pi refuses calls from this extension, so the loop stops first.
388
+ pi.on("session_shutdown", () => {
389
+ watching?.abort();
390
+ watching = undefined;
391
+ });
392
+
300
393
  pi.registerCommand("mockpit", {
301
394
  description: "Show mockpit extension status or reset its remembered session: /mockpit [reset]",
302
395
  handler: async (args, ctx) => {
@@ -358,13 +451,23 @@ export default function mockpitExtension(pi) {
358
451
  ...mockProps,
359
452
  title: { type: "string", description: "Mock title" },
360
453
  kind: { type: "string", enum: ["component", "page"], description: "Mock kind" },
361
- html: { type: "string", description: "HTML body fragment; mark parts with data-part" },
454
+ area: {
455
+ type: "string",
456
+ description: 'Where in the product it belongs ("Shell"); Home groups by it. "" clears',
457
+ },
458
+ source: { type: "string", description: 'Code path that implements it. "" clears' },
459
+ html: { type: "string", description: "HTML body fragment; declare parts with data-part" },
362
460
  path: { type: "string", description: "File to read the html from instead" },
461
+ poster: {
462
+ type: "string",
463
+ description:
464
+ "A still of the page (image file path, or asset id) that Studio and Home show instead of running it; send one with every version that changes the page",
465
+ },
363
466
  surfaces: surfacesSchema,
364
467
  parts: {
365
468
  type: "object",
366
469
  description:
367
- '{"name"|"name#key": outer html}; splices marked parts of the latest version',
470
+ '{"name"|"name#key": outer html}; splices declared parts of the latest version',
368
471
  },
369
472
  knobs: { type: "object", description: "Knobs in tunekit usePane shape, keyed by path" },
370
473
  variantKnobs: { type: "object", description: "Knobs this variant alone has" },
@@ -378,11 +481,13 @@ export default function mockpitExtension(pi) {
378
481
  ? await readFile(resolve(ctx.cwd, params.path), "utf8")
379
482
  : params.html;
380
483
  const { path: _path, ...rest } = params;
484
+ const poster = await posterId(params.poster, ctx);
381
485
  const result = await requestJson("/api/mocks", {
382
486
  method: "POST",
383
487
  body: JSON.stringify({
384
488
  ...rest,
385
489
  html,
490
+ ...(poster === undefined ? {} : { poster }),
386
491
  project: projectOf(params, ctx),
387
492
  session: state.sessionId,
388
493
  // Only used when this publish creates the session.
@@ -424,7 +529,7 @@ export default function mockpitExtension(pi) {
424
529
  name: "mockpit_ask",
425
530
  label: "Mockpit Ask",
426
531
  description:
427
- "Ask structured questions on a mock. A choice is several variants plus one ask that binds them ({label, variant}); options may also set knob values ({label, set}). Reusing an ask id replaces it.",
532
+ "Ask structured questions on a mock. A choice is several variants plus one question that binds them ({label, variant}); options may also set knob values ({label, set}). Reusing a question id replaces it.",
428
533
  promptSnippet: "Ask the user to choose between mockpit variants.",
429
534
  promptGuidelines: [
430
535
  "Use mockpit_ask once the variants are published, tell the user where to look, then end your turn.",
@@ -515,7 +620,7 @@ export default function mockpitExtension(pi) {
515
620
  name: "mockpit_feedback",
516
621
  label: "Mockpit Feedback",
517
622
  description:
518
- "Returns at once: what the user sent since you last heard (one batch per mock: answers, mix, tuned knobs, part comments, comments, accepted/archived), delivered once, plus pending (viewer open, draft progress).",
623
+ "Returns at once: what the user sent since you last heard (one batch per mock: answers, borrowed parts, tuned knobs, part comments, verdict; thread messages; inUse/archived; or closed with the questions it dismissed), delivered once, plus pending (viewer open, draft progress).",
519
624
  promptSnippet: "Read the user's mockpit reply when they say they answered.",
520
625
  promptGuidelines: [
521
626
  "Call mockpit_feedback when the user says they answered; it returns at once, so do not call it in a loop.",
@@ -576,12 +681,70 @@ export default function mockpitExtension(pi) {
576
681
  },
577
682
  });
578
683
 
684
+ pi.registerTool({
685
+ name: "mockpit_edit",
686
+ label: "Mockpit Edit",
687
+ description:
688
+ 'Change a mock without a new version: slug, title, kind, area, source ("" clears). A new slug rewrites every embed, data-goto and flow step naming the old one. Or, with no mock, rename the project (rename) or order its areas on Home (areaOrder). Returns feedback.',
689
+ promptSnippet: "Rename, retitle or reclassify a mockpit mock, or rename the project.",
690
+ promptGuidelines: [feedbackGuideline],
691
+ parameters: {
692
+ type: "object",
693
+ properties: {
694
+ mock: { type: "string", description: "Mock slug or id" },
695
+ project: { type: "string", description: "Project name; defaults to the repo" },
696
+ slug: { type: "string", description: "New slug" },
697
+ title: { type: "string", description: "Mock title" },
698
+ kind: { type: "string", enum: ["page", "component", "sketch"] },
699
+ area: { type: "string", description: 'Area in the product; "" clears' },
700
+ source: { type: "string", description: 'Code path that implements it; "" clears' },
701
+ rename: { type: "string", description: "Without mock: the project's new name" },
702
+ areaOrder: {
703
+ type: "array",
704
+ items: { type: "string" },
705
+ description: "Without mock: Home's area order; unlisted areas follow alphabetically",
706
+ },
707
+ },
708
+ },
709
+ async execute(_id, params, _signal, _onUpdate, ctx) {
710
+ const project = projectOf(params, ctx);
711
+ const { rename, areaOrder, ...fields } = params;
712
+ const onProject = rename !== undefined || areaOrder !== undefined;
713
+ if (onProject && params.mock !== undefined) {
714
+ throw new Error('"rename" and "areaOrder" edit the project; edit the mock without them');
715
+ }
716
+ const result = onProject
717
+ ? await requestJson(`/api/projects/${encodeURIComponent(project)}`, {
718
+ method: "PATCH",
719
+ body: JSON.stringify({ name: rename, areaOrder, session: state.sessionId }),
720
+ })
721
+ : await requestJson(mockPath(params.mock ?? ""), {
722
+ method: "PATCH",
723
+ body: JSON.stringify({ ...fields, project, session: state.sessionId }),
724
+ });
725
+ const text = result.renamed
726
+ ? `renamed ${result.renamed.from} → ${result.renamed.to} · ${result.url}`
727
+ : result.areaOrder
728
+ ? `area order ${result.areaOrder.join(", ") || "alphabetical"} · ${result.url}`
729
+ : `edited ${result.mock.slug} · ${result.url}`;
730
+ return {
731
+ content: [
732
+ {
733
+ type: "text",
734
+ text: `${[text, ...(result.nudges ?? []).map((n) => `nudge: ${n}`)].join("\n")}${feedbackSummary(result.feedback)}`,
735
+ },
736
+ ],
737
+ details: { ...result, baseUrl: baseUrl() },
738
+ };
739
+ },
740
+ });
741
+
579
742
  pi.registerTool({
580
743
  name: "mockpit_export",
581
744
  label: "Mockpit Export",
582
745
  description:
583
- "The accepted (or current) html per state, version history, and the last reply's tuned values.",
584
- promptSnippet: "Export an accepted mockpit mock.",
746
+ "The html in use (else current) per state, version history, and the last reply's tuned values.",
747
+ promptSnippet: "Export the design in use from a mockpit mock.",
585
748
  parameters: { type: "object", properties: mockProps, required: ["mock"] },
586
749
  async execute(_id, params, _signal, _onUpdate, ctx) {
587
750
  const query = new URLSearchParams({ project: projectOf(params, ctx) });
@@ -0,0 +1,35 @@
1
+ ## Rules
2
+
3
+ - Before designing something new, ask 2–3 short questions with likely answers
4
+ as options (who, when, what matters most), then design. When the user asks for
5
+ a quick draft, skip them and write your assumptions in each variant's note.
6
+ - Variants differ in one clear thing (layout, density, emphasis, voice), are
7
+ named by what they are, and each note says when it's the best pick and what it
8
+ costs. Three by default, five at most. A difference that is only colour or
9
+ size is a knob, not a variant.
10
+ - Don't ask what has a right answer (big enough to tap, undo, readable in light
11
+ and dark): decide it and say why in one line of the note. Ask only what is the
12
+ user's to decide.
13
+ - Real content: realistic text and counts, and the awkward cases (a long name,
14
+ a zero, many items). No lorem.
15
+ - No filler: no subtitles that say nothing, emoji, eyebrow labels, italic last
16
+ words, pep talk, badges everywhere, decorative gradients or boxes in boxes,
17
+ unless the user asks for that direction. Filler out, craft in: put the effort
18
+ into hierarchy, real data and visuals that carry meaning. Never bare by default.
19
+ - Errors say what happened and what to do next. Nothing is cut off at the
20
+ narrowest width.
21
+ - Once a direction is chosen, add the other situations the state has (empty,
22
+ many, error, loading) as situations of the state (`--situation`). Not in the
23
+ first round.
24
+ - Once the user has chosen, the next version is that design with the likely fix
25
+ applied, and the note says what changed. Don't reopen it with new options; ask
26
+ one focused question only if it is still off.
27
+
28
+ ## Suggestions
29
+
30
+ - One word per thing everywhere; buttons say what happens ("Log lunch", not "OK").
31
+ - Make things stand out on purpose: what looks different means something
32
+ different, and the same meaning looks the same everywhere. Say the meaning in
33
+ the note when it matters.
34
+ - Gestures the product is judged by are built working in Try mode, every phase
35
+ including cancel. Not every button.
@@ -1,17 +1,20 @@
1
1
  # mockpit topic: asks
2
2
 
3
- An ask is your structured question on a mock. A choice is several variants
4
- plus one ask that binds them. Ask only when the decision is really the user's,
5
- and ask in the mock, never in chat: after asking, tell the user in one line
6
- where to look and end your turn.
3
+ A question (`ask`) is your structured question on a mock. A choice is several
4
+ variants plus one question that binds them. Ask only when the decision is
5
+ really the user's, and ask in the mock, never in chat: after asking, tell the
6
+ user in one line where to look and end your turn. To the user they are
7
+ questions; `ask` is only the tool's name.
7
8
 
8
9
  ```sh
9
- mockpit ask --mock writer "Which look?" --option Quiet=quiet --option Dark=dark --id look
10
+ mockpit ask --mock writer "Quiet or dark?" --option Quiet=quiet --option Dark=dark --id tone
10
11
  mockpit ask --mock writer "Trim?" --option Above=knob:trim.position=top --option Below=knob:trim.position=bottom
11
12
  mockpit ask --mock writer --asks asks.json
12
13
  ```
13
14
 
14
15
  `=knob:` sets one knob; for an option that sets several, use `--asks` JSON.
16
+ `--recommend Quiet` (`"recommended": true` on one option) marks the one you
17
+ would pick with a quiet dot; the user sees why on hover, in the variant's note.
15
18
 
16
19
  ## Shape
17
20
 
@@ -20,8 +23,8 @@ The same JSON goes to `--asks`, MCP `ask` and `POST /api/mocks/:id/asks`:
20
23
  ```json
21
24
  [
22
25
  {
23
- "id": "look",
24
- "text": "Which look?",
26
+ "id": "tone",
27
+ "text": "Quiet or dark?",
25
28
  "scope": "mock",
26
29
  "options": [
27
30
  { "label": "Quiet", "variant": "quiet" },
@@ -51,49 +54,97 @@ The same JSON goes to `--asks`, MCP `ask` and `POST /api/mocks/:id/asks`:
51
54
 
52
55
  ## Scope
53
56
 
54
- `scope` says what an ask decides: `mock` (the default), `state` (add `state`)
55
- or `part` (add `part`, the `data-part` name).
57
+ `scope` says what a question decides: `mock` (the default), `state` (add
58
+ `state`) or `part` (add `part`, the `data-part` name). Scope also says where
59
+ the user sees it: the viewer shows each state with its own questions. A
60
+ `state` question shows on that state, a `part` question on every state that
61
+ declares the part, and a `mock` question whose options name variants on every
62
+ state that has them (any other `mock` question on every state); answered
63
+ once, it counts once.
64
+
65
+ Only open questions are shown and counted ("1 of 3"). Once a Send answers
66
+ one, it folds into a read-only Answered list and stays out of the next round,
67
+ so ask new questions for what is still open rather than re-sending answered
68
+ ones; the user changes a sent answer only on purpose, and the change arrives
69
+ as a normal Send.
56
70
 
57
71
  ## Options
58
72
 
59
73
  An option binds to one of:
60
74
 
61
- - a `variant`: picking it accepts that variant and archives its siblings in
62
- that state.
75
+ - a `variant`: picking it puts that variant in use and archives its siblings
76
+ in that state. Add `version` to offer one version of it (`"version": 2`, CLI
77
+ `--option Before=quiet@2`): two options can be two versions of one variant,
78
+ and the picked one is put in use. It needs the question's `state` when the
79
+ mock has several.
63
80
  - a knob `set`: picking it applies those values live.
64
81
  - nothing: a plain pill.
65
82
 
66
83
  Options that change how something looks carry a `variant` or a knob `set`,
67
84
  so the user sees them live. Plain pills only for what can't be seen.
68
85
 
86
+ When one question only makes sense once another is answered (an accent's
87
+ placement depends on the palette), give it `after: "<that question's id>"`
88
+ (CLI `--after palette`). It stays closed until that one is answered, then its
89
+ pictures render with that answer and say so. Ask the deciding question first.
90
+
69
91
  The viewer shows a picture of each option it can render. `multi: true` allows
70
- several answers. Option ids default to the slugged label. Reusing an ask `id`
71
- replaces that ask. The viewer adds an "Other…" write-in and a note to every
72
- ask (both take attached images), so don't add your own; the option id `other`
73
- is reserved.
92
+ several answers. Option ids default to the slugged label. Reusing a question
93
+ `id` replaces that question. The viewer adds an "Other…" write-in and a note to
94
+ every question (both take attached images), so don't add your own; the option
95
+ id `other` is reserved.
96
+
97
+ ## Parts with fills
98
+
99
+ A part with fills is one region with alternatives, in one html. Give each
100
+ alternative the same `data-part` and its own `data-fill`:
101
+
102
+ ```html
103
+ <section data-part="entry" data-part-label="Entry" data-fill="dots" data-fill-label="Typing dots">
104
+ …
105
+ </section>
106
+ <section data-part="entry" data-fill="skeleton">…</section>
107
+ <section data-part="entry" data-fill="filling" data-fill-default>…</section>
108
+ ```
109
+
110
+ The first fill is the default unless one carries `data-fill-default`. The
111
+ server derives a select knob at the part's name and shows only the chosen
112
+ fill, so you write no switching CSS; give each fill whatever display it
113
+ needs. Draw anything inside a fill, including further parts with fills: one
114
+ inside a fill is asked only while that fill is chosen, right after its
115
+ parent's question.
116
+
117
+ Publish returns the parts with fills it found (`fills`) and, for each one no
118
+ question sets, a ready `suggestedFillAsks` entry: `scope: "part"`, one option
119
+ per fill with `set: {"entry": "<fill>"}`. Send them with `ask`, relabelled if
120
+ you like. On a pick the viewer outlines the fill on stage. `fills` in publish
121
+ replaces or adds one fill without resending the page (topic `feedback`).
122
+
123
+ Use fills when the alternatives differ in one region; use variants when the
124
+ whole design differs. Both combine: each variant may have parts with fills.
74
125
 
75
- ## The Look ask
126
+ ## The variant question
76
127
 
77
- When you publish variants, ask `scope: "mock"` with one option per variant, and
78
- ask it first. Questions appear in the order you ask them. This one is titled
79
- "Look" and unlocks Mix, which lets the user borrow a part from another look.
80
- Variants without a Look ask get a plain switcher in the frame header.
128
+ When you publish variants, ask `scope: "mock"` with one option per variant,
129
+ and ask it first. Questions appear in the order you ask them. Tune then
130
+ offers, per part, to borrow that part from another variant. Variants without
131
+ a variant question get a plain switcher in the frame header.
81
132
 
82
133
  A variant decides everything it differs on; don't re-ask it. Ask other axes as
83
134
  knob `set`s.
84
135
 
85
136
  ## When you forget to ask
86
137
 
87
- A publish that leaves a state with two or more open variants and no ask
138
+ A publish that leaves a state with two or more open variants and no question
88
139
  binding them returns a nudge and a ready `suggestedAsk` (mock-wide when the
89
140
  variant names line up across states, else for that state), or your existing
90
- ask plus the new variants. Send it as is with `ask`, or write your own question
91
- with options bound to the variants. The CLI prints the `mockpit ask` command
92
- for you. Sending it stops the nudge and keeps the answered Look ask on the
93
- mock; the built-in one is gone once answered.
94
-
95
- Until an ask binds them, the viewer shows a built-in Look ask with a picture
96
- of each variant, so the user can still choose in one Send. Its answer
97
- reads like any other ask under the reserved id `variant` (per state:
98
- `variant:<state>`), and accepts and archives per state. Your own ask over
99
- those variants replaces it.
141
+ question plus the new variants. Send it as is with `ask`, or write your own
142
+ question with options bound to the variants. The CLI prints the `mockpit ask`
143
+ command for you. Sending it stops the nudge and keeps your answered question
144
+ on the mock; the built-in one is gone once answered.
145
+
146
+ Until a question binds them, the viewer shows a built-in "Which variant?"
147
+ with a picture of each variant, so the user can still choose in one Send. Its
148
+ answer reads like any other under the reserved id `variant` (per state:
149
+ `variant:<state>`), and puts in use and archives per state. Your own question
150
+ over those variants replaces it.