@storyshelf/app 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (364) hide show
  1. package/README.md +42 -4
  2. package/dist/app-types.d.mts +1 -3
  3. package/dist/asset-manifest.mjs +13 -5
  4. package/dist/asset-manifest.mjs.map +1 -1
  5. package/dist/assets/{htmx.min.js → htmx.mjs} +10 -1
  6. package/dist/assets/htmx.mjs.map +1 -0
  7. package/dist/assets/mark.mjs +10 -0
  8. package/dist/assets/mark.mjs.map +1 -0
  9. package/dist/boot-tasks.mjs +80 -0
  10. package/dist/boot-tasks.mjs.map +1 -0
  11. package/dist/build-state-flags.mjs +18 -0
  12. package/dist/build-state-flags.mjs.map +1 -0
  13. package/dist/build-state-setup.mjs +75 -0
  14. package/dist/build-state-setup.mjs.map +1 -0
  15. package/dist/capture-alerts.mjs +46 -0
  16. package/dist/capture-alerts.mjs.map +1 -0
  17. package/dist/capture-process.mjs +59 -0
  18. package/dist/capture-process.mjs.map +1 -0
  19. package/dist/capture-setup.mjs +95 -117
  20. package/dist/capture-setup.mjs.map +1 -1
  21. package/dist/connections.mjs +34 -0
  22. package/dist/connections.mjs.map +1 -0
  23. package/dist/consumer-setup.mjs +58 -0
  24. package/dist/consumer-setup.mjs.map +1 -0
  25. package/dist/credentials.mjs +3 -2
  26. package/dist/credentials.mjs.map +1 -1
  27. package/dist/git-status.mjs +46 -0
  28. package/dist/git-status.mjs.map +1 -0
  29. package/dist/hidden-data.mjs +24 -0
  30. package/dist/hidden-data.mjs.map +1 -0
  31. package/dist/index.d.mts +3 -3
  32. package/dist/index.mjs +202 -124
  33. package/dist/index.mjs.map +1 -1
  34. package/dist/insights/budget.mjs +49 -0
  35. package/dist/insights/budget.mjs.map +1 -0
  36. package/dist/insights/deps.mjs +21 -0
  37. package/dist/insights/deps.mjs.map +1 -0
  38. package/dist/insights/errors.mjs +22 -0
  39. package/dist/insights/errors.mjs.map +1 -0
  40. package/dist/insights/health-panel.mjs +40 -0
  41. package/dist/insights/health-panel.mjs.map +1 -0
  42. package/dist/insights/job.mjs +103 -0
  43. package/dist/insights/job.mjs.map +1 -0
  44. package/dist/insights/load-health.mjs +59 -0
  45. package/dist/insights/load-health.mjs.map +1 -0
  46. package/dist/insights/load-triage.mjs +98 -0
  47. package/dist/insights/load-triage.mjs.map +1 -0
  48. package/dist/insights/panel.mjs +28 -0
  49. package/dist/insights/panel.mjs.map +1 -0
  50. package/dist/insights/request.mjs +113 -0
  51. package/dist/insights/request.mjs.map +1 -0
  52. package/dist/insights/stale.mjs +21 -0
  53. package/dist/insights/stale.mjs.map +1 -0
  54. package/dist/insights/usage-report.mjs +60 -0
  55. package/dist/insights/usage-report.mjs.map +1 -0
  56. package/dist/insights/view.mjs +48 -0
  57. package/dist/insights/view.mjs.map +1 -0
  58. package/dist/job-canceller.mjs +14 -0
  59. package/dist/job-canceller.mjs.map +1 -0
  60. package/dist/job-queue.mjs +51 -0
  61. package/dist/job-queue.mjs.map +1 -0
  62. package/dist/jobs-setup.mjs +132 -0
  63. package/dist/jobs-setup.mjs.map +1 -0
  64. package/dist/lifecycle.mjs +109 -19
  65. package/dist/lifecycle.mjs.map +1 -1
  66. package/dist/middleware/auth-gate.mjs +4 -2
  67. package/dist/middleware/auth-gate.mjs.map +1 -1
  68. package/dist/middleware/rate-limit.mjs +41 -33
  69. package/dist/middleware/rate-limit.mjs.map +1 -1
  70. package/dist/middleware/request-log.mjs +3 -2
  71. package/dist/middleware/request-log.mjs.map +1 -1
  72. package/dist/middleware/require-review.mjs +51 -0
  73. package/dist/middleware/require-review.mjs.map +1 -0
  74. package/dist/middleware/require-testing.mjs +41 -0
  75. package/dist/middleware/require-testing.mjs.map +1 -0
  76. package/dist/middleware/setup-gate.mjs.map +1 -1
  77. package/dist/middleware/store-scope.mjs +12 -2
  78. package/dist/middleware/store-scope.mjs.map +1 -1
  79. package/dist/notifications-pause.mjs +33 -0
  80. package/dist/notifications-pause.mjs.map +1 -0
  81. package/dist/notify.d.mts +2 -1
  82. package/dist/notify.mjs +78 -39
  83. package/dist/notify.mjs.map +1 -1
  84. package/dist/pages/a11y-violations.mjs +116 -0
  85. package/dist/pages/a11y-violations.mjs.map +1 -0
  86. package/dist/pages/admin-ai.mjs +78 -0
  87. package/dist/pages/admin-ai.mjs.map +1 -0
  88. package/dist/pages/admin-system-origin.mjs +123 -0
  89. package/dist/pages/admin-system-origin.mjs.map +1 -0
  90. package/dist/pages/admin-system-targets.mjs +53 -0
  91. package/dist/pages/admin-system-targets.mjs.map +1 -0
  92. package/dist/pages/admin-system.mjs +63 -13
  93. package/dist/pages/admin-system.mjs.map +1 -1
  94. package/dist/pages/build-detail.mjs +104 -76
  95. package/dist/pages/build-detail.mjs.map +1 -1
  96. package/dist/pages/build-diff-actions.mjs +87 -33
  97. package/dist/pages/build-diff-actions.mjs.map +1 -1
  98. package/dist/pages/build-diff-comments.mjs +16 -14
  99. package/dist/pages/build-diff-comments.mjs.map +1 -1
  100. package/dist/pages/build-diff-environment.mjs +82 -0
  101. package/dist/pages/build-diff-environment.mjs.map +1 -0
  102. package/dist/pages/build-diff-header.mjs +22 -4
  103. package/dist/pages/build-diff-header.mjs.map +1 -1
  104. package/dist/pages/build-diff-insight.mjs +113 -0
  105. package/dist/pages/build-diff-insight.mjs.map +1 -0
  106. package/dist/pages/build-diff-model.mjs +55 -10
  107. package/dist/pages/build-diff-model.mjs.map +1 -1
  108. package/dist/pages/build-diff-nav.mjs +28 -12
  109. package/dist/pages/build-diff-nav.mjs.map +1 -1
  110. package/dist/pages/build-diff-shortcuts.mjs +5 -1
  111. package/dist/pages/build-diff-shortcuts.mjs.map +1 -1
  112. package/dist/pages/build-diff-usage.mjs +63 -0
  113. package/dist/pages/build-diff-usage.mjs.map +1 -0
  114. package/dist/pages/build-diff-viewer.mjs +62 -5
  115. package/dist/pages/build-diff-viewer.mjs.map +1 -1
  116. package/dist/pages/build-diff.mjs +34 -13
  117. package/dist/pages/build-diff.mjs.map +1 -1
  118. package/dist/pages/build-tests.mjs +74 -0
  119. package/dist/pages/build-tests.mjs.map +1 -0
  120. package/dist/pages/component-detail.mjs +249 -0
  121. package/dist/pages/component-detail.mjs.map +1 -0
  122. package/dist/pages/component-stories.mjs +168 -0
  123. package/dist/pages/component-stories.mjs.map +1 -0
  124. package/dist/pages/compute-jobs.mjs +13 -26
  125. package/dist/pages/compute-jobs.mjs.map +1 -1
  126. package/dist/pages/design-links.mjs +182 -0
  127. package/dist/pages/design-links.mjs.map +1 -0
  128. package/dist/pages/environment-change.mjs +53 -0
  129. package/dist/pages/environment-change.mjs.map +1 -0
  130. package/dist/pages/health-panel.mjs +126 -0
  131. package/dist/pages/health-panel.mjs.map +1 -0
  132. package/dist/pages/library-grid.mjs +105 -0
  133. package/dist/pages/library-grid.mjs.map +1 -0
  134. package/dist/pages/library-model.mjs +47 -0
  135. package/dist/pages/library-model.mjs.map +1 -0
  136. package/dist/pages/library-snapshots.mjs +130 -0
  137. package/dist/pages/library-snapshots.mjs.map +1 -0
  138. package/dist/pages/library.mjs +112 -218
  139. package/dist/pages/library.mjs.map +1 -1
  140. package/dist/pages/notifications-paused.mjs +22 -0
  141. package/dist/pages/notifications-paused.mjs.map +1 -0
  142. package/dist/pages/profile.mjs +53 -48
  143. package/dist/pages/profile.mjs.map +1 -1
  144. package/dist/pages/project-builds-table.mjs +2 -2
  145. package/dist/pages/project-builds-table.mjs.map +1 -1
  146. package/dist/pages/project-builds.mjs +51 -21
  147. package/dist/pages/project-builds.mjs.map +1 -1
  148. package/dist/pages/project-settings.mjs +51 -12
  149. package/dist/pages/project-settings.mjs.map +1 -1
  150. package/dist/pages/projects-card.mjs +14 -1
  151. package/dist/pages/projects-card.mjs.map +1 -1
  152. package/dist/pages/projects.mjs +6 -2
  153. package/dist/pages/projects.mjs.map +1 -1
  154. package/dist/pages/review-changeset.mjs +116 -0
  155. package/dist/pages/review-changeset.mjs.map +1 -0
  156. package/dist/pages/review-comments.mjs +103 -0
  157. package/dist/pages/review-comments.mjs.map +1 -0
  158. package/dist/pages/review-verdict.mjs +102 -0
  159. package/dist/pages/review-verdict.mjs.map +1 -0
  160. package/dist/pages/settings-ai.mjs +69 -0
  161. package/dist/pages/settings-ai.mjs.map +1 -0
  162. package/dist/pages/settings-capture.mjs +124 -0
  163. package/dist/pages/settings-capture.mjs.map +1 -0
  164. package/dist/pages/settings-design-credential.mjs +99 -0
  165. package/dist/pages/settings-design-credential.mjs.map +1 -0
  166. package/dist/pages/settings-design-links.mjs +179 -0
  167. package/dist/pages/settings-design-links.mjs.map +1 -0
  168. package/dist/pages/settings-design.mjs +48 -0
  169. package/dist/pages/settings-design.mjs.map +1 -0
  170. package/dist/pages/settings-general.mjs +46 -21
  171. package/dist/pages/settings-general.mjs.map +1 -1
  172. package/dist/pages/settings-labels.mjs +43 -2
  173. package/dist/pages/settings-labels.mjs.map +1 -1
  174. package/dist/pages/settings-links.mjs +112 -0
  175. package/dist/pages/settings-links.mjs.map +1 -0
  176. package/dist/pages/settings-notifications-git.mjs +128 -0
  177. package/dist/pages/settings-notifications-git.mjs.map +1 -0
  178. package/dist/pages/settings-notifications.mjs +79 -42
  179. package/dist/pages/settings-notifications.mjs.map +1 -1
  180. package/dist/pages/settings-tests.mjs +33 -2
  181. package/dist/pages/settings-tests.mjs.map +1 -1
  182. package/dist/pages/target-choices.mjs +39 -0
  183. package/dist/pages/target-choices.mjs.map +1 -0
  184. package/dist/publish-setup.mjs +89 -0
  185. package/dist/publish-setup.mjs.map +1 -0
  186. package/dist/retention-timer.mjs +2 -1
  187. package/dist/retention-timer.mjs.map +1 -1
  188. package/dist/review-events.mjs +48 -0
  189. package/dist/review-events.mjs.map +1 -0
  190. package/dist/routers/admin-pages.mjs +45 -3
  191. package/dist/routers/admin-pages.mjs.map +1 -1
  192. package/dist/routers/admin-public-origin.mjs +85 -0
  193. package/dist/routers/admin-public-origin.mjs.map +1 -0
  194. package/dist/routers/assets.mjs +9 -2
  195. package/dist/routers/assets.mjs.map +1 -1
  196. package/dist/routers/auth.mjs +2 -2
  197. package/dist/routers/auth.mjs.map +1 -1
  198. package/dist/routers/baseline-guard.mjs +2 -2
  199. package/dist/routers/baseline-guard.mjs.map +1 -1
  200. package/dist/routers/baselines.handlers.mjs +64 -0
  201. package/dist/routers/baselines.handlers.mjs.map +1 -0
  202. package/dist/routers/baselines.mjs +125 -0
  203. package/dist/routers/baselines.mjs.map +1 -0
  204. package/dist/routers/build-cancel.mjs +68 -0
  205. package/dist/routers/build-cancel.mjs.map +1 -0
  206. package/dist/routers/build-changeset.mjs +70 -0
  207. package/dist/routers/build-changeset.mjs.map +1 -0
  208. package/dist/routers/build-reviews.handlers.mjs +86 -0
  209. package/dist/routers/build-reviews.handlers.mjs.map +1 -0
  210. package/dist/routers/build-reviews.mjs +74 -0
  211. package/dist/routers/build-reviews.mjs.map +1 -0
  212. package/dist/routers/builds.handlers.mjs +46 -55
  213. package/dist/routers/builds.handlers.mjs.map +1 -1
  214. package/dist/routers/builds.mjs +71 -17
  215. package/dist/routers/builds.mjs.map +1 -1
  216. package/dist/routers/capture-config.mjs +35 -0
  217. package/dist/routers/capture-config.mjs.map +1 -0
  218. package/dist/routers/comments.mjs +41 -15
  219. package/dist/routers/comments.mjs.map +1 -1
  220. package/dist/routers/component-catalog.handlers.mjs +71 -0
  221. package/dist/routers/component-catalog.handlers.mjs.map +1 -0
  222. package/dist/routers/component.handlers.mjs +121 -0
  223. package/dist/routers/component.handlers.mjs.map +1 -0
  224. package/dist/routers/connections.mjs +118 -0
  225. package/dist/routers/connections.mjs.map +1 -0
  226. package/dist/routers/design-connect.handlers.mjs +44 -0
  227. package/dist/routers/design-connect.handlers.mjs.map +1 -0
  228. package/dist/routers/design.handlers.mjs +114 -0
  229. package/dist/routers/design.handlers.mjs.map +1 -0
  230. package/dist/routers/health-report.mjs +2 -3
  231. package/dist/routers/health-report.mjs.map +1 -1
  232. package/dist/routers/health.mjs +4 -3
  233. package/dist/routers/health.mjs.map +1 -1
  234. package/dist/routers/helpers.mjs +19 -3
  235. package/dist/routers/helpers.mjs.map +1 -1
  236. package/dist/routers/insights-ui.mjs +64 -0
  237. package/dist/routers/insights-ui.mjs.map +1 -0
  238. package/dist/routers/insights.handlers.mjs +45 -0
  239. package/dist/routers/insights.handlers.mjs.map +1 -0
  240. package/dist/routers/insights.mjs +189 -0
  241. package/dist/routers/insights.mjs.map +1 -0
  242. package/dist/routers/links.handlers.mjs +80 -0
  243. package/dist/routers/links.handlers.mjs.map +1 -0
  244. package/dist/routers/links.mjs +139 -0
  245. package/dist/routers/links.mjs.map +1 -0
  246. package/dist/routers/media.mjs +18 -4
  247. package/dist/routers/media.mjs.map +1 -1
  248. package/dist/routers/members.mjs +2 -1
  249. package/dist/routers/members.mjs.map +1 -1
  250. package/dist/routers/notification-channels.mjs +17 -23
  251. package/dist/routers/notification-channels.mjs.map +1 -1
  252. package/dist/routers/oauth-state.mjs +45 -0
  253. package/dist/routers/oauth-state.mjs.map +1 -0
  254. package/dist/routers/preview-base.mjs +38 -0
  255. package/dist/routers/preview-base.mjs.map +1 -0
  256. package/dist/routers/preview-token.mjs +51 -0
  257. package/dist/routers/preview-token.mjs.map +1 -0
  258. package/dist/routers/profile.mjs +4 -2
  259. package/dist/routers/profile.mjs.map +1 -1
  260. package/dist/routers/projects.mjs +17 -1
  261. package/dist/routers/projects.mjs.map +1 -1
  262. package/dist/routers/publish-upload.mjs +78 -0
  263. package/dist/routers/publish-upload.mjs.map +1 -0
  264. package/dist/routers/published-access.mjs +52 -0
  265. package/dist/routers/published-access.mjs.map +1 -0
  266. package/dist/routers/published-files.mjs +52 -0
  267. package/dist/routers/published-files.mjs.map +1 -0
  268. package/dist/routers/review-forms.mjs +72 -0
  269. package/dist/routers/review-forms.mjs.map +1 -0
  270. package/dist/routers/review-section.handlers.mjs +85 -0
  271. package/dist/routers/review-section.handlers.mjs.map +1 -0
  272. package/dist/routers/review-switch-off.mjs +29 -0
  273. package/dist/routers/review-switch-off.mjs.map +1 -0
  274. package/dist/routers/schemas.mjs +140 -22
  275. package/dist/routers/schemas.mjs.map +1 -1
  276. package/dist/routers/scoped-models.mjs +22 -0
  277. package/dist/routers/scoped-models.mjs.map +1 -0
  278. package/dist/routers/settings-ai.mjs +35 -0
  279. package/dist/routers/settings-ai.mjs.map +1 -0
  280. package/dist/routers/settings-capture.mjs +77 -0
  281. package/dist/routers/settings-capture.mjs.map +1 -0
  282. package/dist/routers/settings-design.mjs +103 -0
  283. package/dist/routers/settings-design.mjs.map +1 -0
  284. package/dist/routers/settings-general.mjs +55 -10
  285. package/dist/routers/settings-general.mjs.map +1 -1
  286. package/dist/routers/settings-git-channel.mjs +77 -0
  287. package/dist/routers/settings-git-channel.mjs.map +1 -0
  288. package/dist/routers/settings-labels.mjs +27 -1
  289. package/dist/routers/settings-labels.mjs.map +1 -1
  290. package/dist/routers/settings-links.mjs +63 -0
  291. package/dist/routers/settings-links.mjs.map +1 -0
  292. package/dist/routers/settings-members.mjs +4 -7
  293. package/dist/routers/settings-members.mjs.map +1 -1
  294. package/dist/routers/settings-notifications.mjs +15 -10
  295. package/dist/routers/settings-notifications.mjs.map +1 -1
  296. package/dist/routers/settings-tokens.mjs +2 -5
  297. package/dist/routers/settings-tokens.mjs.map +1 -1
  298. package/dist/routers/settings-webhooks.mjs +7 -24
  299. package/dist/routers/settings-webhooks.mjs.map +1 -1
  300. package/dist/routers/settings.handlers.mjs +107 -26
  301. package/dist/routers/settings.handlers.mjs.map +1 -1
  302. package/dist/routers/settings.mjs +16 -4
  303. package/dist/routers/settings.mjs.map +1 -1
  304. package/dist/routers/signed-storybook.mjs +53 -0
  305. package/dist/routers/signed-storybook.mjs.map +1 -0
  306. package/dist/routers/snapshot-groups.mjs +63 -0
  307. package/dist/routers/snapshot-groups.mjs.map +1 -0
  308. package/dist/routers/snapshots.mjs +103 -5
  309. package/dist/routers/snapshots.mjs.map +1 -1
  310. package/dist/routers/storybook.mjs +37 -88
  311. package/dist/routers/storybook.mjs.map +1 -1
  312. package/dist/routers/tokens.mjs +2 -5
  313. package/dist/routers/tokens.mjs.map +1 -1
  314. package/dist/routers/ui.mjs +34 -5
  315. package/dist/routers/ui.mjs.map +1 -1
  316. package/dist/routers/webhooks.mjs +5 -11
  317. package/dist/routers/webhooks.mjs.map +1 -1
  318. package/dist/routers/well-known.mjs +11 -4
  319. package/dist/routers/well-known.mjs.map +1 -1
  320. package/dist/routes/api-routes.mjs +93 -0
  321. package/dist/routes/api-routes.mjs.map +1 -0
  322. package/dist/routes/classification.mjs +38 -0
  323. package/dist/routes/classification.mjs.map +1 -0
  324. package/dist/routes/openapi-classes.mjs +22 -0
  325. package/dist/routes/openapi-classes.mjs.map +1 -0
  326. package/dist/routes/page-routes.mjs +104 -0
  327. package/dist/routes/page-routes.mjs.map +1 -0
  328. package/dist/runtime.mjs +18 -13
  329. package/dist/runtime.mjs.map +1 -1
  330. package/dist/serverless.mjs +5 -0
  331. package/dist/store.mjs +19 -1
  332. package/dist/store.mjs.map +1 -1
  333. package/dist/targets.mjs +46 -0
  334. package/dist/targets.mjs.map +1 -0
  335. package/dist/ui/buttons.mjs +16 -3
  336. package/dist/ui/buttons.mjs.map +1 -1
  337. package/dist/ui/design-reference.mjs +97 -0
  338. package/dist/ui/design-reference.mjs.map +1 -0
  339. package/dist/ui/document.mjs +1 -2
  340. package/dist/ui/document.mjs.map +1 -1
  341. package/dist/ui/forms.mjs +6 -5
  342. package/dist/ui/forms.mjs.map +1 -1
  343. package/dist/ui/icons/paths.mjs +1 -0
  344. package/dist/ui/icons/paths.mjs.map +1 -1
  345. package/dist/ui/preview-embed.mjs +82 -0
  346. package/dist/ui/preview-embed.mjs.map +1 -0
  347. package/dist/ui/shell/client-script.mjs +85 -0
  348. package/dist/ui/shell/client-script.mjs.map +1 -1
  349. package/dist/ui/shell/nav.mjs +12 -8
  350. package/dist/ui/shell/nav.mjs.map +1 -1
  351. package/dist/ui/shell/review-script.mjs +8 -0
  352. package/dist/ui/shell/review-script.mjs.map +1 -1
  353. package/dist/ui/shell/sidebar.mjs +2 -2
  354. package/dist/ui/shell/sidebar.mjs.map +1 -1
  355. package/package.json +11 -6
  356. package/dist/asset-paths.mjs +0 -23
  357. package/dist/asset-paths.mjs.map +0 -1
  358. package/dist/assets/mark.svg +0 -1
  359. package/dist/pages/settings-status.mjs +0 -137
  360. package/dist/pages/settings-status.mjs.map +0 -1
  361. package/dist/routers/settings-status.mjs +0 -77
  362. package/dist/routers/settings-status.mjs.map +0 -1
  363. package/dist/routers/status-configs.mjs +0 -108
  364. package/dist/routers/status-configs.mjs.map +0 -1
