@storyshelf/app 0.7.0 → 0.9.0-next-d2cc082

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 (270) hide show
  1. package/dist/app-types.d.mts +1 -3
  2. package/dist/asset-manifest.mjs +1 -2
  3. package/dist/assets/htmx.mjs +1 -2
  4. package/dist/assets/mark.mjs +1 -2
  5. package/dist/boot-tasks.mjs +1 -2
  6. package/dist/build-state-flags.mjs +1 -2
  7. package/dist/build-state-setup.mjs +1 -2
  8. package/dist/capture-alerts.mjs +1 -2
  9. package/dist/capture-process.mjs +1 -2
  10. package/dist/capture-setup.mjs +1 -2
  11. package/dist/connections.mjs +1 -2
  12. package/dist/consumer-setup.mjs +1 -2
  13. package/dist/credential-keys.mjs +1 -2
  14. package/dist/credentials.mjs +1 -2
  15. package/dist/design-events.mjs +71 -0
  16. package/dist/design-events.mjs.map +1 -0
  17. package/dist/git-status.mjs +1 -2
  18. package/dist/hidden-data.mjs +1 -2
  19. package/dist/index.d.mts +0 -2
  20. package/dist/index.mjs +2 -3
  21. package/dist/insights/budget.mjs +1 -2
  22. package/dist/insights/deps.mjs +1 -2
  23. package/dist/insights/errors.mjs +1 -2
  24. package/dist/insights/health-panel.mjs +0 -1
  25. package/dist/insights/job.mjs +1 -2
  26. package/dist/insights/load-health.mjs +1 -2
  27. package/dist/insights/load-triage.mjs +1 -2
  28. package/dist/insights/panel.mjs +1 -2
  29. package/dist/insights/request.mjs +1 -2
  30. package/dist/insights/stale.mjs +1 -2
  31. package/dist/insights/usage-report.mjs +1 -2
  32. package/dist/insights/view.mjs +1 -2
  33. package/dist/job-canceller.mjs +1 -2
  34. package/dist/job-queue.mjs +1 -2
  35. package/dist/jobs-setup.mjs +1 -2
  36. package/dist/lifecycle.mjs +2 -3
  37. package/dist/lifecycle.mjs.map +1 -1
  38. package/dist/middleware/auth-gate.mjs +1 -2
  39. package/dist/middleware/csrf.mjs +1 -2
  40. package/dist/middleware/rate-limit.mjs +1 -2
  41. package/dist/middleware/request-log.mjs +1 -2
  42. package/dist/middleware/require-review.mjs +1 -2
  43. package/dist/middleware/require-testing.mjs +1 -2
  44. package/dist/middleware/server-timing.mjs +1 -2
  45. package/dist/middleware/setup-gate.mjs +1 -2
  46. package/dist/middleware/store-scope.mjs +1 -2
  47. package/dist/notifications-pause.mjs +1 -2
  48. package/dist/notify.d.mts +1 -3
  49. package/dist/notify.mjs +5 -3
  50. package/dist/notify.mjs.map +1 -1
  51. package/dist/pages/a11y-violations.mjs +1 -2
  52. package/dist/pages/admin-ai.mjs +1 -2
  53. package/dist/pages/admin-system-origin.mjs +19 -10
  54. package/dist/pages/admin-system-origin.mjs.map +1 -1
  55. package/dist/pages/admin-system-targets.mjs +1 -2
  56. package/dist/pages/admin-system.mjs +1 -2
  57. package/dist/pages/build-detail.mjs +1 -2
  58. package/dist/pages/build-diff-actions.mjs +1 -2
  59. package/dist/pages/build-diff-comments.mjs +1 -2
  60. package/dist/pages/build-diff-default.mjs +53 -0
  61. package/dist/pages/build-diff-default.mjs.map +1 -0
  62. package/dist/pages/build-diff-environment.mjs +34 -5
  63. package/dist/pages/build-diff-environment.mjs.map +1 -1
  64. package/dist/pages/build-diff-header.mjs +1 -2
  65. package/dist/pages/build-diff-insight.mjs +1 -2
  66. package/dist/pages/build-diff-model.mjs +20 -3
  67. package/dist/pages/build-diff-model.mjs.map +1 -1
  68. package/dist/pages/build-diff-nav.mjs +1 -2
  69. package/dist/pages/build-diff-reference.mjs +84 -0
  70. package/dist/pages/build-diff-reference.mjs.map +1 -0
  71. package/dist/pages/build-diff-shortcuts.mjs +1 -2
  72. package/dist/pages/build-diff-stale.mjs +1 -2
  73. package/dist/pages/build-diff-usage.mjs +1 -2
  74. package/dist/pages/build-diff-viewer.mjs +38 -12
  75. package/dist/pages/build-diff-viewer.mjs.map +1 -1
  76. package/dist/pages/build-diff.mjs +6 -12
  77. package/dist/pages/build-diff.mjs.map +1 -1
  78. package/dist/pages/build-tests.mjs +1 -2
  79. package/dist/pages/component-detail.mjs +1 -2
  80. package/dist/pages/component-stories.mjs +1 -2
  81. package/dist/pages/compute-jobs.mjs +1 -2
  82. package/dist/pages/design-links.mjs +1 -2
  83. package/dist/pages/environment-change.mjs +16 -4
  84. package/dist/pages/environment-change.mjs.map +1 -1
  85. package/dist/pages/health-panel.mjs +1 -2
  86. package/dist/pages/invite.mjs +1 -2
  87. package/dist/pages/label-detail.mjs +1 -2
  88. package/dist/pages/library-grid.mjs +1 -2
  89. package/dist/pages/library-model.mjs +1 -2
  90. package/dist/pages/library-snapshots.mjs +1 -2
  91. package/dist/pages/library.mjs +1 -2
  92. package/dist/pages/login.mjs +1 -2
  93. package/dist/pages/notifications-paused.mjs +1 -2
  94. package/dist/pages/passkey-ceremony.mjs +1 -2
  95. package/dist/pages/profile.mjs +1 -2
  96. package/dist/pages/project-builds-table.mjs +1 -2
  97. package/dist/pages/project-builds.mjs +1 -2
  98. package/dist/pages/project-create.mjs +1 -2
  99. package/dist/pages/project-settings.mjs +1 -2
  100. package/dist/pages/projects-card.mjs +1 -2
  101. package/dist/pages/projects.mjs +1 -2
  102. package/dist/pages/review-changeset.mjs +1 -2
  103. package/dist/pages/review-comments.mjs +1 -2
  104. package/dist/pages/review-verdict.mjs +1 -2
  105. package/dist/pages/root.mjs +1 -2
  106. package/dist/pages/settings-ai.mjs +1 -2
  107. package/dist/pages/settings-capture.mjs +1 -2
  108. package/dist/pages/settings-design-credential.mjs +1 -2
  109. package/dist/pages/settings-design-links.mjs +1 -2
  110. package/dist/pages/settings-design.mjs +1 -2
  111. package/dist/pages/settings-general.mjs +1 -2
  112. package/dist/pages/settings-labels.mjs +1 -2
  113. package/dist/pages/settings-links.mjs +1 -2
  114. package/dist/pages/settings-members.mjs +1 -2
  115. package/dist/pages/settings-notifications-git.mjs +1 -2
  116. package/dist/pages/settings-notifications.mjs +1 -2
  117. package/dist/pages/settings-tests.mjs +1 -2
  118. package/dist/pages/settings-tokens.mjs +1 -2
  119. package/dist/pages/settings-webhooks.mjs +1 -2
  120. package/dist/pages/storybook.mjs +1 -2
  121. package/dist/pages/target-choices.mjs +1 -2
  122. package/dist/publish-setup.mjs +1 -2
  123. package/dist/retention-timer.mjs +10 -11
  124. package/dist/retention-timer.mjs.map +1 -1
  125. package/dist/review-events.mjs +1 -2
  126. package/dist/routers/admin-pages.mjs +1 -2
  127. package/dist/routers/admin-public-origin.mjs +10 -3
  128. package/dist/routers/admin-public-origin.mjs.map +1 -1
  129. package/dist/routers/admin.mjs +8 -4
  130. package/dist/routers/admin.mjs.map +1 -1
  131. package/dist/routers/assets.mjs +1 -2
  132. package/dist/routers/attempts.mjs +1 -2
  133. package/dist/routers/auth-sync.mjs +1 -2
  134. package/dist/routers/auth.mjs +1 -2
  135. package/dist/routers/baseline-guard.mjs +2 -3
  136. package/dist/routers/baseline-guard.mjs.map +1 -1
  137. package/dist/routers/baseline-reference.mjs +72 -0
  138. package/dist/routers/baseline-reference.mjs.map +1 -0
  139. package/dist/routers/baselines.handlers.mjs +1 -2
  140. package/dist/routers/baselines.mjs +1 -2
  141. package/dist/routers/build-cancel.mjs +1 -2
  142. package/dist/routers/build-changeset.mjs +1 -2
  143. package/dist/routers/build-reviews.handlers.mjs +1 -2
  144. package/dist/routers/build-reviews.mjs +1 -2
  145. package/dist/routers/builds.handlers.mjs +23 -9
  146. package/dist/routers/builds.handlers.mjs.map +1 -1
  147. package/dist/routers/builds.mjs +7 -3
  148. package/dist/routers/builds.mjs.map +1 -1
  149. package/dist/routers/capture-cache.mjs +29 -0
  150. package/dist/routers/capture-cache.mjs.map +1 -0
  151. package/dist/routers/capture-config.mjs +1 -2
  152. package/dist/routers/comments.mjs +1 -2
  153. package/dist/routers/comparison-image.mjs +29 -0
  154. package/dist/routers/comparison-image.mjs.map +1 -0
  155. package/dist/routers/component-catalog.handlers.mjs +1 -2
  156. package/dist/routers/component.handlers.mjs +1 -2
  157. package/dist/routers/connections.mjs +1 -2
  158. package/dist/routers/design-connect.handlers.mjs +1 -2
  159. package/dist/routers/design.handlers.mjs +8 -2
  160. package/dist/routers/design.handlers.mjs.map +1 -1
  161. package/dist/routers/flash.mjs +1 -2
  162. package/dist/routers/health-report.mjs +1 -2
  163. package/dist/routers/health.mjs +1 -2
  164. package/dist/routers/helpers.mjs +1 -2
  165. package/dist/routers/htmx.mjs +1 -2
  166. package/dist/routers/insights-ui.mjs +1 -2
  167. package/dist/routers/insights.handlers.mjs +1 -2
  168. package/dist/routers/insights.mjs +1 -2
  169. package/dist/routers/labels.mjs +1 -2
  170. package/dist/routers/links.handlers.mjs +1 -2
  171. package/dist/routers/links.mjs +1 -2
  172. package/dist/routers/media.mjs +14 -5
  173. package/dist/routers/media.mjs.map +1 -1
  174. package/dist/routers/members.mjs +1 -2
  175. package/dist/routers/notification-channels.mjs +1 -2
  176. package/dist/routers/notification-subscriptions.mjs +1 -2
  177. package/dist/routers/oauth-state.mjs +1 -2
  178. package/dist/routers/preview-base.mjs +1 -2
  179. package/dist/routers/preview-token.mjs +1 -2
  180. package/dist/routers/profile.mjs +1 -2
  181. package/dist/routers/projects.mjs +1 -2
  182. package/dist/routers/publish-upload.mjs +19 -12
  183. package/dist/routers/publish-upload.mjs.map +1 -1
  184. package/dist/routers/published-access.mjs +1 -2
  185. package/dist/routers/published-compression.mjs +109 -0
  186. package/dist/routers/published-compression.mjs.map +1 -0
  187. package/dist/routers/published-files.mjs +26 -9
  188. package/dist/routers/published-files.mjs.map +1 -1
  189. package/dist/routers/review-forms.mjs +1 -2
  190. package/dist/routers/review-next.mjs +1 -2
  191. package/dist/routers/review-section.handlers.mjs +1 -2
  192. package/dist/routers/review-switch-off.mjs +1 -2
  193. package/dist/routers/schemas.mjs +1 -2
  194. package/dist/routers/scoped-models.mjs +1 -2
  195. package/dist/routers/settings-ai.mjs +1 -2
  196. package/dist/routers/settings-capture.mjs +1 -2
  197. package/dist/routers/settings-design.mjs +1 -2
  198. package/dist/routers/settings-general.mjs +1 -2
  199. package/dist/routers/settings-git-channel.mjs +1 -2
  200. package/dist/routers/settings-labels.mjs +1 -2
  201. package/dist/routers/settings-links.mjs +1 -2
  202. package/dist/routers/settings-members.mjs +1 -2
  203. package/dist/routers/settings-notifications.mjs +1 -2
  204. package/dist/routers/settings-tokens.mjs +1 -2
  205. package/dist/routers/settings-webhooks.mjs +1 -2
  206. package/dist/routers/settings.handlers.mjs +1 -2
  207. package/dist/routers/settings.mjs +1 -2
  208. package/dist/routers/signed-storybook.mjs +5 -3
  209. package/dist/routers/signed-storybook.mjs.map +1 -1
  210. package/dist/routers/snapshot-groups.mjs +1 -2
  211. package/dist/routers/snapshots.mjs +1 -2
  212. package/dist/routers/storybook.mjs +21 -17
  213. package/dist/routers/storybook.mjs.map +1 -1
  214. package/dist/routers/tokens.mjs +1 -2
  215. package/dist/routers/ui.mjs +4 -2
  216. package/dist/routers/ui.mjs.map +1 -1
  217. package/dist/routers/webhooks.mjs +1 -2
  218. package/dist/routers/well-known.mjs +1 -2
  219. package/dist/routes/api-routes.mjs +1 -2
  220. package/dist/routes/classification.mjs +1 -2
  221. package/dist/routes/openapi-classes.mjs +1 -2
  222. package/dist/routes/page-routes.mjs +2 -3
  223. package/dist/routes/page-routes.mjs.map +1 -1
  224. package/dist/runtime.mjs +1 -2
  225. package/dist/serverless.mjs +1 -2
  226. package/dist/store.mjs +1 -2
  227. package/dist/targets.mjs +1 -2
  228. package/dist/ui/avatar.mjs +1 -2
  229. package/dist/ui/buttons.mjs +1 -2
  230. package/dist/ui/code-block.mjs +1 -2
  231. package/dist/ui/compare-stage.mjs +86 -54
  232. package/dist/ui/compare-stage.mjs.map +1 -1
  233. package/dist/ui/csrf-field.mjs +1 -2
  234. package/dist/ui/css.mjs +1 -2
  235. package/dist/ui/design-reference.mjs +1 -2
  236. package/dist/ui/document.mjs +1 -2
  237. package/dist/ui/dropdown.mjs +1 -2
  238. package/dist/ui/feedback.mjs +1 -2
  239. package/dist/ui/filter-input.mjs +1 -2
  240. package/dist/ui/forms.mjs +1 -2
  241. package/dist/ui/icons/icon.mjs +1 -2
  242. package/dist/ui/icons/paths.mjs +1 -2
  243. package/dist/ui/icons/sprite.mjs +1 -2
  244. package/dist/ui/kbd.mjs +1 -2
  245. package/dist/ui/layout.mjs +1 -2
  246. package/dist/ui/preview-embed.mjs +1 -2
  247. package/dist/ui/progress.mjs +1 -2
  248. package/dist/ui/segmented.mjs +1 -2
  249. package/dist/ui/shell/account.mjs +1 -2
  250. package/dist/ui/shell/bare.mjs +1 -2
  251. package/dist/ui/shell/boot-script.mjs +1 -2
  252. package/dist/ui/shell/client-script.mjs +1 -2
  253. package/dist/ui/shell/nav.mjs +1 -2
  254. package/dist/ui/shell/review-script.mjs +17 -3
  255. package/dist/ui/shell/review-script.mjs.map +1 -1
  256. package/dist/ui/shell/sidebar.mjs +1 -2
  257. package/dist/ui/shell/topbar.mjs +1 -2
  258. package/dist/ui/stacks.mjs +1 -2
  259. package/dist/ui/styles/base.mjs +1 -2
  260. package/dist/ui/styles/behavior.mjs +1 -2
  261. package/dist/ui/styles/review-layout.mjs +1 -2
  262. package/dist/ui/styles/review.mjs +1 -2
  263. package/dist/ui/styles/tokens.mjs +1 -2
  264. package/dist/ui/styles.mjs +1 -2
  265. package/dist/ui/subnav.mjs +1 -2
  266. package/dist/ui/table.mjs +1 -2
  267. package/dist/ui/theme.mjs +1 -2
  268. package/dist/ui/thumbnail.mjs +1 -2
  269. package/dist/ui/time.mjs +1 -2
  270. package/package.json +9 -18
