@storyshelf/app 0.6.1 → 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 (364) 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/credentials.mjs +3 -2
  26. package/dist/credentials.mjs.map +1 -1
  27. package/dist/git-status.mjs +46 -0
  28. package/dist/git-status.mjs.map +1 -0
  29. package/dist/hidden-data.mjs +24 -0
  30. package/dist/hidden-data.mjs.map +1 -0
  31. package/dist/index.d.mts +3 -3
  32. package/dist/index.mjs +202 -124
  33. package/dist/index.mjs.map +1 -1
  34. package/dist/insights/budget.mjs +49 -0
  35. package/dist/insights/budget.mjs.map +1 -0
  36. package/dist/insights/deps.mjs +21 -0
  37. package/dist/insights/deps.mjs.map +1 -0
  38. package/dist/insights/errors.mjs +22 -0
  39. package/dist/insights/errors.mjs.map +1 -0
  40. package/dist/insights/health-panel.mjs +40 -0
  41. package/dist/insights/health-panel.mjs.map +1 -0
  42. package/dist/insights/job.mjs +103 -0
  43. package/dist/insights/job.mjs.map +1 -0
  44. package/dist/insights/load-health.mjs +59 -0
  45. package/dist/insights/load-health.mjs.map +1 -0
  46. package/dist/insights/load-triage.mjs +98 -0
  47. package/dist/insights/load-triage.mjs.map +1 -0
  48. package/dist/insights/panel.mjs +28 -0
  49. package/dist/insights/panel.mjs.map +1 -0
  50. package/dist/insights/request.mjs +113 -0
  51. package/dist/insights/request.mjs.map +1 -0
  52. package/dist/insights/stale.mjs +21 -0
  53. package/dist/insights/stale.mjs.map +1 -0
  54. package/dist/insights/usage-report.mjs +60 -0
  55. package/dist/insights/usage-report.mjs.map +1 -0
  56. package/dist/insights/view.mjs +48 -0
  57. package/dist/insights/view.mjs.map +1 -0
  58. package/dist/job-canceller.mjs +14 -0
  59. package/dist/job-canceller.mjs.map +1 -0
  60. package/dist/job-queue.mjs +51 -0
  61. package/dist/job-queue.mjs.map +1 -0
  62. package/dist/jobs-setup.mjs +132 -0
  63. package/dist/jobs-setup.mjs.map +1 -0
  64. package/dist/lifecycle.mjs +109 -19
  65. package/dist/lifecycle.mjs.map +1 -1
  66. package/dist/middleware/auth-gate.mjs +4 -2
  67. package/dist/middleware/auth-gate.mjs.map +1 -1
  68. package/dist/middleware/rate-limit.mjs +41 -33
  69. package/dist/middleware/rate-limit.mjs.map +1 -1
  70. package/dist/middleware/request-log.mjs +3 -2
  71. package/dist/middleware/request-log.mjs.map +1 -1
  72. package/dist/middleware/require-review.mjs +51 -0
  73. package/dist/middleware/require-review.mjs.map +1 -0
  74. package/dist/middleware/require-testing.mjs +41 -0
  75. package/dist/middleware/require-testing.mjs.map +1 -0
  76. package/dist/middleware/setup-gate.mjs.map +1 -1
  77. package/dist/middleware/store-scope.mjs +12 -2
  78. package/dist/middleware/store-scope.mjs.map +1 -1
  79. package/dist/notifications-pause.mjs +33 -0
  80. package/dist/notifications-pause.mjs.map +1 -0
  81. package/dist/notify.d.mts +2 -1
  82. package/dist/notify.mjs +78 -39
  83. package/dist/notify.mjs.map +1 -1
  84. package/dist/pages/a11y-violations.mjs +116 -0
  85. package/dist/pages/a11y-violations.mjs.map +1 -0
  86. package/dist/pages/admin-ai.mjs +78 -0
  87. package/dist/pages/admin-ai.mjs.map +1 -0
  88. package/dist/pages/admin-system-origin.mjs +123 -0
  89. package/dist/pages/admin-system-origin.mjs.map +1 -0
  90. package/dist/pages/admin-system-targets.mjs +53 -0
  91. package/dist/pages/admin-system-targets.mjs.map +1 -0
  92. package/dist/pages/admin-system.mjs +63 -13
  93. package/dist/pages/admin-system.mjs.map +1 -1
  94. package/dist/pages/build-detail.mjs +104 -76
  95. package/dist/pages/build-detail.mjs.map +1 -1
  96. package/dist/pages/build-diff-actions.mjs +87 -33
  97. package/dist/pages/build-diff-actions.mjs.map +1 -1
  98. package/dist/pages/build-diff-comments.mjs +16 -14
  99. package/dist/pages/build-diff-comments.mjs.map +1 -1
  100. package/dist/pages/build-diff-environment.mjs +82 -0
  101. package/dist/pages/build-diff-environment.mjs.map +1 -0
  102. package/dist/pages/build-diff-header.mjs +22 -4
  103. package/dist/pages/build-diff-header.mjs.map +1 -1
  104. package/dist/pages/build-diff-insight.mjs +113 -0
  105. package/dist/pages/build-diff-insight.mjs.map +1 -0
  106. package/dist/pages/build-diff-model.mjs +55 -10
  107. package/dist/pages/build-diff-model.mjs.map +1 -1
  108. package/dist/pages/build-diff-nav.mjs +28 -12
  109. package/dist/pages/build-diff-nav.mjs.map +1 -1
  110. package/dist/pages/build-diff-shortcuts.mjs +5 -1
  111. package/dist/pages/build-diff-shortcuts.mjs.map +1 -1
  112. package/dist/pages/build-diff-usage.mjs +63 -0
  113. package/dist/pages/build-diff-usage.mjs.map +1 -0
  114. package/dist/pages/build-diff-viewer.mjs +62 -5
  115. package/dist/pages/build-diff-viewer.mjs.map +1 -1
  116. package/dist/pages/build-diff.mjs +34 -13
  117. package/dist/pages/build-diff.mjs.map +1 -1
  118. package/dist/pages/build-tests.mjs +74 -0
  119. package/dist/pages/build-tests.mjs.map +1 -0
  120. package/dist/pages/component-detail.mjs +249 -0
  121. package/dist/pages/component-detail.mjs.map +1 -0
  122. package/dist/pages/component-stories.mjs +168 -0
  123. package/dist/pages/component-stories.mjs.map +1 -0
  124. package/dist/pages/compute-jobs.mjs +13 -26
  125. package/dist/pages/compute-jobs.mjs.map +1 -1
  126. package/dist/pages/design-links.mjs +182 -0
  127. package/dist/pages/design-links.mjs.map +1 -0
  128. package/dist/pages/environment-change.mjs +53 -0
  129. package/dist/pages/environment-change.mjs.map +1 -0
  130. package/dist/pages/health-panel.mjs +126 -0
  131. package/dist/pages/health-panel.mjs.map +1 -0
  132. package/dist/pages/library-grid.mjs +105 -0
  133. package/dist/pages/library-grid.mjs.map +1 -0
  134. package/dist/pages/library-model.mjs +47 -0
  135. package/dist/pages/library-model.mjs.map +1 -0
  136. package/dist/pages/library-snapshots.mjs +130 -0
  137. package/dist/pages/library-snapshots.mjs.map +1 -0
  138. package/dist/pages/library.mjs +112 -218
  139. package/dist/pages/library.mjs.map +1 -1
  140. package/dist/pages/notifications-paused.mjs +22 -0
  141. package/dist/pages/notifications-paused.mjs.map +1 -0
  142. package/dist/pages/profile.mjs +53 -48
  143. package/dist/pages/profile.mjs.map +1 -1
  144. package/dist/pages/project-builds-table.mjs +2 -2
  145. package/dist/pages/project-builds-table.mjs.map +1 -1
  146. package/dist/pages/project-builds.mjs +51 -21
  147. package/dist/pages/project-builds.mjs.map +1 -1
  148. package/dist/pages/project-settings.mjs +51 -12
  149. package/dist/pages/project-settings.mjs.map +1 -1
  150. package/dist/pages/projects-card.mjs +14 -1
  151. package/dist/pages/projects-card.mjs.map +1 -1
  152. package/dist/pages/projects.mjs +6 -2
  153. package/dist/pages/projects.mjs.map +1 -1
  154. package/dist/pages/review-changeset.mjs +116 -0
  155. package/dist/pages/review-changeset.mjs.map +1 -0
  156. package/dist/pages/review-comments.mjs +103 -0
  157. package/dist/pages/review-comments.mjs.map +1 -0
  158. package/dist/pages/review-verdict.mjs +102 -0
  159. package/dist/pages/review-verdict.mjs.map +1 -0
  160. package/dist/pages/settings-ai.mjs +69 -0
  161. package/dist/pages/settings-ai.mjs.map +1 -0
  162. package/dist/pages/settings-capture.mjs +124 -0
  163. package/dist/pages/settings-capture.mjs.map +1 -0
  164. package/dist/pages/settings-design-credential.mjs +99 -0
  165. package/dist/pages/settings-design-credential.mjs.map +1 -0
  166. package/dist/pages/settings-design-links.mjs +179 -0
  167. package/dist/pages/settings-design-links.mjs.map +1 -0
  168. package/dist/pages/settings-design.mjs +48 -0
  169. package/dist/pages/settings-design.mjs.map +1 -0
  170. package/dist/pages/settings-general.mjs +46 -21
  171. package/dist/pages/settings-general.mjs.map +1 -1
  172. package/dist/pages/settings-labels.mjs +43 -2
  173. package/dist/pages/settings-labels.mjs.map +1 -1
  174. package/dist/pages/settings-links.mjs +112 -0
  175. package/dist/pages/settings-links.mjs.map +1 -0
  176. package/dist/pages/settings-notifications-git.mjs +128 -0
  177. package/dist/pages/settings-notifications-git.mjs.map +1 -0
  178. package/dist/pages/settings-notifications.mjs +79 -42
  179. package/dist/pages/settings-notifications.mjs.map +1 -1
  180. package/dist/pages/settings-tests.mjs +33 -2
  181. package/dist/pages/settings-tests.mjs.map +1 -1
  182. package/dist/pages/target-choices.mjs +39 -0
  183. package/dist/pages/target-choices.mjs.map +1 -0
  184. package/dist/publish-setup.mjs +89 -0
  185. package/dist/publish-setup.mjs.map +1 -0
  186. package/dist/retention-timer.mjs +2 -1
  187. package/dist/retention-timer.mjs.map +1 -1
  188. package/dist/review-events.mjs +48 -0
  189. package/dist/review-events.mjs.map +1 -0
  190. package/dist/routers/admin-pages.mjs +45 -3
  191. package/dist/routers/admin-pages.mjs.map +1 -1
  192. package/dist/routers/admin-public-origin.mjs +85 -0
  193. package/dist/routers/admin-public-origin.mjs.map +1 -0
  194. package/dist/routers/assets.mjs +9 -2
  195. package/dist/routers/assets.mjs.map +1 -1
  196. package/dist/routers/auth.mjs +2 -2
  197. package/dist/routers/auth.mjs.map +1 -1
  198. package/dist/routers/baseline-guard.mjs +2 -2
  199. package/dist/routers/baseline-guard.mjs.map +1 -1
  200. package/dist/routers/baselines.handlers.mjs +64 -0
  201. package/dist/routers/baselines.handlers.mjs.map +1 -0
  202. package/dist/routers/baselines.mjs +125 -0
  203. package/dist/routers/baselines.mjs.map +1 -0
  204. package/dist/routers/build-cancel.mjs +68 -0
  205. package/dist/routers/build-cancel.mjs.map +1 -0
  206. package/dist/routers/build-changeset.mjs +70 -0
  207. package/dist/routers/build-changeset.mjs.map +1 -0
  208. package/dist/routers/build-reviews.handlers.mjs +86 -0
  209. package/dist/routers/build-reviews.handlers.mjs.map +1 -0
  210. package/dist/routers/build-reviews.mjs +74 -0
  211. package/dist/routers/build-reviews.mjs.map +1 -0
  212. package/dist/routers/builds.handlers.mjs +46 -55
  213. package/dist/routers/builds.handlers.mjs.map +1 -1
  214. package/dist/routers/builds.mjs +71 -17
  215. package/dist/routers/builds.mjs.map +1 -1
  216. package/dist/routers/capture-config.mjs +35 -0
  217. package/dist/routers/capture-config.mjs.map +1 -0
  218. package/dist/routers/comments.mjs +41 -15
  219. package/dist/routers/comments.mjs.map +1 -1
  220. package/dist/routers/component-catalog.handlers.mjs +71 -0
  221. package/dist/routers/component-catalog.handlers.mjs.map +1 -0
  222. package/dist/routers/component.handlers.mjs +121 -0
  223. package/dist/routers/component.handlers.mjs.map +1 -0
  224. package/dist/routers/connections.mjs +118 -0
  225. package/dist/routers/connections.mjs.map +1 -0
  226. package/dist/routers/design-connect.handlers.mjs +44 -0
  227. package/dist/routers/design-connect.handlers.mjs.map +1 -0
  228. package/dist/routers/design.handlers.mjs +114 -0
  229. package/dist/routers/design.handlers.mjs.map +1 -0
  230. package/dist/routers/health-report.mjs +2 -3
  231. package/dist/routers/health-report.mjs.map +1 -1
  232. package/dist/routers/health.mjs +4 -3
  233. package/dist/routers/health.mjs.map +1 -1
  234. package/dist/routers/helpers.mjs +19 -3
  235. package/dist/routers/helpers.mjs.map +1 -1
  236. package/dist/routers/insights-ui.mjs +64 -0
  237. package/dist/routers/insights-ui.mjs.map +1 -0
  238. package/dist/routers/insights.handlers.mjs +45 -0
  239. package/dist/routers/insights.handlers.mjs.map +1 -0
  240. package/dist/routers/insights.mjs +189 -0
  241. package/dist/routers/insights.mjs.map +1 -0
  242. package/dist/routers/links.handlers.mjs +80 -0
  243. package/dist/routers/links.handlers.mjs.map +1 -0
  244. package/dist/routers/links.mjs +139 -0
  245. package/dist/routers/links.mjs.map +1 -0
  246. package/dist/routers/media.mjs +18 -4
  247. package/dist/routers/media.mjs.map +1 -1
  248. package/dist/routers/members.mjs +2 -1
  249. package/dist/routers/members.mjs.map +1 -1
  250. package/dist/routers/notification-channels.mjs +17 -23
  251. package/dist/routers/notification-channels.mjs.map +1 -1
  252. package/dist/routers/oauth-state.mjs +45 -0
  253. package/dist/routers/oauth-state.mjs.map +1 -0
  254. package/dist/routers/preview-base.mjs +38 -0
  255. package/dist/routers/preview-base.mjs.map +1 -0
  256. package/dist/routers/preview-token.mjs +51 -0
  257. package/dist/routers/preview-token.mjs.map +1 -0
  258. package/dist/routers/profile.mjs +4 -2
  259. package/dist/routers/profile.mjs.map +1 -1
  260. package/dist/routers/projects.mjs +17 -1
  261. package/dist/routers/projects.mjs.map +1 -1
  262. package/dist/routers/publish-upload.mjs +78 -0
  263. package/dist/routers/publish-upload.mjs.map +1 -0
  264. package/dist/routers/published-access.mjs +52 -0
  265. package/dist/routers/published-access.mjs.map +1 -0
  266. package/dist/routers/published-files.mjs +52 -0
  267. package/dist/routers/published-files.mjs.map +1 -0
  268. package/dist/routers/review-forms.mjs +72 -0
  269. package/dist/routers/review-forms.mjs.map +1 -0
  270. package/dist/routers/review-section.handlers.mjs +85 -0
  271. package/dist/routers/review-section.handlers.mjs.map +1 -0
  272. package/dist/routers/review-switch-off.mjs +29 -0
  273. package/dist/routers/review-switch-off.mjs.map +1 -0
  274. package/dist/routers/schemas.mjs +140 -22
  275. package/dist/routers/schemas.mjs.map +1 -1
  276. package/dist/routers/scoped-models.mjs +22 -0
  277. package/dist/routers/scoped-models.mjs.map +1 -0
  278. package/dist/routers/settings-ai.mjs +35 -0
  279. package/dist/routers/settings-ai.mjs.map +1 -0
  280. package/dist/routers/settings-capture.mjs +77 -0
  281. package/dist/routers/settings-capture.mjs.map +1 -0
  282. package/dist/routers/settings-design.mjs +103 -0
  283. package/dist/routers/settings-design.mjs.map +1 -0
  284. package/dist/routers/settings-general.mjs +55 -10
  285. package/dist/routers/settings-general.mjs.map +1 -1
  286. package/dist/routers/settings-git-channel.mjs +77 -0
  287. package/dist/routers/settings-git-channel.mjs.map +1 -0
  288. package/dist/routers/settings-labels.mjs +27 -1
  289. package/dist/routers/settings-labels.mjs.map +1 -1
  290. package/dist/routers/settings-links.mjs +63 -0
  291. package/dist/routers/settings-links.mjs.map +1 -0
  292. package/dist/routers/settings-members.mjs +4 -7
  293. package/dist/routers/settings-members.mjs.map +1 -1
  294. package/dist/routers/settings-notifications.mjs +15 -10
  295. package/dist/routers/settings-notifications.mjs.map +1 -1
  296. package/dist/routers/settings-tokens.mjs +2 -5
  297. package/dist/routers/settings-tokens.mjs.map +1 -1
  298. package/dist/routers/settings-webhooks.mjs +7 -24
  299. package/dist/routers/settings-webhooks.mjs.map +1 -1
  300. package/dist/routers/settings.handlers.mjs +107 -26
  301. package/dist/routers/settings.handlers.mjs.map +1 -1
  302. package/dist/routers/settings.mjs +16 -4
  303. package/dist/routers/settings.mjs.map +1 -1
  304. package/dist/routers/signed-storybook.mjs +53 -0
  305. package/dist/routers/signed-storybook.mjs.map +1 -0
  306. package/dist/routers/snapshot-groups.mjs +63 -0
  307. package/dist/routers/snapshot-groups.mjs.map +1 -0
  308. package/dist/routers/snapshots.mjs +103 -5
  309. package/dist/routers/snapshots.mjs.map +1 -1
  310. package/dist/routers/storybook.mjs +37 -88
  311. package/dist/routers/storybook.mjs.map +1 -1
  312. package/dist/routers/tokens.mjs +2 -5
  313. package/dist/routers/tokens.mjs.map +1 -1
  314. package/dist/routers/ui.mjs +34 -5
  315. package/dist/routers/ui.mjs.map +1 -1
  316. package/dist/routers/webhooks.mjs +5 -11
  317. package/dist/routers/webhooks.mjs.map +1 -1
  318. package/dist/routers/well-known.mjs +11 -4
  319. package/dist/routers/well-known.mjs.map +1 -1
  320. package/dist/routes/api-routes.mjs +93 -0
  321. package/dist/routes/api-routes.mjs.map +1 -0
  322. package/dist/routes/classification.mjs +38 -0
  323. package/dist/routes/classification.mjs.map +1 -0
  324. package/dist/routes/openapi-classes.mjs +22 -0
  325. package/dist/routes/openapi-classes.mjs.map +1 -0
  326. package/dist/routes/page-routes.mjs +104 -0
  327. package/dist/routes/page-routes.mjs.map +1 -0
  328. package/dist/runtime.mjs +18 -13
  329. package/dist/runtime.mjs.map +1 -1
  330. package/dist/serverless.mjs +5 -0
  331. package/dist/store.mjs +19 -1
  332. package/dist/store.mjs.map +1 -1
  333. package/dist/targets.mjs +46 -0
  334. package/dist/targets.mjs.map +1 -0
  335. package/dist/ui/buttons.mjs +16 -3
  336. package/dist/ui/buttons.mjs.map +1 -1
  337. package/dist/ui/design-reference.mjs +97 -0
  338. package/dist/ui/design-reference.mjs.map +1 -0
  339. package/dist/ui/document.mjs +1 -2
  340. package/dist/ui/document.mjs.map +1 -1
  341. package/dist/ui/forms.mjs +6 -5
  342. package/dist/ui/forms.mjs.map +1 -1
  343. package/dist/ui/icons/paths.mjs +1 -0
  344. package/dist/ui/icons/paths.mjs.map +1 -1
  345. package/dist/ui/preview-embed.mjs +82 -0
  346. package/dist/ui/preview-embed.mjs.map +1 -0
  347. package/dist/ui/shell/client-script.mjs +85 -0
  348. package/dist/ui/shell/client-script.mjs.map +1 -1
  349. package/dist/ui/shell/nav.mjs +12 -8
  350. package/dist/ui/shell/nav.mjs.map +1 -1
  351. package/dist/ui/shell/review-script.mjs +8 -0
  352. package/dist/ui/shell/review-script.mjs.map +1 -1
  353. package/dist/ui/shell/sidebar.mjs +2 -2
  354. package/dist/ui/shell/sidebar.mjs.map +1 -1
  355. package/package.json +11 -6
  356. package/dist/asset-paths.mjs +0 -23
  357. package/dist/asset-paths.mjs.map +0 -1
  358. package/dist/assets/mark.svg +0 -1
  359. package/dist/pages/settings-status.mjs +0 -137
  360. package/dist/pages/settings-status.mjs.map +0 -1
  361. package/dist/routers/settings-status.mjs +0 -77
  362. package/dist/routers/settings-status.mjs.map +0 -1
  363. package/dist/routers/status-configs.mjs +0 -108
  364. package/dist/routers/status-configs.mjs.map +0 -1