@@ -0,0 +1,99 @@
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 { Field, FormActions } from "../ui/forms.mjs";
7
+ import { Card, SectionTitle } from "../ui/layout.mjs";
8
+ import { csrfField } from "../ui/csrf-field.mjs";
9
+ import { RemoveForm } from "./settings-design-links.mjs";
10
+ import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
11
+ //#region src/pages/settings-design-credential.tsx
12
+ const STATUS_BADGES = {
13
+ unknown: {
14
+ tone: "neutral",
15
+ text: "Not used yet"
16
+ },
17
+ ok: {
18
+ tone: "success",
19
+ text: "Working"
20
+ },
21
+ unauthorized: {
22
+ tone: "danger",
23
+ text: "Expired or revoked"
24
+ },
25
+ forbidden: {
26
+ tone: "warning",
27
+ text: "No access to a linked file"
28
+ }
29
+ };
30
+ /** Settings card for one provider's project-wide credential (admin only). */
31
+ function CredentialCard(props) {
32
+ const { base, provider, formState } = props;
33
+ const action = `${base}/${provider.kind}/token`;
34
+ return /* @__PURE__ */ jsxs(Card, { children: [
35
+ /* @__PURE__ */ jsx(SectionTitle, { children: provider.credentialLabel }),
36
+ /* @__PURE__ */ jsx(Meta, { children: provider.credentialHelp }),
37
+ provider.credential ? /* @__PURE__ */ jsx(CredentialStatus, {
38
+ action,
39
+ provider,
40
+ credential: provider.credential
41
+ }) : /* @__PURE__ */ jsx(Meta, { children: "No credential set." }),
42
+ /* @__PURE__ */ jsx(CredentialForm, {
43
+ action,
44
+ provider,
45
+ error: formState?.errors?.[`token:${provider.kind}`]
46
+ })
47
+ ] });
48
+ }
49
+ function CredentialStatus(props) {
50
+ const { action, provider, credential } = props;
51
+ const badge = STATUS_BADGES[credential.status];
52
+ const days = credential.daysLeft;
53
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("p", { children: [
54
+ /* @__PURE__ */ jsx(Badge, {
55
+ tone: badge.tone,
56
+ children: badge.text
57
+ }),
58
+ " ",
59
+ days === null ? null : /* @__PURE__ */ jsx(Meta, {
60
+ as: "span",
61
+ children: days >= 0 ? `Rotate within ${days} day${days === 1 ? "" : "s"}` : `Past the ${provider.maxAgeDays}-day cap — rotate now`
62
+ })
63
+ ] }), /* @__PURE__ */ jsx(RemoveForm, {
64
+ action: `${action}/delete`,
65
+ confirm: `Remove the ${provider.name} credential?`,
66
+ label: `Remove ${provider.name} credential`
67
+ })] });
68
+ }
69
+ function CredentialForm(props) {
70
+ const { action, provider } = props;
71
+ const replace = provider.credential !== null;
72
+ return /* @__PURE__ */ jsxs("form", {
73
+ method: "post",
74
+ action,
75
+ "hx-post": action,
76
+ "hx-target": "body",
77
+ children: [
78
+ csrfField(),
79
+ /* @__PURE__ */ jsx(Field, {
80
+ label: replace ? "Replace token" : "Token",
81
+ name: "token",
82
+ type: "password",
83
+ required: true,
84
+ autocomplete: "off",
85
+ placeholder: provider.credentialPlaceholder,
86
+ error: props.error
87
+ }),
88
+ /* @__PURE__ */ jsx(FormActions, { children: /* @__PURE__ */ jsx(Button, {
89
+ variant: "primary",
90
+ type: "submit",
91
+ children: replace ? "Rotate token" : "Save token"
92
+ }) })
93
+ ]
94
+ });
95
+ }
96
+ //#endregion
97
+ export { CredentialCard };
98
+
99
+ //# sourceMappingURL=settings-design-credential.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings-design-credential.mjs","names":[],"sources":["../../src/pages/settings-design-credential.tsx"],"sourcesContent":["import type { HtmlEscapedString } from \"hono/utils/html\";\nimport { Badge, Button, Card, Field, FormActions, Meta, SectionTitle } from \"../ui/components.tsx\";\nimport { csrfField } from \"../ui/csrf-field.tsx\";\nimport { RemoveForm, type DesignFormState } from \"./settings-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/** Last observed outcome of a design credential. */\nexport type DesignCredentialState = \"unknown\" | \"ok\" | \"unauthorized\" | \"forbidden\";\n\n/** A stored credential as shown to admins: never the value. */\nexport interface SettingsDesignCredential {\n status: DesignCredentialState;\n /** Days until the provider's maximum credential age, counted from when it was set or replaced (negative once past); null without a limit. */\n daysLeft: number | null;\n checkedAt: string | null;\n}\n\n/** A configured design provider with its (optional) project credential. */\nexport interface SettingsDesignProvider {\n kind: string;\n name: string;\n credentialLabel: string;\n credentialHelp: string;\n credentialPlaceholder?: string;\n urlHint: string;\n /** Provider's maximum credential lifetime in days, when it has one. */\n maxAgeDays?: number;\n credential: SettingsDesignCredential | null;\n}\n\nconst STATUS_BADGES: Record<\n DesignCredentialState,\n { tone: \"neutral\" | \"success\" | \"warning\" | \"danger\"; text: string }\n> = {\n unknown: { tone: \"neutral\", text: \"Not used yet\" },\n ok: { tone: \"success\", text: \"Working\" },\n unauthorized: { tone: \"danger\", text: \"Expired or revoked\" },\n forbidden: { tone: \"warning\", text: \"No access to a linked file\" },\n};\n\n/** Settings card for one provider's project-wide credential (admin only). */\nexport function CredentialCard(props: {\n base: string;\n provider: SettingsDesignProvider;\n formState?: DesignFormState;\n}): Rendered {\n const { base, provider, formState } = props;\n const action = `${base}/${provider.kind}/token`;\n return (\n <Card>\n <SectionTitle>{provider.credentialLabel}</SectionTitle>\n <Meta>{provider.credentialHelp}</Meta>\n {provider.credential ? (\n <CredentialStatus action={action} provider={provider} credential={provider.credential} />\n ) : (\n <Meta>No credential set.</Meta>\n )}\n <CredentialForm\n action={action}\n provider={provider}\n error={formState?.errors?.[`token:${provider.kind}`]}\n />\n </Card>\n );\n}\n\nfunction CredentialStatus(props: {\n action: string;\n provider: SettingsDesignProvider;\n credential: SettingsDesignCredential;\n}): Rendered {\n const { action, provider, credential } = props;\n const badge = STATUS_BADGES[credential.status];\n const days = credential.daysLeft;\n return (\n <>\n <p>\n <Badge tone={badge.tone}>{badge.text}</Badge>{\" \"}\n {days === null ? null : (\n <Meta as=\"span\">\n {days >= 0\n ? `Rotate within ${days} day${days === 1 ? \"\" : \"s\"}`\n : `Past the ${provider.maxAgeDays}-day cap — rotate now`}\n </Meta>\n )}\n </p>\n <RemoveForm\n action={`${action}/delete`}\n confirm={`Remove the ${provider.name} credential?`}\n label={`Remove ${provider.name} credential`}\n />\n </>\n );\n}\n\nfunction CredentialForm(props: {\n action: string;\n provider: SettingsDesignProvider;\n error?: string;\n}): Rendered {\n const { action, provider } = props;\n const replace = provider.credential !== null;\n return (\n <form method=\"post\" action={action} hx-post={action} hx-target=\"body\">\n {csrfField()}\n <Field\n label={replace ? \"Replace token\" : \"Token\"}\n name=\"token\"\n type=\"password\"\n required\n autocomplete=\"off\"\n placeholder={provider.credentialPlaceholder}\n error={props.error}\n />\n <FormActions>\n <Button variant=\"primary\" type=\"submit\">\n {replace ? \"Rotate token\" : \"Save token\"}\n </Button>\n </FormActions>\n </form>\n );\n}\n"],"mappings":";;;;;;;;;;;AAiCA,MAAM,gBAGF;CACF,SAAS;EAAE,MAAM;EAAW,MAAM;CAAe;CACjD,IAAI;EAAE,MAAM;EAAW,MAAM;CAAU;CACvC,cAAc;EAAE,MAAM;EAAU,MAAM;CAAqB;CAC3D,WAAW;EAAE,MAAM;EAAW,MAAM;CAA6B;AACnE;;AAGA,SAAgB,eAAe,OAIlB;CACX,MAAM,EAAE,MAAM,UAAU,cAAc;CACtC,MAAM,SAAS,GAAG,KAAK,GAAG,SAAS,KAAK;CACxC,OACE,qBAAC,MAAD,EAAA,UAAA;EACE,oBAAC,cAAD,EAAA,UAAe,SAAS,gBAA8B,CAAA;EACtD,oBAAC,MAAD,EAAA,UAAO,SAAS,eAAqB,CAAA;EACpC,SAAS,aACR,oBAAC,kBAAD;GAA0B;GAAkB;GAAU,YAAY,SAAS;EAAa,CAAA,IAExF,oBAAC,MAAD,EAAA,UAAM,qBAAwB,CAAA;EAEhC,oBAAC,gBAAD;GACU;GACE;GACV,OAAO,WAAW,SAAS,SAAS,SAAS;EAC9C,CAAA;CACG,EAAA,CAAA;AAEV;AAEA,SAAS,iBAAiB,OAIb;CACX,MAAM,EAAE,QAAQ,UAAU,eAAe;CACzC,MAAM,QAAQ,cAAc,WAAW;CACvC,MAAM,OAAO,WAAW;CACxB,OACE,qBAAA,UAAA,EAAA,UAAA,CACE,qBAAC,KAAD,EAAA,UAAA;EACE,oBAAC,OAAD;GAAO,MAAM,MAAM;GAAO,UAAA,MAAM;EAAY,CAAA;EAAE;EAC7C,SAAS,OAAO,OACf,oBAAC,MAAD;GAAM,IAAG;GACN,UAAA,QAAQ,IACL,iBAAiB,KAAK,MAAM,SAAS,IAAI,KAAK,QAC9C,YAAY,SAAS,WAAW;EAChC,CAAA;CAEP,EAAA,CAAA,GACH,oBAAC,YAAD;EACE,QAAQ,GAAG,OAAO;EAClB,SAAS,cAAc,SAAS,KAAK;EACrC,OAAO,UAAU,SAAS,KAAK;CAChC,CAAA,CACD,EAAA,CAAA;AAEN;AAEA,SAAS,eAAe,OAIX;CACX,MAAM,EAAE,QAAQ,aAAa;CAC7B,MAAM,UAAU,SAAS,eAAe;CACxC,OACE,qBAAC,QAAD;EAAM,QAAO;EAAe;EAAQ,WAAS;EAAQ,aAAU;EAA/D,UAAA;GACG,UAAU;GACX,oBAAC,OAAD;IACE,OAAO,UAAU,kBAAkB;IACnC,MAAK;IACL,MAAK;IACL,UAAA;IACA,cAAa;IACb,aAAa,SAAS;IACtB,OAAO,MAAM;GACd,CAAA;GACD,oBAAC,aAAD,EAAA,UACE,oBAAC,QAAD;IAAQ,SAAQ;IAAU,MAAK;IAC5B,UAAA,UAAU,iBAAiB;GACtB,CAAA,EACG,CAAA;EACT;;AAEV"}
@@ -0,0 +1,179 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { Button } from "../ui/buttons.mjs";
5
+ import { Meta } from "../ui/feedback.mjs";
6
+ import { Field, FormActions } from "../ui/forms.mjs";
7
+ import { Card, SectionTitle } from "../ui/layout.mjs";
8
+ import { Table } from "../ui/table.mjs";
9
+ import { csrfField } from "../ui/csrf-field.mjs";
10
+ import { jsx, jsxs } from "hono/jsx/jsx-runtime";
11
+ //#region src/pages/settings-design-links.tsx
12
+ function FilesCard(props) {
13
+ const { base, files, isAdmin, formState, urlHint } = props;
14
+ return /* @__PURE__ */ jsxs(Card, { children: [
15
+ /* @__PURE__ */ jsx(SectionTitle, { children: "Design files" }),
16
+ /* @__PURE__ */ jsx(Meta, { children: "Design files for this project. They appear in the library header; individual stories link to a design through their component (see below) or their story parameters." }),
17
+ /* @__PURE__ */ jsx(LinkTable, {
18
+ rows: files.map((file) => ({
19
+ id: file.id,
20
+ key: file.label,
21
+ url: file.url,
22
+ removeAction: isAdmin ? `${base}/files/${file.id}/delete` : void 0
23
+ })),
24
+ keyHeading: "Label",
25
+ confirm: "Remove this design file?"
26
+ }),
27
+ files.length === 0 ? /* @__PURE__ */ jsx(Meta, { children: "No design files linked." }) : null,
28
+ isAdmin ? /* @__PURE__ */ jsx(AddFileForm, {
29
+ base,
30
+ urlHint,
31
+ error: formState?.errors?.["file"]
32
+ }) : null
33
+ ] });
34
+ }
35
+ /** Rows of `key → design link` with an optional remove action. */
36
+ function LinkTable(props) {
37
+ return /* @__PURE__ */ jsx(Table, { children: /* @__PURE__ */ jsxs("table", { children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
38
+ /* @__PURE__ */ jsx("th", { children: props.keyHeading }),
39
+ /* @__PURE__ */ jsx("th", { children: "Design" }),
40
+ /* @__PURE__ */ jsx("th", { children: /* @__PURE__ */ jsx("span", {
41
+ class: "visually-hidden",
42
+ children: "Actions"
43
+ }) })
44
+ ] }) }), /* @__PURE__ */ jsx("tbody", { children: props.rows.map((row) => /* @__PURE__ */ jsxs("tr", { children: [
45
+ /* @__PURE__ */ jsx("td", { children: row.key }),
46
+ /* @__PURE__ */ jsx("td", {
47
+ class: "truncate max-w-cell",
48
+ title: row.url,
49
+ children: /* @__PURE__ */ jsx("a", {
50
+ href: row.url,
51
+ target: "_blank",
52
+ rel: "noopener noreferrer",
53
+ children: row.url
54
+ })
55
+ }),
56
+ /* @__PURE__ */ jsx("td", { children: row.removeAction ? /* @__PURE__ */ jsx(RemoveForm, {
57
+ action: row.removeAction,
58
+ confirm: props.confirm,
59
+ label: `Remove ${row.key}`
60
+ }) : null })
61
+ ] }, row.id)) })] }) });
62
+ }
63
+ function AddFileForm(props) {
64
+ const action = `${props.base}/files`;
65
+ return /* @__PURE__ */ jsxs("form", {
66
+ method: "post",
67
+ action,
68
+ "hx-post": action,
69
+ "hx-target": "body",
70
+ children: [
71
+ csrfField(),
72
+ /* @__PURE__ */ jsxs("div", {
73
+ class: "grid grid--2",
74
+ children: [/* @__PURE__ */ jsx(Field, {
75
+ label: "Label",
76
+ name: "label",
77
+ required: true,
78
+ placeholder: "Design system"
79
+ }), /* @__PURE__ */ jsx(Field, {
80
+ label: "Design URL",
81
+ name: "url",
82
+ type: "url",
83
+ required: true,
84
+ placeholder: props.urlHint,
85
+ error: props.error
86
+ })]
87
+ }),
88
+ /* @__PURE__ */ jsx(FormActions, { children: /* @__PURE__ */ jsx(Button, {
89
+ variant: "primary",
90
+ type: "submit",
91
+ icon: "plus",
92
+ children: "Add design file"
93
+ }) })
94
+ ]
95
+ });
96
+ }
97
+ function ComponentsCard(props) {
98
+ const { base, components, formState, urlHint } = props;
99
+ return /* @__PURE__ */ jsxs(Card, { children: [
100
+ /* @__PURE__ */ jsx(SectionTitle, { children: "Component mapping" }),
101
+ /* @__PURE__ */ jsxs(Meta, { children: [
102
+ "Map a story file (its import path) to a design. A manual mapping overrides the story's own ",
103
+ /* @__PURE__ */ jsx("code", { children: "storyshelf.figma" }),
104
+ " and ",
105
+ /* @__PURE__ */ jsx("code", { children: "design" }),
106
+ " parameters, and survives new builds. Mappings are keyed by the story file path, so update one after a file is renamed or moved."
107
+ ] }),
108
+ /* @__PURE__ */ jsx(LinkTable, {
109
+ rows: components.map((component) => ({
110
+ id: component.id,
111
+ key: component.importPath,
112
+ url: component.url,
113
+ removeAction: `${base}/components/${component.id}/delete`
114
+ })),
115
+ keyHeading: "Story file",
116
+ confirm: "Remove this mapping?"
117
+ }),
118
+ components.length === 0 ? /* @__PURE__ */ jsx(Meta, { children: "No component mappings." }) : null,
119
+ /* @__PURE__ */ jsx(MapComponentForm, {
120
+ base,
121
+ urlHint,
122
+ error: formState?.errors?.["component"]
123
+ })
124
+ ] });
125
+ }
126
+ function MapComponentForm(props) {
127
+ const action = `${props.base}/components`;
128
+ return /* @__PURE__ */ jsxs("form", {
129
+ method: "post",
130
+ action,
131
+ "hx-post": action,
132
+ "hx-target": "body",
133
+ children: [
134
+ csrfField(),
135
+ /* @__PURE__ */ jsxs("div", {
136
+ class: "grid grid--2",
137
+ children: [/* @__PURE__ */ jsx(Field, {
138
+ label: "Story file",
139
+ name: "importPath",
140
+ required: true,
141
+ placeholder: "./src/Button.stories.tsx"
142
+ }), /* @__PURE__ */ jsx(Field, {
143
+ label: "Design URL",
144
+ name: "url",
145
+ type: "url",
146
+ required: true,
147
+ placeholder: props.urlHint,
148
+ error: props.error
149
+ })]
150
+ }),
151
+ /* @__PURE__ */ jsx(FormActions, { children: /* @__PURE__ */ jsx(Button, {
152
+ variant: "primary",
153
+ type: "submit",
154
+ icon: "plus",
155
+ children: "Map component"
156
+ }) })
157
+ ]
158
+ });
159
+ }
160
+ function RemoveForm(props) {
161
+ return /* @__PURE__ */ jsxs("form", {
162
+ method: "post",
163
+ action: props.action,
164
+ "hx-post": props.action,
165
+ "hx-target": "body",
166
+ "hx-confirm": props.confirm,
167
+ children: [csrfField(), /* @__PURE__ */ jsx(Button, {
168
+ variant: "ghost",
169
+ size: "sm",
170
+ type: "submit",
171
+ "aria-label": props.label,
172
+ children: "Remove"
173
+ })]
174
+ });
175
+ }
176
+ //#endregion
177
+ export { ComponentsCard, FilesCard, RemoveForm };
178
+
179
+ //# sourceMappingURL=settings-design-links.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings-design-links.mjs","names":[],"sources":["../../src/pages/settings-design-links.tsx"],"sourcesContent":["import type { HtmlEscapedString } from \"hono/utils/html\";\nimport { Button, Card, Field, FormActions, Meta, SectionTitle, Table } from \"../ui/components.tsx\";\nimport { csrfField } from \"../ui/csrf-field.tsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\ntype Rendered = HtmlEscapedString | Promise<HtmlEscapedString>;\n\n/** Field-level and page-level errors of the Design settings forms. */\nexport type DesignFormState = { errors?: Record<string, string>; globalError?: string };\n\n/** A project-level design file link (shown in the library header). */\nexport interface SettingsDesignFile {\n id: string;\n label: string;\n url: string;\n}\n\n/** A manual `import_path → URL` component mapping. */\nexport interface SettingsDesignComponent {\n id: string;\n importPath: string;\n url: string;\n}\n\nexport function FilesCard(props: {\n base: string;\n files: SettingsDesignFile[];\n isAdmin: boolean;\n formState?: DesignFormState;\n urlHint: string;\n}): Rendered {\n const { base, files, isAdmin, formState, urlHint } = props;\n return (\n <Card>\n <SectionTitle>Design files</SectionTitle>\n <Meta>\n Design files for this project. They appear in the library header; individual stories link to\n a design through their component (see below) or their story parameters.\n </Meta>\n <LinkTable\n rows={files.map((file) => ({\n id: file.id,\n key: file.label,\n url: file.url,\n removeAction: isAdmin ? `${base}/files/${file.id}/delete` : undefined,\n }))}\n keyHeading=\"Label\"\n confirm=\"Remove this design file?\"\n />\n {files.length === 0 ? <Meta>No design files linked.</Meta> : null}\n {isAdmin ? (\n <AddFileForm base={base} urlHint={urlHint} error={formState?.errors?.[\"file\"]} />\n ) : null}\n </Card>\n );\n}\n\n/** Rows of `key → design link` with an optional remove action. */\nfunction LinkTable(props: {\n rows: { id: string; key: string; url: string; removeAction?: string }[];\n keyHeading: string;\n confirm: string;\n}): Rendered {\n return (\n <Table>\n <table>\n <thead>\n <tr>\n <th>{props.keyHeading}</th>\n <th>Design</th>\n <th>\n <span class=\"visually-hidden\">Actions</span>\n </th>\n </tr>\n </thead>\n <tbody>\n {props.rows.map((row): Rendered => (\n <tr key={row.id}>\n <td>{row.key}</td>\n <td class=\"truncate max-w-cell\" title={row.url}>\n <a href={row.url} target=\"_blank\" rel=\"noopener noreferrer\">\n {row.url}\n </a>\n </td>\n <td>\n {row.removeAction ? (\n <RemoveForm\n action={row.removeAction}\n confirm={props.confirm}\n label={`Remove ${row.key}`}\n />\n ) : null}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </Table>\n );\n}\n\nfunction AddFileForm(props: { base: string; urlHint: string; error?: string }): Rendered {\n const action = `${props.base}/files`;\n return (\n <form method=\"post\" action={action} hx-post={action} hx-target=\"body\">\n {csrfField()}\n <div class=\"grid grid--2\">\n <Field label=\"Label\" name=\"label\" required placeholder=\"Design system\" />\n <Field\n label=\"Design URL\"\n name=\"url\"\n type=\"url\"\n required\n placeholder={props.urlHint}\n error={props.error}\n />\n </div>\n <FormActions>\n <Button variant=\"primary\" type=\"submit\" icon=\"plus\">\n Add design file\n </Button>\n </FormActions>\n </form>\n );\n}\n\nexport function ComponentsCard(props: {\n base: string;\n components: SettingsDesignComponent[];\n formState?: DesignFormState;\n urlHint: string;\n}): Rendered {\n const { base, components, formState, urlHint } = props;\n return (\n <Card>\n <SectionTitle>Component mapping</SectionTitle>\n <Meta>\n Map a story file (its import path) to a design. A manual mapping overrides the story&apos;s\n own <code>storyshelf.figma</code> and <code>design</code> parameters, and survives new\n builds. Mappings are keyed by the story file path, so update one after a file is renamed or\n moved.\n </Meta>\n <LinkTable\n rows={components.map((component) => ({\n id: component.id,\n key: component.importPath,\n url: component.url,\n removeAction: `${base}/components/${component.id}/delete`,\n }))}\n keyHeading=\"Story file\"\n confirm=\"Remove this mapping?\"\n />\n {components.length === 0 ? <Meta>No component mappings.</Meta> : null}\n <MapComponentForm base={base} urlHint={urlHint} error={formState?.errors?.[\"component\"]} />\n </Card>\n );\n}\n\nfunction MapComponentForm(props: { base: string; urlHint: string; error?: string }): Rendered {\n const action = `${props.base}/components`;\n return (\n <form method=\"post\" action={action} hx-post={action} hx-target=\"body\">\n {csrfField()}\n <div class=\"grid grid--2\">\n <Field\n label=\"Story file\"\n name=\"importPath\"\n required\n placeholder=\"./src/Button.stories.tsx\"\n />\n <Field\n label=\"Design URL\"\n name=\"url\"\n type=\"url\"\n required\n placeholder={props.urlHint}\n error={props.error}\n />\n </div>\n <FormActions>\n <Button variant=\"primary\" type=\"submit\" icon=\"plus\">\n Map component\n </Button>\n </FormActions>\n </form>\n );\n}\n\nexport function RemoveForm(props: { action: string; confirm: string; label: string }): Rendered {\n return (\n <form\n method=\"post\"\n action={props.action}\n hx-post={props.action}\n hx-target=\"body\"\n hx-confirm={props.confirm}\n >\n {csrfField()}\n <Button variant=\"ghost\" size=\"sm\" type=\"submit\" aria-label={props.label}>\n Remove\n </Button>\n </form>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { Alert, EmptyState } from "../ui/feedback.mjs";
5
+ import { ComponentsCard, FilesCard } from "./settings-design-links.mjs";
6
+ import { CredentialCard } from "./settings-design-credential.mjs";
7
+ import { jsx, jsxs } from "hono/jsx/jsx-runtime";
8
+ //#region src/pages/settings-design.tsx
9
+ /** Design settings tab: project file links, component mapping and provider credentials (admin-only parts gated). */
10
+ function renderSettingsDesign(project, design, isAdmin, formState) {
11
+ const base = `/projects/${project.slug}/settings/design`;
12
+ const urlHint = design.providers.map((provider) => provider.urlHint).join(" or ");
13
+ if (design.providers.length === 0) return /* @__PURE__ */ jsx(EmptyState, {
14
+ title: "No design provider configured",
15
+ description: "Add a design provider (for example @storyshelf/design-figma) to createShelfApp({ design: [...] }) to link components to design files."
16
+ });
17
+ return /* @__PURE__ */ jsxs("div", {
18
+ class: "grid max-w-form",
19
+ children: [
20
+ formState?.globalError ? /* @__PURE__ */ jsx(Alert, {
21
+ tone: "danger",
22
+ children: formState.globalError
23
+ }) : null,
24
+ /* @__PURE__ */ jsx(FilesCard, {
25
+ base,
26
+ files: design.files,
27
+ isAdmin,
28
+ formState,
29
+ urlHint
30
+ }),
31
+ isAdmin ? /* @__PURE__ */ jsx(ComponentsCard, {
32
+ base,
33
+ components: design.components,
34
+ formState,
35
+ urlHint
36
+ }) : null,
37
+ isAdmin ? design.providers.map((provider) => /* @__PURE__ */ jsx(CredentialCard, {
38
+ base,
39
+ provider,
40
+ formState
41
+ }, provider.kind)) : null
42
+ ]
43
+ });
44
+ }
45
+ //#endregion
46
+ export { renderSettingsDesign };
47
+
48
+ //# sourceMappingURL=settings-design.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings-design.mjs","names":[],"sources":["../../src/pages/settings-design.tsx"],"sourcesContent":["import type { Project } from \"@storyshelf/core/schema\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport { Alert, EmptyState } from \"../ui/components.tsx\";\nimport { CredentialCard, type SettingsDesignProvider } from \"./settings-design-credential.tsx\";\nimport {\n ComponentsCard,\n FilesCard,\n type DesignFormState,\n type SettingsDesignComponent,\n type SettingsDesignFile,\n} from \"./settings-design-links.tsx\";\n\nexport type { SettingsDesignComponent, SettingsDesignFile } from \"./settings-design-links.tsx\";\nexport type {\n DesignCredentialState,\n SettingsDesignCredential,\n SettingsDesignProvider,\n} from \"./settings-design-credential.tsx\";\n\n/** Data behind the Design settings tab. */\nexport interface SettingsDesignData {\n files: SettingsDesignFile[];\n components: SettingsDesignComponent[];\n /** Configured design providers; empty when the server wires none. */\n providers: SettingsDesignProvider[];\n}\n\n/** Design settings tab: project file links, component mapping and provider credentials (admin-only parts gated). */\nexport function renderSettingsDesign(\n project: Project,\n design: SettingsDesignData,\n isAdmin: boolean,\n formState?: DesignFormState,\n): unknown {\n const base = `/projects/${project.slug}/settings/design`;\n const urlHint = design.providers.map((provider) => provider.urlHint).join(\" or \");\n if (design.providers.length === 0) {\n return (\n <EmptyState\n title=\"No design provider configured\"\n description=\"Add a design provider (for example @storyshelf/design-figma) to createShelfApp({ design: [...] }) to link components to design files.\"\n />\n );\n }\n return (\n <div class=\"grid max-w-form\">\n {formState?.globalError ? <Alert tone=\"danger\">{formState.globalError}</Alert> : null}\n <FilesCard\n base={base}\n files={design.files}\n isAdmin={isAdmin}\n formState={formState}\n urlHint={urlHint}\n />\n {isAdmin ? (\n <ComponentsCard\n base={base}\n components={design.components}\n formState={formState}\n urlHint={urlHint}\n />\n ) : null}\n {isAdmin\n ? design.providers.map((provider): HtmlEscapedString | Promise<HtmlEscapedString> => (\n <CredentialCard\n key={provider.kind}\n base={base}\n provider={provider}\n formState={formState}\n />\n ))\n : null}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;AA4BA,SAAgB,qBACd,SACA,QACA,SACA,WACS;CACT,MAAM,OAAO,aAAa,QAAQ,KAAK;CACvC,MAAM,UAAU,OAAO,UAAU,KAAK,aAAa,SAAS,OAAO,CAAC,CAAC,KAAK,MAAM;CAChF,IAAI,OAAO,UAAU,WAAW,GAC9B,OACE,oBAAC,YAAD;EACE,OAAM;EACN,aAAY;CACb,CAAA;CAGL,OACE,qBAAC,OAAD;EAAK,OAAM;EAAX,UAAA;GACG,WAAW,cAAc,oBAAC,OAAD;IAAO,MAAK;IAAU,UAAA,UAAU;GAAmB,CAAA,IAAI;GACjF,oBAAC,WAAD;IACQ;IACN,OAAO,OAAO;IACL;IACE;IACF;GACV,CAAA;GACA,UACC,oBAAC,gBAAD;IACQ;IACN,YAAY,OAAO;IACR;IACF;GACV,CAAA,IACC;GACH,UACG,OAAO,UAAU,KAAK,aACpB,oBAAC,gBAAD;IAEQ;IACI;IACC;GACZ,GAJM,SAAS,IAIf,CACF,IACD;EACD;;AAET"}
@@ -3,15 +3,18 @@ import "node:url";
3
3
  import.meta.url;
