@storyshelf/app 0.6.2 → 0.8.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 (429) hide show
  1. package/README.md +42 -4
  2. package/dist/app-types.d.mts +2 -6
  3. package/dist/asset-manifest.mjs +14 -7
  4. package/dist/asset-manifest.mjs.map +1 -1
  5. package/dist/assets/{htmx.min.js → htmx.mjs} +9 -1
  6. package/dist/assets/htmx.mjs.map +1 -0
  7. package/dist/assets/mark.mjs +9 -0
  8. package/dist/assets/mark.mjs.map +1 -0
  9. package/dist/boot-tasks.mjs +79 -0
  10. package/dist/boot-tasks.mjs.map +1 -0
  11. package/dist/build-state-flags.mjs +17 -0
  12. package/dist/build-state-flags.mjs.map +1 -0
  13. package/dist/build-state-setup.mjs +74 -0
  14. package/dist/build-state-setup.mjs.map +1 -0
  15. package/dist/capture-alerts.mjs +45 -0
  16. package/dist/capture-alerts.mjs.map +1 -0
  17. package/dist/capture-process.mjs +58 -0
  18. package/dist/capture-process.mjs.map +1 -0
  19. package/dist/capture-setup.mjs +96 -119
  20. package/dist/capture-setup.mjs.map +1 -1
  21. package/dist/connections.mjs +33 -0
  22. package/dist/connections.mjs.map +1 -0
  23. package/dist/consumer-setup.mjs +57 -0
  24. package/dist/consumer-setup.mjs.map +1 -0
  25. package/dist/credential-keys.mjs +1 -2
  26. package/dist/credentials.mjs +1 -2
  27. package/dist/git-status.mjs +45 -0
  28. package/dist/git-status.mjs.map +1 -0
  29. package/dist/hidden-data.mjs +23 -0
  30. package/dist/hidden-data.mjs.map +1 -0
  31. package/dist/index.d.mts +3 -5
  32. package/dist/index.mjs +204 -127
  33. package/dist/index.mjs.map +1 -1
  34. package/dist/insights/budget.mjs +48 -0
  35. package/dist/insights/budget.mjs.map +1 -0
  36. package/dist/insights/deps.mjs +20 -0
  37. package/dist/insights/deps.mjs.map +1 -0
  38. package/dist/insights/errors.mjs +21 -0
  39. package/dist/insights/errors.mjs.map +1 -0
  40. package/dist/insights/health-panel.mjs +39 -0
  41. package/dist/insights/health-panel.mjs.map +1 -0
  42. package/dist/insights/job.mjs +102 -0
  43. package/dist/insights/job.mjs.map +1 -0
  44. package/dist/insights/load-health.mjs +58 -0
  45. package/dist/insights/load-health.mjs.map +1 -0
  46. package/dist/insights/load-triage.mjs +97 -0
  47. package/dist/insights/load-triage.mjs.map +1 -0
  48. package/dist/insights/panel.mjs +27 -0
  49. package/dist/insights/panel.mjs.map +1 -0
  50. package/dist/insights/request.mjs +112 -0
  51. package/dist/insights/request.mjs.map +1 -0
  52. package/dist/insights/stale.mjs +20 -0
  53. package/dist/insights/stale.mjs.map +1 -0
  54. package/dist/insights/usage-report.mjs +59 -0
  55. package/dist/insights/usage-report.mjs.map +1 -0
  56. package/dist/insights/view.mjs +47 -0
  57. package/dist/insights/view.mjs.map +1 -0
  58. package/dist/job-canceller.mjs +13 -0
  59. package/dist/job-canceller.mjs.map +1 -0
  60. package/dist/job-queue.mjs +50 -0
  61. package/dist/job-queue.mjs.map +1 -0
  62. package/dist/jobs-setup.mjs +131 -0
  63. package/dist/jobs-setup.mjs.map +1 -0
  64. package/dist/lifecycle.mjs +93 -22
  65. package/dist/lifecycle.mjs.map +1 -1
  66. package/dist/middleware/auth-gate.mjs +5 -4
  67. package/dist/middleware/auth-gate.mjs.map +1 -1
  68. package/dist/middleware/csrf.mjs +1 -2
  69. package/dist/middleware/rate-limit.mjs +42 -35
  70. package/dist/middleware/rate-limit.mjs.map +1 -1
  71. package/dist/middleware/request-log.mjs +4 -4
  72. package/dist/middleware/request-log.mjs.map +1 -1
  73. package/dist/middleware/require-review.mjs +50 -0
  74. package/dist/middleware/require-review.mjs.map +1 -0
  75. package/dist/middleware/require-testing.mjs +40 -0
  76. package/dist/middleware/require-testing.mjs.map +1 -0
  77. package/dist/middleware/server-timing.mjs +1 -2
  78. package/dist/middleware/setup-gate.mjs +1 -2
  79. package/dist/middleware/setup-gate.mjs.map +1 -1
  80. package/dist/middleware/store-scope.mjs +13 -4
  81. package/dist/middleware/store-scope.mjs.map +1 -1
  82. package/dist/notifications-pause.mjs +32 -0
  83. package/dist/notifications-pause.mjs.map +1 -0
  84. package/dist/notify.d.mts +3 -4
  85. package/dist/notify.mjs +79 -41
  86. package/dist/notify.mjs.map +1 -1
  87. package/dist/pages/a11y-violations.mjs +115 -0
  88. package/dist/pages/a11y-violations.mjs.map +1 -0
  89. package/dist/pages/admin-ai.mjs +77 -0
  90. package/dist/pages/admin-ai.mjs.map +1 -0
  91. package/dist/pages/admin-system-origin.mjs +132 -0
  92. package/dist/pages/admin-system-origin.mjs.map +1 -0
  93. package/dist/pages/admin-system-targets.mjs +52 -0
  94. package/dist/pages/admin-system-targets.mjs.map +1 -0
  95. package/dist/pages/admin-system.mjs +64 -15
  96. package/dist/pages/admin-system.mjs.map +1 -1
  97. package/dist/pages/build-detail.mjs +105 -78
  98. package/dist/pages/build-detail.mjs.map +1 -1
  99. package/dist/pages/build-diff-actions.mjs +88 -35
  100. package/dist/pages/build-diff-actions.mjs.map +1 -1
  101. package/dist/pages/build-diff-comments.mjs +17 -16
  102. package/dist/pages/build-diff-comments.mjs.map +1 -1
  103. package/dist/pages/build-diff-default.mjs +53 -0
  104. package/dist/pages/build-diff-default.mjs.map +1 -0
  105. package/dist/pages/build-diff-environment.mjs +81 -0
  106. package/dist/pages/build-diff-environment.mjs.map +1 -0
  107. package/dist/pages/build-diff-header.mjs +23 -6
  108. package/dist/pages/build-diff-header.mjs.map +1 -1
  109. package/dist/pages/build-diff-insight.mjs +112 -0
  110. package/dist/pages/build-diff-insight.mjs.map +1 -0
  111. package/dist/pages/build-diff-model.mjs +74 -12
  112. package/dist/pages/build-diff-model.mjs.map +1 -1
  113. package/dist/pages/build-diff-nav.mjs +29 -14
  114. package/dist/pages/build-diff-nav.mjs.map +1 -1
  115. package/dist/pages/build-diff-reference.mjs +84 -0
  116. package/dist/pages/build-diff-reference.mjs.map +1 -0
  117. package/dist/pages/build-diff-shortcuts.mjs +6 -3
  118. package/dist/pages/build-diff-shortcuts.mjs.map +1 -1
  119. package/dist/pages/build-diff-stale.mjs +1 -2
  120. package/dist/pages/build-diff-usage.mjs +62 -0
  121. package/dist/pages/build-diff-usage.mjs.map +1 -0
  122. package/dist/pages/build-diff-viewer.mjs +97 -14
  123. package/dist/pages/build-diff-viewer.mjs.map +1 -1
  124. package/dist/pages/build-diff.mjs +37 -22
  125. package/dist/pages/build-diff.mjs.map +1 -1
  126. package/dist/pages/build-tests.mjs +73 -0
  127. package/dist/pages/build-tests.mjs.map +1 -0
  128. package/dist/pages/component-detail.mjs +248 -0
  129. package/dist/pages/component-detail.mjs.map +1 -0
  130. package/dist/pages/component-stories.mjs +167 -0
  131. package/dist/pages/component-stories.mjs.map +1 -0
  132. package/dist/pages/compute-jobs.mjs +14 -28
  133. package/dist/pages/compute-jobs.mjs.map +1 -1
  134. package/dist/pages/design-links.mjs +181 -0
  135. package/dist/pages/design-links.mjs.map +1 -0
  136. package/dist/pages/environment-change.mjs +52 -0
  137. package/dist/pages/environment-change.mjs.map +1 -0
  138. package/dist/pages/health-panel.mjs +125 -0
  139. package/dist/pages/health-panel.mjs.map +1 -0
  140. package/dist/pages/invite.mjs +1 -2
  141. package/dist/pages/label-detail.mjs +1 -2
  142. package/dist/pages/library-grid.mjs +104 -0
  143. package/dist/pages/library-grid.mjs.map +1 -0
  144. package/dist/pages/library-model.mjs +46 -0
  145. package/dist/pages/library-model.mjs.map +1 -0
  146. package/dist/pages/library-snapshots.mjs +129 -0
  147. package/dist/pages/library-snapshots.mjs.map +1 -0
  148. package/dist/pages/library.mjs +113 -220
  149. package/dist/pages/library.mjs.map +1 -1
  150. package/dist/pages/login.mjs +1 -2
  151. package/dist/pages/notifications-paused.mjs +21 -0
  152. package/dist/pages/notifications-paused.mjs.map +1 -0
  153. package/dist/pages/passkey-ceremony.mjs +1 -2
  154. package/dist/pages/profile.mjs +54 -50
  155. package/dist/pages/profile.mjs.map +1 -1
  156. package/dist/pages/project-builds-table.mjs +3 -4
  157. package/dist/pages/project-builds-table.mjs.map +1 -1
  158. package/dist/pages/project-builds.mjs +52 -23
  159. package/dist/pages/project-builds.mjs.map +1 -1
  160. package/dist/pages/project-create.mjs +1 -2
  161. package/dist/pages/project-settings.mjs +52 -14
  162. package/dist/pages/project-settings.mjs.map +1 -1
  163. package/dist/pages/projects-card.mjs +15 -3
  164. package/dist/pages/projects-card.mjs.map +1 -1
  165. package/dist/pages/projects.mjs +7 -4
  166. package/dist/pages/projects.mjs.map +1 -1
  167. package/dist/pages/review-changeset.mjs +115 -0
  168. package/dist/pages/review-changeset.mjs.map +1 -0
  169. package/dist/pages/review-comments.mjs +102 -0
  170. package/dist/pages/review-comments.mjs.map +1 -0
  171. package/dist/pages/review-verdict.mjs +101 -0
  172. package/dist/pages/review-verdict.mjs.map +1 -0
  173. package/dist/pages/root.mjs +1 -2
  174. package/dist/pages/settings-ai.mjs +68 -0
  175. package/dist/pages/settings-ai.mjs.map +1 -0
  176. package/dist/pages/settings-capture.mjs +123 -0
  177. package/dist/pages/settings-capture.mjs.map +1 -0
  178. package/dist/pages/settings-design-credential.mjs +98 -0
  179. package/dist/pages/settings-design-credential.mjs.map +1 -0
  180. package/dist/pages/settings-design-links.mjs +178 -0
  181. package/dist/pages/settings-design-links.mjs.map +1 -0
  182. package/dist/pages/settings-design.mjs +47 -0
  183. package/dist/pages/settings-design.mjs.map +1 -0
  184. package/dist/pages/settings-general.mjs +47 -23
  185. package/dist/pages/settings-general.mjs.map +1 -1
  186. package/dist/pages/settings-labels.mjs +44 -4
  187. package/dist/pages/settings-labels.mjs.map +1 -1
  188. package/dist/pages/settings-links.mjs +111 -0
  189. package/dist/pages/settings-links.mjs.map +1 -0
  190. package/dist/pages/settings-members.mjs +1 -2
  191. package/dist/pages/settings-notifications-git.mjs +127 -0
  192. package/dist/pages/settings-notifications-git.mjs.map +1 -0
  193. package/dist/pages/settings-notifications.mjs +80 -44
  194. package/dist/pages/settings-notifications.mjs.map +1 -1
  195. package/dist/pages/settings-tests.mjs +34 -4
  196. package/dist/pages/settings-tests.mjs.map +1 -1
  197. package/dist/pages/settings-tokens.mjs +1 -2
  198. package/dist/pages/settings-webhooks.mjs +1 -2
  199. package/dist/pages/storybook.mjs +1 -2
  200. package/dist/pages/target-choices.mjs +38 -0
  201. package/dist/pages/target-choices.mjs.map +1 -0
  202. package/dist/publish-setup.mjs +88 -0
  203. package/dist/publish-setup.mjs.map +1 -0
  204. package/dist/retention-timer.mjs +12 -12
  205. package/dist/retention-timer.mjs.map +1 -1
  206. package/dist/review-events.mjs +47 -0
  207. package/dist/review-events.mjs.map +1 -0
  208. package/dist/routers/admin-pages.mjs +46 -5
  209. package/dist/routers/admin-pages.mjs.map +1 -1
  210. package/dist/routers/admin-public-origin.mjs +92 -0
  211. package/dist/routers/admin-public-origin.mjs.map +1 -0
  212. package/dist/routers/admin.mjs +8 -4
  213. package/dist/routers/admin.mjs.map +1 -1
  214. package/dist/routers/assets.mjs +10 -4
  215. package/dist/routers/assets.mjs.map +1 -1
  216. package/dist/routers/attempts.mjs +1 -2
  217. package/dist/routers/auth-sync.mjs +1 -2
  218. package/dist/routers/auth.mjs +2 -3
  219. package/dist/routers/baseline-guard.mjs +4 -5
  220. package/dist/routers/baseline-guard.mjs.map +1 -1
  221. package/dist/routers/baseline-reference.mjs +72 -0
  222. package/dist/routers/baseline-reference.mjs.map +1 -0
  223. package/dist/routers/baselines.handlers.mjs +63 -0
  224. package/dist/routers/baselines.handlers.mjs.map +1 -0
  225. package/dist/routers/baselines.mjs +124 -0
  226. package/dist/routers/baselines.mjs.map +1 -0
  227. package/dist/routers/build-cancel.mjs +67 -0
  228. package/dist/routers/build-cancel.mjs.map +1 -0
  229. package/dist/routers/build-changeset.mjs +69 -0
  230. package/dist/routers/build-changeset.mjs.map +1 -0
  231. package/dist/routers/build-reviews.handlers.mjs +85 -0
  232. package/dist/routers/build-reviews.handlers.mjs.map +1 -0
  233. package/dist/routers/build-reviews.mjs +73 -0
  234. package/dist/routers/build-reviews.mjs.map +1 -0
  235. package/dist/routers/builds.handlers.mjs +65 -60
  236. package/dist/routers/builds.handlers.mjs.map +1 -1
  237. package/dist/routers/builds.mjs +77 -19
  238. package/dist/routers/builds.mjs.map +1 -1
  239. package/dist/routers/capture-cache.mjs +29 -0
  240. package/dist/routers/capture-cache.mjs.map +1 -0
  241. package/dist/routers/capture-config.mjs +34 -0
  242. package/dist/routers/capture-config.mjs.map +1 -0
  243. package/dist/routers/comments.mjs +42 -17
  244. package/dist/routers/comments.mjs.map +1 -1
  245. package/dist/routers/comparison-image.mjs +29 -0
  246. package/dist/routers/comparison-image.mjs.map +1 -0
  247. package/dist/routers/component-catalog.handlers.mjs +70 -0
  248. package/dist/routers/component-catalog.handlers.mjs.map +1 -0
  249. package/dist/routers/component.handlers.mjs +120 -0
  250. package/dist/routers/component.handlers.mjs.map +1 -0
  251. package/dist/routers/connections.mjs +117 -0
  252. package/dist/routers/connections.mjs.map +1 -0
  253. package/dist/routers/design-connect.handlers.mjs +43 -0
  254. package/dist/routers/design-connect.handlers.mjs.map +1 -0
  255. package/dist/routers/design.handlers.mjs +113 -0
  256. package/dist/routers/design.handlers.mjs.map +1 -0
  257. package/dist/routers/flash.mjs +1 -2
  258. package/dist/routers/health-report.mjs +3 -5
  259. package/dist/routers/health-report.mjs.map +1 -1
  260. package/dist/routers/health.mjs +5 -5
  261. package/dist/routers/health.mjs.map +1 -1
  262. package/dist/routers/helpers.mjs +20 -5
  263. package/dist/routers/helpers.mjs.map +1 -1
  264. package/dist/routers/htmx.mjs +1 -2
  265. package/dist/routers/insights-ui.mjs +63 -0
  266. package/dist/routers/insights-ui.mjs.map +1 -0
  267. package/dist/routers/insights.handlers.mjs +44 -0
  268. package/dist/routers/insights.handlers.mjs.map +1 -0
  269. package/dist/routers/insights.mjs +188 -0
  270. package/dist/routers/insights.mjs.map +1 -0
  271. package/dist/routers/labels.mjs +1 -2
  272. package/dist/routers/links.handlers.mjs +79 -0
  273. package/dist/routers/links.handlers.mjs.map +1 -0
  274. package/dist/routers/links.mjs +138 -0
  275. package/dist/routers/links.mjs.map +1 -0
  276. package/dist/routers/media.mjs +30 -7
  277. package/dist/routers/media.mjs.map +1 -1
  278. package/dist/routers/members.mjs +3 -3
  279. package/dist/routers/members.mjs.map +1 -1
  280. package/dist/routers/notification-channels.mjs +18 -25
  281. package/dist/routers/notification-channels.mjs.map +1 -1
  282. package/dist/routers/notification-subscriptions.mjs +1 -2
  283. package/dist/routers/oauth-state.mjs +44 -0
  284. package/dist/routers/oauth-state.mjs.map +1 -0
  285. package/dist/routers/preview-base.mjs +37 -0
  286. package/dist/routers/preview-base.mjs.map +1 -0
  287. package/dist/routers/preview-token.mjs +50 -0
  288. package/dist/routers/preview-token.mjs.map +1 -0
  289. package/dist/routers/profile.mjs +4 -3
  290. package/dist/routers/profile.mjs.map +1 -1
  291. package/dist/routers/projects.mjs +18 -3
  292. package/dist/routers/projects.mjs.map +1 -1
  293. package/dist/routers/publish-upload.mjs +85 -0
  294. package/dist/routers/publish-upload.mjs.map +1 -0
  295. package/dist/routers/published-access.mjs +51 -0
  296. package/dist/routers/published-access.mjs.map +1 -0
  297. package/dist/routers/published-compression.mjs +109 -0
  298. package/dist/routers/published-compression.mjs.map +1 -0
  299. package/dist/routers/published-files.mjs +69 -0
  300. package/dist/routers/published-files.mjs.map +1 -0
  301. package/dist/routers/review-forms.mjs +71 -0
  302. package/dist/routers/review-forms.mjs.map +1 -0
  303. package/dist/routers/review-next.mjs +1 -2
  304. package/dist/routers/review-section.handlers.mjs +84 -0
  305. package/dist/routers/review-section.handlers.mjs.map +1 -0
  306. package/dist/routers/review-switch-off.mjs +28 -0
  307. package/dist/routers/review-switch-off.mjs.map +1 -0
  308. package/dist/routers/schemas.mjs +141 -24
  309. package/dist/routers/schemas.mjs.map +1 -1
  310. package/dist/routers/scoped-models.mjs +21 -0
  311. package/dist/routers/scoped-models.mjs.map +1 -0
  312. package/dist/routers/settings-ai.mjs +34 -0
  313. package/dist/routers/settings-ai.mjs.map +1 -0
  314. package/dist/routers/settings-capture.mjs +76 -0
  315. package/dist/routers/settings-capture.mjs.map +1 -0
  316. package/dist/routers/settings-design.mjs +102 -0
  317. package/dist/routers/settings-design.mjs.map +1 -0
  318. package/dist/routers/settings-general.mjs +56 -12
  319. package/dist/routers/settings-general.mjs.map +1 -1
  320. package/dist/routers/settings-git-channel.mjs +76 -0
  321. package/dist/routers/settings-git-channel.mjs.map +1 -0
  322. package/dist/routers/settings-labels.mjs +28 -3
  323. package/dist/routers/settings-labels.mjs.map +1 -1
  324. package/dist/routers/settings-links.mjs +62 -0
  325. package/dist/routers/settings-links.mjs.map +1 -0
  326. package/dist/routers/settings-members.mjs +5 -9
  327. package/dist/routers/settings-members.mjs.map +1 -1
  328. package/dist/routers/settings-notifications.mjs +16 -12
  329. package/dist/routers/settings-notifications.mjs.map +1 -1
  330. package/dist/routers/settings-tokens.mjs +3 -7
  331. package/dist/routers/settings-tokens.mjs.map +1 -1
  332. package/dist/routers/settings-webhooks.mjs +8 -26
  333. package/dist/routers/settings-webhooks.mjs.map +1 -1
  334. package/dist/routers/settings.handlers.mjs +108 -28
  335. package/dist/routers/settings.handlers.mjs.map +1 -1
  336. package/dist/routers/settings.mjs +17 -6
  337. package/dist/routers/settings.mjs.map +1 -1
  338. package/dist/routers/signed-storybook.mjs +55 -0
  339. package/dist/routers/signed-storybook.mjs.map +1 -0
  340. package/dist/routers/snapshot-groups.mjs +62 -0
  341. package/dist/routers/snapshot-groups.mjs.map +1 -0
  342. package/dist/routers/snapshots.mjs +104 -7
  343. package/dist/routers/snapshots.mjs.map +1 -1
  344. package/dist/routers/storybook.mjs +57 -104
  345. package/dist/routers/storybook.mjs.map +1 -1
  346. package/dist/routers/tokens.mjs +3 -7
  347. package/dist/routers/tokens.mjs.map +1 -1
  348. package/dist/routers/ui.mjs +38 -7
  349. package/dist/routers/ui.mjs.map +1 -1
  350. package/dist/routers/webhooks.mjs +6 -13
  351. package/dist/routers/webhooks.mjs.map +1 -1
  352. package/dist/routers/well-known.mjs +12 -6
  353. package/dist/routers/well-known.mjs.map +1 -1
  354. package/dist/routes/api-routes.mjs +92 -0
  355. package/dist/routes/api-routes.mjs.map +1 -0
  356. package/dist/routes/classification.mjs +37 -0
  357. package/dist/routes/classification.mjs.map +1 -0
  358. package/dist/routes/openapi-classes.mjs +21 -0
  359. package/dist/routes/openapi-classes.mjs.map +1 -0
  360. package/dist/routes/page-routes.mjs +103 -0
  361. package/dist/routes/page-routes.mjs.map +1 -0
  362. package/dist/runtime.mjs +19 -15
  363. package/dist/runtime.mjs.map +1 -1
  364. package/dist/serverless.mjs +4 -0
  365. package/dist/store.mjs +20 -3
  366. package/dist/store.mjs.map +1 -1
  367. package/dist/targets.mjs +45 -0
  368. package/dist/targets.mjs.map +1 -0
  369. package/dist/ui/avatar.mjs +1 -2
  370. package/dist/ui/buttons.mjs +17 -5
  371. package/dist/ui/buttons.mjs.map +1 -1
  372. package/dist/ui/code-block.mjs +1 -2
  373. package/dist/ui/compare-stage.mjs +86 -54
  374. package/dist/ui/compare-stage.mjs.map +1 -1
  375. package/dist/ui/csrf-field.mjs +1 -2
  376. package/dist/ui/css.mjs +1 -2
  377. package/dist/ui/design-reference.mjs +96 -0
  378. package/dist/ui/design-reference.mjs.map +1 -0
  379. package/dist/ui/document.mjs +2 -4
  380. package/dist/ui/document.mjs.map +1 -1
  381. package/dist/ui/dropdown.mjs +1 -2
  382. package/dist/ui/feedback.mjs +1 -2
  383. package/dist/ui/filter-input.mjs +1 -2
  384. package/dist/ui/forms.mjs +7 -7
  385. package/dist/ui/forms.mjs.map +1 -1
  386. package/dist/ui/icons/icon.mjs +1 -2
  387. package/dist/ui/icons/paths.mjs +2 -2
  388. package/dist/ui/icons/paths.mjs.map +1 -1
  389. package/dist/ui/icons/sprite.mjs +1 -2
  390. package/dist/ui/kbd.mjs +1 -2
  391. package/dist/ui/layout.mjs +1 -2
  392. package/dist/ui/preview-embed.mjs +81 -0
  393. package/dist/ui/preview-embed.mjs.map +1 -0
  394. package/dist/ui/progress.mjs +1 -2
  395. package/dist/ui/segmented.mjs +1 -2
  396. package/dist/ui/shell/account.mjs +1 -2
  397. package/dist/ui/shell/bare.mjs +1 -2
  398. package/dist/ui/shell/boot-script.mjs +1 -2
  399. package/dist/ui/shell/client-script.mjs +86 -2
  400. package/dist/ui/shell/client-script.mjs.map +1 -1
  401. package/dist/ui/shell/nav.mjs +13 -10
  402. package/dist/ui/shell/nav.mjs.map +1 -1
  403. package/dist/ui/shell/review-script.mjs +25 -3
  404. package/dist/ui/shell/review-script.mjs.map +1 -1
  405. package/dist/ui/shell/sidebar.mjs +3 -4
  406. package/dist/ui/shell/sidebar.mjs.map +1 -1
  407. package/dist/ui/shell/topbar.mjs +1 -2
  408. package/dist/ui/stacks.mjs +1 -2
  409. package/dist/ui/styles/base.mjs +1 -2
  410. package/dist/ui/styles/behavior.mjs +1 -2
  411. package/dist/ui/styles/review-layout.mjs +1 -2
  412. package/dist/ui/styles/review.mjs +1 -2
  413. package/dist/ui/styles/tokens.mjs +1 -2
  414. package/dist/ui/styles.mjs +1 -2
  415. package/dist/ui/subnav.mjs +1 -2
  416. package/dist/ui/table.mjs +1 -2
  417. package/dist/ui/theme.mjs +1 -2
  418. package/dist/ui/thumbnail.mjs +1 -2
  419. package/dist/ui/time.mjs +1 -2
  420. package/package.json +14 -12
  421. package/dist/asset-paths.mjs +0 -23
  422. package/dist/asset-paths.mjs.map +0 -1
  423. package/dist/assets/mark.svg +0 -1
  424. package/dist/pages/settings-status.mjs +0 -137
  425. package/dist/pages/settings-status.mjs.map +0 -1
  426. package/dist/routers/settings-status.mjs +0 -77
  427. package/dist/routers/settings-status.mjs.map +0 -1
  428. package/dist/routers/status-configs.mjs +0 -108
  429. package/dist/routers/status-configs.mjs.map +0 -1
