@storyshelf/app 0.5.3

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 (268) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +18 -0
  3. package/dist/app-types.d.mts +54 -0
  4. package/dist/asset-manifest.mjs +41 -0
  5. package/dist/asset-manifest.mjs.map +1 -0
  6. package/dist/asset-paths.mjs +23 -0
  7. package/dist/asset-paths.mjs.map +1 -0
  8. package/dist/assets/htmx.min.js +3304 -0
  9. package/dist/assets/mark.svg +1 -0
  10. package/dist/capture-setup.mjs +139 -0
  11. package/dist/capture-setup.mjs.map +1 -0
  12. package/dist/credential-keys.mjs +20 -0
  13. package/dist/credential-keys.mjs.map +1 -0
  14. package/dist/credentials.mjs +56 -0
  15. package/dist/credentials.mjs.map +1 -0
  16. package/dist/index.d.mts +18 -0
  17. package/dist/index.mjs +310 -0
  18. package/dist/index.mjs.map +1 -0
  19. package/dist/lifecycle.mjs +132 -0
  20. package/dist/lifecycle.mjs.map +1 -0
  21. package/dist/middleware/auth-gate.mjs +36 -0
  22. package/dist/middleware/auth-gate.mjs.map +1 -0
  23. package/dist/middleware/csrf.mjs +75 -0
  24. package/dist/middleware/csrf.mjs.map +1 -0
  25. package/dist/middleware/rate-limit.mjs +68 -0
  26. package/dist/middleware/rate-limit.mjs.map +1 -0
  27. package/dist/middleware/request-log.mjs +30 -0
  28. package/dist/middleware/request-log.mjs.map +1 -0
  29. package/dist/middleware/server-timing.mjs +39 -0
  30. package/dist/middleware/server-timing.mjs.map +1 -0
  31. package/dist/middleware/setup-gate.mjs +37 -0
  32. package/dist/middleware/setup-gate.mjs.map +1 -0
  33. package/dist/middleware/store-scope.mjs +33 -0
  34. package/dist/middleware/store-scope.mjs.map +1 -0
  35. package/dist/notify.d.mts +17 -0
  36. package/dist/notify.mjs +141 -0
  37. package/dist/notify.mjs.map +1 -0
  38. package/dist/pages/admin-system.mjs +243 -0
  39. package/dist/pages/admin-system.mjs.map +1 -0
  40. package/dist/pages/build-detail.mjs +392 -0
  41. package/dist/pages/build-detail.mjs.map +1 -0
  42. package/dist/pages/build-diff-actions.mjs +162 -0
  43. package/dist/pages/build-diff-actions.mjs.map +1 -0
  44. package/dist/pages/build-diff-comments.mjs +104 -0
  45. package/dist/pages/build-diff-comments.mjs.map +1 -0
  46. package/dist/pages/build-diff-header.mjs +117 -0
  47. package/dist/pages/build-diff-header.mjs.map +1 -0
  48. package/dist/pages/build-diff-model.mjs +48 -0
  49. package/dist/pages/build-diff-model.mjs.map +1 -0
  50. package/dist/pages/build-diff-nav.mjs +122 -0
  51. package/dist/pages/build-diff-nav.mjs.map +1 -0
  52. package/dist/pages/build-diff-shortcuts.mjs +78 -0
  53. package/dist/pages/build-diff-shortcuts.mjs.map +1 -0
  54. package/dist/pages/build-diff-stale.mjs +70 -0
  55. package/dist/pages/build-diff-stale.mjs.map +1 -0
  56. package/dist/pages/build-diff-viewer.mjs +70 -0
  57. package/dist/pages/build-diff-viewer.mjs.map +1 -0
  58. package/dist/pages/build-diff.mjs +110 -0
  59. package/dist/pages/build-diff.mjs.map +1 -0
  60. package/dist/pages/compute-jobs.mjs +165 -0
  61. package/dist/pages/compute-jobs.mjs.map +1 -0
  62. package/dist/pages/invite.mjs +91 -0
  63. package/dist/pages/invite.mjs.map +1 -0
  64. package/dist/pages/label-detail.mjs +154 -0
  65. package/dist/pages/label-detail.mjs.map +1 -0
  66. package/dist/pages/library.mjs +343 -0
  67. package/dist/pages/library.mjs.map +1 -0
  68. package/dist/pages/login.mjs +117 -0
  69. package/dist/pages/login.mjs.map +1 -0
  70. package/dist/pages/passkey-ceremony.mjs +97 -0
  71. package/dist/pages/passkey-ceremony.mjs.map +1 -0
  72. package/dist/pages/profile.mjs +364 -0
  73. package/dist/pages/profile.mjs.map +1 -0
  74. package/dist/pages/project-builds-table.mjs +151 -0
  75. package/dist/pages/project-builds-table.mjs.map +1 -0
  76. package/dist/pages/project-builds.mjs +143 -0
  77. package/dist/pages/project-builds.mjs.map +1 -0
  78. package/dist/pages/project-create.mjs +90 -0
  79. package/dist/pages/project-create.mjs.map +1 -0
  80. package/dist/pages/project-settings.mjs +95 -0
  81. package/dist/pages/project-settings.mjs.map +1 -0
  82. package/dist/pages/projects-card.mjs +194 -0
  83. package/dist/pages/projects-card.mjs.map +1 -0
  84. package/dist/pages/projects.mjs +129 -0
  85. package/dist/pages/projects.mjs.map +1 -0
  86. package/dist/pages/root.mjs +98 -0
  87. package/dist/pages/root.mjs.map +1 -0
  88. package/dist/pages/settings-general.mjs +138 -0
  89. package/dist/pages/settings-general.mjs.map +1 -0
  90. package/dist/pages/settings-labels.mjs +107 -0
  91. package/dist/pages/settings-labels.mjs.map +1 -0
  92. package/dist/pages/settings-members.mjs +192 -0
  93. package/dist/pages/settings-members.mjs.map +1 -0
  94. package/dist/pages/settings-notifications.mjs +240 -0
  95. package/dist/pages/settings-notifications.mjs.map +1 -0
  96. package/dist/pages/settings-status.mjs +137 -0
  97. package/dist/pages/settings-status.mjs.map +1 -0
  98. package/dist/pages/settings-tests.mjs +111 -0
  99. package/dist/pages/settings-tests.mjs.map +1 -0
  100. package/dist/pages/settings-tokens.mjs +90 -0
  101. package/dist/pages/settings-tokens.mjs.map +1 -0
  102. package/dist/pages/settings-webhooks.mjs +104 -0
  103. package/dist/pages/settings-webhooks.mjs.map +1 -0
  104. package/dist/pages/storybook.mjs +116 -0
  105. package/dist/pages/storybook.mjs.map +1 -0
  106. package/dist/retention-timer.mjs +65 -0
  107. package/dist/retention-timer.mjs.map +1 -0
  108. package/dist/routers/admin-pages.mjs +48 -0
  109. package/dist/routers/admin-pages.mjs.map +1 -0
  110. package/dist/routers/admin.mjs +113 -0
  111. package/dist/routers/admin.mjs.map +1 -0
  112. package/dist/routers/assets.mjs +64 -0
  113. package/dist/routers/assets.mjs.map +1 -0
  114. package/dist/routers/attempts.mjs +96 -0
  115. package/dist/routers/attempts.mjs.map +1 -0
  116. package/dist/routers/auth-sync.mjs +64 -0
  117. package/dist/routers/auth-sync.mjs.map +1 -0
  118. package/dist/routers/auth.mjs +236 -0
  119. package/dist/routers/auth.mjs.map +1 -0
  120. package/dist/routers/baseline-guard.mjs +83 -0
  121. package/dist/routers/baseline-guard.mjs.map +1 -0
  122. package/dist/routers/builds.handlers.mjs +217 -0
  123. package/dist/routers/builds.handlers.mjs.map +1 -0
  124. package/dist/routers/builds.mjs +381 -0
  125. package/dist/routers/builds.mjs.map +1 -0
  126. package/dist/routers/comments.mjs +109 -0
  127. package/dist/routers/comments.mjs.map +1 -0
  128. package/dist/routers/flash.mjs +35 -0
  129. package/dist/routers/flash.mjs.map +1 -0
  130. package/dist/routers/health-report.mjs +159 -0
  131. package/dist/routers/health-report.mjs.map +1 -0
  132. package/dist/routers/health.mjs +46 -0
  133. package/dist/routers/health.mjs.map +1 -0
  134. package/dist/routers/helpers.mjs +110 -0
  135. package/dist/routers/helpers.mjs.map +1 -0
  136. package/dist/routers/htmx.mjs +24 -0
  137. package/dist/routers/htmx.mjs.map +1 -0
  138. package/dist/routers/labels.mjs +118 -0
  139. package/dist/routers/labels.mjs.map +1 -0
  140. package/dist/routers/media.mjs +53 -0
  141. package/dist/routers/media.mjs.map +1 -0
  142. package/dist/routers/members.mjs +177 -0
  143. package/dist/routers/members.mjs.map +1 -0
  144. package/dist/routers/notification-channels.mjs +190 -0
  145. package/dist/routers/notification-channels.mjs.map +1 -0
  146. package/dist/routers/notification-subscriptions.mjs +120 -0
  147. package/dist/routers/notification-subscriptions.mjs.map +1 -0
  148. package/dist/routers/profile.mjs +276 -0
  149. package/dist/routers/profile.mjs.map +1 -0
  150. package/dist/routers/projects.mjs +133 -0
  151. package/dist/routers/projects.mjs.map +1 -0
  152. package/dist/routers/review-next.mjs +47 -0
  153. package/dist/routers/review-next.mjs.map +1 -0
  154. package/dist/routers/schemas.mjs +337 -0
  155. package/dist/routers/schemas.mjs.map +1 -0
  156. package/dist/routers/settings-general.mjs +104 -0
  157. package/dist/routers/settings-general.mjs.map +1 -0
  158. package/dist/routers/settings-labels.mjs +52 -0
  159. package/dist/routers/settings-labels.mjs.map +1 -0
  160. package/dist/routers/settings-members.mjs +58 -0
  161. package/dist/routers/settings-members.mjs.map +1 -0
  162. package/dist/routers/settings-notifications.mjs +108 -0
  163. package/dist/routers/settings-notifications.mjs.map +1 -0
  164. package/dist/routers/settings-status.mjs +77 -0
  165. package/dist/routers/settings-status.mjs.map +1 -0
  166. package/dist/routers/settings-tokens.mjs +40 -0
  167. package/dist/routers/settings-tokens.mjs.map +1 -0
  168. package/dist/routers/settings-webhooks.mjs +67 -0
  169. package/dist/routers/settings-webhooks.mjs.map +1 -0
  170. package/dist/routers/settings.handlers.mjs +149 -0
  171. package/dist/routers/settings.handlers.mjs.map +1 -0
  172. package/dist/routers/settings.mjs +25 -0
  173. package/dist/routers/settings.mjs.map +1 -0
  174. package/dist/routers/snapshots.mjs +240 -0
  175. package/dist/routers/snapshots.mjs.map +1 -0
  176. package/dist/routers/status-configs.mjs +108 -0
  177. package/dist/routers/status-configs.mjs.map +1 -0
  178. package/dist/routers/storybook.mjs +171 -0
  179. package/dist/routers/storybook.mjs.map +1 -0
  180. package/dist/routers/tokens.mjs +91 -0
  181. package/dist/routers/tokens.mjs.map +1 -0
  182. package/dist/routers/ui.mjs +166 -0
  183. package/dist/routers/ui.mjs.map +1 -0
  184. package/dist/routers/webhooks.mjs +97 -0
  185. package/dist/routers/webhooks.mjs.map +1 -0
  186. package/dist/routers/well-known.mjs +43 -0
  187. package/dist/routers/well-known.mjs.map +1 -0
  188. package/dist/runtime.mjs +52 -0
  189. package/dist/runtime.mjs.map +1 -0
  190. package/dist/store.mjs +20 -0
  191. package/dist/store.mjs.map +1 -0
  192. package/dist/ui/avatar.mjs +76 -0
  193. package/dist/ui/avatar.mjs.map +1 -0
  194. package/dist/ui/buttons.mjs +205 -0
  195. package/dist/ui/buttons.mjs.map +1 -0
  196. package/dist/ui/code-block.mjs +47 -0
  197. package/dist/ui/code-block.mjs.map +1 -0
  198. package/dist/ui/compare-stage.mjs +344 -0
  199. package/dist/ui/compare-stage.mjs.map +1 -0
  200. package/dist/ui/csrf-field.mjs +20 -0
  201. package/dist/ui/csrf-field.mjs.map +1 -0
  202. package/dist/ui/css.mjs +36 -0
  203. package/dist/ui/css.mjs.map +1 -0
  204. package/dist/ui/document.mjs +130 -0
  205. package/dist/ui/document.mjs.map +1 -0
  206. package/dist/ui/dropdown.mjs +181 -0
  207. package/dist/ui/dropdown.mjs.map +1 -0
  208. package/dist/ui/feedback.mjs +341 -0
  209. package/dist/ui/feedback.mjs.map +1 -0
  210. package/dist/ui/filter-input.mjs +58 -0
  211. package/dist/ui/filter-input.mjs.map +1 -0
  212. package/dist/ui/forms.mjs +291 -0
  213. package/dist/ui/forms.mjs.map +1 -0
  214. package/dist/ui/icons/icon.mjs +53 -0
  215. package/dist/ui/icons/icon.mjs.map +1 -0
  216. package/dist/ui/icons/paths.mjs +61 -0
  217. package/dist/ui/icons/paths.mjs.map +1 -0
  218. package/dist/ui/icons/sprite.mjs +26 -0
  219. package/dist/ui/icons/sprite.mjs.map +1 -0
  220. package/dist/ui/kbd.mjs +33 -0
  221. package/dist/ui/kbd.mjs.map +1 -0
  222. package/dist/ui/layout.mjs +173 -0
  223. package/dist/ui/layout.mjs.map +1 -0
  224. package/dist/ui/progress.mjs +44 -0
  225. package/dist/ui/progress.mjs.map +1 -0
  226. package/dist/ui/segmented.mjs +77 -0
  227. package/dist/ui/segmented.mjs.map +1 -0
  228. package/dist/ui/shell/account.mjs +161 -0
  229. package/dist/ui/shell/account.mjs.map +1 -0
  230. package/dist/ui/shell/bare.mjs +69 -0
  231. package/dist/ui/shell/bare.mjs.map +1 -0
  232. package/dist/ui/shell/boot-script.mjs +46 -0
  233. package/dist/ui/shell/boot-script.mjs.map +1 -0
  234. package/dist/ui/shell/client-script.mjs +259 -0
  235. package/dist/ui/shell/client-script.mjs.map +1 -0
  236. package/dist/ui/shell/nav.mjs +111 -0
  237. package/dist/ui/shell/nav.mjs.map +1 -0
  238. package/dist/ui/shell/review-script.mjs +195 -0
  239. package/dist/ui/shell/review-script.mjs.map +1 -0
  240. package/dist/ui/shell/sidebar.mjs +323 -0
  241. package/dist/ui/shell/sidebar.mjs.map +1 -0
  242. package/dist/ui/shell/topbar.mjs +268 -0
  243. package/dist/ui/shell/topbar.mjs.map +1 -0
  244. package/dist/ui/stacks.mjs +82 -0
  245. package/dist/ui/stacks.mjs.map +1 -0
  246. package/dist/ui/styles/base.mjs +41 -0
  247. package/dist/ui/styles/base.mjs.map +1 -0
  248. package/dist/ui/styles/behavior.mjs +43 -0
  249. package/dist/ui/styles/behavior.mjs.map +1 -0
  250. package/dist/ui/styles/review-layout.mjs +347 -0
  251. package/dist/ui/styles/review-layout.mjs.map +1 -0
  252. package/dist/ui/styles/review.mjs +86 -0
  253. package/dist/ui/styles/review.mjs.map +1 -0
  254. package/dist/ui/styles/tokens.mjs +132 -0
  255. package/dist/ui/styles/tokens.mjs.map +1 -0
  256. package/dist/ui/styles.mjs +19 -0
  257. package/dist/ui/styles.mjs.map +1 -0
  258. package/dist/ui/subnav.mjs +91 -0
  259. package/dist/ui/subnav.mjs.map +1 -0
  260. package/dist/ui/table.mjs +73 -0
  261. package/dist/ui/table.mjs.map +1 -0
  262. package/dist/ui/theme.mjs +64 -0
  263. package/dist/ui/theme.mjs.map +1 -0
  264. package/dist/ui/thumbnail.mjs +51 -0
  265. package/dist/ui/thumbnail.mjs.map +1 -0
  266. package/dist/ui/time.mjs +50 -0
  267. package/dist/ui/time.mjs.map +1 -0
  268. package/package.json +92 -0