@@ -0,0 +1,112 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { Button } from "../ui/buttons.mjs";
5
+ import { Alert, Badge, Meta } from "../ui/feedback.mjs";
6
+ import { Field, FormActions, SelectField } from "../ui/forms.mjs";
7
+ import { Card, SectionTitle } from "../ui/layout.mjs";
8
+ import { Table } from "../ui/table.mjs";
9
+ import { csrfField } from "../ui/csrf-field.mjs";
10
+ import { jsx, jsxs } from "hono/jsx/jsx-runtime";
11
+ //#region src/pages/settings-links.tsx
12
+ const linkRow = (base, link, isAdmin) => /* @__PURE__ */ jsxs("tr", { children: [
13
+ /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(Badge, {
14
+ tone: "neutral",
15
+ children: link.packageName
16
+ }) }),
17
+ /* @__PURE__ */ jsx("td", { children: link.upstreamName }),
18
+ /* @__PURE__ */ jsx("td", { children: isAdmin ? /* @__PURE__ */ jsxs("form", {
19
+ method: "post",
20
+ action: `${base}/delete`,
21
+ "hx-post": `${base}/delete`,
22
+ "hx-target": "body",
23
+ "hx-confirm": "Remove this link?",
24
+ children: [
25
+ csrfField(),
26
+ /* @__PURE__ */ jsx("input", {
27
+ type: "hidden",
28
+ name: "package",
29
+ value: link.packageName
30
+ }),
31
+ /* @__PURE__ */ jsx(Button, {
32
+ variant: "ghost",
33
+ type: "submit",
34
+ "aria-label": `Remove ${link.packageName}`,
35
+ children: "Remove"
36
+ })
37
+ ]
38
+ }) : null })
39
+ ] }, link.packageName);
40
+ /** Links settings tab: linked packages plus the add-link form. */
41
+ function renderSettingsLinks(project, links, candidates, isAdmin, formState) {
42
+ const base = `/projects/${project.slug}/settings/links`;
43
+ return /* @__PURE__ */ jsxs("div", {
44
+ class: "grid max-w-form",
45
+ children: [
46
+ formState?.globalError ? /* @__PURE__ */ jsx(Alert, {
47
+ tone: "danger",
48
+ children: formState.globalError
49
+ }) : null,
50
+ /* @__PURE__ */ jsxs(Card, { children: [
51
+ /* @__PURE__ */ jsx(SectionTitle, { children: "Linked packages" }),
52
+ /* @__PURE__ */ jsx(Meta, { children: "Link a package this project consumes (e.g. @acme/design-system) to the project that publishes it. Review pages then show where a story uses it, with links to the upstream component." }),
53
+ /* @__PURE__ */ jsx(Table, { children: /* @__PURE__ */ jsxs("table", { children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
54
+ /* @__PURE__ */ jsx("th", { children: "Package" }),
55
+ /* @__PURE__ */ jsx("th", { children: "Upstream project" }),
56
+ /* @__PURE__ */ jsx("th", { children: /* @__PURE__ */ jsx("span", {
57
+ class: "visually-hidden",
58
+ children: "Actions"
59
+ }) })
60
+ ] }) }), /* @__PURE__ */ jsx("tbody", { children: links.map((link) => linkRow(base, link, isAdmin)) })] }) }),
61
+ links.length === 0 ? /* @__PURE__ */ jsx(Meta, { children: "No packages linked." }) : null
62
+ ] }),
63
+ isAdmin ? /* @__PURE__ */ jsx(AddLinkCard, {
64
+ base,
65
+ candidates
66
+ }) : null
67
+ ]
68
+ });
69
+ }
70
+ /** Form card linking a package to an upstream project. */
71
+ function AddLinkCard(props) {
72
+ const { base, candidates } = props;
73
+ return /* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsx(SectionTitle, {
74
+ level: 3,
75
+ children: "Link a package"
76
+ }), /* @__PURE__ */ jsxs("form", {
77
+ method: "post",
78
+ action: base,
79
+ "hx-post": base,
80
+ "hx-target": "body",
81
+ children: [
82
+ csrfField(),
83
+ /* @__PURE__ */ jsxs("div", {
84
+ class: "grid grid--2",
85
+ children: [/* @__PURE__ */ jsx(Field, {
86
+ label: "Package",
87
+ name: "package",
88
+ required: true,
89
+ placeholder: "@acme/design-system"
90
+ }), /* @__PURE__ */ jsx(SelectField, {
91
+ label: "Upstream project",
92
+ name: "upstreamSlug",
93
+ required: true,
94
+ options: candidates.map((candidate) => ({
95
+ value: candidate.slug,
96
+ label: candidate.name
97
+ }))
98
+ })]
99
+ }),
100
+ /* @__PURE__ */ jsx(FormActions, { children: /* @__PURE__ */ jsx(Button, {
101
+ variant: "primary",
102
+ type: "submit",
103
+ icon: "plus",
104
+ children: "Link package"
105
+ }) })
106
+ ]
107
+ })] });
108
+ }
109
+ //#endregion
110
+ export { renderSettingsLinks };
111
+
112
+ //# sourceMappingURL=settings-links.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings-links.mjs","names":[],"sources":["../../src/pages/settings-links.tsx"],"sourcesContent":["import type { Project } from \"@storyshelf/core/schema\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport {\n Alert,\n Badge,\n Button,\n Card,\n Field,\n FormActions,\n Meta,\n SectionTitle,\n SelectField,\n Table,\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\n/** A package linked to the upstream project that publishes it. */\nexport interface SettingsLink {\n packageName: string;\n upstreamSlug: string;\n upstreamName: string;\n}\n\n/** Another project that can be chosen as an upstream. */\nexport interface SettingsLinkCandidate {\n slug: string;\n name: string;\n}\n\nconst linkRow = (\n base: string,\n link: SettingsLink,\n isAdmin: boolean,\n): HtmlEscapedString | Promise<HtmlEscapedString> => (\n <tr key={link.packageName}>\n <td>\n <Badge tone=\"neutral\">{link.packageName}</Badge>\n </td>\n <td>{link.upstreamName}</td>\n <td>\n {isAdmin ? (\n <form\n method=\"post\"\n action={`${base}/delete`}\n hx-post={`${base}/delete`}\n hx-target=\"body\"\n hx-confirm=\"Remove this link?\"\n >\n {csrfField()}\n <input type=\"hidden\" name=\"package\" value={link.packageName} />\n <Button variant=\"ghost\" type=\"submit\" aria-label={`Remove ${link.packageName}`}>\n Remove\n </Button>\n </form>\n ) : null}\n </td>\n </tr>\n);\n\n/** Links settings tab: linked packages plus the add-link form. */\nexport function renderSettingsLinks(\n project: Project,\n links: SettingsLink[],\n candidates: SettingsLinkCandidate[],\n isAdmin: boolean,\n formState?: { globalError?: string },\n): unknown {\n const base = `/projects/${project.slug}/settings/links`;\n return (\n <div class=\"grid max-w-form\">\n {formState?.globalError ? <Alert tone=\"danger\">{formState.globalError}</Alert> : null}\n <Card>\n <SectionTitle>Linked packages</SectionTitle>\n <Meta>\n Link a package this project consumes (e.g. @acme/design-system) to the project that\n publishes it. Review pages then show where a story uses it, with links to the upstream\n component.\n </Meta>\n <Table>\n <table>\n <thead>\n <tr>\n <th>Package</th>\n <th>Upstream project</th>\n <th>\n <span class=\"visually-hidden\">Actions</span>\n </th>\n </tr>\n </thead>\n <tbody>{links.map((link) => linkRow(base, link, isAdmin))}</tbody>\n </table>\n </Table>\n {links.length === 0 ? <Meta>No packages linked.</Meta> : null}\n </Card>\n {isAdmin ? <AddLinkCard base={base} candidates={candidates} /> : null}\n </div>\n );\n}\n\n/** Form card linking a package to an upstream project. */\nfunction AddLinkCard(props: {\n base: string;\n candidates: SettingsLinkCandidate[];\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { base, candidates } = props;\n return (\n <Card>\n <SectionTitle level={3}>Link a package</SectionTitle>\n <form method=\"post\" action={base} hx-post={base} hx-target=\"body\">\n {csrfField()}\n <div class=\"grid grid--2\">\n <Field label=\"Package\" name=\"package\" required placeholder=\"@acme/design-system\" />\n <SelectField\n label=\"Upstream project\"\n name=\"upstreamSlug\"\n required\n options={candidates.map((candidate) => ({\n value: candidate.slug,\n label: candidate.name,\n }))}\n />\n </div>\n <FormActions>\n <Button variant=\"primary\" type=\"submit\" icon=\"plus\">\n Link package\n </Button>\n </FormActions>\n </form>\n </Card>\n );\n}\n"],"mappings":";;;;;;;;;;;AA+BA,MAAM,WACJ,MACA,MACA,YAEA,qBAAC,MAAD,EAAA,UAAA;CACE,oBAAC,MAAD,EAAA,UACE,oBAAC,OAAD;EAAO,MAAK;EAAW,UAAA,KAAK;CAAmB,CAAA,EAC7C,CAAA;CACJ,oBAAC,MAAD,EAAA,UAAK,KAAK,aAAiB,CAAA;CAC3B,oBAAC,MAAD,EAAA,UACG,UACC,qBAAC,QAAD;EACE,QAAO;EACP,QAAQ,GAAG,KAAK;EAChB,WAAS,GAAG,KAAK;EACjB,aAAU;EACV,cAAW;EALb,UAAA;GAOG,UAAU;GACX,oBAAC,SAAD;IAAO,MAAK;IAAS,MAAK;IAAU,OAAO,KAAK;GAAc,CAAA;GAC9D,oBAAC,QAAD;IAAQ,SAAQ;IAAQ,MAAK;IAAS,cAAY,UAAU,KAAK;IAAe,UAAA;GAExE,CAAA;EACJ;CACJ,CAAA,IAAA,KACF,CAAA;AACF,EAAA,GAtBK,KAAK,WAsBV;;AAIN,SAAgB,oBACd,SACA,OACA,YACA,SACA,WACS;CACT,MAAM,OAAO,aAAa,QAAQ,KAAK;CACvC,OACE,qBAAC,OAAD;EAAK,OAAM;EAAX,UAAA;GACG,WAAW,cAAc,oBAAC,OAAD;IAAO,MAAK;IAAU,UAAA,UAAU;GAAmB,CAAA,IAAI;GACjF,qBAAC,MAAD,EAAA,UAAA;IACE,oBAAC,cAAD,EAAA,UAAc,kBAA6B,CAAA;IAC3C,oBAAC,MAAD,EAAA,UAAM,wLAIA,CAAA;IACN,oBAAC,OAAD,EAAA,UACE,qBAAC,SAAD,EAAA,UAAA,CACE,oBAAC,SAAD,EAAA,UACE,qBAAC,MAAD,EAAA,UAAA;KACE,oBAAC,MAAD,EAAA,UAAI,UAAW,CAAA;KACf,oBAAC,MAAD,EAAA,UAAI,mBAAoB,CAAA;KACxB,oBAAC,MAAD,EAAA,UACE,oBAAC,QAAD;MAAM,OAAM;MAAkB,UAAA;KAAa,CAAA,EACzC,CAAA;IACF,EAAA,CAAA,EACC,CAAA,GACP,oBAAC,SAAD,EAAA,UAAQ,MAAM,KAAK,SAAS,QAAQ,MAAM,MAAM,OAAO,CAAC,EAAS,CAAA,CAC5D,EAAA,CAAA,EACF,CAAA;IACN,MAAM,WAAW,IAAI,oBAAC,MAAD,EAAA,UAAM,sBAAyB,CAAA,IAAI;GACrD,EAAA,CAAA;GACL,UAAU,oBAAC,aAAD;IAAmB;IAAkB;GAAa,CAAA,IAAI;EAC9D;;AAET;;AAGA,SAAS,YAAY,OAG8B;CACjD,MAAM,EAAE,MAAM,eAAe;CAC7B,OACE,qBAAC,MAAD,EAAA,UAAA,CACE,oBAAC,cAAD;EAAc,OAAO;EAAG,UAAA;CAA4B,CAAA,GACpD,qBAAC,QAAD;EAAM,QAAO;EAAO,QAAQ;EAAM,WAAS;EAAM,aAAU;EAA3D,UAAA;GACG,UAAU;GACX,qBAAC,OAAD;IAAK,OAAM;IAAX,UAAA,CACE,oBAAC,OAAD;KAAO,OAAM;KAAU,MAAK;KAAU,UAAA;KAAS,aAAY;IAAuB,CAAA,GAClF,oBAAC,aAAD;KACE,OAAM;KACN,MAAK;KACL,UAAA;KACA,SAAS,WAAW,KAAK,eAAe;MACtC,OAAO,UAAU;MACjB,OAAO,UAAU;KACnB,EAAE;IACH,CAAA,CACE;;GACL,oBAAC,aAAD,EAAA,UACE,oBAAC,QAAD;IAAQ,SAAQ;IAAU,MAAK;IAAS,MAAK;IAAO,UAAA;GAE5C,CAAA,EACG,CAAA;EACT;CACF,CAAA,CAAA,EAAA,CAAA;AAEV"}
@@ -0,0 +1,128 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { Button } from "../ui/buttons.mjs";
5
+ import { Alert } from "../ui/feedback.mjs";
6
+ import { 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 { isServerlessHost } from "@storyshelf/core/config";
10
+ import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
11
+ //#region src/pages/settings-notifications-git.tsx
12
+ /** GitHub only: show builds as check runs posted by a GitHub App, not as commit statuses. */
13
+ const GitHubAppFields = ({ errors }) => /* @__PURE__ */ jsxs(Fragment, { children: [
14
+ /* @__PURE__ */ jsx(SelectField, {
15
+ label: "GitHub mode",
16
+ name: "mode",
17
+ options: [{
18
+ value: "statuses",
19
+ label: "Commit statuses (token)"
20
+ }, {
21
+ value: "checks",
22
+ label: "Check runs (GitHub App)"
23
+ }],
24
+ hint: errors?.["mode"] ?? "GitHub only. Check runs list each changed snapshot and need a GitHub App installed on the repository; GitLab always uses statuses."
25
+ }),
26
+ /* @__PURE__ */ jsx(Field, {
27
+ label: "App ID",
28
+ name: "appId",
29
+ placeholder: "123456",
30
+ error: errors?.["appId"]
31
+ }),
32
+ /* @__PURE__ */ jsx(Field, {
33
+ label: "Installation ID",
34
+ name: "installationId",
35
+ placeholder: "7654321",
36
+ error: errors?.["installationId"]
37
+ }),
38
+ /* @__PURE__ */ jsx(TextareaField, {
39
+ label: "App private key",
40
+ name: "privateKey",
41
+ rows: 4,
42
+ placeholder: "-----BEGIN RSA PRIVATE KEY-----",
43
+ hint: "The PEM key generated for the App. It is stored encrypted and never shown again.",
44
+ error: errors?.["privateKey"]
45
+ })
46
+ ] });
47
+ /** The repository, prefix and token fields of the git channel form. */
48
+ const GitChannelFields = ({ errors }) => /* @__PURE__ */ jsxs(Fragment, { children: [
49
+ /* @__PURE__ */ jsx(Field, {
50
+ label: "Owner",
51
+ name: "owner",
52
+ required: true,
53
+ placeholder: "my-org",
54
+ hint: "Organization, user or group that owns the repository.",
55
+ error: errors?.["owner"]
56
+ }),
57
+ /* @__PURE__ */ jsx(Field, {
58
+ label: "Repository",
59
+ name: "repo",
60
+ required: true,
61
+ placeholder: "my-repo",
62
+ error: errors?.["repo"]
63
+ }),
64
+ /* @__PURE__ */ jsx(Field, {
65
+ label: "Host",
66
+ name: "host",
67
+ placeholder: "https://gitlab.example.com",
68
+ hint: "GitLab only: a self-managed host. Leave blank for gitlab.com.",
69
+ error: errors?.["host"]
70
+ }),
71
+ /* @__PURE__ */ jsx(Field, {
72
+ label: "Status prefix",
73
+ name: "prefix",
74
+ placeholder: "storyshelf",
75
+ hint: "Statuses (or check runs) are named <prefix>/testing and <prefix>/storybook; require <prefix>/testing in branch protection (`storyshelf protect` prints the rule). Give each project in a shared repository its own prefix.",
76
+ error: errors?.["prefix"]
77
+ }),
78
+ /* @__PURE__ */ jsx(Field, {
79
+ label: "Token",
80
+ name: "token",
81
+ type: "password",
82
+ placeholder: "ghp_…",
83
+ hint: "A token that can write commit statuses (GitHub: repo:status; GitLab: api). Not needed for GitHub checks.",
84
+ error: errors?.["token"]
85
+ }),
86
+ /* @__PURE__ */ jsx(GitHubAppFields, { errors })
87
+ ] });
88
+ /** Git channel creation card (admins only): repository, status prefix and token. */
89
+ const CreateGitChannelCard = ({ project, providers, formState }) => /* @__PURE__ */ jsxs(Card, { children: [
90
+ /* @__PURE__ */ jsx(SectionTitle, {
91
+ level: 3,
92
+ children: "Add a git channel"
93
+ }),
94
+ isServerlessHost() ? /* @__PURE__ */ jsx(Alert, {
95
+ tone: "info",
96
+ title: "Statuses are delivered inline",
97
+ children: "This host has no job consumer, so a build's state is written in the request that changes it, without retries. A failure shows on the channel and the next change writes the current state again."
98
+ }) : null,
99
+ /* @__PURE__ */ jsxs("form", {
100
+ method: "post",
101
+ action: `/projects/${project.slug}/settings/notifications`,
102
+ "hx-post": `/projects/${project.slug}/settings/notifications`,
103
+ "hx-target": "body",
104
+ children: [
105
+ csrfField(),
106
+ /* @__PURE__ */ jsx(SelectField, {
107
+ label: "Git provider",
108
+ name: "gitProvider",
109
+ options: providers.map((provider) => ({
110
+ value: provider.kind,
111
+ label: provider.name
112
+ })),
113
+ hint: formState?.errors?.["gitProvider"]
114
+ }),
115
+ /* @__PURE__ */ jsx(GitChannelFields, { errors: formState?.errors }),
116
+ /* @__PURE__ */ jsx(FormActions, { children: /* @__PURE__ */ jsx(Button, {
117
+ variant: "primary",
118
+ type: "submit",
119
+ icon: "plus",
120
+ children: "Add git channel"
121
+ }) })
122
+ ]
123
+ })
124
+ ] });
125
+ //#endregion
126
+ export { CreateGitChannelCard };
127
+
128
+ //# sourceMappingURL=settings-notifications-git.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings-notifications-git.mjs","names":[],"sources":["../../src/pages/settings-notifications-git.tsx"],"sourcesContent":["import { isServerlessHost } from \"@storyshelf/core/config\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport type { FC } from \"hono/jsx\";\nimport {\n Alert,\n Button,\n Card,\n Field,\n FormActions,\n SectionTitle,\n SelectField,\n TextareaField,\n} from \"../ui/components.tsx\";\nimport { csrfField } from \"../ui/csrf-field.tsx\";\nimport type { NotificationsFormState, SettingsNotifyProvider } from \"./settings-notifications.tsx\";\n\n/* eslint-disable promise-function-async -- JSX components return HtmlEscapedString */\n\n/** GitHub only: show builds as check runs posted by a GitHub App, not as commit statuses. */\nconst GitHubAppFields: FC<{ errors?: Record<string, string> }> = ({ errors }) => (\n <>\n <SelectField\n label=\"GitHub mode\"\n name=\"mode\"\n options={[\n { value: \"statuses\", label: \"Commit statuses (token)\" },\n { value: \"checks\", label: \"Check runs (GitHub App)\" },\n ]}\n hint={\n errors?.[\"mode\"] ??\n \"GitHub only. Check runs list each changed snapshot and need a GitHub App installed on the repository; GitLab always uses statuses.\"\n }\n />\n <Field label=\"App ID\" name=\"appId\" placeholder=\"123456\" error={errors?.[\"appId\"]} />\n <Field\n label=\"Installation ID\"\n name=\"installationId\"\n placeholder=\"7654321\"\n error={errors?.[\"installationId\"]}\n />\n <TextareaField\n label=\"App private key\"\n name=\"privateKey\"\n rows={4}\n placeholder=\"-----BEGIN RSA PRIVATE KEY-----\"\n hint=\"The PEM key generated for the App. It is stored encrypted and never shown again.\"\n error={errors?.[\"privateKey\"]}\n />\n </>\n);\n\n/** The repository, prefix and token fields of the git channel form. */\nconst GitChannelFields: FC<{ errors?: Record<string, string> }> = ({ errors }) => (\n <>\n <Field\n label=\"Owner\"\n name=\"owner\"\n required\n placeholder=\"my-org\"\n hint=\"Organization, user or group that owns the repository.\"\n error={errors?.[\"owner\"]}\n />\n <Field label=\"Repository\" name=\"repo\" required placeholder=\"my-repo\" error={errors?.[\"repo\"]} />\n <Field\n label=\"Host\"\n name=\"host\"\n placeholder=\"https://gitlab.example.com\"\n hint=\"GitLab only: a self-managed host. Leave blank for gitlab.com.\"\n error={errors?.[\"host\"]}\n />\n <Field\n label=\"Status prefix\"\n name=\"prefix\"\n placeholder=\"storyshelf\"\n hint=\"Statuses (or check runs) are named <prefix>/testing and <prefix>/storybook; require <prefix>/testing in branch protection (`storyshelf protect` prints the rule). Give each project in a shared repository its own prefix.\"\n error={errors?.[\"prefix\"]}\n />\n <Field\n label=\"Token\"\n name=\"token\"\n type=\"password\"\n placeholder=\"ghp_…\"\n hint=\"A token that can write commit statuses (GitHub: repo:status; GitLab: api). Not needed for GitHub checks.\"\n error={errors?.[\"token\"]}\n />\n <GitHubAppFields errors={errors} />\n </>\n);\n\n/** Git channel creation card (admins only): repository, status prefix and token. */\nexport const CreateGitChannelCard: FC<{\n project: Project;\n providers: SettingsNotifyProvider[];\n formState?: NotificationsFormState;\n}> = ({ project, providers, formState }) => (\n <Card>\n <SectionTitle level={3}>Add a git channel</SectionTitle>\n {isServerlessHost() ? (\n <Alert tone=\"info\" title=\"Statuses are delivered inline\">\n This host has no job consumer, so a build's state is written in the request that changes it,\n without retries. A failure shows on the channel and the next change writes the current state\n again.\n </Alert>\n ) : null}\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/notifications`}\n hx-post={`/projects/${project.slug}/settings/notifications`}\n hx-target=\"body\"\n >\n {csrfField()}\n <SelectField\n label=\"Git provider\"\n name=\"gitProvider\"\n options={providers.map((provider) => ({ value: provider.kind, label: provider.name }))}\n hint={formState?.errors?.[\"gitProvider\"]}\n />\n <GitChannelFields errors={formState?.errors} />\n <FormActions>\n <Button variant=\"primary\" type=\"submit\" icon=\"plus\">\n Add git channel\n </Button>\n </FormActions>\n </form>\n </Card>\n);\n"],"mappings":";;;;;;;;;;;;AAmBA,MAAM,mBAA4D,EAAE,aAClE,qBAAA,UAAA,EAAA,UAAA;CACE,oBAAC,aAAD;EACE,OAAM;EACN,MAAK;EACL,SAAS,CACP;GAAE,OAAO;GAAY,OAAO;EAA0B,GACtD;GAAE,OAAO;GAAU,OAAO;EAA0B,CACtD;EACA,MACE,SAAS,WACT;CAEH,CAAA;CACD,oBAAC,OAAD;EAAO,OAAM;EAAS,MAAK;EAAQ,aAAY;EAAS,OAAO,SAAS;CAAW,CAAA;CACnF,oBAAC,OAAD;EACE,OAAM;EACN,MAAK;EACL,aAAY;EACZ,OAAO,SAAS;CACjB,CAAA;CACD,oBAAC,eAAD;EACE,OAAM;EACN,MAAK;EACL,MAAM;EACN,aAAY;EACZ,MAAK;EACL,OAAO,SAAS;CACjB,CAAA;AACD,EAAA,CAAA;;AAIJ,MAAM,oBAA6D,EAAE,aACnE,qBAAA,UAAA,EAAA,UAAA;CACE,oBAAC,OAAD;EACE,OAAM;EACN,MAAK;EACL,UAAA;EACA,aAAY;EACZ,MAAK;EACL,OAAO,SAAS;CACjB,CAAA;CACD,oBAAC,OAAD;EAAO,OAAM;EAAa,MAAK;EAAO,UAAA;EAAS,aAAY;EAAU,OAAO,SAAS;CAAU,CAAA;CAC/F,oBAAC,OAAD;EACE,OAAM;EACN,MAAK;EACL,aAAY;EACZ,MAAK;EACL,OAAO,SAAS;CACjB,CAAA;CACD,oBAAC,OAAD;EACE,OAAM;EACN,MAAK;EACL,aAAY;EACZ,MAAK;EACL,OAAO,SAAS;CACjB,CAAA;CACD,oBAAC,OAAD;EACE,OAAM;EACN,MAAK;EACL,MAAK;EACL,aAAY;EACZ,MAAK;EACL,OAAO,SAAS;CACjB,CAAA;CACD,oBAAC,iBAAD,EAAyB,OAAS,CAAA;AAClC,EAAA,CAAA;;AAIJ,MAAa,wBAIP,EAAE,SAAS,WAAW,gBAC1B,qBAAC,MAAD,EAAA,UAAA;CACE,oBAAC,cAAD;EAAc,OAAO;EAAG,UAAA;CAA+B,CAAA;CACtD,iBAAiB,IAChB,oBAAC,OAAD;EAAO,MAAK;EAAO,OAAM;EAAgC,UAAA;CAIlD,CAAA,IACL;CACJ,qBAAC,QAAD;EACE,QAAO;EACP,QAAQ,aAAa,QAAQ,KAAK;EAClC,WAAS,aAAa,QAAQ,KAAK;EACnC,aAAU;EAJZ,UAAA;GAMG,UAAU;GACX,oBAAC,aAAD;IACE,OAAM;IACN,MAAK;IACL,SAAS,UAAU,KAAK,cAAc;KAAE,OAAO,SAAS;KAAM,OAAO,SAAS;IAAK,EAAE;IACrF,MAAM,WAAW,SAAS;GAC3B,CAAA;GACD,oBAAC,kBAAD,EAAkB,QAAQ,WAAW,OAAS,CAAA;GAC9C,oBAAC,aAAD,EAAA,UACE,oBAAC,QAAD;IAAQ,SAAQ;IAAU,MAAK;IAAS,MAAK;IAAO,UAAA;GAE5C,CAAA,EACG,CAAA;EACT;;AACF,EAAA,CAAA"}
@@ -5,9 +5,12 @@ import { Button } from "../ui/buttons.mjs";
5
5
  import { Alert, Badge, Meta } from "../ui/feedback.mjs";
6
6
  import { CheckField, Field, FormActions, SelectField } from "../ui/forms.mjs";
7
7
  import { Card, SectionTitle } from "../ui/layout.mjs";
8
- import { HStack } from "../ui/stacks.mjs";
8
+ import { VStack } from "../ui/stacks.mjs";
9
9
  import { Table } from "../ui/table.mjs";
10
+ import { RelativeTime } from "../ui/time.mjs";
10
11
  import { csrfField } from "../ui/csrf-field.mjs";
12
+ import { NotificationsPausedNotice } from "./notifications-paused.mjs";
13
+ import { CreateGitChannelCard } from "./settings-notifications-git.mjs";
11
14
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
12
15
  //#region src/pages/settings-notifications.tsx
13
16
  /** Notification topic catalog (mirrors the webhook event names). */
@@ -28,6 +31,18 @@ const NOTIFICATION_TOPICS = [
28
31
  event: "build:rejected",
29
32
  label: "Build rejected"
30
33
  },
34
+ {
35
+ event: "build:pending",
36
+ label: "Build sent back to pending"
37
+ },
38
+ {
39
+ event: "build:failed",
40
+ label: "Build failed"
41
+ },
42
+ {
43
+ event: "build:cancelled",
44
+ label: "Build cancelled"
45
+ },
31
46
  {
32
47
  event: "baseline:created",
33
48
  label: "Baseline created"
@@ -83,14 +98,43 @@ const MyNotificationsCard = ({ project, mySubscription }) => {
83
98
  })
84
99
  ] });