@@ -1,62 +1,101 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
+ import { testingEnabled } from "../store.mjs";
4
5
  import { PageHeader } from "../ui/layout.mjs";
5
6
  import { SubNavLayout } from "../ui/subnav.mjs";
6
7
  import { DocumentLayout } from "../ui/document.mjs";
8
+ import { renderSettingsAi } from "./settings-ai.mjs";
9
+ import { renderSettingsCapture } from "./settings-capture.mjs";
10
+ import { renderSettingsDesign } from "./settings-design.mjs";
7
11
  import { renderSettingsGeneral } from "./settings-general.mjs";
8
12
  import { renderSettingsLabels } from "./settings-labels.mjs";
13
+ import { renderSettingsLinks } from "./settings-links.mjs";
9
14
  import { renderSettingsMembers } from "./settings-members.mjs";
10
15
  import { renderSettingsNotifications } from "./settings-notifications.mjs";
11
- import { renderSettingsStatus } from "./settings-status.mjs";
12
16
  import { renderSettingsTests } from "./settings-tests.mjs";
13
17
  import { renderSettingsTokens } from "./settings-tokens.mjs";
14
18
  import { renderSettingsWebhooks } from "./settings-webhooks.mjs";
15
19
  import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
16
- //#region src/pages/project-settings.tsx
20
+ // src/pages/project-settings.tsx
17
21
  function tabHref(project, tab) {
18
22
  return tab === "general" ? `/projects/${project.slug}/settings` : `/projects/${project.slug}/settings/${tab}`;
19
23
  }
