@storyshelf/app 0.6.2 → 0.7.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 (361) hide show
  1. package/README.md +42 -4
  2. package/dist/app-types.d.mts +1 -3
  3. package/dist/asset-manifest.mjs +13 -5
  4. package/dist/asset-manifest.mjs.map +1 -1
  5. package/dist/assets/{htmx.min.js → htmx.mjs} +10 -1
  6. package/dist/assets/htmx.mjs.map +1 -0
  7. package/dist/assets/mark.mjs +10 -0
  8. package/dist/assets/mark.mjs.map +1 -0
  9. package/dist/boot-tasks.mjs +80 -0
  10. package/dist/boot-tasks.mjs.map +1 -0
  11. package/dist/build-state-flags.mjs +18 -0
  12. package/dist/build-state-flags.mjs.map +1 -0
  13. package/dist/build-state-setup.mjs +75 -0
  14. package/dist/build-state-setup.mjs.map +1 -0
  15. package/dist/capture-alerts.mjs +46 -0
  16. package/dist/capture-alerts.mjs.map +1 -0
  17. package/dist/capture-process.mjs +59 -0
  18. package/dist/capture-process.mjs.map +1 -0
  19. package/dist/capture-setup.mjs +95 -117
  20. package/dist/capture-setup.mjs.map +1 -1
  21. package/dist/connections.mjs +34 -0
  22. package/dist/connections.mjs.map +1 -0
  23. package/dist/consumer-setup.mjs +58 -0
  24. package/dist/consumer-setup.mjs.map +1 -0
  25. package/dist/git-status.mjs +46 -0
  26. package/dist/git-status.mjs.map +1 -0
  27. package/dist/hidden-data.mjs +24 -0
  28. package/dist/hidden-data.mjs.map +1 -0
  29. package/dist/index.d.mts +3 -3
  30. package/dist/index.mjs +203 -125
  31. package/dist/index.mjs.map +1 -1
  32. package/dist/insights/budget.mjs +49 -0
  33. package/dist/insights/budget.mjs.map +1 -0
  34. package/dist/insights/deps.mjs +21 -0
  35. package/dist/insights/deps.mjs.map +1 -0
  36. package/dist/insights/errors.mjs +22 -0
  37. package/dist/insights/errors.mjs.map +1 -0
  38. package/dist/insights/health-panel.mjs +40 -0
  39. package/dist/insights/health-panel.mjs.map +1 -0
  40. package/dist/insights/job.mjs +103 -0
  41. package/dist/insights/job.mjs.map +1 -0
  42. package/dist/insights/load-health.mjs +59 -0
  43. package/dist/insights/load-health.mjs.map +1 -0
  44. package/dist/insights/load-triage.mjs +98 -0
  45. package/dist/insights/load-triage.mjs.map +1 -0
  46. package/dist/insights/panel.mjs +28 -0
  47. package/dist/insights/panel.mjs.map +1 -0
  48. package/dist/insights/request.mjs +113 -0
  49. package/dist/insights/request.mjs.map +1 -0
  50. package/dist/insights/stale.mjs +21 -0
  51. package/dist/insights/stale.mjs.map +1 -0
  52. package/dist/insights/usage-report.mjs +60 -0
  53. package/dist/insights/usage-report.mjs.map +1 -0
  54. package/dist/insights/view.mjs +48 -0
  55. package/dist/insights/view.mjs.map +1 -0
  56. package/dist/job-canceller.mjs +14 -0
  57. package/dist/job-canceller.mjs.map +1 -0
  58. package/dist/job-queue.mjs +51 -0
  59. package/dist/job-queue.mjs.map +1 -0
  60. package/dist/jobs-setup.mjs +132 -0
  61. package/dist/jobs-setup.mjs.map +1 -0
  62. package/dist/lifecycle.mjs +91 -19
  63. package/dist/lifecycle.mjs.map +1 -1
  64. package/dist/middleware/auth-gate.mjs +4 -2
  65. package/dist/middleware/auth-gate.mjs.map +1 -1
  66. package/dist/middleware/rate-limit.mjs +41 -33
  67. package/dist/middleware/rate-limit.mjs.map +1 -1
  68. package/dist/middleware/request-log.mjs +3 -2
  69. package/dist/middleware/request-log.mjs.map +1 -1
  70. package/dist/middleware/require-review.mjs +51 -0
  71. package/dist/middleware/require-review.mjs.map +1 -0
  72. package/dist/middleware/require-testing.mjs +41 -0
  73. package/dist/middleware/require-testing.mjs.map +1 -0
  74. package/dist/middleware/setup-gate.mjs.map +1 -1
  75. package/dist/middleware/store-scope.mjs +12 -2
  76. package/dist/middleware/store-scope.mjs.map +1 -1
  77. package/dist/notifications-pause.mjs +33 -0
  78. package/dist/notifications-pause.mjs.map +1 -0
  79. package/dist/notify.d.mts +2 -1
  80. package/dist/notify.mjs +78 -39
  81. package/dist/notify.mjs.map +1 -1
  82. package/dist/pages/a11y-violations.mjs +116 -0
  83. package/dist/pages/a11y-violations.mjs.map +1 -0
  84. package/dist/pages/admin-ai.mjs +78 -0
  85. package/dist/pages/admin-ai.mjs.map +1 -0
  86. package/dist/pages/admin-system-origin.mjs +123 -0
  87. package/dist/pages/admin-system-origin.mjs.map +1 -0
  88. package/dist/pages/admin-system-targets.mjs +53 -0
  89. package/dist/pages/admin-system-targets.mjs.map +1 -0
  90. package/dist/pages/admin-system.mjs +63 -13
  91. package/dist/pages/admin-system.mjs.map +1 -1
  92. package/dist/pages/build-detail.mjs +104 -76
  93. package/dist/pages/build-detail.mjs.map +1 -1
  94. package/dist/pages/build-diff-actions.mjs +87 -33
  95. package/dist/pages/build-diff-actions.mjs.map +1 -1
  96. package/dist/pages/build-diff-comments.mjs +16 -14
  97. package/dist/pages/build-diff-comments.mjs.map +1 -1
  98. package/dist/pages/build-diff-environment.mjs +82 -0
  99. package/dist/pages/build-diff-environment.mjs.map +1 -0
  100. package/dist/pages/build-diff-header.mjs +22 -4
  101. package/dist/pages/build-diff-header.mjs.map +1 -1
  102. package/dist/pages/build-diff-insight.mjs +113 -0
  103. package/dist/pages/build-diff-insight.mjs.map +1 -0
  104. package/dist/pages/build-diff-model.mjs +55 -10
  105. package/dist/pages/build-diff-model.mjs.map +1 -1
  106. package/dist/pages/build-diff-nav.mjs +28 -12
  107. package/dist/pages/build-diff-nav.mjs.map +1 -1
  108. package/dist/pages/build-diff-shortcuts.mjs +5 -1
  109. package/dist/pages/build-diff-shortcuts.mjs.map +1 -1
  110. package/dist/pages/build-diff-usage.mjs +63 -0
  111. package/dist/pages/build-diff-usage.mjs.map +1 -0
  112. package/dist/pages/build-diff-viewer.mjs +62 -5
  113. package/dist/pages/build-diff-viewer.mjs.map +1 -1
  114. package/dist/pages/build-diff.mjs +34 -13
  115. package/dist/pages/build-diff.mjs.map +1 -1
  116. package/dist/pages/build-tests.mjs +74 -0
  117. package/dist/pages/build-tests.mjs.map +1 -0
  118. package/dist/pages/component-detail.mjs +249 -0
  119. package/dist/pages/component-detail.mjs.map +1 -0
  120. package/dist/pages/component-stories.mjs +168 -0
  121. package/dist/pages/component-stories.mjs.map +1 -0
  122. package/dist/pages/compute-jobs.mjs +13 -26
  123. package/dist/pages/compute-jobs.mjs.map +1 -1
  124. package/dist/pages/design-links.mjs +182 -0
  125. package/dist/pages/design-links.mjs.map +1 -0
  126. package/dist/pages/environment-change.mjs +53 -0
  127. package/dist/pages/environment-change.mjs.map +1 -0
  128. package/dist/pages/health-panel.mjs +126 -0
  129. package/dist/pages/health-panel.mjs.map +1 -0
  130. package/dist/pages/library-grid.mjs +105 -0
  131. package/dist/pages/library-grid.mjs.map +1 -0
  132. package/dist/pages/library-model.mjs +47 -0
  133. package/dist/pages/library-model.mjs.map +1 -0
  134. package/dist/pages/library-snapshots.mjs +130 -0
  135. package/dist/pages/library-snapshots.mjs.map +1 -0
  136. package/dist/pages/library.mjs +112 -218
  137. package/dist/pages/library.mjs.map +1 -1
  138. package/dist/pages/notifications-paused.mjs +22 -0
  139. package/dist/pages/notifications-paused.mjs.map +1 -0
  140. package/dist/pages/profile.mjs +53 -48
  141. package/dist/pages/profile.mjs.map +1 -1
  142. package/dist/pages/project-builds-table.mjs +2 -2
  143. package/dist/pages/project-builds-table.mjs.map +1 -1
  144. package/dist/pages/project-builds.mjs +51 -21
  145. package/dist/pages/project-builds.mjs.map +1 -1
  146. package/dist/pages/project-settings.mjs +51 -12
  147. package/dist/pages/project-settings.mjs.map +1 -1
  148. package/dist/pages/projects-card.mjs +14 -1
  149. package/dist/pages/projects-card.mjs.map +1 -1
  150. package/dist/pages/projects.mjs +6 -2
  151. package/dist/pages/projects.mjs.map +1 -1
  152. package/dist/pages/review-changeset.mjs +116 -0
  153. package/dist/pages/review-changeset.mjs.map +1 -0
  154. package/dist/pages/review-comments.mjs +103 -0
  155. package/dist/pages/review-comments.mjs.map +1 -0
  156. package/dist/pages/review-verdict.mjs +102 -0
  157. package/dist/pages/review-verdict.mjs.map +1 -0
  158. package/dist/pages/settings-ai.mjs +69 -0
  159. package/dist/pages/settings-ai.mjs.map +1 -0
  160. package/dist/pages/settings-capture.mjs +124 -0
  161. package/dist/pages/settings-capture.mjs.map +1 -0
  162. package/dist/pages/settings-design-credential.mjs +99 -0
  163. package/dist/pages/settings-design-credential.mjs.map +1 -0
  164. package/dist/pages/settings-design-links.mjs +179 -0
  165. package/dist/pages/settings-design-links.mjs.map +1 -0
  166. package/dist/pages/settings-design.mjs +48 -0
  167. package/dist/pages/settings-design.mjs.map +1 -0
  168. package/dist/pages/settings-general.mjs +46 -21
  169. package/dist/pages/settings-general.mjs.map +1 -1
  170. package/dist/pages/settings-labels.mjs +43 -2
  171. package/dist/pages/settings-labels.mjs.map +1 -1
  172. package/dist/pages/settings-links.mjs +112 -0
  173. package/dist/pages/settings-links.mjs.map +1 -0
  174. package/dist/pages/settings-notifications-git.mjs +128 -0
  175. package/dist/pages/settings-notifications-git.mjs.map +1 -0
  176. package/dist/pages/settings-notifications.mjs +79 -42
  177. package/dist/pages/settings-notifications.mjs.map +1 -1
  178. package/dist/pages/settings-tests.mjs +33 -2
  179. package/dist/pages/settings-tests.mjs.map +1 -1
  180. package/dist/pages/target-choices.mjs +39 -0
  181. package/dist/pages/target-choices.mjs.map +1 -0
  182. package/dist/publish-setup.mjs +89 -0
  183. package/dist/publish-setup.mjs.map +1 -0
  184. package/dist/retention-timer.mjs +2 -1
  185. package/dist/retention-timer.mjs.map +1 -1
  186. package/dist/review-events.mjs +48 -0
  187. package/dist/review-events.mjs.map +1 -0
  188. package/dist/routers/admin-pages.mjs +45 -3
  189. package/dist/routers/admin-pages.mjs.map +1 -1
  190. package/dist/routers/admin-public-origin.mjs +85 -0
  191. package/dist/routers/admin-public-origin.mjs.map +1 -0
  192. package/dist/routers/assets.mjs +9 -2
  193. package/dist/routers/assets.mjs.map +1 -1
  194. package/dist/routers/auth.mjs +1 -1
  195. package/dist/routers/baseline-guard.mjs +2 -2
  196. package/dist/routers/baseline-guard.mjs.map +1 -1
  197. package/dist/routers/baselines.handlers.mjs +64 -0
  198. package/dist/routers/baselines.handlers.mjs.map +1 -0
  199. package/dist/routers/baselines.mjs +125 -0
  200. package/dist/routers/baselines.mjs.map +1 -0
  201. package/dist/routers/build-cancel.mjs +68 -0
  202. package/dist/routers/build-cancel.mjs.map +1 -0
  203. package/dist/routers/build-changeset.mjs +70 -0
  204. package/dist/routers/build-changeset.mjs.map +1 -0
  205. package/dist/routers/build-reviews.handlers.mjs +86 -0
  206. package/dist/routers/build-reviews.handlers.mjs.map +1 -0
  207. package/dist/routers/build-reviews.mjs +74 -0
  208. package/dist/routers/build-reviews.mjs.map +1 -0
  209. package/dist/routers/builds.handlers.mjs +46 -55
  210. package/dist/routers/builds.handlers.mjs.map +1 -1
  211. package/dist/routers/builds.mjs +71 -17
  212. package/dist/routers/builds.mjs.map +1 -1
  213. package/dist/routers/capture-config.mjs +35 -0
  214. package/dist/routers/capture-config.mjs.map +1 -0
  215. package/dist/routers/comments.mjs +41 -15
  216. package/dist/routers/comments.mjs.map +1 -1
  217. package/dist/routers/component-catalog.handlers.mjs +71 -0
  218. package/dist/routers/component-catalog.handlers.mjs.map +1 -0
  219. package/dist/routers/component.handlers.mjs +121 -0
  220. package/dist/routers/component.handlers.mjs.map +1 -0
  221. package/dist/routers/connections.mjs +118 -0
  222. package/dist/routers/connections.mjs.map +1 -0
  223. package/dist/routers/design-connect.handlers.mjs +44 -0
  224. package/dist/routers/design-connect.handlers.mjs.map +1 -0
  225. package/dist/routers/design.handlers.mjs +114 -0
  226. package/dist/routers/design.handlers.mjs.map +1 -0
  227. package/dist/routers/health-report.mjs +2 -3
  228. package/dist/routers/health-report.mjs.map +1 -1
  229. package/dist/routers/health.mjs +4 -3
  230. package/dist/routers/health.mjs.map +1 -1
  231. package/dist/routers/helpers.mjs +19 -3
  232. package/dist/routers/helpers.mjs.map +1 -1
  233. package/dist/routers/insights-ui.mjs +64 -0
  234. package/dist/routers/insights-ui.mjs.map +1 -0
  235. package/dist/routers/insights.handlers.mjs +45 -0
  236. package/dist/routers/insights.handlers.mjs.map +1 -0
  237. package/dist/routers/insights.mjs +189 -0
  238. package/dist/routers/insights.mjs.map +1 -0
  239. package/dist/routers/links.handlers.mjs +80 -0
  240. package/dist/routers/links.handlers.mjs.map +1 -0
  241. package/dist/routers/links.mjs +139 -0
  242. package/dist/routers/links.mjs.map +1 -0
  243. package/dist/routers/media.mjs +18 -4
  244. package/dist/routers/media.mjs.map +1 -1
  245. package/dist/routers/members.mjs +2 -1
  246. package/dist/routers/members.mjs.map +1 -1
  247. package/dist/routers/notification-channels.mjs +17 -23
  248. package/dist/routers/notification-channels.mjs.map +1 -1
  249. package/dist/routers/oauth-state.mjs +45 -0
  250. package/dist/routers/oauth-state.mjs.map +1 -0
  251. package/dist/routers/preview-base.mjs +38 -0
  252. package/dist/routers/preview-base.mjs.map +1 -0
  253. package/dist/routers/preview-token.mjs +51 -0
  254. package/dist/routers/preview-token.mjs.map +1 -0
  255. package/dist/routers/profile.mjs +3 -1
  256. package/dist/routers/profile.mjs.map +1 -1
  257. package/dist/routers/projects.mjs +17 -1
  258. package/dist/routers/projects.mjs.map +1 -1
  259. package/dist/routers/publish-upload.mjs +78 -0
  260. package/dist/routers/publish-upload.mjs.map +1 -0
  261. package/dist/routers/published-access.mjs +52 -0
  262. package/dist/routers/published-access.mjs.map +1 -0
  263. package/dist/routers/published-files.mjs +52 -0
  264. package/dist/routers/published-files.mjs.map +1 -0
  265. package/dist/routers/review-forms.mjs +72 -0
  266. package/dist/routers/review-forms.mjs.map +1 -0
  267. package/dist/routers/review-section.handlers.mjs +85 -0
  268. package/dist/routers/review-section.handlers.mjs.map +1 -0
  269. package/dist/routers/review-switch-off.mjs +29 -0
  270. package/dist/routers/review-switch-off.mjs.map +1 -0
  271. package/dist/routers/schemas.mjs +140 -22
  272. package/dist/routers/schemas.mjs.map +1 -1
  273. package/dist/routers/scoped-models.mjs +22 -0
  274. package/dist/routers/scoped-models.mjs.map +1 -0
  275. package/dist/routers/settings-ai.mjs +35 -0
  276. package/dist/routers/settings-ai.mjs.map +1 -0
  277. package/dist/routers/settings-capture.mjs +77 -0
  278. package/dist/routers/settings-capture.mjs.map +1 -0
  279. package/dist/routers/settings-design.mjs +103 -0
  280. package/dist/routers/settings-design.mjs.map +1 -0
  281. package/dist/routers/settings-general.mjs +55 -10
  282. package/dist/routers/settings-general.mjs.map +1 -1
  283. package/dist/routers/settings-git-channel.mjs +77 -0
  284. package/dist/routers/settings-git-channel.mjs.map +1 -0
  285. package/dist/routers/settings-labels.mjs +27 -1
  286. package/dist/routers/settings-labels.mjs.map +1 -1
  287. package/dist/routers/settings-links.mjs +63 -0
  288. package/dist/routers/settings-links.mjs.map +1 -0
  289. package/dist/routers/settings-members.mjs +4 -7
  290. package/dist/routers/settings-members.mjs.map +1 -1
  291. package/dist/routers/settings-notifications.mjs +15 -10
  292. package/dist/routers/settings-notifications.mjs.map +1 -1
  293. package/dist/routers/settings-tokens.mjs +2 -5
  294. package/dist/routers/settings-tokens.mjs.map +1 -1
  295. package/dist/routers/settings-webhooks.mjs +7 -24
  296. package/dist/routers/settings-webhooks.mjs.map +1 -1
  297. package/dist/routers/settings.handlers.mjs +107 -26
  298. package/dist/routers/settings.handlers.mjs.map +1 -1
  299. package/dist/routers/settings.mjs +16 -4
  300. package/dist/routers/settings.mjs.map +1 -1
  301. package/dist/routers/signed-storybook.mjs +53 -0
  302. package/dist/routers/signed-storybook.mjs.map +1 -0
  303. package/dist/routers/snapshot-groups.mjs +63 -0
  304. package/dist/routers/snapshot-groups.mjs.map +1 -0
  305. package/dist/routers/snapshots.mjs +103 -5
  306. package/dist/routers/snapshots.mjs.map +1 -1
  307. package/dist/routers/storybook.mjs +37 -88
  308. package/dist/routers/storybook.mjs.map +1 -1
  309. package/dist/routers/tokens.mjs +2 -5
  310. package/dist/routers/tokens.mjs.map +1 -1
  311. package/dist/routers/ui.mjs +34 -5
  312. package/dist/routers/ui.mjs.map +1 -1
  313. package/dist/routers/webhooks.mjs +5 -11
  314. package/dist/routers/webhooks.mjs.map +1 -1
  315. package/dist/routers/well-known.mjs +11 -4
  316. package/dist/routers/well-known.mjs.map +1 -1
  317. package/dist/routes/api-routes.mjs +93 -0
  318. package/dist/routes/api-routes.mjs.map +1 -0
  319. package/dist/routes/classification.mjs +38 -0
  320. package/dist/routes/classification.mjs.map +1 -0
  321. package/dist/routes/openapi-classes.mjs +22 -0
  322. package/dist/routes/openapi-classes.mjs.map +1 -0
  323. package/dist/routes/page-routes.mjs +104 -0
  324. package/dist/routes/page-routes.mjs.map +1 -0
  325. package/dist/runtime.mjs +18 -13
  326. package/dist/runtime.mjs.map +1 -1
  327. package/dist/serverless.mjs +5 -0
  328. package/dist/store.mjs +19 -1
  329. package/dist/store.mjs.map +1 -1
  330. package/dist/targets.mjs +46 -0
  331. package/dist/targets.mjs.map +1 -0
  332. package/dist/ui/buttons.mjs +16 -3
  333. package/dist/ui/buttons.mjs.map +1 -1
  334. package/dist/ui/design-reference.mjs +97 -0
  335. package/dist/ui/design-reference.mjs.map +1 -0
  336. package/dist/ui/document.mjs +1 -2
  337. package/dist/ui/document.mjs.map +1 -1
  338. package/dist/ui/forms.mjs +6 -5
  339. package/dist/ui/forms.mjs.map +1 -1
  340. package/dist/ui/icons/paths.mjs +1 -0
  341. package/dist/ui/icons/paths.mjs.map +1 -1
  342. package/dist/ui/preview-embed.mjs +82 -0
  343. package/dist/ui/preview-embed.mjs.map +1 -0
  344. package/dist/ui/shell/client-script.mjs +85 -0
  345. package/dist/ui/shell/client-script.mjs.map +1 -1
  346. package/dist/ui/shell/nav.mjs +12 -8
  347. package/dist/ui/shell/nav.mjs.map +1 -1
  348. package/dist/ui/shell/review-script.mjs +8 -0
  349. package/dist/ui/shell/review-script.mjs.map +1 -1
  350. package/dist/ui/shell/sidebar.mjs +2 -2
  351. package/dist/ui/shell/sidebar.mjs.map +1 -1
  352. package/package.json +11 -9
  353. package/dist/asset-paths.mjs +0 -23
  354. package/dist/asset-paths.mjs.map +0 -1
  355. package/dist/assets/mark.svg +0 -1
  356. package/dist/pages/settings-status.mjs +0 -137
  357. package/dist/pages/settings-status.mjs.map +0 -1
  358. package/dist/routers/settings-status.mjs +0 -77
  359. package/dist/routers/settings-status.mjs.map +0 -1
  360. package/dist/routers/status-configs.mjs +0 -108
  361. package/dist/routers/status-configs.mjs.map +0 -1
