@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
@@ -1 +1 @@
1
- {"version":3,"file":"projects.mjs","names":[],"sources":["../../src/pages/projects.tsx"],"sourcesContent":["import { BuildModel, ProjectModel, SnapshotModel } from \"@storyshelf/core/models\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport { createUrlBuilder } from \"@storyshelf/core/urls\";\nimport { getStore } from \"../store.ts\";\nimport {\n Button,\n Card,\n CodeBlock,\n EmptyState,\n FilterInput,\n HStack,\n Meta,\n PageHeader,\n VStack,\n} from \"../ui/components.tsx\";\nimport { css } from \"../ui/css.ts\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\nimport { ProjectCard, type ProjectSummary } from \"./projects-card.tsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\nconst projectGrid = css`\n /* project-grid */\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(min(100%, 320px), 1fr));\n gap: var(--space-4);\n`;\n\nconst setupSummary = css`\n /* setup-summary */\n cursor: pointer;\n font-weight: 650;\n font-size: var(--text-lg);\n letter-spacing: -0.01em;\n`;\n\n/** Latest build, a few previews, and the review backlog for one project. */\nasync function summarize(project: Project): Promise<ProjectSummary> {\n const { db } = getStore();\n const [latest] = await new BuildModel(db).list(project.id);\n if (!latest) {\n return { project, latest: null, previews: [], pending: 0 };\n }\n const snapshots = await new SnapshotModel(db).listByBuild(latest.id);\n const pending = snapshots.filter((snap) => snap.status === \"new\" || snap.status === \"changed\");\n const preferred = pending.length > 0 ? pending : snapshots;\n return { project, latest, previews: preferred.slice(0, 3), pending: pending.length };\n}\n\n/** Copy-ready steps to get a first build uploaded. */\nfunction SetupGuide({ open }: { open: boolean }): ReturnType<typeof Card> {\n return (\n <Card>\n <details open={open}>\n <summary class={setupSummary}>Set up your first upload</summary>\n <VStack>\n <Meta>1. Create a project (writes .storybook/storyshelf.json for later runs).</Meta>\n <CodeBlock code='npx storyshelf create --url http://localhost:3000 --name \"My Storybook\" --token $STORYSHELF_ADMIN_TOKEN' />\n <Meta>\n 2. Generate a token in <strong>Settings → Tokens</strong> and set it in CI.\n </Meta>\n <CodeBlock code=\"export STORYSHELF_TOKEN=<your token>\" />\n <Meta>3. Upload your built Storybook.</Meta>\n <CodeBlock code=\"npx storyshelf upload\" />\n </VStack>\n </details>\n </Card>\n );\n}\n\n/** Projects overview: searchable project cards with review state, plus setup help. */\nexport async function renderProjectsPage(): Promise<RenderedContent> {\n const { db, user, config } = getStore();\n const urls = createUrlBuilder(\"/\", config.publishedBaseDomain);\n const projects = await new ProjectModel(db).list();\n const summaries = await Promise.all(projects.map((project) => summarize(project)));\n const canCreate = !user || user.role === \"admin\" || user.role === \"member\";\n const newProject = canCreate ? (\n <Button variant=\"primary\" icon=\"plus\" href={urls.projectsNew()}>\n New project\n </Button>\n ) : null;\n\n return (\n <DocumentLayout title=\"Projects\" nav={{ active: \"projects\" }}>\n <PageHeader\n title=\"Projects\"\n description=\"Each project is one Storybook. Create a project, then upload builds from CI.\"\n actions={newProject}\n />\n <VStack gap=\"lg\">\n {projects.length > 1 ? (\n <HStack>\n <FilterInput label=\"Filter projects\" placeholder=\"Filter projects…\" />\n </HStack>\n ) : null}\n {projects.length === 0 ? (\n <EmptyState\n title=\"No projects yet\"\n description=\"Create your first project to start visual testing. Projects are free and unlimited.\"\n action={newProject}\n />\n ) : (\n <div class={projectGrid} data-filter-scope>\n {summaries.map((summary) => (\n <ProjectCard\n key={summary.project.id}\n summary={summary}\n urls={{\n library: urls.library(summary.project.slug),\n settings: urls.settings(summary.project.slug),\n storybook: urls.short(summary.project.slug),\n review: summary.latest\n ? urls.buildDiff(summary.project.slug, summary.latest.id)\n : urls.library(summary.project.slug),\n }}\n />\n ))}\n <div data-filter-empty hidden>\n <Meta center>No projects match your filter.</Meta>\n </div>\n </div>\n )}\n <SetupGuide open={projects.length === 0} />\n </VStack>\n </DocumentLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"projects.mjs","names":[],"sources":["../../src/pages/projects.tsx"],"sourcesContent":["import { BuildModel, ProjectModel, SnapshotModel } from \"@storyshelf/core/models\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport { createUrlBuilder } from \"@storyshelf/core/urls\";\nimport { loadHealthBadge } from \"../insights/health-panel.ts\";\nimport { getStore } from \"../store.ts\";\nimport {\n Button,\n Card,\n CodeBlock,\n EmptyState,\n FilterInput,\n HStack,\n Meta,\n PageHeader,\n VStack,\n} from \"../ui/components.tsx\";\nimport { css } from \"../ui/css.ts\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\nimport { ProjectCard, type ProjectSummary } from \"./projects-card.tsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\nconst projectGrid = css`\n /* project-grid */\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(min(100%, 320px), 1fr));\n gap: var(--space-4);\n`;\n\nconst setupSummary = css`\n /* setup-summary */\n cursor: pointer;\n font-weight: 650;\n font-size: var(--text-lg);\n letter-spacing: -0.01em;\n`;\n\n/** Latest build, a few previews, and the review backlog for one project. */\nasync function summarize(project: Project): Promise<ProjectSummary> {\n const { db } = getStore();\n const [latest] = await new BuildModel(db).list(project.id);\n const health = await loadHealthBadge(project);\n if (!latest) {\n return { project, latest: null, previews: [], pending: 0, health };\n }\n const snapshots = await new SnapshotModel(db).listByBuild(latest.id);\n const pending = snapshots.filter((snap) => snap.status === \"new\" || snap.status === \"changed\");\n const preferred = pending.length > 0 ? pending : snapshots;\n return { project, latest, previews: preferred.slice(0, 3), pending: pending.length, health };\n}\n\n/** Copy-ready steps to get a first build uploaded. */\nfunction SetupGuide({ open }: { open: boolean }): ReturnType<typeof Card> {\n return (\n <Card>\n <details open={open}>\n <summary class={setupSummary}>Set up your first upload</summary>\n <VStack>\n <Meta>1. Create a project (writes .storybook/storyshelf.json for later runs).</Meta>\n <CodeBlock code='npx storyshelf create --url http://localhost:3000 --name \"My Storybook\" --token $STORYSHELF_ADMIN_TOKEN' />\n <Meta>\n 2. Generate a token in <strong>Settings → Tokens</strong> and set it in CI.\n </Meta>\n <CodeBlock code=\"export STORYSHELF_TOKEN=<your token>\" />\n <Meta>3. Upload your built Storybook.</Meta>\n <CodeBlock code=\"npx storyshelf upload\" />\n </VStack>\n </details>\n </Card>\n );\n}\n\n/** Projects overview: searchable project cards with review state, plus setup help. */\nexport async function renderProjectsPage(): Promise<RenderedContent> {\n const { db, user, config } = getStore();\n const urls = createUrlBuilder(\"/\", config.publishedBaseDomain);\n const projects = await new ProjectModel(db).list();\n const summaries = await Promise.all(projects.map((project) => summarize(project)));\n const canCreate = !user || user.role === \"admin\" || user.role === \"member\";\n const newProject = canCreate ? (\n <Button variant=\"primary\" icon=\"plus\" href={urls.projectsNew()}>\n New project\n </Button>\n ) : null;\n\n return (\n <DocumentLayout title=\"Projects\" nav={{ active: \"projects\" }}>\n <PageHeader\n title=\"Projects\"\n description=\"Each project is one Storybook. Create a project, then upload builds from CI.\"\n actions={newProject}\n />\n <VStack gap=\"lg\">\n {projects.length > 1 ? (\n <HStack>\n <FilterInput label=\"Filter projects\" placeholder=\"Filter projects…\" />\n </HStack>\n ) : null}\n {projects.length === 0 ? (\n <EmptyState\n title=\"No projects yet\"\n description=\"Create your first project to start visual testing. Projects are free and unlimited.\"\n action={newProject}\n />\n ) : (\n <div class={projectGrid} data-filter-scope>\n {summaries.map((summary) => (\n <ProjectCard\n key={summary.project.id}\n summary={summary}\n urls={{\n library: urls.library(summary.project.slug),\n settings: urls.settings(summary.project.slug),\n storybook: urls.short(summary.project.slug),\n review: summary.latest\n ? urls.buildDiff(summary.project.slug, summary.latest.id)\n : urls.library(summary.project.slug),\n }}\n />\n ))}\n <div data-filter-empty hidden>\n <Meta center>No projects match your filter.</Meta>\n </div>\n </div>\n )}\n <SetupGuide open={projects.length === 0} />\n </VStack>\n </DocumentLayout>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,116 @@
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 } from "../ui/feedback.mjs";
7
+ import { Card, SectionTitle } from "../ui/layout.mjs";
8
+ import { HStack, VStack } from "../ui/stacks.mjs";
9
+ import { Table } from "../ui/table.mjs";
10
+ import { jsx, jsxs } from "hono/jsx/jsx-runtime";
11
+ import { createUrlBuilder } from "@storyshelf/core/urls";
12
+ //#region src/pages/review-changeset.tsx
13
+ /** What the build changed in the Storybook catalog, against its baseline build. */
14
+ function ChangesetCard(props) {
15
+ const { data } = props;
16
+ const { baseline, changeset } = data.changeset;
17
+ const total = changeset.added.length + changeset.changed.length + changeset.removed.length;
18
+ return /* @__PURE__ */ jsxs(Card, { children: [
19
+ /* @__PURE__ */ jsx(SectionTitle, { children: "Changes in this build" }),
20
+ /* @__PURE__ */ jsxs(Meta, { children: [
21
+ baseline ? `Compared with the accepted build ${baseline.gitSha.slice(0, 7)} on ${baseline.gitBranch}.` : "No accepted build to compare with: every component counts as new.",
22
+ " ",
23
+ "A changed component has different stories, not necessarily a different look."
24
+ ] }),
25
+ total === 0 ? /* @__PURE__ */ jsx(EmptyState, {
26
+ title: "No component changes",
27
+ description: `${changeset.unchanged} components are the same as in the baseline.`
28
+ }) : /* @__PURE__ */ jsx(ChangesetTable, { data })
29
+ ] });
30
+ }
31
+ function ChangesetTable(props) {
32
+ const { data } = props;
33
+ const { changeset } = data.changeset;
34
+ return /* @__PURE__ */ jsxs(VStack, { children: [/* @__PURE__ */ jsxs(HStack, { children: [
35
+ /* @__PURE__ */ jsxs(Badge, {
36
+ tone: "success",
37
+ children: [changeset.added.length, " added"]
38
+ }),
39
+ /* @__PURE__ */ jsxs(Badge, {
40
+ tone: "warning",
41
+ children: [changeset.changed.length, " changed"]
42
+ }),
43
+ /* @__PURE__ */ jsxs(Badge, {
44
+ tone: "danger",
45
+ children: [changeset.removed.length, " removed"]
46
+ }),
47
+ /* @__PURE__ */ jsxs(Meta, {
48
+ as: "span",
49
+ children: [changeset.unchanged, " unchanged"]
50
+ })
51
+ ] }), /* @__PURE__ */ jsx(Table, { children: /* @__PURE__ */ jsxs("table", { children: [
52
+ /* @__PURE__ */ jsx("caption", {
53
+ class: "visually-hidden",
54
+ children: "Components that changed in this build"
55
+ }),
56
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
57
+ /* @__PURE__ */ jsx("th", { children: "Component" }),
58
+ /* @__PURE__ */ jsx("th", { children: "Change" }),
59
+ /* @__PURE__ */ jsx("th", { children: "Stories" }),
60
+ /* @__PURE__ */ jsx("th", { children: /* @__PURE__ */ jsx("span", {
61
+ class: "visually-hidden",
62
+ children: "Links"
63
+ }) })
64
+ ] }) }),
65
+ /* @__PURE__ */ jsxs("tbody", { children: [
66
+ rows(data, "added", changeset.added),
67
+ rows(data, "changed", changeset.changed),
68
+ rows(data, "removed", changeset.removed)
69
+ ] })
70
+ ] }) })] });
71
+ }
72
+ function rows(data, kind, items) {
73
+ return items.map((item) => /* @__PURE__ */ jsx(ChangeRow, {
74
+ data,
75
+ kind,
76
+ item
77
+ }, `${kind}:${item.importPath}:${item.title}`));
78
+ }
79
+ const KIND_TONE = {
80
+ added: "success",
81
+ changed: "warning",
82
+ removed: "danger"
83
+ };
84
+ function ChangeRow(props) {
85
+ const { data, kind, item } = props;
86
+ const { project, build } = data;
87
+ const urls = createUrlBuilder("/", getStore().config.publishedBaseDomain);
88
+ const story = data.firstStory.get(item.importPath);
89
+ return /* @__PURE__ */ jsxs("tr", { children: [
90
+ /* @__PURE__ */ jsxs("td", { children: [/* @__PURE__ */ jsx("strong", { children: item.title }), /* @__PURE__ */ jsx(Meta, {
91
+ as: "div",
92
+ mono: true,
93
+ children: item.importPath
94
+ })] }),
95
+ /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(Badge, {
96
+ tone: KIND_TONE[kind],
97
+ children: kind
98
+ }) }),
99
+ /* @__PURE__ */ jsx("td", { children: item.previousStoryCount !== void 0 && item.previousStoryCount !== item.storyCount ? `${item.previousStoryCount} → ${item.storyCount}` : item.storyCount }),
100
+ /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsxs(HStack, { children: [kind !== "removed" && story ? /* @__PURE__ */ jsx(Button, {
101
+ variant: "ghost",
102
+ size: "sm",
103
+ href: `${urls.storybookBuild(project.slug, build.id)}/?path=/story/${encodeURIComponent(story)}`,
104
+ children: "Story"
105
+ }) : null, item.importPath ? /* @__PURE__ */ jsx(Button, {
106
+ variant: "ghost",
107
+ size: "sm",
108
+ href: urls.component(project.slug, item.importPath),
109
+ children: "Component"
110
+ }) : null] }) })
111
+ ] });
112
+ }
113
+ //#endregion
114
+ export { ChangesetCard };
115
+
116
+ //# sourceMappingURL=review-changeset.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"review-changeset.mjs","names":[],"sources":["../../src/pages/review-changeset.tsx"],"sourcesContent":["/** The changeset card (ADR 0013, decision 8): components the build added, removed or changed. */\nimport type { ChangesetComponent } from \"@storyshelf/core/models\";\nimport { createUrlBuilder } from \"@storyshelf/core/urls\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport type { ReviewSectionData } from \"../routers/review-section.handlers.ts\";\nimport { getStore } from \"../store.ts\";\nimport {\n Badge,\n Button,\n Card,\n EmptyState,\n HStack,\n Meta,\n SectionTitle,\n Table,\n VStack,\n} from \"../ui/components.tsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\ntype Rendered = HtmlEscapedString | Promise<HtmlEscapedString>;\n\ntype Kind = \"added\" | \"changed\" | \"removed\";\n\n/** What the build changed in the Storybook catalog, against its baseline build. */\nexport function ChangesetCard(props: { data: ReviewSectionData }): Rendered {\n const { data } = props;\n const { baseline, changeset } = data.changeset;\n const total = changeset.added.length + changeset.changed.length + changeset.removed.length;\n return (\n <Card>\n <SectionTitle>Changes in this build</SectionTitle>\n <Meta>\n {baseline\n ? `Compared with the accepted build ${baseline.gitSha.slice(0, 7)} on ${baseline.gitBranch}.`\n : \"No accepted build to compare with: every component counts as new.\"}{\" \"}\n A changed component has different stories, not necessarily a different look.\n </Meta>\n {total === 0 ? (\n <EmptyState\n title=\"No component changes\"\n description={`${changeset.unchanged} components are the same as in the baseline.`}\n />\n ) : (\n <ChangesetTable data={data} />\n )}\n </Card>\n );\n}\n\nfunction ChangesetTable(props: { data: ReviewSectionData }): Rendered {\n const { data } = props;\n const { changeset } = data.changeset;\n return (\n <VStack>\n <HStack>\n <Badge tone=\"success\">{changeset.added.length} added</Badge>\n <Badge tone=\"warning\">{changeset.changed.length} changed</Badge>\n <Badge tone=\"danger\">{changeset.removed.length} removed</Badge>\n <Meta as=\"span\">{changeset.unchanged} unchanged</Meta>\n </HStack>\n <Table>\n <table>\n <caption class=\"visually-hidden\">Components that changed in this build</caption>\n <thead>\n <tr>\n <th>Component</th>\n <th>Change</th>\n <th>Stories</th>\n <th>\n <span class=\"visually-hidden\">Links</span>\n </th>\n </tr>\n </thead>\n <tbody>\n {rows(data, \"added\", changeset.added)}\n {rows(data, \"changed\", changeset.changed)}\n {rows(data, \"removed\", changeset.removed)}\n </tbody>\n </table>\n </Table>\n </VStack>\n );\n}\n\nfunction rows(\n data: ReviewSectionData,\n kind: Kind,\n items: readonly (ChangesetComponent & { previousStoryCount?: number })[],\n): Rendered[] {\n return items.map((item): Rendered => (\n <ChangeRow\n key={`${kind}:${item.importPath}:${item.title}`}\n data={data}\n kind={kind}\n item={item}\n />\n ));\n}\n\nconst KIND_TONE = { added: \"success\", changed: \"warning\", removed: \"danger\" } as const;\n\nfunction ChangeRow(props: {\n data: ReviewSectionData;\n kind: Kind;\n item: ChangesetComponent & { previousStoryCount?: number };\n}): Rendered {\n const { data, kind, item } = props;\n const { project, build } = data;\n const urls = createUrlBuilder(\"/\", getStore().config.publishedBaseDomain);\n const story = data.firstStory.get(item.importPath);\n return (\n <tr>\n <td>\n <strong>{item.title}</strong>\n <Meta as=\"div\" mono>\n {item.importPath}\n </Meta>\n </td>\n <td>\n <Badge tone={KIND_TONE[kind]}>{kind}</Badge>\n </td>\n <td>\n {item.previousStoryCount !== undefined && item.previousStoryCount !== item.storyCount\n ? `${item.previousStoryCount} → ${item.storyCount}`\n : item.storyCount}\n </td>\n <td>\n <HStack>\n {kind !== \"removed\" && story ? (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n href={`${urls.storybookBuild(project.slug, build.id)}/?path=/story/${encodeURIComponent(story)}`}\n >\n Story\n </Button>\n ) : null}\n {item.importPath ? (\n <Button variant=\"ghost\" size=\"sm\" href={urls.component(project.slug, item.importPath)}>\n Component\n </Button>\n ) : null}\n </HStack>\n </td>\n </tr>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,103 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { Button } from "../ui/buttons.mjs";
5
+ import { Badge, Meta } from "../ui/feedback.mjs";
6
+ import { TextareaField } from "../ui/forms.mjs";
7
+ import { Card, SectionTitle } from "../ui/layout.mjs";
8
+ import { VStack } from "../ui/stacks.mjs";
9
+ import { csrfField } from "../ui/csrf-field.mjs";
10
+ import { CommentCard } from "./build-diff-comments.mjs";
11
+ import { jsx, jsxs } from "hono/jsx/jsx-runtime";
12
+ //#region src/pages/review-comments.tsx
13
+ /** Comments on the build as a whole and on its stories, with a form for a build comment. */
14
+ function CommentsCard(props) {
15
+ const { data } = props;
16
+ const { project, build, comments, canComment } = data;
17
+ return /* @__PURE__ */ jsxs(Card, { children: [
18
+ /* @__PURE__ */ jsx(SectionTitle, { children: "Comments" }),
19
+ comments.length === 0 ? /* @__PURE__ */ jsxs(Meta, { children: [
20
+ "No comments yet.",
21
+ " ",
22
+ canComment ? "Comment on the build here, or on a story from its component page." : "Developers, approvers and admins can comment."
23
+ ] }) : null,
24
+ /* @__PURE__ */ jsx(VStack, { children: comments.map((comment) => /* @__PURE__ */ jsx(CommentCard, {
25
+ project,
26
+ build,
27
+ comment,
28
+ canReview: canComment,
29
+ anchor: /* @__PURE__ */ jsx(Badge, {
30
+ tone: "neutral",
31
+ children: comment.storyId ? `story ${comment.storyId}` : "build"
32
+ })
33
+ }, comment.id)) }),
34
+ canComment ? /* @__PURE__ */ jsx(CommentForm, {
35
+ project,
36
+ build,
37
+ storyId: void 0
38
+ }) : null
39
+ ] });
40
+ }
41
+ /** The comments of one story in the build a component page shows, with a form to add one. */
42
+ function StoryComments(props) {
43
+ const { project, build, storyId, canComment } = props;
44
+ const comments = props.comments.filter((comment) => comment.storyId === storyId);
45
+ return /* @__PURE__ */ jsxs(Card, {
46
+ id: "story-comments",
47
+ children: [
48
+ /* @__PURE__ */ jsx(SectionTitle, { children: "Comments on this story" }),
49
+ /* @__PURE__ */ jsxs(Meta, { children: [
50
+ "On build ",
51
+ build.gitSha.slice(0, 7),
52
+ "."
53
+ ] }),
54
+ comments.length === 0 ? /* @__PURE__ */ jsx(Meta, { children: "No comments on this story yet." }) : null,
55
+ /* @__PURE__ */ jsx(VStack, { children: comments.map((comment) => /* @__PURE__ */ jsx(CommentCard, {
56
+ project,
57
+ build,
58
+ comment,
59
+ canReview: canComment
60
+ }, comment.id)) }),
61
+ canComment ? /* @__PURE__ */ jsx(CommentForm, {
62
+ project,
63
+ build,
64
+ storyId
65
+ }) : null
66
+ ]
67
+ });
68
+ }
69
+ function CommentForm(props) {
70
+ const { project, build, storyId } = props;
71
+ const url = `/projects/${project.slug}/builds/${build.id}/comments`;
72
+ return /* @__PURE__ */ jsxs("form", {
73
+ method: "post",
74
+ action: url,
75
+ "hx-post": url,
76
+ "hx-target": "body",
77
+ class: "mt-1 max-w-prose",
78
+ children: [csrfField(), /* @__PURE__ */ jsxs(VStack, { children: [
79
+ storyId ? /* @__PURE__ */ jsx("input", {
80
+ type: "hidden",
81
+ name: "storyId",
82
+ value: storyId
83
+ }) : null,
84
+ /* @__PURE__ */ jsx(TextareaField, {
85
+ label: storyId ? "Add story comment" : "Add build comment",
86
+ name: "body",
87
+ id: storyId ? "story-comment-body" : "build-comment-body",
88
+ rows: 3,
89
+ required: true,
90
+ placeholder: storyId ? "Leave a comment on this story…" : "Leave a comment on this build…"
91
+ }),
92
+ /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(Button, {
93
+ variant: "primary",
94
+ type: "submit",
95
+ children: "Comment"
96
+ }) })
97
+ ] })]
98
+ });
99
+ }
100
+ //#endregion
101
+ export { CommentsCard, StoryComments };
102
+
103
+ //# sourceMappingURL=review-comments.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"review-comments.mjs","names":[],"sources":["../../src/pages/review-comments.tsx"],"sourcesContent":["/** Build and story comments (ADR 0013, decision 2): the build page card and the story thread. */\nimport type { Build, Comment, Project } from \"@storyshelf/core/schema\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport type { ReviewSectionData } from \"../routers/review-section.handlers.ts\";\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 { CommentCard } from \"./build-diff-comments.tsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\ntype Rendered = HtmlEscapedString | Promise<HtmlEscapedString>;\n\n/** Comments on the build as a whole and on its stories, with a form for a build comment. */\nexport function CommentsCard(props: { data: ReviewSectionData }): Rendered {\n const { data } = props;\n const { project, build, comments, canComment } = data;\n return (\n <Card>\n <SectionTitle>Comments</SectionTitle>\n {comments.length === 0 ? (\n <Meta>\n No comments yet.{\" \"}\n {canComment\n ? \"Comment on the build here, or on a story from its component page.\"\n : \"Developers, approvers and admins can comment.\"}\n </Meta>\n ) : null}\n <VStack>\n {comments.map((comment): Rendered => (\n <CommentCard\n key={comment.id}\n project={project}\n build={build}\n comment={comment}\n canReview={canComment}\n anchor={\n <Badge tone=\"neutral\">{comment.storyId ? `story ${comment.storyId}` : \"build\"}</Badge>\n }\n />\n ))}\n </VStack>\n {canComment ? <CommentForm project={project} build={build} storyId={undefined} /> : null}\n </Card>\n );\n}\n\n/** The comments of one story in the build a component page shows, with a form to add one. */\nexport function StoryComments(props: {\n project: Project;\n build: Build;\n storyId: string;\n comments: readonly Comment[];\n canComment: boolean;\n}): Rendered {\n const { project, build, storyId, canComment } = props;\n const comments = props.comments.filter((comment) => comment.storyId === storyId);\n return (\n <Card id=\"story-comments\">\n <SectionTitle>Comments on this story</SectionTitle>\n <Meta>On build {build.gitSha.slice(0, 7)}.</Meta>\n {comments.length === 0 ? <Meta>No comments on this story yet.</Meta> : null}\n <VStack>\n {comments.map((comment): Rendered => (\n <CommentCard\n key={comment.id}\n project={project}\n build={build}\n comment={comment}\n canReview={canComment}\n />\n ))}\n </VStack>\n {canComment ? <CommentForm project={project} build={build} storyId={storyId} /> : null}\n </Card>\n );\n}\n\nfunction CommentForm(props: {\n project: Project;\n build: Build;\n storyId: string | undefined;\n}): Rendered {\n const { project, build, storyId } = props;\n const url = `/projects/${project.slug}/builds/${build.id}/comments`;\n return (\n <form method=\"post\" action={url} hx-post={url} hx-target=\"body\" class=\"mt-1 max-w-prose\">\n {csrfField()}\n <VStack>\n {storyId ? <input type=\"hidden\" name=\"storyId\" value={storyId} /> : null}\n <TextareaField\n label={storyId ? \"Add story comment\" : \"Add build comment\"}\n name=\"body\"\n id={storyId ? \"story-comment-body\" : \"build-comment-body\"}\n rows={3}\n required\n placeholder={\n storyId ? \"Leave a comment on this story…\" : \"Leave a comment on this build…\"\n }\n />\n <div>\n <Button variant=\"primary\" type=\"submit\">\n Comment\n </Button>\n </div>\n </VStack>\n </form>\n );\n}\n"],"mappings":";;;;;;;;;;;;;AAqBA,SAAgB,aAAa,OAA8C;CACzE,MAAM,EAAE,SAAS;CACjB,MAAM,EAAE,SAAS,OAAO,UAAU,eAAe;CACjD,OACE,qBAAC,MAAD,EAAA,UAAA;EACE,oBAAC,cAAD,EAAA,UAAc,WAAsB,CAAA;EACnC,SAAS,WAAW,IACnB,qBAAC,MAAD,EAAA,UAAA;GAAM;GACa;GAChB,aACG,sEACA;EACA,EAAA,CAAA,IACJ;EACJ,oBAAC,QAAD,EAAA,UACG,SAAS,KAAK,YACb,oBAAC,aAAD;GAEW;GACF;GACE;GACT,WAAW;GACX,QACE,oBAAC,OAAD;IAAO,MAAK;IAAW,UAAA,QAAQ,UAAU,SAAS,QAAQ,YAAY;GAAe,CAAA;EAExF,GARM,QAAQ,EAQd,CACF,EACK,CAAA;EACP,aAAa,oBAAC,aAAD;GAAsB;GAAgB;GAAO,SAAS,KAAA;EAAY,CAAA,IAAI;CAChF,EAAA,CAAA;AAEV;;AAGA,SAAgB,cAAc,OAMjB;CACX,MAAM,EAAE,SAAS,OAAO,SAAS,eAAe;CAChD,MAAM,WAAW,MAAM,SAAS,QAAQ,YAAY,QAAQ,YAAY,OAAO;CAC/E,OACE,qBAAC,MAAD;EAAM,IAAG;EAAT,UAAA;GACE,oBAAC,cAAD,EAAA,UAAc,yBAAoC,CAAA;GAClD,qBAAC,MAAD,EAAA,UAAA;IAAM;IAAU,MAAM,OAAO,MAAM,GAAG,CAAC;IAAE;GAAO,EAAA,CAAA;GAC/C,SAAS,WAAW,IAAI,oBAAC,MAAD,EAAA,UAAM,iCAAoC,CAAA,IAAI;GACvE,oBAAC,QAAD,EAAA,UACG,SAAS,KAAK,YACb,oBAAC,aAAD;IAEW;IACF;IACE;IACT,WAAW;GACZ,GALM,QAAQ,EAKd,CACF,EACK,CAAA;GACP,aAAa,oBAAC,aAAD;IAAsB;IAAgB;IAAgB;GAAU,CAAA,IAAI;EAC9E;;AAEV;AAEA,SAAS,YAAY,OAIR;CACX,MAAM,EAAE,SAAS,OAAO,YAAY;CACpC,MAAM,MAAM,aAAa,QAAQ,KAAK,UAAU,MAAM,GAAG;CACzD,OACE,qBAAC,QAAD;EAAM,QAAO;EAAO,QAAQ;EAAK,WAAS;EAAK,aAAU;EAAO,OAAM;EAAtE,UAAA,CACG,UAAU,GACX,qBAAC,QAAD,EAAA,UAAA;GACG,UAAU,oBAAC,SAAD;IAAO,MAAK;IAAS,MAAK;IAAU,OAAO;GAAU,CAAA,IAAI;GACpE,oBAAC,eAAD;IACE,OAAO,UAAU,sBAAsB;IACvC,MAAK;IACL,IAAI,UAAU,uBAAuB;IACrC,MAAM;IACN,UAAA;IACA,aACE,UAAU,mCAAmC;GAEhD,CAAA;GACD,oBAAC,OAAD,EAAA,UACE,oBAAC,QAAD;IAAQ,SAAQ;IAAU,MAAK;IAAS,UAAA;GAEhC,CAAA,EACL,CAAA;EACC,EAAA,CAAA,CACJ;;AAEV"}
@@ -0,0 +1,102 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { Button } from "../ui/buttons.mjs";
5
+ import { Badge, Meta, statusTone } from "../ui/feedback.mjs";
6
+ import { TextareaField } from "../ui/forms.mjs";
7
+ import { Card, SectionTitle } from "../ui/layout.mjs";
8
+ import { HStack, VStack } from "../ui/stacks.mjs";
9
+ import { RelativeTime } from "../ui/time.mjs";
10
+ import { csrfField } from "../ui/csrf-field.mjs";
11
+ import { BUILD_STATUSES } from "@storyshelf/core/types";
12
+ import { jsx, jsxs } from "hono/jsx/jsx-runtime";
13
+ //#region src/pages/review-verdict.tsx
14
+ /** The verdict card of the build page: where the build stands, the verdicts, and the reviewer's controls. */
15
+ /** Build-level verdicts: the approvals so far, each reviewer's latest verdict, and the controls. */
16
+ function VerdictCard(props) {
17
+ const { data } = props;
18
+ return /* @__PURE__ */ jsxs(Card, { children: [
19
+ /* @__PURE__ */ jsxs(HStack, {
20
+ justify: "between",
21
+ children: [/* @__PURE__ */ jsx(SectionTitle, { children: "Review" }), /* @__PURE__ */ jsx(Badge, {
22
+ tone: statusTone(data.state.status),
23
+ children: standing(data)
24
+ })]
25
+ }),
26
+ /* @__PURE__ */ jsx(Meta, { children: summary(data) }),
27
+ /* @__PURE__ */ jsx(VStack, { children: data.state.latest.map((review) => /* @__PURE__ */ jsxs(HStack, {
28
+ align: "baseline",
29
+ wrap: false,
30
+ children: [
31
+ /* @__PURE__ */ jsx(Badge, {
32
+ tone: review.verdict === "approved" ? "success" : "danger",
33
+ children: review.verdict
34
+ }),
35
+ /* @__PURE__ */ jsx("strong", { children: review.userId ? data.names.get(review.userId) ?? review.userId : "anonymous" }),
36
+ /* @__PURE__ */ jsx(Meta, {
37
+ as: "span",
38
+ children: /* @__PURE__ */ jsx(RelativeTime, { value: review.createdAt })
39
+ }),
40
+ review.body ? /* @__PURE__ */ jsx("span", { children: review.body }) : null
41
+ ]
42
+ }, review.id)) }),
43
+ /* @__PURE__ */ jsx(VerdictControls, { data })
44
+ ] });
45
+ }
46
+ /**
47
+ * The form for a reviewer once the build can take a verdict; while its Storybook is still
48
+ * publishing a notice stands in (the server refuses a verdict until then, ADR 0013, decision 3).
49
+ */
50
+ function VerdictControls(props) {
51
+ const { data } = props;
52
+ if (!data.canVerdict || data.build.status === "failed") return null;
53
+ if (data.publishing === "pending") return /* @__PURE__ */ jsx(Meta, { children: "Waiting for the Storybook to publish. You can give a verdict once it has." });
54
+ return data.publishing === "success" ? /* @__PURE__ */ jsx(VerdictForm, { data }) : null;
55
+ }
56
+ /** The reviewer's controls: an optional note and the two verdicts. */
57
+ function VerdictForm(props) {
58
+ const { project, build } = props.data;
59
+ const url = `/projects/${project.slug}/builds/${build.id}/reviews`;
60
+ return /* @__PURE__ */ jsxs("form", {
61
+ method: "post",
62
+ action: url,
63
+ "hx-post": url,
64
+ "hx-target": "body",
65
+ class: "mt-1 max-w-prose",
66
+ children: [csrfField(), /* @__PURE__ */ jsxs(VStack, { children: [/* @__PURE__ */ jsx(TextareaField, {
67
+ label: "Note with your verdict (optional)",
68
+ name: "body",
69
+ id: "verdict-body",
70
+ rows: 2
71
+ }), /* @__PURE__ */ jsxs(HStack, { children: [/* @__PURE__ */ jsx(Button, {
72
+ variant: "primary",
73
+ type: "submit",
74
+ name: "verdict",
75
+ value: "approved",
76
+ children: "Approve build"
77
+ }), /* @__PURE__ */ jsx(Button, {
78
+ variant: "danger",
79
+ type: "submit",
80
+ name: "verdict",
81
+ value: "rejected",
82
+ children: "Reject build"
83
+ })] })] })]
84
+ });
85
+ }
86
+ /** The status badge text: the build's status, worded for review. */
87
+ function standing(data) {
88
+ const { status } = data.state;
89
+ if (status === "pending") return "waiting for review";
90
+ return BUILD_STATUSES.includes(status) ? status : "unknown";
91
+ }
92
+ /** One line on what decides the build. */
93
+ function summary(data) {
94
+ const { state, testing } = data;
95
+ if (testing) return "Screenshots decide this build; a verdict is a sign-off and does not change its status.";
96
+ const needed = `${state.approvalsRequired} ${state.approvalsRequired === 1 ? "approval" : "approvals"}`;
97
+ return `${state.approvals} of ${needed} given. Any rejection rejects the build. Open comments do not block a verdict.`;
98
+ }
99
+ //#endregion
100
+ export { VerdictCard };
101
+
102
+ //# sourceMappingURL=review-verdict.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"review-verdict.mjs","names":[],"sources":["../../src/pages/review-verdict.tsx"],"sourcesContent":["/** The verdict card of the build page: where the build stands, the verdicts, and the reviewer's controls. */\nimport { BUILD_STATUSES } from \"@storyshelf/core/types\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport type { ReviewSectionData } from \"../routers/review-section.handlers.ts\";\nimport {\n Badge,\n Button,\n Card,\n HStack,\n Meta,\n RelativeTime,\n SectionTitle,\n TextareaField,\n VStack,\n statusTone,\n} from \"../ui/components.tsx\";\nimport { csrfField } from \"../ui/csrf-field.tsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\ntype Rendered = HtmlEscapedString | Promise<HtmlEscapedString>;\n\n/** Build-level verdicts: the approvals so far, each reviewer's latest verdict, and the controls. */\nexport function VerdictCard(props: { data: ReviewSectionData }): Rendered {\n const { data } = props;\n return (\n <Card>\n <HStack justify=\"between\">\n <SectionTitle>Review</SectionTitle>\n <Badge tone={statusTone(data.state.status)}>{standing(data)}</Badge>\n </HStack>\n <Meta>{summary(data)}</Meta>\n <VStack>\n {data.state.latest.map((review): Rendered => (\n <HStack key={review.id} align=\"baseline\" wrap={false}>\n <Badge tone={review.verdict === \"approved\" ? \"success\" : \"danger\"}>\n {review.verdict}\n </Badge>\n <strong>\n {review.userId ? (data.names.get(review.userId) ?? review.userId) : \"anonymous\"}\n </strong>\n <Meta as=\"span\">\n <RelativeTime value={review.createdAt} />\n </Meta>\n {review.body ? <span>{review.body}</span> : null}\n </HStack>\n ))}\n </VStack>\n <VerdictControls data={data} />\n </Card>\n );\n}\n\n/**\n * The form for a reviewer once the build can take a verdict; while its Storybook is still\n * publishing a notice stands in (the server refuses a verdict until then, ADR 0013, decision 3).\n */\nfunction VerdictControls(props: { data: ReviewSectionData }): Rendered | null {\n const { data } = props;\n if (!data.canVerdict || data.build.status === \"failed\") {\n return null;\n }\n if (data.publishing === \"pending\") {\n return <Meta>Waiting for the Storybook to publish. You can give a verdict once it has.</Meta>;\n }\n return data.publishing === \"success\" ? <VerdictForm data={data} /> : null;\n}\n\n/** The reviewer's controls: an optional note and the two verdicts. */\nfunction VerdictForm(props: { data: ReviewSectionData }): Rendered {\n const { project, build } = props.data;\n const url = `/projects/${project.slug}/builds/${build.id}/reviews`;\n return (\n <form method=\"post\" action={url} hx-post={url} hx-target=\"body\" class=\"mt-1 max-w-prose\">\n {csrfField()}\n <VStack>\n <TextareaField\n label=\"Note with your verdict (optional)\"\n name=\"body\"\n id=\"verdict-body\"\n rows={2}\n />\n <HStack>\n <Button variant=\"primary\" type=\"submit\" name=\"verdict\" value=\"approved\">\n Approve build\n </Button>\n <Button variant=\"danger\" type=\"submit\" name=\"verdict\" value=\"rejected\">\n Reject build\n </Button>\n </HStack>\n </VStack>\n </form>\n );\n}\n\n/** The status badge text: the build's status, worded for review. */\nfunction standing(data: ReviewSectionData): string {\n const { status } = data.state;\n if (status === \"pending\") {\n return \"waiting for review\";\n }\n return BUILD_STATUSES.includes(status) ? status : \"unknown\";\n}\n\n/** One line on what decides the build. */\nfunction summary(data: ReviewSectionData): string {\n const { state, testing } = data;\n if (testing) {\n return \"Screenshots decide this build; a verdict is a sign-off and does not change its status.\";\n }\n const needed = `${state.approvalsRequired} ${state.approvalsRequired === 1 ? \"approval\" : \"approvals\"}`;\n return `${state.approvals} of ${needed} given. Any rejection rejects the build. Open comments do not block a verdict.`;\n}\n"],"mappings":";;;;;;;;;;;;;;;AAuBA,SAAgB,YAAY,OAA8C;CACxE,MAAM,EAAE,SAAS;CACjB,OACE,qBAAC,MAAD,EAAA,UAAA;EACE,qBAAC,QAAD;GAAQ,SAAQ;GAAhB,UAAA,CACE,oBAAC,cAAD,EAAA,UAAc,SAAoB,CAAA,GAClC,oBAAC,OAAD;IAAO,MAAM,WAAW,KAAK,MAAM,MAAM;IAAI,UAAA,SAAS,IAAI;GAAS,CAAA,CAC7D;;EACR,oBAAC,MAAD,EAAA,UAAO,QAAQ,IAAI,EAAQ,CAAA;EAC3B,oBAAC,QAAD,EAAA,UACG,KAAK,MAAM,OAAO,KAAK,WACtB,qBAAC,QAAD;GAAwB,OAAM;GAAW,MAAM;GAA/C,UAAA;IACE,oBAAC,OAAD;KAAO,MAAM,OAAO,YAAY,aAAa,YAAY;KACtD,UAAA,OAAO;IACH,CAAA;IACP,oBAAC,UAAD,EAAA,UACG,OAAO,SAAU,KAAK,MAAM,IAAI,OAAO,MAAM,KAAK,OAAO,SAAU,YAC9D,CAAA;IACR,oBAAC,MAAD;KAAM,IAAG;KACP,UAAA,oBAAC,cAAD,EAAc,OAAO,OAAO,UAAY,CAAA;IACpC,CAAA;IACL,OAAO,OAAO,oBAAC,QAAD,EAAA,UAAO,OAAO,KAAW,CAAA,IAAI;GACtC;EAXK,GAAA,OAAO,EAWZ,CACT,EACK,CAAA;EACR,oBAAC,iBAAD,EAAuB,KAAO,CAAA;CAC1B,EAAA,CAAA;AAEV;;;;;AAMA,SAAS,gBAAgB,OAAqD;CAC5E,MAAM,EAAE,SAAS;CACjB,IAAI,CAAC,KAAK,cAAc,KAAK,MAAM,WAAW,UAC5C,OAAO;CAET,IAAI,KAAK,eAAe,WACtB,OAAO,oBAAC,MAAD,EAAA,UAAM,4EAA+E,CAAA;CAE9F,OAAO,KAAK,eAAe,YAAY,oBAAC,aAAD,EAAmB,KAAO,CAAA,IAAI;AACvE;;AAGA,SAAS,YAAY,OAA8C;CACjE,MAAM,EAAE,SAAS,UAAU,MAAM;CACjC,MAAM,MAAM,aAAa,QAAQ,KAAK,UAAU,MAAM,GAAG;CACzD,OACE,qBAAC,QAAD;EAAM,QAAO;EAAO,QAAQ;EAAK,WAAS;EAAK,aAAU;EAAO,OAAM;EAAtE,UAAA,CACG,UAAU,GACX,qBAAC,QAAD,EAAA,UAAA,CACE,oBAAC,eAAD;GACE,OAAM;GACN,MAAK;GACL,IAAG;GACH,MAAM;EACP,CAAA,GACD,qBAAC,QAAD,EAAA,UAAA,CACE,oBAAC,QAAD;GAAQ,SAAQ;GAAU,MAAK;GAAS,MAAK;GAAU,OAAM;GAAW,UAAA;EAEhE,CAAA,GACR,oBAAC,QAAD;GAAQ,SAAQ;GAAS,MAAK;GAAS,MAAK;GAAU,OAAM;GAAW,UAAA;EAE/D,CAAA,CACF,EAAA,CAAA,CACF,EAAA,CAAA,CACJ;;AAEV;;AAGA,SAAS,SAAS,MAAiC;CACjD,MAAM,EAAE,WAAW,KAAK;CACxB,IAAI,WAAW,WACb,OAAO;CAET,OAAO,eAAe,SAAS,MAAM,IAAI,SAAS;AACpD;;AAGA,SAAS,QAAQ,MAAiC;CAChD,MAAM,EAAE,OAAO,YAAY;CAC3B,IAAI,SACF,OAAO;CAET,MAAM,SAAS,GAAG,MAAM,kBAAkB,GAAG,MAAM,sBAAsB,IAAI,aAAa;CAC1F,OAAO,GAAG,MAAM,UAAU,MAAM,OAAO;AACzC"}
@@ -0,0 +1,69 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { Button } from "../ui/buttons.mjs";
5
+ import { Alert, Meta } from "../ui/feedback.mjs";
6
+ import { FormActions, SelectField } from "../ui/forms.mjs";
7
+ import { Card, SectionTitle } from "../ui/layout.mjs";
8
+ import { csrfField } from "../ui/csrf-field.mjs";
9
+ import { jsx, jsxs } from "hono/jsx/jsx-runtime";
10
+ //#region src/pages/settings-ai.tsx
11
+ function ProfileForm(props) {
12
+ const { project, ai } = props;
13
+ return /* @__PURE__ */ jsxs("form", {
14
+ method: "post",
15
+ action: `/projects/${project.slug}/settings/ai`,
16
+ "hx-post": `/projects/${project.slug}/settings/ai`,
17
+ "hx-target": "body",
18
+ "hx-swap": "outerHTML",
19
+ children: [
20
+ csrfField(),
21
+ /* @__PURE__ */ jsx(SelectField, {
22
+ label: "AI profile",
23
+ name: "aiProfile",
24
+ value: project.aiProfile ?? "",
25
+ disabled: !ai.isSiteAdmin,
26
+ options: [{
27
+ value: "",
28
+ label: "Off (no data leaves the server)"
29
+ }, ...ai.profiles.map((name) => ({
30
+ value: name,
31
+ label: name === ai.defaultProfile ? `${name} (default)` : name
32
+ }))],
33
+ hint: "Only site admins can change this because it decides where project data is sent."
34
+ }),
35
+ ai.isSiteAdmin ? /* @__PURE__ */ jsx(FormActions, { children: /* @__PURE__ */ jsx(Button, {
36
+ variant: "primary",
37
+ type: "submit",
38
+ icon: "check",
39
+ children: "Save changes"
40
+ }) }) : /* @__PURE__ */ jsx(Meta, { children: "You need site admin access to change the AI profile." })
41
+ ]
42
+ });
43
+ }
44
+ /** AI settings tab: per-project profile gate (site admins choose; others read). */
45
+ function renderSettingsAi(project, ai, formState) {
46
+ if (!ai?.available) return /* @__PURE__ */ jsx(Alert, {
47
+ tone: "info",
48
+ title: "AI is not configured",
49
+ children: "This server has no AI configured, so insights are unavailable."
50
+ });
51
+ return /* @__PURE__ */ jsxs("div", {
52
+ class: "grid max-w-form",
53
+ children: [formState?.globalError ? /* @__PURE__ */ jsx(Alert, {
54
+ tone: "danger",
55
+ children: formState.globalError
56
+ }) : null, /* @__PURE__ */ jsxs(Card, { children: [
57
+ /* @__PURE__ */ jsx(SectionTitle, { children: "AI insights" }),
58
+ /* @__PURE__ */ jsx(Meta, { children: "Choosing a profile turns on AI triage for builds and a project health digest. The profile decides which models see this project's data (including screenshots when its model supports vision). Insights are advisory only: they never approve or reject anything." }),
59
+ /* @__PURE__ */ jsx(ProfileForm, {
60
+ project,
61
+ ai
62
+ })
63
+ ] })]
64
+ });
65
+ }
66
+ //#endregion
67
+ export { renderSettingsAi };
68
+
69
+ //# sourceMappingURL=settings-ai.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings-ai.mjs","names":[],"sources":["../../src/pages/settings-ai.tsx"],"sourcesContent":["import type { Project } from \"@storyshelf/core/schema\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\nimport {\n Alert,\n Button,\n Card,\n FormActions,\n Meta,\n SectionTitle,\n SelectField,\n} from \"../ui/components.tsx\";\nimport { csrfField } from \"../ui/csrf-field.tsx\";\n\n/** AI availability and profile choices passed to the settings tab. */\nexport interface SettingsAiData {\n available: boolean;\n profiles: string[];\n defaultProfile: string;\n isSiteAdmin: boolean;\n}\n\nfunction ProfileForm(props: {\n project: Project;\n ai: SettingsAiData;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, ai } = props;\n return (\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/ai`}\n hx-post={`/projects/${project.slug}/settings/ai`}\n hx-target=\"body\"\n hx-swap=\"outerHTML\"\n >\n {csrfField()}\n <SelectField\n label=\"AI profile\"\n name=\"aiProfile\"\n value={project.aiProfile ?? \"\"}\n disabled={!ai.isSiteAdmin}\n options={[\n { value: \"\", label: \"Off (no data leaves the server)\" },\n ...ai.profiles.map((name) => ({\n value: name,\n label: name === ai.defaultProfile ? `${name} (default)` : name,\n })),\n ]}\n hint=\"Only site admins can change this because it decides where project data is sent.\"\n />\n {ai.isSiteAdmin ? (\n <FormActions>\n <Button variant=\"primary\" type=\"submit\" icon=\"check\">\n Save changes\n </Button>\n </FormActions>\n ) : (\n <Meta>You need site admin access to change the AI profile.</Meta>\n )}\n </form>\n );\n}\n\n/** AI settings tab: per-project profile gate (site admins choose; others read). */\nexport function renderSettingsAi(\n project: Project,\n ai: SettingsAiData | undefined,\n formState?: { globalError?: string },\n): unknown {\n if (!ai?.available) {\n return (\n <Alert tone=\"info\" title=\"AI is not configured\">\n This server has no AI configured, so insights are unavailable.\n </Alert>\n );\n }\n return (\n <div class=\"grid max-w-form\">\n {formState?.globalError ? <Alert tone=\"danger\">{formState.globalError}</Alert> : null}\n <Card>\n <SectionTitle>AI insights</SectionTitle>\n <Meta>\n Choosing a profile turns on AI triage for builds and a project health digest. The profile\n decides which models see this project&apos;s data (including screenshots when its model\n supports vision). Insights are advisory only: they never approve or reject anything.\n </Meta>\n <ProfileForm project={project} ai={ai} />\n </Card>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;AAsBA,SAAS,YAAY,OAG8B;CACjD,MAAM,EAAE,SAAS,OAAO;CACxB,OACE,qBAAC,QAAD;EACE,QAAO;EACP,QAAQ,aAAa,QAAQ,KAAK;EAClC,WAAS,aAAa,QAAQ,KAAK;EACnC,aAAU;EACV,WAAQ;EALV,UAAA;GAOG,UAAU;GACX,oBAAC,aAAD;IACE,OAAM;IACN,MAAK;IACL,OAAO,QAAQ,aAAa;IAC5B,UAAU,CAAC,GAAG;IACd,SAAS,CACP;KAAE,OAAO;KAAI,OAAO;IAAkC,GACtD,GAAG,GAAG,SAAS,KAAK,UAAU;KAC5B,OAAO;KACP,OAAO,SAAS,GAAG,iBAAiB,GAAG,KAAK,cAAc;IAC5D,EAAE,CACJ;IACA,MAAK;GACN,CAAA;GACA,GAAG,cACF,oBAAC,aAAD,EAAA,UACE,oBAAC,QAAD;IAAQ,SAAQ;IAAU,MAAK;IAAS,MAAK;IAAQ,UAAA;GAE7C,CAAA,EACG,CAAA,IAEb,oBAAC,MAAD,EAAA,UAAM,uDAA0D,CAAA;EAE9D;;AAEV;;AAGA,SAAgB,iBACd,SACA,IACA,WACS;CACT,IAAI,CAAC,IAAI,WACP,OACE,oBAAC,OAAD;EAAO,MAAK;EAAO,OAAM;EAAuB,UAAA;CAEzC,CAAA;CAGX,OACE,qBAAC,OAAD;EAAK,OAAM;EAAX,UAAA,CACG,WAAW,cAAc,oBAAC,OAAD;GAAO,MAAK;GAAU,UAAA,UAAU;EAAmB,CAAA,IAAI,MACjF,qBAAC,MAAD,EAAA,UAAA;GACE,oBAAC,cAAD,EAAA,UAAc,cAAyB,CAAA;GACvC,oBAAC,MAAD,EAAA,UAAM,oQAIA,CAAA;GACN,oBAAC,aAAD;IAAsB;IAAa;GAAK,CAAA;EACpC,EAAA,CAAA,CACH;;AAET"}
@@ -0,0 +1,124 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { Button } from "../ui/buttons.mjs";
5
+ import { Alert, Meta } from "../ui/feedback.mjs";
6
+ import { CheckField, Field, FormActions, SelectField, TextareaField } from "../ui/forms.mjs";
7
+ import { Card, SectionTitle } from "../ui/layout.mjs";
8
+ import { csrfField } from "../ui/csrf-field.mjs";
9
+ import { targetChoices } from "./target-choices.mjs";
10
+ import { MAX_STORYBOOK_URL_TTL, MIN_STORYBOOK_URL_TTL_OVERRIDE } from "@storyshelf/core/utils";
11
+ import { MAX_BROWSERS, parseBrowsers, parseModesAllowlist } from "@storyshelf/core/capture";
12
+ import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
13
+ import { isCaptureTarget } from "@storyshelf/core/adapter/capture-runner";
14
+ //#region src/pages/settings-capture.tsx
15
+ /** Primary engine select plus one checkbox per additional engine. */
16
+ function BrowserFields(props) {
17
+ const { primary, extras, disabled, targets } = props;
18
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
19
+ /* @__PURE__ */ jsx(SelectField, {
20
+ label: "Primary browser",
21
+ name: "browser",
22
+ value: primary,
23
+ disabled,
24
+ options: targets.choices,
25
+ hint: "Used for the published preview and to bootstrap other engines."
26
+ }),
27
+ targets.choices.map((engine) => /* @__PURE__ */ jsx(CheckField, {
28
+ label: `Also capture on ${engine.label}`,
29
+ name: `extra_${engine.value}`,
30
+ value: engine.value,
31
+ checked: extras.includes(engine.value) ? true : void 0,
32
+ disabled
33
+ }, engine.value)),
34
+ targets.unavailable.length > 0 ? /* @__PURE__ */ jsxs(Meta, { children: [
35
+ "Not selectable on this server: ",
36
+ targets.unavailable.join("; "),
37
+ "."
38
+ ] }) : null
39
+ ] });
40
+ }
41
+ /** One mode name per line; empty runs every declared mode. */
42
+ function AllowlistField(props) {
43
+ return /* @__PURE__ */ jsx(TextareaField, {
44
+ label: "Mode allowlist",
45
+ name: "modesAllowlist",
46
+ rows: 4,
47
+ value: props.allowlist.join("\n"),
48
+ placeholder: "dark\nmobile",
49
+ hint: "One mode name per line. Empty runs every mode declared in your stories' parameters."
50
+ });
51
+ }
52
+ /** The project's override of the signed Storybook URL lifetime (ADR 0007, decision 5). */
53
+ function UrlLifetimeField(props) {
54
+ const { project, disabled } = props;
55
+ return /* @__PURE__ */ jsx(Field, {
56
+ label: "Signed Storybook URL lifetime (seconds)",
57
+ name: "storybookUrlTtlSeconds",
58
+ type: "number",
59
+ step: "1",
60
+ min: String(MIN_STORYBOOK_URL_TTL_OVERRIDE),
61
+ max: String(MAX_STORYBOOK_URL_TTL),
62
+ value: project.storybookUrlTtlSeconds ? String(project.storybookUrlTtlSeconds) : "",
63
+ placeholder: "Derived from the stories",
64
+ disabled,
65
+ hint: `How long the URL given to a remote browser that cannot reach this server stays valid (${MIN_STORYBOOK_URL_TTL_OVERRIDE} to ${MAX_STORYBOOK_URL_TTL}; the floor is one slice of captures plus a grace, so a render never outlives its URL). Empty derives it from the build's size: at least 2 hours, at most 24.`
66
+ });
67
+ }
68
+ /** Capture settings tab: engines to capture on and the mode allowlist. */
69
+ function renderSettingsCapture(project, isAdmin, formState, offered) {
70
+ const browsers = parseBrowsers(project.browsers) ?? [project.browser ?? "chromium"];
71
+ const [primary = "chromium", ...extras] = browsers;
72
+ const targets = targetChoices(offered, browsers.filter((entry) => isCaptureTarget(entry)));
73
+ const allowlist = parseModesAllowlist(project.modesAllowlist) ?? [];
74
+ const action = `/projects/${project.slug}/settings/capture`;
75
+ return /* @__PURE__ */ jsxs("div", {
76
+ class: "grid max-w-form",
77
+ children: [formState?.globalError ? /* @__PURE__ */ jsx(Alert, {
78
+ tone: "danger",
79
+ children: formState.globalError
80
+ }) : null, /* @__PURE__ */ jsxs(Card, { children: [
81
+ /* @__PURE__ */ jsx(SectionTitle, { children: "Browsers" }),
82
+ /* @__PURE__ */ jsxs(Meta, { children: [
83
+ "Every build is captured on each selected target, one after another, with a baseline per target. One approval covers a story and mode on all engines; use",
84
+ " ",
85
+ /* @__PURE__ */ jsx("em", { children: "Only this browser" }),
86
+ " in the review for an engine-specific decision. Up to",
87
+ " ",
88
+ MAX_BROWSERS,
89
+ " targets. A newly added target starts from its first build."
90
+ ] }),
91
+ /* @__PURE__ */ jsxs("form", {
92
+ method: "post",
93
+ action,
94
+ "hx-post": action,
95
+ "hx-target": "body",
96
+ "hx-swap": "outerHTML",
97
+ children: [
98
+ csrfField(),
99
+ /* @__PURE__ */ jsx(BrowserFields, {
100
+ primary,
101
+ extras,
102
+ disabled: !isAdmin,
103
+ targets
104
+ }),
105
+ /* @__PURE__ */ jsx(AllowlistField, { allowlist }),
106
+ /* @__PURE__ */ jsx(UrlLifetimeField, {
107
+ project,
108
+ disabled: !isAdmin
109
+ }),
110
+ isAdmin ? /* @__PURE__ */ jsx(FormActions, { children: /* @__PURE__ */ jsx(Button, {
111
+ variant: "primary",
112
+ type: "submit",
113
+ icon: "check",
114
+ children: "Save changes"
115
+ }) }) : /* @__PURE__ */ jsx(Meta, { children: "You need admin access to edit settings." })
116
+ ]
117
+ })
118
+ ] })]
119
+ });
120
+ }
121
+ //#endregion
122
+ export { renderSettingsCapture };
123
+
124
+ //# sourceMappingURL=settings-capture.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings-capture.mjs","names":[],"sources":["../../src/pages/settings-capture.tsx"],"sourcesContent":["import { isCaptureTarget, type RunnerTarget } from \"@storyshelf/core/adapter/capture-runner\";\nimport { MAX_BROWSERS, parseBrowsers, parseModesAllowlist } from \"@storyshelf/core/capture\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport { MAX_STORYBOOK_URL_TTL, MIN_STORYBOOK_URL_TTL_OVERRIDE } from \"@storyshelf/core/utils\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport {\n Alert,\n Button,\n Card,\n CheckField,\n Field,\n FormActions,\n Meta,\n SectionTitle,\n SelectField,\n TextareaField,\n} from \"../ui/components.tsx\";\nimport { csrfField } from \"../ui/csrf-field.tsx\";\nimport { targetChoices, type TargetChoices } from \"./target-choices.ts\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\n/** Primary engine select plus one checkbox per additional engine. */\nfunction BrowserFields(props: {\n primary: string;\n extras: string[];\n disabled: boolean;\n targets: TargetChoices;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { primary, extras, disabled, targets } = props;\n return (\n <>\n <SelectField\n label=\"Primary browser\"\n name=\"browser\"\n value={primary}\n disabled={disabled}\n options={targets.choices}\n hint=\"Used for the published preview and to bootstrap other engines.\"\n />\n {targets.choices.map((engine) => (\n <CheckField\n key={engine.value}\n label={`Also capture on ${engine.label}`}\n name={`extra_${engine.value}`}\n value={engine.value}\n checked={extras.includes(engine.value) ? true : undefined}\n disabled={disabled}\n />\n ))}\n {targets.unavailable.length > 0 ? (\n <Meta>Not selectable on this server: {targets.unavailable.join(\"; \")}.</Meta>\n ) : null}\n </>\n );\n}\n\n/** One mode name per line; empty runs every declared mode. */\nfunction AllowlistField(props: {\n allowlist: string[];\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n return (\n <TextareaField\n label=\"Mode allowlist\"\n name=\"modesAllowlist\"\n rows={4}\n value={props.allowlist.join(\"\\n\")}\n placeholder=\"dark&#10;mobile\"\n hint=\"One mode name per line. Empty runs every mode declared in your stories' parameters.\"\n />\n );\n}\n\n/** The project's override of the signed Storybook URL lifetime (ADR 0007, decision 5). */\nfunction UrlLifetimeField(props: {\n project: Project;\n disabled: boolean;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, disabled } = props;\n return (\n <Field\n label=\"Signed Storybook URL lifetime (seconds)\"\n name=\"storybookUrlTtlSeconds\"\n type=\"number\"\n step=\"1\"\n min={String(MIN_STORYBOOK_URL_TTL_OVERRIDE)}\n max={String(MAX_STORYBOOK_URL_TTL)}\n value={project.storybookUrlTtlSeconds ? String(project.storybookUrlTtlSeconds) : \"\"}\n placeholder=\"Derived from the stories\"\n disabled={disabled}\n hint={`How long the URL given to a remote browser that cannot reach this server stays valid (${MIN_STORYBOOK_URL_TTL_OVERRIDE} to ${MAX_STORYBOOK_URL_TTL}; the floor is one slice of captures plus a grace, so a render never outlives its URL). Empty derives it from the build's size: at least 2 hours, at most 24.`}\n />\n );\n}\n\n/** Capture settings tab: engines to capture on and the mode allowlist. */\nexport function renderSettingsCapture(\n project: Project,\n isAdmin: boolean,\n formState?: { globalError?: string },\n /** What the runner offers; omitted when it publishes no list. */\n offered?: readonly RunnerTarget[],\n): unknown {\n const browsers = parseBrowsers(project.browsers) ?? [project.browser ?? \"chromium\"];\n const [primary = \"chromium\", ...extras] = browsers;\n const targets = targetChoices(\n offered,\n browsers.filter((entry) => isCaptureTarget(entry)),\n );\n const allowlist = parseModesAllowlist(project.modesAllowlist) ?? [];\n const action = `/projects/${project.slug}/settings/capture`;\n return (\n <div class=\"grid max-w-form\">\n {formState?.globalError ? <Alert tone=\"danger\">{formState.globalError}</Alert> : null}\n <Card>\n <SectionTitle>Browsers</SectionTitle>\n <Meta>\n Every build is captured on each selected target, one after another, with a baseline per\n target. One approval covers a story and mode on all engines; use{\" \"}\n <em>Only this browser</em> in the review for an engine-specific decision. Up to{\" \"}\n {MAX_BROWSERS} targets. A newly added target starts from its first build.\n </Meta>\n <form method=\"post\" action={action} hx-post={action} hx-target=\"body\" hx-swap=\"outerHTML\">\n {csrfField()}\n <BrowserFields primary={primary} extras={extras} disabled={!isAdmin} targets={targets} />\n <AllowlistField allowlist={allowlist} />\n <UrlLifetimeField project={project} disabled={!isAdmin} />\n {isAdmin ? (\n <FormActions>\n <Button variant=\"primary\" type=\"submit\" icon=\"check\">\n Save changes\n </Button>\n </FormActions>\n ) : (\n <Meta>You need admin access to edit settings.</Meta>\n )}\n </form>\n </Card>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;AAuBA,SAAS,cAAc,OAK4B;CACjD,MAAM,EAAE,SAAS,QAAQ,UAAU,YAAY;CAC/C,OACE,qBAAA,UAAA,EAAA,UAAA;EACE,oBAAC,aAAD;GACE,OAAM;GACN,MAAK;GACL,OAAO;GACG;GACV,SAAS,QAAQ;GACjB,MAAK;EACN,CAAA;EACA,QAAQ,QAAQ,KAAK,WACpB,oBAAC,YAAD;GAEE,OAAO,mBAAmB,OAAO;GACjC,MAAM,SAAS,OAAO;GACtB,OAAO,OAAO;GACd,SAAS,OAAO,SAAS,OAAO,KAAK,IAAI,OAAO,KAAA;GACtC;EACX,GANM,OAAO,KAMb,CACF;EACA,QAAQ,YAAY,SAAS,IAC5B,qBAAC,MAAD,EAAA,UAAA;GAAM;GAAgC,QAAQ,YAAY,KAAK,IAAI;GAAE;EAAO,EAAA,CAAA,IAC1E;CACJ,EAAA,CAAA;AAEN;;AAGA,SAAS,eAAe,OAE2B;CACjD,OACE,oBAAC,eAAD;EACE,OAAM;EACN,MAAK;EACL,MAAM;EACN,OAAO,MAAM,UAAU,KAAK,IAAI;EAChC,aAAY;EACZ,MAAK;CACN,CAAA;AAEL;;AAGA,SAAS,iBAAiB,OAGyB;CACjD,MAAM,EAAE,SAAS,aAAa;CAC9B,OACE,oBAAC,OAAD;EACE,OAAM;EACN,MAAK;EACL,MAAK;EACL,MAAK;EACL,KAAK,OAAO,8BAA8B;EAC1C,KAAK,OAAO,qBAAqB;EACjC,OAAO,QAAQ,yBAAyB,OAAO,QAAQ,sBAAsB,IAAI;EACjF,aAAY;EACF;EACV,MAAM,yFAAyF,+BAA+B,MAAM,sBAAsB;CAC3J,CAAA;AAEL;;AAGA,SAAgB,sBACd,SACA,SACA,WAEA,SACS;CACT,MAAM,WAAW,cAAc,QAAQ,QAAQ,KAAK,CAAC,QAAQ,WAAW,UAAU;CAClF,MAAM,CAAC,UAAU,YAAY,GAAG,UAAU;CAC1C,MAAM,UAAU,cACd,SACA,SAAS,QAAQ,UAAU,gBAAgB,KAAK,CAAC,CACnD;CACA,MAAM,YAAY,oBAAoB,QAAQ,cAAc,KAAK,CAAC;CAClE,MAAM,SAAS,aAAa,QAAQ,KAAK;CACzC,OACE,qBAAC,OAAD;EAAK,OAAM;EAAX,UAAA,CACG,WAAW,cAAc,oBAAC,OAAD;GAAO,MAAK;GAAU,UAAA,UAAU;EAAmB,CAAA,IAAI,MACjF,qBAAC,MAAD,EAAA,UAAA;GACE,oBAAC,cAAD,EAAA,UAAc,WAAsB,CAAA;GACpC,qBAAC,MAAD,EAAA,UAAA;IAAM;IAE6D;IACjE,oBAAC,MAAD,EAAA,UAAI,oBAAqB,CAAA;IAAC;IAAsD;IAC/E;IAAa;GACV,EAAA,CAAA;GACN,qBAAC,QAAD;IAAM,QAAO;IAAe;IAAQ,WAAS;IAAQ,aAAU;IAAO,WAAQ;IAA9E,UAAA;KACG,UAAU;KACX,oBAAC,eAAD;MAAwB;MAAiB;MAAQ,UAAU,CAAC;MAAkB;KAAU,CAAA;KACxF,oBAAC,gBAAD,EAA2B,UAAY,CAAA;KACvC,oBAAC,kBAAD;MAA2B;MAAS,UAAU,CAAC;KAAU,CAAA;KACxD,UACC,oBAAC,aAAD,EAAA,UACE,oBAAC,QAAD;MAAQ,SAAQ;MAAU,MAAK;MAAS,MAAK;MAAQ,UAAA;KAE7C,CAAA,EACG,CAAA,IAEb,oBAAC,MAAD,EAAA,UAAM,0CAA6C,CAAA;IAEjD;;EACF,EAAA,CAAA,CACH;;AAET"}