@storyshelf/app 0.6.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (361) hide show
  1. package/README.md +42 -4
  2. package/dist/app-types.d.mts +1 -3
  3. package/dist/asset-manifest.mjs +13 -5
  4. package/dist/asset-manifest.mjs.map +1 -1
  5. package/dist/assets/{htmx.min.js → htmx.mjs} +10 -1
  6. package/dist/assets/htmx.mjs.map +1 -0
  7. package/dist/assets/mark.mjs +10 -0
  8. package/dist/assets/mark.mjs.map +1 -0
  9. package/dist/boot-tasks.mjs +80 -0
  10. package/dist/boot-tasks.mjs.map +1 -0
  11. package/dist/build-state-flags.mjs +18 -0
  12. package/dist/build-state-flags.mjs.map +1 -0
  13. package/dist/build-state-setup.mjs +75 -0
  14. package/dist/build-state-setup.mjs.map +1 -0
  15. package/dist/capture-alerts.mjs +46 -0
  16. package/dist/capture-alerts.mjs.map +1 -0
  17. package/dist/capture-process.mjs +59 -0
  18. package/dist/capture-process.mjs.map +1 -0
  19. package/dist/capture-setup.mjs +95 -117
  20. package/dist/capture-setup.mjs.map +1 -1
  21. package/dist/connections.mjs +34 -0
  22. package/dist/connections.mjs.map +1 -0
  23. package/dist/consumer-setup.mjs +58 -0
  24. package/dist/consumer-setup.mjs.map +1 -0
  25. package/dist/git-status.mjs +46 -0
  26. package/dist/git-status.mjs.map +1 -0
  27. package/dist/hidden-data.mjs +24 -0
  28. package/dist/hidden-data.mjs.map +1 -0
  29. package/dist/index.d.mts +3 -3
  30. package/dist/index.mjs +203 -125
  31. package/dist/index.mjs.map +1 -1
  32. package/dist/insights/budget.mjs +49 -0
  33. package/dist/insights/budget.mjs.map +1 -0
  34. package/dist/insights/deps.mjs +21 -0
  35. package/dist/insights/deps.mjs.map +1 -0
  36. package/dist/insights/errors.mjs +22 -0
  37. package/dist/insights/errors.mjs.map +1 -0
  38. package/dist/insights/health-panel.mjs +40 -0
  39. package/dist/insights/health-panel.mjs.map +1 -0
  40. package/dist/insights/job.mjs +103 -0
  41. package/dist/insights/job.mjs.map +1 -0
  42. package/dist/insights/load-health.mjs +59 -0
  43. package/dist/insights/load-health.mjs.map +1 -0
  44. package/dist/insights/load-triage.mjs +98 -0
  45. package/dist/insights/load-triage.mjs.map +1 -0
  46. package/dist/insights/panel.mjs +28 -0
  47. package/dist/insights/panel.mjs.map +1 -0
  48. package/dist/insights/request.mjs +113 -0
  49. package/dist/insights/request.mjs.map +1 -0
  50. package/dist/insights/stale.mjs +21 -0
  51. package/dist/insights/stale.mjs.map +1 -0
  52. package/dist/insights/usage-report.mjs +60 -0
  53. package/dist/insights/usage-report.mjs.map +1 -0
  54. package/dist/insights/view.mjs +48 -0
  55. package/dist/insights/view.mjs.map +1 -0
  56. package/dist/job-canceller.mjs +14 -0
  57. package/dist/job-canceller.mjs.map +1 -0
  58. package/dist/job-queue.mjs +51 -0
  59. package/dist/job-queue.mjs.map +1 -0
  60. package/dist/jobs-setup.mjs +132 -0
  61. package/dist/jobs-setup.mjs.map +1 -0
  62. package/dist/lifecycle.mjs +91 -19
  63. package/dist/lifecycle.mjs.map +1 -1
  64. package/dist/middleware/auth-gate.mjs +4 -2
  65. package/dist/middleware/auth-gate.mjs.map +1 -1
  66. package/dist/middleware/rate-limit.mjs +41 -33
  67. package/dist/middleware/rate-limit.mjs.map +1 -1
  68. package/dist/middleware/request-log.mjs +3 -2
  69. package/dist/middleware/request-log.mjs.map +1 -1
  70. package/dist/middleware/require-review.mjs +51 -0
  71. package/dist/middleware/require-review.mjs.map +1 -0
  72. package/dist/middleware/require-testing.mjs +41 -0
  73. package/dist/middleware/require-testing.mjs.map +1 -0
  74. package/dist/middleware/setup-gate.mjs.map +1 -1
  75. package/dist/middleware/store-scope.mjs +12 -2
  76. package/dist/middleware/store-scope.mjs.map +1 -1
  77. package/dist/notifications-pause.mjs +33 -0
  78. package/dist/notifications-pause.mjs.map +1 -0
  79. package/dist/notify.d.mts +2 -1
  80. package/dist/notify.mjs +78 -39
  81. package/dist/notify.mjs.map +1 -1
  82. package/dist/pages/a11y-violations.mjs +116 -0
  83. package/dist/pages/a11y-violations.mjs.map +1 -0
  84. package/dist/pages/admin-ai.mjs +78 -0
  85. package/dist/pages/admin-ai.mjs.map +1 -0
  86. package/dist/pages/admin-system-origin.mjs +123 -0
  87. package/dist/pages/admin-system-origin.mjs.map +1 -0
  88. package/dist/pages/admin-system-targets.mjs +53 -0
  89. package/dist/pages/admin-system-targets.mjs.map +1 -0
  90. package/dist/pages/admin-system.mjs +63 -13
  91. package/dist/pages/admin-system.mjs.map +1 -1
  92. package/dist/pages/build-detail.mjs +104 -76
  93. package/dist/pages/build-detail.mjs.map +1 -1
  94. package/dist/pages/build-diff-actions.mjs +87 -33
  95. package/dist/pages/build-diff-actions.mjs.map +1 -1
  96. package/dist/pages/build-diff-comments.mjs +16 -14
  97. package/dist/pages/build-diff-comments.mjs.map +1 -1
  98. package/dist/pages/build-diff-environment.mjs +82 -0
  99. package/dist/pages/build-diff-environment.mjs.map +1 -0
  100. package/dist/pages/build-diff-header.mjs +22 -4
  101. package/dist/pages/build-diff-header.mjs.map +1 -1
  102. package/dist/pages/build-diff-insight.mjs +113 -0
  103. package/dist/pages/build-diff-insight.mjs.map +1 -0
  104. package/dist/pages/build-diff-model.mjs +55 -10
  105. package/dist/pages/build-diff-model.mjs.map +1 -1
  106. package/dist/pages/build-diff-nav.mjs +28 -12
  107. package/dist/pages/build-diff-nav.mjs.map +1 -1
  108. package/dist/pages/build-diff-shortcuts.mjs +5 -1
  109. package/dist/pages/build-diff-shortcuts.mjs.map +1 -1
  110. package/dist/pages/build-diff-usage.mjs +63 -0
  111. package/dist/pages/build-diff-usage.mjs.map +1 -0
  112. package/dist/pages/build-diff-viewer.mjs +62 -5
  113. package/dist/pages/build-diff-viewer.mjs.map +1 -1
  114. package/dist/pages/build-diff.mjs +34 -13
  115. package/dist/pages/build-diff.mjs.map +1 -1
  116. package/dist/pages/build-tests.mjs +74 -0
  117. package/dist/pages/build-tests.mjs.map +1 -0
  118. package/dist/pages/component-detail.mjs +249 -0
  119. package/dist/pages/component-detail.mjs.map +1 -0
  120. package/dist/pages/component-stories.mjs +168 -0
  121. package/dist/pages/component-stories.mjs.map +1 -0
  122. package/dist/pages/compute-jobs.mjs +13 -26
  123. package/dist/pages/compute-jobs.mjs.map +1 -1
  124. package/dist/pages/design-links.mjs +182 -0
  125. package/dist/pages/design-links.mjs.map +1 -0
  126. package/dist/pages/environment-change.mjs +53 -0
  127. package/dist/pages/environment-change.mjs.map +1 -0
  128. package/dist/pages/health-panel.mjs +126 -0
  129. package/dist/pages/health-panel.mjs.map +1 -0
  130. package/dist/pages/library-grid.mjs +105 -0
  131. package/dist/pages/library-grid.mjs.map +1 -0
  132. package/dist/pages/library-model.mjs +47 -0
  133. package/dist/pages/library-model.mjs.map +1 -0
  134. package/dist/pages/library-snapshots.mjs +130 -0
  135. package/dist/pages/library-snapshots.mjs.map +1 -0
  136. package/dist/pages/library.mjs +112 -218
  137. package/dist/pages/library.mjs.map +1 -1
  138. package/dist/pages/notifications-paused.mjs +22 -0
  139. package/dist/pages/notifications-paused.mjs.map +1 -0
  140. package/dist/pages/profile.mjs +53 -48
  141. package/dist/pages/profile.mjs.map +1 -1
  142. package/dist/pages/project-builds-table.mjs +2 -2
  143. package/dist/pages/project-builds-table.mjs.map +1 -1
  144. package/dist/pages/project-builds.mjs +51 -21
  145. package/dist/pages/project-builds.mjs.map +1 -1
  146. package/dist/pages/project-settings.mjs +51 -12
  147. package/dist/pages/project-settings.mjs.map +1 -1
  148. package/dist/pages/projects-card.mjs +14 -1
  149. package/dist/pages/projects-card.mjs.map +1 -1
  150. package/dist/pages/projects.mjs +6 -2
  151. package/dist/pages/projects.mjs.map +1 -1
  152. package/dist/pages/review-changeset.mjs +116 -0
  153. package/dist/pages/review-changeset.mjs.map +1 -0
  154. package/dist/pages/review-comments.mjs +103 -0
  155. package/dist/pages/review-comments.mjs.map +1 -0
  156. package/dist/pages/review-verdict.mjs +102 -0
  157. package/dist/pages/review-verdict.mjs.map +1 -0
  158. package/dist/pages/settings-ai.mjs +69 -0
  159. package/dist/pages/settings-ai.mjs.map +1 -0
  160. package/dist/pages/settings-capture.mjs +124 -0
  161. package/dist/pages/settings-capture.mjs.map +1 -0
  162. package/dist/pages/settings-design-credential.mjs +99 -0
  163. package/dist/pages/settings-design-credential.mjs.map +1 -0
  164. package/dist/pages/settings-design-links.mjs +179 -0
  165. package/dist/pages/settings-design-links.mjs.map +1 -0
  166. package/dist/pages/settings-design.mjs +48 -0
  167. package/dist/pages/settings-design.mjs.map +1 -0
  168. package/dist/pages/settings-general.mjs +46 -21
  169. package/dist/pages/settings-general.mjs.map +1 -1
  170. package/dist/pages/settings-labels.mjs +43 -2
  171. package/dist/pages/settings-labels.mjs.map +1 -1
  172. package/dist/pages/settings-links.mjs +112 -0
  173. package/dist/pages/settings-links.mjs.map +1 -0
  174. package/dist/pages/settings-notifications-git.mjs +128 -0
  175. package/dist/pages/settings-notifications-git.mjs.map +1 -0
  176. package/dist/pages/settings-notifications.mjs +79 -42
  177. package/dist/pages/settings-notifications.mjs.map +1 -1
  178. package/dist/pages/settings-tests.mjs +33 -2
  179. package/dist/pages/settings-tests.mjs.map +1 -1
  180. package/dist/pages/target-choices.mjs +39 -0
  181. package/dist/pages/target-choices.mjs.map +1 -0
  182. package/dist/publish-setup.mjs +89 -0
  183. package/dist/publish-setup.mjs.map +1 -0
  184. package/dist/retention-timer.mjs +2 -1
  185. package/dist/retention-timer.mjs.map +1 -1
  186. package/dist/review-events.mjs +48 -0
  187. package/dist/review-events.mjs.map +1 -0
  188. package/dist/routers/admin-pages.mjs +45 -3
  189. package/dist/routers/admin-pages.mjs.map +1 -1
  190. package/dist/routers/admin-public-origin.mjs +85 -0
  191. package/dist/routers/admin-public-origin.mjs.map +1 -0
  192. package/dist/routers/assets.mjs +9 -2
  193. package/dist/routers/assets.mjs.map +1 -1
  194. package/dist/routers/auth.mjs +1 -1
  195. package/dist/routers/baseline-guard.mjs +2 -2
  196. package/dist/routers/baseline-guard.mjs.map +1 -1
  197. package/dist/routers/baselines.handlers.mjs +64 -0
  198. package/dist/routers/baselines.handlers.mjs.map +1 -0
  199. package/dist/routers/baselines.mjs +125 -0
  200. package/dist/routers/baselines.mjs.map +1 -0
  201. package/dist/routers/build-cancel.mjs +68 -0
  202. package/dist/routers/build-cancel.mjs.map +1 -0
  203. package/dist/routers/build-changeset.mjs +70 -0
  204. package/dist/routers/build-changeset.mjs.map +1 -0
  205. package/dist/routers/build-reviews.handlers.mjs +86 -0
  206. package/dist/routers/build-reviews.handlers.mjs.map +1 -0
  207. package/dist/routers/build-reviews.mjs +74 -0
  208. package/dist/routers/build-reviews.mjs.map +1 -0
  209. package/dist/routers/builds.handlers.mjs +46 -55
  210. package/dist/routers/builds.handlers.mjs.map +1 -1
  211. package/dist/routers/builds.mjs +71 -17
  212. package/dist/routers/builds.mjs.map +1 -1
  213. package/dist/routers/capture-config.mjs +35 -0
  214. package/dist/routers/capture-config.mjs.map +1 -0
  215. package/dist/routers/comments.mjs +41 -15
  216. package/dist/routers/comments.mjs.map +1 -1
  217. package/dist/routers/component-catalog.handlers.mjs +71 -0
  218. package/dist/routers/component-catalog.handlers.mjs.map +1 -0
  219. package/dist/routers/component.handlers.mjs +121 -0
  220. package/dist/routers/component.handlers.mjs.map +1 -0
  221. package/dist/routers/connections.mjs +118 -0
  222. package/dist/routers/connections.mjs.map +1 -0
  223. package/dist/routers/design-connect.handlers.mjs +44 -0
  224. package/dist/routers/design-connect.handlers.mjs.map +1 -0
  225. package/dist/routers/design.handlers.mjs +114 -0
  226. package/dist/routers/design.handlers.mjs.map +1 -0
  227. package/dist/routers/health-report.mjs +2 -3
  228. package/dist/routers/health-report.mjs.map +1 -1
  229. package/dist/routers/health.mjs +4 -3
  230. package/dist/routers/health.mjs.map +1 -1
  231. package/dist/routers/helpers.mjs +19 -3
  232. package/dist/routers/helpers.mjs.map +1 -1
  233. package/dist/routers/insights-ui.mjs +64 -0
  234. package/dist/routers/insights-ui.mjs.map +1 -0
  235. package/dist/routers/insights.handlers.mjs +45 -0
  236. package/dist/routers/insights.handlers.mjs.map +1 -0
  237. package/dist/routers/insights.mjs +189 -0
  238. package/dist/routers/insights.mjs.map +1 -0
  239. package/dist/routers/links.handlers.mjs +80 -0
  240. package/dist/routers/links.handlers.mjs.map +1 -0
  241. package/dist/routers/links.mjs +139 -0
  242. package/dist/routers/links.mjs.map +1 -0
  243. package/dist/routers/media.mjs +18 -4
  244. package/dist/routers/media.mjs.map +1 -1
  245. package/dist/routers/members.mjs +2 -1
  246. package/dist/routers/members.mjs.map +1 -1
  247. package/dist/routers/notification-channels.mjs +17 -23
  248. package/dist/routers/notification-channels.mjs.map +1 -1
  249. package/dist/routers/oauth-state.mjs +45 -0
  250. package/dist/routers/oauth-state.mjs.map +1 -0
  251. package/dist/routers/preview-base.mjs +38 -0
  252. package/dist/routers/preview-base.mjs.map +1 -0
  253. package/dist/routers/preview-token.mjs +51 -0
  254. package/dist/routers/preview-token.mjs.map +1 -0
  255. package/dist/routers/profile.mjs +3 -1
  256. package/dist/routers/profile.mjs.map +1 -1
  257. package/dist/routers/projects.mjs +17 -1
  258. package/dist/routers/projects.mjs.map +1 -1
  259. package/dist/routers/publish-upload.mjs +78 -0
  260. package/dist/routers/publish-upload.mjs.map +1 -0
  261. package/dist/routers/published-access.mjs +52 -0
  262. package/dist/routers/published-access.mjs.map +1 -0
  263. package/dist/routers/published-files.mjs +52 -0
  264. package/dist/routers/published-files.mjs.map +1 -0
  265. package/dist/routers/review-forms.mjs +72 -0
  266. package/dist/routers/review-forms.mjs.map +1 -0
  267. package/dist/routers/review-section.handlers.mjs +85 -0
  268. package/dist/routers/review-section.handlers.mjs.map +1 -0
  269. package/dist/routers/review-switch-off.mjs +29 -0
  270. package/dist/routers/review-switch-off.mjs.map +1 -0
  271. package/dist/routers/schemas.mjs +140 -22
  272. package/dist/routers/schemas.mjs.map +1 -1
  273. package/dist/routers/scoped-models.mjs +22 -0
  274. package/dist/routers/scoped-models.mjs.map +1 -0
  275. package/dist/routers/settings-ai.mjs +35 -0
  276. package/dist/routers/settings-ai.mjs.map +1 -0
  277. package/dist/routers/settings-capture.mjs +77 -0
  278. package/dist/routers/settings-capture.mjs.map +1 -0
  279. package/dist/routers/settings-design.mjs +103 -0
  280. package/dist/routers/settings-design.mjs.map +1 -0
  281. package/dist/routers/settings-general.mjs +55 -10
  282. package/dist/routers/settings-general.mjs.map +1 -1
  283. package/dist/routers/settings-git-channel.mjs +77 -0
  284. package/dist/routers/settings-git-channel.mjs.map +1 -0
  285. package/dist/routers/settings-labels.mjs +27 -1
  286. package/dist/routers/settings-labels.mjs.map +1 -1
  287. package/dist/routers/settings-links.mjs +63 -0
  288. package/dist/routers/settings-links.mjs.map +1 -0
  289. package/dist/routers/settings-members.mjs +4 -7
  290. package/dist/routers/settings-members.mjs.map +1 -1
  291. package/dist/routers/settings-notifications.mjs +15 -10
  292. package/dist/routers/settings-notifications.mjs.map +1 -1
  293. package/dist/routers/settings-tokens.mjs +2 -5
  294. package/dist/routers/settings-tokens.mjs.map +1 -1
  295. package/dist/routers/settings-webhooks.mjs +7 -24
  296. package/dist/routers/settings-webhooks.mjs.map +1 -1
  297. package/dist/routers/settings.handlers.mjs +107 -26
  298. package/dist/routers/settings.handlers.mjs.map +1 -1
  299. package/dist/routers/settings.mjs +16 -4
  300. package/dist/routers/settings.mjs.map +1 -1
  301. package/dist/routers/signed-storybook.mjs +53 -0
  302. package/dist/routers/signed-storybook.mjs.map +1 -0
  303. package/dist/routers/snapshot-groups.mjs +63 -0
  304. package/dist/routers/snapshot-groups.mjs.map +1 -0
  305. package/dist/routers/snapshots.mjs +103 -5
  306. package/dist/routers/snapshots.mjs.map +1 -1
  307. package/dist/routers/storybook.mjs +37 -88
  308. package/dist/routers/storybook.mjs.map +1 -1
  309. package/dist/routers/tokens.mjs +2 -5
  310. package/dist/routers/tokens.mjs.map +1 -1
  311. package/dist/routers/ui.mjs +34 -5
  312. package/dist/routers/ui.mjs.map +1 -1
  313. package/dist/routers/webhooks.mjs +5 -11
  314. package/dist/routers/webhooks.mjs.map +1 -1
  315. package/dist/routers/well-known.mjs +11 -4
  316. package/dist/routers/well-known.mjs.map +1 -1
  317. package/dist/routes/api-routes.mjs +93 -0
  318. package/dist/routes/api-routes.mjs.map +1 -0
  319. package/dist/routes/classification.mjs +38 -0
  320. package/dist/routes/classification.mjs.map +1 -0
  321. package/dist/routes/openapi-classes.mjs +22 -0
  322. package/dist/routes/openapi-classes.mjs.map +1 -0
  323. package/dist/routes/page-routes.mjs +104 -0
  324. package/dist/routes/page-routes.mjs.map +1 -0
  325. package/dist/runtime.mjs +18 -13
  326. package/dist/runtime.mjs.map +1 -1
  327. package/dist/serverless.mjs +5 -0
  328. package/dist/store.mjs +19 -1
  329. package/dist/store.mjs.map +1 -1
  330. package/dist/targets.mjs +46 -0
  331. package/dist/targets.mjs.map +1 -0
  332. package/dist/ui/buttons.mjs +16 -3
  333. package/dist/ui/buttons.mjs.map +1 -1
  334. package/dist/ui/design-reference.mjs +97 -0
  335. package/dist/ui/design-reference.mjs.map +1 -0
  336. package/dist/ui/document.mjs +1 -2
  337. package/dist/ui/document.mjs.map +1 -1
  338. package/dist/ui/forms.mjs +6 -5
  339. package/dist/ui/forms.mjs.map +1 -1
  340. package/dist/ui/icons/paths.mjs +1 -0
  341. package/dist/ui/icons/paths.mjs.map +1 -1
  342. package/dist/ui/preview-embed.mjs +82 -0
  343. package/dist/ui/preview-embed.mjs.map +1 -0
  344. package/dist/ui/shell/client-script.mjs +85 -0
  345. package/dist/ui/shell/client-script.mjs.map +1 -1
  346. package/dist/ui/shell/nav.mjs +12 -8
  347. package/dist/ui/shell/nav.mjs.map +1 -1
  348. package/dist/ui/shell/review-script.mjs +8 -0
  349. package/dist/ui/shell/review-script.mjs.map +1 -1
  350. package/dist/ui/shell/sidebar.mjs +2 -2
  351. package/dist/ui/shell/sidebar.mjs.map +1 -1
  352. package/package.json +11 -9
  353. package/dist/asset-paths.mjs +0 -23
  354. package/dist/asset-paths.mjs.map +0 -1
  355. package/dist/assets/mark.svg +0 -1
  356. package/dist/pages/settings-status.mjs +0 -137
  357. package/dist/pages/settings-status.mjs.map +0 -1
  358. package/dist/routers/settings-status.mjs +0 -77
  359. package/dist/routers/settings-status.mjs.map +0 -1
  360. package/dist/routers/status-configs.mjs +0 -108
  361. package/dist/routers/status-configs.mjs.map +0 -1