@@ -7,7 +7,7 @@ import { Kbd } from "../ui/kbd.mjs";
7
7
  import { groupOf, neighbours, snapshotPageUrl } from "./build-diff-model.mjs";
8
8
  import { actionBar, actionGroup, actionHints, actionPosition } from "../ui/styles/review-layout.mjs";
9
9
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
10
- //#region src/pages/build-diff-actions.tsx
10
+ // src/pages/build-diff-actions.tsx
11
11
  function apiBase(project, build) {
12
12
  return `/api/v1/projects/${project.slug}/builds/${build.id}`;
13
13
  }
@@ -210,7 +210,6 @@ function DiffActionBar(props) {
210
210
  ]
211
211
  });
212
212
  }
213
- //#endregion
214
213
  export { DiffActionBar };
215
214
 
216
215
  //# sourceMappingURL=build-diff-actions.mjs.map
@@ -9,7 +9,7 @@ import { VStack } from "../ui/stacks.mjs";
9
9
  import { csrfField } from "../ui/csrf-field.mjs";
10
10
  import { reviewComment, reviewCommentActions, reviewCommentBody, reviewCommentHead } from "../ui/styles/review.mjs";
11
11
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
12
- //#region src/pages/build-diff-comments.tsx
12
+ // src/pages/build-diff-comments.tsx
13
13
  /** One comment with an optional resolve action. */
14
14
  function CommentCard(props) {
15
15
  const { project, build, comment, canReview, anchor } = props;
@@ -100,7 +100,6 @@ function DiffComments(props) {
100
100
  })
101
101
  ] });
102
102
  }
103
- //#endregion
104
103
  export { CommentCard, DiffComments };
105
104
 
106
105
  //# sourceMappingURL=build-diff-comments.mjs.map
@@ -0,0 +1,53 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { css } from "../ui/css.mjs";
5
+ import { HStack } from "../ui/stacks.mjs";
6
+ import { snapshotImageUrl } from "./build-diff-model.mjs";
7
+ import { jsx, jsxs } from "hono/jsx/jsx-runtime";
8
+ // src/pages/build-diff-default.tsx
9
+ const comparisonImage = css`
10
+ /* default-comparison-image */
11
+ max-width: 100%;
12
+ `;
13
+ function DefaultComparisonView(props) {
14
+ const { project, build, snapshot, comparison } = props;
15
+ const params = new URLSearchParams({
16
+ reference: "default",
17
+ id: comparison.id,
18
+ version: comparison.version
19
+ });
20
+ const baseline = `${snapshotImageUrl(project, build, snapshot, "baseline")}?${params}`;
21
+ return /* @__PURE__ */ jsxs("details", { children: [
22
+ /* @__PURE__ */ jsxs("summary", { children: [
23
+ "Compare with current default branch (",
24
+ comparison.branch,
25
+ ")"
26
+ ] }),
27
+ /* @__PURE__ */ jsxs("p", { children: [
28
+ "This is a secondary comparison. Approval still uses the primary comparison above. Default baseline updated ",
29
+ comparison.version,
30
+ "."
31
+ ] }),
32
+ /* @__PURE__ */ jsx("p", { children: comparison.environment === "different" ? "Recorded environment fingerprints differ; some components may be unknown and differences may be environmental." : comparison.environment === "unknown" ? "Capture environment information is unavailable." : "Recorded capture environment fingerprints match; some components may be unknown." }),
33
+ comparison.available ? /* @__PURE__ */ jsxs(HStack, {
34
+ align: "start",
35
+ children: [/* @__PURE__ */ jsx(BaselineImage, { src: baseline }), /* @__PURE__ */ jsxs("figure", { children: [/* @__PURE__ */ jsx("figcaption", { children: "This build" }), /* @__PURE__ */ jsx("img", {
36
+ src: snapshotImageUrl(project, build, snapshot, "image"),
37
+ alt: "This build's screenshot",
38
+ class: comparisonImage
39
+ })] })]
40
+ }) : /* @__PURE__ */ jsx("p", { children: "The current default-branch baseline image is unavailable." })
41
+ ] });
42
+ }
43
+ function BaselineImage(props) {
44
+ return /* @__PURE__ */ jsxs("figure", { children: [/* @__PURE__ */ jsx("figcaption", { children: "Current default-branch baseline" }), /* @__PURE__ */ jsx("img", {
45
+ src: props.src,
46
+ alt: "Current default-branch baseline",
47
+ loading: "lazy",
48
+ class: comparisonImage
49
+ })] });
50
+ }
51
+ export { DefaultComparisonView };
52
+
53
+ //# sourceMappingURL=build-diff-default.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-diff-default.mjs","names":[],"sources":["../../src/pages/build-diff-default.tsx"],"sourcesContent":["import type { Build, Project, Snapshot } from \"@storyshelf/core/schema\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport type { DefaultComparison } from \"../routers/baseline-reference.ts\";\nimport { HStack } from \"../ui/components.tsx\";\nimport { css } from \"../ui/css.ts\";\nimport { snapshotImageUrl } from \"./build-diff-model.ts\";\n\nconst comparisonImage = css`\n /* default-comparison-image */\n max-width: 100%;\n`;\n\n/* eslint-disable promise-function-async -- Hono JSX components may return HTML or a promise */\nexport function DefaultComparisonView(props: {\n project: Project;\n build: Build;\n snapshot: Snapshot;\n comparison: DefaultComparison;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, build, snapshot, comparison } = props;\n const params = new URLSearchParams({\n reference: \"default\",\n id: comparison.id,\n version: comparison.version,\n });\n const baseline = `${snapshotImageUrl(project, build, snapshot, \"baseline\")}?${params}`;\n return (\n <details>\n <summary>Compare with current default branch ({comparison.branch})</summary>\n <p>\n This is a secondary comparison. Approval still uses the primary comparison above. Default\n baseline updated {comparison.version}.\n </p>\n <p>\n {comparison.environment === \"different\"\n ? \"Recorded environment fingerprints differ; some components may be unknown and differences may be environmental.\"\n : comparison.environment === \"unknown\"\n ? \"Capture environment information is unavailable.\"\n : \"Recorded capture environment fingerprints match; some components may be unknown.\"}\n </p>\n {comparison.available ? (\n <HStack align=\"start\">\n <BaselineImage src={baseline} />\n <figure>\n <figcaption>This build</figcaption>\n <img\n src={snapshotImageUrl(project, build, snapshot, \"image\")}\n alt=\"This build's screenshot\"\n class={comparisonImage}\n />\n </figure>\n </HStack>\n ) : (\n <p>The current default-branch baseline image is unavailable.</p>\n )}\n </details>\n );\n}\n\nfunction BaselineImage(props: { src: string }): HtmlEscapedString | Promise<HtmlEscapedString> {\n return (\n <figure>\n <figcaption>Current default-branch baseline</figcaption>\n <img\n src={props.src}\n alt=\"Current default-branch baseline\"\n loading=\"lazy\"\n class={comparisonImage}\n />\n </figure>\n );\n}\n"],"mappings":";;;;;;;;AAOA,MAAM,kBAAkB,GAAG;;;;AAM3B,SAAgB,sBAAsB,OAKa;CACjD,MAAM,EAAE,SAAS,OAAO,UAAU,eAAe;CACjD,MAAM,SAAS,IAAI,gBAAgB;EACjC,WAAW;EACX,IAAI,WAAW;EACf,SAAS,WAAW;CACtB,CAAC;CACD,MAAM,WAAW,GAAG,iBAAiB,SAAS,OAAO,UAAU,UAAU,EAAE,GAAG;CAC9E,OACE,qBAAC,WAAD,EAAA,UAAA;EACE,qBAAC,WAAD,EAAA,UAAA;GAAS;GAAsC,WAAW;GAAO;EAAU,EAAA,CAAA;EAC3E,qBAAC,KAAD,EAAA,UAAA;GAAG;GAEiB,WAAW;GAAQ;EACpC,EAAA,CAAA;EACH,oBAAC,KAAD,EAAA,UACG,WAAW,gBAAgB,cACxB,mHACA,WAAW,gBAAgB,YACzB,oDACA,mFACL,CAAA;EACF,WAAW,YACV,qBAAC,QAAD;GAAQ,OAAM;GAAd,UAAA,CACE,oBAAC,eAAD,EAAe,KAAK,SAAW,CAAA,GAC/B,qBAAC,UAAD,EAAA,UAAA,CACE,oBAAC,cAAD,EAAA,UAAY,aAAsB,CAAA,GAClC,oBAAC,OAAD;IACE,KAAK,iBAAiB,SAAS,OAAO,UAAU,OAAO;IACvD,KAAI;IACJ,OAAO;GACR,CAAA,CACK,EAAA,CAAA,CACF;EAER,CAAA,IAAA,oBAAC,KAAD,EAAA,UAAG,4DAA4D,CAAA;CAE1D,EAAA,CAAA;AAEb;AAEA,SAAS,cAAc,OAAwE;CAC7F,OACE,qBAAC,UAAD,EAAA,UAAA,CACE,oBAAC,cAAD,EAAA,UAAY,kCAA2C,CAAA,GACvD,oBAAC,OAAD;EACE,KAAK,MAAM;EACX,KAAI;EACJ,SAAQ;EACR,OAAO;CACR,CAAA,CACK,EAAA,CAAA;AAEZ"}
@@ -3,14 +3,38 @@ import "node:url";
3
3
  import.meta.url;