20
24
  const TAB_LABELS = {
21
25
  general: "General",
26
+ capture: "Capture",
22
27
  tests: "Tests",
23
28
  labels: "Labels",
29
+ links: "Links",
30
+ design: "Design",
24
31
  tokens: "Tokens",
25
32
  webhooks: "Webhooks",
26
33
  notifications: "Notifications",
27
34
  members: "Members",
28
- status: "Git status"
35
+ ai: "AI"
29
36
  };
30
37
  const TAB_ICONS = {
31
38
  general: "settings",
39
+ capture: "image",
32
40
  tests: "activity",
33
41
  labels: "tag",
42
+ links: "external-link",
43
+ design: "image",
34
44
  tokens: "key",
35
45
  webhooks: "webhook",
36
46
  notifications: "bell",
37
47
  members: "users",
38
- status: "git-branch"
48
+ ai: "sparkles"
39
49
  };
50
+ /** Tabs whose pages are `testing` routes: hidden on a hosting-only instance. */
51
+ const TESTING_TABS = /* @__PURE__ */ new Set([
52
+ "capture",
53
+ "tests",
54
+ "ai"
55
+ ]);
40
56
  const SETTINGS_TABS = [
41
57
  "general",
58
+ "capture",
42
59
  "tests",
43
60
  "labels",
61
+ "links",
62
+ "design",
44
63
  "tokens",
45
64
  "webhooks",
46
65
  "notifications",
47
66
  "members",
48
- "status"
67
+ "ai"
49
68
  ];
69
+ /** A tab shows when its page exists: `testing` tabs need testing, and the AI tab needs AI. */
70
+ function settingsTabShown(tab, data) {
71
+ if (TESTING_TABS.has(tab) && !testingEnabled()) return false;
72
+ return tab !== "ai" || data.ai?.available === true;
73
+ }
74
+ /** Project-behaviour tabs (general, tests, AI); undefined for any other tab. */
75
+ function renderBehaviourTab(data, formState) {
76
+ const { project, activeTab } = data;
77
+ if (activeTab === "general") return renderSettingsGeneral(project, formState, data.isAdmin, data.targets);
78
+ if (activeTab === "capture") return renderSettingsCapture(project, data.isAdmin, formState, data.targets);
79
+ if (activeTab === "ai") return renderSettingsAi(project, data.ai, formState);
80
+ return activeTab === "tests" ? renderSettingsTests(project, data.isAdmin, formState) : void 0;
81
+ }
82
+ /** Labels, links, tokens, and webhooks tabs; undefined for any other tab. */
83
+ function renderCatalogTab(data, formState) {
84
+ const { project, activeTab, isAdmin } = data;
85
+ if (activeTab === "labels") return renderSettingsLabels(project, data.labelTypes, isAdmin, formState);
86
+ if (activeTab === "links") return renderSettingsLinks(project, data.links, data.linkCandidates, isAdmin, formState);
87
+ if (activeTab === "design") return renderSettingsDesign(project, data.design, isAdmin, formState);
88
+ if (activeTab === "tokens") return renderSettingsTokens(project, data.tokens, isAdmin, formState);
89
+ return activeTab === "webhooks" ? renderSettingsWebhooks(project, data.webhooks, isAdmin, formState) : void 0;
90
+ }
50
91
  function renderActiveTab(data, formState) {
51
92
  const { project, activeTab } = data;
52
- if (activeTab === "general") return renderSettingsGeneral(project, formState, data.isAdmin);
53
- if (activeTab === "tests") return renderSettingsTests(project, data.isAdmin, formState);
54
- if (activeTab === "labels") return renderSettingsLabels(project, data.labelTypes, data.isAdmin, formState);
55
- if (activeTab === "tokens") return renderSettingsTokens(project, data.tokens, data.isAdmin, formState);
56
- if (activeTab === "webhooks") return renderSettingsWebhooks(project, data.webhooks, data.isAdmin, formState);
57
- if (activeTab === "notifications") return renderSettingsNotifications(project, data.notificationChannels, data.notifyProviders, data.mySubscription, data.isAdmin, formState);
93
+ const behaviour = renderBehaviourTab(data, formState);
94
+ if (behaviour !== void 0) return behaviour;
95
+ const catalog = renderCatalogTab(data, formState);
96
+ if (catalog !== void 0) return catalog;
97
+ if (activeTab === "notifications") return renderSettingsNotifications(project, data.notificationChannels, data.notifyProviders, data.mySubscription, data.isAdmin, formState, data.notificationsPaused);
58
98
  if (activeTab === "members") return renderSettingsMembers(project, data.members, data.groupMappings, data.isAdmin, formState);
59
- if (activeTab === "status") return renderSettingsStatus(project, data.statusConfigs, data.gitHosts, data.isAdmin, formState);
60
99
  return null;
61
100
  }
62
101
  /** Project settings shell: tab navigation plus the active settings tab. */
@@ -79,7 +118,7 @@ function renderProjectSettingsPage(data, formState) {
79
118
  ] })