@@ -4,24 +4,51 @@ import.meta.url;
4
4
  import { Button } from "../ui/buttons.mjs";
5
5
  import { Badge, statusTone } from "../ui/feedback.mjs";
6
6
  import { Kbd } from "../ui/kbd.mjs";
7
+ import { groupOf, neighbours, snapshotPageUrl } from "./build-diff-model.mjs";
7
8
  import { actionBar, actionGroup, actionHints, actionPosition } from "../ui/styles/review-layout.mjs";
8
- import { neighbours, snapshotPageUrl } from "./build-diff-model.mjs";
9
9
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
10
10
  //#region src/pages/build-diff-actions.tsx
11
- function snapshotBase(project, build, snapshot) {
12
- return `/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${snapshot.id}`;
11
+ function apiBase(project, build) {
12
+ return `/api/v1/projects/${project.slug}/builds/${build.id}`;
13
13
  }
14
- /** Approve (or "Approve anyway" when the baseline moved) as its own form. */
14
+ const isOpenStatus = (snap) => snap.status === "new" || snap.status === "changed";
15
+ function sharedTarget(base, group, open, name) {
16
+ return {
17
+ base: `${base}/stories/${encodeURIComponent(group.storyId)}/modes/${encodeURIComponent(group.modeKey)}`,
18
+ shared: true,
19
+ label: `${name} (${open.length} browsers)`
20
+ };
21
+ }
22
+ function decisionTarget(props) {
23
+ const { project, build, snapshots, selected, drifted } = props;
24
+ const group = groupOf(snapshots, selected.id);
25
+ const open = (group?.members ?? [selected]).filter((snap) => isOpenStatus(snap));
26
+ const [first] = open;
27
+ if (!first) return null;
28
+ const base = apiBase(project, build);
29
+ const drift = { drifted: open.some((snap) => drifted[snap.id] !== void 0) };
30
+ if (open.length > 1 && group) return {
31
+ ...sharedTarget(base, group, open, selected.storyName),
32
+ ...drift
33
+ };
34
+ return {
35
+ base: `${base}/snapshots/${first.id}`,
36
+ shared: false,
37
+ label: first.storyName,
38
+ ...drift
39
+ };
40
+ }
41
+ /** Approve (or "Approve anyway" when a baseline moved) as its own form. */
15
42
  function ApproveForm(props) {
16
- const { project, build, selected, drifted } = props;
17
- const url = `${snapshotBase(project, build, selected)}/approve${drifted ? "?force=true" : ""}`;
43
+ const { target } = props;
44
+ const url = `${target.base}/approve${target.drifted ? "?force=true" : ""}`;
18
45
  return /* @__PURE__ */ jsx("form", {
19
46
  method: "post",
20
47
  action: url,
21
48
  "hx-post": url,
22
49
  "hx-target": "body",
23
- "data-toast": `Approved ${selected.storyName}`,
24
- children: drifted ? /* @__PURE__ */ jsx(Button, {
50
+ "data-toast": `Approved ${target.label}`,
51
+ children: target.drifted ? /* @__PURE__ */ jsx(Button, {
25
52
  variant: "secondary",
26
53
  size: "sm",
27
54
  type: "submit",
@@ -34,34 +61,61 @@ function ApproveForm(props) {
34
61
  type: "submit",
35
62
  icon: "check",
36
63
  "data-approve": true,
37
- title: "Approve (a)",
38
- children: "Approve"
64
+ title: target.shared ? "Approve every browser (a)" : "Approve (a)",
65
+ children: target.shared ? "Approve all browsers" : "Approve"
39
66
  })
40
67
  });
41
68
  }
42
- /** Approve/reject for a reviewable snapshot; both advance to the next open one. */
43
- function DecisionButtons(props) {
69
+ /** Per-snapshot escape hatch: approve only the browser being viewed. */
70
+ function ApproveOnlyThisForm(props) {
44
71
  const { project, build, selected } = props;
45
- const base = snapshotBase(project, build, selected);
72
+ const url = `${apiBase(project, build)}/snapshots/${selected.id}/approve`;
73
+ return /* @__PURE__ */ jsx("form", {
74
+ method: "post",
75
+ action: url,
76
+ "hx-post": url,
77
+ "hx-target": "body",
78
+ "data-toast": `Approved ${selected.storyName} on ${selected.browser ?? "chromium"}`,
79
+ children: /* @__PURE__ */ jsxs(Button, {
80
+ variant: "ghost",
81
+ size: "sm",
82
+ type: "submit",
83
+ title: "Approve only this browser",
84
+ children: ["Only ", selected.browser ?? "chromium"]
85
+ })
86
+ });
87
+ }
88
+ /** Approve/reject for a reviewable story/mode; both advance to the next open one. */
89
+ function DecisionButtons(props) {
90
+ const { project, build, selected, target } = props;
91
+ const selectedOpen = selected.status === "new" || selected.status === "changed";
46
92
  return /* @__PURE__ */ jsxs("div", {
47
93
  class: actionGroup,
48
- children: [/* @__PURE__ */ jsx(ApproveForm, { ...props }), /* @__PURE__ */ jsx("form", {
49
- method: "post",
50
- action: `${base}/reject`,
51
- "hx-post": `${base}/reject`,
52
- "hx-target": "body",
53
- "data-toast": `Rejected ${selected.storyName}`,
54
- "data-toast-tone": "warning",
55
- children: /* @__PURE__ */ jsx(Button, {
56
- variant: "danger",
57
- size: "sm",
58
- type: "submit",
59
- icon: "x",
60
- "data-reject": true,
61
- title: "Reject (r)",
62
- children: "Reject"
94
+ children: [
95
+ /* @__PURE__ */ jsx(ApproveForm, { target }),
96
+ target.shared && selectedOpen ? /* @__PURE__ */ jsx(ApproveOnlyThisForm, {
97
+ project,
98
+ build,
99
+ selected
100
+ }) : null,
101
+ /* @__PURE__ */ jsx("form", {
102
+ method: "post",
103
+ action: `${target.base}/reject`,
104
+ "hx-post": `${target.base}/reject`,
105
+ "hx-target": "body",
106
+ "data-toast": `Rejected ${target.label}`,
107
+ "data-toast-tone": "warning",
108
+ children: /* @__PURE__ */ jsx(Button, {
109
+ variant: "danger",
110
+ size: "sm",
111
+ type: "submit",
112
+ icon: "x",
113
+ "data-reject": true,
114
+ title: "Reject (r)",
115
+ children: "Reject"
116
+ })
63
117
  })
64
- })]
118
+ ]
65
119
  });
66
120
  }
67
121
  function StepButton(props) {
@@ -91,9 +145,9 @@ function StepButton(props) {
91
145
  }
92
146
  /** Previous/next stepping, position, shortcut hints, and the review decision. */
93
147
  function DiffActionBar(props) {
94
- const { project, build, snapshots, selected, canReview, drifted } = props;
148
+ const { project, build, snapshots, selected, canReview } = props;
95
149
  const around = neighbours(snapshots, selected.id);
96
- const reviewable = selected.status === "new" || selected.status === "changed";
150
+ const target = decisionTarget(props);
97
151
  return /* @__PURE__ */ jsxs("div", {
98
152
  class: actionBar,
99
153
  role: "toolbar",
@@ -143,11 +197,11 @@ function DiffActionBar(props) {
143
197
  })
144
198
  ]
145
199
  }),
146
- canReview && reviewable ? /* @__PURE__ */ jsx(DecisionButtons, {
200
+ canReview && target ? /* @__PURE__ */ jsx(DecisionButtons, {
147
201
  project,
148
202
  build,
149
203
  selected,
150
- drifted
204
+ target
151
205
  }) : /* @__PURE__ */ jsx(Badge, {
152
206
  tone: statusTone(selected.status),
153
207
  icon: true,
@@ -1 +1 @@
1
- {"version":3,"file":"build-diff-actions.mjs","names":[],"sources":["../../src/pages/build-diff-actions.tsx"],"sourcesContent":["import type { Build, Project, Snapshot } from \"@storyshelf/core/schema\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport { Badge, Button, Kbd, statusTone } from \"../ui/components.tsx\";\nimport { actionBar, actionGroup, actionHints, actionPosition } from \"../ui/styles/review-layout.ts\";\nimport { neighbours, snapshotPageUrl } from \"./build-diff-model.ts\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\n/** Sticky bottom bar props. */\nexport interface DiffActionBarProps {\n project: Project;\n build: Build;\n snapshots: Snapshot[];\n selected: Snapshot;\n canReview: boolean;\n drifted: boolean;\n}\n\nfunction snapshotBase(project: Project, build: Build, snapshot: Snapshot): string {\n return `/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${snapshot.id}`;\n}\n\ntype DecisionProps = { project: Project; build: Build; selected: Snapshot; drifted: boolean };\n\n/** Approve (or \"Approve anyway\" when the baseline moved) as its own form. */\nfunction ApproveForm(props: DecisionProps): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, build, selected, drifted } = props;\n const url = `${snapshotBase(project, build, selected)}/approve${drifted ? \"?force=true\" : \"\"}`;\n return (\n <form\n method=\"post\"\n action={url}\n hx-post={url}\n hx-target=\"body\"\n data-toast={`Approved ${selected.storyName}`}\n >\n {drifted ? (\n <Button\n variant=\"secondary\"\n size=\"sm\"\n type=\"submit\"\n icon=\"check\"\n title=\"The baseline changed after this diff; replace it with this screenshot\"\n >\n Approve anyway\n </Button>\n ) : (\n <Button\n variant=\"primary\"\n size=\"sm\"\n type=\"submit\"\n icon=\"check\"\n data-approve\n title=\"Approve (a)\"\n >\n Approve\n </Button>\n )}\n </form>\n );\n}\n\n/** Approve/reject for a reviewable snapshot; both advance to the next open one. */\nfunction DecisionButtons(props: DecisionProps): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, build, selected } = props;\n const base = snapshotBase(project, build, selected);\n return (\n <div class={actionGroup}>\n <ApproveForm {...props} />\n <form\n method=\"post\"\n action={`${base}/reject`}\n hx-post={`${base}/reject`}\n hx-target=\"body\"\n data-toast={`Rejected ${selected.storyName}`}\n data-toast-tone=\"warning\"\n >\n <Button variant=\"danger\" size=\"sm\" type=\"submit\" icon=\"x\" data-reject title=\"Reject (r)\">\n Reject\n </Button>\n </form>\n </div>\n );\n}\n\nfunction StepButton(props: {\n target: Snapshot | null;\n project: Project;\n build: Build;\n direction: \"prev\" | \"next\";\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { target, project, build, direction } = props;\n const label = direction === \"prev\" ? \"Previous snapshot (k)\" : \"Next snapshot (j)\";\n const icon = direction === \"prev\" ? \"chevron-left\" : \"chevron-right\";\n if (!target) {\n return <Button variant=\"ghost\" size=\"sm\" icon={icon} disabled aria-label={label} />;\n }\n const url = snapshotPageUrl(project, build, target.id);\n const hook = direction === \"prev\" ? { \"data-snap-prev\": true } : { \"data-snap-next\": true };\n return (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n icon={icon}\n href={url}\n aria-label={label}\n title={label}\n hx-get={url}\n hx-target=\"body\"\n hx-push-url=\"true\"\n {...hook}\n />\n );\n}\n\n/** Previous/next stepping, position, shortcut hints, and the review decision. */\nexport function DiffActionBar(\n props: DiffActionBarProps,\n): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, build, snapshots, selected, canReview, drifted } = props;\n const around = neighbours(snapshots, selected.id);\n const reviewable = selected.status === \"new\" || selected.status === \"changed\";\n return (\n <div class={actionBar} role=\"toolbar\" aria-label=\"Review actions\">\n <div class={actionGroup}>\n <StepButton target={around.prev} project={project} build={build} direction=\"prev\" />\n <span class={actionPosition} aria-live=\"polite\">\n {around.position} / {around.total}\n </span>\n <StepButton target={around.next} project={project} build={build} direction=\"next\" />\n </div>\n <div class={actionHints}>\n <Kbd keys={[\"j\", \"k\"]} /> navigate <Kbd keys=\"a\" /> approve <Kbd keys=\"r\" /> reject\n <Button variant=\"ghost\" size=\"sm\" data-shortcuts-open aria-label=\"Keyboard shortcuts (?)\">\n <Kbd keys=\"?\" />\n </Button>\n </div>\n {canReview && reviewable ? (\n <DecisionButtons project={project} build={build} selected={selected} drifted={drifted} />\n ) : (\n <Badge tone={statusTone(selected.status)} icon>\n {selected.status}\n </Badge>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"build-diff-actions.mjs","names":[],"sources":["../../src/pages/build-diff-actions.tsx"],"sourcesContent":["import type { Build, Project, Snapshot } from \"@storyshelf/core/schema\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport { Badge, Button, Kbd, statusTone } from \"../ui/components.tsx\";\nimport { actionBar, actionGroup, actionHints, actionPosition } from \"../ui/styles/review-layout.ts\";\nimport { groupOf, neighbours, snapshotPageUrl, type SnapshotGroup } from \"./build-diff-model.ts\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\n/** Sticky bottom bar props. */\nexport interface DiffActionBarProps {\n project: Project;\n build: Build;\n snapshots: Snapshot[];\n selected: Snapshot;\n canReview: boolean;\n /** Open snapshots whose baseline changed after their diff was computed. */\n drifted: Record<string, \"stale\" | \"removed\">;\n}\n\n/** What one decision covers: a whole story/mode across browsers, or a single snapshot. */\ninterface DecisionTarget {\n /** Endpoint prefix; `/approve` and `/reject` are appended. */\n base: string;\n /** True when the decision applies to every open browser of the story/mode. */\n shared: boolean;\n drifted: boolean;\n label: string;\n}\n\nfunction apiBase(project: Project, build: Build): string {\n return `/api/v1/projects/${project.slug}/builds/${build.id}`;\n}\n\nconst isOpenStatus = (snap: Snapshot): boolean =>\n snap.status === \"new\" || snap.status === \"changed\";\n\nfunction sharedTarget(\n base: string,\n group: SnapshotGroup,\n open: Snapshot[],\n name: string,\n): Pick<DecisionTarget, \"base\" | \"shared\" | \"label\"> {\n const story = encodeURIComponent(group.storyId);\n const mode = encodeURIComponent(group.modeKey);\n return {\n base: `${base}/stories/${story}/modes/${mode}`,\n shared: true,\n label: `${name} (${open.length} browsers)`,\n };\n}\n\nfunction decisionTarget(props: DiffActionBarProps): DecisionTarget | null {\n const { project, build, snapshots, selected, drifted } = props;\n const group = groupOf(snapshots, selected.id);\n const open = (group?.members ?? [selected]).filter((snap) => isOpenStatus(snap));\n const [first] = open;\n if (!first) return null;\n const base = apiBase(project, build);\n const drift = { drifted: open.some((snap) => drifted[snap.id] !== undefined) };\n if (open.length > 1 && group) {\n return { ...sharedTarget(base, group, open, selected.storyName), ...drift };\n }\n return { base: `${base}/snapshots/${first.id}`, shared: false, label: first.storyName, ...drift };\n}\n\n/** Approve (or \"Approve anyway\" when a baseline moved) as its own form. */\nfunction ApproveForm(props: {\n target: DecisionTarget;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { target } = props;\n const url = `${target.base}/approve${target.drifted ? \"?force=true\" : \"\"}`;\n return (\n <form\n method=\"post\"\n action={url}\n hx-post={url}\n hx-target=\"body\"\n data-toast={`Approved ${target.label}`}\n >\n {target.drifted ? (\n <Button\n variant=\"secondary\"\n size=\"sm\"\n type=\"submit\"\n icon=\"check\"\n title=\"The baseline changed after this diff; replace it with this screenshot\"\n >\n Approve anyway\n </Button>\n ) : (\n <Button\n variant=\"primary\"\n size=\"sm\"\n type=\"submit\"\n icon=\"check\"\n data-approve\n title={target.shared ? \"Approve every browser (a)\" : \"Approve (a)\"}\n >\n {target.shared ? \"Approve all browsers\" : \"Approve\"}\n </Button>\n )}\n </form>\n );\n}\n\n/** Per-snapshot escape hatch: approve only the browser being viewed. */\nfunction ApproveOnlyThisForm(props: {\n project: Project;\n build: Build;\n selected: Snapshot;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, build, selected } = props;\n const url = `${apiBase(project, build)}/snapshots/${selected.id}/approve`;\n return (\n <form\n method=\"post\"\n action={url}\n hx-post={url}\n hx-target=\"body\"\n data-toast={`Approved ${selected.storyName} on ${selected.browser ?? \"chromium\"}`}\n >\n <Button variant=\"ghost\" size=\"sm\" type=\"submit\" title=\"Approve only this browser\">\n Only {selected.browser ?? \"chromium\"}\n </Button>\n </form>\n );\n}\n\n/** Approve/reject for a reviewable story/mode; both advance to the next open one. */\nfunction DecisionButtons(props: {\n project: Project;\n build: Build;\n selected: Snapshot;\n target: DecisionTarget;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, build, selected, target } = props;\n const selectedOpen = selected.status === \"new\" || selected.status === \"changed\";\n return (\n <div class={actionGroup}>\n <ApproveForm target={target} />\n {target.shared && selectedOpen ? (\n <ApproveOnlyThisForm project={project} build={build} selected={selected} />\n ) : null}\n <form\n method=\"post\"\n action={`${target.base}/reject`}\n hx-post={`${target.base}/reject`}\n hx-target=\"body\"\n data-toast={`Rejected ${target.label}`}\n data-toast-tone=\"warning\"\n >\n <Button variant=\"danger\" size=\"sm\" type=\"submit\" icon=\"x\" data-reject title=\"Reject (r)\">\n Reject\n </Button>\n </form>\n </div>\n );\n}\n\nfunction StepButton(props: {\n target: Snapshot | null;\n project: Project;\n build: Build;\n direction: \"prev\" | \"next\";\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { target, project, build, direction } = props;\n const label = direction === \"prev\" ? \"Previous snapshot (k)\" : \"Next snapshot (j)\";\n const icon = direction === \"prev\" ? \"chevron-left\" : \"chevron-right\";\n if (!target) {\n return <Button variant=\"ghost\" size=\"sm\" icon={icon} disabled aria-label={label} />;\n }\n const url = snapshotPageUrl(project, build, target.id);\n const hook = direction === \"prev\" ? { \"data-snap-prev\": true } : { \"data-snap-next\": true };\n return (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n icon={icon}\n href={url}\n aria-label={label}\n title={label}\n hx-get={url}\n hx-target=\"body\"\n hx-push-url=\"true\"\n {...hook}\n />\n );\n}\n\n/** Previous/next stepping, position, shortcut hints, and the review decision. */\nexport function DiffActionBar(\n props: DiffActionBarProps,\n): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, build, snapshots, selected, canReview } = props;\n const around = neighbours(snapshots, selected.id);\n const target = decisionTarget(props);\n return (\n <div class={actionBar} role=\"toolbar\" aria-label=\"Review actions\">\n <div class={actionGroup}>\n <StepButton target={around.prev} project={project} build={build} direction=\"prev\" />\n <span class={actionPosition} aria-live=\"polite\">\n {around.position} / {around.total}\n </span>\n <StepButton target={around.next} project={project} build={build} direction=\"next\" />\n </div>\n <div class={actionHints}>\n <Kbd keys={[\"j\", \"k\"]} /> navigate <Kbd keys=\"a\" /> approve <Kbd keys=\"r\" /> reject\n <Button variant=\"ghost\" size=\"sm\" data-shortcuts-open aria-label=\"Keyboard shortcuts (?)\">\n <Kbd keys=\"?\" />\n </Button>\n </div>\n {canReview && target ? (\n <DecisionButtons project={project} build={build} selected={selected} target={target} />\n ) : (\n <Badge tone={statusTone(selected.status)} icon>\n {selected.status}\n </Badge>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -6,12 +6,13 @@ import { Badge, Meta } from "../ui/feedback.mjs";
6
6
  import { TextareaField } from "../ui/forms.mjs";
7
7
  import { Card, SectionTitle } from "../ui/layout.mjs";
8
8
  import { VStack } from "../ui/stacks.mjs";
9
+ import { csrfField } from "../ui/csrf-field.mjs";
9
10
  import { reviewComment, reviewCommentActions, reviewCommentBody, reviewCommentHead } from "../ui/styles/review.mjs";
10
11
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
11
12
  //#region src/pages/build-diff-comments.tsx
12
13
  /** One comment with an optional resolve action. */
13
14
  function CommentCard(props) {
14
- const { project, build, comment, canReview } = props;
15
+ const { project, build, comment, canReview, anchor } = props;
15
16
  return /* @__PURE__ */ jsxs("div", {
16
17
  class: reviewComment,
17
18
  children: [
@@ -20,6 +21,7 @@ function CommentCard(props) {
20
21
  children: [
21
22
  /* @__PURE__ */ jsx("strong", { children: comment.userId ?? "anonymous" }),
22
23
  /* @__PURE__ */ jsxs("span", { children: ["· ", new Date(comment.createdAt).toLocaleString()] }),
24
+ anchor ?? null,
23
25
  comment.resolved ? /* @__PURE__ */ jsx(Badge, {
24
26
  tone: "success",
25
27
  children: "resolved"
@@ -30,18 +32,18 @@ function CommentCard(props) {
30
32
  class: reviewCommentBody,
31
33
  children: comment.body
32
34
  }),
33
- !comment.resolved && canReview ? /* @__PURE__ */ jsx("form", {
35
+ !comment.resolved && canReview ? /* @__PURE__ */ jsxs("form", {
34
36
  method: "post",
35
- action: `/api/v1/projects/${project.slug}/builds/${build.id}/comments/${comment.id}/resolve`,
36
- "hx-post": `/api/v1/projects/${project.slug}/builds/${build.id}/comments/${comment.id}/resolve`,
37
+ action: `/projects/${project.slug}/builds/${build.id}/comments/${comment.id}/resolve`,
38
+ "hx-post": `/projects/${project.slug}/builds/${build.id}/comments/${comment.id}/resolve`,
37
39
  "hx-target": "body",
38
40
  class: reviewCommentActions,
39
- children: /* @__PURE__ */ jsx(Button, {
41
+ children: [csrfField(), /* @__PURE__ */ jsx(Button, {
40
42
  variant: "ghost",
41
43
  size: "sm",
42
44
  type: "submit",
43
45
  children: "Mark resolved"
44
- })
46
+ })]
45
47
  }) : null
46
48
  ]
47
49
  }, comment.id);
@@ -49,13 +51,13 @@ function CommentCard(props) {
49
51
  /** Reply form scoped to the selected snapshot. */
50
52
  function CommentForm(props) {
51
53
  const { project, build, selectedId } = props;
52
- return /* @__PURE__ */ jsx("form", {
54
+ return /* @__PURE__ */ jsxs("form", {
53
55
  method: "post",
54
- action: `/api/v1/projects/${project.slug}/builds/${build.id}/comments`,
55
- "hx-post": `/api/v1/projects/${project.slug}/builds/${build.id}/comments`,
56
+ action: `/projects/${project.slug}/builds/${build.id}/comments`,
57
+ "hx-post": `/projects/${project.slug}/builds/${build.id}/comments`,
56
58
  "hx-target": "body",
57
59
  class: "mt-1",
58
- children: /* @__PURE__ */ jsxs(VStack, { children: [
60
+ children: [csrfField(), /* @__PURE__ */ jsxs(VStack, { children: [
59
61
  /* @__PURE__ */ jsx("input", {
60
62
  type: "hidden",
61
63
  name: "snapshotId",
@@ -73,13 +75,13 @@ function CommentForm(props) {
73
75
  type: "submit",
74
76
  children: "Comment"
75
77
  }) })
76
- ] })
78
+ ] })]
77
79
  });
78
80
  }
79
81
  /** Threaded comments for the selected snapshot plus the reply form. */
80
82
  function DiffComments(props) {
81
- const { project, build, selectedId, comments, canReview } = props;
82
- const visible = comments.filter((c) => !c.snapshotId || c.snapshotId === selectedId);
83
+ const { project, build, selectedId, selectedStoryId, comments, canReview } = props;
84
+ const visible = comments.filter((c) => c.snapshotId ? c.snapshotId === selectedId : !c.storyId || c.storyId === selectedStoryId);
83
85
  return /* @__PURE__ */ jsxs(Card, { children: [
84
86
  /* @__PURE__ */ jsx(SectionTitle, {
85
87
  level: 3,
@@ -99,6 +101,6 @@ function DiffComments(props) {
99
101
  ] });
100
102
  }
101
103
  //#endregion
102
- export { DiffComments };
104
+ export { CommentCard, DiffComments };
103
105
 
104
106
  //# sourceMappingURL=build-diff-comments.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"build-diff-comments.mjs","names":[],"sources":["../../src/pages/build-diff-comments.tsx"],"sourcesContent":["import type { Build } from \"@storyshelf/core/schema\";\nimport type { Comment } from \"@storyshelf/core/schema\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport {\n Badge,\n Button,\n Card,\n Meta,\n SectionTitle,\n TextareaField,\n VStack,\n} from \"../ui/components.tsx\";\nimport {\n reviewComment,\n reviewCommentActions,\n reviewCommentBody,\n reviewCommentHead,\n} from \"../ui/styles/review.ts\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\n/** Snapshot comment thread props. */\nexport interface DiffCommentsProps {\n project: Project;\n build: Build;\n selectedId: string;\n comments: Comment[];\n canReview: boolean;\n}\n\ninterface CommentCardProps {\n project: Project;\n build: Build;\n comment: Comment;\n canReview: boolean;\n}\n\n/** One comment with an optional resolve action. */\nfunction CommentCard(props: CommentCardProps): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, build, comment, canReview } = props;\n return (\n <div key={comment.id} class={reviewComment}>\n <div class={reviewCommentHead}>\n <strong>{comment.userId ?? \"anonymous\"}</strong>\n <span>· {new Date(comment.createdAt).toLocaleString()}</span>\n {comment.resolved ? <Badge tone=\"success\">resolved</Badge> : null}\n </div>\n <p class={reviewCommentBody}>{comment.body}</p>\n {!comment.resolved && canReview ? (\n <form\n method=\"post\"\n action={`/api/v1/projects/${project.slug}/builds/${build.id}/comments/${comment.id}/resolve`}\n hx-post={`/api/v1/projects/${project.slug}/builds/${build.id}/comments/${comment.id}/resolve`}\n hx-target=\"body\"\n class={reviewCommentActions}\n >\n <Button variant=\"ghost\" size=\"sm\" type=\"submit\">\n Mark resolved\n </Button>\n </form>\n ) : null}\n </div>\n );\n}\n\ninterface CommentFormProps {\n project: Project;\n build: Build;\n selectedId: string;\n}\n\n/** Reply form scoped to the selected snapshot. */\nfunction CommentForm(props: CommentFormProps): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, build, selectedId } = props;\n return (\n <form\n method=\"post\"\n action={`/api/v1/projects/${project.slug}/builds/${build.id}/comments`}\n hx-post={`/api/v1/projects/${project.slug}/builds/${build.id}/comments`}\n hx-target=\"body\"\n class=\"mt-1\"\n >\n <VStack>\n <input type=\"hidden\" name=\"snapshotId\" value={selectedId} />\n <TextareaField\n label=\"Add comment\"\n name=\"body\"\n rows={3}\n required\n placeholder=\"Leave feedback on this snapshot…\"\n />\n <div>\n <Button variant=\"primary\" type=\"submit\">\n Comment\n </Button>\n </div>\n </VStack>\n </form>\n );\n}\n\n/** Threaded comments for the selected snapshot plus the reply form. */\nexport function DiffComments(\n props: DiffCommentsProps,\n): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, build, selectedId, comments, canReview } = props;\n const visible = comments.filter((c) => !c.snapshotId || c.snapshotId === selectedId);\n return (\n <Card>\n <SectionTitle level={3}>Comments</SectionTitle>\n <VStack>\n {visible.map((comment): HtmlEscapedString | Promise<HtmlEscapedString> => (\n <CommentCard\n key={comment.id}\n project={project}\n build={build}\n comment={comment}\n canReview={canReview}\n />\n ))}\n {visible.length === 0 ? <Meta>No comments on this snapshot.</Meta> : null}\n </VStack>\n\n <CommentForm project={project} build={build} selectedId={selectedId} />\n </Card>\n );\n}\n"],"mappings":";;;;;;;;;;;;AAuCA,SAAS,YAAY,OAAyE;CAC5F,MAAM,EAAE,SAAS,OAAO,SAAS,cAAc;CAC/C,OACE,qBAAC,OAAD;EAAsB,OAAO;EAA7B,UAAA;GACE,qBAAC,OAAD;IAAK,OAAO;IAAZ,UAAA;KACE,oBAAC,UAAD,EAAA,UAAS,QAAQ,UAAU,YAAoB,CAAA;KAC/C,qBAAC,QAAD,EAAA,UAAA,CAAM,MAAG,IAAI,KAAK,QAAQ,SAAS,CAAC,CAAC,eAAe,CAAQ,EAAA,CAAA;KAC3D,QAAQ,WAAW,oBAAC,OAAD;MAAO,MAAK;MAAU,UAAA;KAAe,CAAA,IAAI;IAC1D;;GACL,oBAAC,KAAD;IAAG,OAAO;IAAoB,UAAA,QAAQ;GAAQ,CAAA;GAC7C,CAAC,QAAQ,YAAY,YACpB,oBAAC,QAAD;IACE,QAAO;IACP,QAAQ,oBAAoB,QAAQ,KAAK,UAAU,MAAM,GAAG,YAAY,QAAQ,GAAG;IACnF,WAAS,oBAAoB,QAAQ,KAAK,UAAU,MAAM,GAAG,YAAY,QAAQ,GAAG;IACpF,aAAU;IACV,OAAO;IAEP,UAAA,oBAAC,QAAD;KAAQ,SAAQ;KAAQ,MAAK;KAAK,MAAK;KAAS,UAAA;IAExC,CAAA;GACJ,CAAA,IACJ;EACD;CApBK,GAAA,QAAQ,EAoBb;AAET;;AASA,SAAS,YAAY,OAAyE;CAC5F,MAAM,EAAE,SAAS,OAAO,eAAe;CACvC,OACE,oBAAC,QAAD;EACE,QAAO;EACP,QAAQ,oBAAoB,QAAQ,KAAK,UAAU,MAAM,GAAG;EAC5D,WAAS,oBAAoB,QAAQ,KAAK,UAAU,MAAM,GAAG;EAC7D,aAAU;EACV,OAAM;EAEN,UAAA,qBAAC,QAAD,EAAA,UAAA;GACE,oBAAC,SAAD;IAAO,MAAK;IAAS,MAAK;IAAa,OAAO;GAAa,CAAA;GAC3D,oBAAC,eAAD;IACE,OAAM;IACN,MAAK;IACL,MAAM;IACN,UAAA;IACA,aAAY;GACb,CAAA;GACD,oBAAC,OAAD,EAAA,UACE,oBAAC,QAAD;IAAQ,SAAQ;IAAU,MAAK;IAAS,UAAA;GAEhC,CAAA,EACL,CAAA;EACC,EAAA,CAAA;CACJ,CAAA;AAEV;;AAGA,SAAgB,aACd,OACgD;CAChD,MAAM,EAAE,SAAS,OAAO,YAAY,UAAU,cAAc;CAC5D,MAAM,UAAU,SAAS,QAAQ,MAAM,CAAC,EAAE,cAAc,EAAE,eAAe,UAAU;CACnF,OACE,qBAAC,MAAD,EAAA,UAAA;EACE,oBAAC,cAAD;GAAc,OAAO;GAAG,UAAA;EAAsB,CAAA;EAC9C,qBAAC,QAAD,EAAA,UAAA,CACG,QAAQ,KAAK,YACZ,oBAAC,aAAD;GAEW;GACF;GACE;GACE;EACZ,GALM,QAAQ,EAKd,CACF,GACA,QAAQ,WAAW,IAAI,oBAAC,MAAD,EAAA,UAAM,gCAAmC,CAAA,IAAI,IAC/D,EAAA,CAAA;EAER,oBAAC,aAAD;GAAsB;GAAgB;GAAmB;EAAa,CAAA;CAClE,EAAA,CAAA;AAEV"}
1
+ {"version":3,"file":"build-diff-comments.mjs","names":[],"sources":["../../src/pages/build-diff-comments.tsx"],"sourcesContent":["import type { Build } from \"@storyshelf/core/schema\";\nimport type { Comment } from \"@storyshelf/core/schema\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport {\n Badge,\n Button,\n Card,\n Meta,\n SectionTitle,\n TextareaField,\n VStack,\n} from \"../ui/components.tsx\";\nimport { csrfField } from \"../ui/csrf-field.tsx\";\nimport {\n reviewComment,\n reviewCommentActions,\n reviewCommentBody,\n reviewCommentHead,\n} from \"../ui/styles/review.ts\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\n/** Snapshot comment thread props. */\nexport interface DiffCommentsProps {\n project: Project;\n build: Build;\n selectedId: string;\n /** Story of the selected snapshot: its story comments show here too (ADR 0013, decision 2). */\n selectedStoryId?: string;\n comments: Comment[];\n canReview: boolean;\n}\n\n/** One comment's props: where it sits, and what its head shows beside the author. */\nexport interface CommentCardProps {\n project: Project;\n build: Build;\n comment: Comment;\n canReview: boolean;\n /** What the comment is anchored to (a story badge), shown in its head. */\n anchor?: unknown;\n}\n\n/** One comment with an optional resolve action. */\nexport function CommentCard(\n props: CommentCardProps,\n): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, build, comment, canReview, anchor } = props;\n return (\n <div key={comment.id} class={reviewComment}>\n <div class={reviewCommentHead}>\n <strong>{comment.userId ?? \"anonymous\"}</strong>\n <span>· {new Date(comment.createdAt).toLocaleString()}</span>\n {anchor ?? null}\n {comment.resolved ? <Badge tone=\"success\">resolved</Badge> : null}\n </div>\n <p class={reviewCommentBody}>{comment.body}</p>\n {!comment.resolved && canReview ? (\n <form\n method=\"post\"\n action={`/projects/${project.slug}/builds/${build.id}/comments/${comment.id}/resolve`}\n hx-post={`/projects/${project.slug}/builds/${build.id}/comments/${comment.id}/resolve`}\n hx-target=\"body\"\n class={reviewCommentActions}\n >\n {csrfField()}\n <Button variant=\"ghost\" size=\"sm\" type=\"submit\">\n Mark resolved\n </Button>\n </form>\n ) : null}\n </div>\n );\n}\n\ninterface CommentFormProps {\n project: Project;\n build: Build;\n selectedId: string;\n}\n\n/** Reply form scoped to the selected snapshot. */\nfunction CommentForm(props: CommentFormProps): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, build, selectedId } = props;\n return (\n <form\n method=\"post\"\n action={`/projects/${project.slug}/builds/${build.id}/comments`}\n hx-post={`/projects/${project.slug}/builds/${build.id}/comments`}\n hx-target=\"body\"\n class=\"mt-1\"\n >\n {csrfField()}\n <VStack>\n <input type=\"hidden\" name=\"snapshotId\" value={selectedId} />\n <TextareaField\n label=\"Add comment\"\n name=\"body\"\n rows={3}\n required\n placeholder=\"Leave feedback on this snapshot…\"\n />\n <div>\n <Button variant=\"primary\" type=\"submit\">\n Comment\n </Button>\n </div>\n </VStack>\n </form>\n );\n}\n\n/** Threaded comments for the selected snapshot plus the reply form. */\nexport function DiffComments(\n props: DiffCommentsProps,\n): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, build, selectedId, selectedStoryId, comments, canReview } = props;\n const visible = comments.filter((c) =>\n c.snapshotId ? c.snapshotId === selectedId : !c.storyId || c.storyId === selectedStoryId,\n );\n return (\n <Card>\n <SectionTitle level={3}>Comments</SectionTitle>\n <VStack>\n {visible.map((comment): HtmlEscapedString | Promise<HtmlEscapedString> => (\n <CommentCard\n key={comment.id}\n project={project}\n build={build}\n comment={comment}\n canReview={canReview}\n />\n ))}\n {visible.length === 0 ? <Meta>No comments on this snapshot.</Meta> : null}\n </VStack>\n\n <CommentForm project={project} build={build} selectedId={selectedId} />\n </Card>\n );\n}\n"],"mappings":";;;;;;;;;;;;;AA6CA,SAAgB,YACd,OACgD;CAChD,MAAM,EAAE,SAAS,OAAO,SAAS,WAAW,WAAW;CACvD,OACE,qBAAC,OAAD;EAAsB,OAAO;EAA7B,UAAA;GACE,qBAAC,OAAD;IAAK,OAAO;IAAZ,UAAA;KACE,oBAAC,UAAD,EAAA,UAAS,QAAQ,UAAU,YAAoB,CAAA;KAC/C,qBAAC,QAAD,EAAA,UAAA,CAAM,MAAG,IAAI,KAAK,QAAQ,SAAS,CAAC,CAAC,eAAe,CAAQ,EAAA,CAAA;KAC3D,UAAU;KACV,QAAQ,WAAW,oBAAC,OAAD;MAAO,MAAK;MAAU,UAAA;KAAe,CAAA,IAAI;IAC1D;;GACL,oBAAC,KAAD;IAAG,OAAO;IAAoB,UAAA,QAAQ;GAAQ,CAAA;GAC7C,CAAC,QAAQ,YAAY,YACpB,qBAAC,QAAD;IACE,QAAO;IACP,QAAQ,aAAa,QAAQ,KAAK,UAAU,MAAM,GAAG,YAAY,QAAQ,GAAG;IAC5E,WAAS,aAAa,QAAQ,KAAK,UAAU,MAAM,GAAG,YAAY,QAAQ,GAAG;IAC7E,aAAU;IACV,OAAO;IALT,UAAA,CAOG,UAAU,GACX,oBAAC,QAAD;KAAQ,SAAQ;KAAQ,MAAK;KAAK,MAAK;KAAS,UAAA;IAExC,CAAA,CACJ;GACJ,CAAA,IAAA;EACD;CAtBK,GAAA,QAAQ,EAsBb;AAET;;AASA,SAAS,YAAY,OAAyE;CAC5F,MAAM,EAAE,SAAS,OAAO,eAAe;CACvC,OACE,qBAAC,QAAD;EACE,QAAO;EACP,QAAQ,aAAa,QAAQ,KAAK,UAAU,MAAM,GAAG;EACrD,WAAS,aAAa,QAAQ,KAAK,UAAU,MAAM,GAAG;EACtD,aAAU;EACV,OAAM;EALR,UAAA,CAOG,UAAU,GACX,qBAAC,QAAD,EAAA,UAAA;GACE,oBAAC,SAAD;IAAO,MAAK;IAAS,MAAK;IAAa,OAAO;GAAa,CAAA;GAC3D,oBAAC,eAAD;IACE,OAAM;IACN,MAAK;IACL,MAAM;IACN,UAAA;IACA,aAAY;GACb,CAAA;GACD,oBAAC,OAAD,EAAA,UACE,oBAAC,QAAD;IAAQ,SAAQ;IAAU,MAAK;IAAS,UAAA;GAEhC,CAAA,EACL,CAAA;EACC,EAAA,CAAA,CACJ;;AAEV;;AAGA,SAAgB,aACd,OACgD;CAChD,MAAM,EAAE,SAAS,OAAO,YAAY,iBAAiB,UAAU,cAAc;CAC7E,MAAM,UAAU,SAAS,QAAQ,MAC/B,EAAE,aAAa,EAAE,eAAe,aAAa,CAAC,EAAE,WAAW,EAAE,YAAY,eAC3E;CACA,OACE,qBAAC,MAAD,EAAA,UAAA;EACE,oBAAC,cAAD;GAAc,OAAO;GAAG,UAAA;EAAsB,CAAA;EAC9C,qBAAC,QAAD,EAAA,UAAA,CACG,QAAQ,KAAK,YACZ,oBAAC,aAAD;GAEW;GACF;GACE;GACE;EACZ,GALM,QAAQ,EAKd,CACF,GACA,QAAQ,WAAW,IAAI,oBAAC,MAAD,EAAA,UAAM,gCAAmC,CAAA,IAAI,IAC/D,EAAA,CAAA;EAER,oBAAC,aAAD;GAAsB;GAAgB;GAAmB;EAAa,CAAA;CAClE,EAAA,CAAA;AAEV"}
@@ -0,0 +1,82 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { Alert } from "../ui/feedback.mjs";
5
+ import { StaleBaselineNotice } from "./build-diff-stale.mjs";
6
+ import { environmentChangeOf, labelOf, summariseEnvironmentChanges } from "./environment-change.mjs";
7
+ import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
8
+ //#region src/pages/build-diff-environment.tsx
9
+ /**
10
+ * Build-level note: some snapshots were captured in a different environment than their baseline.
11
+ * The default branch migrates them without a diff; a feature branch still diffs them (ADR 0005).
12
+ */
13
+ function EnvironmentChangeNotice(props) {
14
+ const summary = summariseEnvironmentChanges(props.snapshots);
15
+ if (!summary) return null;
16
+ const { count, fields, browserVersion } = summary;
17
+ const what = `the ${fields.join(", ")} changed${browserVersion ? ` (${browserVersion.from} → ${browserVersion.to})` : ""}`;
18
+ return props.migrated ? /* @__PURE__ */ jsxs(Alert, {
19
+ tone: "info",
20
+ title: "Capture environment changed",
21
+ children: [
22
+ count,
23
+ " ",
24
+ count === 1 ? "snapshot was" : "snapshots were",
25
+ " accepted without a pixel diff because ",
26
+ what,
27
+ ". The same code renders differently here, so a diff would only show the environment. The baselines were updated to match."
28
+ ]
29
+ }) : /* @__PURE__ */ jsxs(Alert, {
30
+ tone: "info",
31
+ title: "Capture environment changed",
32
+ children: [
33
+ count,
34
+ " ",
35
+ count === 1 ? "snapshot was" : "snapshots were",
36
+ " compared against a baseline from a different capture environment: ",
37
+ what,
38
+ ". A difference may come from the environment rather than the code; once the default branch captures in the new environment, a rebuild compares like with like."
39
+ ]
40
+ });
41
+ }
42
+ /** Per-snapshot note shown above the viewer for a snapshot whose capture environment changed. */
43
+ function EnvironmentChangeSnapshotNotice(props) {
44
+ const change = environmentChangeOf(props.snapshot);
45
+ if (!change) return null;
46
+ const what = change.changed.map((field) => labelOf(field)).join(", ");
47
+ return props.migrated ? /* @__PURE__ */ jsxs(Alert, {
48
+ tone: "info",
49
+ title: "No diff: capture environment changed",
50
+ children: [
51
+ "This snapshot was not compared pixel by pixel. What changed: ",
52
+ what,
53
+ "."
54
+ ]
55
+ }) : /* @__PURE__ */ jsxs(Alert, {
56
+ tone: "info",
57
+ title: "Capture environment changed",
58
+ children: [
59
+ "The baseline was captured in a different environment (",
60
+ what,
61
+ "), so a difference may come from the environment rather than the code."
62
+ ]
63
+ });
64
+ }
65
+ /** Build-level notices above the review grid: stale baselines and environment migrations. */
66
+ function ReviewNotices(props) {
67
+ const { project, build, snapshots, drifted, canReview } = props;
68
+ const staleCount = Object.keys(drifted).length;
69
+ return /* @__PURE__ */ jsxs(Fragment, { children: [staleCount > 0 ? /* @__PURE__ */ jsx(StaleBaselineNotice, {
70
+ project,
71
+ build,
72
+ staleCount,
73
+ canReview
74
+ }) : null, /* @__PURE__ */ jsx(EnvironmentChangeNotice, {
75
+ snapshots,
76
+ migrated: build.isDefault
77
+ })] });
78
+ }
79
+ //#endregion
80
+ export { EnvironmentChangeNotice, EnvironmentChangeSnapshotNotice, ReviewNotices };
81
+
82
+ //# sourceMappingURL=build-diff-environment.mjs.map
@@ -0,0 +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"}
@@ -5,8 +5,9 @@ import { Button } from "../ui/buttons.mjs";
5
5
  import { Badge, Meta, statusTone } from "../ui/feedback.mjs";
6
6
  import { Avatar } from "../ui/avatar.mjs";
7
7
  import { Progress } from "../ui/progress.mjs";
8
- import { headerActions, headerMain, headerMeta, headerProgress, headerSide, headerTitle, reviewHeader } from "../ui/styles/review-layout.mjs";
8
+ import { a11ySummaryOf, a11yUnavailableNote } from "./a11y-violations.mjs";
9
9
  import { reviewProgress } from "./build-diff-model.mjs";
10
+ import { headerActions, headerMain, headerMeta, headerProgress, headerSide, headerTitle, reviewHeader } from "../ui/styles/review-layout.mjs";
10
11
  import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
11
12
  //#region src/pages/build-diff-header.tsx
12
13
  /** Approve-all / reject-all bulk actions (each asks for confirmation). */
@@ -46,7 +47,8 @@ function BulkActions({ project, build, pendingCount }) {
46
47
  })
47
48
  })] });
48
49
  }
49
- function HeaderTitle({ build }) {
50
+ function HeaderTitle({ build, snapshots }) {
51
+ const a11y = a11ySummaryOf(snapshots);
50
52
  return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("h1", {
51
53
  class: headerTitle,
52
54
  children: [
@@ -70,7 +72,20 @@ function HeaderTitle({ build }) {
70
72
  size: "sm"
71
73
  }) : null,
72
74
  build.authorName ? /* @__PURE__ */ jsx("span", { children: build.authorName }) : null,
73
- /* @__PURE__ */ jsx("span", { children: build.message ?? "No message" })
75
+ /* @__PURE__ */ jsx("span", { children: build.message ?? "No message" }),
76
+ a11y.findings > 0 ? /* @__PURE__ */ jsxs(Badge, {
77
+ tone: "warning",
78
+ icon: true,
79
+ children: [
80
+ "a11y ",
81
+ a11y.findings,
82
+ " (does not block)"
83
+ ]
84
+ }) : null,
85
+ a11y.unavailable > 0 ? /* @__PURE__ */ jsx(Badge, {
86
+ tone: "neutral",
87
+ children: a11yUnavailableNote(a11y.unavailable)
88
+ }) : null
74
89
  ]
75
90
  })] });
76
91
  }
@@ -92,7 +107,10 @@ function DiffHeader(props) {
92
107
  class: reviewHeader,
93
108
  children: [/* @__PURE__ */ jsx("div", {
94
109
  class: headerMain,
95
- children: /* @__PURE__ */ jsx(HeaderTitle, { build })
110
+ children: /* @__PURE__ */ jsx(HeaderTitle, {
111
+ build,
112
+ snapshots
113
+ })
96
114
  }), /* @__PURE__ */ jsxs("div", {
97
115
  class: headerSide,
98
116
  children: [/* @__PURE__ */ jsx(HeaderProgress, { snapshots }), /* @__PURE__ */ jsxs("div", {
@@ -1 +1 @@
1
- {"version":3,"file":"build-diff-header.mjs","names":[],"sources":["../../src/pages/build-diff-header.tsx"],"sourcesContent":["import type { Build, Project, Snapshot } from \"@storyshelf/core/schema\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport { Avatar, Badge, Button, Meta, Progress, statusTone } from \"../ui/components.tsx\";\nimport {\n headerActions,\n headerMain,\n headerMeta,\n headerProgress,\n headerSide,\n headerTitle,\n reviewHeader,\n} from \"../ui/styles/review-layout.ts\";\nimport { reviewProgress } from \"./build-diff-model.ts\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\n/** Header props for the diff review page. */\nexport interface DiffHeaderProps {\n project: Project;\n build: Build;\n snapshots: Snapshot[];\n pendingCount: number;\n canReview: boolean;\n}\n\ninterface BulkProps {\n project: Project;\n build: Build;\n pendingCount: number;\n}\n\n/** Approve-all / reject-all bulk actions (each asks for confirmation). */\nfunction BulkActions({\n project,\n build,\n pendingCount,\n}: BulkProps): HtmlEscapedString | Promise<HtmlEscapedString> {\n const base = `/api/v1/projects/${project.slug}/builds/${build.id}`;\n return (\n <>\n <form\n method=\"post\"\n action={`${base}/approve-all`}\n hx-post={`${base}/approve-all`}\n hx-target=\"body\"\n hx-confirm={`Approve all ${pendingCount} remaining changes?`}\n data-toast={`Approved ${pendingCount} ${pendingCount === 1 ? \"change\" : \"changes\"}`}\n >\n <Button variant=\"primary\" size=\"sm\" type=\"submit\" icon=\"check\">\n Approve all ({pendingCount})\n </Button>\n </form>\n <form\n method=\"post\"\n action={`${base}/reject-all`}\n hx-post={`${base}/reject-all`}\n hx-target=\"body\"\n hx-confirm={`Reject all ${pendingCount} remaining changes?`}\n data-toast={`Rejected ${pendingCount} ${pendingCount === 1 ? \"change\" : \"changes\"}`}\n data-toast-tone=\"warning\"\n >\n <Button variant=\"secondary\" size=\"sm\" type=\"submit\">\n Reject all\n </Button>\n </form>\n </>\n );\n}\n\nfunction HeaderTitle({ build }: { build: Build }): HtmlEscapedString | Promise<HtmlEscapedString> {\n return (\n <>\n <h1 class={headerTitle}>\n <span>{build.gitBranch}</span>\n <Meta as=\"span\" mono>\n {build.gitSha.slice(0, 7)}\n </Meta>\n <Badge tone={statusTone(build.status)} icon>\n {build.status}\n </Badge>\n </h1>\n <p class={headerMeta}>\n {build.authorName ? <Avatar name={build.authorName} size=\"sm\" /> : null}\n {build.authorName ? <span>{build.authorName}</span> : null}\n <span>{build.message ?? \"No message\"}</span>\n </p>\n </>\n );\n}\n\nfunction HeaderProgress({\n snapshots,\n}: {\n snapshots: Snapshot[];\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const progress = reviewProgress(snapshots);\n return (\n <div class={headerProgress}>\n <span>\n {progress.reviewable === 0\n ? \"No changes to review\"\n : `${progress.done} of ${progress.reviewable} reviewed`}\n </span>\n <Progress value={progress.done} max={progress.reviewable} label=\"Review progress\" />\n </div>\n );\n}\n\n/** Compact build summary with review progress and bulk actions. */\nexport function DiffHeader(props: DiffHeaderProps): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, build, snapshots, pendingCount, canReview } = props;\n return (\n <header class={reviewHeader}>\n <div class={headerMain}>\n <HeaderTitle build={build} />\n </div>\n <div class={headerSide}>\n <HeaderProgress snapshots={snapshots} />\n <div class={headerActions}>\n <Button\n variant=\"secondary\"\n size=\"sm\"\n href={`/projects/${project.slug}/builds/${build.id}`}\n >\n Build overview\n </Button>\n {canReview && pendingCount > 0 ? (\n <BulkActions project={project} build={build} pendingCount={pendingCount} />\n ) : null}\n </div>\n </div>\n </header>\n );\n}\n"],"mappings":";;;;;;;;;;;;AAgCA,SAAS,YAAY,EACnB,SACA,OACA,gBAC4D;CAC5D,MAAM,OAAO,oBAAoB,QAAQ,KAAK,UAAU,MAAM;CAC9D,OACE,qBAAA,UAAA,EAAA,UAAA,CACE,oBAAC,QAAD;EACE,QAAO;EACP,QAAQ,GAAG,KAAK;EAChB,WAAS,GAAG,KAAK;EACjB,aAAU;EACV,cAAY,eAAe,aAAa;EACxC,cAAY,YAAY,aAAa,GAAG,iBAAiB,IAAI,WAAW;EAExE,UAAA,qBAAC,QAAD;GAAQ,SAAQ;GAAU,MAAK;GAAK,MAAK;GAAS,MAAK;GAAvD,UAAA;IAA+D;IAC/C;IAAa;GACrB;;CACJ,CAAA,GACN,oBAAC,QAAD;EACE,QAAO;EACP,QAAQ,GAAG,KAAK;EAChB,WAAS,GAAG,KAAK;EACjB,aAAU;EACV,cAAY,cAAc,aAAa;EACvC,cAAY,YAAY,aAAa,GAAG,iBAAiB,IAAI,WAAW;EACxE,mBAAgB;EAEhB,UAAA,oBAAC,QAAD;GAAQ,SAAQ;GAAY,MAAK;GAAK,MAAK;GAAS,UAAA;EAE5C,CAAA;CACJ,CAAA,CACN,EAAA,CAAA;AAEN;AAEA,SAAS,YAAY,EAAE,SAA2E;CAChG,OACE,qBAAA,UAAA,EAAA,UAAA,CACE,qBAAC,MAAD;EAAI,OAAO;EAAX,UAAA;GACE,oBAAC,QAAD,EAAA,UAAO,MAAM,UAAgB,CAAA;GAC7B,oBAAC,MAAD;IAAM,IAAG;IAAO,MAAA;IACb,UAAA,MAAM,OAAO,MAAM,GAAG,CAAC;GACpB,CAAA;GACN,oBAAC,OAAD;IAAO,MAAM,WAAW,MAAM,MAAM;IAAG,MAAA;IACpC,UAAA,MAAM;GACF,CAAA;EACL;CACJ,CAAA,GAAA,qBAAC,KAAD;EAAG,OAAO;EAAV,UAAA;GACG,MAAM,aAAa,oBAAC,QAAD;IAAQ,MAAM,MAAM;IAAY,MAAK;GAAM,CAAA,IAAI;GAClE,MAAM,aAAa,oBAAC,QAAD,EAAA,UAAO,MAAM,WAAiB,CAAA,IAAI;GACtD,oBAAC,QAAD,EAAA,UAAO,MAAM,WAAW,aAAmB,CAAA;EAC1C;CACH,CAAA,CAAA,EAAA,CAAA;AAEN;AAEA,SAAS,eAAe,EACtB,aAGiD;CACjD,MAAM,WAAW,eAAe,SAAS;CACzC,OACE,qBAAC,OAAD;EAAK,OAAO;EAAZ,UAAA,CACE,oBAAC,QAAD,EAAA,UACG,SAAS,eAAe,IACrB,yBACA,GAAG,SAAS,KAAK,MAAM,SAAS,WAAW,WAC3C,CAAA,GACN,oBAAC,UAAD;GAAU,OAAO,SAAS;GAAM,KAAK,SAAS;GAAY,OAAM;EAAmB,CAAA,CAChF;;AAET;;AAGA,SAAgB,WAAW,OAAwE;CACjG,MAAM,EAAE,SAAS,OAAO,WAAW,cAAc,cAAc;CAC/D,OACE,qBAAC,UAAD;EAAQ,OAAO;EAAf,UAAA,CACE,oBAAC,OAAD;GAAK,OAAO;GACV,UAAA,oBAAC,aAAD,EAAoB,MAAQ,CAAA;EACzB,CAAA,GACL,qBAAC,OAAD;GAAK,OAAO;GAAZ,UAAA,CACE,oBAAC,gBAAD,EAA2B,UAAY,CAAA,GACvC,qBAAC,OAAD;IAAK,OAAO;IAAZ,UAAA,CACE,oBAAC,QAAD;KACE,SAAQ;KACR,MAAK;KACL,MAAM,aAAa,QAAQ,KAAK,UAAU,MAAM;KACjD,UAAA;IAEO,CAAA,GACP,aAAa,eAAe,IAC3B,oBAAC,aAAD;KAAsB;KAAgB;KAAqB;IAAe,CAAA,IACxE,IACD;GACF,CAAA,CAAA;EACC,CAAA,CAAA;;AAEZ"}
1
+ {"version":3,"file":"build-diff-header.mjs","names":[],"sources":["../../src/pages/build-diff-header.tsx"],"sourcesContent":["import type { Build, Project, Snapshot } from \"@storyshelf/core/schema\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport { Avatar, Badge, Button, Meta, Progress, statusTone } from \"../ui/components.tsx\";\nimport {\n headerActions,\n headerMain,\n headerMeta,\n headerProgress,\n headerSide,\n headerTitle,\n reviewHeader,\n} from \"../ui/styles/review-layout.ts\";\nimport { a11ySummaryOf, a11yUnavailableNote } from \"./a11y-violations.tsx\";\nimport { reviewProgress } from \"./build-diff-model.ts\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\n/** Header props for the diff review page. */\nexport interface DiffHeaderProps {\n project: Project;\n build: Build;\n snapshots: Snapshot[];\n pendingCount: number;\n canReview: boolean;\n}\n\ninterface BulkProps {\n project: Project;\n build: Build;\n pendingCount: number;\n}\n\n/** Approve-all / reject-all bulk actions (each asks for confirmation). */\nfunction BulkActions({\n project,\n build,\n pendingCount,\n}: BulkProps): HtmlEscapedString | Promise<HtmlEscapedString> {\n const base = `/api/v1/projects/${project.slug}/builds/${build.id}`;\n return (\n <>\n <form\n method=\"post\"\n action={`${base}/approve-all`}\n hx-post={`${base}/approve-all`}\n hx-target=\"body\"\n hx-confirm={`Approve all ${pendingCount} remaining changes?`}\n data-toast={`Approved ${pendingCount} ${pendingCount === 1 ? \"change\" : \"changes\"}`}\n >\n <Button variant=\"primary\" size=\"sm\" type=\"submit\" icon=\"check\">\n Approve all ({pendingCount})\n </Button>\n </form>\n <form\n method=\"post\"\n action={`${base}/reject-all`}\n hx-post={`${base}/reject-all`}\n hx-target=\"body\"\n hx-confirm={`Reject all ${pendingCount} remaining changes?`}\n data-toast={`Rejected ${pendingCount} ${pendingCount === 1 ? \"change\" : \"changes\"}`}\n data-toast-tone=\"warning\"\n >\n <Button variant=\"secondary\" size=\"sm\" type=\"submit\">\n Reject all\n </Button>\n </form>\n </>\n );\n}\n\nfunction HeaderTitle({\n build,\n snapshots,\n}: {\n build: Build;\n snapshots: Snapshot[];\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const a11y = a11ySummaryOf(snapshots);\n return (\n <>\n <h1 class={headerTitle}>\n <span>{build.gitBranch}</span>\n <Meta as=\"span\" mono>\n {build.gitSha.slice(0, 7)}\n </Meta>\n <Badge tone={statusTone(build.status)} icon>\n {build.status}\n </Badge>\n </h1>\n <p class={headerMeta}>\n {build.authorName ? <Avatar name={build.authorName} size=\"sm\" /> : null}\n {build.authorName ? <span>{build.authorName}</span> : null}\n <span>{build.message ?? \"No message\"}</span>\n {a11y.findings > 0 ? (\n <Badge tone=\"warning\" icon>\n a11y {a11y.findings} (does not block)\n </Badge>\n ) : null}\n {a11y.unavailable > 0 ? (\n <Badge tone=\"neutral\">{a11yUnavailableNote(a11y.unavailable)}</Badge>\n ) : null}\n </p>\n </>\n );\n}\n\nfunction HeaderProgress({\n snapshots,\n}: {\n snapshots: Snapshot[];\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const progress = reviewProgress(snapshots);\n return (\n <div class={headerProgress}>\n <span>\n {progress.reviewable === 0\n ? \"No changes to review\"\n : `${progress.done} of ${progress.reviewable} reviewed`}\n </span>\n <Progress value={progress.done} max={progress.reviewable} label=\"Review progress\" />\n </div>\n );\n}\n\n/** Compact build summary with review progress and bulk actions. */\nexport function DiffHeader(props: DiffHeaderProps): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, build, snapshots, pendingCount, canReview } = props;\n return (\n <header class={reviewHeader}>\n <div class={headerMain}>\n <HeaderTitle build={build} snapshots={snapshots} />\n </div>\n <div class={headerSide}>\n <HeaderProgress snapshots={snapshots} />\n <div class={headerActions}>\n <Button\n variant=\"secondary\"\n size=\"sm\"\n href={`/projects/${project.slug}/builds/${build.id}`}\n >\n Build overview\n </Button>\n {canReview && pendingCount > 0 ? (\n <BulkActions project={project} build={build} pendingCount={pendingCount} />\n ) : null}\n </div>\n </div>\n </header>\n );\n}\n"],"mappings":"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"}