4
4
  import { Alert } from "../ui/feedback.mjs";
5
5
  import { StaleBaselineNotice } from "./build-diff-stale.mjs";
6
- import { environmentChangeOf, labelOf, summariseEnvironmentChanges } from "./environment-change.mjs";
6
+ import { environmentChangeOf, labelOf, summariseEnvironmentChanges, summarisePartlyUnknown } from "./environment-change.mjs";
7
7
  import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
8
- //#region src/pages/build-diff-environment.tsx
8
+ // src/pages/build-diff-environment.tsx
9
9
  /**
10
10
  * Build-level note: some snapshots were captured in a different environment than their baseline.
11
11
  * The default branch migrates them without a diff; a feature branch still diffs them (ADR 0005).
12
12
  */
13
13
  function EnvironmentChangeNotice(props) {
14
+ const unknown = summarisePartlyUnknown(props.snapshots);
15
+ const changed = /* @__PURE__ */ jsx(EnvironmentMigrationNotice, { ...props });
16
+ return unknown ? /* @__PURE__ */ jsxs(Fragment, { children: [changed, /* @__PURE__ */ jsx(PartlyUnknownNotice, {
17
+ count: unknown.count,
18
+ fields: unknown.fields
19
+ })] }) : changed;
20
+ }
21
+ /** The measurement version changed: some components were compared as unknown, not as different. */
22
+ function PartlyUnknownNotice(props) {
23
+ const { count, fields } = props;
24
+ return /* @__PURE__ */ jsxs(Alert, {
25
+ tone: "info",
26
+ title: "Environment partly unknown (measurement updated)",
27
+ children: [
28
+ count,
29
+ " ",
30
+ count === 1 ? "snapshot has" : "snapshots have",
31
+ " a baseline whose environment was measured an older way, so the ",
32
+ fields.join(", "),
33
+ " could not be compared. They are not treated as a change, and nothing was migrated because of them."
34
+ ]
35
+ });
36
+ }
37
+ function EnvironmentMigrationNotice(props) {
14
38
  const summary = summariseEnvironmentChanges(props.snapshots);
15
39
  if (!summary) return null;
16
40
  const { count, fields, browserVersion } = summary;
@@ -43,8 +67,14 @@ function EnvironmentChangeNotice(props) {
43
67
  function EnvironmentChangeSnapshotNotice(props) {
44
68
  const change = environmentChangeOf(props.snapshot);
45
69
  if (!change) return null;
70
+ const unknown = (change.unknown ?? []).map((field) => labelOf(field));
71
+ const note = unknown.length > 0 ? /* @__PURE__ */ jsx(PartlyUnknownNotice, {
72
+ count: 1,
73
+ fields: unknown
74
+ }) : null;
75
+ if (change.changed.length === 0) return note;
46
76
  const what = change.changed.map((field) => labelOf(field)).join(", ");
47
- return props.migrated ? /* @__PURE__ */ jsxs(Alert, {
77
+ return /* @__PURE__ */ jsxs(Fragment, { children: [props.migrated ? /* @__PURE__ */ jsxs(Alert, {
48
78
  tone: "info",
49
79
  title: "No diff: capture environment changed",
50
80
  children: [
@@ -60,7 +90,7 @@ function EnvironmentChangeSnapshotNotice(props) {
60
90
  what,
61
91
  "), so a difference may come from the environment rather than the code."
62
92
  ]
63
- });
93
+ }), note] });
64
94
  }
65
95
  /** Build-level notices above the review grid: stale baselines and environment migrations. */
66
96
  function ReviewNotices(props) {
@@ -76,7 +106,6 @@ function ReviewNotices(props) {
76
106
  migrated: build.isDefault
77
107
  })] });
78
108
  }
79
- //#endregion
80
109
  export { EnvironmentChangeNotice, EnvironmentChangeSnapshotNotice, ReviewNotices };
81
110
 
82
111
  //# sourceMappingURL=build-diff-environment.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"build-diff-environment.mjs","names":[],"sources":["../../src/pages/build-diff-environment.tsx"],"sourcesContent":["import type { Build, Project, Snapshot } from \"@storyshelf/core/schema\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport { Alert } from \"../ui/components.tsx\";\nimport { StaleBaselineNotice } from \"./build-diff-stale.tsx\";\nimport { environmentChangeOf, labelOf, summariseEnvironmentChanges } from \"./environment-change.ts\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\n/**\n * Build-level note: some snapshots were captured in a different environment than their baseline.\n * The default branch migrates them without a diff; a feature branch still diffs them (ADR 0005).\n */\nexport function EnvironmentChangeNotice(props: {\n snapshots: readonly Snapshot[];\n migrated: boolean;\n}): HtmlEscapedString | Promise<HtmlEscapedString> | null {\n const summary = summariseEnvironmentChanges(props.snapshots);\n if (!summary) {\n return null;\n }\n const { count, fields, browserVersion } = summary;\n const what = `the ${fields.join(\", \")} changed${\n browserVersion ? ` (${browserVersion.from} → ${browserVersion.to})` : \"\"\n }`;\n return props.migrated ? (\n <Alert tone=\"info\" title=\"Capture environment changed\">\n {count} {count === 1 ? \"snapshot was\" : \"snapshots were\"} accepted without a pixel diff\n because {what}. The same code renders differently here, so a diff would only show the\n environment. The baselines were updated to match.\n </Alert>\n ) : (\n <Alert tone=\"info\" title=\"Capture environment changed\">\n {count} {count === 1 ? \"snapshot was\" : \"snapshots were\"} compared against a baseline from a\n different capture environment: {what}. A difference may come from the environment rather than\n the code; once the default branch captures in the new environment, a rebuild compares like\n with like.\n </Alert>\n );\n}\n\n/** Per-snapshot note shown above the viewer for a snapshot whose capture environment changed. */\nexport function EnvironmentChangeSnapshotNotice(props: {\n snapshot: Pick<Snapshot, \"envChange\">;\n migrated: boolean;\n}): HtmlEscapedString | Promise<HtmlEscapedString> | null {\n const change = environmentChangeOf(props.snapshot);\n if (!change) {\n return null;\n }\n const what = change.changed.map((field) => labelOf(field)).join(\", \");\n return props.migrated ? (\n <Alert tone=\"info\" title=\"No diff: capture environment changed\">\n This snapshot was not compared pixel by pixel. What changed: {what}.\n </Alert>\n ) : (\n <Alert tone=\"info\" title=\"Capture environment changed\">\n The baseline was captured in a different environment ({what}), so a difference may come from\n the environment rather than the code.\n </Alert>\n );\n}\n\n/** Build-level notices above the review grid: stale baselines and environment migrations. */\nexport function ReviewNotices(props: {\n project: Project;\n build: Build;\n snapshots: readonly Snapshot[];\n drifted: Record<string, unknown>;\n canReview: boolean;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, build, snapshots, drifted, canReview } = props;\n const staleCount = Object.keys(drifted).length;\n return (\n <>\n {staleCount > 0 ? (\n <StaleBaselineNotice\n project={project}\n build={build}\n staleCount={staleCount}\n canReview={canReview}\n />\n ) : null}\n <EnvironmentChangeNotice snapshots={snapshots} migrated={build.isDefault} />\n </>\n );\n}\n"],"mappings":";;;;;;;;;;;;AAYA,SAAgB,wBAAwB,OAGkB;CACxD,MAAM,UAAU,4BAA4B,MAAM,SAAS;CAC3D,IAAI,CAAC,SACH,OAAO;CAET,MAAM,EAAE,OAAO,QAAQ,mBAAmB;CAC1C,MAAM,OAAO,OAAO,OAAO,KAAK,IAAI,EAAE,UACpC,iBAAiB,KAAK,eAAe,KAAK,KAAK,eAAe,GAAG,KAAK;CAExE,OAAO,MAAM,WACX,qBAAC,OAAD;EAAO,MAAK;EAAO,OAAM;EAAzB,UAAA;GACG;GAAM;GAAE,UAAU,IAAI,iBAAiB;GAAiB;GAChD;GAAK;EAET;CAEP,CAAA,IAAA,qBAAC,OAAD;EAAO,MAAK;EAAO,OAAM;EAAzB,UAAA;GACG;GAAM;GAAE,UAAU,IAAI,iBAAiB;GAAiB;GACzB;GAAK;EAGhC;;AAEX;;AAGA,SAAgB,gCAAgC,OAGU;CACxD,MAAM,SAAS,oBAAoB,MAAM,QAAQ;CACjD,IAAI,CAAC,QACH,OAAO;CAET,MAAM,OAAO,OAAO,QAAQ,KAAK,UAAU,QAAQ,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI;CACpE,OAAO,MAAM,WACX,qBAAC,OAAD;EAAO,MAAK;EAAO,OAAM;EAAzB,UAAA;GAAgE;GACA;GAAK;EAC9D;CAEP,CAAA,IAAA,qBAAC,OAAD;EAAO,MAAK;EAAO,OAAM;EAAzB,UAAA;GAAuD;GACE;GAAK;EAEvD;;AAEX;;AAGA,SAAgB,cAAc,OAMqB;CACjD,MAAM,EAAE,SAAS,OAAO,WAAW,SAAS,cAAc;CAC1D,MAAM,aAAa,OAAO,KAAK,OAAO,CAAC,CAAC;CACxC,OACE,qBAAA,UAAA,EAAA,UAAA,CACG,aAAa,IACZ,oBAAC,qBAAD;EACW;EACF;EACK;EACD;CACZ,CAAA,IACC,MACJ,oBAAC,yBAAD;EAAoC;EAAW,UAAU,MAAM;CAAY,CAAA,CAC3E,EAAA,CAAA;AAEN"}
