@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
@@ -1,91 +1,111 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
- import { getStore } from "../store.mjs";
5
- import { css } from "../ui/css.mjs";
4
+ import { catalogOf, getStore, testingEnabled } from "../store.mjs";
6
5
  import { Button } from "../ui/buttons.mjs";
7
6
  import { Badge, EmptyState, Meta, statusTone } from "../ui/feedback.mjs";
8
7
  import { SelectField } from "../ui/forms.mjs";
9
- import { Card, CardSection, PageHeader, SectionTitle } from "../ui/layout.mjs";
10
- import { HStack, VStack } from "../ui/stacks.mjs";
8
+ import { PageHeader } from "../ui/layout.mjs";
9
+ import { HStack } from "../ui/stacks.mjs";
11
10
  import { FilterInput } from "../ui/filter-input.mjs";
12
11
  import { DocumentLayout } from "../ui/document.mjs";
13
- import { diffPaneImg, snapshotCard, snapshotCardBody, snapshotCardHead, snapshotCardMeta, snapshotGrid } from "../ui/styles/review.mjs";
12
+ import { loadHealthPanel } from "../insights/health-panel.mjs";
13
+ import { HealthSlot } from "./health-panel.mjs";
14
+ import { ProjectDesignLinks } from "./design-links.mjs";
15
+ import { loadSnapshotLayer } from "./library-snapshots.mjs";
16
+ import { LibraryGrid } from "./library-grid.mjs";
17
+ import { entriesFromSnapshots, groupLibrary } from "./library-model.mjs";
14
18
  import { BuildModel, ProjectModel, SnapshotModel } from "@storyshelf/core/models";
15
- import { storybookDir } from "@storyshelf/core/utils";
16
19
  import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
17
20
  import { createUrlBuilder } from "@storyshelf/core/urls";
18
- import { posix } from "node:path";
19
21
  //#region src/pages/library.tsx
20
- /** Snapshot image with library height cap, extending the shared diff pane image. */
21
- const snapshotImg = css`
22
- /* snapshot-img */
23
- ${diffPaneImg}
24
- max-height: 240px;
25
- object-fit: contain;
26
- `;
27
- /** Library page: gallery grouped by title from latest build on selected branch. */
22
+ /**
23
+ * Library page (ADR 0006, decision 9): the components of the latest build of the selected branch,
24
+ * whatever its status, from the build's Storybook catalog. With `testing` the cards gain
25
+ * screenshots, statuses and design links, and the page the health panel.
26
+ */
28
27
  async function renderLibraryPage(slug, branch) {
29
- const { db, storage } = getStore();
30
- const project = await new ProjectModel(db).getBySlug(slug);
28
+ const project = await new ProjectModel(getStore().db).getBySlug(slug);
31
29
  if (!project) return null;
32
- const build = await getLibraryBuild(db, project, branch);
30
+ const build = await getLibraryBuild(project, branch);
33
31
  if (!build) return renderEmptyLibrary(project);
34
- const snapshots = await new SnapshotModel(db).listByBuild(build.id);
35
- if (snapshots.length === 0) return renderEmptySnapshots(project, build);
36
- return renderLibraryGrid(project, build, snapshots, await distinctBranches(db, project.id), await docsEntriesByStory(storage, project.id, build.id, snapshots));
32
+ const data = await loadLibraryData(project, build);
33
+ if (data.groups.length === 0) return renderNoStories(project, build);
34
+ return /* @__PURE__ */ jsxs(DocumentLayout, {
35
+ title: "Library",
36
+ nav: {
37
+ active: "library",
38
+ projectSlug: project.slug,
39
+ projectName: project.name
40
+ },
41
+ children: [
42
+ /* @__PURE__ */ jsx(LibraryHeader, {
43
+ project,
44
+ build,
45
+ components: data.groups.length,
46
+ stories: data.groups.reduce((total, group) => total + group.stories.length, 0),
47
+ branches: data.branches
48
+ }),
49
+ data.layer && data.layer.design.projectLinks.length > 0 ? /* @__PURE__ */ jsx(ProjectDesignLinks, { links: data.layer.design.projectLinks }) : null,
50
+ /* @__PURE__ */ jsx(HealthSlot, {
51
+ project,
52
+ data: data.health
53
+ }),
54
+ /* @__PURE__ */ jsx(LibraryGrid, {
55
+ groups: data.groups,
56
+ project,
57
+ build,
58
+ layer: data.layer
59
+ }),
60
+ /* @__PURE__ */ jsx("div", {
61
+ "data-filter-empty": true,
62
+ hidden: true,
63
+ children: /* @__PURE__ */ jsx(Meta, {
64
+ center: true,
65
+ children: "No stories match your filter."
66
+ })
67
+ })
68
+ ]
69
+ });
37
70
  }
