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.
Files changed (51) hide show
  1. package/README.md +13 -8
  2. package/bin/mockpit.js +116 -16
  3. package/dist/mcp/server.js +10 -7
  4. package/dist/server/apiViews.d.ts +118 -17
  5. package/dist/server/apiViews.js +66 -8
  6. package/dist/server/app.d.ts +2 -1
  7. package/dist/server/app.js +917 -144
  8. package/dist/server/decisions.d.ts +23 -0
  9. package/dist/server/decisions.js +99 -0
  10. package/dist/server/designGuide.js +45 -10
  11. package/dist/server/embeds.d.ts +25 -0
  12. package/dist/server/embeds.js +118 -0
  13. package/dist/server/eventLog.js +2 -1
  14. package/dist/server/feedbackBatch.d.ts +12 -1
  15. package/dist/server/feedbackBatch.js +20 -3
  16. package/dist/server/fills.d.ts +41 -0
  17. package/dist/server/fills.js +328 -0
  18. package/dist/server/index.js +15 -1
  19. package/dist/server/knobs.d.ts +6 -2
  20. package/dist/server/knobs.js +11 -1
  21. package/dist/server/mcpHttp.d.ts +3 -0
  22. package/dist/server/mcpHttp.js +3 -0
  23. package/dist/server/mcpSpec.d.ts +13 -13
  24. package/dist/server/mcpSpec.js +73 -8
  25. package/dist/server/postSurfaces.js +3 -12
  26. package/dist/server/richRender.js +3 -1
  27. package/dist/server/run.js +4 -0
  28. package/dist/server/runApi.d.ts +1 -1
  29. package/dist/server/runApi.js +4 -3
  30. package/dist/server/sqlStore.d.ts +89 -4
  31. package/dist/server/sqlStore.js +149 -23
  32. package/dist/server/surfacePage.d.ts +6 -2
  33. package/dist/server/surfacePage.js +142 -10
  34. package/dist/server/themes.d.ts +2 -0
  35. package/dist/server/themes.js +23 -3
  36. package/dist/server/types.d.ts +73 -11
  37. package/dist/server/types.js +42 -2
  38. package/dist/server/viewerRequest.d.ts +2 -0
  39. package/dist/server/viewerRequest.js +14 -0
  40. package/guide/topics/asks.md +50 -3
  41. package/guide/topics/feedback.md +89 -11
  42. package/guide/topics/html.md +5 -1
  43. package/guide/topics/http.md +2 -1
  44. package/guide/topics/knobs.md +2 -1
  45. package/guide/topics/surfaces.md +24 -8
  46. package/package.json +4 -13
  47. package/skills/launch-video/SKILL.md +1 -1
  48. package/skills/mockpit/SKILL.md +12 -4
  49. package/viewer/dist/index.html +8 -8
  50. package/dist/server/slots.d.ts +0 -14
  51. package/dist/server/slots.js +0 -71
@@ -1,4 +1,4 @@
1
- import { collectAssetIds, collectAttachedIds, DEFAULT_PROJECT, DEFAULT_VARIANT, hashAssetId, HISTORY_LIMIT, htmlSurface, MAX_WORKSPACE_ASSET_BYTES, newId, normalizeSurfaceIds, projectFromCwd, reservedAgent, selectEvictions, slugify, stripNul, summarizeProjects, } from "./types.js";
1
+ import { collectAssetIds, collectAttachedIds, DEFAULT_PROJECT, DEFAULT_VARIANT, hashAssetId, HISTORY_LIMIT, htmlSurface, MAX_WORKSPACE_ASSET_BYTES, newId, normalizeSurfaceIds, projectFromCwd, reservedAgent, sameScreen, selectEvictions, slugify, stripNul, summarizeProjects, } from "./types.js";
2
2
  function parseJson(raw, fallback) {
3
3
  if (typeof raw !== "string" || !raw)
4
4
  return fallback;
@@ -14,7 +14,7 @@ const POST_COLUMNS = `
14
14
  variant TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'open', title TEXT NOT NULL,
15
15
  surfaces TEXT NOT NULL, knobs TEXT, slots TEXT NOT NULL DEFAULT '[]',
16
16
  createdAt TEXT NOT NULL, updatedAt TEXT NOT NULL, version INTEGER NOT NULL,
17
- curFrom INTEGER, curPrompt TEXT, curAuthor TEXT`;
17
+ curFrom INTEGER, curPrompt TEXT, curAuthor TEXT, note TEXT`;
18
18
  // Store implementation on SQLite — a Durable Object's `ctx.storage.sql` in the
19
19
  // Worker, or node:sqlite via an adapter on Node (see server/sqliteStorage.ts).
20
20
  // One workspace = one database, so plain SQL with no tenant columns.
@@ -71,7 +71,8 @@ export class SqlStore {
71
71
  id TEXT PRIMARY KEY, project TEXT NOT NULL, slug TEXT NOT NULL, title TEXT NOT NULL,
72
72
  kind TEXT NOT NULL DEFAULT 'component', states TEXT NOT NULL DEFAULT '[]',
73
73
  asks TEXT NOT NULL DEFAULT '[]', knobs TEXT NOT NULL DEFAULT '{}', draft TEXT,
74
- sessionId TEXT, createdAt TEXT NOT NULL, updatedAt TEXT NOT NULL
74
+ sessionId TEXT, createdAt TEXT NOT NULL, updatedAt TEXT NOT NULL,
75
+ widths TEXT NOT NULL DEFAULT '[]'
75
76
  );
76
77
  CREATE TABLE IF NOT EXISTS posts (${POST_COLUMNS});
77
78
  CREATE TABLE IF NOT EXISTS post_versions (
@@ -95,6 +96,11 @@ export class SqlStore {
95
96
  CREATE TABLE IF NOT EXISTS settings (
96
97
  key TEXT PRIMARY KEY, value TEXT NOT NULL
97
98
  );
99
+ CREATE TABLE IF NOT EXISTS flows (
100
+ project TEXT NOT NULL, name TEXT NOT NULL, steps TEXT NOT NULL DEFAULT '[]',
101
+ createdAt TEXT NOT NULL, updatedAt TEXT NOT NULL,
102
+ PRIMARY KEY (project, name)
103
+ );
98
104
  `);
99
105
  this.addMissing("sessions", {
100
106
  agentSeq: "INTEGER NOT NULL DEFAULT 0",
@@ -102,8 +108,22 @@ export class SqlStore {
102
108
  key: "TEXT",
103
109
  agentActiveAt: "TEXT",
104
110
  });
111
+ this.addMissing("mocks", { widths: "TEXT NOT NULL DEFAULT '[]'", resolvedAt: "TEXT" });
105
112
  if (legacy)
106
113
  this.migrateLegacy();
114
+ // After the legacy lift, which rebuilds posts from POST_COLUMNS alone.
115
+ const pinned = this.columns("posts").has("acceptedVersion");
116
+ this.addMissing("posts", {
117
+ note: "TEXT",
118
+ forkedFrom: "TEXT",
119
+ starred: "INTEGER NOT NULL DEFAULT 0",
120
+ acceptedVersion: "INTEGER",
121
+ hiddenVersions: "TEXT",
122
+ });
123
+ // Before the pin, accepted meant "its latest": freeze that as the version in use.
124
+ if (!pinned) {
125
+ this.sql.exec("UPDATE posts SET acceptedVersion = version WHERE status = 'accepted'");
126
+ }
107
127
  this.createIndexes();
108
128
  }