1
+ {"version":3,"file":"build-diff-environment.mjs","names":[],"sources":["../../src/pages/build-diff-environment.tsx"],"sourcesContent":["import type { Build, Project, Snapshot } from \"@storyshelf/core/schema\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport { Alert } from \"../ui/components.tsx\";\nimport { StaleBaselineNotice } from \"./build-diff-stale.tsx\";\nimport {\n environmentChangeOf,\n labelOf,\n summariseEnvironmentChanges,\n summarisePartlyUnknown,\n} from \"./environment-change.ts\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\n/**\n * Build-level note: some snapshots were captured in a different environment than their baseline.\n * The default branch migrates them without a diff; a feature branch still diffs them (ADR 0005).\n */\nexport function EnvironmentChangeNotice(props: {\n snapshots: readonly Snapshot[];\n migrated: boolean;\n}): HtmlEscapedString | Promise<HtmlEscapedString> | null {\n const unknown = summarisePartlyUnknown(props.snapshots);\n const changed = <EnvironmentMigrationNotice {...props} />;\n return unknown ? (\n <>\n {changed}\n <PartlyUnknownNotice count={unknown.count} fields={unknown.fields} />\n </>\n ) : (\n changed\n );\n}\n\n/** The measurement version changed: some components were compared as unknown, not as different. */\nfunction PartlyUnknownNotice(props: {\n count: number;\n fields: string[];\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { count, fields } = props;\n return (\n <Alert tone=\"info\" title=\"Environment partly unknown (measurement updated)\">\n {count} {count === 1 ? \"snapshot has\" : \"snapshots have\"} a baseline whose environment was\n measured an older way, so the {fields.join(\", \")} could not be compared. They are not treated\n as a change, and nothing was migrated because of them.\n </Alert>\n );\n}\n\nfunction EnvironmentMigrationNotice(props: {\n snapshots: readonly Snapshot[];\n migrated: boolean;\n}): HtmlEscapedString | Promise<HtmlEscapedString> | null {\n const summary = summariseEnvironmentChanges(props.snapshots);\n if (!summary) {\n return null;\n }\n const { count, fields, browserVersion } = summary;\n const what = `the ${fields.join(\", \")} changed${\n browserVersion ? ` (${browserVersion.from} → ${browserVersion.to})` : \"\"\n }`;\n return props.migrated ? (\n <Alert tone=\"info\" title=\"Capture environment changed\">\n {count} {count === 1 ? \"snapshot was\" : \"snapshots were\"} accepted without a pixel diff\n because {what}. The same code renders differently here, so a diff would only show the\n environment. The baselines were updated to match.\n </Alert>\n ) : (\n <Alert tone=\"info\" title=\"Capture environment changed\">\n {count} {count === 1 ? \"snapshot was\" : \"snapshots were\"} compared against a baseline from a\n different capture environment: {what}. A difference may come from the environment rather than\n the code; once the default branch captures in the new environment, a rebuild compares like\n with like.\n </Alert>\n );\n}\n\n/** Per-snapshot note shown above the viewer for a snapshot whose capture environment changed. */\nexport function EnvironmentChangeSnapshotNotice(props: {\n snapshot: Pick<Snapshot, \"envChange\">;\n migrated: boolean;\n}): HtmlEscapedString | Promise<HtmlEscapedString> | null {\n const change = environmentChangeOf(props.snapshot);\n if (!change) {\n return null;\n }\n const unknown = (change.unknown ?? []).map((field) => labelOf(field));\n const note = unknown.length > 0 ? <PartlyUnknownNotice count={1} fields={unknown} /> : null;\n if (change.changed.length === 0) {\n return note;\n }\n const what = change.changed.map((field) => labelOf(field)).join(\", \");\n return (\n <>\n {props.migrated ? (\n <Alert tone=\"info\" title=\"No diff: capture environment changed\">\n This snapshot was not compared pixel by pixel. What changed: {what}.\n </Alert>\n ) : (\n <Alert tone=\"info\" title=\"Capture environment changed\">\n The baseline was captured in a different environment ({what}), so a difference may come\n from the environment rather than the code.\n </Alert>\n )}\n {note}\n </>\n );\n}\n\n/** Build-level notices above the review grid: stale baselines and environment migrations. */\nexport function ReviewNotices(props: {\n project: Project;\n build: Build;\n snapshots: readonly Snapshot[];\n drifted: Record<string, unknown>;\n canReview: boolean;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, build, snapshots, drifted, canReview } = props;\n const staleCount = Object.keys(drifted).length;\n return (\n <>\n {staleCount > 0 ? (\n <StaleBaselineNotice\n project={project}\n build={build}\n staleCount={staleCount}\n canReview={canReview}\n />\n ) : null}\n <EnvironmentChangeNotice snapshots={snapshots} migrated={build.isDefault} />\n </>\n );\n}\n"],"mappings":";;;;;;;;;;;;AAiBA,SAAgB,wBAAwB,OAGkB;CACxD,MAAM,UAAU,uBAAuB,MAAM,SAAS;CACtD,MAAM,UAAU,oBAAC,4BAAD,EAA4B,GAAI,MAAQ,CAAA;CACxD,OAAO,UACL,qBAAA,UAAA,EAAA,UAAA,CACG,SACD,oBAAC,qBAAD;EAAqB,OAAO,QAAQ;EAAO,QAAQ,QAAQ;CAAS,CAAA,CACpE,EAAA,CAAA,IAEF;AAEJ;;AAGA,SAAS,oBAAoB,OAGsB;CACjD,MAAM,EAAE,OAAO,WAAW;CAC1B,OACE,qBAAC,OAAD;EAAO,MAAK;EAAO,OAAM;EAAzB,UAAA;GACG;GAAM;GAAE,UAAU,IAAI,iBAAiB;GAAiB;GAC1B,OAAO,KAAK,IAAI;GAAE;EAE5C;;AAEX;AAEA,SAAS,2BAA2B,OAGsB;CACxD,MAAM,UAAU,4BAA4B,MAAM,SAAS;CAC3D,IAAI,CAAC,SACH,OAAO;CAET,MAAM,EAAE,OAAO,QAAQ,mBAAmB;CAC1C,MAAM,OAAO,OAAO,OAAO,KAAK,IAAI,EAAE,UACpC,iBAAiB,KAAK,eAAe,KAAK,KAAK,eAAe,GAAG,KAAK;CAExE,OAAO,MAAM,WACX,qBAAC,OAAD;EAAO,MAAK;EAAO,OAAM;EAAzB,UAAA;GACG;GAAM;GAAE,UAAU,IAAI,iBAAiB;GAAiB;GAChD;GAAK;EAET;CAEP,CAAA,IAAA,qBAAC,OAAD;EAAO,MAAK;EAAO,OAAM;EAAzB,UAAA;GACG;GAAM;GAAE,UAAU,IAAI,iBAAiB;GAAiB;GACzB;GAAK;EAGhC;;AAEX;;AAGA,SAAgB,gCAAgC,OAGU;CACxD,MAAM,SAAS,oBAAoB,MAAM,QAAQ;CACjD,IAAI,CAAC,QACH,OAAO;CAET,MAAM,WAAW,OAAO,WAAW,CAAC,EAAA,CAAG,KAAK,UAAU,QAAQ,KAAK,CAAC;CACpE,MAAM,OAAO,QAAQ,SAAS,IAAI,oBAAC,qBAAD;EAAqB,OAAO;EAAG,QAAQ;CAAU,CAAA,IAAI;CACvF,IAAI,OAAO,QAAQ,WAAW,GAC5B,OAAO;CAET,MAAM,OAAO,OAAO,QAAQ,KAAK,UAAU,QAAQ,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI;CACpE,OACE,qBAAA,UAAA,EAAA,UAAA,CACG,MAAM,WACL,qBAAC,OAAD;EAAO,MAAK;EAAO,OAAM;EAAzB,UAAA;GAAgE;GACA;GAAK;EAC9D;CAEP,CAAA,IAAA,qBAAC,OAAD;EAAO,MAAK;EAAO,OAAM;EAAzB,UAAA;GAAuD;GACE;GAAK;EAEvD;CAER,CAAA,GAAA,IACD,EAAA,CAAA;AAEN;;AAGA,SAAgB,cAAc,OAMqB;CACjD,MAAM,EAAE,SAAS,OAAO,WAAW,SAAS,cAAc;CAC1D,MAAM,aAAa,OAAO,KAAK,OAAO,CAAC,CAAC;CACxC,OACE,qBAAA,UAAA,EAAA,UAAA,CACG,aAAa,IACZ,oBAAC,qBAAD;EACW;EACF;EACK;EACD;CACZ,CAAA,IACC,MACJ,oBAAC,yBAAD;EAAoC;EAAW,UAAU,MAAM;CAAY,CAAA,CAC3E,EAAA,CAAA;AAEN"}
@@ -9,7 +9,7 @@ import { a11ySummaryOf, a11yUnavailableNote } from "./a11y-violations.mjs";
9
9
  import { reviewProgress } from "./build-diff-model.mjs";
10
10
  import { headerActions, headerMain, headerMeta, headerProgress, headerSide, headerTitle, reviewHeader } from "../ui/styles/review-layout.mjs";
11
11
  import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
12
- //#region src/pages/build-diff-header.tsx
12
+ // src/pages/build-diff-header.tsx
13
13
  /** Approve-all / reject-all bulk actions (each asks for confirmation). */
14
14
  function BulkActions({ project, build, pendingCount }) {
15
15
  const base = `/api/v1/projects/${project.slug}/builds/${build.id}`;
@@ -129,7 +129,6 @@ function DiffHeader(props) {
129
129
  })]
130
130
  });
131
131
  }
132
- //#endregion
133
132
  export { DiffHeader };
134
133
 
135
134
  //# sourceMappingURL=build-diff-header.mjs.map
@@ -7,7 +7,7 @@ import { Card, SectionTitle } from "../ui/layout.mjs";
7
7
  import { HStack, VStack } from "../ui/stacks.mjs";
8
8
  import { csrfField } from "../ui/csrf-field.mjs";
9
9
  import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
10
- //#region src/pages/build-diff-insight.tsx
10
+ // src/pages/build-diff-insight.tsx
11
11
  function itemsOf(output) {
12
12
  const items = output?.items;
13
13
  return Array.isArray(items) ? items : [];
@@ -107,7 +107,6 @@ function InsightSlot(props) {
107
107
  data: props.data
108
108
  }) : null;
109
109
  }
110
- //#endregion
111
110
  export { InsightPanel, InsightSlot };
112
111
 
113
112
  //# sourceMappingURL=build-diff-insight.mjs.map
@@ -1,7 +1,7 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
- //#region src/pages/build-diff-model.ts
4
+ // src/pages/build-diff-model.ts
5
5
  const OPEN = /* @__PURE__ */ new Set(["new", "changed"]);
6
6
  const DECIDED = /* @__PURE__ */ new Set(["approved", "rejected"]);
7
7
  const browserOf = (snap) => snap.browser ?? "chromium";
@@ -87,7 +87,24 @@ function snapshotVariant(snap) {
87
87
  const browser = snap.browser ?? "chromium";
88
88
  return browser === "chromium" ? snap.viewportName : `${snap.viewportName} · ${browser}`;
89
89
  }
90
- //#endregion
91
- export { diffPercent, groupOf, groupSnapshots, isOpenGroup, neighbours, reviewProgress, snapshotImageUrl, snapshotPageUrl, snapshotVariant };
90
+ /**
91
+ * The design links of a component that have a cached render, as comparison sources.
92
+ * A link without a render is not offered (the design strip still lists it).
93
+ */
94
+ function designSources(items) {
95
+ const rendered = items.flatMap((item) => item.renderUrl === null ? [] : [{
96
+ item,
97
+ src: item.renderUrl
98
+ }]);
99
+ const several = rendered.length > 1;
100
+ return rendered.map(({ item, src }, index) => ({
101
+ value: several ? `design-${index + 1}` : "design",
102
+ label: several ? `${item.providerName} ${index + 1}` : "Design",
103
+ src,
104
+ name: item.name,
105
+ providerName: item.providerName
106
+ }));
107
+ }
108
+ export { designSources, diffPercent, groupOf, groupSnapshots, isOpenGroup, neighbours, reviewProgress, snapshotImageUrl, snapshotPageUrl, snapshotVariant };
92
109
 