85
100
  };
86
- /** Human-readable channel target (email address or masked webhook host). */
101
+ /** A config value as text; anything but a string reads as empty. */
102
+ function textOf(value) {
103
+ return typeof value === "string" ? value : "";
104
+ }
105
+ /** Human-readable channel target (git repository, email address or masked webhook host). */
87
106
  function targetOf(channel) {
107
+ if (channel.delivery === "state") {
108
+ const where = `${textOf(channel.config["owner"])}/${textOf(channel.config["repo"])}`;
109
+ const prefix = textOf(channel.config["prefix"]);
110
+ return prefix ? `${where} (${prefix})` : where;
111
+ }
88
112
  const to = channel.config["to"];
89
113
  if (typeof to === "string" && to) return to;
90
114
  return channel.hasSecret ? `${channel.provider} webhook` : "—";
91
115
  }
92
- /** One channel table row with the admin delete action. */
93
- const ChannelRow = ({ project, channel }) => /* @__PURE__ */ jsxs("tr", { children: [
116
+ /** What a channel reacts to: its topic filter, or the build state for git channels. */
117
+ function reactsTo(channel) {
118
+ if (channel.delivery === "state") return "build state";
119
+ return channel.events.length === 0 ? "all events" : channel.events.join(", ");
120
+ }
121
+ /** Delivery health: failing with the last error and time, or ok. */
122
+ const ChannelHealth = ({ channel }) => channel.lastError === null ? /* @__PURE__ */ jsx(Badge, {
123
+ tone: "success",
124
+ children: "ok"
125
+ }) : /* @__PURE__ */ jsxs(VStack, { children: [/* @__PURE__ */ jsx(Badge, {
126
+ tone: "danger",
127
+ children: "failing"
128
+ }), /* @__PURE__ */ jsxs(Meta, {
129
+ as: "span",
130
+ children: [/* @__PURE__ */ jsx("span", {
131
+ class: "truncate max-w-cell",
132
+ title: channel.lastError,
133
+ children: channel.lastError
134
+ }), channel.lastErrorAt ? /* @__PURE__ */ jsx(RelativeTime, { value: channel.lastErrorAt }) : null]
135
+ })] });
136
+ /** One channel table row; the delete action renders for admins only. */
137
+ const ChannelRow = ({ project, channel, isAdmin }) => /* @__PURE__ */ jsxs("tr", { children: [
94
138
  /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(Badge, {
95
139
  tone: "neutral",
96
140
  children: channel.provider
@@ -100,64 +144,49 @@ const ChannelRow = ({ project, channel }) => /* @__PURE__ */ jsxs("tr", { childr
100
144
  title: targetOf(channel),
101
145
  children: targetOf(channel)
102
146
  }),
103
- /* @__PURE__ */ jsx("td", { children: channel.events.length === 0 ? /* @__PURE__ */ jsx(Meta, {
147
+ /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(Meta, {
104
148
  as: "span",
105
- children: "all events"
106
- }) : /* @__PURE__ */ jsx(HStack, { children: channel.events.map((event) => /* @__PURE__ */ jsx(Badge, {
107
- tone: "neutral",
108
- children: event
109
- })) }) }),
110
- /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsxs("form", {
149
+ children: reactsTo(channel)
150
+ }) }),
151
+ /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(ChannelHealth, { channel }) }),
152
+ /* @__PURE__ */ jsx("td", { children: isAdmin ? /* @__PURE__ */ jsxs("form", {
111
153
  method: "post",
112
154
  action: `/projects/${project.slug}/settings/notifications/${channel.id}/delete`,
113
155
  "hx-post": `/projects/${project.slug}/settings/notifications/${channel.id}/delete`,
114
156
  "hx-target": "body",
115
- "hx-confirm": "Remove this notification channel?",
157
+ "hx-confirm": "Remove this channel?",
116
158
  children: [csrfField(), /* @__PURE__ */ jsx(Button, {
117
159
  variant: "ghost",
118
160
  size: "sm",
119
161
  type: "submit",
120
162
  children: "Delete"
121
163
  })]
122
- }) })
123
- ] });
124
- /** Channel list card (delete action renders for admins only). */
164
+ }) : null })
165
+ ] }, channel.id);
166
+ /** Channel list card: chat, email and git channels side by side, each with its delivery health. */
125
167
  const ChannelsCard = ({ project, channels, isAdmin }) => /* @__PURE__ */ jsxs(Card, { children: [
126
168
  /* @__PURE__ */ jsx(SectionTitle, { children: "Channels" }),
127
- /* @__PURE__ */ jsx(Meta, { children: "Project admins route build events to Slack, Teams, or email. Secrets are stored encrypted and decrypted only in memory at send time." }),
169
+ /* @__PURE__ */ jsx(Meta, { children: "Project admins route build events to Slack, Teams or email, and show each build's state on GitHub or GitLab as a commit status. Secrets and tokens are stored encrypted and decrypted only in memory at delivery time. Git channels follow the build state, not a topic list, and their delivery is best-effort: a failure shows here, and the next build state retries it." }),
128
170
  /* @__PURE__ */ jsx(Table, { children: /* @__PURE__ */ jsxs("table", { children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
129
171
  /* @__PURE__ */ jsx("th", { children: "Provider" }),
130
172
  /* @__PURE__ */ jsx("th", { children: "Target" }),
131
- /* @__PURE__ */ jsx("th", { children: "Events" }),
173
+ /* @__PURE__ */ jsx("th", { children: "Reacts to" }),
174
+ /* @__PURE__ */ jsx("th", { children: "Delivery" }),
132
175
  /* @__PURE__ */ jsx("th", { children: /* @__PURE__ */ jsx("span", {
133
176
  class: "visually-hidden",
134
177
  children: "Actions"
135
178
  }) })
136
- ] }) }), /* @__PURE__ */ jsx("tbody", { children: channels.map((channel) => isAdmin ? /* @__PURE__ */ jsx(ChannelRow, {
179
+ ] }) }), /* @__PURE__ */ jsx("tbody", { children: channels.map((channel) => /* @__PURE__ */ jsx(ChannelRow, {
137
180
  project,
138
- channel
139
- }) : /* @__PURE__ */ jsxs("tr", { children: [
140
- /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(Badge, {
141
- tone: "neutral",
142
- children: channel.provider
143
- }) }),
144
- /* @__PURE__ */ jsx("td", {
145
- class: "truncate max-w-cell",
146
- title: targetOf(channel),
147
- children: targetOf(channel)
148
- }),
149
- /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(Meta, {
150
- as: "span",
151
- children: channel.events.length === 0 ? "all events" : channel.events.join(", ")
152
- }) }),
153
- /* @__PURE__ */ jsx("td", { children: null })
154
- ] }, channel.id)) })] }) }),
181
+ channel,
182
+ isAdmin
183
+ })) })] }) }),
155
184
  channels.length === 0 ? /* @__PURE__ */ jsx(Meta, { children: "No channels configured." }) : null