@@ -0,0 +1,392 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { getStore } from "../store.mjs";
5
+ import { Button } from "../ui/buttons.mjs";
6
+ import { Badge, EmptyState, Meta, Stat, statusTone } from "../ui/feedback.mjs";
7
+ import { TextareaField } from "../ui/forms.mjs";
8
+ import { Card, PageHeader, SectionTitle } from "../ui/layout.mjs";
9
+ import { HStack, VStack } from "../ui/stacks.mjs";
10
+ import { Thumbnail } from "../ui/thumbnail.mjs";
11
+ import { DocumentLayout } from "../ui/document.mjs";
12
+ import { loadBaselineView } from "../routers/baseline-guard.mjs";
13
+ import { reviewComment, reviewCommentBody, reviewCommentHead, snapshotCard, snapshotCardBody, snapshotCardHead, snapshotCardMeta, snapshotGrid } from "../ui/styles/review.mjs";
14
+ import { StaleBaselineNotice } from "./build-diff-stale.mjs";
15
+ import { BuildModel, CaptureAttemptModel, CaptureLogModel, CommentModel, ProjectModel, SnapshotModel } from "@storyshelf/core/models";
16
+ import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
17
+ import { createUrlBuilder } from "@storyshelf/core/urls";
18
+ //#region src/pages/build-detail.tsx
19
+ /** Map a capture log level to its badge tone. */
20
+ function logTone(level) {
21
+ if (level === "error") return "danger";
22
+ if (level === "warn") return "warning";
23
+ if (level === "info") return "info";
24
+ return "neutral";
25
+ }
26
+ /** Build overview page: snapshot grid, bulk actions, and build comments. */
27
+ async function renderBuildDetailPage(buildId) {
28
+ const { db } = getStore();
29
+ const build = await new BuildModel(db).get(buildId);
30
+ if (!build) return null;
31
+ const project = await new ProjectModel(db).get(build.projectId);
32
+ if (!project) return null;
33
+ const snapshots = await new SnapshotModel(db).listByBuild(build.id);
34
+ const comments = await new CommentModel(db).listByBuild(build.id);
35
+ const attempts = await new CaptureAttemptModel(db).listByBuild(build.id);
36
+ const logs = new CaptureLogModel(db);
37
+ const attemptLogs = /* @__PURE__ */ new Map();
38
+ for (const attempt of attempts) attemptLogs.set(attempt.id, await logs.listByAttempt(attempt.id));
39
+ const canReview = !getStore().authEnabled || Boolean(getStore().user);
40
+ const { drifted } = await loadBaselineView(project, build, snapshots);
41
+ const staleCount = Object.keys(drifted).length;
42
+ const urls = createUrlBuilder("/", getStore().config.publishedBaseDomain);
43
+ const grouped = /* @__PURE__ */ new Map();
44
+ for (const snap of snapshots) {
45
+ const key = snap.status;
46
+ const list = grouped.get(key) ?? [];
47
+ list.push(snap);
48
+ grouped.set(key, list);
49
+ }
50
+ const inheritedCount = snapshots.filter((snap) => snap.inherited).length;
51
+ const selective = build.affectedOnly && build.affectedImportPaths !== null;
52
+ return /* @__PURE__ */ jsxs(DocumentLayout, {
53
+ title: `Build ${build.gitBranch}`,
54
+ nav: {
55
+ active: "builds",
56
+ projectSlug: project.slug,
57
+ projectName: project.name,
58
+ trail: [{ label: `${build.gitBranch} · ${build.gitSha.slice(0, 7)}` }]
59
+ },
60
+ children: [
61
+ /* @__PURE__ */ jsx(PageHeader, {
62
+ title: /* @__PURE__ */ jsxs(Fragment, { children: [
63
+ build.gitBranch,
64
+ " ",
65
+ /* @__PURE__ */ jsxs(Meta, {
66
+ as: "span",
67
+ children: ["· ", build.gitSha.slice(0, 7)]
68
+ }),
69
+ " ",
70
+ /* @__PURE__ */ jsx(Badge, {
71
+ tone: statusTone(build.status),
72
+ children: build.status
73
+ }),
74
+ selective ? /* @__PURE__ */ jsxs(Fragment, { children: [" ", /* @__PURE__ */ jsx(Badge, {
75
+ tone: "info",
76
+ children: "affected capture"
77
+ })] }) : null,
78
+ !build.affectedOnly ? /* @__PURE__ */ jsxs(Fragment, { children: [" ", /* @__PURE__ */ jsx(Badge, {
79
+ tone: "neutral",
80
+ children: "full capture"
81
+ })] }) : null
82
+ ] }),
83
+ description: /* @__PURE__ */ jsxs(Fragment, { children: [
84
+ build.message ?? "No message",
85
+ " ",
86
+ build.authorName ? `· ${build.authorName}${build.authorEmail ? ` <${build.authorEmail}>` : ""}` : "",
87
+ " ",
88
+ "· ",
89
+ new Date(build.createdAt).toLocaleString(),
90
+ selective ? /* @__PURE__ */ jsxs(Fragment, { children: [
91
+ " ",
92
+ "· ",
93
+ snapshots.length - inheritedCount,
94
+ " rendered, ",
95
+ inheritedCount,
96
+ " inherited"
97
+ ] }) : null
98
+ ] }),
99
+ actions: /* @__PURE__ */ jsxs(Fragment, { children: [
100
+ /* @__PURE__ */ jsx(Button, {
101
+ variant: "primary",
102
+ href: urls.buildDiff(project.slug, build.id),
103
+ children: "Review diffs"
104
+ }),
105
+ /* @__PURE__ */ jsx(Button, {
106
+ variant: "ghost",
107
+ href: urls.short(build.id),
108
+ children: "View Storybook"
109
+ }),
110
+ /* @__PURE__ */ jsx("form", {
111
+ method: "post",
112
+ action: `/api/v1/projects/${project.slug}/builds/${build.id}/retry`,
113
+ "hx-post": `/api/v1/projects/${project.slug}/builds/${build.id}/retry`,
114
+ "hx-target": "body",
115
+ children: /* @__PURE__ */ jsx(Button, {
116
+ variant: "secondary",
117
+ type: "submit",
118
+ children: "Retry capture"
119
+ })
120
+ })
121
+ ] })
122
+ }),
123
+ staleCount > 0 ? /* @__PURE__ */ jsx(StaleBaselineNotice, {
124
+ project,
125
+ build,
126
+ staleCount,
127
+ canReview
128
+ }) : null,
129
+ /* @__PURE__ */ jsx("div", {
130
+ class: "mb-1",
131
+ children: /* @__PURE__ */ jsxs("div", {
132
+ class: "grid grid--3",
133
+ children: [
134
+ /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx(Stat, {
135
+ label: "Snapshots",
136
+ value: snapshots.length
137
+ }) }),
138
+ /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx(Stat, {
139
+ label: "Changed / new",
140
+ value: build.changedCount,
141
+ tone: "warning"
142
+ }) }),
143
+ /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx(Stat, {
144
+ label: "Approved / unchanged",
145
+ value: build.approvedCount,
146
+ tone: "success"
147
+ }) })
148
+ ]
149
+ })
150
+ }),
151
+ attempts.length > 0 ? /* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsx(SectionTitle, { children: "Capture attempts" }), attempts.map((attempt, index) => /* @__PURE__ */ jsxs("details", {
152
+ open: index === attempts.length - 1,
153
+ children: [
154
+ /* @__PURE__ */ jsxs("summary", { children: [
155
+ "Attempt ",
156
+ attempt.attemptNo,
157
+ " ",
158
+ /* @__PURE__ */ jsx(Badge, {
159
+ tone: statusTone(attempt.status),
160
+ children: attempt.status
161
+ }),
162
+ " ",
163
+ /* @__PURE__ */ jsxs(Meta, {
164
+ as: "span",
165
+ children: [
166
+ attempt.storyCount,
167
+ " stories",
168
+ attempt.reqId ? ` · ${attempt.reqId}` : ""
169
+ ]
170
+ })
171
+ ] }),
172
+ attempt.error ? /* @__PURE__ */ jsx(Meta, {
173
+ as: "pre",
174
+ children: attempt.error
175
+ }) : null,
176
+ /* @__PURE__ */ jsx("div", {
177
+ class: "mt-1",
178
+ children: /* @__PURE__ */ jsx(VStack, { children: (attemptLogs.get(attempt.id) ?? []).map((line) => /* @__PURE__ */ jsxs(HStack, {
179
+ align: "baseline",
180
+ wrap: false,
181
+ children: [
182
+ /* @__PURE__ */ jsx(Badge, {
183
+ tone: logTone(line.level),
184
+ children: line.level
185
+ }),
186
+ /* @__PURE__ */ jsx("span", { children: line.message }),
187
+ line.fields ? /* @__PURE__ */ jsx(Meta, {
188
+ as: "code",
189
+ children: line.fields
190
+ }) : null
191
+ ]
192
+ }, line.id)) })
193
+ })
194
+ ]
195
+ }, attempt.id))] }) : null,
196
+ canReview && (build.status === "reviewing" || build.status === "comparing") ? /* @__PURE__ */ jsx("div", {
197
+ class: "mb-1",
198
+ children: /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(HStack, { children: [
199
+ /* @__PURE__ */ jsx(Meta, {
200
+ as: "span",
201
+ children: "Bulk actions:"
202
+ }),
203
+ /* @__PURE__ */ jsx("form", {
204
+ method: "post",
205
+ action: `/api/v1/projects/${project.slug}/builds/${build.id}/approve-all`,
206
+ "hx-post": `/api/v1/projects/${project.slug}/builds/${build.id}/approve-all`,
207
+ "hx-target": "body",
208
+ "hx-confirm": "Approve all remaining changes?",
209
+ "data-toast": "Approved all remaining changes",
210
+ children: /* @__PURE__ */ jsx(Button, {
211
+ variant: "primary",
212
+ size: "sm",
213
+ type: "submit",
214
+ children: "Approve all"
215
+ })
216
+ }),
217
+ /* @__PURE__ */ jsx("form", {
218
+ method: "post",
219
+ action: `/api/v1/projects/${project.slug}/builds/${build.id}/reject-all`,
220
+ "hx-post": `/api/v1/projects/${project.slug}/builds/${build.id}/reject-all`,
221
+ "hx-target": "body",
222
+ "hx-confirm": "Reject all remaining changes?",
223
+ "data-toast": "Rejected all remaining changes",
224
+ "data-toast-tone": "warning",
225
+ children: /* @__PURE__ */ jsx(Button, {
226
+ variant: "danger",
227
+ size: "sm",
228
+ type: "submit",
229
+ children: "Reject all"
230
+ })
231
+ }),
232
+ /* @__PURE__ */ jsx(Meta, {
233
+ as: "span",
234
+ children: "Or review individually in the diff view."
235
+ })
236
+ ] }) })
237
+ }) : null,
238
+ snapshots.length === 0 ? /* @__PURE__ */ jsx(EmptyState, {
239
+ title: "No snapshots yet",
240
+ description: "Capture is pending or failed. Try retrying capture."
241
+ }) : /* @__PURE__ */ jsx("div", {
242
+ class: snapshotGrid,
243
+ children: snapshots.map((snap) => /* @__PURE__ */ jsxs("div", {
244
+ class: snapshotCard,
245
+ children: [/* @__PURE__ */ jsxs("div", {
246
+ class: snapshotCardHead,
247
+ children: [
248
+ /* @__PURE__ */ jsx(Badge, {
249
+ tone: statusTone(snap.status),
250
+ children: snap.status
251
+ }),
252
+ snap.inherited ? /* @__PURE__ */ jsxs(Fragment, { children: [" ", /* @__PURE__ */ jsx(Badge, {
253
+ tone: "neutral",
254
+ children: "inherited"
255
+ })] }) : null,
256
+ /* @__PURE__ */ jsxs("span", {
257
+ class: snapshotCardMeta,
258
+ children: [
259
+ snap.viewportName,
260
+ " · ",
261
+ snap.viewportWidth,
262
+ "×",
263
+ snap.viewportHeight
264
+ ]
265
+ })
266
+ ]
267
+ }), /* @__PURE__ */ jsxs("div", {
268
+ class: snapshotCardBody,
269
+ children: [
270
+ /* @__PURE__ */ jsx("a", {
271
+ href: `${urls.buildDiff(project.slug, build.id)}?snapshot=${snap.id}`,
272
+ children: /* @__PURE__ */ jsx(Thumbnail, {
273
+ src: `/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${snap.id}/image`,
274
+ alt: `${snap.storyTitle} / ${snap.storyName}`
275
+ })
276
+ }),
277
+ /* @__PURE__ */ jsxs(SectionTitle, { children: [
278
+ snap.storyTitle,
279
+ " ",
280
+ /* @__PURE__ */ jsxs(Meta, {
281
+ as: "span",
282
+ children: ["/ ", snap.storyName]
283
+ })
284
+ ] }),
285
+ /* @__PURE__ */ jsxs(Meta, {
286
+ as: "div",
287
+ children: [
288
+ snap.storyId,
289
+ snap.diffRatio !== null && snap.diffRatio !== void 0 ? ` · diff ${(snap.diffRatio * 100).toFixed(2)}%` : "",
290
+ snap.diffPixels === null || snap.diffPixels === void 0 ? "" : ` · ${snap.diffPixels} px`
291
+ ]
292
+ }),
293
+ /* @__PURE__ */ jsxs(HStack, { children: [
294
+ /* @__PURE__ */ jsx(Button, {
295
+ variant: "secondary",
296
+ size: "sm",
297
+ href: `${urls.buildDiff(project.slug, build.id)}?snapshot=${snap.id}`,
298
+ children: "Review"
299
+ }),
300
+ /* @__PURE__ */ jsx(Button, {
301
+ variant: "ghost",
302
+ size: "sm",
303
+ href: urls.short(build.id),
304
+ children: "View Storybook"
305
+ }),
306
+ canReview && (snap.status === "new" || snap.status === "changed") ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("form", {
307
+ method: "post",
308
+ action: `/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${snap.id}/approve`,
309
+ "hx-post": `/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${snap.id}/approve`,
310
+ "hx-target": "body",
311
+ "data-toast": `Approved ${snap.storyName}`,
312
+ children: /* @__PURE__ */ jsx(Button, {
313
+ variant: "ghost",
314
+ size: "sm",
315
+ type: "submit",
316
+ children: "Approve"
317
+ })
318
+ }), /* @__PURE__ */ jsx("form", {
319
+ method: "post",
320
+ action: `/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${snap.id}/reject`,
321
+ "hx-post": `/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${snap.id}/reject`,
322
+ "hx-target": "body",
323
+ "data-toast": `Rejected ${snap.storyName}`,
324
+ "data-toast-tone": "warning",
325
+ children: /* @__PURE__ */ jsx(Button, {
326
+ variant: "ghost",
327
+ size: "sm",
328
+ type: "submit",
329
+ children: "Reject"
330
+ })
331
+ })] }) : null
332
+ ] })
333
+ ]
334
+ })]
335
+ }, snap.id))
336
+ }),
337
+ /* @__PURE__ */ jsx("div", {
338
+ class: "mt-1",
339
+ children: /* @__PURE__ */ jsxs(Card, { children: [
340
+ /* @__PURE__ */ jsx(SectionTitle, { children: "Comments" }),
341
+ comments.length === 0 ? /* @__PURE__ */ jsx(Meta, { children: "No comments. Add one in the diff review page for a specific snapshot." }) : null,
342
+ /* @__PURE__ */ jsx(VStack, { children: comments.map((comment) => /* @__PURE__ */ jsxs("div", {
343
+ class: reviewComment,
344
+ children: [/* @__PURE__ */ jsxs("div", {
345
+ class: reviewCommentHead,
346
+ children: [
347
+ /* @__PURE__ */ jsx("strong", { children: comment.userId ?? "anonymous" }),
348
+ /* @__PURE__ */ jsxs("span", { children: ["· ", new Date(comment.createdAt).toLocaleString()] }),
349
+ comment.snapshotId ? /* @__PURE__ */ jsx(Badge, {
350
+ tone: "neutral",
351
+ children: snapshots.find((snapshot) => snapshot.id === comment.snapshotId)?.storyName ?? comment.snapshotId.slice(0, 6)
352
+ }) : /* @__PURE__ */ jsx(Badge, {
353
+ tone: "neutral",
354
+ children: "build"
355
+ }),
356
+ comment.resolved ? /* @__PURE__ */ jsx(Badge, {
357
+ tone: "success",
358
+ children: "resolved"
359
+ }) : null
360
+ ]
361
+ }), /* @__PURE__ */ jsx("p", {
362
+ class: reviewCommentBody,
363
+ children: comment.body
364
+ })]
365
+ }, comment.id)) }),
366
+ /* @__PURE__ */ jsx("form", {
367
+ method: "post",
368
+ action: `/api/v1/projects/${project.slug}/builds/${build.id}/comments`,
369
+ "hx-post": `/api/v1/projects/${project.slug}/builds/${build.id}/comments`,
370
+ "hx-target": "body",
371
+ class: "mt-1 max-w-prose",
372
+ children: /* @__PURE__ */ jsxs(VStack, { children: [/* @__PURE__ */ jsx(TextareaField, {
373
+ label: "Add build comment",
374
+ name: "body",
375
+ rows: 3,
376
+ required: true,
377
+ placeholder: "Leave a comment on this build…"
378
+ }), /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(Button, {
379
+ variant: "primary",
380
+ type: "submit",
381
+ children: "Comment"
382
+ }) })] })
383
+ })
384
+ ] })
385
+ })
386
+ ]
387
+ });
388
+ }
389
+ //#endregion
390
+ export { renderBuildDetailPage };
391
+
392
+ //# sourceMappingURL=build-detail.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-detail.mjs","names":[],"sources":["../../src/pages/build-detail.tsx"],"sourcesContent":["import { BuildModel } from \"@storyshelf/core/models\";\nimport { CaptureAttemptModel } from \"@storyshelf/core/models\";\nimport { CaptureLogModel } from \"@storyshelf/core/models\";\nimport { CommentModel } from \"@storyshelf/core/models\";\nimport { ProjectModel } from \"@storyshelf/core/models\";\nimport { SnapshotModel } from \"@storyshelf/core/models\";\nimport { createUrlBuilder } from \"@storyshelf/core/urls\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport { loadBaselineView } from \"../routers/baseline-guard.ts\";\nimport { getStore } from \"../store.ts\";\nimport {\n Badge,\n Button,\n Card,\n EmptyState,\n HStack,\n Meta,\n PageHeader,\n SectionTitle,\n Stat,\n TextareaField,\n Thumbnail,\n VStack,\n statusTone,\n} from \"../ui/components.tsx\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\nimport {\n reviewComment,\n reviewCommentBody,\n reviewCommentHead,\n snapshotCard,\n snapshotCardBody,\n snapshotCardHead,\n snapshotCardMeta,\n snapshotGrid,\n} from \"../ui/styles/review.ts\";\nimport { StaleBaselineNotice } from \"./build-diff-stale.tsx\";\n\n/** Map a capture log level to its badge tone. */\nfunction logTone(level: string): \"neutral\" | \"success\" | \"warning\" | \"danger\" | \"info\" {\n if (level === \"error\") {\n return \"danger\";\n }\n if (level === \"warn\") {\n return \"warning\";\n }\n if (level === \"info\") {\n return \"info\";\n }\n return \"neutral\";\n}\n/** Build overview page: snapshot grid, bulk actions, and build comments. */\nexport async function renderBuildDetailPage(buildId: string): Promise<RenderedContent | null> {\n const { db } = getStore();\n const build = await new BuildModel(db).get(buildId);\n if (!build) {\n return null;\n }\n const project = await new ProjectModel(db).get(build.projectId);\n if (!project) {\n return null;\n }\n const snapshots = await new SnapshotModel(db).listByBuild(build.id);\n const comments = await new CommentModel(db).listByBuild(build.id);\n const attempts = await new CaptureAttemptModel(db).listByBuild(build.id);\n const logs = new CaptureLogModel(db);\n const attemptLogs = new Map<string, Awaited<ReturnType<typeof logs.listByAttempt>>>();\n /* oxlint-disable-next-line eslint/no-await-in-loop -- attempts are few; sequential reads keep code simple */\n for (const attempt of attempts) {\n attemptLogs.set(attempt.id, await logs.listByAttempt(attempt.id));\n }\n const canReview = !getStore().authEnabled || Boolean(getStore().user);\n const { drifted } = await loadBaselineView(project, build, snapshots);\n const staleCount = Object.keys(drifted).length;\n const urls = createUrlBuilder(\"/\", getStore().config.publishedBaseDomain);\n\n const grouped = new Map<string, typeof snapshots>();\n for (const snap of snapshots) {\n const key = snap.status;\n const list = grouped.get(key) ?? [];\n list.push(snap);\n grouped.set(key, list);\n }\n const inheritedCount = snapshots.filter((snap) => snap.inherited).length;\n const selective = build.affectedOnly && build.affectedImportPaths !== null;\n\n return (\n <DocumentLayout\n title={`Build ${build.gitBranch}`}\n nav={{\n active: \"builds\",\n projectSlug: project.slug,\n projectName: project.name,\n trail: [{ label: `${build.gitBranch} · ${build.gitSha.slice(0, 7)}` }],\n }}\n >\n <PageHeader\n title={\n <>\n {build.gitBranch} <Meta as=\"span\">· {build.gitSha.slice(0, 7)}</Meta>{\" \"}\n <Badge tone={statusTone(build.status)}>{build.status}</Badge>\n {selective ? (\n <>\n {\" \"}\n <Badge tone=\"info\">affected capture</Badge>\n </>\n ) : null}\n {!build.affectedOnly ? (\n <>\n {\" \"}\n <Badge tone=\"neutral\">full capture</Badge>\n </>\n ) : null}\n </>\n }\n description={\n <>\n {build.message ?? \"No message\"}{\" \"}\n {build.authorName\n ? `· ${build.authorName}${build.authorEmail ? ` <${build.authorEmail}>` : \"\"}`\n : \"\"}{\" \"}\n · {new Date(build.createdAt).toLocaleString()}\n {selective ? (\n <>\n {\" \"}\n · {snapshots.length - inheritedCount} rendered, {inheritedCount} inherited\n </>\n ) : null}\n </>\n }\n actions={\n <>\n <Button variant=\"primary\" href={urls.buildDiff(project.slug, build.id)}>\n Review diffs\n </Button>\n <Button variant=\"ghost\" href={urls.short(build.id)}>\n View Storybook\n </Button>\n <form\n method=\"post\"\n action={`/api/v1/projects/${project.slug}/builds/${build.id}/retry`}\n hx-post={`/api/v1/projects/${project.slug}/builds/${build.id}/retry`}\n hx-target=\"body\"\n >\n <Button variant=\"secondary\" type=\"submit\">\n Retry capture\n </Button>\n </form>\n </>\n }\n />\n\n {staleCount > 0 ? (\n <StaleBaselineNotice\n project={project}\n build={build}\n staleCount={staleCount}\n canReview={canReview}\n />\n ) : null}\n\n <div class=\"mb-1\">\n <div class=\"grid grid--3\">\n <Card>\n <Stat label=\"Snapshots\" value={snapshots.length} />\n </Card>\n <Card>\n <Stat label=\"Changed / new\" value={build.changedCount} tone=\"warning\" />\n </Card>\n <Card>\n <Stat label=\"Approved / unchanged\" value={build.approvedCount} tone=\"success\" />\n </Card>\n </div>\n </div>\n\n {attempts.length > 0 ? (\n <Card>\n <SectionTitle>Capture attempts</SectionTitle>\n {attempts.map((attempt, index): HtmlEscapedString | Promise<HtmlEscapedString> => (\n <details key={attempt.id} open={index === attempts.length - 1}>\n <summary>\n Attempt {attempt.attemptNo}{\" \"}\n <Badge tone={statusTone(attempt.status)}>{attempt.status}</Badge>{\" \"}\n <Meta as=\"span\">\n {attempt.storyCount} stories\n {attempt.reqId ? ` · ${attempt.reqId}` : \"\"}\n </Meta>\n </summary>\n {attempt.error ? <Meta as=\"pre\">{attempt.error}</Meta> : null}\n <div class=\"mt-1\">\n <VStack>\n {(attemptLogs.get(attempt.id) ?? []).map(\n (line): HtmlEscapedString | Promise<HtmlEscapedString> => (\n <HStack key={line.id} align=\"baseline\" wrap={false}>\n <Badge tone={logTone(line.level)}>{line.level}</Badge>\n <span>{line.message}</span>\n {line.fields ? <Meta as=\"code\">{line.fields}</Meta> : null}\n </HStack>\n ),\n )}\n </VStack>\n </div>\n </details>\n ))}\n </Card>\n ) : null}\n\n {canReview && (build.status === \"reviewing\" || build.status === \"comparing\") ? (\n <div class=\"mb-1\">\n <Card>\n <HStack>\n <Meta as=\"span\">Bulk actions:</Meta>\n <form\n method=\"post\"\n action={`/api/v1/projects/${project.slug}/builds/${build.id}/approve-all`}\n hx-post={`/api/v1/projects/${project.slug}/builds/${build.id}/approve-all`}\n hx-target=\"body\"\n hx-confirm=\"Approve all remaining changes?\"\n data-toast=\"Approved all remaining changes\"\n >\n <Button variant=\"primary\" size=\"sm\" type=\"submit\">\n Approve all\n </Button>\n </form>\n <form\n method=\"post\"\n action={`/api/v1/projects/${project.slug}/builds/${build.id}/reject-all`}\n hx-post={`/api/v1/projects/${project.slug}/builds/${build.id}/reject-all`}\n hx-target=\"body\"\n hx-confirm=\"Reject all remaining changes?\"\n data-toast=\"Rejected all remaining changes\"\n data-toast-tone=\"warning\"\n >\n <Button variant=\"danger\" size=\"sm\" type=\"submit\">\n Reject all\n </Button>\n </form>\n <Meta as=\"span\">Or review individually in the diff view.</Meta>\n </HStack>\n </Card>\n </div>\n ) : null}\n\n {snapshots.length === 0 ? (\n <EmptyState\n title=\"No snapshots yet\"\n description=\"Capture is pending or failed. Try retrying capture.\"\n />\n ) : (\n <div class={snapshotGrid}>\n {snapshots.map((snap): HtmlEscapedString | Promise<HtmlEscapedString> => (\n <div key={snap.id} class={snapshotCard}>\n <div class={snapshotCardHead}>\n <Badge tone={statusTone(snap.status)}>{snap.status}</Badge>\n {snap.inherited ? (\n <>\n {\" \"}\n <Badge tone=\"neutral\">inherited</Badge>\n </>\n ) : null}\n <span class={snapshotCardMeta}>\n {snap.viewportName} · {snap.viewportWidth}×{snap.viewportHeight}\n </span>\n </div>\n <div class={snapshotCardBody}>\n <a href={`${urls.buildDiff(project.slug, build.id)}?snapshot=${snap.id}`}>\n <Thumbnail\n src={`/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${snap.id}/image`}\n alt={`${snap.storyTitle} / ${snap.storyName}`}\n />\n </a>\n <SectionTitle>\n {snap.storyTitle} <Meta as=\"span\">/ {snap.storyName}</Meta>\n </SectionTitle>\n <Meta as=\"div\">\n {snap.storyId}\n {snap.diffRatio !== null && snap.diffRatio !== undefined\n ? ` · diff ${(snap.diffRatio * 100).toFixed(2)}%`\n : \"\"}\n {snap.diffPixels === null || snap.diffPixels === undefined\n ? \"\"\n : ` · ${snap.diffPixels} px`}\n </Meta>\n <HStack>\n <Button\n variant=\"secondary\"\n size=\"sm\"\n href={`${urls.buildDiff(project.slug, build.id)}?snapshot=${snap.id}`}\n >\n Review\n </Button>\n <Button variant=\"ghost\" size=\"sm\" href={urls.short(build.id)}>\n View Storybook\n </Button>\n {canReview && (snap.status === \"new\" || snap.status === \"changed\") ? (\n <>\n <form\n method=\"post\"\n action={`/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${snap.id}/approve`}\n hx-post={`/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${snap.id}/approve`}\n hx-target=\"body\"\n data-toast={`Approved ${snap.storyName}`}\n >\n <Button variant=\"ghost\" size=\"sm\" type=\"submit\">\n Approve\n </Button>\n </form>\n <form\n method=\"post\"\n action={`/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${snap.id}/reject`}\n hx-post={`/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${snap.id}/reject`}\n hx-target=\"body\"\n data-toast={`Rejected ${snap.storyName}`}\n data-toast-tone=\"warning\"\n >\n <Button variant=\"ghost\" size=\"sm\" type=\"submit\">\n Reject\n </Button>\n </form>\n </>\n ) : null}\n </HStack>\n </div>\n </div>\n ))}\n </div>\n )}\n\n <div class=\"mt-1\">\n <Card>\n <SectionTitle>Comments</SectionTitle>\n {comments.length === 0 ? (\n <Meta>No comments. Add one in the diff review page for a specific snapshot.</Meta>\n ) : null}\n <VStack>\n {comments.map((comment): HtmlEscapedString | Promise<HtmlEscapedString> => (\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.snapshotId ? (\n <Badge tone=\"neutral\">\n {snapshots.find((snapshot) => snapshot.id === comment.snapshotId)\n ?.storyName ?? comment.snapshotId.slice(0, 6)}\n </Badge>\n ) : (\n <Badge tone=\"neutral\">build</Badge>\n )}\n {comment.resolved ? <Badge tone=\"success\">resolved</Badge> : null}\n </div>\n <p class={reviewCommentBody}>{comment.body}</p>\n </div>\n ))}\n </VStack>\n\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 max-w-prose\"\n >\n <VStack>\n <TextareaField\n label=\"Add build comment\"\n name=\"body\"\n rows={3}\n required\n placeholder=\"Leave a comment on this build…\"\n />\n <div>\n <Button variant=\"primary\" type=\"submit\">\n Comment\n </Button>\n </div>\n </VStack>\n </form>\n </Card>\n </div>\n </DocumentLayout>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,162 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { Button } from "../ui/buttons.mjs";
5
+ import { Badge, statusTone } from "../ui/feedback.mjs";
6
+ import { Kbd } from "../ui/kbd.mjs";
7
+ import { actionBar, actionGroup, actionHints, actionPosition } from "../ui/styles/review-layout.mjs";
8
+ import { neighbours, snapshotPageUrl } from "./build-diff-model.mjs";
9
+ import { jsx, jsxs } from "hono/jsx/jsx-runtime";
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}`;
13
+ }
14
+ /** Approve (or "Approve anyway" when the baseline moved) as its own form. */
15
+ function ApproveForm(props) {
16
+ const { project, build, selected, drifted } = props;
17
+ const url = `${snapshotBase(project, build, selected)}/approve${drifted ? "?force=true" : ""}`;
18
+ return /* @__PURE__ */ jsx("form", {
19
+ method: "post",
20
+ action: url,
21
+ "hx-post": url,
22
+ "hx-target": "body",
23
+ "data-toast": `Approved ${selected.storyName}`,
24
+ children: drifted ? /* @__PURE__ */ jsx(Button, {
25
+ variant: "secondary",
26
+ size: "sm",
27
+ type: "submit",
28
+ icon: "check",
29
+ title: "The baseline changed after this diff; replace it with this screenshot",
30
+ children: "Approve anyway"
31
+ }) : /* @__PURE__ */ jsx(Button, {
32
+ variant: "primary",
33
+ size: "sm",
34
+ type: "submit",
35
+ icon: "check",
36
+ "data-approve": true,
37
+ title: "Approve (a)",
38
+ children: "Approve"
39
+ })
40
+ });
41
+ }
42
+ /** Approve/reject for a reviewable snapshot; both advance to the next open one. */
43
+ function DecisionButtons(props) {
44
+ const { project, build, selected } = props;
45
+ const base = snapshotBase(project, build, selected);
46
+ return /* @__PURE__ */ jsxs("div", {
47
+ 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"
63
+ })
64
+ })]
65
+ });
66
+ }
67
+ function StepButton(props) {
68
+ const { target, project, build, direction } = props;
69
+ const label = direction === "prev" ? "Previous snapshot (k)" : "Next snapshot (j)";
70
+ const icon = direction === "prev" ? "chevron-left" : "chevron-right";
71
+ if (!target) return /* @__PURE__ */ jsx(Button, {
72
+ variant: "ghost",
73
+ size: "sm",
74
+ icon,
75
+ disabled: true,
76
+ "aria-label": label
77
+ });
78
+ const url = snapshotPageUrl(project, build, target.id);
79
+ return /* @__PURE__ */ jsx(Button, {
80
+ variant: "ghost",
81
+ size: "sm",
82
+ icon,
83
+ href: url,
84
+ "aria-label": label,
85
+ title: label,
86
+ "hx-get": url,
87
+ "hx-target": "body",
88
+ "hx-push-url": "true",
89
+ ...direction === "prev" ? { "data-snap-prev": true } : { "data-snap-next": true }
90
+ });
91
+ }
92
+ /** Previous/next stepping, position, shortcut hints, and the review decision. */
93
+ function DiffActionBar(props) {
94
+ const { project, build, snapshots, selected, canReview, drifted } = props;
95
+ const around = neighbours(snapshots, selected.id);
96
+ const reviewable = selected.status === "new" || selected.status === "changed";
97
+ return /* @__PURE__ */ jsxs("div", {
98
+ class: actionBar,
99
+ role: "toolbar",
100
+ "aria-label": "Review actions",
101
+ children: [
102
+ /* @__PURE__ */ jsxs("div", {
103
+ class: actionGroup,
104
+ children: [
105
+ /* @__PURE__ */ jsx(StepButton, {
106
+ target: around.prev,
107
+ project,
108
+ build,
109
+ direction: "prev"
110
+ }),
111
+ /* @__PURE__ */ jsxs("span", {
112
+ class: actionPosition,
113
+ "aria-live": "polite",
114
+ children: [
115
+ around.position,
116
+ " / ",
117
+ around.total
118
+ ]
119
+ }),
120
+ /* @__PURE__ */ jsx(StepButton, {
121
+ target: around.next,
122
+ project,
123
+ build,
124
+ direction: "next"
125
+ })
126
+ ]
127
+ }),
128
+ /* @__PURE__ */ jsxs("div", {
129
+ class: actionHints,
130
+ children: [
131
+ /* @__PURE__ */ jsx(Kbd, { keys: ["j", "k"] }),
132
+ " navigate ",
133
+ /* @__PURE__ */ jsx(Kbd, { keys: "a" }),
134
+ " approve ",
135
+ /* @__PURE__ */ jsx(Kbd, { keys: "r" }),
136
+ " reject",
137
+ /* @__PURE__ */ jsx(Button, {
138
+ variant: "ghost",
139
+ size: "sm",
140
+ "data-shortcuts-open": true,
141
+ "aria-label": "Keyboard shortcuts (?)",
142
+ children: /* @__PURE__ */ jsx(Kbd, { keys: "?" })
143
+ })
144
+ ]
145
+ }),
146
+ canReview && reviewable ? /* @__PURE__ */ jsx(DecisionButtons, {
147
+ project,
148
+ build,
149
+ selected,
150
+ drifted
151
+ }) : /* @__PURE__ */ jsx(Badge, {
152
+ tone: statusTone(selected.status),
153
+ icon: true,
154
+ children: selected.status
155
+ })
156
+ ]
157
+ });
158
+ }
159
+ //#endregion
160
+ export { DiffActionBar };
161
+
162
+ //# sourceMappingURL=build-diff-actions.mjs.map
@@ -0,0 +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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"}