93
110
  //# sourceMappingURL=build-diff-model.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"build-diff-model.mjs","names":[],"sources":["../../src/pages/build-diff-model.ts"],"sourcesContent":["import type { Build, Project, Snapshot } from \"@storyshelf/core/schema\";\n\n/** How far a build's review has progressed, counted per story/mode (not per browser). */\nexport interface ReviewProgress {\n /** Groups a human has to decide on (any open or decided member). */\n reviewable: number;\n /** Reviewable groups with no open member left. */\n done: number;\n /** Groups still waiting for a decision. */\n pending: number;\n}\n\nconst OPEN = new Set([\"new\", \"changed\"]);\nconst DECIDED = new Set([\"approved\", \"rejected\"]);\n\n/**\n * One story in one mode, with its snapshot on every engine. Approval is shared:\n * a single decision covers all members.\n */\nexport interface SnapshotGroup {\n /** Stable identity (story + mode). */\n key: string;\n storyId: string;\n /** The mode key (`viewportName`). */\n modeKey: string;\n /** Member snapshots, chromium first, then by engine name. */\n members: Snapshot[];\n /** The primary engine's snapshot (chromium when present). */\n primary: Snapshot;\n /** Snapshot the filmstrip links to: first open member, else the first member. */\n representative: Snapshot;\n}\n\nconst browserOf = (snap: Pick<Snapshot, \"browser\">): string => snap.browser ?? \"chromium\";\n\nfunction memberOrder(a: Snapshot, b: Snapshot): number {\n const left = browserOf(a);\n const right = browserOf(b);\n if (left === right) return 0;\n if (left === \"chromium\") return -1;\n if (right === \"chromium\") return 1;\n return left < right ? -1 : 1;\n}\n\n/** Group snapshots by story and mode, keeping first-appearance order. */\nexport function groupSnapshots(snapshots: Snapshot[]): SnapshotGroup[] {\n const groups = new Map<string, Snapshot[]>();\n for (const snap of snapshots) {\n const key = `${snap.storyId}\\u0000${snap.viewportName}`;\n groups.set(key, [...(groups.get(key) ?? []), snap]);\n }\n return [...groups.entries()].flatMap(([key, list]): SnapshotGroup[] => {\n const members = list.toSorted(memberOrder);\n const [primary] = members;\n if (!primary) return [];\n return [\n {\n key,\n storyId: primary.storyId,\n modeKey: primary.viewportName,\n members,\n primary,\n representative: members.find((snap) => OPEN.has(snap.status)) ?? primary,\n },\n ];\n });\n}\n\n/** Whether a group still has a snapshot waiting for a decision. */\nexport function isOpenGroup(group: SnapshotGroup): boolean {\n return group.members.some((snap) => OPEN.has(snap.status));\n}\n\n/** Count review progress per group; groups of unchanged snapshots need no decision. */\nexport function reviewProgress(snapshots: Snapshot[]): ReviewProgress {\n const groups = groupSnapshots(snapshots);\n const pending = groups.filter((group) => isOpenGroup(group)).length;\n const done = groups.filter(\n (group) => !isOpenGroup(group) && group.members.some((snap) => DECIDED.has(snap.status)),\n ).length;\n return { reviewable: pending + done, done, pending };\n}\n\n/** Position of the selected group plus its neighbours in list order. */\nexport interface Neighbours {\n /** 1-based group position, or 0 when nothing is selected. */\n position: number;\n /** Number of groups. */\n total: number;\n prev: Snapshot | null;\n next: Snapshot | null;\n}\n\n/** Previous/next group (as its representative snapshot) around the selected snapshot. */\nexport function neighbours(snapshots: Snapshot[], selectedId?: string): Neighbours {\n const groups = groupSnapshots(snapshots);\n const index = groups.findIndex((group) => group.members.some((snap) => snap.id === selectedId));\n if (index === -1) {\n return { position: 0, total: groups.length, prev: null, next: null };\n }\n return {\n position: index + 1,\n total: groups.length,\n prev: groups[index - 1]?.representative ?? null,\n next: groups[index + 1]?.representative ?? null,\n };\n}\n\n/** The group a snapshot belongs to. */\nexport function groupOf(snapshots: Snapshot[], snapshotId: string): SnapshotGroup | undefined {\n return groupSnapshots(snapshots).find((group) =>\n group.members.some((snap) => snap.id === snapshotId),\n );\n}\n\n/** Diff size as a percentage string (\"3.1%\"), or null when not computed. */\nexport function diffPercent(snapshot: Pick<Snapshot, \"diffRatio\">): string | null {\n return snapshot.diffRatio === null || snapshot.diffRatio === undefined\n ? null\n : `${(snapshot.diffRatio * 100).toFixed(1)}%`;\n}\n\n/** Review-page URL for one snapshot. */\nexport function snapshotPageUrl(project: Project, build: Build, snapshotId: string): string {\n return `/projects/${project.slug}/builds/${build.id}/diff?snapshot=${snapshotId}`;\n}\n\n/** API URL of a snapshot image (`image`), its diff overlay, or its baseline. */\nexport function snapshotImageUrl(\n project: Project,\n build: Build,\n snapshot: Snapshot,\n kind: \"image\" | \"diff\" | \"baseline\",\n): string {\n return `/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${snapshot.id}/${kind}`;\n}\n\n/** Mode and engine of a snapshot for labels: `dark`, `dark · firefox` (chromium is implicit). */\nexport function snapshotVariant(snap: Pick<Snapshot, \"viewportName\" | \"browser\">): string {\n const browser = snap.browser ?? \"chromium\";\n return browser === \"chromium\" ? snap.viewportName : `${snap.viewportName} · ${browser}`;\n}\n"],"mappings":";;;;AAYA,MAAM,uBAAO,IAAI,IAAI,CAAC,OAAO,SAAS,CAAC;AACvC,MAAM,0BAAU,IAAI,IAAI,CAAC,YAAY,UAAU,CAAC;AAoBhD,MAAM,aAAa,SAA4C,KAAK,WAAW;AAE/E,SAAS,YAAY,GAAa,GAAqB;CACrD,MAAM,OAAO,UAAU,CAAC;CACxB,MAAM,QAAQ,UAAU,CAAC;CACzB,IAAI,SAAS,OAAO,OAAO;CAC3B,IAAI,SAAS,YAAY,OAAO;CAChC,IAAI,UAAU,YAAY,OAAO;CACjC,OAAO,OAAO,QAAQ,KAAK;AAC7B;;AAGA,SAAgB,eAAe,WAAwC;CACrE,MAAM,yBAAS,IAAI,IAAwB;CAC3C,KAAK,MAAM,QAAQ,WAAW;EAC5B,MAAM,MAAM,GAAG,KAAK,QAAQ,QAAQ,KAAK;EACzC,OAAO,IAAI,KAAK,CAAC,GAAI,OAAO,IAAI,GAAG,KAAK,CAAC,GAAI,IAAI,CAAC;CACpD;CACA,OAAO,CAAC,GAAG,OAAO,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,UAA2B;EACrE,MAAM,UAAU,KAAK,SAAS,WAAW;EACzC,MAAM,CAAC,WAAW;EAClB,IAAI,CAAC,SAAS,OAAO,CAAC;EACtB,OAAO,CACL;GACE;GACA,SAAS,QAAQ;GACjB,SAAS,QAAQ;GACjB;GACA;GACA,gBAAgB,QAAQ,MAAM,SAAS,KAAK,IAAI,KAAK,MAAM,CAAC,KAAK;EACnE,CACF;CACF,CAAC;AACH;;AAGA,SAAgB,YAAY,OAA+B;CACzD,OAAO,MAAM,QAAQ,MAAM,SAAS,KAAK,IAAI,KAAK,MAAM,CAAC;AAC3D;;AAGA,SAAgB,eAAe,WAAuC;CACpE,MAAM,SAAS,eAAe,SAAS;CACvC,MAAM,UAAU,OAAO,QAAQ,UAAU,YAAY,KAAK,CAAC,CAAC,CAAC;CAC7D,MAAM,OAAO,OAAO,QACjB,UAAU,CAAC,YAAY,KAAK,KAAK,MAAM,QAAQ,MAAM,SAAS,QAAQ,IAAI,KAAK,MAAM,CAAC,CACzF,CAAC,CAAC;CACF,OAAO;EAAE,YAAY,UAAU;EAAM;EAAM;CAAQ;AACrD;;AAaA,SAAgB,WAAW,WAAuB,YAAiC;CACjF,MAAM,SAAS,eAAe,SAAS;CACvC,MAAM,QAAQ,OAAO,WAAW,UAAU,MAAM,QAAQ,MAAM,SAAS,KAAK,OAAO,UAAU,CAAC;CAC9F,IAAI,UAAU,IACZ,OAAO;EAAE,UAAU;EAAG,OAAO,OAAO;EAAQ,MAAM;EAAM,MAAM;CAAK;CAErE,OAAO;EACL,UAAU,QAAQ;EAClB,OAAO,OAAO;EACd,MAAM,OAAO,QAAQ,EAAE,EAAE,kBAAkB;EAC3C,MAAM,OAAO,QAAQ,EAAE,EAAE,kBAAkB;CAC7C;AACF;;AAGA,SAAgB,QAAQ,WAAuB,YAA+C;CAC5F,OAAO,eAAe,SAAS,CAAC,CAAC,MAAM,UACrC,MAAM,QAAQ,MAAM,SAAS,KAAK,OAAO,UAAU,CACrD;AACF;;AAGA,SAAgB,YAAY,UAAsD;CAChF,OAAO,SAAS,cAAc,QAAQ,SAAS,cAAc,KAAA,IACzD,OACA,IAAI,SAAS,YAAY,IAAA,CAAK,QAAQ,CAAC,EAAE;AAC/C;;AAGA,SAAgB,gBAAgB,SAAkB,OAAc,YAA4B;CAC1F,OAAO,aAAa,QAAQ,KAAK,UAAU,MAAM,GAAG,iBAAiB;AACvE;;AAGA,SAAgB,iBACd,SACA,OACA,UACA,MACQ;CACR,OAAO,oBAAoB,QAAQ,KAAK,UAAU,MAAM,GAAG,aAAa,SAAS,GAAG,GAAG;AACzF;;AAGA,SAAgB,gBAAgB,MAA0D;CACxF,MAAM,UAAU,KAAK,WAAW;CAChC,OAAO,YAAY,aAAa,KAAK,eAAe,GAAG,KAAK,aAAa,KAAK;AAChF"}