38
- async function getLibraryBuild(db, project, branch) {
39
- const buildModel = new BuildModel(db);
71
+ async function loadLibraryData(project, build) {
72
+ const testing = testingEnabled();
73
+ const [catalog, snapshots, branches] = await Promise.all([
74
+ catalogOf().load(project.id, build.id),
75
+ testing ? new SnapshotModel(getStore().db).listByBuild(build.id) : Promise.resolve([]),
76
+ distinctBranches(project.id)
77
+ ]);
78
+ const entries = catalog?.entries ?? entriesFromSnapshots(snapshots);
79
+ return {
80
+ groups: groupLibrary(entries),
81
+ branches,
82
+ layer: snapshots.length > 0 ? await loadSnapshotLayer(project, snapshots) : null,
83
+ health: testing ? await loadHealthPanel(project) : null
84
+ };
85
+ }
86
+ /** Latest build of the branch (default branch when none is chosen), whatever its status. */
87
+ async function getLibraryBuild(project, branch) {
88
+ const buildModel = new BuildModel(getStore().db);
40
89
  const trimmed = branch?.trim();
41
90
  const target = trimmed === void 0 || trimmed === "" ? project.gitDefaultBranch : trimmed;
42
- const byBranch = await buildModel.list(project.id, { branch: target });
91
+ const byBranch = await buildModel.list(project.id, {
92
+ branch: target,
93
+ limit: 1
94
+ });
43
95
  if (byBranch[0]) return byBranch[0];
44
96
  if (target !== project.gitDefaultBranch) {
45
- const defaults = await buildModel.list(project.id, { branch: project.gitDefaultBranch });
97
+ const defaults = await buildModel.list(project.id, {
98
+ branch: project.gitDefaultBranch,
99
+ limit: 1
100
+ });
46
101
  if (defaults[0]) return defaults[0];
47
102
  }
48
- return (await buildModel.list(project.id))[0] ?? null;
103
+ return (await buildModel.list(project.id, { limit: 1 }))[0] ?? null;
49
104
  }
50
- async function distinctBranches(db, projectId) {
51
- const buildsList = await new BuildModel(db).list(projectId);
105
+ async function distinctBranches(projectId) {
106
+ const buildsList = await new BuildModel(getStore().db).list(projectId);
52
107
  return [...new Set(buildsList.map((b) => b.gitBranch))].toSorted((a, b) => a.localeCompare(b));
53
108
  }
54
- /**
55
- * Map snapshot story ids to their component's docs entry id. Reads the build's
56
- * Storybook index from storage and returns an empty map when statics are
57
- * purged or the build has no docs entries, so the Library never links to a
58
- * docs page that does not exist.
59
- */
60
- async function docsEntriesByStory(storage, projectId, buildId, snapshots) {
61
- const prefixes = await readDocsPrefixes(storage, projectId, buildId);
62
- const byStory = /* @__PURE__ */ new Map();
63
- for (const snap of snapshots) {
64
- const docsId = docsEntryId(prefixes, snap.storyId);
65
- if (docsId) byStory.set(snap.storyId, docsId);
66
- }
67
- return byStory;
68
- }
69
- async function readDocsPrefixes(storage, projectId, buildId) {
70
- const candidates = await Promise.all(["index.json", "stories.json"].map((name) => readIndexEntries(storage, projectId, buildId, name)));
71
- const prefixes = /* @__PURE__ */ new Set();
72
- for (const entries of candidates) if (entries) {
73
- for (const entry of Object.values(entries)) if (entry?.type === "docs" && typeof entry.id === "string" && entry.id.endsWith("--docs")) prefixes.add(entry.id.slice(0, -6));
74
- return prefixes;
75
- }
76
- return prefixes;
77
- }
78
- async function readIndexEntries(storage, projectId, buildId, name) {
79
- try {
80
- const raw = await storage.read(posix.join(storybookDir(projectId, buildId), name));
81
- return JSON.parse(raw.toString("utf8")).entries ?? null;
82
- } catch {
83
- return null;
84
- }
85
- }
86
- function docsEntryId(prefixes, storyId) {
87
- for (const prefix of prefixes) if (storyId.startsWith(`${prefix}--`) || storyId === prefix) return `${prefix}--docs`;
88
- }
89
109
  function renderEmptyLibrary(project) {
90
110
  return /* @__PURE__ */ jsxs(DocumentLayout, {
91
111
  title: "Library",
@@ -107,8 +127,10 @@ function renderEmptyLibrary(project) {
107
127
  })]
108
128
  });
109
129
  }
110
- function renderEmptySnapshots(project, build) {
130
+ /** A build with nothing to list: still publishing, failed, or its Storybook is gone. */
131
+ function renderNoStories(project, build) {
111
132
  const urls = createUrlBuilder("/", getStore().config.publishedBaseDomain);
133
+ const preparing = build.status === "pending";
112
134
  return /* @__PURE__ */ jsxs(DocumentLayout, {
113
135
  title: "Library",
114
136
  nav: {
@@ -127,8 +149,8 @@ function renderEmptySnapshots(project, build) {
127
149
  build.status
128
150
  ] })
129
151
  }), /* @__PURE__ */ jsx(EmptyState, {
130
- title: "No stories in latest build",
131
- description: "The latest build has no snapshots yet.",
152
+ title: preparing ? "Preparing the library" : "No stories in the latest build",
153
+ description: preparing ? "The latest build is still being published. Reload in a moment." : "The latest build has no readable Storybook index, so there is nothing to list.",
132
154
  action: /* @__PURE__ */ jsx(Button, {
133
155
  variant: "secondary",
134
156
  href: urls.build(project.slug, build.id),
@@ -137,55 +159,20 @@ function renderEmptySnapshots(project, build) {
137
159
  })]
138
160
  });
139
161
  }