4
4
  import { Button } from "../ui/buttons.mjs";
5
5
  import { Alert, Meta } from "../ui/feedback.mjs";
6
- import { Field, FormActions, SelectField } from "../ui/forms.mjs";
6
+ import { CheckField, Field, FormActions, SelectField } from "../ui/forms.mjs";
7
7
  import { Card, SectionTitle } from "../ui/layout.mjs";
8
8
  import { csrfField } from "../ui/csrf-field.mjs";
9
+ import { targetChoices } from "./target-choices.mjs";
9
10
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
11
+ import { isCaptureTarget } from "@storyshelf/core/adapter/capture-runner";
10
12
  //#region src/pages/settings-general.tsx
11
13
  /** General settings tab: project metadata, diff thresholds, and danger zone. */
12
- function renderSettingsGeneral(project, formState, isAdmin) {
14
+ function renderSettingsGeneral(project, formState, isAdmin, offered) {
13
15
  const errors = formState?.errors ?? {};
14
16
  const browser = project.browser ?? "chromium";
17
+ const targets = targetChoices(offered, isCaptureTarget(browser) ? [browser] : []);
15
18
  return /* @__PURE__ */ jsxs("div", {
16
19
  class: "grid max-w-form",
17
20
  children: [
@@ -71,25 +74,8 @@ function renderSettingsGeneral(project, formState, isAdmin) {
71
74
  name: "browser",
72
75
  value: browser,
73
76
  disabled: !isAdmin,
74
- options: [
75
- {
76
- value: "chromium",
77
- label: "Chromium (default)"
78
- },
79
- {
80
- value: "firefox",
81
- label: "Firefox"
82
- },
83
- {
84
- value: "webkit",
85
- label: "WebKit (Safari engine)"
86
- },
87
- {
88
- value: "chrome",
89
- label: "Chrome (Chromium engine)"
90
- }
91
- ],
92
- hint: "Rendering engine for captures. Baselines track the browser, so switching starts a fresh baseline. Firefox and WebKit need Playwright system dependencies installed on the server."
77
+ options: targets.choices,
78
+ hint: `Where captures render. Baselines are kept per target, so switching starts a fresh baseline for the new one.${targets.unavailable.length > 0 ? ` Not selectable on this server: ${targets.unavailable.join("; ")}.` : ""}`
93
79
  }),
94
80
  /* @__PURE__ */ jsx(Field, {
95
81
  label: "Public branch regex",
@@ -106,6 +92,45 @@ function renderSettingsGeneral(project, formState, isAdmin) {
106
92
  }) }) : /* @__PURE__ */ jsx(Meta, { children: "You need admin access to edit settings." })
107
93
  ]
108
94
  })] }),