1
+ {"version":3,"file":"build-diff-model.mjs","names":[],"sources":["../../src/pages/build-diff-model.ts"],"sourcesContent":["import type { Build, Project, Snapshot } from \"@storyshelf/core/schema\";\nimport type { DesignItem } from \"./design-links.tsx\";\n\n/** How far a build's review has progressed, counted per story/mode (not per browser). */\nexport interface ReviewProgress {\n /** Groups a human has to decide on (any open or decided member). */\n reviewable: number;\n /** Reviewable groups with no open member left. */\n done: number;\n /** Groups still waiting for a decision. */\n pending: number;\n}\n\nconst OPEN = new Set([\"new\", \"changed\"]);\nconst DECIDED = new Set([\"approved\", \"rejected\"]);\n\n/**\n * One story in one mode, with its snapshot on every engine. Approval is shared:\n * a single decision covers all members.\n */\nexport interface SnapshotGroup {\n /** Stable identity (story + mode). */\n key: string;\n storyId: string;\n /** The mode key (`viewportName`). */\n modeKey: string;\n /** Member snapshots, chromium first, then by engine name. */\n members: Snapshot[];\n /** The primary engine's snapshot (chromium when present). */\n primary: Snapshot;\n /** Snapshot the filmstrip links to: first open member, else the first member. */\n representative: Snapshot;\n}\n\nconst browserOf = (snap: Pick<Snapshot, \"browser\">): string => snap.browser ?? \"chromium\";\n\nfunction memberOrder(a: Snapshot, b: Snapshot): number {\n const left = browserOf(a);\n const right = browserOf(b);\n if (left === right) return 0;\n if (left === \"chromium\") return -1;\n if (right === \"chromium\") return 1;\n return left < right ? -1 : 1;\n}\n\n/** Group snapshots by story and mode, keeping first-appearance order. */\nexport function groupSnapshots(snapshots: Snapshot[]): SnapshotGroup[] {\n const groups = new Map<string, Snapshot[]>();\n for (const snap of snapshots) {\n const key = `${snap.storyId}\\u0000${snap.viewportName}`;\n groups.set(key, [...(groups.get(key) ?? []), snap]);\n }\n return [...groups.entries()].flatMap(([key, list]): SnapshotGroup[] => {\n const members = list.toSorted(memberOrder);\n const [primary] = members;\n if (!primary) return [];\n return [\n {\n key,\n storyId: primary.storyId,\n modeKey: primary.viewportName,\n members,\n primary,\n representative: members.find((snap) => OPEN.has(snap.status)) ?? primary,\n },\n ];\n });\n}\n\n/** Whether a group still has a snapshot waiting for a decision. */\nexport function isOpenGroup(group: SnapshotGroup): boolean {\n return group.members.some((snap) => OPEN.has(snap.status));\n}\n\n/** Count review progress per group; groups of unchanged snapshots need no decision. */\nexport function reviewProgress(snapshots: Snapshot[]): ReviewProgress {\n const groups = groupSnapshots(snapshots);\n const pending = groups.filter((group) => isOpenGroup(group)).length;\n const done = groups.filter(\n (group) => !isOpenGroup(group) && group.members.some((snap) => DECIDED.has(snap.status)),\n ).length;\n return { reviewable: pending + done, done, pending };\n}\n\n/** Position of the selected group plus its neighbours in list order. */\nexport interface Neighbours {\n /** 1-based group position, or 0 when nothing is selected. */\n position: number;\n /** Number of groups. */\n total: number;\n prev: Snapshot | null;\n next: Snapshot | null;\n}\n\n/** Previous/next group (as its representative snapshot) around the selected snapshot. */\nexport function neighbours(snapshots: Snapshot[], selectedId?: string): Neighbours {\n const groups = groupSnapshots(snapshots);\n const index = groups.findIndex((group) => group.members.some((snap) => snap.id === selectedId));\n if (index === -1) {\n return { position: 0, total: groups.length, prev: null, next: null };\n }\n return {\n position: index + 1,\n total: groups.length,\n prev: groups[index - 1]?.representative ?? null,\n next: groups[index + 1]?.representative ?? null,\n };\n}\n\n/** The group a snapshot belongs to. */\nexport function groupOf(snapshots: Snapshot[], snapshotId: string): SnapshotGroup | undefined {\n return groupSnapshots(snapshots).find((group) =>\n group.members.some((snap) => snap.id === snapshotId),\n );\n}\n\n/** Diff size as a percentage string (\"3.1%\"), or null when not computed. */\nexport function diffPercent(snapshot: Pick<Snapshot, \"diffRatio\">): string | null {\n return snapshot.diffRatio === null || snapshot.diffRatio === undefined\n ? null\n : `${(snapshot.diffRatio * 100).toFixed(1)}%`;\n}\n\n/** Review-page URL for one snapshot. */\nexport function snapshotPageUrl(project: Project, build: Build, snapshotId: string): string {\n return `/projects/${project.slug}/builds/${build.id}/diff?snapshot=${snapshotId}`;\n}\n\n/** API URL of a snapshot image (`image`), its diff overlay, or its baseline. */\nexport function snapshotImageUrl(\n project: Project,\n build: Build,\n snapshot: Snapshot,\n kind: \"image\" | \"diff\" | \"baseline\",\n): string {\n return `/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${snapshot.id}/${kind}`;\n}\n\n/** Mode and engine of a snapshot for labels: `dark`, `dark · firefox` (chromium is implicit). */\nexport function snapshotVariant(snap: Pick<Snapshot, \"viewportName\" | \"browser\">): string {\n const browser = snap.browser ?? \"chromium\";\n return browser === \"chromium\" ? snap.viewportName : `${snap.viewportName} · ${browser}`;\n}\n\n/** A cached design render the review stage can compare the screenshot with. */\nexport interface DesignSource {\n /** Stage key: `design` for a lone render, `design-<n>` when the component links several. */\n value: string;\n /** Source switch label. */\n label: string;\n /** API URL of the cached render. */\n src: string;\n name: string | null;\n providerName: string;\n}\n\n/**\n * The design links of a component that have a cached render, as comparison sources.\n * A link without a render is not offered (the design strip still lists it).\n */\nexport function designSources(items: readonly DesignItem[]): DesignSource[] {\n const rendered = items.flatMap((item) =>\n item.renderUrl === null ? [] : [{ item, src: item.renderUrl }],\n );\n const several = rendered.length > 1;\n return rendered.map(({ item, src }, index) => ({\n value: several ? `design-${index + 1}` : \"design\",\n label: several ? `${item.providerName} ${index + 1}` : \"Design\",\n src,\n name: item.name,\n providerName: item.providerName,\n }));\n}\n"],"mappings":"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"}
@@ -9,7 +9,7 @@ import { a11yCarrierOf, a11yFindingsOf } from "./a11y-violations.mjs";
9
9
  import { diffPercent, groupSnapshots, isOpenGroup, reviewProgress, snapshotImageUrl, snapshotPageUrl } from "./build-diff-model.mjs";
10
10
  import { filmBody, filmItem, filmItemActive, filmStatus, filmSub, filmTitle, filmstrip, filmstripHead, filmstripList, statusDots } from "../ui/styles/review-layout.mjs";
11
11
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
12
- //#region src/pages/build-diff-nav.tsx
12
+ // src/pages/build-diff-nav.tsx
13
13
  /**
14
14
  * The list filters purely in CSS off `data-filter`, so it works before the
15
15
  * review script runs. The selected snapshot always stays visible.
@@ -132,7 +132,6 @@ function DiffNav(props) {
132
132
  })]
133
133
  });
134
134
  }
135
- //#endregion
136
135
  export { DiffNav };
137
136
 
138
137
  //# sourceMappingURL=build-diff-nav.mjs.map
@@ -0,0 +1,84 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { Alert } from "../ui/feedback.mjs";
5
+ import { Segmented } from "../ui/segmented.mjs";
6
+ import { CompareStage } from "../ui/compare-stage.mjs";
7
+ import { jsx, jsxs } from "hono/jsx/jsx-runtime";
8
+ // src/pages/build-diff-reference.tsx
9
+ function BaselineReferenceNotice(props) {
10
+ const reference = props.reference;
11
+ if (reference.kind === "none") return /* @__PURE__ */ jsx(Alert, {
12
+ tone: "info",
13
+ title: "No baseline at capture",
14
+ children: "This was a new comparison; no baseline existed when it was captured."
15
+ });
16
+ if (reference.kind === "unavailable") return /* @__PURE__ */ jsx(Alert, {
17
+ tone: "warning",
18
+ title: "Baseline reference unavailable",
19
+ children: "The original baseline details or image are unavailable or have changed since this diff was computed. For older records, any image shown may be the current baseline rather than the one used for the diff."
20
+ });
21
+ return /* @__PURE__ */ jsxs(Alert, {
22
+ tone: "info",
23
+ title: "Comparison baseline",
24
+ children: [
25
+ reference.defaultBranch ? "Default branch" : "Accepted branch",
26
+ ": ",
27
+ reference.branch,
28
+ ". Baseline updated ",
29
+ reference.updatedAt,
30
+ ".",
31
+ reference.imageAvailable ? "" : " The baseline image is unavailable.",
32
+ reference.build ? ` Reference build ${reference.build.id}, commit ${reference.build.gitSha.slice(0, 7)}.` : " Reference build and commit are unavailable."
33
+ ]
34
+ });
35
+ }
36
+ /** What the Design source is, shown under its controls. */
37
+ const DESIGN_NOTE = "Design reference: not a test. Scaled to the screenshot’s width.";
38
+ /**
39
+ * The comparison stage with a source switch. The baseline stage is always there; each cached design
40
+ * render adds a stage (hidden until chosen) that compares the screenshot with the design using the
41
+ * same side-by-side, swipe and onion tools. A reference only: no diff, score or verdict. Without a
42
+ * render, only the baseline stage is rendered.
43
+ */
44
+ function ComparisonSources(props) {
45
+ const { baseline, sources, currentSrc, subject } = props;
46
+ if (sources.length === 0) return baseline;
47
+ return /* @__PURE__ */ jsxs("div", {
48
+ "data-compare-sources": true,
49
+ children: [
50
+ /* @__PURE__ */ jsx(Segmented, {
51
+ label: "Compare against",
52
+ "data-compare-source-switch": true,
53
+ items: [{
54
+ label: "Baseline",
55
+ value: "baseline",
56
+ active: true
57
+ }, ...sources.map((source) => ({
58
+ label: source.label,
59
+ value: source.value
60
+ }))]
61
+ }),
62
+ /* @__PURE__ */ jsx("div", {
63
+ "data-compare-source": "baseline",
64
+ children: baseline
65
+ }),
66
+ sources.map((source) => /* @__PURE__ */ jsx("div", {
67
+ "data-compare-source": source.value,
68
+ hidden: true,
69
+ children: /* @__PURE__ */ jsx(CompareStage, {
70
+ variant: "design",
71
+ baselineLabel: source.name ? `${source.providerName} · ${source.name}` : "Design",
72
+ baselineSrc: source.src,
73
+ currentSrc,
74
+ diffSrc: null,
75
+ subject,
76
+ note: DESIGN_NOTE
77
+ })
78
+ }, source.value))
79
+ ]
80
+ });
81
+ }
82
+ export { BaselineReferenceNotice, ComparisonSources };
83
+
84
+ //# sourceMappingURL=build-diff-reference.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-diff-reference.mjs","names":[],"sources":["../../src/pages/build-diff-reference.tsx"],"sourcesContent":["import type { HtmlEscapedString } from \"hono/utils/html\";\nimport type { BaselineReference } from \"../routers/baseline-reference.ts\";\nimport { Alert, CompareStage, Segmented } from \"../ui/components.tsx\";\nimport type { DesignSource } from \"./build-diff-model.ts\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HTML or a promise */\nexport function BaselineReferenceNotice(props: {\n reference: BaselineReference;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const reference = props.reference;\n if (reference.kind === \"none\")\n return (\n <Alert tone=\"info\" title=\"No baseline at capture\">\n This was a new comparison; no baseline existed when it was captured.\n </Alert>\n );\n if (reference.kind === \"unavailable\")\n return (\n <Alert tone=\"warning\" title=\"Baseline reference unavailable\">\n The original baseline details or image are unavailable or have changed since this diff was\n computed. For older records, any image shown may be the current baseline rather than the one\n used for the diff.\n </Alert>\n );\n return (\n <Alert tone=\"info\" title=\"Comparison baseline\">\n {reference.defaultBranch ? \"Default branch\" : \"Accepted branch\"}: {reference.branch}. Baseline\n updated {reference.updatedAt}.\n {reference.imageAvailable ? \"\" : \" The baseline image is unavailable.\"}\n {reference.build\n ? ` Reference build ${reference.build.id}, commit ${reference.build.gitSha.slice(0, 7)}.`\n : \" Reference build and commit are unavailable.\"}\n </Alert>\n );\n}\n\n/** What the Design source is, shown under its controls. */\nconst DESIGN_NOTE = \"Design reference: not a test. Scaled to the screenshot’s width.\";\n\n/**\n * The comparison stage with a source switch. The baseline stage is always there; each cached design\n * render adds a stage (hidden until chosen) that compares the screenshot with the design using the\n * same side-by-side, swipe and onion tools. A reference only: no diff, score or verdict. Without a\n * render, only the baseline stage is rendered.\n */\nexport function ComparisonSources(props: {\n /** The baseline comparison stage. */\n baseline: HtmlEscapedString | Promise<HtmlEscapedString>;\n sources: DesignSource[];\n currentSrc: string;\n /** Accessible description shared by the images, e.g. \"Button / Primary\". */\n subject: string;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { baseline, sources, currentSrc, subject } = props;\n if (sources.length === 0) return baseline;\n return (\n <div data-compare-sources>\n <Segmented\n label=\"Compare against\"\n data-compare-source-switch\n items={[\n { label: \"Baseline\", value: \"baseline\", active: true },\n ...sources.map((source) => ({ label: source.label, value: source.value })),\n ]}\n />\n <div data-compare-source=\"baseline\">{baseline}</div>\n {sources.map((source) => (\n <div key={source.value} data-compare-source={source.value} hidden>\n <CompareStage\n variant=\"design\"\n baselineLabel={source.name ? `${source.providerName} · ${source.name}` : \"Design\"}\n baselineSrc={source.src}\n currentSrc={currentSrc}\n diffSrc={null}\n subject={subject}\n note={DESIGN_NOTE}\n />\n </div>\n ))}\n </div>\n );\n}\n"],"mappings":";;;;;;;;AAMA,SAAgB,wBAAwB,OAEW;CACjD,MAAM,YAAY,MAAM;CACxB,IAAI,UAAU,SAAS,QACrB,OACE,oBAAC,OAAD;EAAO,MAAK;EAAO,OAAM;EAAyB,UAAA;CAE3C,CAAA;CAEX,IAAI,UAAU,SAAS,eACrB,OACE,oBAAC,OAAD;EAAO,MAAK;EAAU,OAAM;EAAiC,UAAA;CAItD,CAAA;CAEX,OACE,qBAAC,OAAD;EAAO,MAAK;EAAO,OAAM;EAAzB,UAAA;GACG,UAAU,gBAAgB,mBAAmB;GAAkB;GAAG,UAAU;GAAO;GAC3E,UAAU;GAAU;GAC5B,UAAU,iBAAiB,KAAK;GAChC,UAAU,QACP,oBAAoB,UAAU,MAAM,GAAG,WAAW,UAAU,MAAM,OAAO,MAAM,GAAG,CAAC,EAAE,KACrF;EACC;;AAEX;;AAGA,MAAM,cAAc;;;;;;;AAQpB,SAAgB,kBAAkB,OAOiB;CACjD,MAAM,EAAE,UAAU,SAAS,YAAY,YAAY;CACnD,IAAI,QAAQ,WAAW,GAAG,OAAO;CACjC,OACE,qBAAC,OAAD;EAAK,wBAAA;EAAL,UAAA;GACE,oBAAC,WAAD;IACE,OAAM;IACN,8BAAA;IACA,OAAO,CACL;KAAE,OAAO;KAAY,OAAO;KAAY,QAAQ;IAAK,GACrD,GAAG,QAAQ,KAAK,YAAY;KAAE,OAAO,OAAO;KAAO,OAAO,OAAO;IAAM,EAAE,CAC3E;GACD,CAAA;GACD,oBAAC,OAAD;IAAK,uBAAoB;IAAY,UAAA;GAAc,CAAA;GAClD,QAAQ,KAAK,WACZ,oBAAC,OAAD;IAAwB,uBAAqB,OAAO;IAAO,QAAA;IACzD,UAAA,oBAAC,cAAD;KACE,SAAQ;KACR,eAAe,OAAO,OAAO,GAAG,OAAO,aAAa,KAAK,OAAO,SAAS;KACzE,aAAa,OAAO;KACR;KACZ,SAAS;KACA;KACT,MAAM;IACP,CAAA;GACE,GAVK,OAAO,KAUZ,CACN;EACE;;AAET"}
@@ -5,7 +5,7 @@ import { Button } from "../ui/buttons.mjs";
5
5
  import { Kbd } from "../ui/kbd.mjs";