140
- function groupByTitle(snapshots) {
141
- const map = /* @__PURE__ */ new Map();
142
- for (const snap of snapshots) {
143
- const key = snap.storyTitle || "Untitled";
144
- const list = map.get(key) ?? [];
145
- list.push(snap);
146
- map.set(key, list);
147
- }
148
- return map;
149
- }
150
- function distinctViewports(snapshots) {
151
- return [...new Set(snapshots.map((s) => s.viewportName))].toSorted();
152
- }
153
- function renderLibraryGrid(project, build, snapshots, branches, docsByStory) {
154
- const byTitle = groupByTitle(snapshots);
155
- const viewportNames = distinctViewports(snapshots);
156
- const multi = viewportNames.length > 1;
157
- return /* @__PURE__ */ jsxs(DocumentLayout, {
158
- title: "Library",
159
- nav: {
160
- active: "library",
161
- projectSlug: project.slug,
162
- projectName: project.name
163
- },
164
- children: [
165
- renderLibraryHeader(project, build, snapshots.length, branches),
166
- multi ? /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(Meta, {
167
- as: "span",
168
- children: ["Viewports: ", viewportNames.join(" · ")]
169
- }) }) : null,
170
- renderTitleList(byTitle, project, build, multi, docsByStory),
171
- /* @__PURE__ */ jsx("div", {
172
- "data-filter-empty": true,
173
- hidden: true,
174
- children: /* @__PURE__ */ jsx(Meta, {
175
- center: true,
176
- children: "No stories match your filter."
177
- })
178
- })
179
- ]
180
- });
181
- }
182
- function renderLibraryHeader(project, build, storyCount, branches) {
162
+ function LibraryHeader(props) {
163
+ const { project, build } = props;
183
164
  const urls = createUrlBuilder("/", getStore().config.publishedBaseDomain);
184
165
  return /* @__PURE__ */ jsx(PageHeader, {
185
166
  title: "Library",
186
167
  description: /* @__PURE__ */ jsxs(Fragment, { children: [
187
- storyCount,
188
- " stories · Latest build",
168
+ props.components,
169
+ " ",
170
+ props.components === 1 ? "component" : "components",
171
+ " · ",
172
+ props.stories,
173
+ " ",
174
+ props.stories === 1 ? "story" : "stories",
175
+ " · Latest build",
189
176
  " ",
190
177
  /* @__PURE__ */ jsxs("a", {
191
178
  href: urls.build(project.slug, build.id),
@@ -198,8 +185,11 @@ function renderLibraryHeader(project, build, storyCount, branches) {
198
185
  " ",
199
186
  "· ",
200
187
  new Date(build.createdAt).toLocaleString(),
201
- " · ",
202
- build.status
188
+ " ",
189
+ /* @__PURE__ */ jsx(Badge, {
190
+ tone: statusTone(build.status),
191
+ children: build.status
192
+ })
203
193
  ] }),
204
194
  actions: /* @__PURE__ */ jsxs(HStack, { children: [
205
195
  /* @__PURE__ */ jsx(FilterInput, {
@@ -213,20 +203,24 @@ function renderLibraryHeader(project, build, storyCount, branches) {
213
203
  href: urls.short(build.id),
214
204
  children: "View Storybook"
215
205
  }),
216
- renderBranchPicker(project, build, branches)
206
+ /* @__PURE__ */ jsx(BranchPicker, {
207
+ project,
208
+ build,
209
+ branches: props.branches
210
+ })
217
211
  ] })
218
212
  });
219
213
  }
220
- function renderBranchPicker(project, build, branches) {
214
+ function BranchPicker(props) {
221
215
  return /* @__PURE__ */ jsx("form", {
222
216
  method: "get",
223
- action: `/projects/${project.slug}/library`,
217
+ action: `/projects/${props.project.slug}/library`,
224
218
  children: /* @__PURE__ */ jsxs(HStack, { children: [/* @__PURE__ */ jsx(SelectField, {
225
219
  label: "Branch",
226
220
  name: "branch",
227
221
  layout: "inline",
228
- value: build.gitBranch,
229
- options: branches.map((b) => ({
222
+ value: props.build.gitBranch,
223
+ options: props.branches.map((b) => ({
230
224
  value: b,
231
225
  label: b
232
226
  }))
@@ -237,106 +231,6 @@ function renderBranchPicker(project, build, branches) {
237
231
  })] })
238
232
  });
239
233
  }
240
- function renderTitleList(byTitle, project, build, multi, docsByStory) {
241
- const titles = [...byTitle.keys()].toSorted((a, b) => a.localeCompare(b));
242
- return /* @__PURE__ */ jsx(VStack, { children: titles.map((title) => renderTitleGroup(title, byTitle.get(title) ?? [], project, build, multi, docsByStory)) });
243
- }
244
- function renderTitleGroup(title, group, project, build, hasMultipleViewports, docsByStory) {
245
- const byViewport = groupByViewport(group);
246
- const vNames = [...byViewport.keys()].toSorted();
247
- return /* @__PURE__ */ jsxs(Card, {
248
- padded: false,
249
- "data-filter-item": true,
250
- "data-filter-text": `${title} ${group.map((snap) => snap.storyName).join(" ")}`,
251
- children: [/* @__PURE__ */ jsxs(CardSection, {
252
- divider: true,
253
- children: [/* @__PURE__ */ jsx(SectionTitle, { children: title }), /* @__PURE__ */ jsxs(Meta, {
254
- as: "span",
255
- children: [group.length, " stories"]
256
- })]
257
- }), /* @__PURE__ */ jsx(CardSection, { children: /* @__PURE__ */ jsx(VStack, { children: vNames.map((vName) => renderViewportGroup(vName, byViewport.get(vName) ?? [], project, build, hasMultipleViewports, docsByStory)) }) })]
258
- }, title);
259
- }
260
- function groupByViewport(group) {
261
- const map = /* @__PURE__ */ new Map();
262
- for (const snap of group) {
263
- const list = map.get(snap.viewportName) ?? [];
264
- list.push(snap);
265
- map.set(snap.viewportName, list);
266
- }
267
- return map;
268
- }
269
- function renderViewportGroup(vName, snaps, project, build, hasMultipleViewports, docsByStory) {
270
- return /* @__PURE__ */ jsxs("div", { children: [hasMultipleViewports ? /* @__PURE__ */ jsx(SectionTitle, {
271
- level: 3,
272
- children: vName
273
- }) : null, /* @__PURE__ */ jsx("div", {
274
- class: snapshotGrid,
275
- children: snaps.toSorted((a, b) => a.storyName.localeCompare(b.storyName)).map((snap) => renderSnapshotCard(snap, project, build, docsByStory))
276
- })] }, vName);
277
- }
278
- function renderSnapshotCard(snap, project, build, docsByStory) {
279
- const docsId = docsByStory.get(snap.storyId);
280
- return /* @__PURE__ */ jsxs("div", {
281
- class: snapshotCard,
282
- "data-filter-item": true,
283
- "data-filter-text": `${snap.storyTitle} ${snap.storyName}`,
284
- children: [/* @__PURE__ */ jsxs("div", {
285
- class: snapshotCardHead,
286
- children: [/* @__PURE__ */ jsx("strong", { children: snap.storyName }), /* @__PURE__ */ jsxs("span", {
287
- class: snapshotCardMeta,
288
- children: [
289
- /* @__PURE__ */ jsx(Badge, {
290
- tone: statusTone(snap.status),
291
- children: snap.status
292
- }),
293
- " ",
294
- snap.viewportWidth,
295
- "×",
296
- snap.viewportHeight
297
- ]
298
- })]
299
- }), /* @__PURE__ */ jsxs("div", {
300
- class: snapshotCardBody,
301
- children: [/* @__PURE__ */ jsx("img", {
302
- class: snapshotImg,
303
- src: `/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${snap.id}/image`,
304
- alt: `${snap.storyTitle} / ${snap.storyName}`,
305
- loading: "lazy"
306
- }), /* @__PURE__ */ jsx(HStack, { children: renderCardActions(snap, project, build, docsId) })]
307
- })]
308
- }, snap.id);
309
- }
310
- function renderCardActions(snap, project, build, docsId) {
311
- const urls = createUrlBuilder("/", getStore().config.publishedBaseDomain);
312
- return /* @__PURE__ */ jsxs(Fragment, { children: [
313
- /* @__PURE__ */ jsx(Button, {
314
- variant: "secondary",
315
- size: "sm",
316
- icon: "columns",
317
- href: `${urls.buildDiff(project.slug, build.id)}?snapshot=${snap.id}`,
318
- children: "Review"
319
- }),
320
- /* @__PURE__ */ jsx(Button, {
321
- variant: "ghost",
322
- size: "sm",
323
- icon: "external-link",
324
- href: `${urls.storybookBuild(project.slug, build.id)}/?storyId=${encodeURIComponent(snap.storyId)}`,
325
- target: "_blank",
326
- rel: "noopener",
327
- children: "Preview"
328
- }),
329
- docsId ? /* @__PURE__ */ jsx(Button, {
330
- variant: "ghost",
331
- size: "sm",
332
- icon: "book",
333
- href: `/projects/${project.slug}/storybook/build/${build.id}/?storyId=${encodeURIComponent(docsId)}&viewMode=docs`,
334
- target: "_blank",
335
- rel: "noopener",
336
- children: "Docs"
337
- }) : null
338
- ] });
339
- }
340
234
  //#endregion
341
235
  export { renderLibraryPage };
342
236
 
@@ -1 +1 @@
1
- {"version":3,"file":"library.mjs","names":[],"sources":["../../src/pages/library.tsx"],"sourcesContent":["// oxlint-disable typescript/promise-function-async -- Hono JSX may return Promise<HtmlEscapedString>\nimport { BuildModel } from \"@storyshelf/core/models\";\nimport { ProjectModel } from \"@storyshelf/core/models\";\nimport { SnapshotModel } from \"@storyshelf/core/models\";\nimport type { Build } from \"@storyshelf/core/schema\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport type { Snapshot } from \"@storyshelf/core/schema\";\nimport { createUrlBuilder } from \"@storyshelf/core/urls\";\nimport { storybookDir } from \"@storyshelf/core/utils\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport { posix } from \"node:path\";\nimport { getStore } from \"../store.ts\";\nimport {\n Badge,\n Button,\n Card,\n CardSection,\n EmptyState,\n FilterInput,\n HStack,\n Meta,\n PageHeader,\n SectionTitle,\n SelectField,\n VStack,\n statusTone,\n} from \"../ui/components.tsx\";\nimport { css } from \"../ui/css.ts\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\nimport {\n diffPaneImg,\n snapshotCard,\n snapshotCardBody,\n snapshotCardHead,\n snapshotCardMeta,\n snapshotGrid,\n} from \"../ui/styles/review.ts\";\n\n/** Snapshot image with library height cap, extending the shared diff pane image. */\nconst snapshotImg = css`\n /* snapshot-img */\n ${diffPaneImg}\n max-height: 240px;\n object-fit: contain;\n`;\n\n/** Library page: gallery grouped by title from latest build on selected branch. */\nexport async function renderLibraryPage(\n slug: string,\n branch?: string,\n): Promise<RenderedContent | null> {\n const { db, storage } = getStore();\n const project = await new ProjectModel(db).getBySlug(slug);\n if (!project) return null;\n const build = await getLibraryBuild(db, project, branch);\n if (!build) return renderEmptyLibrary(project);\n const snapshots = await new SnapshotModel(db).listByBuild(build.id);\n if (snapshots.length === 0) return renderEmptySnapshots(project, build);\n const branches = await distinctBranches(db, project.id);\n const docsByStory = await docsEntriesByStory(storage, project.id, build.id, snapshots);\n return renderLibraryGrid(project, build, snapshots, branches, docsByStory);\n}\n\nasync function getLibraryBuild(\n db: ReturnType<typeof getStore>[\"db\"],\n project: Project,\n branch?: string,\n): Promise<Build | null> {\n const buildModel = new BuildModel(db);\n const trimmed = branch?.trim();\n const target = trimmed === undefined || trimmed === \"\" ? project.gitDefaultBranch : trimmed;\n const byBranch = await buildModel.list(project.id, { branch: target });\n if (byBranch[0]) return byBranch[0];\n if (target !== project.gitDefaultBranch) {\n const defaults = await buildModel.list(project.id, { branch: project.gitDefaultBranch });\n if (defaults[0]) return defaults[0];\n }\n const all = await buildModel.list(project.id);\n return all[0] ?? null;\n}\n\nasync function distinctBranches(\n db: ReturnType<typeof getStore>[\"db\"],\n projectId: string,\n): Promise<string[]> {\n const buildsList = await new BuildModel(db).list(projectId);\n return [...new Set(buildsList.map((b) => b.gitBranch))].toSorted((a, b) => a.localeCompare(b));\n}\n\n/** Storybook index entries (only the docs subset we need). */\ninterface LibraryIndexEntry {\n id?: string;\n type?: string;\n}\n\n/**\n * Map snapshot story ids to their component's docs entry id. Reads the build's\n * Storybook index from storage and returns an empty map when statics are\n * purged or the build has no docs entries, so the Library never links to a\n * docs page that does not exist.\n */\nasync function docsEntriesByStory(\n storage: ReturnType<typeof getStore>[\"storage\"],\n projectId: string,\n buildId: string,\n snapshots: Snapshot[],\n): Promise<Map<string, string>> {\n const prefixes = await readDocsPrefixes(storage, projectId, buildId);\n const byStory = new Map<string, string>();\n for (const snap of snapshots) {\n const docsId = docsEntryId(prefixes, snap.storyId);\n if (docsId) byStory.set(snap.storyId, docsId);\n }\n return byStory;\n}\n\nasync function readDocsPrefixes(\n storage: ReturnType<typeof getStore>[\"storage\"],\n projectId: string,\n buildId: string,\n): Promise<Set<string>> {\n const candidates = await Promise.all(\n [\"index.json\", \"stories.json\"].map((name) =>\n readIndexEntries(storage, projectId, buildId, name),\n ),\n );\n const prefixes = new Set<string>();\n for (const entries of candidates) {\n if (entries) {\n for (const entry of Object.values(entries)) {\n if (entry?.type === \"docs\" && typeof entry.id === \"string\" && entry.id.endsWith(\"--docs\")) {\n prefixes.add(entry.id.slice(0, -\"--docs\".length));\n }\n }\n return prefixes;\n }\n }\n return prefixes;\n}\n\nasync function readIndexEntries(\n storage: ReturnType<typeof getStore>[\"storage\"],\n projectId: string,\n buildId: string,\n name: string,\n): Promise<Record<string, LibraryIndexEntry> | null> {\n try {\n const raw = await storage.read(posix.join(storybookDir(projectId, buildId), name));\n const parsed = JSON.parse(raw.toString(\"utf8\")) as {\n entries?: Record<string, LibraryIndexEntry>;\n };\n return parsed.entries ?? null;\n } catch {\n return null;\n }\n}\n\nfunction docsEntryId(prefixes: Set<string>, storyId: string): string | undefined {\n for (const prefix of prefixes) {\n if (storyId.startsWith(`${prefix}--`) || storyId === prefix) {\n return `${prefix}--docs`;\n }\n }\n return undefined;\n}\n\nfunction renderEmptyLibrary(project: Project): RenderedContent {\n return (\n <DocumentLayout\n title=\"Library\"\n nav={{ active: \"library\", projectSlug: project.slug, projectName: project.name }}\n >\n <PageHeader\n title=\"Library\"\n description={\n <>All stories/components from the latest build on {project.gitDefaultBranch}.</>\n }\n />\n <EmptyState title=\"No builds yet\" description=\"Upload a build to populate the library.\" />\n </DocumentLayout>\n );\n}\n\nfunction renderEmptySnapshots(project: Project, build: Build): RenderedContent {\n const urls = createUrlBuilder(\"/\", getStore().config.publishedBaseDomain);\n return (\n <DocumentLayout\n title=\"Library\"\n nav={{ active: \"library\", projectSlug: project.slug, projectName: project.name }}\n >\n <PageHeader\n title=\"Library\"\n description={\n <>\n Latest build {build.gitBranch} · {build.gitSha.slice(0, 7)} · {build.status}\n </>\n }\n />\n <EmptyState\n title=\"No stories in latest build\"\n description=\"The latest build has no snapshots yet.\"\n action={\n <Button variant=\"secondary\" href={urls.build(project.slug, build.id)}>\n View build\n </Button>\n }\n />\n </DocumentLayout>\n );\n}\n\nfunction groupByTitle(snapshots: Snapshot[]): Map<string, Snapshot[]> {\n const map = new Map<string, Snapshot[]>();\n for (const snap of snapshots) {\n const key = snap.storyTitle || \"Untitled\";\n const list = map.get(key) ?? [];\n list.push(snap);\n map.set(key, list);\n }\n return map;\n}\n\nfunction distinctViewports(snapshots: Snapshot[]): string[] {\n return [...new Set(snapshots.map((s) => s.viewportName))].toSorted();\n}\n\nfunction renderLibraryGrid(\n project: Project,\n build: Build,\n snapshots: Snapshot[],\n branches: string[],\n docsByStory: Map<string, string>,\n): RenderedContent {\n const byTitle = groupByTitle(snapshots);\n const viewportNames = distinctViewports(snapshots);\n const multi = viewportNames.length > 1;\n return (\n <DocumentLayout\n title=\"Library\"\n nav={{ active: \"library\", projectSlug: project.slug, projectName: project.name }}\n >\n {renderLibraryHeader(project, build, snapshots.length, branches)}\n {multi ? (\n <Card>\n <Meta as=\"span\">Viewports: {viewportNames.join(\" · \")}</Meta>\n </Card>\n ) : null}\n {renderTitleList(byTitle, project, build, multi, docsByStory)}\n <div data-filter-empty hidden>\n <Meta center>No stories match your filter.</Meta>\n </div>\n </DocumentLayout>\n );\n}\n\nfunction renderLibraryHeader(\n project: Project,\n build: Build,\n storyCount: number,\n branches: string[],\n): HtmlEscapedString | Promise<HtmlEscapedString> {\n const urls = createUrlBuilder(\"/\", getStore().config.publishedBaseDomain);\n return (\n <PageHeader\n title=\"Library\"\n description={\n <>\n {storyCount} stories · Latest build{\" \"}\n <a href={urls.build(project.slug, build.id)}>\n {build.gitBranch} · {build.gitSha.slice(0, 7)}\n </a>{\" \"}\n · {new Date(build.createdAt).toLocaleString()} · {build.status}\n </>\n }\n actions={\n <HStack>\n <FilterInput label=\"Filter stories\" placeholder=\"Filter stories…\" />\n <Button variant=\"ghost\" size=\"sm\" icon=\"external-link\" href={urls.short(build.id)}>\n View Storybook\n </Button>\n {renderBranchPicker(project, build, branches)}\n </HStack>\n }\n />\n );\n}\n\nfunction renderBranchPicker(\n project: Project,\n build: Build,\n branches: string[],\n): HtmlEscapedString | Promise<HtmlEscapedString> {\n return (\n <form method=\"get\" action={`/projects/${project.slug}/library`}>\n <HStack>\n <SelectField\n label=\"Branch\"\n name=\"branch\"\n layout=\"inline\"\n value={build.gitBranch}\n options={branches.map((b) => ({ value: b, label: b }))}\n />\n <Button variant=\"secondary\" type=\"submit\">\n View\n </Button>\n </HStack>\n </form>\n );\n}\n\nfunction renderTitleList(\n byTitle: Map<string, Snapshot[]>,\n project: Project,\n build: Build,\n multi: boolean,\n docsByStory: Map<string, string>,\n): HtmlEscapedString | Promise<HtmlEscapedString> {\n const titles = [...byTitle.keys()].toSorted((a, b) => a.localeCompare(b));\n return (\n <VStack>\n {titles.map((title) =>\n renderTitleGroup(title, byTitle.get(title) ?? [], project, build, multi, docsByStory),\n )}\n </VStack>\n );\n}\n\nfunction renderTitleGroup(\n title: string,\n group: Snapshot[],\n project: Project,\n build: Build,\n hasMultipleViewports: boolean,\n docsByStory: Map<string, string>,\n): HtmlEscapedString | Promise<HtmlEscapedString> {\n const byViewport = groupByViewport(group);\n const vNames = [...byViewport.keys()].toSorted();\n return (\n <Card\n key={title}\n padded={false}\n data-filter-item\n data-filter-text={`${title} ${group.map((snap) => snap.storyName).join(\" \")}`}\n >\n <CardSection divider>\n <SectionTitle>{title}</SectionTitle>\n <Meta as=\"span\">{group.length} stories</Meta>\n </CardSection>\n <CardSection>\n <VStack>\n {vNames.map((vName) =>\n renderViewportGroup(\n vName,\n byViewport.get(vName) ?? [],\n project,\n build,\n hasMultipleViewports,\n docsByStory,\n ),\n )}\n </VStack>\n </CardSection>\n </Card>\n );\n}\n\nfunction groupByViewport(group: Snapshot[]): Map<string, Snapshot[]> {\n const map = new Map<string, Snapshot[]>();\n for (const snap of group) {\n const list = map.get(snap.viewportName) ?? [];\n list.push(snap);\n map.set(snap.viewportName, list);\n }\n return map;\n}\n\nfunction renderViewportGroup(\n vName: string,\n snaps: Snapshot[],\n project: Project,\n build: Build,\n hasMultipleViewports: boolean,\n docsByStory: Map<string, string>,\n): HtmlEscapedString | Promise<HtmlEscapedString> {\n return (\n <div key={vName}>\n {hasMultipleViewports ? <SectionTitle level={3}>{vName}</SectionTitle> : null}\n <div class={snapshotGrid}>\n {snaps\n .toSorted((a, b) => a.storyName.localeCompare(b.storyName))\n .map((snap) => renderSnapshotCard(snap, project, build, docsByStory))}\n </div>\n </div>\n );\n}\n\nfunction renderSnapshotCard(\n snap: Snapshot,\n project: Project,\n build: Build,\n docsByStory: Map<string, string>,\n): HtmlEscapedString | Promise<HtmlEscapedString> {\n const docsId = docsByStory.get(snap.storyId);\n return (\n <div\n key={snap.id}\n class={snapshotCard}\n data-filter-item\n data-filter-text={`${snap.storyTitle} ${snap.storyName}`}\n >\n <div class={snapshotCardHead}>\n <strong>{snap.storyName}</strong>\n <span class={snapshotCardMeta}>\n <Badge tone={statusTone(snap.status)}>{snap.status}</Badge> {snap.viewportWidth}×\n {snap.viewportHeight}\n </span>\n </div>\n <div class={snapshotCardBody}>\n <img\n class={snapshotImg}\n src={`/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${snap.id}/image`}\n alt={`${snap.storyTitle} / ${snap.storyName}`}\n loading=\"lazy\"\n />\n <HStack>{renderCardActions(snap, project, build, docsId)}</HStack>\n </div>\n </div>\n );\n}\n\nfunction renderCardActions(\n snap: Snapshot,\n project: Project,\n build: Build,\n docsId: string | undefined,\n): HtmlEscapedString | Promise<HtmlEscapedString> {\n const urls = createUrlBuilder(\"/\", getStore().config.publishedBaseDomain);\n return (\n <>\n <Button\n variant=\"secondary\"\n size=\"sm\"\n icon=\"columns\"\n href={`${urls.buildDiff(project.slug, build.id)}?snapshot=${snap.id}`}\n >\n Review\n </Button>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n icon=\"external-link\"\n href={`${urls.storybookBuild(project.slug, build.id)}/?storyId=${encodeURIComponent(snap.storyId)}`}\n target=\"_blank\"\n rel=\"noopener\"\n >\n Preview\n </Button>\n {docsId ? (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n icon=\"book\"\n href={`/projects/${project.slug}/storybook/build/${build.id}/?storyId=${encodeURIComponent(docsId)}&viewMode=docs`}\n target=\"_blank\"\n rel=\"noopener\"\n >\n Docs\n </Button>\n ) : null}\n </>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"library.mjs","names":[],"sources":["../../src/pages/library.tsx"],"sourcesContent":["// oxlint-disable typescript/promise-function-async -- Hono JSX may return Promise<HtmlEscapedString>\nimport { BuildModel, ProjectModel, SnapshotModel } from \"@storyshelf/core/models\";\nimport type { Build, Project } from \"@storyshelf/core/schema\";\nimport { createUrlBuilder } from \"@storyshelf/core/urls\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport { loadHealthPanel, type HealthPanelData } from \"../insights/health-panel.ts\";\nimport { catalogOf, getStore, testingEnabled } from \"../store.ts\";\nimport {\n Button,\n EmptyState,\n FilterInput,\n HStack,\n Meta,\n PageHeader,\n SelectField,\n statusTone,\n Badge,\n} from \"../ui/components.tsx\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\nimport { ProjectDesignLinks } from \"./design-links.tsx\";\nimport { HealthSlot } from \"./health-panel.tsx\";\nimport { LibraryGrid } from \"./library-grid.tsx\";\nimport { entriesFromSnapshots, groupLibrary, type LibraryGroup } from \"./library-model.ts\";\nimport { loadSnapshotLayer, type SnapshotLayer } from \"./library-snapshots.tsx\";\n\ntype Rendered = HtmlEscapedString | Promise<HtmlEscapedString>;\n\n/**\n * Library page (ADR 0006, decision 9): the components of the latest build of the selected branch,\n * whatever its status, from the build's Storybook catalog. With `testing` the cards gain\n * screenshots, statuses and design links, and the page the health panel.\n */\nexport async function renderLibraryPage(\n slug: string,\n branch?: string,\n): Promise<RenderedContent | null> {\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) return null;\n const build = await getLibraryBuild(project, branch);\n if (!build) return renderEmptyLibrary(project);\n const data = await loadLibraryData(project, build);\n if (data.groups.length === 0) return renderNoStories(project, build);\n return (\n <DocumentLayout\n title=\"Library\"\n nav={{ active: \"library\", projectSlug: project.slug, projectName: project.name }}\n >\n <LibraryHeader\n project={project}\n build={build}\n components={data.groups.length}\n stories={data.groups.reduce((total, group) => total + group.stories.length, 0)}\n branches={data.branches}\n />\n {data.layer && data.layer.design.projectLinks.length > 0 ? (\n <ProjectDesignLinks links={data.layer.design.projectLinks} />\n ) : null}\n <HealthSlot project={project} data={data.health} />\n <LibraryGrid groups={data.groups} project={project} build={build} layer={data.layer} />\n <div data-filter-empty hidden>\n <Meta center>No stories match your filter.</Meta>\n </div>\n </DocumentLayout>\n );\n}\n\n/** Everything the library renders of a build: catalog groups, branches, and the `testing` layer. */\ninterface LibraryData {\n groups: LibraryGroup[];\n branches: string[];\n layer: SnapshotLayer | null;\n health: HealthPanelData | null;\n}\n\nasync function loadLibraryData(project: Project, build: Build): Promise<LibraryData> {\n const testing = testingEnabled();\n const [catalog, snapshots, branches] = await Promise.all([\n catalogOf().load(project.id, build.id),\n testing ? new SnapshotModel(getStore().db).listByBuild(build.id) : Promise.resolve([]),\n distinctBranches(project.id),\n ]);\n const entries = catalog?.entries ?? entriesFromSnapshots(snapshots);\n return {\n groups: groupLibrary(entries),\n branches,\n layer: snapshots.length > 0 ? await loadSnapshotLayer(project, snapshots) : null,\n health: testing ? await loadHealthPanel(project) : null,\n };\n}\n\n/** Latest build of the branch (default branch when none is chosen), whatever its status. */\nasync function getLibraryBuild(project: Project, branch?: string): Promise<Build | null> {\n const buildModel = new BuildModel(getStore().db);\n const trimmed = branch?.trim();\n const target = trimmed === undefined || trimmed === \"\" ? project.gitDefaultBranch : trimmed;\n const byBranch = await buildModel.list(project.id, { branch: target, limit: 1 });\n if (byBranch[0]) return byBranch[0];\n if (target !== project.gitDefaultBranch) {\n const defaults = await buildModel.list(project.id, {\n branch: project.gitDefaultBranch,\n limit: 1,\n });\n if (defaults[0]) return defaults[0];\n }\n return (await buildModel.list(project.id, { limit: 1 }))[0] ?? null;\n}\n\nasync function distinctBranches(projectId: string): Promise<string[]> {\n const buildsList = await new BuildModel(getStore().db).list(projectId);\n return [...new Set(buildsList.map((b) => b.gitBranch))].toSorted((a, b) => a.localeCompare(b));\n}\n\nfunction renderEmptyLibrary(project: Project): RenderedContent {\n return (\n <DocumentLayout\n title=\"Library\"\n nav={{ active: \"library\", projectSlug: project.slug, projectName: project.name }}\n >\n <PageHeader\n title=\"Library\"\n description={\n <>All stories/components from the latest build on {project.gitDefaultBranch}.</>\n }\n />\n <EmptyState title=\"No builds yet\" description=\"Upload a build to populate the library.\" />\n </DocumentLayout>\n );\n}\n\n/** A build with nothing to list: still publishing, failed, or its Storybook is gone. */\nfunction renderNoStories(project: Project, build: Build): RenderedContent {\n const urls = createUrlBuilder(\"/\", getStore().config.publishedBaseDomain);\n const preparing = build.status === \"pending\";\n return (\n <DocumentLayout\n title=\"Library\"\n nav={{ active: \"library\", projectSlug: project.slug, projectName: project.name }}\n >\n <PageHeader\n title=\"Library\"\n description={\n <>\n Latest build {build.gitBranch} · {build.gitSha.slice(0, 7)} · {build.status}\n </>\n }\n />\n <EmptyState\n title={preparing ? \"Preparing the library\" : \"No stories in the latest build\"}\n description={\n preparing\n ? \"The latest build is still being published. Reload in a moment.\"\n : \"The latest build has no readable Storybook index, so there is nothing to list.\"\n }\n action={\n <Button variant=\"secondary\" href={urls.build(project.slug, build.id)}>\n View build\n </Button>\n }\n />\n </DocumentLayout>\n );\n}\n\nfunction LibraryHeader(props: {\n project: Project;\n build: Build;\n components: number;\n stories: number;\n branches: string[];\n}): Rendered {\n const { project, build } = props;\n const urls = createUrlBuilder(\"/\", getStore().config.publishedBaseDomain);\n return (\n <PageHeader\n title=\"Library\"\n description={\n <>\n {props.components} {props.components === 1 ? \"component\" : \"components\"} · {props.stories}{\" \"}\n {props.stories === 1 ? \"story\" : \"stories\"} · Latest build{\" \"}\n <a href={urls.build(project.slug, build.id)}>\n {build.gitBranch} · {build.gitSha.slice(0, 7)}\n </a>{\" \"}\n · {new Date(build.createdAt).toLocaleString()}{\" \"}\n <Badge tone={statusTone(build.status)}>{build.status}</Badge>\n </>\n }\n actions={\n <HStack>\n <FilterInput label=\"Filter stories\" placeholder=\"Filter stories…\" />\n <Button variant=\"ghost\" size=\"sm\" icon=\"external-link\" href={urls.short(build.id)}>\n View Storybook\n </Button>\n <BranchPicker project={project} build={build} branches={props.branches} />\n </HStack>\n }\n />\n );\n}\n\nfunction BranchPicker(props: { project: Project; build: Build; branches: string[] }): Rendered {\n return (\n <form method=\"get\" action={`/projects/${props.project.slug}/library`}>\n <HStack>\n <SelectField\n label=\"Branch\"\n name=\"branch\"\n layout=\"inline\"\n value={props.build.gitBranch}\n options={props.branches.map((b) => ({ value: b, label: b }))}\n />\n <Button variant=\"secondary\" type=\"submit\">\n View\n </Button>\n </HStack>\n </form>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { Alert } from "../ui/feedback.mjs";
5
+ import { jsx, jsxs } from "hono/jsx/jsx-runtime";
6
+ //#region src/pages/notifications-paused.tsx
7
+ /** Notifications need a public URL, so without one nothing is sent (ADR 0014, decision 11). */
8
+ function NotificationsPausedNotice(props) {
9
+ return props.paused ? /* @__PURE__ */ jsxs(Alert, {
10
+ tone: "warning",
11
+ title: "Notifications are paused",
12
+ children: [
13
+ "Notifications are paused until a public URL is set; set ",
14
+ /* @__PURE__ */ jsx("code", { children: "PUBLIC_BASE_URL" }),
15
+ " or confirm the detected origin on the System page. Nothing is sent meanwhile, and no channel is marked as failing."
16
+ ]
17
+ }) : null;
18
+ }
19
+ //#endregion
20
+ export { NotificationsPausedNotice };
21
+
22
+ //# sourceMappingURL=notifications-paused.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notifications-paused.mjs","names":[],"sources":["../../src/pages/notifications-paused.tsx"],"sourcesContent":["import type { HtmlEscapedString } from \"hono/utils/html\";\nimport { Alert } from \"../ui/components.tsx\";\n\n/** Notifications need a public URL, so without one nothing is sent (ADR 0014, decision 11). */\nexport function NotificationsPausedNotice(props: {\n paused: boolean | undefined;\n}): HtmlEscapedString | Promise<HtmlEscapedString> | null {\n return props.paused ? (\n <Alert tone=\"warning\" title=\"Notifications are paused\">\n Notifications are paused until a public URL is set; set <code>PUBLIC_BASE_URL</code> or\n confirm the detected origin on the System page. Nothing is sent meanwhile, and no channel is\n marked as failing.\n </Alert>\n ) : null;\n}\n"],"mappings":";;;;;;;AAIA,SAAgB,0BAA0B,OAEgB;CACxD,OAAO,MAAM,SACX,qBAAC,OAAD;EAAO,MAAK;EAAU,OAAM;EAA5B,UAAA;GAAuD;GACG,oBAAC,QAAD,EAAA,UAAM,kBAAqB,CAAA;GAAC;EAG/E;CACL,CAAA,IAAA;AACN"}
@@ -10,6 +10,7 @@ import { HStack, VStack } from "../ui/stacks.mjs";
10
10
  import { Table } from "../ui/table.mjs";
11
11
  import { csrfField } from "../ui/csrf-field.mjs";
12
12
  import { DocumentLayout } from "../ui/document.mjs";
13
+ import { NotificationsPausedNotice } from "./notifications-paused.mjs";
13
14
  import { passkeyRegisterScript } from "./passkey-ceremony.mjs";
14
15
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
15
16
  import { safeImageUrl } from "@storyshelf/core/urls";
@@ -297,55 +298,59 @@ function renderProfilePage(state) {
297
298
  ] }, m.projectSlug)) })] })
298
299
  })
299
300
  ] }),