80
119
  }), /* @__PURE__ */ jsx(SubNavLayout, {
81
120
  label: "Settings sections",
82
- items: SETTINGS_TABS.map((tab) => ({
121
+ items: SETTINGS_TABS.filter((tab) => settingsTabShown(tab, data)).map((tab) => ({
83
122
  label: TAB_LABELS[tab],
84
123
  href: tabHref(project, tab),
85
124
  icon: TAB_ICONS[tab],
@@ -89,7 +128,6 @@ function renderProjectSettingsPage(data, formState) {
89
128
  })]
90
129
  });
91
130
  }
92
- //#endregion
93
131
  export { renderProjectSettingsPage };
94
132
 
95
133
  //# sourceMappingURL=project-settings.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"project-settings.mjs","names":[],"sources":["../../src/pages/project-settings.tsx"],"sourcesContent":["import type { GitHostProvider } from \"@storyshelf/core/adapter/git-host\";\nimport type { LabelType } from \"@storyshelf/core/schema\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport type { ProjectGroupMapping } from \"@storyshelf/core/schema\";\nimport type { Token } from \"@storyshelf/core/schema\";\nimport { PageHeader, SubNavLayout } from \"../ui/components.tsx\";\nimport type { IconName } from \"../ui/components.tsx\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\nimport { renderSettingsGeneral } from \"./settings-general.tsx\";\nimport { renderSettingsLabels } from \"./settings-labels.tsx\";\nimport { renderSettingsMembers, type SettingsMember } from \"./settings-members.tsx\";\nimport {\n renderSettingsNotifications,\n type SettingsMySubscription,\n type SettingsNotificationChannel,\n} from \"./settings-notifications.tsx\";\nimport { renderSettingsStatus, type SettingsStatusConfig } from \"./settings-status.tsx\";\nimport { renderSettingsTests } from \"./settings-tests.tsx\";\nimport { renderSettingsTokens } from \"./settings-tokens.tsx\";\nimport { renderSettingsWebhooks, type SettingsWebhook } from \"./settings-webhooks.tsx\";\n\n/** Tabs available in the project settings section. */\nexport type SettingsTab =\n | \"general\"\n | \"tests\"\n | \"labels\"\n | \"tokens\"\n | \"webhooks\"\n | \"notifications\"\n | \"members\"\n | \"status\";\n\n/** Data required to render the project settings page with its active tab. */\nexport interface ProjectSettingsData {\n project: Project;\n activeTab: SettingsTab;\n labelTypes: LabelType[];\n tokens: Omit<Token, \"hash\">[];\n members: SettingsMember[];\n groupMappings: ProjectGroupMapping[];\n webhooks: SettingsWebhook[];\n notificationChannels: SettingsNotificationChannel[];\n notifyProviders: string[];\n mySubscription: SettingsMySubscription | null;\n statusConfigs: SettingsStatusConfig[];\n gitHosts: GitHostProvider[];\n isAdmin: boolean;\n}\n\n/** Form state shared by the settings tabs (field errors, global error, one-time secret). */\nexport interface SettingsFormState {\n errors?: Record<string, string>;\n globalError?: string;\n secret?: string;\n}\n\nfunction tabHref(project: Project, tab: SettingsTab): string {\n return tab === \"general\"\n ? `/projects/${project.slug}/settings`\n : `/projects/${project.slug}/settings/${tab}`;\n}\n\nconst TAB_LABELS: Record<SettingsTab, string> = {\n general: \"General\",\n tests: \"Tests\",\n labels: \"Labels\",\n tokens: \"Tokens\",\n webhooks: \"Webhooks\",\n notifications: \"Notifications\",\n members: \"Members\",\n status: \"Git status\",\n};\n\nconst TAB_ICONS: Record<SettingsTab, IconName> = {\n general: \"settings\",\n tests: \"activity\",\n labels: \"tag\",\n tokens: \"key\",\n webhooks: \"webhook\",\n notifications: \"bell\",\n members: \"users\",\n status: \"git-branch\",\n};\n\nconst SETTINGS_TABS: SettingsTab[] = [\n \"general\",\n \"tests\",\n \"labels\",\n \"tokens\",\n \"webhooks\",\n \"notifications\",\n \"members\",\n \"status\",\n];\n\nfunction renderActiveTab(data: ProjectSettingsData, formState?: SettingsFormState): unknown {\n const { project, activeTab } = data;\n if (activeTab === \"general\") return renderSettingsGeneral(project, formState, data.isAdmin);\n if (activeTab === \"tests\") return renderSettingsTests(project, data.isAdmin, formState);\n if (activeTab === \"labels\")\n return renderSettingsLabels(project, data.labelTypes, data.isAdmin, formState);\n if (activeTab === \"tokens\")\n return renderSettingsTokens(project, data.tokens, data.isAdmin, formState);\n if (activeTab === \"webhooks\")\n return renderSettingsWebhooks(project, data.webhooks, data.isAdmin, formState);\n if (activeTab === \"notifications\")\n return renderSettingsNotifications(\n project,\n data.notificationChannels,\n data.notifyProviders,\n data.mySubscription,\n data.isAdmin,\n formState,\n );\n if (activeTab === \"members\")\n return renderSettingsMembers(\n project,\n data.members,\n data.groupMappings,\n data.isAdmin,\n formState,\n );\n if (activeTab === \"status\")\n return renderSettingsStatus(\n project,\n data.statusConfigs,\n data.gitHosts,\n data.isAdmin,\n formState,\n );\n return null;\n}\n\n/** Project settings shell: tab navigation plus the active settings tab. */\nexport function renderProjectSettingsPage(\n data: ProjectSettingsData,\n formState?: SettingsFormState,\n): RenderedContent {\n const { project, activeTab } = data;\n return (\n <DocumentLayout\n title={`${project.name} · Settings`}\n nav={{\n active: \"settings\",\n projectSlug: project.slug,\n projectName: project.name,\n trail: activeTab === \"general\" ? [] : [{ label: TAB_LABELS[activeTab] }],\n }}\n >\n <PageHeader\n title=\"Project settings\"\n description={\n <>Manage general settings, labels, tokens, webhooks and members for {project.name}.</>\n }\n />\n\n <SubNavLayout\n label=\"Settings sections\"\n items={SETTINGS_TABS.map((tab) => ({\n label: TAB_LABELS[tab],\n href: tabHref(project, tab),\n icon: TAB_ICONS[tab],\n active: activeTab === tab,\n }))}\n >\n {renderActiveTab(data, formState)}\n </SubNavLayout>\n </DocumentLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAwDA,SAAS,QAAQ,SAAkB,KAA0B;CAC3D,OAAO,QAAQ,YACX,aAAa,QAAQ,KAAK,aAC1B,aAAa,QAAQ,KAAK,YAAY;AAC5C;AAEA,MAAM,aAA0C;CAC9C,SAAS;CACT,OAAO;CACP,QAAQ;CACR,QAAQ;CACR,UAAU;CACV,eAAe;CACf,SAAS;CACT,QAAQ;AACV;AAEA,MAAM,YAA2C;CAC/C,SAAS;CACT,OAAO;CACP,QAAQ;CACR,QAAQ;CACR,UAAU;CACV,eAAe;CACf,SAAS;CACT,QAAQ;AACV;AAEA,MAAM,gBAA+B;CACnC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAEA,SAAS,gBAAgB,MAA2B,WAAwC;CAC1F,MAAM,EAAE,SAAS,cAAc;CAC/B,IAAI,cAAc,WAAW,OAAO,sBAAsB,SAAS,WAAW,KAAK,OAAO;CAC1F,IAAI,cAAc,SAAS,OAAO,oBAAoB,SAAS,KAAK,SAAS,SAAS;CACtF,IAAI,cAAc,UAChB,OAAO,qBAAqB,SAAS,KAAK,YAAY,KAAK,SAAS,SAAS;CAC/E,IAAI,cAAc,UAChB,OAAO,qBAAqB,SAAS,KAAK,QAAQ,KAAK,SAAS,SAAS;CAC3E,IAAI,cAAc,YAChB,OAAO,uBAAuB,SAAS,KAAK,UAAU,KAAK,SAAS,SAAS;CAC/E,IAAI,cAAc,iBAChB,OAAO,4BACL,SACA,KAAK,sBACL,KAAK,iBACL,KAAK,gBACL,KAAK,SACL,SACF;CACF,IAAI,cAAc,WAChB,OAAO,sBACL,SACA,KAAK,SACL,KAAK,eACL,KAAK,SACL,SACF;CACF,IAAI,cAAc,UAChB,OAAO,qBACL,SACA,KAAK,eACL,KAAK,UACL,KAAK,SACL,SACF;CACF,OAAO;AACT;;AAGA,SAAgB,0BACd,MACA,WACiB;CACjB,MAAM,EAAE,SAAS,cAAc;CAC/B,OACE,qBAAC,gBAAD;EACE,OAAO,GAAG,QAAQ,KAAK;EACvB,KAAK;GACH,QAAQ;GACR,aAAa,QAAQ;GACrB,aAAa,QAAQ;GACrB,OAAO,cAAc,YAAY,CAAC,IAAI,CAAC,EAAE,OAAO,WAAW,WAAW,CAAC;EACzE;EAPF,UAAA,CASE,oBAAC,YAAD;GACE,OAAM;GACN,aACE,qBAAA,UAAA,EAAA,UAAA;IAAE;IAAmE,QAAQ;IAAK;GAAG,EAAA,CAAA;EAExF,CAAA,GAED,oBAAC,cAAD;GACE,OAAM;GACN,OAAO,cAAc,KAAK,SAAS;IACjC,OAAO,WAAW;IAClB,MAAM,QAAQ,SAAS,GAAG;IAC1B,MAAM,UAAU;IAChB,QAAQ,cAAc;GACxB,EAAE;GAED,UAAA,gBAAgB,MAAM,SAAS;EACpB,CAAA,CACA;;AAEpB"}
1
+ {"version":3,"file":"project-settings.mjs","names":[],"sources":["../../src/pages/project-settings.tsx"],"sourcesContent":["import type { RunnerTarget } from \"@storyshelf/core/adapter/capture-runner\";\nimport type { LabelType } from \"@storyshelf/core/schema\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport type { ProjectGroupMapping } from \"@storyshelf/core/schema\";\nimport type { Token } from \"@storyshelf/core/schema\";\nimport { testingEnabled } from \"../store.ts\";\nimport { PageHeader, SubNavLayout } from \"../ui/components.tsx\";\nimport type { IconName } from \"../ui/components.tsx\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\nimport { renderSettingsAi, type SettingsAiData } from \"./settings-ai.tsx\";\nimport { renderSettingsCapture } from \"./settings-capture.tsx\";\nimport { renderSettingsDesign, type SettingsDesignData } from \"./settings-design.tsx\";\nimport { renderSettingsGeneral } from \"./settings-general.tsx\";\nimport { renderSettingsLabels } from \"./settings-labels.tsx\";\nimport {\n renderSettingsLinks,\n type SettingsLink,\n type SettingsLinkCandidate,\n} from \"./settings-links.tsx\";\nimport { renderSettingsMembers, type SettingsMember } from \"./settings-members.tsx\";\nimport {\n renderSettingsNotifications,\n type SettingsMySubscription,\n type SettingsNotificationChannel,\n type SettingsNotifyProvider,\n} from \"./settings-notifications.tsx\";\nimport { renderSettingsTests } from \"./settings-tests.tsx\";\nimport { renderSettingsTokens } from \"./settings-tokens.tsx\";\nimport { renderSettingsWebhooks, type SettingsWebhook } from \"./settings-webhooks.tsx\";\n\n/** Tabs available in the project settings section. */\nexport type SettingsTab =\n | \"general\"\n | \"capture\"\n | \"tests\"\n | \"labels\"\n | \"links\"\n | \"design\"\n | \"tokens\"\n | \"webhooks\"\n | \"notifications\"\n | \"members\"\n | \"ai\";\n\n/** Data required to render the project settings page with its active tab. */\nexport interface ProjectSettingsData {\n project: Project;\n activeTab: SettingsTab;\n labelTypes: LabelType[];\n links: SettingsLink[];\n linkCandidates: SettingsLinkCandidate[];\n design: SettingsDesignData;\n tokens: Omit<Token, \"hash\">[];\n members: SettingsMember[];\n groupMappings: ProjectGroupMapping[];\n webhooks: SettingsWebhook[];\n notificationChannels: SettingsNotificationChannel[];\n notifyProviders: SettingsNotifyProvider[];\n mySubscription: SettingsMySubscription | null;\n isAdmin: boolean;\n /** Notifiers are wired but no public origin resolves (ADR 0014, decision 11). */\n notificationsPaused?: boolean | undefined;\n /** AI availability and the profile choices (omitted ⇒ AI off). */\n ai?: SettingsAiData;\n /** What the capture runner offers (ADR 0007); omitted when it publishes no list. */\n targets?: readonly RunnerTarget[] | undefined;\n}\n\n/** Form state shared by the settings tabs (field errors, global error, one-time secret). */\nexport interface SettingsFormState {\n errors?: Record<string, string>;\n globalError?: string;\n secret?: string;\n /** Label type key whose edit form is open (labels tab). */\n editKey?: string;\n}\n\nfunction tabHref(project: Project, tab: SettingsTab): string {\n return tab === \"general\"\n ? `/projects/${project.slug}/settings`\n : `/projects/${project.slug}/settings/${tab}`;\n}\n\nconst TAB_LABELS: Record<SettingsTab, string> = {\n general: \"General\",\n capture: \"Capture\",\n tests: \"Tests\",\n labels: \"Labels\",\n links: \"Links\",\n design: \"Design\",\n tokens: \"Tokens\",\n webhooks: \"Webhooks\",\n notifications: \"Notifications\",\n members: \"Members\",\n ai: \"AI\",\n};\n\nconst TAB_ICONS: Record<SettingsTab, IconName> = {\n general: \"settings\",\n capture: \"image\",\n tests: \"activity\",\n labels: \"tag\",\n links: \"external-link\",\n design: \"image\",\n tokens: \"key\",\n webhooks: \"webhook\",\n notifications: \"bell\",\n members: \"users\",\n ai: \"sparkles\",\n};\n\n/** Tabs whose pages are `testing` routes: hidden on a hosting-only instance. */\nconst TESTING_TABS: ReadonlySet<SettingsTab> = new Set([\"capture\", \"tests\", \"ai\"]);\n\nconst SETTINGS_TABS: SettingsTab[] = [\n \"general\",\n \"capture\",\n \"tests\",\n \"labels\",\n \"links\",\n \"design\",\n \"tokens\",\n \"webhooks\",\n \"notifications\",\n \"members\",\n \"ai\",\n];\n\n/** A tab shows when its page exists: `testing` tabs need testing, and the AI tab needs AI. */\nfunction settingsTabShown(tab: SettingsTab, data: ProjectSettingsData): boolean {\n if (TESTING_TABS.has(tab) && !testingEnabled()) {\n return false;\n }\n return tab !== \"ai\" || data.ai?.available === true;\n}\n\n/** Project-behaviour tabs (general, tests, AI); undefined for any other tab. */\nfunction renderBehaviourTab(data: ProjectSettingsData, formState?: SettingsFormState): unknown {\n const { project, activeTab } = data;\n if (activeTab === \"general\")\n return renderSettingsGeneral(project, formState, data.isAdmin, data.targets);\n if (activeTab === \"capture\")\n return renderSettingsCapture(project, data.isAdmin, formState, data.targets);\n if (activeTab === \"ai\") return renderSettingsAi(project, data.ai, formState);\n return activeTab === \"tests\" ? renderSettingsTests(project, data.isAdmin, formState) : undefined;\n}\n\n/** Labels, links, tokens, and webhooks tabs; undefined for any other tab. */\nfunction renderCatalogTab(data: ProjectSettingsData, formState?: SettingsFormState): unknown {\n const { project, activeTab, isAdmin } = data;\n if (activeTab === \"labels\")\n return renderSettingsLabels(project, data.labelTypes, isAdmin, formState);\n if (activeTab === \"links\")\n return renderSettingsLinks(project, data.links, data.linkCandidates, isAdmin, formState);\n if (activeTab === \"design\") return renderSettingsDesign(project, data.design, isAdmin, formState);\n if (activeTab === \"tokens\") return renderSettingsTokens(project, data.tokens, isAdmin, formState);\n return activeTab === \"webhooks\"\n ? renderSettingsWebhooks(project, data.webhooks, isAdmin, formState)\n : undefined;\n}\n\nfunction renderActiveTab(data: ProjectSettingsData, formState?: SettingsFormState): unknown {\n const { project, activeTab } = data;\n const behaviour = renderBehaviourTab(data, formState);\n if (behaviour !== undefined) return behaviour;\n const catalog = renderCatalogTab(data, formState);\n if (catalog !== undefined) return catalog;\n if (activeTab === \"notifications\")\n return renderSettingsNotifications(\n project,\n data.notificationChannels,\n data.notifyProviders,\n data.mySubscription,\n data.isAdmin,\n formState,\n data.notificationsPaused,\n );\n if (activeTab === \"members\")\n return renderSettingsMembers(\n project,\n data.members,\n data.groupMappings,\n data.isAdmin,\n formState,\n );\n return null;\n}\n\n/** Project settings shell: tab navigation plus the active settings tab. */\nexport function renderProjectSettingsPage(\n data: ProjectSettingsData,\n formState?: SettingsFormState,\n): RenderedContent {\n const { project, activeTab } = data;\n return (\n <DocumentLayout\n title={`${project.name} · Settings`}\n nav={{\n active: \"settings\",\n projectSlug: project.slug,\n projectName: project.name,\n trail: activeTab === \"general\" ? [] : [{ label: TAB_LABELS[activeTab] }],\n }}\n >\n <PageHeader\n title=\"Project settings\"\n description={\n <>Manage general settings, labels, tokens, webhooks and members for {project.name}.</>\n }\n />\n\n <SubNavLayout\n label=\"Settings sections\"\n items={SETTINGS_TABS.filter((tab) => settingsTabShown(tab, data)).map((tab) => ({\n label: TAB_LABELS[tab],\n href: tabHref(project, tab),\n icon: TAB_ICONS[tab],\n active: activeTab === tab,\n }))}\n >\n {renderActiveTab(data, formState)}\n </SubNavLayout>\n </DocumentLayout>\n );\n}\n"],"mappings":"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"}
@@ -7,8 +7,9 @@ import { Badge, Meta, statusTone } from "../ui/feedback.mjs";
7
7
  import { Card } from "../ui/layout.mjs";
8
8
  import { Thumbnail } from "../ui/thumbnail.mjs";
9
9
  import { RelativeTime } from "../ui/time.mjs";
10
+ import { healthTone } from "./health-panel.mjs";
10
11
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
11
- //#region src/pages/projects-card.tsx
12
+ // src/pages/projects-card.tsx
12
13
  const thumbStrip = css`
13
14
  /* project-thumbs */
14
15
  display: grid;
@@ -156,7 +157,19 @@ function ProjectCard(props) {
156
157
  children: project.gitDefaultBranch
157
158
  })
158
159
  ] }),
159
- /* @__PURE__ */ jsx(LatestBuild, { latest })
160
+ /* @__PURE__ */ jsx(LatestBuild, { latest }),
161
+ summary.health ? /* @__PURE__ */ jsx("a", {
162
+ href: urls.library,
163
+ children: /* @__PURE__ */ jsxs(Badge, {
164
+ tone: healthTone(summary.health.verdict),
165
+ icon: true,
166
+ children: [
167
+ "health: ",
168
+ summary.health.verdict,
169
+ summary.health.score === null ? "" : ` · ${Math.round(summary.health.score)}`
170
+ ]
171
+ })
172
+ }) : null
160
173
  ]
161
174
  }),
162
175
  /* @__PURE__ */ jsxs("div", {
@@ -188,7 +201,6 @@ function ProjectCard(props) {
188
201
  ]
189
202
  });
190
203
  }
191
- //#endregion
192
204
  export { ProjectCard };
193
205
 
194
206
  //# sourceMappingURL=projects-card.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"projects-card.mjs","names":[],"sources":["../../src/pages/projects-card.tsx"],"sourcesContent":["import type { Build, Project, Snapshot } from \"@storyshelf/core/schema\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport {\n Badge,\n Button,\n Card,\n Meta,\n RelativeTime,\n Thumbnail,\n statusTone,\n} from \"../ui/components.tsx\";\nimport { css } from \"../ui/css.ts\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\nconst thumbStrip = css`\n /* project-thumbs */\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: var(--space-2);\n padding: var(--space-3);\n background: var(--accent-wash);\n border-bottom: 1px solid var(--border-subtle);\n`;\n\nconst cardBody = css`\n /* project-body */\n flex: 1;\n display: grid;\n align-content: start;\n gap: var(--space-2);\n padding: var(--space-4);\n`;\n\nconst cardTitleRow = css`\n /* project-title-row */\n display: flex;\n gap: var(--space-2);\n align-items: flex-start;\n justify-content: space-between;\n`;\n\nconst cardTitle = css`\n /* project-title */\n margin: 0;\n font-size: var(--text-lg);\n font-weight: 650;\n letter-spacing: -0.01em;\n & a {\n color: var(--text-primary);\n }\n & a:hover {\n color: var(--accent-fg);\n }\n`;\n\nconst latestRow = css`\n /* project-latest */\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-2);\n align-items: center;\n color: var(--text-secondary);\n font-size: var(--text-sm);\n`;\n\nconst cardFooter = css`\n /* project-footer */\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-2);\n padding: var(--space-3) var(--space-4);\n border-top: 1px solid var(--border-subtle);\n`;\n\n/** What the overview shows about a project's most recent build. */\nexport interface ProjectSummary {\n project: Project;\n latest: Build | null;\n /** Up to three screenshots of the latest build. */\n previews: Snapshot[];\n /** Snapshots in the latest build still waiting for a decision. */\n pending: number;\n}\n\nfunction PendingBadge({\n pending,\n href,\n}: {\n pending: number;\n href: string;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n if (pending === 0) {\n return (\n <Badge tone=\"success\" icon>\n All clear\n </Badge>\n );\n }\n return (\n <a href={href}>\n <Badge tone=\"warning\" icon>\n {pending} to review\n </Badge>\n </a>\n );\n}\n\nfunction LatestBuild({\n latest,\n}: {\n latest: Build | null;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n if (!latest) {\n return <Meta>No builds yet — upload one from CI.</Meta>;\n }\n return (\n <div class={latestRow}>\n <Meta as=\"span\" mono>\n {latest.gitBranch} · {latest.gitSha.slice(0, 7)}\n </Meta>\n <Badge tone={statusTone(latest.status)}>{latest.status}</Badge>\n <RelativeTime value={latest.createdAt} />\n </div>\n );\n}\n\nfunction Previews({\n project,\n latest,\n previews,\n}: Pick<ProjectSummary, \"project\" | \"latest\" | \"previews\">):\n | HtmlEscapedString\n | Promise<HtmlEscapedString> {\n return (\n <div class={thumbStrip}>\n {[0, 1, 2].map((slot): HtmlEscapedString | Promise<HtmlEscapedString> => {\n const snap = previews[slot];\n return (\n <Thumbnail\n key={String(slot)}\n src={\n snap && latest\n ? `/api/v1/projects/${project.slug}/builds/${latest.id}/snapshots/${snap.id}/image`\n : null\n }\n alt={snap ? `${snap.storyTitle} / ${snap.storyName}` : \"\"}\n placeholder=\"\"\n />\n );\n })}\n </div>\n );\n}\n\n/** One project in the overview grid: previews, latest build, review state, actions. */\nexport function ProjectCard(props: {\n summary: ProjectSummary;\n urls: { library: string; settings: string; storybook: string; review: string };\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { summary, urls } = props;\n const { project, latest, previews, pending } = summary;\n return (\n <Card\n fill\n data-filter-item\n data-filter-text={`${project.name} ${project.slug} ${project.gitRepository ?? \"\"}`}\n >\n {previews.length > 0 ? (\n <Previews project={project} latest={latest} previews={previews} />\n ) : null}\n <div class={cardBody}>\n <div class={cardTitleRow}>\n <h2 class={cardTitle}>\n <a href={urls.library}>{project.name}</a>\n </h2>\n {latest ? <PendingBadge pending={pending} href={urls.review} /> : null}\n </div>\n <Meta>\n {project.gitRepository ?? project.slug} · default{\" \"}\n <Badge tone=\"neutral\">{project.gitDefaultBranch}</Badge>\n </Meta>\n <LatestBuild latest={latest} />\n </div>\n <div class={cardFooter}>\n <Button variant=\"secondary\" size=\"sm\" icon=\"image\" href={urls.library}>\n Library\n </Button>\n <Button variant=\"ghost\" size=\"sm\" icon=\"external-link\" href={urls.storybook}>\n Storybook\n </Button>\n <Button variant=\"ghost\" size=\"sm\" icon=\"settings\" href={urls.settings}>\n Settings\n </Button>\n </div>\n </Card>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"projects-card.mjs","names":[],"sources":["../../src/pages/projects-card.tsx"],"sourcesContent":["import type { Build, Project, Snapshot } from \"@storyshelf/core/schema\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport type { HealthBadge } from \"../insights/health-panel.ts\";\nimport {\n Badge,\n Button,\n Card,\n Meta,\n RelativeTime,\n Thumbnail,\n statusTone,\n} from \"../ui/components.tsx\";\nimport { css } from \"../ui/css.ts\";\nimport { healthTone } from \"./health-panel.tsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\nconst thumbStrip = css`\n /* project-thumbs */\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: var(--space-2);\n padding: var(--space-3);\n background: var(--accent-wash);\n border-bottom: 1px solid var(--border-subtle);\n`;\n\nconst cardBody = css`\n /* project-body */\n flex: 1;\n display: grid;\n align-content: start;\n gap: var(--space-2);\n padding: var(--space-4);\n`;\n\nconst cardTitleRow = css`\n /* project-title-row */\n display: flex;\n gap: var(--space-2);\n align-items: flex-start;\n justify-content: space-between;\n`;\n\nconst cardTitle = css`\n /* project-title */\n margin: 0;\n font-size: var(--text-lg);\n font-weight: 650;\n letter-spacing: -0.01em;\n & a {\n color: var(--text-primary);\n }\n & a:hover {\n color: var(--accent-fg);\n }\n`;\n\nconst latestRow = css`\n /* project-latest */\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-2);\n align-items: center;\n color: var(--text-secondary);\n font-size: var(--text-sm);\n`;\n\nconst cardFooter = css`\n /* project-footer */\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-2);\n padding: var(--space-3) var(--space-4);\n border-top: 1px solid var(--border-subtle);\n`;\n\n/** What the overview shows about a project's most recent build. */\nexport interface ProjectSummary {\n project: Project;\n latest: Build | null;\n /** Up to three screenshots of the latest build. */\n previews: Snapshot[];\n /** Snapshots in the latest build still waiting for a decision. */\n pending: number;\n /** AI health digest (last 30 days) when one has finished. */\n health?: HealthBadge | null;\n}\n\nfunction PendingBadge({\n pending,\n href,\n}: {\n pending: number;\n href: string;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n if (pending === 0) {\n return (\n <Badge tone=\"success\" icon>\n All clear\n </Badge>\n );\n }\n return (\n <a href={href}>\n <Badge tone=\"warning\" icon>\n {pending} to review\n </Badge>\n </a>\n );\n}\n\nfunction LatestBuild({\n latest,\n}: {\n latest: Build | null;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n if (!latest) {\n return <Meta>No builds yet — upload one from CI.</Meta>;\n }\n return (\n <div class={latestRow}>\n <Meta as=\"span\" mono>\n {latest.gitBranch} · {latest.gitSha.slice(0, 7)}\n </Meta>\n <Badge tone={statusTone(latest.status)}>{latest.status}</Badge>\n <RelativeTime value={latest.createdAt} />\n </div>\n );\n}\n\nfunction Previews({\n project,\n latest,\n previews,\n}: Pick<ProjectSummary, \"project\" | \"latest\" | \"previews\">):\n | HtmlEscapedString\n | Promise<HtmlEscapedString> {\n return (\n <div class={thumbStrip}>\n {[0, 1, 2].map((slot): HtmlEscapedString | Promise<HtmlEscapedString> => {\n const snap = previews[slot];\n return (\n <Thumbnail\n key={String(slot)}\n src={\n snap && latest\n ? `/api/v1/projects/${project.slug}/builds/${latest.id}/snapshots/${snap.id}/image`\n : null\n }\n alt={snap ? `${snap.storyTitle} / ${snap.storyName}` : \"\"}\n placeholder=\"\"\n />\n );\n })}\n </div>\n );\n}\n\n/** One project in the overview grid: previews, latest build, review state, actions. */\nexport function ProjectCard(props: {\n summary: ProjectSummary;\n urls: { library: string; settings: string; storybook: string; review: string };\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { summary, urls } = props;\n const { project, latest, previews, pending } = summary;\n return (\n <Card\n fill\n data-filter-item\n data-filter-text={`${project.name} ${project.slug} ${project.gitRepository ?? \"\"}`}\n >\n {previews.length > 0 ? (\n <Previews project={project} latest={latest} previews={previews} />\n ) : null}\n <div class={cardBody}>\n <div class={cardTitleRow}>\n <h2 class={cardTitle}>\n <a href={urls.library}>{project.name}</a>\n </h2>\n {latest ? <PendingBadge pending={pending} href={urls.review} /> : null}\n </div>\n <Meta>\n {project.gitRepository ?? project.slug} · default{\" \"}\n <Badge tone=\"neutral\">{project.gitDefaultBranch}</Badge>\n </Meta>\n <LatestBuild latest={latest} />\n {summary.health ? (\n <a href={urls.library}>\n <Badge tone={healthTone(summary.health.verdict)} icon>\n health: {summary.health.verdict}\n {summary.health.score === null ? \"\" : ` · ${Math.round(summary.health.score)}`}\n </Badge>\n </a>\n ) : null}\n </div>\n <div class={cardFooter}>\n <Button variant=\"secondary\" size=\"sm\" icon=\"image\" href={urls.library}>\n Library\n </Button>\n <Button variant=\"ghost\" size=\"sm\" icon=\"external-link\" href={urls.storybook}>\n Storybook\n </Button>\n <Button variant=\"ghost\" size=\"sm\" icon=\"settings\" href={urls.settings}>\n Settings\n </Button>\n </div>\n </Card>\n );\n}\n"],"mappings":"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"}