95
+ /* @__PURE__ */ jsxs(Card, { children: [
96
+ /* @__PURE__ */ jsx(SectionTitle, { children: "Review" }),
97
+ /* @__PURE__ */ jsx(Meta, { children: "Comments on builds and stories, and a build-level verdict from approvers and admins. The default branch is never held for review." }),
98
+ /* @__PURE__ */ jsxs("form", {
99
+ method: "post",
100
+ action: `/projects/${project.slug}/settings/review`,
101
+ "hx-post": `/projects/${project.slug}/settings/review`,
102
+ "hx-target": "body",
103
+ "hx-swap": "outerHTML",
104
+ children: [
105
+ csrfField(),
106
+ /* @__PURE__ */ jsx(CheckField, {
107
+ label: "Review builds before they are accepted",
108
+ name: "reviewEnabled",
109
+ value: "true",
110
+ checked: project.reviewEnabled !== false,
111
+ disabled: !isAdmin,
112
+ hint: "With review off, comments and verdicts are hidden, not deleted, and the project's builds waiting for a verdict are accepted when you save."
113
+ }),
114
+ /* @__PURE__ */ jsx(Field, {
115
+ label: "Approvals required",
116
+ name: "reviewApprovals",
117
+ type: "number",
118
+ step: "1",
119
+ min: "1",
120
+ max: "20",
121
+ value: String(project.reviewApprovals ?? 1),
122
+ error: errors["reviewApprovals"],
123
+ hint: "Approvals a build needs when screenshots are not captured. A rejection always rejects the build."
124
+ }),
125
+ isAdmin ? /* @__PURE__ */ jsx(FormActions, { children: /* @__PURE__ */ jsx(Button, {
126
+ variant: "primary",
127
+ type: "submit",
128
+ icon: "check",
129
+ children: "Save review settings"
130
+ }) }) : null
131
+ ]
132
+ })
133
+ ] }),
109
134
  isAdmin ? /* @__PURE__ */ jsxs(Card, {
110
135
  tone: "danger",
111
136
  children: [
@@ -1 +1 @@
1
- {"version":3,"file":"settings-general.mjs","names":[],"sources":["../../src/pages/settings-general.tsx"],"sourcesContent":["import type { Project } from \"@storyshelf/core/schema\";\nimport {\n Alert,\n Button,\n Card,\n Field,\n FormActions,\n Meta,\n SectionTitle,\n SelectField,\n} from \"../ui/components.tsx\";\nimport { csrfField } from \"../ui/csrf-field.tsx\";\n\n/** Form state for the settings tabs (field errors and global error). */\nexport interface SettingsFormState {\n errors?: Record<string, string>;\n globalError?: string;\n}\n\n/** General settings tab: project metadata, diff thresholds, and danger zone. */\nexport function renderSettingsGeneral(\n project: Project,\n formState: SettingsFormState | undefined,\n isAdmin: boolean,\n): unknown {\n const errors = formState?.errors ?? {};\n const browser = project.browser ?? \"chromium\";\n return (\n <div class=\"grid max-w-form\">\n {formState?.globalError ? <Alert tone=\"danger\">{formState.globalError}</Alert> : null}\n <Card>\n <SectionTitle>General</SectionTitle>\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings`}\n hx-post={`/projects/${project.slug}/settings`}\n hx-target=\"body\"\n hx-swap=\"outerHTML\"\n >\n {csrfField()}\n <Field label=\"Name\" name=\"name\" value={project.name} error={errors[\"name\"]} />\n <Field\n label=\"Git repository\"\n name=\"gitRepository\"\n value={project.gitRepository ?? \"\"}\n placeholder=\"owner/repo\"\n />\n <Field label=\"Default branch\" name=\"gitDefaultBranch\" value={project.gitDefaultBranch} />\n <div class=\"grid grid--2\">\n <Field\n label=\"Pixel threshold\"\n name=\"pixelThreshold\"\n type=\"number\"\n step=\"0.01\"\n min=\"0\"\n max=\"1\"\n value={String(project.pixelThreshold)}\n hint=\"Per-pixel color distance 0–1\"\n />\n <Field\n label=\"Max diff ratio\"\n name=\"maxDiffRatio\"\n type=\"number\"\n step=\"0.001\"\n min=\"0\"\n max=\"1\"\n value={String(project.maxDiffRatio)}\n hint=\"Allowed diff ratio 0–1\"\n />\n </div>\n <SelectField\n label=\"Capture browser\"\n name=\"browser\"\n value={browser}\n disabled={!isAdmin}\n options={[\n { value: \"chromium\", label: \"Chromium (default)\" },\n { value: \"firefox\", label: \"Firefox\" },\n { value: \"webkit\", label: \"WebKit (Safari engine)\" },\n { value: \"chrome\", label: \"Chrome (Chromium engine)\" },\n ]}\n hint=\"Rendering engine for captures. Baselines track the browser, so switching starts a fresh baseline. Firefox and WebKit need Playwright system dependencies installed on the server.\"\n />\n <Field\n label=\"Public branch regex\"\n name=\"publicBranchRegex\"\n value={project.publicBranchRegex ?? \"\"}\n placeholder=\"^main$\"\n hint=\"Branches matching this regex are publicly viewable.\"\n />\n {isAdmin ? (\n <FormActions>\n <Button variant=\"primary\" type=\"submit\" icon=\"check\">\n Save changes\n </Button>\n </FormActions>\n ) : (\n <Meta>You need admin access to edit settings.</Meta>\n )}\n </form>\n </Card>\n\n {isAdmin ? (\n <Card tone=\"danger\">\n <SectionTitle level={3} tone=\"danger\">\n Danger zone\n </SectionTitle>\n <Meta>\n Deleting a project removes all builds, snapshots, baselines and tokens. This cannot be\n undone.\n </Meta>\n <form\n method=\"post\"\n action={`/projects/${project.slug}/delete`}\n hx-post={`/projects/${project.slug}/delete`}\n hx-target=\"body\"\n hx-confirm=\"Delete this project? This cannot be undone.\"\n >\n <Button variant=\"danger\" type=\"submit\">\n Delete project\n </Button>\n </form>\n </Card>\n ) : null}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;AAoBA,SAAgB,sBACd,SACA,WACA,SACS;CACT,MAAM,SAAS,WAAW,UAAU,CAAC;CACrC,MAAM,UAAU,QAAQ,WAAW;CACnC,OACE,qBAAC,OAAD;EAAK,OAAM;EAAX,UAAA;GACG,WAAW,cAAc,oBAAC,OAAD;IAAO,MAAK;IAAU,UAAA,UAAU;GAAmB,CAAA,IAAI;GACjF,qBAAC,MAAD,EAAA,UAAA,CACE,oBAAC,cAAD,EAAA,UAAc,UAAqB,CAAA,GACnC,qBAAC,QAAD;IACE,QAAO;IACP,QAAQ,aAAa,QAAQ,KAAK;IAClC,WAAS,aAAa,QAAQ,KAAK;IACnC,aAAU;IACV,WAAQ;IALV,UAAA;KAOG,UAAU;KACX,oBAAC,OAAD;MAAO,OAAM;MAAO,MAAK;MAAO,OAAO,QAAQ;MAAM,OAAO,OAAO;KAAU,CAAA;KAC7E,oBAAC,OAAD;MACE,OAAM;MACN,MAAK;MACL,OAAO,QAAQ,iBAAiB;MAChC,aAAY;KACb,CAAA;KACD,oBAAC,OAAD;MAAO,OAAM;MAAiB,MAAK;MAAmB,OAAO,QAAQ;KAAmB,CAAA;KACxF,qBAAC,OAAD;MAAK,OAAM;MAAX,UAAA,CACE,oBAAC,OAAD;OACE,OAAM;OACN,MAAK;OACL,MAAK;OACL,MAAK;OACL,KAAI;OACJ,KAAI;OACJ,OAAO,OAAO,QAAQ,cAAc;OACpC,MAAK;MACN,CAAA,GACD,oBAAC,OAAD;OACE,OAAM;OACN,MAAK;OACL,MAAK;OACL,MAAK;OACL,KAAI;OACJ,KAAI;OACJ,OAAO,OAAO,QAAQ,YAAY;OAClC,MAAK;MACN,CAAA,CACE;;KACL,oBAAC,aAAD;MACE,OAAM;MACN,MAAK;MACL,OAAO;MACP,UAAU,CAAC;MACX,SAAS;OACP;QAAE,OAAO;QAAY,OAAO;OAAqB;OACjD;QAAE,OAAO;QAAW,OAAO;OAAU;OACrC;QAAE,OAAO;QAAU,OAAO;OAAyB;OACnD;QAAE,OAAO;QAAU,OAAO;OAA2B;MACvD;MACA,MAAK;KACN,CAAA;KACD,oBAAC,OAAD;MACE,OAAM;MACN,MAAK;MACL,OAAO,QAAQ,qBAAqB;MACpC,aAAY;MACZ,MAAK;KACN,CAAA;KACA,UACC,oBAAC,aAAD,EAAA,UACE,oBAAC,QAAD;MAAQ,SAAQ;MAAU,MAAK;MAAS,MAAK;MAAQ,UAAA;KAE7C,CAAA,EACG,CAAA,IAEb,oBAAC,MAAD,EAAA,UAAM,0CAA6C,CAAA;IAEjD;GACF,CAAA,CAAA,EAAA,CAAA;GAEL,UACC,qBAAC,MAAD;IAAM,MAAK;IAAX,UAAA;KACE,oBAAC,cAAD;MAAc,OAAO;MAAG,MAAK;MAAS,UAAA;KAExB,CAAA;KACd,oBAAC,MAAD,EAAA,UAAM,iGAGA,CAAA;KACN,oBAAC,QAAD;MACE,QAAO;MACP,QAAQ,aAAa,QAAQ,KAAK;MAClC,WAAS,aAAa,QAAQ,KAAK;MACnC,aAAU;MACV,cAAW;MAEX,UAAA,oBAAC,QAAD;OAAQ,SAAQ;OAAS,MAAK;OAAS,UAAA;MAE/B,CAAA;KACJ,CAAA;IACF;GACJ,CAAA,IAAA;EACD;;AAET"}
1
+ {"version":3,"file":"settings-general.mjs","names":[],"sources":["../../src/pages/settings-general.tsx"],"sourcesContent":["import { isCaptureTarget, type RunnerTarget } from \"@storyshelf/core/adapter/capture-runner\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport {\n Alert,\n Button,\n Card,\n CheckField,\n Field,\n FormActions,\n Meta,\n SectionTitle,\n SelectField,\n} from \"../ui/components.tsx\";\nimport { csrfField } from \"../ui/csrf-field.tsx\";\nimport { targetChoices } from \"./target-choices.ts\";\n\n/** Form state for the settings tabs (field errors and global error). */\nexport interface SettingsFormState {\n errors?: Record<string, string>;\n globalError?: string;\n}\n\n/** General settings tab: project metadata, diff thresholds, and danger zone. */\nexport function renderSettingsGeneral(\n project: Project,\n formState: SettingsFormState | undefined,\n isAdmin: boolean,\n /** What the runner offers; omitted when it publishes no list. */\n offered?: readonly RunnerTarget[],\n): unknown {\n const errors = formState?.errors ?? {};\n const browser = project.browser ?? \"chromium\";\n const targets = targetChoices(offered, isCaptureTarget(browser) ? [browser] : []);\n return (\n <div class=\"grid max-w-form\">\n {formState?.globalError ? <Alert tone=\"danger\">{formState.globalError}</Alert> : null}\n <Card>\n <SectionTitle>General</SectionTitle>\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings`}\n hx-post={`/projects/${project.slug}/settings`}\n hx-target=\"body\"\n hx-swap=\"outerHTML\"\n >\n {csrfField()}\n <Field label=\"Name\" name=\"name\" value={project.name} error={errors[\"name\"]} />\n <Field\n label=\"Git repository\"\n name=\"gitRepository\"\n value={project.gitRepository ?? \"\"}\n placeholder=\"owner/repo\"\n />\n <Field label=\"Default branch\" name=\"gitDefaultBranch\" value={project.gitDefaultBranch} />\n <div class=\"grid grid--2\">\n <Field\n label=\"Pixel threshold\"\n name=\"pixelThreshold\"\n type=\"number\"\n step=\"0.01\"\n min=\"0\"\n max=\"1\"\n value={String(project.pixelThreshold)}\n hint=\"Per-pixel color distance 0–1\"\n />\n <Field\n label=\"Max diff ratio\"\n name=\"maxDiffRatio\"\n type=\"number\"\n step=\"0.001\"\n min=\"0\"\n max=\"1\"\n value={String(project.maxDiffRatio)}\n hint=\"Allowed diff ratio 0–1\"\n />\n </div>\n <SelectField\n label=\"Capture browser\"\n name=\"browser\"\n value={browser}\n disabled={!isAdmin}\n options={targets.choices}\n hint={`Where captures render. Baselines are kept per target, so switching starts a fresh baseline for the new one.${\n targets.unavailable.length > 0\n ? ` Not selectable on this server: ${targets.unavailable.join(\"; \")}.`\n : \"\"\n }`}\n />\n <Field\n label=\"Public branch regex\"\n name=\"publicBranchRegex\"\n value={project.publicBranchRegex ?? \"\"}\n placeholder=\"^main$\"\n hint=\"Branches matching this regex are publicly viewable.\"\n />\n {isAdmin ? (\n <FormActions>\n <Button variant=\"primary\" type=\"submit\" icon=\"check\">\n Save changes\n </Button>\n </FormActions>\n ) : (\n <Meta>You need admin access to edit settings.</Meta>\n )}\n </form>\n </Card>\n\n <Card>\n <SectionTitle>Review</SectionTitle>\n <Meta>\n Comments on builds and stories, and a build-level verdict from approvers and admins. The\n default branch is never held for review.\n </Meta>\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/review`}\n hx-post={`/projects/${project.slug}/settings/review`}\n hx-target=\"body\"\n hx-swap=\"outerHTML\"\n >\n {csrfField()}\n <CheckField\n label=\"Review builds before they are accepted\"\n name=\"reviewEnabled\"\n value=\"true\"\n checked={project.reviewEnabled !== false}\n disabled={!isAdmin}\n hint=\"With review off, comments and verdicts are hidden, not deleted, and the project's builds waiting for a verdict are accepted when you save.\"\n />\n <Field\n label=\"Approvals required\"\n name=\"reviewApprovals\"\n type=\"number\"\n step=\"1\"\n min=\"1\"\n max=\"20\"\n value={String(project.reviewApprovals ?? 1)}\n error={errors[\"reviewApprovals\"]}\n hint=\"Approvals a build needs when screenshots are not captured. A rejection always rejects the build.\"\n />\n {isAdmin ? (\n <FormActions>\n <Button variant=\"primary\" type=\"submit\" icon=\"check\">\n Save review settings\n </Button>\n </FormActions>\n ) : null}\n </form>\n </Card>\n\n {isAdmin ? (\n <Card tone=\"danger\">\n <SectionTitle level={3} tone=\"danger\">\n Danger zone\n </SectionTitle>\n <Meta>\n Deleting a project removes all builds, snapshots, baselines and tokens. This cannot be\n undone.\n </Meta>\n <form\n method=\"post\"\n action={`/projects/${project.slug}/delete`}\n hx-post={`/projects/${project.slug}/delete`}\n hx-target=\"body\"\n hx-confirm=\"Delete this project? This cannot be undone.\"\n >\n <Button variant=\"danger\" type=\"submit\">\n Delete project\n </Button>\n </form>\n </Card>\n ) : null}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -11,6 +11,7 @@ import { jsx, jsxs } from "hono/jsx/jsx-runtime";
11
11
  //#region src/pages/settings-labels.tsx
12
12
  /** Labels settings tab: label-type table plus the create-type form. */
13
13
  function renderSettingsLabels(project, labelTypes, isAdmin, formState) {
14
+ const editing = isAdmin ? labelTypes.find((item) => item.key === formState?.editKey) : void 0;
14
15
  return /* @__PURE__ */ jsxs("div", {
15
16
  class: "grid max-w-form",
16
17
  children: [
@@ -39,7 +40,13 @@ function renderSettingsLabels(project, labelTypes, isAdmin, formState) {
39
40
  class: "truncate max-w-cell",
40
41
  children: labelType.linkTemplate ?? "—"
41
42
  }),
42
- /* @__PURE__ */ jsx("td", { children: isAdmin && labelType.key !== "persistent" && labelType.key !== "branch" ? /* @__PURE__ */ jsxs("form", {
43
+ /* @__PURE__ */ jsxs("td", { children: [isAdmin && labelType.key !== "persistent" ? /* @__PURE__ */ jsx(Button, {
44
+ variant: "ghost",
45
+ size: "sm",
46
+ href: `/projects/${project.slug}/settings/labels?edit=${labelType.key}`,
47
+ "aria-label": `Edit ${labelType.key}`,
48
+ children: "Edit"
49
+ }) : null, isAdmin && labelType.key !== "persistent" && labelType.key !== "branch" ? /* @__PURE__ */ jsxs("form", {
43
50
  method: "post",
44
51
  action: `/projects/${project.slug}/settings/labels/${labelType.key}/delete`,
45
52
  "hx-post": `/projects/${project.slug}/settings/labels/${labelType.key}/delete`,
@@ -54,10 +61,44 @@ function renderSettingsLabels(project, labelTypes, isAdmin, formState) {
54
61
  }) : /* @__PURE__ */ jsx(Meta, {
55
62
  as: "span",
56
63
  children: "built-in"
57
- }) })
64
+ })] })
58
65
  ] }, labelType.id)) })] }) }),