109
129
  createIndexes() {
@@ -396,14 +416,17 @@ export class SqlStore {
396
416
  states: parseJson(r.states, []),
397
417
  asks: parseJson(r.asks, []),
398
418
  knobs: parseJson(r.knobs, {}),
419
+ widths: parseJson(r.widths, []),
399
420
  draft: parseJson(r.draft, null),
400
421
  sessionId: r.sessionId ?? null,
422
+ resolvedAt: r.resolvedAt ?? null,
401
423
  createdAt: r.createdAt,
402
424
  updatedAt: r.updatedAt,
403
425
  };
404
426
  }
405
427
  rowToPost(r, history) {
406
428
  const knobs = parseJson(r.knobs, null);
429
+ const hidden = parseJson(r.hiddenVersions, []);
407
430
  return {
408
431
  id: r.id,
409
432
  sessionId: r.sessionId,
@@ -418,7 +441,15 @@ export class SqlStore {
418
441
  version: r.version,
419
442
  history,
420
443
  ...(knobs ? { knobs } : {}),
421
- slots: parseJson(r.slots, []),
444
+ ...(r.note == null ? {} : { note: r.note }),
445
+ // The column keeps its first name, from when embeds were called slots.
446
+ embeds: parseJson(r.slots, []),
447
+ ...(r.forkedFrom == null ? {} : { forkedFrom: parseJson(r.forkedFrom, null) }),
448
+ ...(r.starred ? { starred: true } : {}),
449
+ ...(r.status === "accepted" && r.acceptedVersion != null
450
+ ? { acceptedVersion: r.acceptedVersion }
451
+ : {}),
452
+ ...(hidden.length ? { hiddenVersions: hidden } : {}),
422
453
  ...(r.curFrom == null ? {} : { from: r.curFrom }),
423
454
  ...(r.curPrompt == null ? {} : { prompt: r.curPrompt }),
424
455
  ...(r.curAuthor == null ? {} : { author: r.curAuthor }),
@@ -585,12 +616,14 @@ export class SqlStore {
585
616
  states: (input.states ?? []).map((s) => stripNul(s)),
586
617
  asks: [],
587
618
  knobs: input.knobs ?? {},
619
+ widths: input.widths ?? [],
588
620
  draft: null,
589
621
  sessionId: input.sessionId ?? null,
622
+ resolvedAt: null,
590
623
  createdAt: now,
591
624
  updatedAt: now,
592
625
  };
593
- this.sql.exec("INSERT INTO mocks (id, project, slug, title, kind, states, asks, knobs, draft, sessionId, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?, '[]', ?, NULL, ?, ?, ?)", mock.id, mock.project, mock.slug, mock.title, mock.kind, JSON.stringify(mock.states), JSON.stringify(mock.knobs), mock.sessionId, mock.createdAt, mock.updatedAt);
626
+ this.sql.exec("INSERT INTO mocks (id, project, slug, title, kind, states, asks, knobs, widths, draft, sessionId, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?, '[]', ?, ?, NULL, ?, ?, ?)", mock.id, mock.project, mock.slug, mock.title, mock.kind, JSON.stringify(mock.states), JSON.stringify(mock.knobs), JSON.stringify(mock.widths), mock.sessionId, mock.createdAt, mock.updatedAt);
594
627
  return mock;
595
628
  }
596
629
  async updateMock(id, patch) {
@@ -604,26 +637,80 @@ export class SqlStore {
604
637
  ...(patch.states !== undefined && { states: patch.states.map((s) => stripNul(s)) }),
605
638
  ...(patch.asks !== undefined && { asks: patch.asks }),
606
639
  ...(patch.knobs !== undefined && { knobs: patch.knobs }),
640
+ ...(patch.widths !== undefined && { widths: patch.widths }),
607
641
  ...(patch.sessionId !== undefined && { sessionId: patch.sessionId }),
642
+ ...(patch.resolvedAt !== undefined && { resolvedAt: patch.resolvedAt }),
608
643
  updatedAt: new Date().toISOString(),
609
644
  };
610
- this.sql.exec("UPDATE mocks SET title = ?, kind = ?, states = ?, asks = ?, knobs = ?, sessionId = ?, updatedAt = ? WHERE id = ?", next.title, next.kind, JSON.stringify(next.states), JSON.stringify(next.asks), JSON.stringify(next.knobs), next.sessionId, next.updatedAt, id);
645
+ this.sql.exec("UPDATE mocks SET title = ?, kind = ?, states = ?, asks = ?, knobs = ?, widths = ?, sessionId = ?, resolvedAt = ?, updatedAt = ? WHERE id = ?", next.title, next.kind, JSON.stringify(next.states), JSON.stringify(next.asks), JSON.stringify(next.knobs), JSON.stringify(next.widths), next.sessionId, next.resolvedAt, next.updatedAt, id);
611
646
  if (patch.asks !== undefined)
612
647
  this.invalidateAssetRefs();
613
648
  return next;
614
649
  }
615
650
  async removeMock(id) {
616
- if (!(await this.getMock(id)))
651
+ const mock = await this.getMock(id);
652
+ if (!mock)
617
653
  return false;
618
654
  this.atomic(() => {
619
655
  this.sql.exec("DELETE FROM comments WHERE mockId = ?", id);
620
656
  this.sql.exec("DELETE FROM post_versions WHERE postId IN (SELECT id FROM posts WHERE mockId = ?)", id);
621
657
  this.sql.exec("DELETE FROM posts WHERE mockId = ?", id);
622
658
  this.sql.exec("DELETE FROM mocks WHERE id = ?", id);
659
+ for (const flow of this.flowsOf(mock.project)) {
660
+ const steps = flow.steps.filter((x) => x.mock !== mock.slug);
661
+ if (steps.length !== flow.steps.length)
662
+ this.writeFlow(flow, steps);
663
+ }
623
664
  });
624
665
  this.invalidateAssetRefs();
625
666
  return true;
626
667
  }
668
+ // --- flows ---
669
+ flowsOf(project) {
670
+ return this.sql
671
+ .exec("SELECT * FROM flows WHERE project = ? ORDER BY createdAt, rowid", project)
672
+ .toArray()
673
+ .map((r) => ({
674
+ project: r.project,
675
+ name: r.name,
676
+ steps: parseJson(r.steps, []),
677
+ createdAt: r.createdAt,
678
+ updatedAt: r.updatedAt,
679
+ }));
680
+ }
681
+ // An emptied flow is deleted rather than kept as a name with nothing to walk.
682
+ writeFlow(flow, steps) {
683
+ if (steps.length === 0) {
684
+ this.sql.exec("DELETE FROM flows WHERE project = ? AND name = ?", flow.project, flow.name);
685
+ return null;
686
+ }
687
+ const next = { ...flow, steps, updatedAt: new Date().toISOString() };
688
+ this.sql.exec("INSERT INTO flows (project, name, steps, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?) ON CONFLICT(project, name) DO UPDATE SET steps = excluded.steps, updatedAt = excluded.updatedAt", next.project, next.name, JSON.stringify(next.steps), next.createdAt, next.updatedAt);
689
+ return next;
690
+ }
691
+ async listFlows(project) {
692
+ return this.flowsOf(project);
693
+ }
694
+ async placeFlowStep(project, name, screen, step) {
695
+ const clean = { mock: stripNul(screen.mock), state: stripNul(screen.state) };
696
+ const label = stripNul(name);
697
+ return this.atomic(() => {
698
+ const now = new Date().toISOString();
699
+ const flow = this.flowsOf(project).find((f) => f.name === label) ?? {
700
+ project,
701
+ name: label,
702
+ steps: [],
703
+ createdAt: now,
704
+ updatedAt: now,
705
+ };
706
+ const steps = flow.steps.filter((x) => !sameScreen(x, clean));
707
+ if (step > 0)
708
+ steps.splice(Math.min(step, steps.length + 1) - 1, 0, clean);
709
+ if (steps.length === 0 && flow.steps.length === 0)
710
+ return null;
711
+ return this.writeFlow(flow, steps);
712
+ });
713
+ }
627
714
  async putDraft(mockId, draft) {
628
715
  const mock = await this.getMock(mockId);
629
716
  if (!mock)
@@ -692,12 +779,14 @@ export class SqlStore {
692
779
  version: 1,
693
780
  history: [],
694
781
  ...(input.knobs ? { knobs: input.knobs } : {}),
695
- slots: input.slots ?? [],
782
+ ...(stripNul(input.note ?? "").trim() ? { note: stripNul(input.note ?? "").trim() } : {}),
783
+ embeds: input.embeds ?? [],
784
+ ...(input.forkedFrom ? { forkedFrom: input.forkedFrom } : {}),
696
785
  ...(input.from === undefined ? {} : { from: input.from }),
697
786
  ...(input.prompt === undefined ? {} : { prompt: stripNul(input.prompt) }),
698
787
  ...(input.author === undefined ? {} : { author: stripNul(input.author) }),
699
788
  };
700
- this.sql.exec("INSERT INTO posts (id, sessionId, mockId, state, variant, status, title, surfaces, knobs, slots, createdAt, updatedAt, version, curFrom, curPrompt, curAuthor) VALUES (?, ?, ?, ?, ?, 'open', ?, ?, ?, ?, ?, ?, 1, ?, ?, ?)", post.id, post.sessionId, post.mock, post.state, post.variant, post.title, JSON.stringify(post.surfaces), post.knobs ? JSON.stringify(post.knobs) : null, JSON.stringify(post.slots), post.createdAt, post.updatedAt, post.from ?? null, post.prompt ?? null, post.author ?? null);
789
+ this.sql.exec("INSERT INTO posts (id, sessionId, mockId, state, variant, status, title, surfaces, knobs, slots, createdAt, updatedAt, version, curFrom, curPrompt, curAuthor, note, forkedFrom) VALUES (?, ?, ?, ?, ?, 'open', ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?)", post.id, post.sessionId, post.mock, post.state, post.variant, post.title, JSON.stringify(post.surfaces), post.knobs ? JSON.stringify(post.knobs) : null, JSON.stringify(post.embeds), post.createdAt, post.updatedAt, post.from ?? null, post.prompt ?? null, post.author ?? null, post.note ?? null, post.forkedFrom ? JSON.stringify(post.forkedFrom) : null);
701
790
  this.touch(input.sessionId);
702
791
  this.touchMock(post.mock, now);
703
792
  this.addAssetRefs(post.surfaces);
@@ -719,13 +808,17 @@ export class SqlStore {
719
808
  ...(post.prompt === undefined ? {} : { prompt: post.prompt }),
720
809
  ...(post.author === undefined ? {} : { author: post.author }),
721
810
  };
811
+ // The version in use is kept past the limit: export and embeds resolve to it.
812
+ const pin = post.acceptedVersion ?? -1;
722
813
  const history = [...post.history, archived];
723
- if (history.length > HISTORY_LIMIT)
724
- history.shift();
814
+ const prunable = history.filter((h) => h.version !== pin);
815
+ if (prunable.length > HISTORY_LIMIT)
816
+ history.splice(history.indexOf(prunable[0]), 1);
725
817
  const title = patch.title !== undefined ? stripNul(patch.title).trim() || post.title : post.title;
726
818
  const surfaces = patch.surfaces !== undefined ? normalizeSurfaceIds(patch.surfaces) : post.surfaces;
727
819
  const knobs = patch.knobs === undefined ? post.knobs : (patch.knobs ?? undefined);
728
- const slots = patch.slots ?? post.slots;
820
+ const note = patch.note === undefined ? post.note : stripNul(patch.note).trim() || undefined;
821
+ const embeds = patch.embeds ?? post.embeds;
729
822
  const state = patch.state === undefined ? post.state : patch.state;
730
823
  const version = post.version + 1;
731
824
  const updatedAt = new Date().toISOString();
@@ -733,22 +826,23 @@ export class SqlStore {
733
826
  const from = patch.from ?? post.version;
734
827
  const prompt = stripNul(patch.prompt ?? "");
735
828
  const author = patch.author === undefined ? null : stripNul(patch.author);
736
- this.sql.exec("UPDATE posts SET title = ?, surfaces = ?, knobs = ?, slots = ?, state = ?, updatedAt = ?, version = ?, curFrom = ?, curPrompt = ?, curAuthor = ? WHERE id = ? AND version = ?", title, JSON.stringify(surfaces), knobs ? JSON.stringify(knobs) : null, JSON.stringify(slots), state, updatedAt, version, from, prompt, author, id, post.version);
829
+ this.sql.exec("UPDATE posts SET title = ?, surfaces = ?, knobs = ?, note = ?, slots = ?, state = ?, updatedAt = ?, version = ?, curFrom = ?, curPrompt = ?, curAuthor = ? WHERE id = ? AND version = ?", title, JSON.stringify(surfaces), knobs ? JSON.stringify(knobs) : null, note ?? null, JSON.stringify(embeds), state, updatedAt, version, from, prompt, author, id, post.version);
737
830
  const affected = this.sql.exec("SELECT changes() AS n").one().n;
738
831
  if (affected > 0) {
739
832
  this.insertVersion(id, archived);
740
- this.sql.exec("DELETE FROM post_versions WHERE postId = ? AND version <= ?", id, version - 1 - HISTORY_LIMIT);
833
+ this.sql.exec("DELETE FROM post_versions WHERE postId = ? AND version <= ? AND version != ?", id, version - 1 - HISTORY_LIMIT, pin);
741
834
  this.touch(post.sessionId);
742
835
  this.touchMock(post.mock, updatedAt);
743
836
  if (patch.surfaces !== undefined)
744
837
  this.addAssetRefs(surfaces);
745
- const { knobs: _knobs, author: _author, ...rest } = post;
838
+ const { knobs: _knobs, note: _note, author: _author, ...rest } = post;
746
839
  return {
747
840
  ...rest,
748
841
  title,
749
842
  surfaces,
750
843
  ...(knobs ? { knobs } : {}),
751
- slots,
844
+ ...(note ? { note } : {}),
845
+ embeds,
752
846
  state,
753
847
  version,
754
848
  updatedAt,
@@ -772,12 +866,36 @@ export class SqlStore {
772
866
  this.invalidateAssetRefs();
773
867
  return true;
774
868
  }
775
- async setPostStatus(id, status) {
869
+ async setPostStatus(id, status, acceptedVersion) {
776
870
  const post = await this.getPost(id);
777
871
  if (!post)
778
872
  return null;
779
- this.sql.exec("UPDATE posts SET status = ? WHERE id = ?", status, id);
780
- return { ...post, status };
873
+ const pin = status === "accepted" ? (acceptedVersion ?? post.version) : null;
874
+ this.sql.exec("UPDATE posts SET status = ?, acceptedVersion = ? WHERE id = ?", status, pin, id);
875
+ const { acceptedVersion: _, ...rest } = post;
876
+ return pin === null ? { ...rest, status } : { ...rest, status, acceptedVersion: pin };
877
+ }
878
+ async setPostStarred(id, starred) {
879
+ const post = await this.getPost(id);
880
+ if (!post)
881
+ return null;
882
+ this.sql.exec("UPDATE posts SET starred = ? WHERE id = ?", starred ? 1 : 0, id);
883
+ const { starred: _, ...rest } = post;
884
+ return starred ? { ...rest, starred: true } : rest;
885
+ }
886
+ async setVersionHidden(id, version, hidden) {
887
+ const post = await this.getPost(id);
888
+ if (!post)
889
+ return null;
890
+ const set = new Set(post.hiddenVersions ?? []);
891
+ if (hidden)
892
+ set.add(version);
893
+ else
894
+ set.delete(version);
895
+ const list = [...set].sort((a, b) => a - b);
896
+ this.sql.exec("UPDATE posts SET hiddenVersions = ? WHERE id = ?", list.length ? JSON.stringify(list) : null, id);
897
+ const { hiddenVersions: _, ...rest } = post;
898
+ return list.length ? { ...rest, hiddenVersions: list } : rest;
781
899
  }
782
900
  // --- comments ---
783
901
  async listComments(query) {
@@ -864,12 +982,20 @@ export class SqlStore {
864
982
  postVersion: input.payload.version,
865
983
  payload: input.payload,
866
984
  });
867
- this.sql.exec("UPDATE mocks SET asks = ?, draft = NULL WHERE id = ?", JSON.stringify(input.asks), input.mockId);
868
- for (const id of input.accept) {
869
- this.sql.exec("UPDATE posts SET status = 'accepted' WHERE id = ? AND mockId = ?", id, input.mockId);
985
+ this.sql.exec(input.keepDraft
986
+ ? "UPDATE mocks SET asks = ? WHERE id = ?"
987
+ : "UPDATE mocks SET asks = ?, draft = NULL WHERE id = ?", JSON.stringify(input.asks), input.mockId);
988
+ if (input.resolvedAt !== undefined) {
989
+ this.sql.exec("UPDATE mocks SET resolvedAt = ?, updatedAt = ? WHERE id = ?", input.resolvedAt, input.resolvedAt, input.mockId);
990
+ }
991
+ for (const { id, version } of input.accept) {
992
+ this.sql.exec("UPDATE posts SET status = 'accepted', acceptedVersion = ? WHERE id = ? AND mockId = ?", version, id, input.mockId);
870
993
  }
871
994
  for (const id of input.archive) {
872
- this.sql.exec("UPDATE posts SET status = 'archived' WHERE id = ? AND mockId = ?", id, input.mockId);
995
+ this.sql.exec("UPDATE posts SET status = 'archived', acceptedVersion = NULL WHERE id = ? AND mockId = ?", id, input.mockId);
996
+ }
997
+ for (const id of input.drop ?? []) {
998
+ this.sql.exec("UPDATE posts SET status = 'dropped', acceptedVersion = NULL WHERE id = ? AND mockId = ?", id, input.mockId);
873
999
  }
874
1000
  this.invalidateAssetRefs();
875
1001
  return comment;
@@ -3,8 +3,8 @@ import type { DesignSettings } from "./types.ts";
3
3
  import { type Mode, type Theme } from "./themes.ts";
4
4
  export declare const BRIDGE_JS = "\nwindow.sendPrompt = function (text) {\n parent.postMessage({ __mockpit: true, type: 'send-prompt', text: String(text) }, '*');\n};\nwindow.openLink = function (url) {\n parent.postMessage({ __mockpit: true, type: 'open-link', url: String(url) }, '*');\n};\nwindow.copyToClipboard = function (text) {\n parent.postMessage({ __mockpit: true, type: 'copy', text: String(text) }, '*');\n};\ndocument.addEventListener('click', function (e) {\n var a = e.target && e.target.closest ? e.target.closest('a[href]') : null;\n if (a && /^https?:/.test(a.href)) { e.preventDefault(); window.openLink(a.href); }\n});\n// Cmd+Option+Up/Down switches sessions in the sidebar, but keydowns fire in\n// whichever document holds focus \u2014 once the user clicks into a surface, this\n// sandboxed iframe swallows them. Forward just that combo to the host.\ndocument.addEventListener('keydown', function (e) {\n if (!e.metaKey || !e.altKey || e.ctrlKey || e.shiftKey) return;\n if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return;\n e.preventDefault();\n parent.postMessage({ __mockpit: true, type: 'switch-session', key: e.key }, '*');\n});\n// Report content height to the parent so it can size this iframe, while\n// breaking a feedback loop that can peg a CPU core.\n//\n// The loop: the parent sets the iframe's height to whatever we report, but some\n// content's height *inverts* with the frame's height \u2014 a scrollbar that appears\n// at height A reflows the content to height B, then disappears at B and reflows\n// back to A (or any 100vh / percentage-derived layout). The ResizeObserver then\n// fires on every flip, so reported heights alternate A, B, A, B... forever. With\n// a cheap surface that's a brief blip; with a heavy one (a big syntax-highlighted\n// diff/markdown surface) each relayout is expensive and the tab sits at 100% CPU\n// until the surface unmounts.\n//\n// A plain h !== __lastH guard can't stop this: in a 2-cycle every value differs\n// from the one immediately before it. So we remember the previous height too and\n// defer a return to it *if it recurs faster than a human could* (< 250ms) \u2014 that's\n// the runaway. The deferred pass keeps one trailing re-measure and reports the\n// taller height in the pair, so an ordinary font/image reflow can't leave the\n// frame permanently clipped.\nvar __lastH = 0;\nvar __prevH = 0;\nvar __lastT = 0;\nvar __seenH = 0;\nvar __trailTimer = 0;\nvar __trailH = 0;\nvar __FLIP_MS = 250;\nvar __TRAIL_MS = 350;\nfunction __now() {\n return typeof performance !== 'undefined' && performance.now ? performance.now() : Date.now();\n}\nfunction __measureHeight() {\n return document.body\n ? document.body.scrollHeight\n : document.documentElement.scrollHeight;\n}\nfunction __postHeight(h, t) {\n __prevH = __lastH;\n __lastH = h;\n __lastT = t;\n parent.postMessage({ __mockpit: true, type: 'resize', height: h }, '*');\n}\nfunction __clearTrailing() {\n if (__trailTimer && typeof clearTimeout !== 'undefined') clearTimeout(__trailTimer);\n __trailTimer = 0;\n __trailH = 0;\n}\nfunction __flushTrailing() {\n __trailTimer = 0;\n var measured = __measureHeight();\n if (measured > 0) __seenH = measured;\n var target = Math.max(__trailH || 0, measured || 0);\n __trailH = 0;\n if (target <= 0 || target === __lastH) return;\n __postHeight(target, __now());\n}\nfunction __scheduleTrailing(h, reset) {\n __trailH = Math.max(__trailH || 0, h || 0, __lastH || 0);\n if (__trailTimer) {\n if (!reset) return;\n if (typeof clearTimeout !== 'undefined') clearTimeout(__trailTimer);\n }\n __trailTimer = setTimeout(__flushTrailing, __TRAIL_MS);\n}\nfunction __report() {\n var h = __measureHeight();\n if (h <= 0) return; // no content yet\n var changed = h !== __seenH;\n __seenH = h;\n if (h === __lastH) return; // unchanged\n var t = __now();\n if (h === __prevH && (__trailTimer || t - __lastT < __FLIP_MS)) {\n __scheduleTrailing(h, true); // rapid A<->B flip: defer one settled report\n return;\n }\n __clearTrailing();\n __postHeight(h, t);\n}\nif (document.readyState === 'complete') __report();\nelse window.addEventListener('load', function () { requestAnimationFrame(__report); });\nsetTimeout(__report, 60);\nsetTimeout(__report, 350);\nsetTimeout(__report, 1500);\nsetTimeout(__report, 3000);\nsetTimeout(__report, 6000);\nsetTimeout(__report, 10000);\nif (document.fonts && document.fonts.ready && document.fonts.ready.then) {\n document.fonts.ready.then(function () { __report(); });\n}\nif (window.ResizeObserver) {\n window.__ssRO = new ResizeObserver(__report);\n window.__ssRO.observe(document.documentElement);\n if (document.body) window.__ssRO.observe(document.body);\n}\n";
5
5
  export declare const HIT_TEST_JS = "\n(function () {\n var MAX_DEPTH = 6;\n var MAX_TEXT = 60;\n function docBox() {\n var w = document.documentElement.clientWidth || 1;\n var h = (document.body && document.body.scrollHeight) || document.documentElement.scrollHeight || 1;\n return { w: w, h: h };\n }\n function ident(v) { return typeof v === 'string' && /^[A-Za-z][\\w-]*$/.test(v); }\n function cssPath(el) {\n var parts = [];\n while (el && el.nodeType === 1 && el !== document.body && el !== document.documentElement) {\n var tag = el.tagName.toLowerCase();\n if (ident(el.id)) { parts.unshift(tag + '#' + el.id); break; }\n var sel = tag;\n var cls = (el.getAttribute('class') || '').split(/\\s+/).filter(ident).slice(0, 2);\n if (cls.length) sel += '.' + cls.join('.');\n var p = el.parentElement;\n if (p) {\n var sibs = [];\n for (var i = 0; i < p.children.length; i++) {\n if (p.children[i].tagName === el.tagName) sibs.push(p.children[i]);\n }\n if (sibs.length > 1) sel += ':nth-of-type(' + (sibs.indexOf(el) + 1) + ')';\n }\n parts.unshift(sel);\n if (parts.length >= MAX_DEPTH) break;\n el = p;\n }\n return parts.join(' > ');\n }\n function firstLine(el) {\n var t = (el.innerText || el.textContent || '').replace(/\\s+/g, ' ').trim();\n var line = t.split('\\n')[0].trim();\n return line.length > MAX_TEXT ? line.slice(0, MAX_TEXT - 1) + '\\u2026' : line;\n }\n window.addEventListener('message', function (e) {\n if (e.source !== parent || parent === window) return;\n var d = e.data;\n if (!d || d.__mockpit !== true || d.type !== 'hit-test') return;\n var box = docBox();\n var px = Math.max(0, Math.min(1, Number(d.x) || 0)) * box.w;\n var py = Math.max(0, Math.min(1, Number(d.y) || 0)) * box.h;\n var el = document.elementFromPoint(px - (window.scrollX || 0), py - (window.scrollY || 0));\n var reply = { __mockpit: true, type: 'hit-test-result', ref: d.ref, path: '', text: '', rect: [0, 0, 0, 0] };\n if (el && el.nodeType === 1) {\n var r = el.getBoundingClientRect();\n reply.path = cssPath(el);\n reply.text = firstLine(el);\n reply.rect = [\n (r.left + (window.scrollX || 0)) / box.w,\n (r.top + (window.scrollY || 0)) / box.h,\n r.width / box.w,\n r.height / box.h,\n ];\n }\n parent.postMessage(reply, '*');\n });\n})();\n";
6
- export declare const PARTS_JS = "\n(function () {\n if (window.parent === window) return;\n var HL = 'mockpit-part-hl';\n var MAX_PARTS = 200;\n var MAX_TEXT = 200;\n var doc = window.__mockpitDoc || {};\n var version = typeof doc.version === 'number' ? doc.version : null;\n function text(v) { return v == null ? null : String(v).slice(0, MAX_TEXT); }\n function isFixed(el) {\n for (var n = el; n && n.nodeType === 1; n = n.parentElement) {\n if (getComputedStyle(n).position === 'fixed') return true;\n }\n return false;\n }\n function collect() {\n var sx = window.scrollX || 0, sy = window.scrollY || 0;\n var els = document.querySelectorAll('[data-part]');\n var parts = [];\n for (var i = 0; i < els.length && parts.length < MAX_PARTS; i++) {\n var el = els[i];\n var name = text(el.getAttribute('data-part'));\n if (!name) continue;\n var r = el.getBoundingClientRect();\n var depth = 0;\n for (var p = el.parentElement; p; p = p.parentElement) if (p.hasAttribute('data-part')) depth++;\n var outer = el.parentElement && el.parentElement.closest('[data-part]');\n parts.push({\n name: name,\n label: text(el.getAttribute('data-part-label')) || name,\n key: text(el.getAttribute('data-part-key')),\n box: { x: r.left + sx, y: r.top + sy, w: r.width, h: r.height },\n visible: (r.width > 0 || r.height > 0) && getComputedStyle(el).visibility !== 'hidden',\n fixed: isFixed(el),\n depth: depth,\n parent: outer ? text(outer.getAttribute('data-part')) : null,\n order: i,\n });\n }\n return {\n version: version,\n parts: parts,\n height: document.body ? document.body.scrollHeight : document.documentElement.scrollHeight,\n scroll: { x: sx, y: sy },\n viewport: { w: innerWidth, h: innerHeight },\n };\n }\n var last = '';\n var timer = 0;\n // setTimeout, not rAF: a hidden cross-origin frame may have rendering (and\n // with it rAF and ResizeObserver) throttled, but timers still run.\n function schedule() {\n if (timer) return;\n timer = setTimeout(function () {\n timer = 0;\n var m = collect();\n var key = JSON.stringify(m);\n if (key === last) return;\n last = key;\n m.__mockpit = true;\n m.type = 'parts';\n parent.postMessage(m, '*');\n }, 0);\n }\n\n var ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(schedule) : null;\n var observed = typeof WeakSet !== 'undefined' ? new WeakSet() : null;\n function observeAll() {\n if (!ro || !observed) return;\n // html/body too: a non-part sibling growing moves parts without resizing them.\n var els = [document.documentElement, document.body].concat([].slice.call(document.querySelectorAll('[data-part]')));\n for (var i = 0; i < els.length; i++) {\n var el = els[i];\n if (el && !observed.has(el)) { observed.add(el); ro.observe(el); }\n }\n }\n var moTimer = 0;\n var mo = new MutationObserver(function () {\n clearTimeout(moTimer);\n moTimer = setTimeout(function () { observeAll(); schedule(); }, 40);\n });\n mo.observe(document.documentElement, { subtree: true, childList: true, attributes: true, characterData: true });\n\n // Transforms and transitions move boxes without resizing or mutating anything.\n document.addEventListener('animationend', schedule, true);\n document.addEventListener('transitionend', schedule, true);\n window.addEventListener('scroll', schedule, { passive: true });\n window.addEventListener('resize', schedule);\n if (document.fonts && document.fonts.ready) document.fonts.ready.then(schedule);\n observeAll();\n schedule();\n // WebKit: RO's initial callback may never fire in a sandboxed frame.\n function settle() {\n schedule();\n [50, 150, 400, 1000].forEach(function (ms) { setTimeout(schedule, ms); });\n }\n if (document.readyState === 'complete') settle();\n else window.addEventListener('load', settle);\n\n function setHighlight(names) {\n var on = document.querySelectorAll('.' + HL);\n for (var i = 0; i < on.length; i++) on[i].classList.remove(HL);\n if (names && names.length) {\n var want = {};\n for (var k = 0; k < names.length && k < MAX_PARTS; k++) want[String(names[k])] = true;\n var els = document.querySelectorAll('[data-part]');\n for (var j = 0; j < els.length; j++) {\n if (want[els[j].getAttribute('data-part')] === true) els[j].classList.add(HL);\n }\n }\n // Our own class flip must not look like the agent's page changing.\n mo.takeRecords();\n }\n\n window.addEventListener('message', function (e) {\n if (e.source !== parent) return;\n var d = e.data;\n if (!d || d.__mockpit !== true) return;\n if (d.type === 'highlight') setHighlight(Array.isArray(d.parts) ? d.parts : []);\n else if (d.type === 'clear') setHighlight(null);\n else if (d.type === 'scroll') window.scrollBy(Number(d.dx) || 0, Number(d.dy) || 0);\n else if (d.type === 'hit') {\n var el = document.elementFromPoint(\n (Number(d.x) || 0) - (window.scrollX || 0),\n (Number(d.y) || 0) - (window.scrollY || 0)\n );\n var part = el && el.closest ? el.closest('[data-part]') : null;\n parent.postMessage({\n __mockpit: true,\n type: 'hit',\n ref: d.ref,\n part: part ? text(part.getAttribute('data-part')) : null,\n key: part ? text(part.getAttribute('data-part-key')) : null,\n }, '*');\n }\n });\n})();\n";
7
- export declare const KNOBS_JS = "\n(function () {\n\nvar __kPathRe = /^[A-Za-z_][\\w-]{0,63}(\\.[A-Za-z_][\\w-]{0,63}){0,3}$/;\nvar __kTokenRe = /^[#\\w(),.%/+-][#\\w\\s(),.%/+-]*$/;\nfunction __kName(path) { return path.replace(/\\./g, '-'); }\nfunction __kCssString(s) {\n return '\"' + s.replace(/[\\\\\"]/g, function (c) { return '\\\\' + c; })\n .replace(/[\\u0000-\\u001f\\u007f]/g, function (c) { return '\\\\' + c.charCodeAt(0).toString(16) + ' '; }) + '\"';\n}\nfunction __kCss(v) {\n if (typeof v === 'number') return isFinite(v) ? String(v) : null;\n if (typeof v === 'boolean') return v ? '1' : '0';\n if (typeof v === 'string') return __kTokenRe.test(v) ? v : __kCssString(v);\n return null;\n}\nfunction __kVars(path, v) {\n var name = __kName(path);\n if (v && typeof v === 'object' && !Array.isArray(v)) {\n var out = [];\n for (var f in v) {\n if (!Object.prototype.hasOwnProperty.call(v, f) || !/^[A-Za-z_][\\w-]{0,63}$/.test(f)) continue;\n var c = __kCss(v[f]);\n if (c !== null) out.push([name + '-' + f, c]);\n }\n return out;\n }\n var css = __kCss(v);\n return css === null ? [] : [[name, css]];\n}\nfunction __kAttr(v) {\n if (typeof v === 'number') return isFinite(v) ? String(v) : null;\n if (typeof v === 'boolean') return v ? 'true' : 'false';\n if (typeof v === 'string') return v;\n return null;\n}\n\n var doc = window.__mockpitDoc || {};\n var current = {};\n function own(o, k) { return Object.prototype.hasOwnProperty.call(o, k); }\n function bind(path, v) {\n var els = document.querySelectorAll('[data-k-bind]');\n for (var i = 0; i < els.length; i++) {\n if (els[i].getAttribute('data-k-bind') !== path) continue;\n els[i].textContent = v && typeof v === 'object' ? JSON.stringify(v) : String(v);\n }\n }\n function apply(values, live) {\n var root = document.documentElement;\n for (var path in values) {\n if (!own(values, path) || !__kPathRe.test(path)) continue;\n var v = values[path];\n if (v === null || v === undefined) continue;\n current[path] = v;\n if (live) {\n var vars = __kVars(path, v);\n if (!vars.length) root.style.removeProperty('--k-' + __kName(path));\n for (var i = 0; i < vars.length; i++) root.style.setProperty('--k-' + vars[i][0], vars[i][1]);\n var attr = __kAttr(v);\n if (attr === null) root.removeAttribute('data-k-' + __kName(path));\n else root.setAttribute('data-k-' + __kName(path), attr);\n }\n bind(path, v);\n }\n window.dispatchEvent(new CustomEvent('mockpit:knobs', {\n detail: { values: JSON.parse(JSON.stringify(current)) },\n }));\n }\n function initial() { apply(doc.knobs && typeof doc.knobs === 'object' ? doc.knobs : {}, false); }\n if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', initial);\n else initial();\n window.addEventListener('message', function (e) {\n if (e.source !== parent || parent === window) return;\n var d = e.data;\n if (!d || d.__mockpit !== true || d.type !== 'knobs') return;\n if (d.values && typeof d.values === 'object' && !Array.isArray(d.values)) apply(d.values, true);\n });\n})();\n";
6
+ export declare const PARTS_JS = "\n(function () {\n if (window.parent === window) return;\n var HL = 'mockpit-part-hl';\n var MAX_PARTS = 200;\n var MAX_TEXT = 200;\n var doc = window.__mockpitDoc || {};\n var version = typeof doc.version === 'number' ? doc.version : null;\n function text(v) { return v == null ? null : String(v).slice(0, MAX_TEXT); }\n function isFixed(el) {\n for (var n = el; n && n.nodeType === 1; n = n.parentElement) {\n if (getComputedStyle(n).position === 'fixed') return true;\n }\n return false;\n }\n function collect() {\n var sx = window.scrollX || 0, sy = window.scrollY || 0;\n var els = document.querySelectorAll('[data-part]');\n var parts = [];\n for (var i = 0; i < els.length && parts.length < MAX_PARTS; i++) {\n var el = els[i];\n var name = text(el.getAttribute('data-part'));\n if (!name) continue;\n var r = el.getBoundingClientRect();\n var depth = 0;\n for (var p = el.parentElement; p; p = p.parentElement) if (p.hasAttribute('data-part')) depth++;\n var outer = el.parentElement && el.parentElement.closest('[data-part]');\n parts.push({\n name: name,\n label: text(el.getAttribute('data-part-label')) || name,\n key: text(el.getAttribute('data-part-key')),\n box: { x: r.left + sx, y: r.top + sy, w: r.width, h: r.height },\n visible: (r.width > 0 || r.height > 0) && getComputedStyle(el).visibility !== 'hidden',\n fixed: isFixed(el),\n depth: depth,\n parent: outer ? text(outer.getAttribute('data-part')) : null,\n order: i,\n });\n }\n return {\n version: version,\n parts: parts,\n height: document.body ? document.body.scrollHeight : document.documentElement.scrollHeight,\n scroll: { x: sx, y: sy },\n // scrollWidth past innerWidth: the design is wider than the frame.\n viewport: { w: innerWidth, h: innerHeight, scrollW: document.documentElement.scrollWidth },\n };\n }\n var last = '';\n var timer = 0;\n // setTimeout, not rAF: a hidden cross-origin frame may have rendering (and\n // with it rAF and ResizeObserver) throttled, but timers still run.\n function schedule() {\n if (timer) return;\n timer = setTimeout(function () {\n timer = 0;\n var m = collect();\n var key = JSON.stringify(m);\n if (key === last) return;\n last = key;\n m.__mockpit = true;\n m.type = 'parts';\n parent.postMessage(m, '*');\n }, 0);\n }\n\n var ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(schedule) : null;\n var observed = typeof WeakSet !== 'undefined' ? new WeakSet() : null;\n function observeAll() {\n if (!ro || !observed) return;\n // html/body too: a non-part sibling growing moves parts without resizing them.\n var els = [document.documentElement, document.body].concat([].slice.call(document.querySelectorAll('[data-part]')));\n for (var i = 0; i < els.length; i++) {\n var el = els[i];\n if (el && !observed.has(el)) { observed.add(el); ro.observe(el); }\n }\n }\n var moTimer = 0;\n var mo = new MutationObserver(function () {\n clearTimeout(moTimer);\n moTimer = setTimeout(function () { observeAll(); schedule(); }, 40);\n });\n mo.observe(document.documentElement, { subtree: true, childList: true, attributes: true, characterData: true });\n\n // Transforms and transitions move boxes without resizing or mutating anything.\n document.addEventListener('animationend', schedule, true);\n document.addEventListener('transitionend', schedule, true);\n window.addEventListener('scroll', schedule, { passive: true });\n window.addEventListener('resize', schedule);\n if (document.fonts && document.fonts.ready) document.fonts.ready.then(schedule);\n observeAll();\n schedule();\n // WebKit: RO's initial callback may never fire in a sandboxed frame.\n function settle() {\n schedule();\n [50, 150, 400, 1000].forEach(function (ms) { setTimeout(schedule, ms); });\n }\n if (document.readyState === 'complete') settle();\n else window.addEventListener('load', settle);\n\n function setHighlight(names) {\n var on = document.querySelectorAll('.' + HL);\n for (var i = 0; i < on.length; i++) on[i].classList.remove(HL);\n if (names && names.length) {\n var want = {};\n for (var k = 0; k < names.length && k < MAX_PARTS; k++) want[String(names[k])] = true;\n var els = document.querySelectorAll('[data-part]');\n for (var j = 0; j < els.length; j++) {\n if (want[els[j].getAttribute('data-part')] === true) els[j].classList.add(HL);\n }\n }\n // Our own class flip must not look like the agent's page changing.\n mo.takeRecords();\n }\n\n // Try: the host lets the user click through the design; an element marked\n // data-goto moves them to another screen. Only the host knows what that\n // names, so the frame just reports it, and the click goes no further.\n var trying = false;\n function setTrying(on) {\n trying = on;\n document.documentElement.classList.toggle('mockpit-try', on);\n mo.takeRecords();\n }\n window.addEventListener('click', function (e) {\n if (!trying) return;\n var t = e.target && e.target.closest ? e.target.closest('[data-goto]') : null;\n if (!t) return;\n e.preventDefault();\n e.stopPropagation();\n parent.postMessage({\n __mockpit: true,\n type: 'goto',\n version: version,\n to: text(t.getAttribute('data-goto')),\n }, '*');\n }, true);\n\n // A finger's tap, in document px, for a host that lets touches through to the\n // page (its canvas, where the page scrolls natively). A drag is a scroll: the\n // browser cancels the pointer or it moves too far. A mouse clicks the host's\n // own overlay instead, so it never reports here.\n var tapAt = null;\n window.addEventListener('pointerdown', function (e) {\n tapAt = e.isPrimary && e.pointerType !== 'mouse' ? { x: e.clientX, y: e.clientY, t: Date.now() } : null;\n }, true);\n window.addEventListener('pointercancel', function () { tapAt = null; }, true);\n window.addEventListener('pointerup', function (e) {\n var a = tapAt;\n tapAt = null;\n if (!a || !e.isPrimary) return;\n if (Math.abs(e.clientX - a.x) > 10 || Math.abs(e.clientY - a.y) > 10 || Date.now() - a.t > 600) return;\n parent.postMessage({\n __mockpit: true,\n type: 'tap',\n x: e.clientX + (window.scrollX || 0),\n y: e.clientY + (window.scrollY || 0),\n }, '*');\n }, true);\n\n // Two fingers are the host's: a page laid on its canvas takes one-finger\n // scrolls itself, but a pinch zooms the canvas, not this page or the browser.\n var pinching = false;\n function pinch(phase, e) {\n var t = e.touches;\n parent.postMessage({\n __mockpit: true,\n type: 'pinch',\n phase: phase,\n a: t[0] ? { x: t[0].clientX, y: t[0].clientY } : null,\n b: t[1] ? { x: t[1].clientX, y: t[1].clientY } : null,\n }, '*');\n }\n window.addEventListener('touchstart', function (e) {\n if (e.touches.length !== 2) return;\n e.preventDefault();\n pinching = true;\n pinch('start', e);\n }, { capture: true, passive: false });\n window.addEventListener('touchmove', function (e) {\n if (!pinching || e.touches.length < 2) return;\n e.preventDefault();\n pinch('move', e);\n }, { capture: true, passive: false });\n function release(e) {\n if (!pinching || e.touches.length >= 2) return;\n pinching = false;\n pinch('end', e);\n }\n window.addEventListener('touchend', release, true);\n window.addEventListener('touchcancel', release, true);\n // \u2318/Ctrl+scroll and a trackpad pinch (ctrl+wheel) are the host's too: they\n // zoom the canvas around the pointer, never this page or the browser.\n window.addEventListener('wheel', function (e) {\n if (!e.ctrlKey && !e.metaKey) return;\n e.preventDefault();\n parent.postMessage({ __mockpit: true, type: 'zoom', x: e.clientX, y: e.clientY, dy: e.deltaY }, '*');\n }, { capture: true, passive: false });\n\n window.addEventListener('message', function (e) {\n if (e.source !== parent) return;\n var d = e.data;\n if (!d || d.__mockpit !== true) return;\n if (d.type === 'highlight') setHighlight(Array.isArray(d.parts) ? d.parts : []);\n else if (d.type === 'clear') setHighlight(null);\n else if (d.type === 'try') setTrying(d.on === true);\n else if (d.type === 'scroll') window.scrollBy(Number(d.dx) || 0, Number(d.dy) || 0);\n else if (d.type === 'hit') {\n var el = document.elementFromPoint(\n (Number(d.x) || 0) - (window.scrollX || 0),\n (Number(d.y) || 0) - (window.scrollY || 0)\n );\n var part = el && el.closest ? el.closest('[data-part]') : null;\n parent.postMessage({\n __mockpit: true,\n type: 'hit',\n ref: d.ref,\n part: part ? text(part.getAttribute('data-part')) : null,\n key: part ? text(part.getAttribute('data-part-key')) : null,\n }, '*');\n }\n });\n})();\n";
7
+ export declare const KNOBS_JS = "\n(function () {\n\nvar __kPathRe = /^[A-Za-z_][\\w-]{0,63}(\\.[A-Za-z_][\\w-]{0,63}){0,3}$/;\nvar __kTokenRe = /^[#\\w(),.%/+-][#\\w\\s(),.%/+-]*$/;\nfunction __kName(path) { return path.replace(/\\./g, '-'); }\nfunction __kCssString(s) {\n return '\"' + s.replace(/[\\\\\"]/g, function (c) { return '\\\\' + c; })\n .replace(/[\\u0000-\\u001f\\u007f]/g, function (c) { return '\\\\' + c.charCodeAt(0).toString(16) + ' '; }) + '\"';\n}\nfunction __kCss(v) {\n if (typeof v === 'number') return isFinite(v) ? String(v) : null;\n if (typeof v === 'boolean') return v ? '1' : '0';\n if (typeof v === 'string') return __kTokenRe.test(v) ? v : __kCssString(v);\n return null;\n}\nfunction __kVars(path, v) {\n var name = __kName(path);\n if (v && typeof v === 'object' && !Array.isArray(v)) {\n var out = [];\n for (var f in v) {\n if (!Object.prototype.hasOwnProperty.call(v, f) || !/^[A-Za-z_][\\w-]{0,63}$/.test(f)) continue;\n var c = __kCss(v[f]);\n if (c !== null) out.push([name + '-' + f, c]);\n }\n return out;\n }\n var css = __kCss(v);\n return css === null ? [] : [[name, css]];\n}\nfunction __kAttr(v) {\n if (typeof v === 'number') return isFinite(v) ? String(v) : null;\n if (typeof v === 'boolean') return v ? 'true' : 'false';\n if (typeof v === 'string') return v;\n return null;\n}\n\n var doc = window.__mockpitDoc || {};\n var current = {};\n function own(o, k) { return Object.prototype.hasOwnProperty.call(o, k); }\n function bind(path, v) {\n var els = document.querySelectorAll('[data-k-bind]');\n for (var i = 0; i < els.length; i++) {\n if (els[i].getAttribute('data-k-bind') !== path) continue;\n els[i].textContent = v && typeof v === 'object' ? JSON.stringify(v) : String(v);\n }\n }\n function apply(values, live) {\n var root = document.documentElement;\n for (var path in values) {\n if (!own(values, path) || !__kPathRe.test(path)) continue;\n var v = values[path];\n if (v === null || v === undefined) continue;\n current[path] = v;\n if (live) {\n var vars = __kVars(path, v);\n if (!vars.length) root.style.removeProperty('--k-' + __kName(path));\n for (var i = 0; i < vars.length; i++) root.style.setProperty('--k-' + vars[i][0], vars[i][1]);\n var attr = __kAttr(v);\n if (attr === null) root.removeAttribute('data-k-' + __kName(path));\n else root.setAttribute('data-k-' + __kName(path), attr);\n }\n bind(path, v);\n }\n window.dispatchEvent(new CustomEvent('mockpit:knobs', {\n detail: { values: JSON.parse(JSON.stringify(current)) },\n }));\n }\n // The URL's values are already on <html>; a live command may also have come\n // in before the body existed, so both are bound once it does.\n function initial() {\n var start = doc.knobs && typeof doc.knobs === 'object' ? doc.knobs : {};\n for (var k in start) if (own(start, k) && !own(current, k)) current[k] = start[k];\n apply(current, false);\n }\n if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', initial);\n else initial();\n // Runs from <head>, before the page's own markup and anything slow it loads\n // (a web font, a CDN stylesheet), so the host can send values at once rather\n // than wait for the first parts report, which those can hold back.\n if (parent !== window) parent.postMessage({ __mockpit: true, type: 'ready', version: doc.version }, '*');\n window.addEventListener('message', function (e) {\n if (e.source !== parent || parent === window) return;\n var d = e.data;\n if (!d || d.__mockpit !== true || d.type !== 'knobs') return;\n if (d.values && typeof d.values === 'object' && !Array.isArray(d.values)) apply(d.values, true);\n });\n})();\n";
8
8
  export declare function knobCss(v: unknown): string | null;
9
9
  export declare function knobVars(path: string, v: unknown): [string, string][];
10
10
  export declare function knobAttr(v: unknown): string | null;
@@ -13,6 +13,10 @@ export declare function knobPreamble(version: number | undefined, knobs: Record<
13
13
  htmlTag: string;
14
14
  headScript: string;
15
15
  };
16
+ export declare function knobAttributes(knobs: Record<string, unknown> | undefined): {
17
+ attrs: string;
18
+ values: Record<string, unknown>;
19
+ };
16
20
  export interface StaticAsset {
17
21
  path: string;
18
22
  contentType: string;