6
6
  import { shortcutsDialog } from "../ui/styles/review-layout.mjs";
7
7
  import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
8
- //#region src/pages/build-diff-shortcuts.tsx
8
+ // src/pages/build-diff-shortcuts.tsx
9
9
  const SHORTCUTS = [
10
10
  {
11
11
  keys: ["j", "→"],
@@ -76,7 +76,6 @@ function ShortcutsDialog() {
76
76
  ]
77
77
  });
78
78
  }
79
- //#endregion
80
79
  export { ShortcutsDialog };
81
80
 
82
81
  //# sourceMappingURL=build-diff-shortcuts.mjs.map
@@ -5,7 +5,7 @@ import { Button } from "../ui/buttons.mjs";
5
5
  import { Alert, Meta } from "../ui/feedback.mjs";
6
6
  import { HStack, VStack } from "../ui/stacks.mjs";
7
7
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
8
- //#region src/pages/build-diff-stale.tsx
8
+ // src/pages/build-diff-stale.tsx
9
9
  /**
10
10
  * Warns that undecided snapshots were compared with a baseline that has since changed.
11
11
  * Builds are never recaptured automatically, so the reviewer chooses how to refresh.
@@ -64,7 +64,6 @@ function StaleSnapshotNotice(props) {
64
64
  ]
65
65
  });
66
66
  }
67
- //#endregion
68
67
  export { StaleBaselineNotice, StaleSnapshotNotice };
69
68
 
70
69
  //# sourceMappingURL=build-diff-stale.mjs.map
@@ -6,7 +6,7 @@ import { Badge } from "../ui/feedback.mjs";
6
6
  import { HStack } from "../ui/stacks.mjs";
7
7
  import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
8
8
  import { createUrlBuilder } from "@storyshelf/core/urls";
9
- //#region src/pages/build-diff-usage.tsx
9
+ // src/pages/build-diff-usage.tsx
10
10
  /** Deep link to the upstream component's published story (or its Storybook root). */
11
11
  function upstreamPreviewUrl(item, publishedBaseDomain) {
12
12
  if (!item.upstreamBuildId) return null;
@@ -57,7 +57,6 @@ function LinkedUsage(props) {
57
57
  }, item.package);
58
58
  }) });
59
59
  }
60
- //#endregion
61
60
  export { LinkedUsage, upstreamPreviewUrl };
62
61
 
63
62
  //# sourceMappingURL=build-diff-usage.mjs.map
@@ -7,15 +7,17 @@ import { VStack } from "../ui/stacks.mjs";
7
7
  import { Segmented } from "../ui/segmented.mjs";
8
8
  import { CompareStage } from "../ui/compare-stage.mjs";
9
9
  import { A11yViolationsNotice, a11yCarrierOf } from "./a11y-violations.mjs";
10
- import { diffPercent, snapshotImageUrl, snapshotPageUrl, snapshotVariant } from "./build-diff-model.mjs";
10
+ import { designSources, diffPercent, snapshotImageUrl, snapshotPageUrl, snapshotVariant } from "./build-diff-model.mjs";
11
11
  import { StaleSnapshotNotice } from "./build-diff-stale.mjs";
12
12
  import { LinkedUsage } from "./build-diff-usage.mjs";
13
13
  import { DesignReferences, DesignStrip } from "./design-links.mjs";
14
14
  import { viewerHead, viewerMeta, viewerTitle } from "../ui/styles/review-layout.mjs";
15
15
  import { EnvironmentChangeSnapshotNotice } from "./build-diff-environment.mjs";
16
+ import { DefaultComparisonView } from "./build-diff-default.mjs";
17
+ import { BaselineReferenceNotice, ComparisonSources } from "./build-diff-reference.mjs";
16
18
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
17
19
  import { createUrlBuilder } from "@storyshelf/core/urls";
18
- //#region src/pages/build-diff-viewer.tsx
20
+ // src/pages/build-diff-viewer.tsx
19
21
  /** Why a snapshot has no diff image, phrased for the empty pane. */
20
22
  function diffEmptyText(status) {
21
23
  return status === "unchanged" || status === "approved" ? "No diff — within threshold" : "No diff yet";
@@ -49,7 +51,7 @@ function ViewComponentButton(props) {
49
51
  }
50
52
  /** Story heading plus the comparison stage for the selected snapshot. */
51
53
  function DiffViewer(props) {
52
- const { project, build, selected, hasBaseline, drifted, usage, design, publishedBaseDomain } = props;
54
+ const { project, build, selected, drifted, usage, design, publishedBaseDomain } = props;
53
55
  const siblings = props.siblings ?? [selected];
54
56
  const percent = diffPercent(selected);
55
57
  return /* @__PURE__ */ jsxs(VStack, { children: [
@@ -93,6 +95,7 @@ function DiffViewer(props) {
93
95
  ]
94
96
  })]
95
97
  }),
98
+ props.reference ? /* @__PURE__ */ jsx(BaselineReferenceNotice, { reference: props.reference }) : null,
96
99
  /* @__PURE__ */ jsx(BrowserTabs, {
97
100
  project,
98
101
  build,
@@ -110,18 +113,41 @@ function DiffViewer(props) {
110
113
  publishedBaseDomain
111
114
  }) : null,
112
115
  design && design.length > 0 ? /* @__PURE__ */ jsx(DesignStrip, { items: design }) : null,
113
- /* @__PURE__ */ jsx(CompareStage, {
114
- baselineSrc: hasBaseline[selected.id] ? snapshotImageUrl(project, build, selected, "baseline") : null,
115
- currentSrc: snapshotImageUrl(project, build, selected, "image"),
116
- diffSrc: selected.diffPath ? snapshotImageUrl(project, build, selected, "diff") : null,
117
- subject: `${selected.storyTitle} / ${selected.storyName}`,
118
- diffEmpty: diffEmptyText(selected.status),
119
- diffMeta: percent ?? void 0
120
- }),
116
+ /* @__PURE__ */ jsx(PrimaryComparison, { ...props }),
117
+ /* @__PURE__ */ jsx(SecondaryComparison, { ...props }),
121
118
  design && design.length > 0 ? /* @__PURE__ */ jsx(DesignReferences, { items: design }) : null
122
119
  ] });
123
120
  }
124
- //#endregion
121
+ function SecondaryComparison(props) {
122
+ if (!props.defaultComparison) return null;
123
+ return /* @__PURE__ */ jsx(DefaultComparisonView, {
124
+ ...props,
125
+ snapshot: props.selected,
126
+ comparison: props.defaultComparison
127
+ });
128
+ }
129
+ function baselineUrl(props) {
130
+ const { hasBaseline, selected, reference, project, build } = props;
131
+ return hasBaseline[selected.id] && reference?.kind !== "none" && !(reference?.kind === "known" && !reference.imageAvailable) && !(reference?.kind === "unavailable" && selected.baselineVersion !== null) ? snapshotImageUrl(project, build, selected, "baseline") : null;
132
+ }
133
+ function PrimaryComparison(props) {
134
+ const { project, build, selected, reference } = props;
135
+ const stage = /* @__PURE__ */ jsx(CompareStage, {
136
+ baselineSrc: baselineUrl(props),
137
+ baselineEmpty: reference?.kind === "unavailable" || reference?.kind === "known" && !reference.imageAvailable ? "The original baseline image is unavailable." : void 0,
138
+ currentSrc: snapshotImageUrl(project, build, selected, "image"),
139
+ diffSrc: selected.diffPath ? snapshotImageUrl(project, build, selected, "diff") : null,
140
+ subject: `${selected.storyTitle} / ${selected.storyName}`,
141
+ diffEmpty: diffEmptyText(selected.status),
142
+ diffMeta: diffPercent(selected) ?? void 0
143
+ });
144
+ return /* @__PURE__ */ jsx(ComparisonSources, {
145
+ baseline: stage,
146
+ sources: designSources(props.design ?? []),
147
+ currentSrc: snapshotImageUrl(project, build, selected, "image"),
148
+ subject: `${selected.storyTitle} / ${selected.storyName}`
149
+ });
150
+ }
125
151
  export { DiffViewer };
126
152
 