@@ -10,11 +10,12 @@ import { HStack, VStack } from "../ui/stacks.mjs";
10
10
  import { CodeBlock } from "../ui/code-block.mjs";
11
11
  import { FilterInput } from "../ui/filter-input.mjs";
12
12
  import { DocumentLayout } from "../ui/document.mjs";
13
+ import { loadHealthBadge } from "../insights/health-panel.mjs";
13
14
  import { ProjectCard } from "./projects-card.mjs";
14
15
  import { BuildModel, ProjectModel, SnapshotModel } from "@storyshelf/core/models";
15
16
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
16
17
  import { createUrlBuilder } from "@storyshelf/core/urls";
17
- //#region src/pages/projects.tsx
18
+ // src/pages/projects.tsx
18
19
  const projectGrid = css`
19
20
  /* project-grid */
20
21
  display: grid;
@@ -32,11 +33,13 @@ const setupSummary = css`
32
33
  async function summarize(project) {
33
34
  const { db } = getStore();
34
35
  const [latest] = await new BuildModel(db).list(project.id);
36
+ const health = await loadHealthBadge(project);
35
37
  if (!latest) return {
36
38
  project,
37
39
  latest: null,
38
40
  previews: [],
39
- pending: 0
41
+ pending: 0,
42
+ health
40
43
  };
41
44
  const snapshots = await new SnapshotModel(db).listByBuild(latest.id);
42
45
  const pending = snapshots.filter((snap) => snap.status === "new" || snap.status === "changed");
@@ -44,7 +47,8 @@ async function summarize(project) {
44
47
  project,
45
48
  latest,
46
49
  previews: (pending.length > 0 ? pending : snapshots).slice(0, 3),
47
- pending: pending.length
50
+ pending: pending.length,
51
+ health
48
52
  };
49
53
  }