@@ -0,0 +1,126 @@
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 { Card, SectionTitle } from "../ui/layout.mjs";
7
+ import { HStack, VStack } from "../ui/stacks.mjs";
8
+ import { csrfField } from "../ui/csrf-field.mjs";
9
+ import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
10
+ //#region src/pages/health-panel.tsx
11
+ /** Badge tone for a health verdict. */
12
+ function healthTone(verdict) {
13
+ if (verdict === "healthy") return "success";
14
+ if (verdict === "watch") return "warning";
15
+ return verdict === "unhealthy" ? "danger" : "neutral";
16
+ }
17
+ function trendsOf(output) {
18
+ const trends = output?.trends;
19
+ return Array.isArray(trends) ? trends : [];
20
+ }
21
+ function scoreText(output) {
22
+ const score = output?.score;
23
+ return typeof score === "number" ? ` · score ${Math.round(score)}/100` : "";
24
+ }
25
+ function HealthBody(props) {
26
+ const { latest } = props;
27
+ if (!latest) return /* @__PURE__ */ jsxs(Meta, { children: [
28
+ "No health digest yet for the last ",
29
+ "30d",
30
+ "."
31
+ ] });
32
+ if (latest.status === "failed") return /* @__PURE__ */ jsxs(Alert, {
33
+ tone: "warning",
34
+ title: "Health digest failed",
35
+ children: [
36
+ "The model call did not finish (",
37
+ latest.errorCode,
38
+ "). You can try again."
39
+ ]
40
+ });
41
+ if (latest.status !== "done") return /* @__PURE__ */ jsx(Meta, { children: "Working on it…" });
42
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
43
+ /* @__PURE__ */ jsx("span", { children: latest.summary }),
44
+ /* @__PURE__ */ jsx("ul", { children: trendsOf(latest.output).map((trend) => /* @__PURE__ */ jsxs("li", { children: [
45
+ /* @__PURE__ */ jsx("strong", { children: trend.label }),
46
+ " (",
47
+ trend.direction,
48
+ "): ",
49
+ trend.note
50
+ ] })) }),
51
+ /* @__PURE__ */ jsxs(Meta, { children: [
52
+ latest.profile,
53
+ " · ",
54
+ latest.model,
55
+ " · ",
56
+ latest.createdAt.slice(0, 16).replace("T", " "),
57
+ scoreText(latest.output)
58
+ ] })
59
+ ] });
60
+ }
61
+ /** Project health digest (advisory) with a generate/refresh button for approvers and admins. */
62
+ function HealthPanel(props) {
63
+ const { project, data } = props;
64
+ const base = `/projects/${project.slug}/insights/health`;
65
+ const latest = data.latest;
66
+ const inFlight = latest?.status === "pending" || latest?.status === "running";
67
+ const poll = inFlight ? {
68
+ "hx-get": `${base}/panel`,
69
+ "hx-trigger": "load delay:3s",
70
+ "hx-swap": "outerHTML"
71
+ } : {};
72
+ return /* @__PURE__ */ jsx("div", {
73
+ id: "health-panel",
74
+ ...poll,
75
+ children: /* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsxs(HStack, { children: [
76
+ /* @__PURE__ */ jsxs(SectionTitle, {
77
+ level: 3,
78
+ children: [
79
+ "Project health (AI, last ",
80
+ "30d",
81
+ ")"
82
+ ]
83
+ }),
84
+ latest?.status === "done" ? /* @__PURE__ */ jsx(Badge, {
85
+ tone: healthTone(latest.verdict),
86
+ children: latest.verdict
87
+ }) : null,
88
+ inFlight ? /* @__PURE__ */ jsx(Badge, {
89
+ tone: "info",
90
+ children: "generating…"
91
+ }) : null
92
+ ] }), /* @__PURE__ */ jsxs(VStack, { children: [/* @__PURE__ */ jsx(HealthBody, { latest }), data.canGenerate && !inFlight ? /* @__PURE__ */ jsxs("form", {
93
+ method: "post",
94
+ action: `${base}/generate`,
95
+ "hx-post": `${base}/generate`,
96
+ "hx-target": "#health-panel",
97
+ "hx-swap": "outerHTML",
98
+ children: [
99
+ csrfField(),
100
+ /* @__PURE__ */ jsx("input", {
101
+ type: "hidden",
102
+ name: "force",
103
+ value: latest ? "true" : "false"
104
+ }),
105
+ /* @__PURE__ */ jsx(Button, {
106
+ variant: "secondary",
107
+ size: "sm",
108
+ type: "submit",
109
+ icon: "sparkles",
110
+ children: latest ? "Refresh health" : "Generate health"
111
+ })
112
+ ]
113
+ }) : null] })] })
114
+ });
115
+ }
116
+ /** Renders the panel when AI health is available for the project, nothing otherwise. */
117
+ function HealthSlot(props) {
118
+ return props.data ? /* @__PURE__ */ jsx(HealthPanel, {
119
+ project: props.project,
120
+ data: props.data
121
+ }) : null;
122
+ }
123
+ //#endregion
124
+ export { HealthPanel, HealthSlot, healthTone };
125
+
126
+ //# sourceMappingURL=health-panel.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"health-panel.mjs","names":[],"sources":["../../src/pages/health-panel.tsx"],"sourcesContent":["import type { Project } from \"@storyshelf/core/schema\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport { HEALTH_UI_WINDOW, type HealthPanelData } from \"../insights/health-panel.ts\";\nimport {\n Alert,\n Badge,\n Button,\n Card,\n HStack,\n Meta,\n SectionTitle,\n VStack,\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/** Badge tone for a health verdict. */\nexport function healthTone(verdict: string | null): \"success\" | \"warning\" | \"danger\" | \"neutral\" {\n if (verdict === \"healthy\") {\n return \"success\";\n }\n if (verdict === \"watch\") {\n return \"warning\";\n }\n return verdict === \"unhealthy\" ? \"danger\" : \"neutral\";\n}\n\ninterface Trend {\n label: string;\n note: string;\n direction: string;\n}\n\nfunction trendsOf(output: unknown): Trend[] {\n const trends = (output as { trends?: unknown } | null)?.trends;\n return Array.isArray(trends) ? (trends as Trend[]) : [];\n}\n\nfunction scoreText(output: unknown): string {\n const score = (output as { score?: unknown } | null)?.score;\n return typeof score === \"number\" ? ` · score ${Math.round(score)}/100` : \"\";\n}\n\nfunction HealthBody(props: {\n latest: HealthPanelData[\"latest\"];\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { latest } = props;\n if (!latest) {\n return <Meta>No health digest yet for the last {HEALTH_UI_WINDOW}.</Meta>;\n }\n if (latest.status === \"failed\") {\n return (\n <Alert tone=\"warning\" title=\"Health digest failed\">\n The model call did not finish ({latest.errorCode}). You can try again.\n </Alert>\n );\n }\n if (latest.status !== \"done\") {\n return <Meta>Working on it…</Meta>;\n }\n return (\n <>\n <span>{latest.summary}</span>\n <ul>\n {trendsOf(latest.output).map((trend) => (\n <li>\n <strong>{trend.label}</strong> ({trend.direction}): {trend.note}\n </li>\n ))}\n </ul>\n <Meta>\n {latest.profile} · {latest.model} · {latest.createdAt.slice(0, 16).replace(\"T\", \" \")}\n {scoreText(latest.output)}\n </Meta>\n </>\n );\n}\n\n/** Props for the health panel. */\nexport interface HealthPanelProps {\n project: Project;\n data: HealthPanelData;\n}\n\n/** Project health digest (advisory) with a generate/refresh button for approvers and admins. */\nexport function HealthPanel(\n props: HealthPanelProps,\n): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, data } = props;\n const base = `/projects/${project.slug}/insights/health`;\n const latest = data.latest;\n const inFlight = latest?.status === \"pending\" || latest?.status === \"running\";\n const poll = inFlight\n ? { \"hx-get\": `${base}/panel`, \"hx-trigger\": \"load delay:3s\", \"hx-swap\": \"outerHTML\" }\n : {};\n return (\n <div id=\"health-panel\" {...poll}>\n <Card>\n <HStack>\n <SectionTitle level={3}>Project health (AI, last {HEALTH_UI_WINDOW})</SectionTitle>\n {latest?.status === \"done\" ? (\n <Badge tone={healthTone(latest.verdict)}>{latest.verdict}</Badge>\n ) : null}\n {inFlight ? <Badge tone=\"info\">generating…</Badge> : null}\n </HStack>\n <VStack>\n <HealthBody latest={latest} />\n {data.canGenerate && !inFlight ? (\n <form\n method=\"post\"\n action={`${base}/generate`}\n hx-post={`${base}/generate`}\n hx-target=\"#health-panel\"\n hx-swap=\"outerHTML\"\n >\n {csrfField()}\n <input type=\"hidden\" name=\"force\" value={latest ? \"true\" : \"false\"} />\n <Button variant=\"secondary\" size=\"sm\" type=\"submit\" icon=\"sparkles\">\n {latest ? \"Refresh health\" : \"Generate health\"}\n </Button>\n </form>\n ) : null}\n </VStack>\n </Card>\n </div>\n );\n}\n\n/** Renders the panel when AI health is available for the project, nothing otherwise. */\nexport function HealthSlot(props: {\n project: Project;\n data: HealthPanelData | null;\n}): HtmlEscapedString | Promise<HtmlEscapedString> | null {\n return props.data ? <HealthPanel project={props.project} data={props.data} /> : null;\n}\n"],"mappings":";;;;;;;;;;;AAkBA,SAAgB,WAAW,SAAsE;CAC/F,IAAI,YAAY,WACd,OAAO;CAET,IAAI,YAAY,SACd,OAAO;CAET,OAAO,YAAY,cAAc,WAAW;AAC9C;AAQA,SAAS,SAAS,QAA0B;CAC1C,MAAM,SAAU,QAAwC;CACxD,OAAO,MAAM,QAAQ,MAAM,IAAK,SAAqB,CAAC;AACxD;AAEA,SAAS,UAAU,QAAyB;CAC1C,MAAM,QAAS,QAAuC;CACtD,OAAO,OAAO,UAAU,WAAW,YAAY,KAAK,MAAM,KAAK,EAAE,QAAQ;AAC3E;AAEA,SAAS,WAAW,OAE+B;CACjD,MAAM,EAAE,WAAW;CACnB,IAAI,CAAC,QACH,OAAO,qBAAC,MAAD,EAAA,UAAA;EAAM;;EAAoD;CAAO,EAAA,CAAA;CAE1E,IAAI,OAAO,WAAW,UACpB,OACE,qBAAC,OAAD;EAAO,MAAK;EAAU,OAAM;EAA5B,UAAA;GAAmD;GACjB,OAAO;GAAU;EAC5C;;CAGX,IAAI,OAAO,WAAW,QACpB,OAAO,oBAAC,MAAD,EAAA,UAAM,iBAAoB,CAAA;CAEnC,OACE,qBAAA,UAAA,EAAA,UAAA;EACE,oBAAC,QAAD,EAAA,UAAO,OAAO,QAAc,CAAA;EAC5B,oBAAC,MAAD,EAAA,UACG,SAAS,OAAO,MAAM,CAAC,CAAC,KAAK,UAC5B,qBAAC,MAAD,EAAA,UAAA;GACE,oBAAC,UAAD,EAAA,UAAS,MAAM,MAAc,CAAA;GAAC;GAAG,MAAM;GAAU;GAAI,MAAM;EACzD,EAAA,CAAA,CACL,EACC,CAAA;EACJ,qBAAC,MAAD,EAAA,UAAA;GACG,OAAO;GAAQ;GAAI,OAAO;GAAM;GAAI,OAAO,UAAU,MAAM,GAAG,EAAE,CAAC,CAAC,QAAQ,KAAK,GAAG;GAClF,UAAU,OAAO,MAAM;EACpB,EAAA,CAAA;CACN,EAAA,CAAA;AAEN;;AASA,SAAgB,YACd,OACgD;CAChD,MAAM,EAAE,SAAS,SAAS;CAC1B,MAAM,OAAO,aAAa,QAAQ,KAAK;CACvC,MAAM,SAAS,KAAK;CACpB,MAAM,WAAW,QAAQ,WAAW,aAAa,QAAQ,WAAW;CACpE,MAAM,OAAO,WACT;EAAE,UAAU,GAAG,KAAK;EAAS,cAAc;EAAiB,WAAW;CAAY,IACnF,CAAC;CACL,OACE,oBAAC,OAAD;EAAK,IAAG;EAAe,GAAI;EACzB,UAAA,qBAAC,MAAD,EAAA,UAAA,CACE,qBAAC,QAAD,EAAA,UAAA;GACE,qBAAC,cAAD;IAAc,OAAO;IAArB,UAAA;KAAwB;;KAA2C;IAAe;;GACjF,QAAQ,WAAW,SAClB,oBAAC,OAAD;IAAO,MAAM,WAAW,OAAO,OAAO;IAAI,UAAA,OAAO;GAAe,CAAA,IAC9D;GACH,WAAW,oBAAC,OAAD;IAAO,MAAK;IAAO,UAAA;GAAkB,CAAA,IAAI;EAC/C,EAAA,CAAA,GACR,qBAAC,QAAD,EAAA,UAAA,CACE,oBAAC,YAAD,EAAoB,OAAS,CAAA,GAC5B,KAAK,eAAe,CAAC,WACpB,qBAAC,QAAD;GACE,QAAO;GACP,QAAQ,GAAG,KAAK;GAChB,WAAS,GAAG,KAAK;GACjB,aAAU;GACV,WAAQ;GALV,UAAA;IAOG,UAAU;IACX,oBAAC,SAAD;KAAO,MAAK;KAAS,MAAK;KAAQ,OAAO,SAAS,SAAS;IAAU,CAAA;IACrE,oBAAC,QAAD;KAAQ,SAAQ;KAAY,MAAK;KAAK,MAAK;KAAS,MAAK;KACtD,UAAA,SAAS,mBAAmB;IACvB,CAAA;GACJ;EACJ,CAAA,IAAA,IACE,EAAA,CAAA,CACJ,EAAA,CAAA;CACH,CAAA;AAET;;AAGA,SAAgB,WAAW,OAG+B;CACxD,OAAO,MAAM,OAAO,oBAAC,aAAD;EAAa,SAAS,MAAM;EAAS,MAAM,MAAM;CAAO,CAAA,IAAI;AAClF"}
@@ -0,0 +1,105 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { getStore } from "../store.mjs";
5
+ import { Button } from "../ui/buttons.mjs";
6
+ import { Badge, Meta } from "../ui/feedback.mjs";
7
+ import { Card, CardSection, SectionTitle } from "../ui/layout.mjs";
8
+ import { HStack, VStack } from "../ui/stacks.mjs";
9
+ import { SnapshotCards } from "./library-snapshots.mjs";
10
+ import { jsx, jsxs } from "hono/jsx/jsx-runtime";
11
+ import { createUrlBuilder } from "@storyshelf/core/urls";
12
+ //#region src/pages/library-grid.tsx
13
+ /** The component cards of a build; `layer` is present only with `testing` and captured snapshots. */
14
+ function LibraryGrid(props) {
15
+ return /* @__PURE__ */ jsx(VStack, { children: props.groups.map((group) => /* @__PURE__ */ jsx(LibraryCard, {
16
+ group,
17
+ project: props.project,
18
+ build: props.build,
19
+ layer: props.layer
20
+ }, group.title)) });
21
+ }
22
+ function LibraryCard(props) {
23
+ const { group, project, build, layer } = props;
24
+ const href = componentHref(project, group);
25
+ const captured = layer ? group.stories.flatMap((story) => layer.byStory.get(story.id) ?? []) : [];
26
+ const plain = group.stories.filter((story) => !layer?.byStory.has(story.id));
27
+ return /* @__PURE__ */ jsxs(Card, {
28
+ padded: false,
29
+ "data-filter-item": true,
30
+ "data-filter-text": `${group.title} ${group.stories.map((story) => story.name).join(" ")}`,
31
+ children: [/* @__PURE__ */ jsxs(CardSection, {
32
+ divider: true,
33
+ children: [/* @__PURE__ */ jsx(SectionTitle, { children: href ? /* @__PURE__ */ jsx("a", {
34
+ href,
35
+ children: group.title
36
+ }) : group.title }), /* @__PURE__ */ jsxs(HStack, { children: [
37
+ group.tags.map((tag) => /* @__PURE__ */ jsx(Badge, {
38
+ tone: "neutral",
39
+ children: tag
40
+ }, tag)),
41
+ /* @__PURE__ */ jsxs(Meta, {
42
+ as: "span",
43
+ children: [
44
+ group.stories.length,
45
+ " ",
46
+ group.stories.length === 1 ? "story" : "stories"
47
+ ]
48
+ }),
49
+ /* @__PURE__ */ jsx(DocsLink, {
50
+ group,
51
+ project,
52
+ build
53
+ })
54
+ ] })]
55
+ }), /* @__PURE__ */ jsx(CardSection, { children: /* @__PURE__ */ jsxs(VStack, { children: [layer && captured.length > 0 ? /* @__PURE__ */ jsx(SnapshotCards, {
56
+ snapshots: captured,
57
+ project,
58
+ buildId: build.id,
59
+ layer,
60
+ componentHref: href
61
+ }) : null, plain.length > 0 ? /* @__PURE__ */ jsx(StoryLinks, {
62
+ stories: plain,
63
+ project,
64
+ build
65
+ }) : null] }) })]
66
+ });
67
+ }
68
+ /** Component page of a group (its story file), when the index names one. */
69
+ function componentHref(project, group) {
70
+ return group.importPath ? createUrlBuilder("/", getStore().config.publishedBaseDomain).component(project.slug, group.importPath) : void 0;
71
+ }
72
+ function DocsLink(props) {
73
+ const docs = props.group.docs[0];
74
+ if (!docs) return null;
75
+ return /* @__PURE__ */ jsx(Button, {
76
+ variant: "ghost",
77
+ size: "sm",
78
+ icon: "book",
79
+ href: `/projects/${props.project.slug}/storybook/build/${props.build.id}/?storyId=${encodeURIComponent(docs.id)}&viewMode=docs`,
80
+ target: "_blank",
81
+ rel: "noopener",
82
+ children: "Docs"
83
+ });
84
+ }
85
+ /** Links into the published Storybook, one per story, for stories without a snapshot. */
86
+ function StoryLinks(props) {
87
+ const base = createUrlBuilder("/", getStore().config.publishedBaseDomain).storybookBuild(props.project.slug, props.build.id);
88
+ return /* @__PURE__ */ jsx(HStack, { children: props.stories.map((story) => /* @__PURE__ */ jsx("span", {
89
+ "data-filter-item": true,
90
+ "data-filter-text": `${story.title} ${story.name}`,
91
+ children: /* @__PURE__ */ jsx(Button, {
92
+ variant: "ghost",
93
+ size: "sm",
94
+ icon: "external-link",
95
+ href: `${base}/?storyId=${encodeURIComponent(story.id)}`,
96
+ target: "_blank",
97
+ rel: "noopener",
98
+ children: story.name
99
+ })
100
+ }, story.id)) });
101
+ }
102
+ //#endregion
103
+ export { LibraryGrid };
104
+
105
+ //# sourceMappingURL=library-grid.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"library-grid.mjs","names":[],"sources":["../../src/pages/library-grid.tsx"],"sourcesContent":["/** The library's component list: one card per title, from the catalog, with snapshots layered on. */\nimport type { Build, Project } from \"@storyshelf/core/schema\";\nimport { createUrlBuilder } from \"@storyshelf/core/urls\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport { getStore } from \"../store.ts\";\nimport {\n Badge,\n Button,\n Card,\n CardSection,\n HStack,\n Meta,\n SectionTitle,\n VStack,\n} from \"../ui/components.tsx\";\nimport type { LibraryGroup } from \"./library-model.ts\";\nimport { SnapshotCards, type SnapshotLayer } from \"./library-snapshots.tsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\ntype Rendered = HtmlEscapedString | Promise<HtmlEscapedString>;\n\n/** The component cards of a build; `layer` is present only with `testing` and captured snapshots. */\nexport function LibraryGrid(props: {\n groups: LibraryGroup[];\n project: Project;\n build: Build;\n layer: SnapshotLayer | null;\n}): Rendered {\n return (\n <VStack>\n {props.groups.map((group): Rendered => (\n <LibraryCard\n key={group.title}\n group={group}\n project={props.project}\n build={props.build}\n layer={props.layer}\n />\n ))}\n </VStack>\n );\n}\n\nfunction LibraryCard(props: {\n group: LibraryGroup;\n project: Project;\n build: Build;\n layer: SnapshotLayer | null;\n}): Rendered {\n const { group, project, build, layer } = props;\n const href = componentHref(project, group);\n const captured = layer ? group.stories.flatMap((story) => layer.byStory.get(story.id) ?? []) : [];\n const plain = group.stories.filter((story) => !layer?.byStory.has(story.id));\n return (\n <Card\n padded={false}\n data-filter-item\n data-filter-text={`${group.title} ${group.stories.map((story) => story.name).join(\" \")}`}\n >\n <CardSection divider>\n <SectionTitle>{href ? <a href={href}>{group.title}</a> : group.title}</SectionTitle>\n <HStack>\n {group.tags.map((tag) => (\n <Badge key={tag} tone=\"neutral\">\n {tag}\n </Badge>\n ))}\n <Meta as=\"span\">\n {group.stories.length} {group.stories.length === 1 ? \"story\" : \"stories\"}\n </Meta>\n <DocsLink group={group} project={project} build={build} />\n </HStack>\n </CardSection>\n <CardSection>\n <VStack>\n {layer && captured.length > 0 ? (\n <SnapshotCards\n snapshots={captured}\n project={project}\n buildId={build.id}\n layer={layer}\n componentHref={href}\n />\n ) : null}\n {plain.length > 0 ? <StoryLinks stories={plain} project={project} build={build} /> : null}\n </VStack>\n </CardSection>\n </Card>\n );\n}\n\n/** Component page of a group (its story file), when the index names one. */\nfunction componentHref(project: Project, group: LibraryGroup): string | undefined {\n return group.importPath\n ? createUrlBuilder(\"/\", getStore().config.publishedBaseDomain).component(\n project.slug,\n group.importPath,\n )\n : undefined;\n}\n\nfunction DocsLink(props: { group: LibraryGroup; project: Project; build: Build }): Rendered | null {\n const docs = props.group.docs[0];\n if (!docs) {\n return null;\n }\n return (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n icon=\"book\"\n href={`/projects/${props.project.slug}/storybook/build/${props.build.id}/?storyId=${encodeURIComponent(docs.id)}&viewMode=docs`}\n target=\"_blank\"\n rel=\"noopener\"\n >\n Docs\n </Button>\n );\n}\n\n/** Links into the published Storybook, one per story, for stories without a snapshot. */\nfunction StoryLinks(props: {\n stories: LibraryGroup[\"stories\"];\n project: Project;\n build: Build;\n}): Rendered {\n const base = createUrlBuilder(\"/\", getStore().config.publishedBaseDomain).storybookBuild(\n props.project.slug,\n props.build.id,\n );\n return (\n <HStack>\n {props.stories.map((story): Rendered => (\n <span key={story.id} data-filter-item data-filter-text={`${story.title} ${story.name}`}>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n icon=\"external-link\"\n href={`${base}/?storyId=${encodeURIComponent(story.id)}`}\n target=\"_blank\"\n rel=\"noopener\"\n >\n {story.name}\n </Button>\n </span>\n ))}\n </HStack>\n );\n}\n"],"mappings":";;;;;;;;;;;;;AAuBA,SAAgB,YAAY,OAKf;CACX,OACE,oBAAC,QAAD,EAAA,UACG,MAAM,OAAO,KAAK,UACjB,oBAAC,aAAD;EAES;EACP,SAAS,MAAM;EACf,OAAO,MAAM;EACb,OAAO,MAAM;CACd,GALM,MAAM,KAKZ,CACF,EACK,CAAA;AAEZ;AAEA,SAAS,YAAY,OAKR;CACX,MAAM,EAAE,OAAO,SAAS,OAAO,UAAU;CACzC,MAAM,OAAO,cAAc,SAAS,KAAK;CACzC,MAAM,WAAW,QAAQ,MAAM,QAAQ,SAAS,UAAU,MAAM,QAAQ,IAAI,MAAM,EAAE,KAAK,CAAC,CAAC,IAAI,CAAC;CAChG,MAAM,QAAQ,MAAM,QAAQ,QAAQ,UAAU,CAAC,OAAO,QAAQ,IAAI,MAAM,EAAE,CAAC;CAC3E,OACE,qBAAC,MAAD;EACE,QAAQ;EACR,oBAAA;EACA,oBAAkB,GAAG,MAAM,MAAM,GAAG,MAAM,QAAQ,KAAK,UAAU,MAAM,IAAI,CAAC,CAAC,KAAK,GAAG;EAHvF,UAAA,CAKE,qBAAC,aAAD;GAAa,SAAA;GAAb,UAAA,CACE,oBAAC,cAAD,EAAA,UAAe,OAAO,oBAAC,KAAD;IAAS;IAAO,UAAA,MAAM;GAAS,CAAA,IAAI,MAAM,MAAoB,CAAA,GACnF,qBAAC,QAAD,EAAA,UAAA;IACG,MAAM,KAAK,KAAK,QACf,oBAAC,OAAD;KAAiB,MAAK;KACnB,UAAA;IACI,GAFK,GAEL,CACR;IACD,qBAAC,MAAD;KAAM,IAAG;KAAT,UAAA;MACG,MAAM,QAAQ;MAAO;MAAE,MAAM,QAAQ,WAAW,IAAI,UAAU;KAC3D;;IACN,oBAAC,UAAD;KAAiB;KAAgB;KAAgB;IAAQ,CAAA;GACnD,EAAA,CAAA,CACG;EACb,CAAA,GAAA,oBAAC,aAAD,EAAA,UACE,qBAAC,QAAD,EAAA,UAAA,CACG,SAAS,SAAS,SAAS,IAC1B,oBAAC,eAAD;GACE,WAAW;GACF;GACT,SAAS,MAAM;GACR;GACP,eAAe;EAChB,CAAA,IACC,MACH,MAAM,SAAS,IAAI,oBAAC,YAAD;GAAY,SAAS;GAAgB;GAAgB;EAAQ,CAAA,IAAI,IAC/E,EAAA,CAAA,EACG,CAAA,CACT;;AAEV;;AAGA,SAAS,cAAc,SAAkB,OAAyC;CAChF,OAAO,MAAM,aACT,iBAAiB,KAAK,SAAS,CAAC,CAAC,OAAO,mBAAmB,CAAC,CAAC,UAC3D,QAAQ,MACR,MAAM,UACR,IACA,KAAA;AACN;AAEA,SAAS,SAAS,OAAiF;CACjG,MAAM,OAAO,MAAM,MAAM,KAAK;CAC9B,IAAI,CAAC,MACH,OAAO;CAET,OACE,oBAAC,QAAD;EACE,SAAQ;EACR,MAAK;EACL,MAAK;EACL,MAAM,aAAa,MAAM,QAAQ,KAAK,mBAAmB,MAAM,MAAM,GAAG,YAAY,mBAAmB,KAAK,EAAE,EAAE;EAChH,QAAO;EACP,KAAI;EACL,UAAA;CAEO,CAAA;AAEZ;;AAGA,SAAS,WAAW,OAIP;CACX,MAAM,OAAO,iBAAiB,KAAK,SAAS,CAAC,CAAC,OAAO,mBAAmB,CAAC,CAAC,eACxE,MAAM,QAAQ,MACd,MAAM,MAAM,EACd;CACA,OACE,oBAAC,QAAD,EAAA,UACG,MAAM,QAAQ,KAAK,UAClB,oBAAC,QAAD;EAAqB,oBAAA;EAAiB,oBAAkB,GAAG,MAAM,MAAM,GAAG,MAAM;EAC9E,UAAA,oBAAC,QAAD;GACE,SAAQ;GACR,MAAK;GACL,MAAK;GACL,MAAM,GAAG,KAAK,YAAY,mBAAmB,MAAM,EAAE;GACrD,QAAO;GACP,KAAI;GAEH,UAAA,MAAM;EACD,CAAA;CACJ,GAXK,MAAM,EAWX,CACP,EACK,CAAA;AAEZ"}
@@ -0,0 +1,47 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { normalizeImportPath } from "@storyshelf/core/models";
5
+ //#region src/pages/library-model.ts
6
+ /** Tags Storybook adds on its own; they say nothing about the component. */
7
+ const STORYBOOK_TAGS = /* @__PURE__ */ new Set([
8
+ "dev",
9
+ "test",
10
+ "autodocs",
11
+ "play-fn",
12
+ "manifest",
13
+ "attached-mdx",
14
+ "unattached-mdx"
15
+ ]);
16
+ /** Group catalog entries by title, ordered by title; stories keep index order. */
17
+ function groupLibrary(entries) {
18
+ return [...Map.groupBy(entries, (entry) => entry.title).entries()].map(([title, group]) => {
19
+ return {
20
+ title,
21
+ importPath: group.find((entry) => entry.importPath !== "")?.importPath ?? "",
22
+ stories: group.filter((entry) => entry.type === "story"),
23
+ docs: group.filter((entry) => entry.type === "docs"),
24
+ tags: [...new Set(group.flatMap((entry) => entry.tags))].filter((tag) => !STORYBOOK_TAGS.has(tag))
25
+ };
26
+ }).toSorted((a, b) => a.title.localeCompare(b.title));
27
+ }
28
+ /**
29
+ * Entries rebuilt from a build's snapshots, for a captured build whose Storybook index is gone
30
+ * (statics purged): the pages still list what was captured.
31
+ */
32
+ function entriesFromSnapshots(snapshots) {
33
+ const seen = /* @__PURE__ */ new Map();
34
+ for (const snap of snapshots) if (!seen.has(snap.storyId)) seen.set(snap.storyId, {
35
+ id: snap.storyId,
36
+ title: snap.storyTitle || "Untitled",
37
+ name: snap.storyName,
38
+ importPath: snap.storyImportPath ? normalizeImportPath(snap.storyImportPath) : "",
39
+ type: "story",
40
+ tags: []
41
+ });
42
+ return [...seen.values()];
43
+ }
44
+ //#endregion
45
+ export { entriesFromSnapshots, groupLibrary };
46
+
47
+ //# sourceMappingURL=library-model.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"library-model.mjs","names":[],"sources":["../../src/pages/library-model.ts"],"sourcesContent":["import type { CatalogEntry } from \"@storyshelf/core/capture\";\nimport { normalizeImportPath } from \"@storyshelf/core/models\";\nimport type { Snapshot } from \"@storyshelf/core/schema\";\n\n/** Tags Storybook adds on its own; they say nothing about the component. */\nconst STORYBOOK_TAGS: ReadonlySet<string> = new Set([\n \"dev\",\n \"test\",\n \"autodocs\",\n \"play-fn\",\n \"manifest\",\n \"attached-mdx\",\n \"unattached-mdx\",\n]);\n\n/** The library's unit: every entry that shares a title, grouped like Storybook's sidebar. */\nexport interface LibraryGroup {\n title: string;\n /** Story file of the first entry that has one; empty when the index names none. */\n importPath: string;\n stories: CatalogEntry[];\n /** Docs entries; the first is the component's docs page. */\n docs: CatalogEntry[];\n /** Tags worth showing, in first-seen order. */\n tags: string[];\n}\n\n/** Group catalog entries by title, ordered by title; stories keep index order. */\nexport function groupLibrary(entries: readonly CatalogEntry[]): LibraryGroup[] {\n const groups = Map.groupBy(entries, (entry) => entry.title);\n return [...groups.entries()]\n .map(([title, group]): LibraryGroup => {\n return {\n title,\n importPath: group.find((entry) => entry.importPath !== \"\")?.importPath ?? \"\",\n stories: group.filter((entry) => entry.type === \"story\"),\n docs: group.filter((entry) => entry.type === \"docs\"),\n tags: [...new Set(group.flatMap((entry) => entry.tags))].filter(\n (tag) => !STORYBOOK_TAGS.has(tag),\n ),\n };\n })\n .toSorted((a, b) => a.title.localeCompare(b.title));\n}\n\n/**\n * Entries rebuilt from a build's snapshots, for a captured build whose Storybook index is gone\n * (statics purged): the pages still list what was captured.\n */\nexport function entriesFromSnapshots(snapshots: readonly Snapshot[]): CatalogEntry[] {\n const seen = new Map<string, CatalogEntry>();\n for (const snap of snapshots) {\n if (!seen.has(snap.storyId)) {\n seen.set(snap.storyId, {\n id: snap.storyId,\n title: snap.storyTitle || \"Untitled\",\n name: snap.storyName,\n importPath: snap.storyImportPath ? normalizeImportPath(snap.storyImportPath) : \"\",\n type: \"story\",\n tags: [],\n });\n }\n }\n return [...seen.values()];\n}\n"],"mappings":";;;;;;AAKA,MAAM,iCAAsC,IAAI,IAAI;CAClD;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;AAeD,SAAgB,aAAa,SAAkD;CAE7E,OAAO,CAAC,GADO,IAAI,QAAQ,UAAU,UAAU,MAAM,KACrC,CAAC,CAAC,QAAQ,CAAC,CAAC,CACzB,KAAK,CAAC,OAAO,WAAyB;EACrC,OAAO;GACL;GACA,YAAY,MAAM,MAAM,UAAU,MAAM,eAAe,EAAE,CAAC,EAAE,cAAc;GAC1E,SAAS,MAAM,QAAQ,UAAU,MAAM,SAAS,OAAO;GACvD,MAAM,MAAM,QAAQ,UAAU,MAAM,SAAS,MAAM;GACnD,MAAM,CAAC,GAAG,IAAI,IAAI,MAAM,SAAS,UAAU,MAAM,IAAI,CAAC,CAAC,CAAC,CAAC,QACtD,QAAQ,CAAC,eAAe,IAAI,GAAG,CAClC;EACF;CACF,CAAC,CAAC,CACD,UAAU,GAAG,MAAM,EAAE,MAAM,cAAc,EAAE,KAAK,CAAC;AACtD;;;;;AAMA,SAAgB,qBAAqB,WAAgD;CACnF,MAAM,uBAAO,IAAI,IAA0B;CAC3C,KAAK,MAAM,QAAQ,WACjB,IAAI,CAAC,KAAK,IAAI,KAAK,OAAO,GACxB,KAAK,IAAI,KAAK,SAAS;EACrB,IAAI,KAAK;EACT,OAAO,KAAK,cAAc;EAC1B,MAAM,KAAK;EACX,YAAY,KAAK,kBAAkB,oBAAoB,KAAK,eAAe,IAAI;EAC/E,MAAM;EACN,MAAM,CAAC;CACT,CAAC;CAGL,OAAO,CAAC,GAAG,KAAK,OAAO,CAAC;AAC1B"}
@@ -0,0 +1,130 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { getStore } from "../store.mjs";
5
+ import { css } from "../ui/css.mjs";
6
+ import { Button } from "../ui/buttons.mjs";
7
+ import { Badge, statusTone } from "../ui/feedback.mjs";
8
+ import { SectionTitle } from "../ui/layout.mjs";
9
+ import { HStack } from "../ui/stacks.mjs";
10
+ import { designUrlsFor, loadDesignPreviews, loadDesignResolutions, loadProjectDesignLinks, providerNameFor } from "../routers/design.handlers.mjs";
11
+ import { diffPaneImg, snapshotCard, snapshotCardBody, snapshotCardHead, snapshotCardMeta, snapshotGrid } from "../ui/styles/review.mjs";
12
+ import { A11yViolationsDetails } from "./a11y-violations.mjs";
13
+ import { groupSnapshots } from "./build-diff-model.mjs";
14
+ import { DesignButtons } from "./design-links.mjs";
15
+ import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
16
+ import { createUrlBuilder } from "@storyshelf/core/urls";
17
+ //#region src/pages/library-snapshots.tsx
18
+ /** Snapshot image with library height cap, extending the shared diff pane image. */
19
+ const snapshotImg = css`
20
+ /* snapshot-img */
21
+ ${diffPaneImg}
22
+ max-height: 240px;
23
+ object-fit: contain;
24
+ `;
25
+ const NO_DESIGN = {
26
+ bySnapshot: /* @__PURE__ */ new Map(),
27
+ previews: /* @__PURE__ */ new Map(),
28
+ projectLinks: []
29
+ };
30
+ /** The layer for a build's snapshots; design data that cannot be loaded leaves the library plain. */
31
+ async function loadSnapshotLayer(project, snapshots) {
32
+ const primary = groupSnapshots(snapshots).map((group) => group.primary);
33
+ const design = await loadLibraryDesign(project, primary).catch(() => NO_DESIGN);
34
+ return {
35
+ byStory: Map.groupBy(primary, (snap) => snap.storyId),
36
+ design,
37
+ multipleViewports: new Set(primary.map((snap) => snap.viewportName)).size > 1
38
+ };
39
+ }
40
+ async function loadLibraryDesign(project, snapshots) {
41
+ const [resolutions, projectLinks] = await Promise.all([loadDesignResolutions(project, snapshots), loadProjectDesignLinks(project)]);
42
+ const bySnapshot = new Map(snapshots.map((snap) => [snap.id, designUrlsFor(resolutions, snap)]));
43
+ return {
44
+ bySnapshot,
45
+ previews: await loadDesignPreviews(project, [...bySnapshot.values()].flat()),
46
+ projectLinks
47
+ };
48
+ }
49
+ /** Snapshot cards of a component's captured stories, one grid per viewport. */
50
+ function SnapshotCards(props) {
51
+ const byViewport = Map.groupBy(props.snapshots, (snap) => snap.viewportName);
52
+ return /* @__PURE__ */ jsx(Fragment, { children: [...byViewport.keys()].toSorted().map((name) => /* @__PURE__ */ jsxs("div", { children: [props.layer.multipleViewports ? /* @__PURE__ */ jsx(SectionTitle, {
53
+ level: 3,
54
+ children: name
55
+ }) : null, /* @__PURE__ */ jsx("div", {
56
+ class: snapshotGrid,
57
+ children: (byViewport.get(name) ?? []).toSorted((a, b) => a.storyName.localeCompare(b.storyName)).map((snap) => /* @__PURE__ */ jsx(SnapshotCardItem, {
58
+ snap,
59
+ ...props
60
+ }, snap.id))
61
+ })] }, name)) });
62
+ }
63
+ function SnapshotCardItem(props) {
64
+ const { snap, project, buildId } = props;
65
+ return /* @__PURE__ */ jsxs("div", {
66
+ class: snapshotCard,
67
+ "data-filter-item": true,
68
+ "data-filter-text": `${snap.storyTitle} ${snap.storyName}`,
69
+ children: [/* @__PURE__ */ jsxs("div", {
70
+ class: snapshotCardHead,
71
+ children: [/* @__PURE__ */ jsx("strong", { children: snap.storyName }), /* @__PURE__ */ jsxs("span", {
72
+ class: snapshotCardMeta,
73
+ children: [
74
+ /* @__PURE__ */ jsx(Badge, {
75
+ tone: statusTone(snap.status),
76
+ children: snap.status
77
+ }),
78
+ " ",
79
+ snap.viewportWidth,
80
+ "×",
81
+ snap.viewportHeight
82
+ ]
83
+ })]
84
+ }), /* @__PURE__ */ jsxs("div", {
85
+ class: snapshotCardBody,
86
+ children: [
87
+ /* @__PURE__ */ jsx("img", {
88
+ class: snapshotImg,
89
+ src: `/api/v1/projects/${project.slug}/builds/${buildId}/snapshots/${snap.id}/image`,
90
+ alt: `${snap.storyTitle} / ${snap.storyName}`,
91
+ loading: "lazy"
92
+ }),
93
+ /* @__PURE__ */ jsx(A11yViolationsDetails, { snapshot: snap }),
94
+ /* @__PURE__ */ jsx(SnapshotActions, { ...props })
95
+ ]
96
+ })]
97
+ });
98
+ }
99
+ function SnapshotActions(props) {
100
+ const { snap, project, buildId, layer } = props;
101
+ const urls = createUrlBuilder("/", getStore().config.publishedBaseDomain);
102
+ return /* @__PURE__ */ jsxs(HStack, { children: [
103
+ /* @__PURE__ */ jsx(Button, {
104
+ variant: "secondary",
105
+ size: "sm",
106
+ icon: "columns",
107
+ href: `${urls.buildDiff(project.slug, buildId)}?snapshot=${snap.id}`,
108
+ children: "Review"
109
+ }),
110
+ /* @__PURE__ */ jsx(Button, {
111
+ variant: "ghost",
112
+ size: "sm",
113
+ icon: "external-link",
114
+ href: `${urls.storybookBuild(project.slug, buildId)}/?storyId=${encodeURIComponent(snap.storyId)}`,
115
+ target: "_blank",
116
+ rel: "noopener",
117
+ children: "Preview"
118
+ }),
119
+ /* @__PURE__ */ jsx(DesignButtons, {
120
+ urls: layer.design.bySnapshot.get(snap.id) ?? [],
121
+ providerName: providerNameFor,
122
+ previews: layer.design.previews,
123
+ componentHref: props.componentHref
124
+ })
125
+ ] });
126
+ }
127
+ //#endregion
128
+ export { SnapshotCards, loadSnapshotLayer };
129
+
130
+ //# sourceMappingURL=library-snapshots.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"library-snapshots.mjs","names":[],"sources":["../../src/pages/library-snapshots.tsx"],"sourcesContent":["/** The snapshot layer of the library: what `testing` adds on top of the catalog (ADR 0006, decision 9). */\nimport type { Project, Snapshot } from \"@storyshelf/core/schema\";\nimport { createUrlBuilder } from \"@storyshelf/core/urls\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport {\n loadDesignResolutions,\n designUrlsFor,\n loadDesignPreviews,\n loadProjectDesignLinks,\n providerNameFor,\n} from \"../routers/design.handlers.ts\";\nimport { getStore } from \"../store.ts\";\nimport { Badge, Button, HStack, SectionTitle, statusTone } from \"../ui/components.tsx\";\nimport { css } from \"../ui/css.ts\";\nimport {\n diffPaneImg,\n snapshotCard,\n snapshotCardBody,\n snapshotCardHead,\n snapshotCardMeta,\n snapshotGrid,\n} from \"../ui/styles/review.ts\";\nimport { A11yViolationsDetails } from \"./a11y-violations.tsx\";\nimport { groupSnapshots } from \"./build-diff-model.ts\";\nimport { DesignButtons, type DesignPreviewInfo, type ProjectDesignLink } from \"./design-links.tsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\ntype Rendered = HtmlEscapedString | Promise<HtmlEscapedString>;\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/** Design data of the library: per-snapshot component links plus project-level file links. */\nexport interface LibraryDesign {\n bySnapshot: Map<string, string[]>;\n previews: Map<string, DesignPreviewInfo>;\n projectLinks: ProjectDesignLink[];\n}\n\n/** Snapshots and design links of the build, by story; layered onto the catalog's stories. */\nexport interface SnapshotLayer {\n /** One snapshot per story and mode (the primary engine's; other browsers are review detail). */\n byStory: Map<string, Snapshot[]>;\n design: LibraryDesign;\n /** Whether the build has more than one viewport (so the cards name theirs). */\n multipleViewports: boolean;\n}\n\nconst NO_DESIGN: LibraryDesign = { bySnapshot: new Map(), previews: new Map(), projectLinks: [] };\n\n/** The layer for a build's snapshots; design data that cannot be loaded leaves the library plain. */\nexport async function loadSnapshotLayer(\n project: Project,\n snapshots: Snapshot[],\n): Promise<SnapshotLayer> {\n const primary = groupSnapshots(snapshots).map((group) => group.primary);\n const design = await loadLibraryDesign(project, primary).catch(() => NO_DESIGN);\n return {\n byStory: Map.groupBy(primary, (snap) => snap.storyId),\n design,\n multipleViewports: new Set(primary.map((snap) => snap.viewportName)).size > 1,\n };\n}\n\nasync function loadLibraryDesign(project: Project, snapshots: Snapshot[]): Promise<LibraryDesign> {\n const [resolutions, projectLinks] = await Promise.all([\n loadDesignResolutions(project, snapshots),\n loadProjectDesignLinks(project),\n ]);\n const bySnapshot = new Map(\n snapshots.map((snap) => [snap.id, designUrlsFor(resolutions, snap)] as const),\n );\n const previews = await loadDesignPreviews(project, [...bySnapshot.values()].flat());\n return { bySnapshot, previews, projectLinks };\n}\n\n/** Snapshot cards of a component's captured stories, one grid per viewport. */\nexport function SnapshotCards(props: {\n snapshots: Snapshot[];\n project: Project;\n buildId: string;\n layer: SnapshotLayer;\n componentHref: string | undefined;\n}): Rendered {\n const byViewport = Map.groupBy(props.snapshots, (snap) => snap.viewportName);\n return (\n <>\n {[...byViewport.keys()].toSorted().map((name): Rendered => (\n <div key={name}>\n {props.layer.multipleViewports ? <SectionTitle level={3}>{name}</SectionTitle> : null}\n <div class={snapshotGrid}>\n {(byViewport.get(name) ?? [])\n .toSorted((a, b) => a.storyName.localeCompare(b.storyName))\n .map((snap) => (\n <SnapshotCardItem key={snap.id} snap={snap} {...props} />\n ))}\n </div>\n </div>\n ))}\n </>\n );\n}\n\nfunction SnapshotCardItem(props: {\n snap: Snapshot;\n project: Project;\n buildId: string;\n layer: SnapshotLayer;\n componentHref: string | undefined;\n}): Rendered {\n const { snap, project, buildId } = props;\n return (\n <div\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/${buildId}/snapshots/${snap.id}/image`}\n alt={`${snap.storyTitle} / ${snap.storyName}`}\n loading=\"lazy\"\n />\n <A11yViolationsDetails snapshot={snap} />\n <SnapshotActions {...props} />\n </div>\n </div>\n );\n}\n\nfunction SnapshotActions(props: {\n snap: Snapshot;\n project: Project;\n buildId: string;\n layer: SnapshotLayer;\n componentHref: string | undefined;\n}): Rendered {\n const { snap, project, buildId, layer } = props;\n const urls = createUrlBuilder(\"/\", getStore().config.publishedBaseDomain);\n return (\n <HStack>\n <Button\n variant=\"secondary\"\n size=\"sm\"\n icon=\"columns\"\n href={`${urls.buildDiff(project.slug, buildId)}?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, buildId)}/?storyId=${encodeURIComponent(snap.storyId)}`}\n target=\"_blank\"\n rel=\"noopener\"\n >\n Preview\n </Button>\n <DesignButtons\n urls={layer.design.bySnapshot.get(snap.id) ?? []}\n providerName={providerNameFor}\n previews={layer.design.previews}\n componentHref={props.componentHref}\n />\n </HStack>\n );\n}\n"],"mappings":"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"}