127
153
  //# sourceMappingURL=build-diff-viewer.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"build-diff-viewer.mjs","names":[],"sources":["../../src/pages/build-diff-viewer.tsx"],"sourcesContent":["import type { Build, Project, Snapshot } from \"@storyshelf/core/schema\";\nimport { createUrlBuilder } from \"@storyshelf/core/urls\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport { Badge, Button, CompareStage, Segmented, VStack, statusTone } from \"../ui/components.tsx\";\nimport { viewerHead, viewerMeta, viewerTitle } from \"../ui/styles/review-layout.ts\";\nimport { A11yViolationsNotice, a11yCarrierOf } from \"./a11y-violations.tsx\";\nimport { EnvironmentChangeSnapshotNotice } from \"./build-diff-environment.tsx\";\nimport {\n diffPercent,\n snapshotImageUrl,\n snapshotPageUrl,\n snapshotVariant,\n} from \"./build-diff-model.ts\";\nimport { StaleSnapshotNotice } from \"./build-diff-stale.tsx\";\nimport { LinkedUsage, type LinkedUsageItem } from \"./build-diff-usage.tsx\";\nimport { DesignReferences, DesignStrip, type DesignItem } from \"./design-links.tsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\n/** Viewer props for the selected snapshot. */\nexport interface DiffViewerProps {\n project: Project;\n build: Build;\n selected: Snapshot;\n /** The selected story/mode's snapshots on every engine (browser tabs when more than one). */\n siblings?: Snapshot[];\n hasBaseline: Record<string, boolean>;\n /** Set when the baseline changed after this snapshot's diff was computed. */\n drifted?: \"stale\" | \"removed\";\n /** Linked packages the story uses (provenance badges + upstream deep links). */\n usage?: LinkedUsageItem[];\n /** Resolved design links of the story's component (open-in-Figma + stale/deleted notices). */\n design?: DesignItem[];\n publishedBaseDomain?: string;\n}\n\n/** Why a snapshot has no diff image, phrased for the empty pane. */\nfunction diffEmptyText(status: string): string {\n return status === \"unchanged\" || status === \"approved\"\n ? \"No diff — within threshold\"\n : \"No diff yet\";\n}\n\n/** Browser tabs (one link per engine) when the story/mode was captured on several. */\nfunction BrowserTabs(props: {\n project: Project;\n build: Build;\n selected: Snapshot;\n siblings: Snapshot[];\n}): HtmlEscapedString | Promise<HtmlEscapedString> | null {\n const { project, build, selected, siblings } = props;\n if (siblings.length < 2) return null;\n return (\n <Segmented\n label=\"Browser\"\n data-browser-tabs\n items={siblings.map((snap) => ({\n label: `${snap.browser ?? \"chromium\"} · ${snap.status}`,\n value: snap.id,\n href: snapshotPageUrl(project, build, snap.id),\n active: snap.id === selected.id,\n }))}\n />\n );\n}\n\n/** Link to the story file's component page, anchored at the story. */\nfunction ViewComponentButton(props: {\n project: Project;\n selected: Snapshot;\n base?: string;\n}): HtmlEscapedString | Promise<HtmlEscapedString> | null {\n const { project, selected, base } = props;\n if (!selected.storyImportPath) return null;\n const urls = createUrlBuilder(\"/\", base);\n return (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n href={`${urls.component(project.slug, selected.storyImportPath)}#${selected.storyId}`}\n >\n View component\n </Button>\n );\n}\n\n/** Story heading plus the comparison stage for the selected snapshot. */\nexport function DiffViewer(props: DiffViewerProps): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, build, selected, hasBaseline, drifted, usage, design, publishedBaseDomain } =\n props;\n const siblings = props.siblings ?? [selected];\n const percent = diffPercent(selected);\n return (\n <VStack>\n <div class={viewerHead}>\n <h2 class={viewerTitle}>\n {selected.storyTitle} / {selected.storyName}\n </h2>\n <p class={viewerMeta}>\n <Badge tone={statusTone(selected.status)} icon>\n {selected.status}\n </Badge>\n <span>\n {snapshotVariant(selected)} · {selected.viewportWidth}×{selected.viewportHeight}\n </span>\n {selected.diffPixels === null || selected.diffPixels === undefined ? null : (\n <span class=\"mono\">{selected.diffPixels} px</span>\n )}\n {percent ? <span class=\"mono\">{percent}</span> : null}\n <ViewComponentButton project={project} selected={selected} base={publishedBaseDomain} />\n </p>\n </div>\n <BrowserTabs project={project} build={build} selected={selected} siblings={siblings} />\n {drifted ? <StaleSnapshotNotice status={drifted} /> : null}\n <EnvironmentChangeSnapshotNotice snapshot={selected} migrated={build.isDefault} />\n <A11yViolationsNotice snapshot={a11yCarrierOf(selected, siblings)} />\n {usage && usage.length > 0 ? (\n <LinkedUsage usage={usage} publishedBaseDomain={publishedBaseDomain} />\n ) : null}\n {design && design.length > 0 ? <DesignStrip items={design} /> : null}\n <CompareStage\n baselineSrc={\n hasBaseline[selected.id] ? snapshotImageUrl(project, build, selected, \"baseline\") : null\n }\n currentSrc={snapshotImageUrl(project, build, selected, \"image\")}\n diffSrc={selected.diffPath ? snapshotImageUrl(project, build, selected, \"diff\") : null}\n subject={`${selected.storyTitle} / ${selected.storyName}`}\n diffEmpty={diffEmptyText(selected.status)}\n diffMeta={percent ?? undefined}\n />\n {design && design.length > 0 ? <DesignReferences items={design} /> : null}\n </VStack>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"build-diff-viewer.mjs","names":[],"sources":["../../src/pages/build-diff-viewer.tsx"],"sourcesContent":["import type { Build, Project, Snapshot } from \"@storyshelf/core/schema\";\nimport { createUrlBuilder } from \"@storyshelf/core/urls\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport type { BaselineReference, DefaultComparison } from \"../routers/baseline-reference.ts\";\nimport { Badge, Button, CompareStage, Segmented, VStack, statusTone } from \"../ui/components.tsx\";\nimport { viewerHead, viewerMeta, viewerTitle } from \"../ui/styles/review-layout.ts\";\nimport { A11yViolationsNotice, a11yCarrierOf } from \"./a11y-violations.tsx\";\nimport { DefaultComparisonView } from \"./build-diff-default.tsx\";\nimport { EnvironmentChangeSnapshotNotice } from \"./build-diff-environment.tsx\";\nimport {\n designSources,\n diffPercent,\n snapshotImageUrl,\n snapshotPageUrl,\n snapshotVariant,\n} from \"./build-diff-model.ts\";\nimport { BaselineReferenceNotice, ComparisonSources } from \"./build-diff-reference.tsx\";\nimport { StaleSnapshotNotice } from \"./build-diff-stale.tsx\";\nimport { LinkedUsage, type LinkedUsageItem } from \"./build-diff-usage.tsx\";\nimport { DesignReferences, DesignStrip, type DesignItem } from \"./design-links.tsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\n/** Viewer props for the selected snapshot. */\nexport interface DiffViewerProps {\n project: Project;\n build: Build;\n selected: Snapshot;\n /** The selected story/mode's snapshots on every engine (browser tabs when more than one). */\n siblings?: Snapshot[];\n hasBaseline: Record<string, boolean>;\n reference?: BaselineReference;\n defaultComparison?: DefaultComparison | null;\n /** Set when the baseline changed after this snapshot's diff was computed. */\n drifted?: \"stale\" | \"removed\";\n /** Linked packages the story uses (provenance badges + upstream deep links). */\n usage?: LinkedUsageItem[];\n /** Resolved design links of the story's component (open-in-Figma + stale/deleted notices). */\n design?: DesignItem[];\n publishedBaseDomain?: string;\n}\n\n/** Why a snapshot has no diff image, phrased for the empty pane. */\nfunction diffEmptyText(status: string): string {\n return status === \"unchanged\" || status === \"approved\"\n ? \"No diff — within threshold\"\n : \"No diff yet\";\n}\n\n/** Browser tabs (one link per engine) when the story/mode was captured on several. */\nfunction BrowserTabs(props: {\n project: Project;\n build: Build;\n selected: Snapshot;\n siblings: Snapshot[];\n}): HtmlEscapedString | Promise<HtmlEscapedString> | null {\n const { project, build, selected, siblings } = props;\n if (siblings.length < 2) return null;\n return (\n <Segmented\n label=\"Browser\"\n data-browser-tabs\n items={siblings.map((snap) => ({\n label: `${snap.browser ?? \"chromium\"} · ${snap.status}`,\n value: snap.id,\n href: snapshotPageUrl(project, build, snap.id),\n active: snap.id === selected.id,\n }))}\n />\n );\n}\n\n/** Link to the story file's component page, anchored at the story. */\nfunction ViewComponentButton(props: {\n project: Project;\n selected: Snapshot;\n base?: string;\n}): HtmlEscapedString | Promise<HtmlEscapedString> | null {\n const { project, selected, base } = props;\n if (!selected.storyImportPath) return null;\n const urls = createUrlBuilder(\"/\", base);\n return (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n href={`${urls.component(project.slug, selected.storyImportPath)}#${selected.storyId}`}\n >\n View component\n </Button>\n );\n}\n\n/** Story heading plus the comparison stage for the selected snapshot. */\nexport function DiffViewer(props: DiffViewerProps): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, build, selected, drifted, usage, design, publishedBaseDomain } = props;\n const siblings = props.siblings ?? [selected];\n const percent = diffPercent(selected);\n return (\n <VStack>\n <div class={viewerHead}>\n <h2 class={viewerTitle}>\n {selected.storyTitle} / {selected.storyName}\n </h2>\n <p class={viewerMeta}>\n <Badge tone={statusTone(selected.status)} icon>\n {selected.status}\n </Badge>\n <span>\n {snapshotVariant(selected)} · {selected.viewportWidth}×{selected.viewportHeight}\n </span>\n {selected.diffPixels === null || selected.diffPixels === undefined ? null : (\n <span class=\"mono\">{selected.diffPixels} px</span>\n )}\n {percent ? <span class=\"mono\">{percent}</span> : null}\n <ViewComponentButton project={project} selected={selected} base={publishedBaseDomain} />\n </p>\n </div>\n {props.reference ? <BaselineReferenceNotice reference={props.reference} /> : null}\n <BrowserTabs project={project} build={build} selected={selected} siblings={siblings} />\n {drifted ? <StaleSnapshotNotice status={drifted} /> : null}\n <EnvironmentChangeSnapshotNotice snapshot={selected} migrated={build.isDefault} />\n <A11yViolationsNotice snapshot={a11yCarrierOf(selected, siblings)} />\n {usage && usage.length > 0 ? (\n <LinkedUsage usage={usage} publishedBaseDomain={publishedBaseDomain} />\n ) : null}\n {design && design.length > 0 ? <DesignStrip items={design} /> : null}\n <PrimaryComparison {...props} />\n <SecondaryComparison {...props} />\n {design && design.length > 0 ? <DesignReferences items={design} /> : null}\n </VStack>\n );\n}\n\nfunction SecondaryComparison(\n props: DiffViewerProps,\n): HtmlEscapedString | Promise<HtmlEscapedString> | null {\n if (!props.defaultComparison) return null;\n return (\n <DefaultComparisonView\n {...props}\n snapshot={props.selected}\n comparison={props.defaultComparison}\n />\n );\n}\n\nfunction baselineUrl(props: DiffViewerProps): string | null {\n const { hasBaseline, selected, reference, project, build } = props;\n return hasBaseline[selected.id] &&\n reference?.kind !== \"none\" &&\n !(reference?.kind === \"known\" && !reference.imageAvailable) &&\n !(reference?.kind === \"unavailable\" && selected.baselineVersion !== null)\n ? snapshotImageUrl(project, build, selected, \"baseline\")\n : null;\n}\n\nfunction PrimaryComparison(props: DiffViewerProps): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, build, selected, reference } = props;\n const stage = (\n <CompareStage\n baselineSrc={baselineUrl(props)}\n baselineEmpty={\n reference?.kind === \"unavailable\" ||\n (reference?.kind === \"known\" && !reference.imageAvailable)\n ? \"The original baseline image is unavailable.\"\n : undefined\n }\n currentSrc={snapshotImageUrl(project, build, selected, \"image\")}\n diffSrc={selected.diffPath ? snapshotImageUrl(project, build, selected, \"diff\") : null}\n subject={`${selected.storyTitle} / ${selected.storyName}`}\n diffEmpty={diffEmptyText(selected.status)}\n diffMeta={diffPercent(selected) ?? undefined}\n />\n );\n return (\n <ComparisonSources\n baseline={stage}\n sources={designSources(props.design ?? [])}\n currentSrc={snapshotImageUrl(project, build, selected, \"image\")}\n subject={`${selected.storyTitle} / ${selected.storyName}`}\n />\n );\n}\n"],"mappings":"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"}