50
54
  /** Copy-ready steps to get a first build uploaded. */
@@ -123,7 +127,6 @@ async function renderProjectsPage() {
123
127
  })]
124
128
  });
125
129
  }
126
- //#endregion
127
130
  export { renderProjectsPage };
128
131
 
129
132
  //# sourceMappingURL=projects.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"projects.mjs","names":[],"sources":["../../src/pages/projects.tsx"],"sourcesContent":["import { BuildModel, ProjectModel, SnapshotModel } from \"@storyshelf/core/models\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport { createUrlBuilder } from \"@storyshelf/core/urls\";\nimport { getStore } from \"../store.ts\";\nimport {\n Button,\n Card,\n CodeBlock,\n EmptyState,\n FilterInput,\n HStack,\n Meta,\n PageHeader,\n VStack,\n} from \"../ui/components.tsx\";\nimport { css } from \"../ui/css.ts\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\nimport { ProjectCard, type ProjectSummary } from \"./projects-card.tsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\nconst projectGrid = css`\n /* project-grid */\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(min(100%, 320px), 1fr));\n gap: var(--space-4);\n`;\n\nconst setupSummary = css`\n /* setup-summary */\n cursor: pointer;\n font-weight: 650;\n font-size: var(--text-lg);\n letter-spacing: -0.01em;\n`;\n\n/** Latest build, a few previews, and the review backlog for one project. */\nasync function summarize(project: Project): Promise<ProjectSummary> {\n const { db } = getStore();\n const [latest] = await new BuildModel(db).list(project.id);\n if (!latest) {\n return { project, latest: null, previews: [], pending: 0 };\n }\n const snapshots = await new SnapshotModel(db).listByBuild(latest.id);\n const pending = snapshots.filter((snap) => snap.status === \"new\" || snap.status === \"changed\");\n const preferred = pending.length > 0 ? pending : snapshots;\n return { project, latest, previews: preferred.slice(0, 3), pending: pending.length };\n}\n\n/** Copy-ready steps to get a first build uploaded. */\nfunction SetupGuide({ open }: { open: boolean }): ReturnType<typeof Card> {\n return (\n <Card>\n <details open={open}>\n <summary class={setupSummary}>Set up your first upload</summary>\n <VStack>\n <Meta>1. Create a project (writes .storybook/storyshelf.json for later runs).</Meta>\n <CodeBlock code='npx storyshelf create --url http://localhost:3000 --name \"My Storybook\" --token $STORYSHELF_ADMIN_TOKEN' />\n <Meta>\n 2. Generate a token in <strong>Settings → Tokens</strong> and set it in CI.\n </Meta>\n <CodeBlock code=\"export STORYSHELF_TOKEN=<your token>\" />\n <Meta>3. Upload your built Storybook.</Meta>\n <CodeBlock code=\"npx storyshelf upload\" />\n </VStack>\n </details>\n </Card>\n );\n}\n\n/** Projects overview: searchable project cards with review state, plus setup help. */\nexport async function renderProjectsPage(): Promise<RenderedContent> {\n const { db, user, config } = getStore();\n const urls = createUrlBuilder(\"/\", config.publishedBaseDomain);\n const projects = await new ProjectModel(db).list();\n const summaries = await Promise.all(projects.map((project) => summarize(project)));\n const canCreate = !user || user.role === \"admin\" || user.role === \"member\";\n const newProject = canCreate ? (\n <Button variant=\"primary\" icon=\"plus\" href={urls.projectsNew()}>\n New project\n </Button>\n ) : null;\n\n return (\n <DocumentLayout title=\"Projects\" nav={{ active: \"projects\" }}>\n <PageHeader\n title=\"Projects\"\n description=\"Each project is one Storybook. Create a project, then upload builds from CI.\"\n actions={newProject}\n />\n <VStack gap=\"lg\">\n {projects.length > 1 ? (\n <HStack>\n <FilterInput label=\"Filter projects\" placeholder=\"Filter projects…\" />\n </HStack>\n ) : null}\n {projects.length === 0 ? (\n <EmptyState\n title=\"No projects yet\"\n description=\"Create your first project to start visual testing. Projects are free and unlimited.\"\n action={newProject}\n />\n ) : (\n <div class={projectGrid} data-filter-scope>\n {summaries.map((summary) => (\n <ProjectCard\n key={summary.project.id}\n summary={summary}\n urls={{\n library: urls.library(summary.project.slug),\n settings: urls.settings(summary.project.slug),\n storybook: urls.short(summary.project.slug),\n review: summary.latest\n ? urls.buildDiff(summary.project.slug, summary.latest.id)\n : urls.library(summary.project.slug),\n }}\n />\n ))}\n <div data-filter-empty hidden>\n <Meta center>No projects match your filter.</Meta>\n </div>\n </div>\n )}\n <SetupGuide open={projects.length === 0} />\n </VStack>\n </DocumentLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"projects.mjs","names":[],"sources":["../../src/pages/projects.tsx"],"sourcesContent":["import { BuildModel, ProjectModel, SnapshotModel } from \"@storyshelf/core/models\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport { createUrlBuilder } from \"@storyshelf/core/urls\";\nimport { loadHealthBadge } from \"../insights/health-panel.ts\";\nimport { getStore } from \"../store.ts\";\nimport {\n Button,\n Card,\n CodeBlock,\n EmptyState,\n FilterInput,\n HStack,\n Meta,\n PageHeader,\n VStack,\n} from \"../ui/components.tsx\";\nimport { css } from \"../ui/css.ts\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\nimport { ProjectCard, type ProjectSummary } from \"./projects-card.tsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\nconst projectGrid = css`\n /* project-grid */\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(min(100%, 320px), 1fr));\n gap: var(--space-4);\n`;\n\nconst setupSummary = css`\n /* setup-summary */\n cursor: pointer;\n font-weight: 650;\n font-size: var(--text-lg);\n letter-spacing: -0.01em;\n`;\n\n/** Latest build, a few previews, and the review backlog for one project. */\nasync function summarize(project: Project): Promise<ProjectSummary> {\n const { db } = getStore();\n const [latest] = await new BuildModel(db).list(project.id);\n const health = await loadHealthBadge(project);\n if (!latest) {\n return { project, latest: null, previews: [], pending: 0, health };\n }\n const snapshots = await new SnapshotModel(db).listByBuild(latest.id);\n const pending = snapshots.filter((snap) => snap.status === \"new\" || snap.status === \"changed\");\n const preferred = pending.length > 0 ? pending : snapshots;\n return { project, latest, previews: preferred.slice(0, 3), pending: pending.length, health };\n}\n\n/** Copy-ready steps to get a first build uploaded. */\nfunction SetupGuide({ open }: { open: boolean }): ReturnType<typeof Card> {\n return (\n <Card>\n <details open={open}>\n <summary class={setupSummary}>Set up your first upload</summary>\n <VStack>\n <Meta>1. Create a project (writes .storybook/storyshelf.json for later runs).</Meta>\n <CodeBlock code='npx storyshelf create --url http://localhost:3000 --name \"My Storybook\" --token $STORYSHELF_ADMIN_TOKEN' />\n <Meta>\n 2. Generate a token in <strong>Settings → Tokens</strong> and set it in CI.\n </Meta>\n <CodeBlock code=\"export STORYSHELF_TOKEN=<your token>\" />\n <Meta>3. Upload your built Storybook.</Meta>\n <CodeBlock code=\"npx storyshelf upload\" />\n </VStack>\n </details>\n </Card>\n );\n}\n\n/** Projects overview: searchable project cards with review state, plus setup help. */\nexport async function renderProjectsPage(): Promise<RenderedContent> {\n const { db, user, config } = getStore();\n const urls = createUrlBuilder(\"/\", config.publishedBaseDomain);\n const projects = await new ProjectModel(db).list();\n const summaries = await Promise.all(projects.map((project) => summarize(project)));\n const canCreate = !user || user.role === \"admin\" || user.role === \"member\";\n const newProject = canCreate ? (\n <Button variant=\"primary\" icon=\"plus\" href={urls.projectsNew()}>\n New project\n </Button>\n ) : null;\n\n return (\n <DocumentLayout title=\"Projects\" nav={{ active: \"projects\" }}>\n <PageHeader\n title=\"Projects\"\n description=\"Each project is one Storybook. Create a project, then upload builds from CI.\"\n actions={newProject}\n />\n <VStack gap=\"lg\">\n {projects.length > 1 ? (\n <HStack>\n <FilterInput label=\"Filter projects\" placeholder=\"Filter projects…\" />\n </HStack>\n ) : null}\n {projects.length === 0 ? (\n <EmptyState\n title=\"No projects yet\"\n description=\"Create your first project to start visual testing. Projects are free and unlimited.\"\n action={newProject}\n />\n ) : (\n <div class={projectGrid} data-filter-scope>\n {summaries.map((summary) => (\n <ProjectCard\n key={summary.project.id}\n summary={summary}\n urls={{\n library: urls.library(summary.project.slug),\n settings: urls.settings(summary.project.slug),\n storybook: urls.short(summary.project.slug),\n review: summary.latest\n ? urls.buildDiff(summary.project.slug, summary.latest.id)\n : urls.library(summary.project.slug),\n }}\n />\n ))}\n <div data-filter-empty hidden>\n <Meta center>No projects match your filter.</Meta>\n </div>\n </div>\n )}\n <SetupGuide open={projects.length === 0} />\n </VStack>\n </DocumentLayout>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,115 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { getStore } from "../store.mjs";