300
- /* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsx(PageHeader, {
301
- title: "Notifications",
302
- description: "Email alerts per project. Fine-tune topics in each project's settings."
303
- }), memberships.length === 0 ? /* @__PURE__ */ jsx("p", {
304
- class: profileMeta,
305
- children: "No project memberships."
306
- }) : /* @__PURE__ */ jsx(Table, {
307
- dense: true,
308
- children: /* @__PURE__ */ jsxs("table", { children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
309
- /* @__PURE__ */ jsx("th", { children: "Project" }),
310
- /* @__PURE__ */ jsx("th", { children: "Status" }),
311
- /* @__PURE__ */ jsx("th", { children: /* @__PURE__ */ jsx("span", {
312
- class: "visually-hidden",
313
- children: "Actions"
314
- }) })
315
- ] }) }), /* @__PURE__ */ jsx("tbody", { children: memberships.map((m) => {
316
- const subscribed = subscribedSlugs.includes(m.projectSlug);
317
- return /* @__PURE__ */ jsxs("tr", { children: [
318
- /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx("a", {
319
- href: `/projects/${m.projectSlug}`,
320
- children: m.projectName
321
- }) }),
322
- /* @__PURE__ */ jsx("td", { children: subscribed ? /* @__PURE__ */ jsx(Badge, { children: "Email on" }) : /* @__PURE__ */ jsx(Badge, { children: "Off" }) }),
323
- /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsxs("form", {
324
- method: "post",
325
- action: "/profile/notifications",
326
- children: [
327
- csrfField(),
328
- /* @__PURE__ */ jsx("input", {
329
- type: "hidden",
330
- name: "slug",
331
- value: m.projectSlug
332
- }),
333
- /* @__PURE__ */ jsx("input", {
334
- type: "hidden",
335
- name: "enabled",
336
- value: subscribed ? "" : "1"
337
- }),
338
- /* @__PURE__ */ jsx(Button, {
339
- variant: "secondary",
340
- size: "sm",
341
- type: "submit",
342
- children: subscribed ? "Disable" : "Enable"
343
- })
344
- ]
301
+ /* @__PURE__ */ jsxs(Card, { children: [
302
+ /* @__PURE__ */ jsx(PageHeader, {
303
+ title: "Notifications",
304
+ description: "Email alerts per project. Fine-tune topics in each project's settings."
305
+ }),
306
+ /* @__PURE__ */ jsx(NotificationsPausedNotice, { paused: state.notificationsPaused }),
307
+ memberships.length === 0 ? /* @__PURE__ */ jsx("p", {
308
+ class: profileMeta,
309
+ children: "No project memberships."
310
+ }) : /* @__PURE__ */ jsx(Table, {
311
+ dense: true,
312
+ children: /* @__PURE__ */ jsxs("table", { children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
313
+ /* @__PURE__ */ jsx("th", { children: "Project" }),
314
+ /* @__PURE__ */ jsx("th", { children: "Status" }),
315
+ /* @__PURE__ */ jsx("th", { children: /* @__PURE__ */ jsx("span", {
316
+ class: "visually-hidden",
317
+ children: "Actions"
345
318
  }) })
346
- ] }, m.projectSlug);
347
- }) })] })
348
- })] }),
319
+ ] }) }), /* @__PURE__ */ jsx("tbody", { children: memberships.map((m) => {
320
+ const subscribed = subscribedSlugs.includes(m.projectSlug);
321
+ return /* @__PURE__ */ jsxs("tr", { children: [
322
+ /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx("a", {
323
+ href: `/projects/${m.projectSlug}`,
324
+ children: m.projectName
325
+ }) }),
326
+ /* @__PURE__ */ jsx("td", { children: subscribed ? /* @__PURE__ */ jsx(Badge, { children: "Email on" }) : /* @__PURE__ */ jsx(Badge, { children: "Off" }) }),
327
+ /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsxs("form", {
328
+ method: "post",
329
+ action: "/profile/notifications",
330
+ children: [
331
+ csrfField(),
332
+ /* @__PURE__ */ jsx("input", {
333
+ type: "hidden",
334
+ name: "slug",
335
+ value: m.projectSlug
336
+ }),
337
+ /* @__PURE__ */ jsx("input", {
338
+ type: "hidden",
339
+ name: "enabled",
340
+ value: subscribed ? "" : "1"
341
+ }),
342
+ /* @__PURE__ */ jsx(Button, {
343
+ variant: "secondary",
344
+ size: "sm",
345
+ type: "submit",
346
+ children: subscribed ? "Disable" : "Enable"
347
+ })
348
+ ]
349
+ }) })
350
+ ] }, m.projectSlug);
351
+ }) })] })
352
+ })
353
+ ] }),
349
354
  /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs("form", {
350
355
  method: "post",
351
356
  action: "/auth/logout",