59
66
  labelTypes.length === 0 ? /* @__PURE__ */ jsx(Meta, { children: "No label types configured." }) : null
60
67
  ] }),
68
+ editing ? /* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsxs(SectionTitle, {
69
+ level: 3,
70
+ children: ["Edit label type: ", editing.key]
71
+ }), /* @__PURE__ */ jsxs("form", {
72
+ method: "post",
73
+ action: `/projects/${project.slug}/settings/labels/${editing.key}`,
74
+ "hx-post": `/projects/${project.slug}/settings/labels/${editing.key}`,
75
+ "hx-target": "body",
76
+ children: [
77
+ csrfField(),
78
+ /* @__PURE__ */ jsx(Field, {
79
+ label: "Name",
80
+ name: "labelName",
81
+ required: true,
82
+ value: editing.name
83
+ }),
84
+ /* @__PURE__ */ jsx(Field, {
85
+ label: "Link template",
86
+ name: "linkTemplate",
87
+ value: editing.linkTemplate ?? "",
88
+ placeholder: "https://jira.example.com/browse/{value}",
89
+ hint: "Placeholders: {value}, {repo}, {branch}. Leave empty for no link."
90
+ }),
91
+ /* @__PURE__ */ jsxs(FormActions, { children: [/* @__PURE__ */ jsx(Button, {
92
+ variant: "primary",
93
+ type: "submit",
94
+ children: "Save label type"
95
+ }), /* @__PURE__ */ jsx(Button, {
96
+ variant: "ghost",
97
+ href: `/projects/${project.slug}/settings/labels`,
98
+ children: "Cancel"
99
+ })] })
100
+ ]
101
+ })] }) : null,
61
102
  isAdmin ? /* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsx(SectionTitle, {
62
103
  level: 3,
63
104
  children: "Create label type"
@@ -1 +1 @@
1
- {"version":3,"file":"settings-labels.mjs","names":[],"sources":["../../src/pages/settings-labels.tsx"],"sourcesContent":["import type { LabelType } from \"@storyshelf/core/schema\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport {\n Alert,\n Badge,\n Button,\n Card,\n Field,\n FormActions,\n Meta,\n SectionTitle,\n Table,\n} from \"../ui/components.tsx\";\nimport { csrfField } from \"../ui/csrf-field.tsx\";\n\n/** Labels settings tab: label-type table plus the create-type form. */\nexport function renderSettingsLabels(\n project: Project,\n labelTypes: LabelType[],\n isAdmin: boolean,\n formState?: { globalError?: string },\n): unknown {\n return (\n <div class=\"grid max-w-form\">\n {formState?.globalError ? <Alert tone=\"danger\">{formState.globalError}</Alert> : null}\n <Card>\n <SectionTitle>Label types</SectionTitle>\n <Meta>\n Labels attach typed values to builds (e.g. pr=123, jira=ABC-123). Values link out via the\n template.\n </Meta>\n <Table>\n <table>\n <thead>\n <tr>\n <th>Key</th>\n <th>Name</th>\n <th>Template</th>\n <th>\n <span class=\"visually-hidden\">Actions</span>\n </th>\n </tr>\n </thead>\n <tbody>\n {labelTypes.map((labelType): HtmlEscapedString | Promise<HtmlEscapedString> => (\n <tr key={labelType.id}>\n <td>\n <Badge tone=\"neutral\">{labelType.key}</Badge>\n </td>\n <td>{labelType.name}</td>\n <td class=\"truncate max-w-cell\">{labelType.linkTemplate ?? \"—\"}</td>\n <td>\n {isAdmin && labelType.key !== \"persistent\" && labelType.key !== \"branch\" ? (\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/labels/${labelType.key}/delete`}\n hx-post={`/projects/${project.slug}/settings/labels/${labelType.key}/delete`}\n hx-target=\"body\"\n hx-confirm=\"Delete this label type?\"\n >\n {csrfField()}\n <Button\n variant=\"ghost\"\n type=\"submit\"\n aria-label={`Delete ${labelType.key}`}\n >\n Delete\n </Button>\n </form>\n ) : (\n <Meta as=\"span\">built-in</Meta>\n )}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </Table>\n {labelTypes.length === 0 ? <Meta>No label types configured.</Meta> : null}\n </Card>\n\n {isAdmin ? (\n <Card>\n <SectionTitle level={3}>Create label type</SectionTitle>\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/labels`}\n hx-post={`/projects/${project.slug}/settings/labels`}\n hx-target=\"body\"\n >\n {csrfField()}\n <div class=\"grid grid--2\">\n <Field\n label=\"Key\"\n name=\"key\"\n required\n placeholder=\"jira\"\n pattern=\"^[a-z0-9_-]+$\"\n hint=\"Lowercase, no spaces.\"\n />\n <Field label=\"Name\" name=\"labelName\" required placeholder=\"Jira issue\" />\n </div>\n <Field\n label=\"Link template\"\n name=\"linkTemplate\"\n placeholder=\"https://jira.example.com/browse/{value}\"\n hint=\"Use {value} placeholder. Optional.\"\n />\n <FormActions>\n <Button variant=\"primary\" type=\"submit\" icon=\"plus\">\n Add label type\n </Button>\n </FormActions>\n </form>\n </Card>\n ) : null}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;AAiBA,SAAgB,qBACd,SACA,YACA,SACA,WACS;CACT,OACE,qBAAC,OAAD;EAAK,OAAM;EAAX,UAAA;GACG,WAAW,cAAc,oBAAC,OAAD;IAAO,MAAK;IAAU,UAAA,UAAU;GAAmB,CAAA,IAAI;GACjF,qBAAC,MAAD,EAAA,UAAA;IACE,oBAAC,cAAD,EAAA,UAAc,cAAyB,CAAA;IACvC,oBAAC,MAAD,EAAA,UAAM,sGAGA,CAAA;IACN,oBAAC,OAAD,EAAA,UACE,qBAAC,SAAD,EAAA,UAAA,CACE,oBAAC,SAAD,EAAA,UACE,qBAAC,MAAD,EAAA,UAAA;KACE,oBAAC,MAAD,EAAA,UAAI,MAAO,CAAA;KACX,oBAAC,MAAD,EAAA,UAAI,OAAQ,CAAA;KACZ,oBAAC,MAAD,EAAA,UAAI,WAAY,CAAA;KAChB,oBAAC,MAAD,EAAA,UACE,oBAAC,QAAD;MAAM,OAAM;MAAkB,UAAA;KAAa,CAAA,EACzC,CAAA;IACF,EAAA,CAAA,EACC,CAAA,GACP,oBAAC,SAAD,EAAA,UACG,WAAW,KAAK,cACf,qBAAC,MAAD,EAAA,UAAA;KACE,oBAAC,MAAD,EAAA,UACE,oBAAC,OAAD;MAAO,MAAK;MAAW,UAAA,UAAU;KAAW,CAAA,EAC1C,CAAA;KACJ,oBAAC,MAAD,EAAA,UAAK,UAAU,KAAS,CAAA;KACxB,oBAAC,MAAD;MAAI,OAAM;MAAuB,UAAA,UAAU,gBAAgB;KAAQ,CAAA;KACnE,oBAAC,MAAD,EAAA,UACG,WAAW,UAAU,QAAQ,gBAAgB,UAAU,QAAQ,WAC9D,qBAAC,QAAD;MACE,QAAO;MACP,QAAQ,aAAa,QAAQ,KAAK,mBAAmB,UAAU,IAAI;MACnE,WAAS,aAAa,QAAQ,KAAK,mBAAmB,UAAU,IAAI;MACpE,aAAU;MACV,cAAW;MALb,UAAA,CAOG,UAAU,GACX,oBAAC,QAAD;OACE,SAAQ;OACR,MAAK;OACL,cAAY,UAAU,UAAU;OACjC,UAAA;MAEO,CAAA,CACJ;KAEN,CAAA,IAAA,oBAAC,MAAD;MAAM,IAAG;MAAO,UAAA;KAAc,CAAA,EAE9B,CAAA;IACF,EAAA,GA5BK,UAAU,EA4Bf,CACL,EACI,CAAA,CACF,EAAA,CAAA,EACF,CAAA;IACN,WAAW,WAAW,IAAI,oBAAC,MAAD,EAAA,UAAM,6BAAgC,CAAA,IAAI;GACjE,EAAA,CAAA;GAEL,UACC,qBAAC,MAAD,EAAA,UAAA,CACE,oBAAC,cAAD;IAAc,OAAO;IAAG,UAAA;GAA+B,CAAA,GACvD,qBAAC,QAAD;IACE,QAAO;IACP,QAAQ,aAAa,QAAQ,KAAK;IAClC,WAAS,aAAa,QAAQ,KAAK;IACnC,aAAU;IAJZ,UAAA;KAMG,UAAU;KACX,qBAAC,OAAD;MAAK,OAAM;MAAX,UAAA,CACE,oBAAC,OAAD;OACE,OAAM;OACN,MAAK;OACL,UAAA;OACA,aAAY;OACZ,SAAQ;OACR,MAAK;MACN,CAAA,GACD,oBAAC,OAAD;OAAO,OAAM;OAAO,MAAK;OAAY,UAAA;OAAS,aAAY;MAAc,CAAA,CACrE;;KACL,oBAAC,OAAD;MACE,OAAM;MACN,MAAK;MACL,aAAY;MACZ,MAAK;KACN,CAAA;KACD,oBAAC,aAAD,EAAA,UACE,oBAAC,QAAD;MAAQ,SAAQ;MAAU,MAAK;MAAS,MAAK;MAAO,UAAA;KAE5C,CAAA,EACG,CAAA;IACT;GACF,CAAA,CAAA,EAAA,CAAA,IACJ;EACD;;AAET"}
1
+ {"version":3,"file":"settings-labels.mjs","names":[],"sources":["../../src/pages/settings-labels.tsx"],"sourcesContent":["import type { LabelType } from \"@storyshelf/core/schema\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport {\n Alert,\n Badge,\n Button,\n Card,\n Field,\n FormActions,\n Meta,\n SectionTitle,\n Table,\n} from \"../ui/components.tsx\";\nimport { csrfField } from \"../ui/csrf-field.tsx\";\n\n/** Labels settings tab: label-type table plus the create-type form. */\nexport function renderSettingsLabels(\n project: Project,\n labelTypes: LabelType[],\n isAdmin: boolean,\n formState?: { globalError?: string; editKey?: string },\n): unknown {\n const editing = isAdmin ? labelTypes.find((item) => item.key === formState?.editKey) : undefined;\n return (\n <div class=\"grid max-w-form\">\n {formState?.globalError ? <Alert tone=\"danger\">{formState.globalError}</Alert> : null}\n <Card>\n <SectionTitle>Label types</SectionTitle>\n <Meta>\n Labels attach typed values to builds (e.g. pr=123, jira=ABC-123). Values link out via the\n template.\n </Meta>\n <Table>\n <table>\n <thead>\n <tr>\n <th>Key</th>\n <th>Name</th>\n <th>Template</th>\n <th>\n <span class=\"visually-hidden\">Actions</span>\n </th>\n </tr>\n </thead>\n <tbody>\n {labelTypes.map((labelType): HtmlEscapedString | Promise<HtmlEscapedString> => (\n <tr key={labelType.id}>\n <td>\n <Badge tone=\"neutral\">{labelType.key}</Badge>\n </td>\n <td>{labelType.name}</td>\n <td class=\"truncate max-w-cell\">{labelType.linkTemplate ?? \"—\"}</td>\n <td>\n {isAdmin && labelType.key !== \"persistent\" ? (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n href={`/projects/${project.slug}/settings/labels?edit=${labelType.key}`}\n aria-label={`Edit ${labelType.key}`}\n >\n Edit\n </Button>\n ) : null}\n {isAdmin && labelType.key !== \"persistent\" && labelType.key !== \"branch\" ? (\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/labels/${labelType.key}/delete`}\n hx-post={`/projects/${project.slug}/settings/labels/${labelType.key}/delete`}\n hx-target=\"body\"\n hx-confirm=\"Delete this label type?\"\n >\n {csrfField()}\n <Button\n variant=\"ghost\"\n type=\"submit\"\n aria-label={`Delete ${labelType.key}`}\n >\n Delete\n </Button>\n </form>\n ) : (\n <Meta as=\"span\">built-in</Meta>\n )}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </Table>\n {labelTypes.length === 0 ? <Meta>No label types configured.</Meta> : null}\n </Card>\n\n {editing ? (\n <Card>\n <SectionTitle level={3}>Edit label type: {editing.key}</SectionTitle>\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/labels/${editing.key}`}\n hx-post={`/projects/${project.slug}/settings/labels/${editing.key}`}\n hx-target=\"body\"\n >\n {csrfField()}\n <Field label=\"Name\" name=\"labelName\" required value={editing.name} />\n <Field\n label=\"Link template\"\n name=\"linkTemplate\"\n value={editing.linkTemplate ?? \"\"}\n placeholder=\"https://jira.example.com/browse/{value}\"\n hint=\"Placeholders: {value}, {repo}, {branch}. Leave empty for no link.\"\n />\n <FormActions>\n <Button variant=\"primary\" type=\"submit\">\n Save label type\n </Button>\n <Button variant=\"ghost\" href={`/projects/${project.slug}/settings/labels`}>\n Cancel\n </Button>\n </FormActions>\n </form>\n </Card>\n ) : null}\n\n {isAdmin ? (\n <Card>\n <SectionTitle level={3}>Create label type</SectionTitle>\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/labels`}\n hx-post={`/projects/${project.slug}/settings/labels`}\n hx-target=\"body\"\n >\n {csrfField()}\n <div class=\"grid grid--2\">\n <Field\n label=\"Key\"\n name=\"key\"\n required\n placeholder=\"jira\"\n pattern=\"^[a-z0-9_-]+$\"\n hint=\"Lowercase, no spaces.\"\n />\n <Field label=\"Name\" name=\"labelName\" required placeholder=\"Jira issue\" />\n </div>\n <Field\n label=\"Link template\"\n name=\"linkTemplate\"\n placeholder=\"https://jira.example.com/browse/{value}\"\n hint=\"Use {value} placeholder. Optional.\"\n />\n <FormActions>\n <Button variant=\"primary\" type=\"submit\" icon=\"plus\">\n Add label type\n </Button>\n </FormActions>\n </form>\n </Card>\n ) : null}\n </div>\n );\n}\n"],"mappings":"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"}