5
+ import { Button } from "../ui/buttons.mjs";
6
+ import { Badge, EmptyState, Meta } from "../ui/feedback.mjs";
7
+ import { Card, SectionTitle } from "../ui/layout.mjs";
8
+ import { HStack, VStack } from "../ui/stacks.mjs";
9
+ import { Table } from "../ui/table.mjs";
10
+ import { jsx, jsxs } from "hono/jsx/jsx-runtime";
11
+ import { createUrlBuilder } from "@storyshelf/core/urls";
12
+ // src/pages/review-changeset.tsx
13
+ /** What the build changed in the Storybook catalog, against its baseline build. */
14
+ function ChangesetCard(props) {
15
+ const { data } = props;
16
+ const { baseline, changeset } = data.changeset;
17
+ const total = changeset.added.length + changeset.changed.length + changeset.removed.length;
18
+ return /* @__PURE__ */ jsxs(Card, { children: [
19
+ /* @__PURE__ */ jsx(SectionTitle, { children: "Changes in this build" }),
20
+ /* @__PURE__ */ jsxs(Meta, { children: [
21
+ baseline ? `Compared with the accepted build ${baseline.gitSha.slice(0, 7)} on ${baseline.gitBranch}.` : "No accepted build to compare with: every component counts as new.",
22
+ " ",
23
+ "A changed component has different stories, not necessarily a different look."
24
+ ] }),
25
+ total === 0 ? /* @__PURE__ */ jsx(EmptyState, {
26
+ title: "No component changes",
27
+ description: `${changeset.unchanged} components are the same as in the baseline.`
28
+ }) : /* @__PURE__ */ jsx(ChangesetTable, { data })
29
+ ] });
30
+ }
31
+ function ChangesetTable(props) {
32
+ const { data } = props;
33
+ const { changeset } = data.changeset;
34
+ return /* @__PURE__ */ jsxs(VStack, { children: [/* @__PURE__ */ jsxs(HStack, { children: [
35
+ /* @__PURE__ */ jsxs(Badge, {
36
+ tone: "success",
37
+ children: [changeset.added.length, " added"]
38
+ }),
39
+ /* @__PURE__ */ jsxs(Badge, {
40
+ tone: "warning",
41
+ children: [changeset.changed.length, " changed"]
42
+ }),
43
+ /* @__PURE__ */ jsxs(Badge, {
44
+ tone: "danger",
45
+ children: [changeset.removed.length, " removed"]
46
+ }),
47
+ /* @__PURE__ */ jsxs(Meta, {
48
+ as: "span",
49
+ children: [changeset.unchanged, " unchanged"]
50
+ })
51
+ ] }), /* @__PURE__ */ jsx(Table, { children: /* @__PURE__ */ jsxs("table", { children: [
52
+ /* @__PURE__ */ jsx("caption", {
53
+ class: "visually-hidden",
54
+ children: "Components that changed in this build"
55
+ }),
56
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
57
+ /* @__PURE__ */ jsx("th", { children: "Component" }),
58
+ /* @__PURE__ */ jsx("th", { children: "Change" }),
59
+ /* @__PURE__ */ jsx("th", { children: "Stories" }),
60
+ /* @__PURE__ */ jsx("th", { children: /* @__PURE__ */ jsx("span", {
61
+ class: "visually-hidden",
62
+ children: "Links"
63
+ }) })
64
+ ] }) }),
65
+ /* @__PURE__ */ jsxs("tbody", { children: [
66
+ rows(data, "added", changeset.added),
67
+ rows(data, "changed", changeset.changed),
68
+ rows(data, "removed", changeset.removed)
69
+ ] })
70
+ ] }) })] });
71
+ }
72
+ function rows(data, kind, items) {
73
+ return items.map((item) => /* @__PURE__ */ jsx(ChangeRow, {
74
+ data,
75
+ kind,
76
+ item
77
+ }, `${kind}:${item.importPath}:${item.title}`));
78
+ }
79
+ const KIND_TONE = {
80
+ added: "success",
81
+ changed: "warning",
82
+ removed: "danger"
83
+ };
84
+ function ChangeRow(props) {
85
+ const { data, kind, item } = props;
86
+ const { project, build } = data;
87
+ const urls = createUrlBuilder("/", getStore().config.publishedBaseDomain);
88
+ const story = data.firstStory.get(item.importPath);
89
+ return /* @__PURE__ */ jsxs("tr", { children: [
90
+ /* @__PURE__ */ jsxs("td", { children: [/* @__PURE__ */ jsx("strong", { children: item.title }), /* @__PURE__ */ jsx(Meta, {
91
+ as: "div",
92
+ mono: true,
93
+ children: item.importPath
94
+ })] }),
95
+ /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(Badge, {
96
+ tone: KIND_TONE[kind],
97
+ children: kind
98
+ }) }),
99
+ /* @__PURE__ */ jsx("td", { children: item.previousStoryCount !== void 0 && item.previousStoryCount !== item.storyCount ? `${item.previousStoryCount} → ${item.storyCount}` : item.storyCount }),
100
+ /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsxs(HStack, { children: [kind !== "removed" && story ? /* @__PURE__ */ jsx(Button, {
101
+ variant: "ghost",
102
+ size: "sm",
103
+ href: `${urls.storybookBuild(project.slug, build.id)}/?path=/story/${encodeURIComponent(story)}`,
104
+ children: "Story"
105
+ }) : null, item.importPath ? /* @__PURE__ */ jsx(Button, {
106
+ variant: "ghost",
107
+ size: "sm",
108
+ href: urls.component(project.slug, item.importPath),
109
+ children: "Component"
110
+ }) : null] }) })
111
+ ] });
112
+ }
113
+ export { ChangesetCard };
114
+
115
+ //# sourceMappingURL=review-changeset.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"review-changeset.mjs","names":[],"sources":["../../src/pages/review-changeset.tsx"],"sourcesContent":["/** The changeset card (ADR 0013, decision 8): components the build added, removed or changed. */\nimport type { ChangesetComponent } from \"@storyshelf/core/models\";\nimport { createUrlBuilder } from \"@storyshelf/core/urls\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport type { ReviewSectionData } from \"../routers/review-section.handlers.ts\";\nimport { getStore } from \"../store.ts\";\nimport {\n Badge,\n Button,\n Card,\n EmptyState,\n HStack,\n Meta,\n SectionTitle,\n Table,\n VStack,\n} from \"../ui/components.tsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\ntype Rendered = HtmlEscapedString | Promise<HtmlEscapedString>;\n\ntype Kind = \"added\" | \"changed\" | \"removed\";\n\n/** What the build changed in the Storybook catalog, against its baseline build. */\nexport function ChangesetCard(props: { data: ReviewSectionData }): Rendered {\n const { data } = props;\n const { baseline, changeset } = data.changeset;\n const total = changeset.added.length + changeset.changed.length + changeset.removed.length;\n return (\n <Card>\n <SectionTitle>Changes in this build</SectionTitle>\n <Meta>\n {baseline\n ? `Compared with the accepted build ${baseline.gitSha.slice(0, 7)} on ${baseline.gitBranch}.`\n : \"No accepted build to compare with: every component counts as new.\"}{\" \"}\n A changed component has different stories, not necessarily a different look.\n </Meta>\n {total === 0 ? (\n <EmptyState\n title=\"No component changes\"\n description={`${changeset.unchanged} components are the same as in the baseline.`}\n />\n ) : (\n <ChangesetTable data={data} />\n )}\n </Card>\n );\n}\n\nfunction ChangesetTable(props: { data: ReviewSectionData }): Rendered {\n const { data } = props;\n const { changeset } = data.changeset;\n return (\n <VStack>\n <HStack>\n <Badge tone=\"success\">{changeset.added.length} added</Badge>\n <Badge tone=\"warning\">{changeset.changed.length} changed</Badge>\n <Badge tone=\"danger\">{changeset.removed.length} removed</Badge>\n <Meta as=\"span\">{changeset.unchanged} unchanged</Meta>\n </HStack>\n <Table>\n <table>\n <caption class=\"visually-hidden\">Components that changed in this build</caption>\n <thead>\n <tr>\n <th>Component</th>\n <th>Change</th>\n <th>Stories</th>\n <th>\n <span class=\"visually-hidden\">Links</span>\n </th>\n </tr>\n </thead>\n <tbody>\n {rows(data, \"added\", changeset.added)}\n {rows(data, \"changed\", changeset.changed)}\n {rows(data, \"removed\", changeset.removed)}\n </tbody>\n </table>\n </Table>\n </VStack>\n );\n}\n\nfunction rows(\n data: ReviewSectionData,\n kind: Kind,\n items: readonly (ChangesetComponent & { previousStoryCount?: number })[],\n): Rendered[] {\n return items.map((item): Rendered => (\n <ChangeRow\n key={`${kind}:${item.importPath}:${item.title}`}\n data={data}\n kind={kind}\n item={item}\n />\n ));\n}\n\nconst KIND_TONE = { added: \"success\", changed: \"warning\", removed: \"danger\" } as const;\n\nfunction ChangeRow(props: {\n data: ReviewSectionData;\n kind: Kind;\n item: ChangesetComponent & { previousStoryCount?: number };\n}): Rendered {\n const { data, kind, item } = props;\n const { project, build } = data;\n const urls = createUrlBuilder(\"/\", getStore().config.publishedBaseDomain);\n const story = data.firstStory.get(item.importPath);\n return (\n <tr>\n <td>\n <strong>{item.title}</strong>\n <Meta as=\"div\" mono>\n {item.importPath}\n </Meta>\n </td>\n <td>\n <Badge tone={KIND_TONE[kind]}>{kind}</Badge>\n </td>\n <td>\n {item.previousStoryCount !== undefined && item.previousStoryCount !== item.storyCount\n ? `${item.previousStoryCount} → ${item.storyCount}`\n : item.storyCount}\n </td>\n <td>\n <HStack>\n {kind !== \"removed\" && story ? (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n href={`${urls.storybookBuild(project.slug, build.id)}/?path=/story/${encodeURIComponent(story)}`}\n >\n Story\n </Button>\n ) : null}\n {item.importPath ? (\n <Button variant=\"ghost\" size=\"sm\" href={urls.component(project.slug, item.importPath)}>\n Component\n </Button>\n ) : null}\n </HStack>\n </td>\n </tr>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,102 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { Button } from "../ui/buttons.mjs";
5
+ import { Badge, Meta } from "../ui/feedback.mjs";
6
+ import { TextareaField } from "../ui/forms.mjs";
7
+ import { Card, SectionTitle } from "../ui/layout.mjs";
8
+ import { VStack } from "../ui/stacks.mjs";
9
+ import { csrfField } from "../ui/csrf-field.mjs";
10
+ import { CommentCard } from "./build-diff-comments.mjs";
11
+ import { jsx, jsxs } from "hono/jsx/jsx-runtime";
12
+ // src/pages/review-comments.tsx
13
+ /** Comments on the build as a whole and on its stories, with a form for a build comment. */
14
+ function CommentsCard(props) {
15
+ const { data } = props;
16
+ const { project, build, comments, canComment } = data;
17
+ return /* @__PURE__ */ jsxs(Card, { children: [
18
+ /* @__PURE__ */ jsx(SectionTitle, { children: "Comments" }),
19
+ comments.length === 0 ? /* @__PURE__ */ jsxs(Meta, { children: [
20
+ "No comments yet.",
21
+ " ",
22
+ canComment ? "Comment on the build here, or on a story from its component page." : "Developers, approvers and admins can comment."
23
+ ] }) : null,
24
+ /* @__PURE__ */ jsx(VStack, { children: comments.map((comment) => /* @__PURE__ */ jsx(CommentCard, {
25
+ project,
26
+ build,
27
+ comment,
28
+ canReview: canComment,
29
+ anchor: /* @__PURE__ */ jsx(Badge, {
30
+ tone: "neutral",
31
+ children: comment.storyId ? `story ${comment.storyId}` : "build"
32
+ })
33
+ }, comment.id)) }),
34
+ canComment ? /* @__PURE__ */ jsx(CommentForm, {
35
+ project,
36
+ build,
37
+ storyId: void 0
38
+ }) : null
39
+ ] });
40
+ }
41
+ /** The comments of one story in the build a component page shows, with a form to add one. */
42
+ function StoryComments(props) {
43
+ const { project, build, storyId, canComment } = props;
44
+ const comments = props.comments.filter((comment) => comment.storyId === storyId);
45
+ return /* @__PURE__ */ jsxs(Card, {
46
+ id: "story-comments",
47
+ children: [
48
+ /* @__PURE__ */ jsx(SectionTitle, { children: "Comments on this story" }),
49
+ /* @__PURE__ */ jsxs(Meta, { children: [
50
+ "On build ",
51
+ build.gitSha.slice(0, 7),
52
+ "."
53
+ ] }),
54
+ comments.length === 0 ? /* @__PURE__ */ jsx(Meta, { children: "No comments on this story yet." }) : null,
55
+ /* @__PURE__ */ jsx(VStack, { children: comments.map((comment) => /* @__PURE__ */ jsx(CommentCard, {
56
+ project,
57
+ build,
58
+ comment,
59
+ canReview: canComment
60
+ }, comment.id)) }),
61
+ canComment ? /* @__PURE__ */ jsx(CommentForm, {
62
+ project,
63
+ build,
64
+ storyId
65
+ }) : null
66
+ ]
67
+ });
68
+ }
69
+ function CommentForm(props) {
70
+ const { project, build, storyId } = props;
71
+ const url = `/projects/${project.slug}/builds/${build.id}/comments`;
72
+ return /* @__PURE__ */ jsxs("form", {
73
+ method: "post",
74
+ action: url,
75
+ "hx-post": url,
76
+ "hx-target": "body",
77
+ class: "mt-1 max-w-prose",
78
+ children: [csrfField(), /* @__PURE__ */ jsxs(VStack, { children: [
79
+ storyId ? /* @__PURE__ */ jsx("input", {
80
+ type: "hidden",
81
+ name: "storyId",
82
+ value: storyId
83
+ }) : null,
84
+ /* @__PURE__ */ jsx(TextareaField, {
85
+ label: storyId ? "Add story comment" : "Add build comment",
86
+ name: "body",
87
+ id: storyId ? "story-comment-body" : "build-comment-body",
88
+ rows: 3,
89
+ required: true,
90
+ placeholder: storyId ? "Leave a comment on this story…" : "Leave a comment on this build…"
91
+ }),
92
+ /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(Button, {
93
+ variant: "primary",
94
+ type: "submit",
95
+ children: "Comment"
96
+ }) })
97
+ ] })]
98
+ });
99
+ }
100
+ export { CommentsCard, StoryComments };
101
+
102
+ //# sourceMappingURL=review-comments.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"review-comments.mjs","names":[],"sources":["../../src/pages/review-comments.tsx"],"sourcesContent":["/** Build and story comments (ADR 0013, decision 2): the build page card and the story thread. */\nimport type { Build, Comment, Project } from \"@storyshelf/core/schema\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport type { ReviewSectionData } from \"../routers/review-section.handlers.ts\";\nimport {\n Badge,\n Button,\n Card,\n Meta,\n SectionTitle,\n TextareaField,\n VStack,\n} from \"../ui/components.tsx\";\nimport { csrfField } from \"../ui/csrf-field.tsx\";\nimport { CommentCard } from \"./build-diff-comments.tsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\ntype Rendered = HtmlEscapedString | Promise<HtmlEscapedString>;\n\n/** Comments on the build as a whole and on its stories, with a form for a build comment. */\nexport function CommentsCard(props: { data: ReviewSectionData }): Rendered {\n const { data } = props;\n const { project, build, comments, canComment } = data;\n return (\n <Card>\n <SectionTitle>Comments</SectionTitle>\n {comments.length === 0 ? (\n <Meta>\n No comments yet.{\" \"}\n {canComment\n ? \"Comment on the build here, or on a story from its component page.\"\n : \"Developers, approvers and admins can comment.\"}\n </Meta>\n ) : null}\n <VStack>\n {comments.map((comment): Rendered => (\n <CommentCard\n key={comment.id}\n project={project}\n build={build}\n comment={comment}\n canReview={canComment}\n anchor={\n <Badge tone=\"neutral\">{comment.storyId ? `story ${comment.storyId}` : \"build\"}</Badge>\n }\n />\n ))}\n </VStack>\n {canComment ? <CommentForm project={project} build={build} storyId={undefined} /> : null}\n </Card>\n );\n}\n\n/** The comments of one story in the build a component page shows, with a form to add one. */\nexport function StoryComments(props: {\n project: Project;\n build: Build;\n storyId: string;\n comments: readonly Comment[];\n canComment: boolean;\n}): Rendered {\n const { project, build, storyId, canComment } = props;\n const comments = props.comments.filter((comment) => comment.storyId === storyId);\n return (\n <Card id=\"story-comments\">\n <SectionTitle>Comments on this story</SectionTitle>\n <Meta>On build {build.gitSha.slice(0, 7)}.</Meta>\n {comments.length === 0 ? <Meta>No comments on this story yet.</Meta> : null}\n <VStack>\n {comments.map((comment): Rendered => (\n <CommentCard\n key={comment.id}\n project={project}\n build={build}\n comment={comment}\n canReview={canComment}\n />\n ))}\n </VStack>\n {canComment ? <CommentForm project={project} build={build} storyId={storyId} /> : null}\n </Card>\n );\n}\n\nfunction CommentForm(props: {\n project: Project;\n build: Build;\n storyId: string | undefined;\n}): Rendered {\n const { project, build, storyId } = props;\n const url = `/projects/${project.slug}/builds/${build.id}/comments`;\n return (\n <form method=\"post\" action={url} hx-post={url} hx-target=\"body\" class=\"mt-1 max-w-prose\">\n {csrfField()}\n <VStack>\n {storyId ? <input type=\"hidden\" name=\"storyId\" value={storyId} /> : null}\n <TextareaField\n label={storyId ? \"Add story comment\" : \"Add build comment\"}\n name=\"body\"\n id={storyId ? \"story-comment-body\" : \"build-comment-body\"}\n rows={3}\n required\n placeholder={\n storyId ? \"Leave a comment on this story…\" : \"Leave a comment on this build…\"\n }\n />\n <div>\n <Button variant=\"primary\" type=\"submit\">\n Comment\n </Button>\n </div>\n </VStack>\n </form>\n );\n}\n"],"mappings":";;;;;;;;;;;;;AAqBA,SAAgB,aAAa,OAA8C;CACzE,MAAM,EAAE,SAAS;CACjB,MAAM,EAAE,SAAS,OAAO,UAAU,eAAe;CACjD,OACE,qBAAC,MAAD,EAAA,UAAA;EACE,oBAAC,cAAD,EAAA,UAAc,WAAsB,CAAA;EACnC,SAAS,WAAW,IACnB,qBAAC,MAAD,EAAA,UAAA;GAAM;GACa;GAChB,aACG,sEACA;EACA,EAAA,CAAA,IACJ;EACJ,oBAAC,QAAD,EAAA,UACG,SAAS,KAAK,YACb,oBAAC,aAAD;GAEW;GACF;GACE;GACT,WAAW;GACX,QACE,oBAAC,OAAD;IAAO,MAAK;IAAW,UAAA,QAAQ,UAAU,SAAS,QAAQ,YAAY;GAAe,CAAA;EAExF,GARM,QAAQ,EAQd,CACF,EACK,CAAA;EACP,aAAa,oBAAC,aAAD;GAAsB;GAAgB;GAAO,SAAS,KAAA;EAAY,CAAA,IAAI;CAChF,EAAA,CAAA;AAEV;;AAGA,SAAgB,cAAc,OAMjB;CACX,MAAM,EAAE,SAAS,OAAO,SAAS,eAAe;CAChD,MAAM,WAAW,MAAM,SAAS,QAAQ,YAAY,QAAQ,YAAY,OAAO;CAC/E,OACE,qBAAC,MAAD;EAAM,IAAG;EAAT,UAAA;GACE,oBAAC,cAAD,EAAA,UAAc,yBAAoC,CAAA;GAClD,qBAAC,MAAD,EAAA,UAAA;IAAM;IAAU,MAAM,OAAO,MAAM,GAAG,CAAC;IAAE;GAAO,EAAA,CAAA;GAC/C,SAAS,WAAW,IAAI,oBAAC,MAAD,EAAA,UAAM,iCAAoC,CAAA,IAAI;GACvE,oBAAC,QAAD,EAAA,UACG,SAAS,KAAK,YACb,oBAAC,aAAD;IAEW;IACF;IACE;IACT,WAAW;GACZ,GALM,QAAQ,EAKd,CACF,EACK,CAAA;GACP,aAAa,oBAAC,aAAD;IAAsB;IAAgB;IAAgB;GAAU,CAAA,IAAI;EAC9E;;AAEV;AAEA,SAAS,YAAY,OAIR;CACX,MAAM,EAAE,SAAS,OAAO,YAAY;CACpC,MAAM,MAAM,aAAa,QAAQ,KAAK,UAAU,MAAM,GAAG;CACzD,OACE,qBAAC,QAAD;EAAM,QAAO;EAAO,QAAQ;EAAK,WAAS;EAAK,aAAU;EAAO,OAAM;EAAtE,UAAA,CACG,UAAU,GACX,qBAAC,QAAD,EAAA,UAAA;GACG,UAAU,oBAAC,SAAD;IAAO,MAAK;IAAS,MAAK;IAAU,OAAO;GAAU,CAAA,IAAI;GACpE,oBAAC,eAAD;IACE,OAAO,UAAU,sBAAsB;IACvC,MAAK;IACL,IAAI,UAAU,uBAAuB;IACrC,MAAM;IACN,UAAA;IACA,aACE,UAAU,mCAAmC;GAEhD,CAAA;GACD,oBAAC,OAAD,EAAA,UACE,oBAAC,QAAD;IAAQ,SAAQ;IAAU,MAAK;IAAS,UAAA;GAEhC,CAAA,EACL,CAAA;EACC,EAAA,CAAA,CACJ;;AAEV"}