156
185
  ] });
157
186
  /** Channel creation card (admins only). */
158
187
  const CreateChannelCard = ({ project, providers, formState }) => /* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsx(SectionTitle, {
159
188
  level: 3,
160
- children: "Create channel"
189
+ children: "Add a message channel"
161
190
  }), /* @__PURE__ */ jsxs("form", {
162
191
  method: "post",
163
192
  action: `/projects/${project.slug}/settings/notifications`,
@@ -169,8 +198,8 @@ const CreateChannelCard = ({ project, providers, formState }) => /* @__PURE__ */
169
198
  label: "Provider",
170
199
  name: "provider",
171
200
  options: providers.map((provider) => ({
172
- value: provider,
173
- label: provider
201
+ value: provider.kind,
202
+ label: provider.name
174
203
  })),
175
204
  hint: formState?.errors?.["provider"]
176
205
  }),
@@ -208,11 +237,14 @@ const CreateChannelCard = ({ project, providers, formState }) => /* @__PURE__ */
208
237
  }) })
209
238
  ]
210
239
  })] });
211
- /** Notifications settings tab: channel list, create form, and my preferences. */
212
- function renderSettingsNotifications(project, channels, providers, mySubscription, isAdmin, formState) {
240
+ /** Notifications settings tab: channel list, create forms, and my preferences. */
241
+ function renderSettingsNotifications(project, channels, providers, mySubscription, isAdmin, formState, paused) {
242
+ const messageProviders = providers.filter((provider) => provider.delivery === "message");
243
+ const gitProviders = providers.filter((provider) => provider.delivery === "state");
213
244
  return /* @__PURE__ */ jsxs("div", {
214
245
  class: "grid max-w-form",
215
246
  children: [
247
+ /* @__PURE__ */ jsx(NotificationsPausedNotice, { paused }),
216
248
  formState?.globalError ? /* @__PURE__ */ jsx(Alert, {
217
249
  tone: "danger",
218
250
  children: formState.globalError
@@ -226,9 +258,14 @@ function renderSettingsNotifications(project, channels, providers, mySubscriptio
226
258
  channels,
227
259
  isAdmin
228
260
  }),
229
- isAdmin ? /* @__PURE__ */ jsx(CreateChannelCard, {
261
+ isAdmin && messageProviders.length > 0 ? /* @__PURE__ */ jsx(CreateChannelCard, {
262
+ project,
263
+ providers: messageProviders,
264
+ formState
265
+ }) : null,
266
+ isAdmin && gitProviders.length > 0 ? /* @__PURE__ */ jsx(CreateGitChannelCard, {
230
267
  project,
231
- providers,
268
+ providers: gitProviders,
232
269
  formState
233
270
  }) : null
234
271
  ]
@@ -1 +1 @@
1
- {"version":3,"file":"settings-notifications.mjs","names":[],"sources":["../../src/pages/settings-notifications.tsx"],"sourcesContent":["import type { Project } from \"@storyshelf/core/schema\";\nimport type { FC } from \"hono/jsx\";\nimport {\n Alert,\n Badge,\n Button,\n Card,\n CheckField,\n Field,\n FormActions,\n HStack,\n Meta,\n SectionTitle,\n SelectField,\n Table,\n} from \"../ui/components.tsx\";\nimport { csrfField } from \"../ui/csrf-field.tsx\";\n\n/** Notification topic catalog (mirrors the webhook event names). */\nexport const NOTIFICATION_TOPICS: { event: string; label: string }[] = [\n { event: \"build:created\", label: \"Build created\" },\n { event: \"build:reviewing\", label: \"Build needs review\" },\n { event: \"build:approved\", label: \"Build approved\" },\n { event: \"build:rejected\", label: \"Build rejected\" },\n { event: \"baseline:created\", label: \"Baseline created\" },\n { event: \"baseline:updated\", label: \"Baseline updated\" },\n { event: \"comment:created\", label: \"Comment posted\" },\n];\n\n/** Channel row as rendered in the notifications settings tab. */\nexport interface SettingsNotificationChannel {\n id: string;\n provider: string;\n config: Record<string, unknown>;\n hasSecret: boolean;\n events: string[];\n enabled: boolean;\n}\n\n/** The viewer's own subscription for this project (null when opted out). */\nexport interface SettingsMySubscription {\n events: string[];\n via: string[];\n enabled: boolean;\n}\n\n/** Form state for the notifications tab (field errors, global error). */\nexport interface NotificationsFormState {\n errors?: Record<string, string>;\n globalError?: string;\n}\n\n/* eslint-disable promise-function-async -- JSX components return HtmlEscapedString */\n\n/** Whether a topic checkbox renders checked (empty allowlist means all). */\nfunction topicChecked(events: string[], event: string): boolean {\n return events.length === 0 || events.includes(event);\n}\n\n/** The viewer's own preferences card (visible to every member). */\nconst MyNotificationsCard: FC<{\n project: Project;\n mySubscription: SettingsMySubscription | null;\n}> = ({ project, mySubscription }) => {\n const events = mySubscription?.events ?? [];\n return (\n <Card>\n <SectionTitle>My notifications for this project</SectionTitle>\n <Meta>Opt in to email alerts for this project. Leave everything off to stay silent.</Meta>\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/notifications/me`}\n hx-post={`/projects/${project.slug}/settings/notifications/me`}\n hx-target=\"body\"\n >\n {csrfField()}\n <CheckField\n label=\"Email me about this project\"\n name=\"enabled\"\n checked={mySubscription?.enabled ?? false}\n />\n {NOTIFICATION_TOPICS.map((topic) => (\n <CheckField\n label={topic.label}\n name=\"events\"\n value={topic.event}\n checked={topicChecked(events, topic.event)}\n />\n ))}\n <p>\n <Meta as=\"span\">No boxes ticked means all topics.</Meta>\n </p>\n <FormActions>\n <Button variant=\"primary\" type=\"submit\" icon=\"check\">\n Save my preferences\n </Button>\n </FormActions>\n </form>\n </Card>\n );\n};\n\n/** Human-readable channel target (email address or masked webhook host). */\nfunction targetOf(channel: SettingsNotificationChannel): string {\n const to = channel.config[\"to\"];\n if (typeof to === \"string\" && to) {\n return to;\n }\n return channel.hasSecret ? `${channel.provider} webhook` : \"—\";\n}\n\n/** One channel table row with the admin delete action. */\nconst ChannelRow: FC<{ project: Project; channel: SettingsNotificationChannel }> = ({\n project,\n channel,\n}) => (\n <tr>\n <td>\n <Badge tone=\"neutral\">{channel.provider}</Badge>\n </td>\n <td class=\"truncate max-w-cell\" title={targetOf(channel)}>\n {targetOf(channel)}\n </td>\n <td>\n {channel.events.length === 0 ? (\n <Meta as=\"span\">all events</Meta>\n ) : (\n <HStack>\n {channel.events.map((event) => (\n <Badge tone=\"neutral\">{event}</Badge>\n ))}\n </HStack>\n )}\n </td>\n <td>\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/notifications/${channel.id}/delete`}\n hx-post={`/projects/${project.slug}/settings/notifications/${channel.id}/delete`}\n hx-target=\"body\"\n hx-confirm=\"Remove this notification channel?\"\n >\n {csrfField()}\n <Button variant=\"ghost\" size=\"sm\" type=\"submit\">\n Delete\n </Button>\n </form>\n </td>\n </tr>\n);\n\n/** Channel list card (delete action renders for admins only). */\nconst ChannelsCard: FC<{\n project: Project;\n channels: SettingsNotificationChannel[];\n isAdmin: boolean;\n}> = ({ project, channels, isAdmin }) => (\n <Card>\n <SectionTitle>Channels</SectionTitle>\n <Meta>\n Project admins route build events to Slack, Teams, or email. Secrets are stored encrypted and\n decrypted only in memory at send time.\n </Meta>\n <Table>\n <table>\n <thead>\n <tr>\n <th>Provider</th>\n <th>Target</th>\n <th>Events</th>\n <th>\n <span class=\"visually-hidden\">Actions</span>\n </th>\n </tr>\n </thead>\n <tbody>\n {channels.map((channel) =>\n isAdmin ? (\n <ChannelRow project={project} channel={channel} />\n ) : (\n <tr key={channel.id}>\n <td>\n <Badge tone=\"neutral\">{channel.provider}</Badge>\n </td>\n <td class=\"truncate max-w-cell\" title={targetOf(channel)}>\n {targetOf(channel)}\n </td>\n <td>\n <Meta as=\"span\">\n {channel.events.length === 0 ? \"all events\" : channel.events.join(\", \")}\n </Meta>\n </td>\n <td>{null}</td>\n </tr>\n ),\n )}\n </tbody>\n </table>\n </Table>\n {channels.length === 0 ? <Meta>No channels configured.</Meta> : null}\n </Card>\n);\n\n/** Channel creation card (admins only). */\nconst CreateChannelCard: FC<{\n project: Project;\n providers: string[];\n formState?: NotificationsFormState;\n}> = ({ project, providers, formState }) => (\n <Card>\n <SectionTitle level={3}>Create channel</SectionTitle>\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/notifications`}\n hx-post={`/projects/${project.slug}/settings/notifications`}\n hx-target=\"body\"\n >\n {csrfField()}\n <SelectField\n label=\"Provider\"\n name=\"provider\"\n options={providers.map((provider) => ({ value: provider, label: provider }))}\n hint={formState?.errors?.[\"provider\"]}\n />\n <Field\n label=\"Target\"\n name=\"target\"\n type=\"text\"\n required\n placeholder=\"team@example.com or https://hooks.slack.com/…\"\n hint=\"Email address for email channels, webhook URL otherwise.\"\n error={formState?.errors?.[\"target\"]}\n />\n <Field\n label=\"Events\"\n name=\"events\"\n placeholder=\"build.reviewing, build.approved\"\n hint=\"Comma-separated. Leave blank to receive all events.\"\n />\n <SelectField\n label=\"Style\"\n name=\"style\"\n options={[\n { value: \"compact\", label: \"Compact\" },\n { value: \"verbose\", label: \"Verbose\" },\n ]}\n />\n <FormActions>\n <Button variant=\"primary\" type=\"submit\" icon=\"plus\">\n Add channel\n </Button>\n </FormActions>\n </form>\n </Card>\n);\n\n/** Notifications settings tab: channel list, create form, and my preferences. */\nexport function renderSettingsNotifications(\n project: Project,\n channels: SettingsNotificationChannel[],\n providers: string[],\n mySubscription: SettingsMySubscription | null,\n isAdmin: boolean,\n formState?: NotificationsFormState,\n): unknown {\n return (\n <div class=\"grid max-w-form\">\n {formState?.globalError ? <Alert tone=\"danger\">{formState.globalError}</Alert> : null}\n <MyNotificationsCard project={project} mySubscription={mySubscription} />\n <ChannelsCard project={project} channels={channels} isAdmin={isAdmin} />\n {isAdmin ? (\n <CreateChannelCard project={project} providers={providers} formState={formState} />\n ) : null}\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"settings-notifications.mjs","names":[],"sources":["../../src/pages/settings-notifications.tsx"],"sourcesContent":["import type { Project } from \"@storyshelf/core/schema\";\nimport type { FC } from \"hono/jsx\";\nimport {\n Alert,\n Badge,\n Button,\n Card,\n CheckField,\n Field,\n FormActions,\n Meta,\n RelativeTime,\n SectionTitle,\n SelectField,\n Table,\n VStack,\n} from \"../ui/components.tsx\";\nimport { csrfField } from \"../ui/csrf-field.tsx\";\nimport { NotificationsPausedNotice } from \"./notifications-paused.tsx\";\nimport { CreateGitChannelCard } from \"./settings-notifications-git.tsx\";\n\n/** Notification topic catalog (mirrors the webhook event names). */\nexport const NOTIFICATION_TOPICS: { event: string; label: string }[] = [\n { event: \"build:created\", label: \"Build created\" },\n { event: \"build:reviewing\", label: \"Build needs review\" },\n { event: \"build:approved\", label: \"Build approved\" },\n { event: \"build:rejected\", label: \"Build rejected\" },\n { event: \"build:pending\", label: \"Build sent back to pending\" },\n { event: \"build:failed\", label: \"Build failed\" },\n { event: \"build:cancelled\", label: \"Build cancelled\" },\n { event: \"baseline:created\", label: \"Baseline created\" },\n { event: \"baseline:updated\", label: \"Baseline updated\" },\n { event: \"comment:created\", label: \"Comment posted\" },\n];\n\n/** A wired provider as offered by the create forms. */\nexport interface SettingsNotifyProvider {\n kind: string;\n name: string;\n /** `message` providers send a message once; `state` providers (git hosts) show a build's state. */\n delivery: \"message\" | \"state\";\n}\n\n/** Channel row as rendered in the notifications settings tab. */\nexport interface SettingsNotificationChannel {\n id: string;\n provider: string;\n delivery: \"message\" | \"state\";\n config: Record<string, unknown>;\n hasSecret: boolean;\n events: string[];\n enabled: boolean;\n /** Last delivery error, null while the channel delivers cleanly. */\n lastError: string | null;\n lastErrorAt: string | null;\n}\n\n/** The viewer's own subscription for this project (null when opted out). */\nexport interface SettingsMySubscription {\n events: string[];\n via: string[];\n enabled: boolean;\n}\n\n/** Form state for the notifications tab (field errors, global error). */\nexport interface NotificationsFormState {\n errors?: Record<string, string>;\n globalError?: string;\n}\n\n/* eslint-disable promise-function-async -- JSX components return HtmlEscapedString */\n\n/** Whether a topic checkbox renders checked (empty allowlist means all). */\nfunction topicChecked(events: string[], event: string): boolean {\n return events.length === 0 || events.includes(event);\n}\n\n/** The viewer's own preferences card (visible to every member). */\nconst MyNotificationsCard: FC<{\n project: Project;\n mySubscription: SettingsMySubscription | null;\n}> = ({ project, mySubscription }) => {\n const events = mySubscription?.events ?? [];\n return (\n <Card>\n <SectionTitle>My notifications for this project</SectionTitle>\n <Meta>Opt in to email alerts for this project. Leave everything off to stay silent.</Meta>\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/notifications/me`}\n hx-post={`/projects/${project.slug}/settings/notifications/me`}\n hx-target=\"body\"\n >\n {csrfField()}\n <CheckField\n label=\"Email me about this project\"\n name=\"enabled\"\n checked={mySubscription?.enabled ?? false}\n />\n {NOTIFICATION_TOPICS.map((topic) => (\n <CheckField\n label={topic.label}\n name=\"events\"\n value={topic.event}\n checked={topicChecked(events, topic.event)}\n />\n ))}\n <p>\n <Meta as=\"span\">No boxes ticked means all topics.</Meta>\n </p>\n <FormActions>\n <Button variant=\"primary\" type=\"submit\" icon=\"check\">\n Save my preferences\n </Button>\n </FormActions>\n </form>\n </Card>\n );\n};\n\n/** A config value as text; anything but a string reads as empty. */\nfunction textOf(value: unknown): string {\n return typeof value === \"string\" ? value : \"\";\n}\n\n/** Human-readable channel target (git repository, email address or masked webhook host). */\nfunction targetOf(channel: SettingsNotificationChannel): string {\n if (channel.delivery === \"state\") {\n const where = `${textOf(channel.config[\"owner\"])}/${textOf(channel.config[\"repo\"])}`;\n const prefix = textOf(channel.config[\"prefix\"]);\n return prefix ? `${where} (${prefix})` : where;\n }\n const to = channel.config[\"to\"];\n if (typeof to === \"string\" && to) {\n return to;\n }\n return channel.hasSecret ? `${channel.provider} webhook` : \"—\";\n}\n\n/** What a channel reacts to: its topic filter, or the build state for git channels. */\nfunction reactsTo(channel: SettingsNotificationChannel): string {\n if (channel.delivery === \"state\") {\n return \"build state\";\n }\n return channel.events.length === 0 ? \"all events\" : channel.events.join(\", \");\n}\n\n/** Delivery health: failing with the last error and time, or ok. */\nconst ChannelHealth: FC<{ channel: SettingsNotificationChannel }> = ({ channel }) =>\n channel.lastError === null ? (\n <Badge tone=\"success\">ok</Badge>\n ) : (\n <VStack>\n <Badge tone=\"danger\">failing</Badge>\n <Meta as=\"span\">\n <span class=\"truncate max-w-cell\" title={channel.lastError}>\n {channel.lastError}\n </span>\n {channel.lastErrorAt ? <RelativeTime value={channel.lastErrorAt} /> : null}\n </Meta>\n </VStack>\n );\n\n/** One channel table row; the delete action renders for admins only. */\nconst ChannelRow: FC<{\n project: Project;\n channel: SettingsNotificationChannel;\n isAdmin: boolean;\n}> = ({ project, channel, isAdmin }) => (\n <tr key={channel.id}>\n <td>\n <Badge tone=\"neutral\">{channel.provider}</Badge>\n </td>\n <td class=\"truncate max-w-cell\" title={targetOf(channel)}>\n {targetOf(channel)}\n </td>\n <td>\n <Meta as=\"span\">{reactsTo(channel)}</Meta>\n </td>\n <td>\n <ChannelHealth channel={channel} />\n </td>\n <td>\n {isAdmin ? (\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/notifications/${channel.id}/delete`}\n hx-post={`/projects/${project.slug}/settings/notifications/${channel.id}/delete`}\n hx-target=\"body\"\n hx-confirm=\"Remove this channel?\"\n >\n {csrfField()}\n <Button variant=\"ghost\" size=\"sm\" type=\"submit\">\n Delete\n </Button>\n </form>\n ) : null}\n </td>\n </tr>\n);\n\n/** Channel list card: chat, email and git channels side by side, each with its delivery health. */\nconst ChannelsCard: FC<{\n project: Project;\n channels: SettingsNotificationChannel[];\n isAdmin: boolean;\n}> = ({ project, channels, isAdmin }) => (\n <Card>\n <SectionTitle>Channels</SectionTitle>\n <Meta>\n Project admins route build events to Slack, Teams or email, and show each build's state on\n GitHub or GitLab as a commit status. Secrets and tokens are stored encrypted and decrypted\n only in memory at delivery time. Git channels follow the build state, not a topic list, and\n their delivery is best-effort: a failure shows here, and the next build state retries it.\n </Meta>\n <Table>\n <table>\n <thead>\n <tr>\n <th>Provider</th>\n <th>Target</th>\n <th>Reacts to</th>\n <th>Delivery</th>\n <th>\n <span class=\"visually-hidden\">Actions</span>\n </th>\n </tr>\n </thead>\n <tbody>\n {channels.map((channel) => (\n <ChannelRow project={project} channel={channel} isAdmin={isAdmin} />\n ))}\n </tbody>\n </table>\n </Table>\n {channels.length === 0 ? <Meta>No channels configured.</Meta> : null}\n </Card>\n);\n\n/** Channel creation card (admins only). */\nconst CreateChannelCard: FC<{\n project: Project;\n providers: SettingsNotifyProvider[];\n formState?: NotificationsFormState;\n}> = ({ project, providers, formState }) => (\n <Card>\n <SectionTitle level={3}>Add a message channel</SectionTitle>\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/notifications`}\n hx-post={`/projects/${project.slug}/settings/notifications`}\n hx-target=\"body\"\n >\n {csrfField()}\n <SelectField\n label=\"Provider\"\n name=\"provider\"\n options={providers.map((provider) => ({ value: provider.kind, label: provider.name }))}\n hint={formState?.errors?.[\"provider\"]}\n />\n <Field\n label=\"Target\"\n name=\"target\"\n type=\"text\"\n required\n placeholder=\"team@example.com or https://hooks.slack.com/…\"\n hint=\"Email address for email channels, webhook URL otherwise.\"\n error={formState?.errors?.[\"target\"]}\n />\n <Field\n label=\"Events\"\n name=\"events\"\n placeholder=\"build.reviewing, build.approved\"\n hint=\"Comma-separated. Leave blank to receive all events.\"\n />\n <SelectField\n label=\"Style\"\n name=\"style\"\n options={[\n { value: \"compact\", label: \"Compact\" },\n { value: \"verbose\", label: \"Verbose\" },\n ]}\n />\n <FormActions>\n <Button variant=\"primary\" type=\"submit\" icon=\"plus\">\n Add channel\n </Button>\n </FormActions>\n </form>\n </Card>\n);\n\n/** Notifications settings tab: channel list, create forms, and my preferences. */\nexport function renderSettingsNotifications(\n project: Project,\n channels: SettingsNotificationChannel[],\n providers: SettingsNotifyProvider[],\n mySubscription: SettingsMySubscription | null,\n isAdmin: boolean,\n formState?: NotificationsFormState,\n paused?: boolean,\n): unknown {\n const messageProviders = providers.filter((provider) => provider.delivery === \"message\");\n const gitProviders = providers.filter((provider) => provider.delivery === \"state\");\n return (\n <div class=\"grid max-w-form\">\n <NotificationsPausedNotice paused={paused} />\n {formState?.globalError ? <Alert tone=\"danger\">{formState.globalError}</Alert> : null}\n <MyNotificationsCard project={project} mySubscription={mySubscription} />\n <ChannelsCard project={project} channels={channels} isAdmin={isAdmin} />\n {isAdmin && messageProviders.length > 0 ? (\n <CreateChannelCard project={project} providers={messageProviders} formState={formState} />\n ) : null}\n {isAdmin && gitProviders.length > 0 ? (\n <CreateGitChannelCard project={project} providers={gitProviders} formState={formState} />\n ) : null}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAsBA,MAAa,sBAA0D;CACrE;EAAE,OAAO;EAAiB,OAAO;CAAgB;CACjD;EAAE,OAAO;EAAmB,OAAO;CAAqB;CACxD;EAAE,OAAO;EAAkB,OAAO;CAAiB;CACnD;EAAE,OAAO;EAAkB,OAAO;CAAiB;CACnD;EAAE,OAAO;EAAiB,OAAO;CAA6B;CAC9D;EAAE,OAAO;EAAgB,OAAO;CAAe;CAC/C;EAAE,OAAO;EAAmB,OAAO;CAAkB;CACrD;EAAE,OAAO;EAAoB,OAAO;CAAmB;CACvD;EAAE,OAAO;EAAoB,OAAO;CAAmB;CACvD;EAAE,OAAO;EAAmB,OAAO;CAAiB;AACtD;;AAwCA,SAAS,aAAa,QAAkB,OAAwB;CAC9D,OAAO,OAAO,WAAW,KAAK,OAAO,SAAS,KAAK;AACrD;;AAGA,MAAM,uBAGA,EAAE,SAAS,qBAAqB;CACpC,MAAM,SAAS,gBAAgB,UAAU,CAAC;CAC1C,OACE,qBAAC,MAAD,EAAA,UAAA;EACE,oBAAC,cAAD,EAAA,UAAc,oCAA+C,CAAA;EAC7D,oBAAC,MAAD,EAAA,UAAM,gFAAmF,CAAA;EACzF,qBAAC,QAAD;GACE,QAAO;GACP,QAAQ,aAAa,QAAQ,KAAK;GAClC,WAAS,aAAa,QAAQ,KAAK;GACnC,aAAU;GAJZ,UAAA;IAMG,UAAU;IACX,oBAAC,YAAD;KACE,OAAM;KACN,MAAK;KACL,SAAS,gBAAgB,WAAW;IACrC,CAAA;IACA,oBAAoB,KAAK,UACxB,oBAAC,YAAD;KACE,OAAO,MAAM;KACb,MAAK;KACL,OAAO,MAAM;KACb,SAAS,aAAa,QAAQ,MAAM,KAAK;IAC1C,CAAA,CACF;IACD,oBAAC,KAAD,EAAA,UACE,oBAAC,MAAD;KAAM,IAAG;KAAO,UAAA;IAAuC,CAAA,EACtD,CAAA;IACH,oBAAC,aAAD,EAAA,UACE,oBAAC,QAAD;KAAQ,SAAQ;KAAU,MAAK;KAAS,MAAK;KAAQ,UAAA;IAE7C,CAAA,EACG,CAAA;GACT;;CACF,EAAA,CAAA;AAEV;;AAGA,SAAS,OAAO,OAAwB;CACtC,OAAO,OAAO,UAAU,WAAW,QAAQ;AAC7C;;AAGA,SAAS,SAAS,SAA8C;CAC9D,IAAI,QAAQ,aAAa,SAAS;EAChC,MAAM,QAAQ,GAAG,OAAO,QAAQ,OAAO,QAAQ,EAAE,GAAG,OAAO,QAAQ,OAAO,OAAO;EACjF,MAAM,SAAS,OAAO,QAAQ,OAAO,SAAS;EAC9C,OAAO,SAAS,GAAG,MAAM,IAAI,OAAO,KAAK;CAC3C;CACA,MAAM,KAAK,QAAQ,OAAO;CAC1B,IAAI,OAAO,OAAO,YAAY,IAC5B,OAAO;CAET,OAAO,QAAQ,YAAY,GAAG,QAAQ,SAAS,YAAY;AAC7D;;AAGA,SAAS,SAAS,SAA8C;CAC9D,IAAI,QAAQ,aAAa,SACvB,OAAO;CAET,OAAO,QAAQ,OAAO,WAAW,IAAI,eAAe,QAAQ,OAAO,KAAK,IAAI;AAC9E;;AAGA,MAAM,iBAA+D,EAAE,cACrE,QAAQ,cAAc,OACpB,oBAAC,OAAD;CAAO,MAAK;CAAU,UAAA;AAAS,CAAA,IAE/B,qBAAC,QAAD,EAAA,UAAA,CACE,oBAAC,OAAD;CAAO,MAAK;CAAS,UAAA;AAAc,CAAA,GACnC,qBAAC,MAAD;CAAM,IAAG;CAAT,UAAA,CACE,oBAAC,QAAD;EAAM,OAAM;EAAsB,OAAO,QAAQ;EAC9C,UAAA,QAAQ;CACL,CAAA,GACL,QAAQ,cAAc,oBAAC,cAAD,EAAc,OAAO,QAAQ,YAAc,CAAA,IAAI,IAClE;AACA,CAAA,CAAA,EAAA,CAAA;;AAIZ,MAAM,cAIA,EAAE,SAAS,SAAS,cACxB,qBAAC,MAAD,EAAA,UAAA;CACE,oBAAC,MAAD,EAAA,UACE,oBAAC,OAAD;EAAO,MAAK;EAAW,UAAA,QAAQ;CAAgB,CAAA,EAC7C,CAAA;CACJ,oBAAC,MAAD;EAAI,OAAM;EAAsB,OAAO,SAAS,OAAO;EACpD,UAAA,SAAS,OAAO;CACf,CAAA;CACJ,oBAAC,MAAD,EAAA,UACE,oBAAC,MAAD;EAAM,IAAG;EAAQ,UAAA,SAAS,OAAO;CAAQ,CAAA,EACvC,CAAA;CACJ,oBAAC,MAAD,EAAA,UACE,oBAAC,eAAD,EAAwB,QAAU,CAAA,EAChC,CAAA;CACJ,oBAAC,MAAD,EAAA,UACG,UACC,qBAAC,QAAD;EACE,QAAO;EACP,QAAQ,aAAa,QAAQ,KAAK,0BAA0B,QAAQ,GAAG;EACvE,WAAS,aAAa,QAAQ,KAAK,0BAA0B,QAAQ,GAAG;EACxE,aAAU;EACV,cAAW;EALb,UAAA,CAOG,UAAU,GACX,oBAAC,QAAD;GAAQ,SAAQ;GAAQ,MAAK;GAAK,MAAK;GAAS,UAAA;EAExC,CAAA,CACJ;CACJ,CAAA,IAAA,KACF,CAAA;AACF,EAAA,GA7BK,QAAQ,EA6Bb;;AAIN,MAAM,gBAIA,EAAE,SAAS,UAAU,cACzB,qBAAC,MAAD,EAAA,UAAA;CACE,oBAAC,cAAD,EAAA,UAAc,WAAsB,CAAA;CACpC,oBAAC,MAAD,EAAA,UAAM,8WAKA,CAAA;CACN,oBAAC,OAAD,EAAA,UACE,qBAAC,SAAD,EAAA,UAAA,CACE,oBAAC,SAAD,EAAA,UACE,qBAAC,MAAD,EAAA,UAAA;EACE,oBAAC,MAAD,EAAA,UAAI,WAAY,CAAA;EAChB,oBAAC,MAAD,EAAA,UAAI,SAAU,CAAA;EACd,oBAAC,MAAD,EAAA,UAAI,YAAa,CAAA;EACjB,oBAAC,MAAD,EAAA,UAAI,WAAY,CAAA;EAChB,oBAAC,MAAD,EAAA,UACE,oBAAC,QAAD;GAAM,OAAM;GAAkB,UAAA;EAAa,CAAA,EACzC,CAAA;CACF,EAAA,CAAA,EACC,CAAA,GACP,oBAAC,SAAD,EAAA,UACG,SAAS,KAAK,YACb,oBAAC,YAAD;EAAqB;EAAkB;EAAkB;CAAU,CAAA,CACpE,EACI,CAAA,CACF,EAAA,CAAA,EACF,CAAA;CACN,SAAS,WAAW,IAAI,oBAAC,MAAD,EAAA,UAAM,0BAA6B,CAAA,IAAI;AAC5D,EAAA,CAAA;;AAIR,MAAM,qBAIA,EAAE,SAAS,WAAW,gBAC1B,qBAAC,MAAD,EAAA,UAAA,CACE,oBAAC,cAAD;CAAc,OAAO;CAAG,UAAA;AAAmC,CAAA,GAC3D,qBAAC,QAAD;CACE,QAAO;CACP,QAAQ,aAAa,QAAQ,KAAK;CAClC,WAAS,aAAa,QAAQ,KAAK;CACnC,aAAU;CAJZ,UAAA;EAMG,UAAU;EACX,oBAAC,aAAD;GACE,OAAM;GACN,MAAK;GACL,SAAS,UAAU,KAAK,cAAc;IAAE,OAAO,SAAS;IAAM,OAAO,SAAS;GAAK,EAAE;GACrF,MAAM,WAAW,SAAS;EAC3B,CAAA;EACD,oBAAC,OAAD;GACE,OAAM;GACN,MAAK;GACL,MAAK;GACL,UAAA;GACA,aAAY;GACZ,MAAK;GACL,OAAO,WAAW,SAAS;EAC5B,CAAA;EACD,oBAAC,OAAD;GACE,OAAM;GACN,MAAK;GACL,aAAY;GACZ,MAAK;EACN,CAAA;EACD,oBAAC,aAAD;GACE,OAAM;GACN,MAAK;GACL,SAAS,CACP;IAAE,OAAO;IAAW,OAAO;GAAU,GACrC;IAAE,OAAO;IAAW,OAAO;GAAU,CACvC;EACD,CAAA;EACD,oBAAC,aAAD,EAAA,UACE,oBAAC,QAAD;GAAQ,SAAQ;GAAU,MAAK;GAAS,MAAK;GAAO,UAAA;EAE5C,CAAA,EACG,CAAA;CACT;AACF,CAAA,CAAA,EAAA,CAAA;;AAIR,SAAgB,4BACd,SACA,UACA,WACA,gBACA,SACA,WACA,QACS;CACT,MAAM,mBAAmB,UAAU,QAAQ,aAAa,SAAS,aAAa,SAAS;CACvF,MAAM,eAAe,UAAU,QAAQ,aAAa,SAAS,aAAa,OAAO;CACjF,OACE,qBAAC,OAAD;EAAK,OAAM;EAAX,UAAA;GACE,oBAAC,2BAAD,EAAmC,OAAS,CAAA;GAC3C,WAAW,cAAc,oBAAC,OAAD;IAAO,MAAK;IAAU,UAAA,UAAU;GAAmB,CAAA,IAAI;GACjF,oBAAC,qBAAD;IAA8B;IAAyB;GAAiB,CAAA;GACxE,oBAAC,cAAD;IAAuB;IAAmB;IAAmB;GAAU,CAAA;GACtE,WAAW,iBAAiB,SAAS,IACpC,oBAAC,mBAAD;IAA4B;IAAS,WAAW;IAA6B;GAAY,CAAA,IACvF;GACH,WAAW,aAAa,SAAS,IAChC,oBAAC,sBAAD;IAA+B;IAAS,WAAW;IAAyB;GAAY,CAAA,IACtF;EACD;;AAET"}
@@ -8,7 +8,7 @@ import { Card, SectionTitle } from "../ui/layout.mjs";
8
8
  import { csrfField } from "../ui/csrf-field.mjs";
9
9
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
10
10
  //#region src/pages/settings-tests.tsx
11
- /** Interaction-tests settings tab: play execution toggle and timeout. */
11
+ /** Tests settings tab: play execution, Storybook 10 test runs, accessibility checks, play timeout. */
12
12
  function renderSettingsTests(project, isAdmin, formState) {
13
13
  return /* @__PURE__ */ jsxs("div", {
14
14
  class: "grid max-w-form",
@@ -18,7 +18,7 @@ function renderSettingsTests(project, isAdmin, formState) {
18
18
  children: formState.globalError
19
19
  }) : null,
20
20
  /* @__PURE__ */ jsxs(Card, { children: [
21
- /* @__PURE__ */ jsx(SectionTitle, { children: "Interaction tests" }),
21
+ /* @__PURE__ */ jsx(SectionTitle, { children: "Interaction and accessibility tests" }),
22
22
  /* @__PURE__ */ jsxs(Meta, { children: [
23
23
  "When enabled, Storybook ",
24
24
  /* @__PURE__ */ jsx("code", { children: "play" }),
@@ -37,6 +37,21 @@ function renderSettingsTests(project, isAdmin, formState) {
37
37
  /* @__PURE__ */ jsx("code", { children: "disableSnapshot" }),
38
38
  " to skip a story entirely."
39
39
  ] }),
40
+ /* @__PURE__ */ jsxs(Meta, { children: [
41
+ "Accessibility checks run axe on every captured story. Violations appear with the snapshot in review and never block a build; a story opts out with",
42
+ " ",
43
+ /* @__PURE__ */ jsx("code", { children: "parameters: { a11y: { disable: true } }" }),
44
+ "."
45
+ ] }),
46
+ /* @__PURE__ */ jsxs(Meta, { children: [
47
+ "Storybook 10 ",
48
+ /* @__PURE__ */ jsx("code", { children: ".test()" }),
49
+ " entries (",
50
+ /* @__PURE__ */ jsx("code", { children: "story.test(\"name\", async ...)" }),
51
+ ") can run as interaction tests too. They take no screenshot: each runs in the Storybook iframe and is reported as passed or failed on the build. A failing test fails the build like a failing ",
52
+ /* @__PURE__ */ jsx("code", { children: "play" }),
53
+ " function. Storybook 8 and 9 builds have none."
54
+ ] }),
40
55
  /* @__PURE__ */ jsxs("form", {
41
56
  method: "post",
42
57
  action: `/projects/${project.slug}/settings/tests`,
@@ -53,6 +68,22 @@ function renderSettingsTests(project, isAdmin, formState) {
53
68
  disabled: !isAdmin,
54
69
  hint: "When enabled, failing play blocks the build (failed). Flaky stories show warnings but status stays successful."
55
70
  }),
71
+ /* @__PURE__ */ jsx(CheckField, {
72
+ label: "Run Storybook tests (.test())",
73
+ name: "runTests",
74
+ value: "true",
75
+ checked: project.runTests === true ? true : void 0,
76
+ disabled: !isAdmin,
77
+ hint: "Off by default, because it changes what a build contains. When enabled, each .test() entry runs once on the primary browser and a failing test fails the build (flaky stories excepted)."
78
+ }),
79
+ /* @__PURE__ */ jsx(CheckField, {
80
+ label: "Accessibility checks (axe)",
81
+ name: "runA11y",
82
+ value: "true",
83
+ checked: project.runA11y === false ? void 0 : true,
84
+ disabled: !isAdmin,
85
+ hint: "On by default. Each story is scanned with axe-core (WCAG A/AA and best-practice rules). Violations are shown on the snapshot in review and never block the build."
86
+ }),
56
87
  /* @__PURE__ */ jsx(Field, {
57
88
  label: "Play timeout (ms)",
58
89
  name: "playTimeoutMs",