@storyshelf/app 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (364) hide show
  1. package/README.md +42 -4
  2. package/dist/app-types.d.mts +1 -3
  3. package/dist/asset-manifest.mjs +13 -5
  4. package/dist/asset-manifest.mjs.map +1 -1
  5. package/dist/assets/{htmx.min.js → htmx.mjs} +10 -1
  6. package/dist/assets/htmx.mjs.map +1 -0
  7. package/dist/assets/mark.mjs +10 -0
  8. package/dist/assets/mark.mjs.map +1 -0
  9. package/dist/boot-tasks.mjs +80 -0
  10. package/dist/boot-tasks.mjs.map +1 -0
  11. package/dist/build-state-flags.mjs +18 -0
  12. package/dist/build-state-flags.mjs.map +1 -0
  13. package/dist/build-state-setup.mjs +75 -0
  14. package/dist/build-state-setup.mjs.map +1 -0
  15. package/dist/capture-alerts.mjs +46 -0
  16. package/dist/capture-alerts.mjs.map +1 -0
  17. package/dist/capture-process.mjs +59 -0
  18. package/dist/capture-process.mjs.map +1 -0
  19. package/dist/capture-setup.mjs +95 -117
  20. package/dist/capture-setup.mjs.map +1 -1
  21. package/dist/connections.mjs +34 -0
  22. package/dist/connections.mjs.map +1 -0
  23. package/dist/consumer-setup.mjs +58 -0
  24. package/dist/consumer-setup.mjs.map +1 -0
  25. package/dist/credentials.mjs +3 -2
  26. package/dist/credentials.mjs.map +1 -1
  27. package/dist/git-status.mjs +46 -0
  28. package/dist/git-status.mjs.map +1 -0
  29. package/dist/hidden-data.mjs +24 -0
  30. package/dist/hidden-data.mjs.map +1 -0
  31. package/dist/index.d.mts +3 -3
  32. package/dist/index.mjs +202 -124
  33. package/dist/index.mjs.map +1 -1
  34. package/dist/insights/budget.mjs +49 -0
  35. package/dist/insights/budget.mjs.map +1 -0
  36. package/dist/insights/deps.mjs +21 -0
  37. package/dist/insights/deps.mjs.map +1 -0
  38. package/dist/insights/errors.mjs +22 -0
  39. package/dist/insights/errors.mjs.map +1 -0
  40. package/dist/insights/health-panel.mjs +40 -0
  41. package/dist/insights/health-panel.mjs.map +1 -0
  42. package/dist/insights/job.mjs +103 -0
  43. package/dist/insights/job.mjs.map +1 -0
  44. package/dist/insights/load-health.mjs +59 -0
  45. package/dist/insights/load-health.mjs.map +1 -0
  46. package/dist/insights/load-triage.mjs +98 -0
  47. package/dist/insights/load-triage.mjs.map +1 -0
  48. package/dist/insights/panel.mjs +28 -0
  49. package/dist/insights/panel.mjs.map +1 -0
  50. package/dist/insights/request.mjs +113 -0
  51. package/dist/insights/request.mjs.map +1 -0
  52. package/dist/insights/stale.mjs +21 -0
  53. package/dist/insights/stale.mjs.map +1 -0
  54. package/dist/insights/usage-report.mjs +60 -0
  55. package/dist/insights/usage-report.mjs.map +1 -0
  56. package/dist/insights/view.mjs +48 -0
  57. package/dist/insights/view.mjs.map +1 -0
  58. package/dist/job-canceller.mjs +14 -0
  59. package/dist/job-canceller.mjs.map +1 -0
  60. package/dist/job-queue.mjs +51 -0
  61. package/dist/job-queue.mjs.map +1 -0
  62. package/dist/jobs-setup.mjs +132 -0
  63. package/dist/jobs-setup.mjs.map +1 -0
  64. package/dist/lifecycle.mjs +109 -19
  65. package/dist/lifecycle.mjs.map +1 -1
  66. package/dist/middleware/auth-gate.mjs +4 -2
  67. package/dist/middleware/auth-gate.mjs.map +1 -1
  68. package/dist/middleware/rate-limit.mjs +41 -33
  69. package/dist/middleware/rate-limit.mjs.map +1 -1
  70. package/dist/middleware/request-log.mjs +3 -2
  71. package/dist/middleware/request-log.mjs.map +1 -1
  72. package/dist/middleware/require-review.mjs +51 -0
  73. package/dist/middleware/require-review.mjs.map +1 -0
  74. package/dist/middleware/require-testing.mjs +41 -0
  75. package/dist/middleware/require-testing.mjs.map +1 -0
  76. package/dist/middleware/setup-gate.mjs.map +1 -1
  77. package/dist/middleware/store-scope.mjs +12 -2
  78. package/dist/middleware/store-scope.mjs.map +1 -1
  79. package/dist/notifications-pause.mjs +33 -0
  80. package/dist/notifications-pause.mjs.map +1 -0
  81. package/dist/notify.d.mts +2 -1
  82. package/dist/notify.mjs +78 -39
  83. package/dist/notify.mjs.map +1 -1
  84. package/dist/pages/a11y-violations.mjs +116 -0
  85. package/dist/pages/a11y-violations.mjs.map +1 -0
  86. package/dist/pages/admin-ai.mjs +78 -0
  87. package/dist/pages/admin-ai.mjs.map +1 -0
  88. package/dist/pages/admin-system-origin.mjs +123 -0
  89. package/dist/pages/admin-system-origin.mjs.map +1 -0
  90. package/dist/pages/admin-system-targets.mjs +53 -0
  91. package/dist/pages/admin-system-targets.mjs.map +1 -0
  92. package/dist/pages/admin-system.mjs +63 -13
  93. package/dist/pages/admin-system.mjs.map +1 -1
  94. package/dist/pages/build-detail.mjs +104 -76
  95. package/dist/pages/build-detail.mjs.map +1 -1
  96. package/dist/pages/build-diff-actions.mjs +87 -33
  97. package/dist/pages/build-diff-actions.mjs.map +1 -1
  98. package/dist/pages/build-diff-comments.mjs +16 -14
  99. package/dist/pages/build-diff-comments.mjs.map +1 -1
  100. package/dist/pages/build-diff-environment.mjs +82 -0
  101. package/dist/pages/build-diff-environment.mjs.map +1 -0
  102. package/dist/pages/build-diff-header.mjs +22 -4
  103. package/dist/pages/build-diff-header.mjs.map +1 -1
  104. package/dist/pages/build-diff-insight.mjs +113 -0
  105. package/dist/pages/build-diff-insight.mjs.map +1 -0
  106. package/dist/pages/build-diff-model.mjs +55 -10
  107. package/dist/pages/build-diff-model.mjs.map +1 -1
  108. package/dist/pages/build-diff-nav.mjs +28 -12
  109. package/dist/pages/build-diff-nav.mjs.map +1 -1
  110. package/dist/pages/build-diff-shortcuts.mjs +5 -1
  111. package/dist/pages/build-diff-shortcuts.mjs.map +1 -1
  112. package/dist/pages/build-diff-usage.mjs +63 -0
  113. package/dist/pages/build-diff-usage.mjs.map +1 -0
  114. package/dist/pages/build-diff-viewer.mjs +62 -5
  115. package/dist/pages/build-diff-viewer.mjs.map +1 -1
  116. package/dist/pages/build-diff.mjs +34 -13
  117. package/dist/pages/build-diff.mjs.map +1 -1
  118. package/dist/pages/build-tests.mjs +74 -0
  119. package/dist/pages/build-tests.mjs.map +1 -0
  120. package/dist/pages/component-detail.mjs +249 -0
  121. package/dist/pages/component-detail.mjs.map +1 -0
  122. package/dist/pages/component-stories.mjs +168 -0
  123. package/dist/pages/component-stories.mjs.map +1 -0
  124. package/dist/pages/compute-jobs.mjs +13 -26
  125. package/dist/pages/compute-jobs.mjs.map +1 -1
  126. package/dist/pages/design-links.mjs +182 -0
  127. package/dist/pages/design-links.mjs.map +1 -0
  128. package/dist/pages/environment-change.mjs +53 -0
  129. package/dist/pages/environment-change.mjs.map +1 -0
  130. package/dist/pages/health-panel.mjs +126 -0
  131. package/dist/pages/health-panel.mjs.map +1 -0
  132. package/dist/pages/library-grid.mjs +105 -0
  133. package/dist/pages/library-grid.mjs.map +1 -0
  134. package/dist/pages/library-model.mjs +47 -0
  135. package/dist/pages/library-model.mjs.map +1 -0
  136. package/dist/pages/library-snapshots.mjs +130 -0
  137. package/dist/pages/library-snapshots.mjs.map +1 -0
  138. package/dist/pages/library.mjs +112 -218
  139. package/dist/pages/library.mjs.map +1 -1
  140. package/dist/pages/notifications-paused.mjs +22 -0
  141. package/dist/pages/notifications-paused.mjs.map +1 -0
  142. package/dist/pages/profile.mjs +53 -48
  143. package/dist/pages/profile.mjs.map +1 -1
  144. package/dist/pages/project-builds-table.mjs +2 -2
  145. package/dist/pages/project-builds-table.mjs.map +1 -1
  146. package/dist/pages/project-builds.mjs +51 -21
  147. package/dist/pages/project-builds.mjs.map +1 -1
  148. package/dist/pages/project-settings.mjs +51 -12
  149. package/dist/pages/project-settings.mjs.map +1 -1
  150. package/dist/pages/projects-card.mjs +14 -1
  151. package/dist/pages/projects-card.mjs.map +1 -1
  152. package/dist/pages/projects.mjs +6 -2
  153. package/dist/pages/projects.mjs.map +1 -1
  154. package/dist/pages/review-changeset.mjs +116 -0
  155. package/dist/pages/review-changeset.mjs.map +1 -0
  156. package/dist/pages/review-comments.mjs +103 -0
  157. package/dist/pages/review-comments.mjs.map +1 -0
  158. package/dist/pages/review-verdict.mjs +102 -0
  159. package/dist/pages/review-verdict.mjs.map +1 -0
  160. package/dist/pages/settings-ai.mjs +69 -0
  161. package/dist/pages/settings-ai.mjs.map +1 -0
  162. package/dist/pages/settings-capture.mjs +124 -0
  163. package/dist/pages/settings-capture.mjs.map +1 -0
  164. package/dist/pages/settings-design-credential.mjs +99 -0
  165. package/dist/pages/settings-design-credential.mjs.map +1 -0
  166. package/dist/pages/settings-design-links.mjs +179 -0
  167. package/dist/pages/settings-design-links.mjs.map +1 -0
  168. package/dist/pages/settings-design.mjs +48 -0
  169. package/dist/pages/settings-design.mjs.map +1 -0
  170. package/dist/pages/settings-general.mjs +46 -21
  171. package/dist/pages/settings-general.mjs.map +1 -1
  172. package/dist/pages/settings-labels.mjs +43 -2
  173. package/dist/pages/settings-labels.mjs.map +1 -1
  174. package/dist/pages/settings-links.mjs +112 -0
  175. package/dist/pages/settings-links.mjs.map +1 -0
  176. package/dist/pages/settings-notifications-git.mjs +128 -0
  177. package/dist/pages/settings-notifications-git.mjs.map +1 -0
  178. package/dist/pages/settings-notifications.mjs +79 -42
  179. package/dist/pages/settings-notifications.mjs.map +1 -1
  180. package/dist/pages/settings-tests.mjs +33 -2
  181. package/dist/pages/settings-tests.mjs.map +1 -1
  182. package/dist/pages/target-choices.mjs +39 -0
  183. package/dist/pages/target-choices.mjs.map +1 -0
  184. package/dist/publish-setup.mjs +89 -0
  185. package/dist/publish-setup.mjs.map +1 -0
  186. package/dist/retention-timer.mjs +2 -1
  187. package/dist/retention-timer.mjs.map +1 -1
  188. package/dist/review-events.mjs +48 -0
  189. package/dist/review-events.mjs.map +1 -0
  190. package/dist/routers/admin-pages.mjs +45 -3
  191. package/dist/routers/admin-pages.mjs.map +1 -1
  192. package/dist/routers/admin-public-origin.mjs +85 -0
  193. package/dist/routers/admin-public-origin.mjs.map +1 -0
  194. package/dist/routers/assets.mjs +9 -2
  195. package/dist/routers/assets.mjs.map +1 -1
  196. package/dist/routers/auth.mjs +2 -2
  197. package/dist/routers/auth.mjs.map +1 -1
  198. package/dist/routers/baseline-guard.mjs +2 -2
  199. package/dist/routers/baseline-guard.mjs.map +1 -1
  200. package/dist/routers/baselines.handlers.mjs +64 -0
  201. package/dist/routers/baselines.handlers.mjs.map +1 -0
  202. package/dist/routers/baselines.mjs +125 -0
  203. package/dist/routers/baselines.mjs.map +1 -0
  204. package/dist/routers/build-cancel.mjs +68 -0
  205. package/dist/routers/build-cancel.mjs.map +1 -0
  206. package/dist/routers/build-changeset.mjs +70 -0
  207. package/dist/routers/build-changeset.mjs.map +1 -0
  208. package/dist/routers/build-reviews.handlers.mjs +86 -0
  209. package/dist/routers/build-reviews.handlers.mjs.map +1 -0
  210. package/dist/routers/build-reviews.mjs +74 -0
  211. package/dist/routers/build-reviews.mjs.map +1 -0
  212. package/dist/routers/builds.handlers.mjs +46 -55
  213. package/dist/routers/builds.handlers.mjs.map +1 -1
  214. package/dist/routers/builds.mjs +71 -17
  215. package/dist/routers/builds.mjs.map +1 -1
  216. package/dist/routers/capture-config.mjs +35 -0
  217. package/dist/routers/capture-config.mjs.map +1 -0
  218. package/dist/routers/comments.mjs +41 -15
  219. package/dist/routers/comments.mjs.map +1 -1
  220. package/dist/routers/component-catalog.handlers.mjs +71 -0
  221. package/dist/routers/component-catalog.handlers.mjs.map +1 -0
  222. package/dist/routers/component.handlers.mjs +121 -0
  223. package/dist/routers/component.handlers.mjs.map +1 -0
  224. package/dist/routers/connections.mjs +118 -0
  225. package/dist/routers/connections.mjs.map +1 -0
  226. package/dist/routers/design-connect.handlers.mjs +44 -0
  227. package/dist/routers/design-connect.handlers.mjs.map +1 -0
  228. package/dist/routers/design.handlers.mjs +114 -0
  229. package/dist/routers/design.handlers.mjs.map +1 -0
  230. package/dist/routers/health-report.mjs +2 -3
  231. package/dist/routers/health-report.mjs.map +1 -1
  232. package/dist/routers/health.mjs +4 -3
  233. package/dist/routers/health.mjs.map +1 -1
  234. package/dist/routers/helpers.mjs +19 -3
  235. package/dist/routers/helpers.mjs.map +1 -1
  236. package/dist/routers/insights-ui.mjs +64 -0
  237. package/dist/routers/insights-ui.mjs.map +1 -0
  238. package/dist/routers/insights.handlers.mjs +45 -0
  239. package/dist/routers/insights.handlers.mjs.map +1 -0
  240. package/dist/routers/insights.mjs +189 -0
  241. package/dist/routers/insights.mjs.map +1 -0
  242. package/dist/routers/links.handlers.mjs +80 -0
  243. package/dist/routers/links.handlers.mjs.map +1 -0
  244. package/dist/routers/links.mjs +139 -0
  245. package/dist/routers/links.mjs.map +1 -0
  246. package/dist/routers/media.mjs +18 -4
  247. package/dist/routers/media.mjs.map +1 -1
  248. package/dist/routers/members.mjs +2 -1
  249. package/dist/routers/members.mjs.map +1 -1
  250. package/dist/routers/notification-channels.mjs +17 -23
  251. package/dist/routers/notification-channels.mjs.map +1 -1
  252. package/dist/routers/oauth-state.mjs +45 -0
  253. package/dist/routers/oauth-state.mjs.map +1 -0
  254. package/dist/routers/preview-base.mjs +38 -0
  255. package/dist/routers/preview-base.mjs.map +1 -0
  256. package/dist/routers/preview-token.mjs +51 -0
  257. package/dist/routers/preview-token.mjs.map +1 -0
  258. package/dist/routers/profile.mjs +4 -2
  259. package/dist/routers/profile.mjs.map +1 -1
  260. package/dist/routers/projects.mjs +17 -1
  261. package/dist/routers/projects.mjs.map +1 -1
  262. package/dist/routers/publish-upload.mjs +78 -0
  263. package/dist/routers/publish-upload.mjs.map +1 -0
  264. package/dist/routers/published-access.mjs +52 -0
  265. package/dist/routers/published-access.mjs.map +1 -0
  266. package/dist/routers/published-files.mjs +52 -0
  267. package/dist/routers/published-files.mjs.map +1 -0
  268. package/dist/routers/review-forms.mjs +72 -0
  269. package/dist/routers/review-forms.mjs.map +1 -0
  270. package/dist/routers/review-section.handlers.mjs +85 -0
  271. package/dist/routers/review-section.handlers.mjs.map +1 -0
  272. package/dist/routers/review-switch-off.mjs +29 -0
  273. package/dist/routers/review-switch-off.mjs.map +1 -0
  274. package/dist/routers/schemas.mjs +140 -22
  275. package/dist/routers/schemas.mjs.map +1 -1
  276. package/dist/routers/scoped-models.mjs +22 -0
  277. package/dist/routers/scoped-models.mjs.map +1 -0
  278. package/dist/routers/settings-ai.mjs +35 -0
  279. package/dist/routers/settings-ai.mjs.map +1 -0
  280. package/dist/routers/settings-capture.mjs +77 -0
  281. package/dist/routers/settings-capture.mjs.map +1 -0
  282. package/dist/routers/settings-design.mjs +103 -0
  283. package/dist/routers/settings-design.mjs.map +1 -0
  284. package/dist/routers/settings-general.mjs +55 -10
  285. package/dist/routers/settings-general.mjs.map +1 -1
  286. package/dist/routers/settings-git-channel.mjs +77 -0
  287. package/dist/routers/settings-git-channel.mjs.map +1 -0
  288. package/dist/routers/settings-labels.mjs +27 -1
  289. package/dist/routers/settings-labels.mjs.map +1 -1
  290. package/dist/routers/settings-links.mjs +63 -0
  291. package/dist/routers/settings-links.mjs.map +1 -0
  292. package/dist/routers/settings-members.mjs +4 -7
  293. package/dist/routers/settings-members.mjs.map +1 -1
  294. package/dist/routers/settings-notifications.mjs +15 -10
  295. package/dist/routers/settings-notifications.mjs.map +1 -1
  296. package/dist/routers/settings-tokens.mjs +2 -5
  297. package/dist/routers/settings-tokens.mjs.map +1 -1
  298. package/dist/routers/settings-webhooks.mjs +7 -24
  299. package/dist/routers/settings-webhooks.mjs.map +1 -1
  300. package/dist/routers/settings.handlers.mjs +107 -26
  301. package/dist/routers/settings.handlers.mjs.map +1 -1
  302. package/dist/routers/settings.mjs +16 -4
  303. package/dist/routers/settings.mjs.map +1 -1
  304. package/dist/routers/signed-storybook.mjs +53 -0
  305. package/dist/routers/signed-storybook.mjs.map +1 -0
  306. package/dist/routers/snapshot-groups.mjs +63 -0
  307. package/dist/routers/snapshot-groups.mjs.map +1 -0
  308. package/dist/routers/snapshots.mjs +103 -5
  309. package/dist/routers/snapshots.mjs.map +1 -1
  310. package/dist/routers/storybook.mjs +37 -88
  311. package/dist/routers/storybook.mjs.map +1 -1
  312. package/dist/routers/tokens.mjs +2 -5
  313. package/dist/routers/tokens.mjs.map +1 -1
  314. package/dist/routers/ui.mjs +34 -5
  315. package/dist/routers/ui.mjs.map +1 -1
  316. package/dist/routers/webhooks.mjs +5 -11
  317. package/dist/routers/webhooks.mjs.map +1 -1
  318. package/dist/routers/well-known.mjs +11 -4
  319. package/dist/routers/well-known.mjs.map +1 -1
  320. package/dist/routes/api-routes.mjs +93 -0
  321. package/dist/routes/api-routes.mjs.map +1 -0
  322. package/dist/routes/classification.mjs +38 -0
  323. package/dist/routes/classification.mjs.map +1 -0
  324. package/dist/routes/openapi-classes.mjs +22 -0
  325. package/dist/routes/openapi-classes.mjs.map +1 -0
  326. package/dist/routes/page-routes.mjs +104 -0
  327. package/dist/routes/page-routes.mjs.map +1 -0
  328. package/dist/runtime.mjs +18 -13
  329. package/dist/runtime.mjs.map +1 -1
  330. package/dist/serverless.mjs +5 -0
  331. package/dist/store.mjs +19 -1
  332. package/dist/store.mjs.map +1 -1
  333. package/dist/targets.mjs +46 -0
  334. package/dist/targets.mjs.map +1 -0
  335. package/dist/ui/buttons.mjs +16 -3
  336. package/dist/ui/buttons.mjs.map +1 -1
  337. package/dist/ui/design-reference.mjs +97 -0
  338. package/dist/ui/design-reference.mjs.map +1 -0
  339. package/dist/ui/document.mjs +1 -2
  340. package/dist/ui/document.mjs.map +1 -1
  341. package/dist/ui/forms.mjs +6 -5
  342. package/dist/ui/forms.mjs.map +1 -1
  343. package/dist/ui/icons/paths.mjs +1 -0
  344. package/dist/ui/icons/paths.mjs.map +1 -1
  345. package/dist/ui/preview-embed.mjs +82 -0
  346. package/dist/ui/preview-embed.mjs.map +1 -0
  347. package/dist/ui/shell/client-script.mjs +85 -0
  348. package/dist/ui/shell/client-script.mjs.map +1 -1
  349. package/dist/ui/shell/nav.mjs +12 -8
  350. package/dist/ui/shell/nav.mjs.map +1 -1
  351. package/dist/ui/shell/review-script.mjs +8 -0
  352. package/dist/ui/shell/review-script.mjs.map +1 -1
  353. package/dist/ui/shell/sidebar.mjs +2 -2
  354. package/dist/ui/shell/sidebar.mjs.map +1 -1
  355. package/package.json +11 -6
  356. package/dist/asset-paths.mjs +0 -23
  357. package/dist/asset-paths.mjs.map +0 -1
  358. package/dist/assets/mark.svg +0 -1
  359. package/dist/pages/settings-status.mjs +0 -137
  360. package/dist/pages/settings-status.mjs.map +0 -1
  361. package/dist/routers/settings-status.mjs +0 -77
  362. package/dist/routers/settings-status.mjs.map +0 -1
  363. package/dist/routers/status-configs.mjs +0 -108
  364. package/dist/routers/status-configs.mjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"forms.mjs","names":[],"sources":["../../src/ui/forms.tsx"],"sourcesContent":["import type { FC } from \"hono/jsx\";\nimport { css } from \"./css.ts\";\nimport { Icon } from \"./icons/icon.tsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\ntype FieldLayout = \"stack\" | \"inline\";\n\nconst fieldWrap = css`\n /* field */\n display: grid;\n gap: var(--space-1);\n /* Don't stretch rows when a neighbouring field is taller (hint text). */\n align-content: start;\n margin-bottom: var(--space-4);\n`;\n\nconst fieldWrapInline = css`\n /* field-inline */\n ${fieldWrap}\n grid-template-columns: auto minmax(0, 1fr);\n align-items: center;\n gap: var(--space-2);\n margin-bottom: 0;\n`;\n\nconst fieldLabel = css`\n /* field-label */\n color: var(--text-primary);\n font-weight: 600;\n font-size: var(--text-sm);\n & span[aria-hidden] {\n color: var(--status-rejected);\n }\n`;\n\nconst fieldInput = css`\n /* field-input */\n width: 100%;\n min-height: 40px;\n padding: 0.5rem 0.75rem;\n border-radius: var(--radius);\n border: 1px solid var(--border);\n background: var(--surface-card);\n color: var(--text-primary);\n font: inherit;\n font-size: var(--text-base);\n box-shadow: var(--shadow);\n transition:\n border-color var(--dur-fast) var(--ease),\n box-shadow var(--dur-fast) var(--ease);\n &::placeholder {\n color: var(--text-muted);\n }\n &:hover:not(:disabled) {\n border-color: color-mix(in srgb, var(--text-secondary) 45%, var(--border));\n }\n &:focus {\n outline: none;\n border-color: var(--ring);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--ring) 25%, transparent);\n }\n &:disabled {\n background: var(--surface-muted);\n color: var(--text-secondary);\n cursor: not-allowed;\n }\n`;\n\nconst fieldInputError = css`\n /* field-input-error */\n ${fieldInput}\n border-color: var(--status-rejected);\n &:focus {\n border-color: var(--status-rejected);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--status-rejected) 25%, transparent);\n }\n`;\n\nconst fieldTextarea = css`\n /* field-textarea */\n ${fieldInput}\n resize: vertical;\n min-height: 5.5rem;\n line-height: var(--leading-normal);\n`;\n\nconst fieldTextareaError = css`\n /* field-textarea-error */\n ${fieldInputError}\n resize: vertical;\n min-height: 5.5rem;\n line-height: var(--leading-normal);\n`;\n\nconst fieldHint = css`\n /* field-hint */\n margin: 0;\n color: var(--text-secondary);\n font-size: var(--text-sm);\n`;\n\nconst fieldError = css`\n /* field-error */\n display: flex;\n gap: var(--space-1);\n align-items: flex-start;\n margin: 0;\n color: var(--status-rejected-fg);\n font-size: var(--text-sm);\n font-weight: 600;\n`;\n\nconst formActions = css`\n /* form-actions */\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-2);\n align-items: center;\n margin-top: var(--space-4);\n`;\n\nconst formActionsEnd = css`\n /* form-actions-end */\n ${formActions}\n justify-content: flex-end;\n`;\n\nconst formActionsSplit = css`\n /* form-actions-split */\n ${formActions}\n justify-content: space-between;\n`;\n\n/** Row of form buttons: primary action first; `align=\"end\"` right-aligns, `\"split\"` spreads. */\nexport const FormActions: FC<{ align?: \"start\" | \"end\" | \"split\"; children?: unknown }> = ({\n align = \"start\",\n children,\n}) => {\n const cls = align === \"end\" ? formActionsEnd : align === \"split\" ? formActionsSplit : formActions;\n return <div class={cls}>{children}</div>;\n};\n\nfunction fieldDescribedBy(\n name: string,\n error: string | undefined,\n hint: string | undefined,\n): string | undefined {\n if (error) {\n return `${name}-error`;\n }\n if (hint) {\n return `${name}-hint`;\n }\n return undefined;\n}\n\n// eslint-disable-next-line promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString>\nconst FieldAssistant: FC<{ name: string; error: string | undefined; hint: string | undefined }> = ({\n name,\n error,\n hint,\n}) => {\n if (error) {\n return (\n <p class={fieldError} id={`${name}-error`} role=\"alert\">\n <Icon name=\"x-circle\" size=\"sm\" />\n <span>{error}</span>\n </p>\n );\n }\n if (hint) {\n return (\n <p class={fieldHint} id={`${name}-hint`}>\n {hint}\n </p>\n );\n }\n return null;\n};\n\n/** Labeled text input with error and hint states. */\n// eslint-disable-next-line promise-function-async -- JSX component return type\nexport const Field: FC<{\n label: string;\n name: string;\n type?: string;\n value?: string;\n placeholder?: string;\n required?: boolean;\n disabled?: boolean;\n autofocus?: boolean;\n pattern?: string;\n min?: string;\n max?: string;\n step?: string;\n layout?: FieldLayout;\n error?: string;\n hint?: string;\n autocomplete?: string;\n}> = ({\n label,\n name,\n type = \"text\",\n value,\n placeholder,\n required,\n disabled,\n autofocus,\n pattern,\n min,\n max,\n step,\n layout = \"stack\",\n error,\n hint,\n autocomplete,\n}) => {\n return (\n <div class={layout === \"inline\" ? fieldWrapInline : fieldWrap}>\n <label class={fieldLabel} for={name}>\n {label}\n {required ? <span aria-hidden=\"true\"> *</span> : null}\n </label>\n <input\n class={error ? fieldInputError : fieldInput}\n id={name}\n name={name}\n type={type}\n value={value}\n placeholder={placeholder}\n required={required}\n disabled={disabled}\n autofocus={autofocus}\n pattern={pattern}\n min={min}\n max={max}\n step={step}\n aria-invalid={error ? \"true\" : undefined}\n aria-describedby={fieldDescribedBy(name, error, hint)}\n autocomplete={autocomplete}\n />\n <FieldAssistant name={name} error={error} hint={hint} />\n </div>\n );\n};\n\nconst checkLabel = css`\n /* check-label */\n display: flex;\n gap: var(--space-2);\n align-items: center;\n font-weight: 500;\n font-size: var(--text-base);\n cursor: pointer;\n`;\n\nconst checkInput = css`\n /* check-input */\n width: 1rem;\n height: 1rem;\n accent-color: var(--accent);\n flex: none;\n`;\n\n/** Inline checkbox row with an optional hint. */\n// eslint-disable-next-line promise-function-async -- JSX component return type\nexport const CheckField: FC<{\n label: unknown;\n name: string;\n value?: string;\n checked?: boolean;\n disabled?: boolean;\n hint?: string;\n}> = ({ label, name, value, checked, disabled, hint }) => {\n return (\n <div class={fieldWrap}>\n <label class={checkLabel} for={name}>\n <input\n class={checkInput}\n type=\"checkbox\"\n id={name}\n name={name}\n value={value}\n checked={checked}\n disabled={disabled}\n />\n {label}\n </label>\n {hint ? (\n <p class={fieldHint} id={`${name}-hint`}>\n {hint}\n </p>\n ) : null}\n </div>\n );\n};\n\n/** Labeled textarea with error and hint states. */\n// eslint-disable-next-line promise-function-async -- JSX component return type\nexport const TextareaField: FC<{\n label: string;\n name: string;\n value?: string;\n placeholder?: string;\n rows?: number;\n required?: boolean;\n hint?: string;\n error?: string;\n}> = ({ label, name, value, placeholder, rows = 3, required, hint, error }) => {\n return (\n <div class={fieldWrap}>\n <label class={fieldLabel} for={name}>\n {label}\n </label>\n <textarea\n class={error ? fieldTextareaError : fieldTextarea}\n id={name}\n name={name}\n placeholder={placeholder}\n rows={rows}\n required={required}\n aria-invalid={error ? \"true\" : undefined}\n aria-describedby={fieldDescribedBy(name, error, hint)}\n >\n {value}\n </textarea>\n <FieldAssistant name={name} error={error} hint={hint} />\n </div>\n );\n};\n\n/** Labeled select dropdown with an optional hint. */\n// eslint-disable-next-line promise-function-async -- JSX component return type\nexport const SelectField: FC<{\n label: string;\n name: string;\n value?: string;\n options: { value: string; label: string }[];\n hint?: string;\n disabled?: boolean;\n required?: boolean;\n layout?: FieldLayout;\n}> = ({ label, name, value, options, hint, disabled, required, layout = \"stack\" }) => {\n return (\n <div class={layout === \"inline\" ? fieldWrapInline : fieldWrap}>\n <label class={fieldLabel} for={name}>\n {label}\n </label>\n <select\n class={fieldInput}\n id={name}\n name={name}\n disabled={disabled}\n required={required}\n aria-describedby={hint ? `${name}-hint` : undefined}\n >\n {options.map((opt) => (\n <option value={opt.value} selected={opt.value === value}>\n {opt.label}\n </option>\n ))}\n </select>\n {hint ? (\n <p class={fieldHint} id={`${name}-hint`}>\n {hint}\n </p>\n ) : null}\n </div>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"forms.mjs","names":[],"sources":["../../src/ui/forms.tsx"],"sourcesContent":["import type { FC } from \"hono/jsx\";\nimport { css } from \"./css.ts\";\nimport { Icon } from \"./icons/icon.tsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\ntype FieldLayout = \"stack\" | \"inline\";\n\nconst fieldWrap = css`\n /* field */\n display: grid;\n gap: var(--space-1);\n /* Don't stretch rows when a neighbouring field is taller (hint text). */\n align-content: start;\n margin-bottom: var(--space-4);\n`;\n\nconst fieldWrapInline = css`\n /* field-inline */\n ${fieldWrap}\n grid-template-columns: auto minmax(0, 1fr);\n align-items: center;\n gap: var(--space-2);\n margin-bottom: 0;\n`;\n\nconst fieldLabel = css`\n /* field-label */\n color: var(--text-primary);\n font-weight: 600;\n font-size: var(--text-sm);\n & span[aria-hidden] {\n color: var(--status-rejected);\n }\n`;\n\nconst fieldInput = css`\n /* field-input */\n width: 100%;\n min-height: 40px;\n padding: 0.5rem 0.75rem;\n border-radius: var(--radius);\n border: 1px solid var(--border);\n background: var(--surface-card);\n color: var(--text-primary);\n font: inherit;\n font-size: var(--text-base);\n box-shadow: var(--shadow);\n transition:\n border-color var(--dur-fast) var(--ease),\n box-shadow var(--dur-fast) var(--ease);\n &::placeholder {\n color: var(--text-muted);\n }\n &:hover:not(:disabled) {\n border-color: color-mix(in srgb, var(--text-secondary) 45%, var(--border));\n }\n &:focus {\n outline: none;\n border-color: var(--ring);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--ring) 25%, transparent);\n }\n &:disabled {\n background: var(--surface-muted);\n color: var(--text-secondary);\n cursor: not-allowed;\n }\n`;\n\nconst fieldInputError = css`\n /* field-input-error */\n ${fieldInput}\n border-color: var(--status-rejected);\n &:focus {\n border-color: var(--status-rejected);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--status-rejected) 25%, transparent);\n }\n`;\n\nconst fieldTextarea = css`\n /* field-textarea */\n ${fieldInput}\n resize: vertical;\n min-height: 5.5rem;\n line-height: var(--leading-normal);\n`;\n\nconst fieldTextareaError = css`\n /* field-textarea-error */\n ${fieldInputError}\n resize: vertical;\n min-height: 5.5rem;\n line-height: var(--leading-normal);\n`;\n\nconst fieldHint = css`\n /* field-hint */\n margin: 0;\n color: var(--text-secondary);\n font-size: var(--text-sm);\n`;\n\nconst fieldError = css`\n /* field-error */\n display: flex;\n gap: var(--space-1);\n align-items: flex-start;\n margin: 0;\n color: var(--status-rejected-fg);\n font-size: var(--text-sm);\n font-weight: 600;\n`;\n\nconst formActions = css`\n /* form-actions */\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-2);\n align-items: center;\n margin-top: var(--space-4);\n`;\n\nconst formActionsEnd = css`\n /* form-actions-end */\n ${formActions}\n justify-content: flex-end;\n`;\n\nconst formActionsSplit = css`\n /* form-actions-split */\n ${formActions}\n justify-content: space-between;\n`;\n\n/** Row of form buttons: primary action first; `align=\"end\"` right-aligns, `\"split\"` spreads. */\nexport const FormActions: FC<{ align?: \"start\" | \"end\" | \"split\"; children?: unknown }> = ({\n align = \"start\",\n children,\n}) => {\n const cls = align === \"end\" ? formActionsEnd : align === \"split\" ? formActionsSplit : formActions;\n return <div class={cls}>{children}</div>;\n};\n\nfunction fieldDescribedBy(\n name: string,\n error: string | undefined,\n hint: string | undefined,\n): string | undefined {\n if (error) {\n return `${name}-error`;\n }\n if (hint) {\n return `${name}-hint`;\n }\n return undefined;\n}\n\n// eslint-disable-next-line promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString>\nconst FieldAssistant: FC<{ name: string; error: string | undefined; hint: string | undefined }> = ({\n name,\n error,\n hint,\n}) => {\n if (error) {\n return (\n <p class={fieldError} id={`${name}-error`} role=\"alert\">\n <Icon name=\"x-circle\" size=\"sm\" />\n <span>{error}</span>\n </p>\n );\n }\n if (hint) {\n return (\n <p class={fieldHint} id={`${name}-hint`}>\n {hint}\n </p>\n );\n }\n return null;\n};\n\n/** Labeled text input with error and hint states. */\n// eslint-disable-next-line promise-function-async -- JSX component return type\nexport const Field: FC<{\n label: string;\n name: string;\n type?: string;\n value?: string;\n placeholder?: string;\n required?: boolean;\n disabled?: boolean;\n autofocus?: boolean;\n pattern?: string;\n min?: string;\n max?: string;\n step?: string;\n layout?: FieldLayout;\n error?: string;\n hint?: string;\n autocomplete?: string;\n}> = ({\n label,\n name,\n type = \"text\",\n value,\n placeholder,\n required,\n disabled,\n autofocus,\n pattern,\n min,\n max,\n step,\n layout = \"stack\",\n error,\n hint,\n autocomplete,\n}) => {\n return (\n <div class={layout === \"inline\" ? fieldWrapInline : fieldWrap}>\n <label class={fieldLabel} for={name}>\n {label}\n {required ? <span aria-hidden=\"true\"> *</span> : null}\n </label>\n <input\n class={error ? fieldInputError : fieldInput}\n id={name}\n name={name}\n type={type}\n value={value}\n placeholder={placeholder}\n required={required}\n disabled={disabled}\n autofocus={autofocus}\n pattern={pattern}\n min={min}\n max={max}\n step={step}\n aria-invalid={error ? \"true\" : undefined}\n aria-describedby={fieldDescribedBy(name, error, hint)}\n autocomplete={autocomplete}\n />\n <FieldAssistant name={name} error={error} hint={hint} />\n </div>\n );\n};\n\nconst checkLabel = css`\n /* check-label */\n display: flex;\n gap: var(--space-2);\n align-items: center;\n font-weight: 500;\n font-size: var(--text-base);\n cursor: pointer;\n`;\n\nconst checkInput = css`\n /* check-input */\n width: 1rem;\n height: 1rem;\n accent-color: var(--accent);\n flex: none;\n`;\n\n/** Inline checkbox row with an optional hint. */\n// eslint-disable-next-line promise-function-async -- JSX component return type\nexport const CheckField: FC<{\n label: unknown;\n name: string;\n value?: string;\n checked?: boolean;\n disabled?: boolean;\n hint?: string;\n}> = ({ label, name, value, checked, disabled, hint }) => {\n return (\n <div class={fieldWrap}>\n <label class={checkLabel} for={name}>\n <input\n class={checkInput}\n type=\"checkbox\"\n id={name}\n name={name}\n value={value}\n checked={checked}\n disabled={disabled}\n />\n {label}\n </label>\n {hint ? (\n <p class={fieldHint} id={`${name}-hint`}>\n {hint}\n </p>\n ) : null}\n </div>\n );\n};\n\n/** Labeled textarea with error and hint states. */\n// eslint-disable-next-line promise-function-async -- JSX component return type\nexport const TextareaField: FC<{\n label: string;\n name: string;\n /** DOM id when several forms on a page post the same field name (defaults to `name`). */\n id?: string;\n value?: string;\n placeholder?: string;\n rows?: number;\n required?: boolean;\n hint?: string;\n error?: string;\n}> = ({ label, name, id, value, placeholder, rows = 3, required, hint, error }) => {\n const domId = id ?? name;\n return (\n <div class={fieldWrap}>\n <label class={fieldLabel} for={domId}>\n {label}\n </label>\n <textarea\n class={error ? fieldTextareaError : fieldTextarea}\n id={domId}\n name={name}\n placeholder={placeholder}\n rows={rows}\n required={required}\n aria-invalid={error ? \"true\" : undefined}\n aria-describedby={fieldDescribedBy(domId, error, hint)}\n >\n {value}\n </textarea>\n <FieldAssistant name={domId} error={error} hint={hint} />\n </div>\n );\n};\n\n/** Labeled select dropdown with an optional hint. */\n// eslint-disable-next-line promise-function-async -- JSX component return type\nexport const SelectField: FC<{\n label: string;\n name: string;\n value?: string;\n options: { value: string; label: string }[];\n hint?: string;\n disabled?: boolean;\n required?: boolean;\n layout?: FieldLayout;\n}> = ({ label, name, value, options, hint, disabled, required, layout = \"stack\" }) => {\n return (\n <div class={layout === \"inline\" ? fieldWrapInline : fieldWrap}>\n <label class={fieldLabel} for={name}>\n {label}\n </label>\n <select\n class={fieldInput}\n id={name}\n name={name}\n disabled={disabled}\n required={required}\n aria-describedby={hint ? `${name}-hint` : undefined}\n >\n {options.map((opt) => (\n <option value={opt.value} selected={opt.value === value}>\n {opt.label}\n </option>\n ))}\n </select>\n {hint ? (\n <p class={fieldHint} id={`${name}-hint`}>\n {hint}\n </p>\n ) : null}\n </div>\n );\n};\n"],"mappings":"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"}
@@ -23,6 +23,7 @@ const ICON_PATHS = {
23
23
  "git-branch": "<path d=\"M6 3v12\"/><circle cx=\"18\" cy=\"6\" r=\"3\"/><circle cx=\"6\" cy=\"18\" r=\"3\"/><path d=\"M18 9a9 9 0 0 1-9 9\"/>",
24
24
  "git-commit": "<circle cx=\"12\" cy=\"12\" r=\"3\"/><path d=\"M3 12h6M15 12h6\"/>",
25
25
  activity: "<path d=\"M22 12h-4l-3 9L9 3l-3 9H2\"/>",
26
+ sparkles: "<path d=\"M9.9 15.5A2 2 0 0 0 8.5 14.1l-6.1-1.6a.5.5 0 0 1 0-1L8.5 9.9A2 2 0 0 0 9.9 8.5l1.6-6.1a.5.5 0 0 1 1 0l1.6 6.1a2 2 0 0 0 1.4 1.4l6.1 1.6a.5.5 0 0 1 0 1l-6.1 1.6a2 2 0 0 0-1.4 1.4l-1.6 6.1a.5.5 0 0 1-1 0z\"/><path d=\"M20 3v4M22 5h-4M4 17v2M5 18H3\"/>",
26
27
  tag: "<path d=\"M12.6 2.6A2 2 0 0 0 11.2 2H4a2 2 0 0 0-2 2v7.2a2 2 0 0 0 .6 1.4l8.7 8.7a2.4 2.4 0 0 0 3.4 0l6.6-6.6a2.4 2.4 0 0 0 0-3.4z\"/><circle cx=\"7.5\" cy=\"7.5\" r=\".5\" fill=\"currentColor\"/>",
27
28
  settings: "<path d=\"M12.2 2h-.4a2 2 0 0 0-2 2v.2a2 2 0 0 1-1 1.7l-.4.3a2 2 0 0 1-2 0l-.2-.1a2 2 0 0 0-2.7.7l-.2.4a2 2 0 0 0 .7 2.7l.2.1a2 2 0 0 1 1 1.7v.5a2 2 0 0 1-1 1.8l-.2.1a2 2 0 0 0-.7 2.7l.2.4a2 2 0 0 0 2.7.7l.2-.1a2 2 0 0 1 2 0l.4.3a2 2 0 0 1 1 1.7v.2a2 2 0 0 0 2 2h.4a2 2 0 0 0 2-2v-.2a2 2 0 0 1 1-1.7l.4-.3a2 2 0 0 1 2 0l.2.1a2 2 0 0 0 2.7-.7l.2-.4a2 2 0 0 0-.7-2.7l-.2-.1a2 2 0 0 1-1-1.8v-.5a2 2 0 0 1 1-1.7l.2-.1a2 2 0 0 0 .7-2.7l-.2-.4a2 2 0 0 0-2.7-.7l-.2.1a2 2 0 0 1-2 0l-.4-.3a2 2 0 0 1-1-1.7V4a2 2 0 0 0-2-2z\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/>",
28
29
  users: "<path d=\"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2\"/><circle cx=\"9\" cy=\"7\" r=\"4\"/><path d=\"M22 21v-2a4 4 0 0 0-3-3.9M16 3.1a4 4 0 0 1 0 7.8\"/>",
@@ -1 +1 @@
1
- {"version":3,"file":"paths.mjs","names":[],"sources":["../../../src/ui/icons/paths.ts"],"sourcesContent":["/**\n * Icon set: inner SVG markup per icon on a 24x24 grid, stroke-based\n * (Lucide-style, ISC). Add an icon here and it is in the sprite and the\n * `IconName` union automatically.\n */\nexport const ICON_PATHS = {\n check: '<path d=\"M20 6 9 17l-5-5\"/>',\n x: '<path d=\"M18 6 6 18M6 6l12 12\"/>',\n plus: '<path d=\"M5 12h14M12 5v14\"/>',\n \"chevron-down\": '<path d=\"m6 9 6 6 6-6\"/>',\n \"chevron-up\": '<path d=\"m18 15-6-6-6 6\"/>',\n \"chevron-right\": '<path d=\"m9 18 6-6-6-6\"/>',\n \"chevron-left\": '<path d=\"m15 18-6-6 6-6\"/>',\n \"chevrons-left\": '<path d=\"m11 17-5-5 5-5M18 17l-5-5 5-5\"/>',\n \"arrow-right\": '<path d=\"M5 12h14M12 5l7 7-7 7\"/>',\n folder:\n '<path d=\"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z\"/>',\n layers: '<path d=\"m12 2 10 5-10 5L2 7zM2 17l10 5 10-5M2 12l10 5 10-5\"/>',\n image:\n '<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"/><circle cx=\"9\" cy=\"9\" r=\"2\"/><path d=\"m21 15-3.1-3.1a2 2 0 0 0-2.8 0L6 21\"/>',\n \"git-branch\":\n '<path d=\"M6 3v12\"/><circle cx=\"18\" cy=\"6\" r=\"3\"/><circle cx=\"6\" cy=\"18\" r=\"3\"/><path d=\"M18 9a9 9 0 0 1-9 9\"/>',\n \"git-commit\": '<circle cx=\"12\" cy=\"12\" r=\"3\"/><path d=\"M3 12h6M15 12h6\"/>',\n activity: '<path d=\"M22 12h-4l-3 9L9 3l-3 9H2\"/>',\n tag: '<path d=\"M12.6 2.6A2 2 0 0 0 11.2 2H4a2 2 0 0 0-2 2v7.2a2 2 0 0 0 .6 1.4l8.7 8.7a2.4 2.4 0 0 0 3.4 0l6.6-6.6a2.4 2.4 0 0 0 0-3.4z\"/><circle cx=\"7.5\" cy=\"7.5\" r=\".5\" fill=\"currentColor\"/>',\n settings:\n '<path d=\"M12.2 2h-.4a2 2 0 0 0-2 2v.2a2 2 0 0 1-1 1.7l-.4.3a2 2 0 0 1-2 0l-.2-.1a2 2 0 0 0-2.7.7l-.2.4a2 2 0 0 0 .7 2.7l.2.1a2 2 0 0 1 1 1.7v.5a2 2 0 0 1-1 1.8l-.2.1a2 2 0 0 0-.7 2.7l.2.4a2 2 0 0 0 2.7.7l.2-.1a2 2 0 0 1 2 0l.4.3a2 2 0 0 1 1 1.7v.2a2 2 0 0 0 2 2h.4a2 2 0 0 0 2-2v-.2a2 2 0 0 1 1-1.7l.4-.3a2 2 0 0 1 2 0l.2.1a2 2 0 0 0 2.7-.7l.2-.4a2 2 0 0 0-.7-2.7l-.2-.1a2 2 0 0 1-1-1.8v-.5a2 2 0 0 1 1-1.7l.2-.1a2 2 0 0 0 .7-2.7l-.2-.4a2 2 0 0 0-2.7-.7l-.2.1a2 2 0 0 1-2 0l-.4-.3a2 2 0 0 1-1-1.7V4a2 2 0 0 0-2-2z\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/>',\n users:\n '<path d=\"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2\"/><circle cx=\"9\" cy=\"7\" r=\"4\"/><path d=\"M22 21v-2a4 4 0 0 0-3-3.9M16 3.1a4 4 0 0 1 0 7.8\"/>',\n user: '<path d=\"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2\"/><circle cx=\"12\" cy=\"7\" r=\"4\"/>',\n key: '<path d=\"m21 2-9.6 9.6M15.5 7.5l3 3L22 7l-3-3\"/><circle cx=\"7.5\" cy=\"15.5\" r=\"5.5\"/>',\n bell: '<path d=\"M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9M10.3 21a1.9 1.9 0 0 0 3.4 0\"/>',\n webhook:\n '<path d=\"M18 16.98h-5.99c-1.1 0-1.95.94-2.48 1.9A4 4 0 0 1 2 17c.01-.7.2-1.4.57-2M9 17l3.13-5.78c.53-.97.43-2.22-.26-3.07a4 4 0 1 1 6.58-4.2c.38.6.58 1.3.57 2M6 17a4 4 0 0 1 6-3.5\"/><path d=\"M12 17h6a2 2 0 1 1 0 4\"/>',\n code: '<path d=\"m16 18 6-6-6-6M8 6l-6 6 6 6\"/>',\n terminal: '<path d=\"m4 17 6-6-6-6M12 19h8\"/>',\n shield:\n '<path d=\"M20 13c0 5-3.5 7.5-7.7 8.9a1 1 0 0 1-.6 0C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.2-2.7a1.2 1.2 0 0 1 1.6 0C14.5 3.8 17 5 19 5a1 1 0 0 1 1 1z\"/>',\n search: '<circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.3-4.3\"/>',\n sun: '<circle cx=\"12\" cy=\"12\" r=\"4\"/><path d=\"M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M6.3 17.7l-1.4 1.4M19.1 4.9l-1.4 1.4\"/>',\n moon: '<path d=\"M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z\"/>',\n monitor: '<rect width=\"20\" height=\"14\" x=\"2\" y=\"3\" rx=\"2\"/><path d=\"M8 21h8M12 17v4\"/>',\n menu: '<path d=\"M4 6h16M4 12h16M4 18h16\"/>',\n \"more-horizontal\":\n '<circle cx=\"12\" cy=\"12\" r=\"1\"/><circle cx=\"19\" cy=\"12\" r=\"1\"/><circle cx=\"5\" cy=\"12\" r=\"1\"/>',\n \"log-out\": '<path d=\"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9\"/>',\n \"external-link\":\n '<path d=\"M15 3h6v6M10 14 21 3M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\"/>',\n copy: '<rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/>',\n \"check-circle\": '<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"m9 12 2 2 4-4\"/>',\n \"x-circle\": '<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"m15 9-6 6M9 9l6 6\"/>',\n \"alert-triangle\":\n '<path d=\"m21.7 18-8-14a2 2 0 0 0-3.5 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.7-3M12 9v4M12 17h.01\"/>',\n info: '<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 16v-4M12 8h.01\"/>',\n clock: '<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 6v6l4 2\"/>',\n loader: '<path d=\"M21 12a9 9 0 1 1-6.2-8.6\"/>',\n \"message-square\": '<path d=\"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z\"/>',\n columns: '<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"/><path d=\"M12 3v18\"/>',\n maximize:\n '<path d=\"M8 3H5a2 2 0 0 0-2 2v3M21 8V5a2 2 0 0 0-2-2h-3M3 16v3a2 2 0 0 0 2 2h3M16 21h3a2 2 0 0 0 2-2v-3\"/>',\n \"zoom-in\": '<circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.3-4.3M11 8v6M8 11h6\"/>',\n book: '<path d=\"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20\"/>',\n server:\n '<rect width=\"20\" height=\"8\" x=\"2\" y=\"2\" rx=\"2\"/><rect width=\"20\" height=\"8\" x=\"2\" y=\"14\" rx=\"2\"/><path d=\"M6 6h.01M6 18h.01\"/>',\n} as const;\n\n/** Every icon name available in the sprite. */\nexport type IconName = keyof typeof ICON_PATHS;\n"],"mappings":";;;;;;;;;AAKA,MAAa,aAAa;CACxB,OAAO;CACP,GAAG;CACH,MAAM;CACN,gBAAgB;CAChB,cAAc;CACd,iBAAiB;CACjB,gBAAgB;CAChB,iBAAiB;CACjB,eAAe;CACf,QACE;CACF,QAAQ;CACR,OACE;CACF,cACE;CACF,cAAc;CACd,UAAU;CACV,KAAK;CACL,UACE;CACF,OACE;CACF,MAAM;CACN,KAAK;CACL,MAAM;CACN,SACE;CACF,MAAM;CACN,UAAU;CACV,QACE;CACF,QAAQ;CACR,KAAK;CACL,MAAM;CACN,SAAS;CACT,MAAM;CACN,mBACE;CACF,WAAW;CACX,iBACE;CACF,MAAM;CACN,gBAAgB;CAChB,YAAY;CACZ,kBACE;CACF,MAAM;CACN,OAAO;CACP,QAAQ;CACR,kBAAkB;CAClB,SAAS;CACT,UACE;CACF,WAAW;CACX,MAAM;CACN,QACE;AACJ"}
1
+ {"version":3,"file":"paths.mjs","names":[],"sources":["../../../src/ui/icons/paths.ts"],"sourcesContent":["/**\n * Icon set: inner SVG markup per icon on a 24x24 grid, stroke-based\n * (Lucide-style, ISC). Add an icon here and it is in the sprite and the\n * `IconName` union automatically.\n */\nexport const ICON_PATHS = {\n check: '<path d=\"M20 6 9 17l-5-5\"/>',\n x: '<path d=\"M18 6 6 18M6 6l12 12\"/>',\n plus: '<path d=\"M5 12h14M12 5v14\"/>',\n \"chevron-down\": '<path d=\"m6 9 6 6 6-6\"/>',\n \"chevron-up\": '<path d=\"m18 15-6-6-6 6\"/>',\n \"chevron-right\": '<path d=\"m9 18 6-6-6-6\"/>',\n \"chevron-left\": '<path d=\"m15 18-6-6 6-6\"/>',\n \"chevrons-left\": '<path d=\"m11 17-5-5 5-5M18 17l-5-5 5-5\"/>',\n \"arrow-right\": '<path d=\"M5 12h14M12 5l7 7-7 7\"/>',\n folder:\n '<path d=\"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z\"/>',\n layers: '<path d=\"m12 2 10 5-10 5L2 7zM2 17l10 5 10-5M2 12l10 5 10-5\"/>',\n image:\n '<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"/><circle cx=\"9\" cy=\"9\" r=\"2\"/><path d=\"m21 15-3.1-3.1a2 2 0 0 0-2.8 0L6 21\"/>',\n \"git-branch\":\n '<path d=\"M6 3v12\"/><circle cx=\"18\" cy=\"6\" r=\"3\"/><circle cx=\"6\" cy=\"18\" r=\"3\"/><path d=\"M18 9a9 9 0 0 1-9 9\"/>',\n \"git-commit\": '<circle cx=\"12\" cy=\"12\" r=\"3\"/><path d=\"M3 12h6M15 12h6\"/>',\n activity: '<path d=\"M22 12h-4l-3 9L9 3l-3 9H2\"/>',\n sparkles:\n '<path d=\"M9.9 15.5A2 2 0 0 0 8.5 14.1l-6.1-1.6a.5.5 0 0 1 0-1L8.5 9.9A2 2 0 0 0 9.9 8.5l1.6-6.1a.5.5 0 0 1 1 0l1.6 6.1a2 2 0 0 0 1.4 1.4l6.1 1.6a.5.5 0 0 1 0 1l-6.1 1.6a2 2 0 0 0-1.4 1.4l-1.6 6.1a.5.5 0 0 1-1 0z\"/><path d=\"M20 3v4M22 5h-4M4 17v2M5 18H3\"/>',\n tag: '<path d=\"M12.6 2.6A2 2 0 0 0 11.2 2H4a2 2 0 0 0-2 2v7.2a2 2 0 0 0 .6 1.4l8.7 8.7a2.4 2.4 0 0 0 3.4 0l6.6-6.6a2.4 2.4 0 0 0 0-3.4z\"/><circle cx=\"7.5\" cy=\"7.5\" r=\".5\" fill=\"currentColor\"/>',\n settings:\n '<path d=\"M12.2 2h-.4a2 2 0 0 0-2 2v.2a2 2 0 0 1-1 1.7l-.4.3a2 2 0 0 1-2 0l-.2-.1a2 2 0 0 0-2.7.7l-.2.4a2 2 0 0 0 .7 2.7l.2.1a2 2 0 0 1 1 1.7v.5a2 2 0 0 1-1 1.8l-.2.1a2 2 0 0 0-.7 2.7l.2.4a2 2 0 0 0 2.7.7l.2-.1a2 2 0 0 1 2 0l.4.3a2 2 0 0 1 1 1.7v.2a2 2 0 0 0 2 2h.4a2 2 0 0 0 2-2v-.2a2 2 0 0 1 1-1.7l.4-.3a2 2 0 0 1 2 0l.2.1a2 2 0 0 0 2.7-.7l.2-.4a2 2 0 0 0-.7-2.7l-.2-.1a2 2 0 0 1-1-1.8v-.5a2 2 0 0 1 1-1.7l.2-.1a2 2 0 0 0 .7-2.7l-.2-.4a2 2 0 0 0-2.7-.7l-.2.1a2 2 0 0 1-2 0l-.4-.3a2 2 0 0 1-1-1.7V4a2 2 0 0 0-2-2z\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/>',\n users:\n '<path d=\"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2\"/><circle cx=\"9\" cy=\"7\" r=\"4\"/><path d=\"M22 21v-2a4 4 0 0 0-3-3.9M16 3.1a4 4 0 0 1 0 7.8\"/>',\n user: '<path d=\"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2\"/><circle cx=\"12\" cy=\"7\" r=\"4\"/>',\n key: '<path d=\"m21 2-9.6 9.6M15.5 7.5l3 3L22 7l-3-3\"/><circle cx=\"7.5\" cy=\"15.5\" r=\"5.5\"/>',\n bell: '<path d=\"M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9M10.3 21a1.9 1.9 0 0 0 3.4 0\"/>',\n webhook:\n '<path d=\"M18 16.98h-5.99c-1.1 0-1.95.94-2.48 1.9A4 4 0 0 1 2 17c.01-.7.2-1.4.57-2M9 17l3.13-5.78c.53-.97.43-2.22-.26-3.07a4 4 0 1 1 6.58-4.2c.38.6.58 1.3.57 2M6 17a4 4 0 0 1 6-3.5\"/><path d=\"M12 17h6a2 2 0 1 1 0 4\"/>',\n code: '<path d=\"m16 18 6-6-6-6M8 6l-6 6 6 6\"/>',\n terminal: '<path d=\"m4 17 6-6-6-6M12 19h8\"/>',\n shield:\n '<path d=\"M20 13c0 5-3.5 7.5-7.7 8.9a1 1 0 0 1-.6 0C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.2-2.7a1.2 1.2 0 0 1 1.6 0C14.5 3.8 17 5 19 5a1 1 0 0 1 1 1z\"/>',\n search: '<circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.3-4.3\"/>',\n sun: '<circle cx=\"12\" cy=\"12\" r=\"4\"/><path d=\"M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M6.3 17.7l-1.4 1.4M19.1 4.9l-1.4 1.4\"/>',\n moon: '<path d=\"M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z\"/>',\n monitor: '<rect width=\"20\" height=\"14\" x=\"2\" y=\"3\" rx=\"2\"/><path d=\"M8 21h8M12 17v4\"/>',\n menu: '<path d=\"M4 6h16M4 12h16M4 18h16\"/>',\n \"more-horizontal\":\n '<circle cx=\"12\" cy=\"12\" r=\"1\"/><circle cx=\"19\" cy=\"12\" r=\"1\"/><circle cx=\"5\" cy=\"12\" r=\"1\"/>',\n \"log-out\": '<path d=\"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9\"/>',\n \"external-link\":\n '<path d=\"M15 3h6v6M10 14 21 3M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\"/>',\n copy: '<rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/>',\n \"check-circle\": '<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"m9 12 2 2 4-4\"/>',\n \"x-circle\": '<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"m15 9-6 6M9 9l6 6\"/>',\n \"alert-triangle\":\n '<path d=\"m21.7 18-8-14a2 2 0 0 0-3.5 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.7-3M12 9v4M12 17h.01\"/>',\n info: '<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 16v-4M12 8h.01\"/>',\n clock: '<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 6v6l4 2\"/>',\n loader: '<path d=\"M21 12a9 9 0 1 1-6.2-8.6\"/>',\n \"message-square\": '<path d=\"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z\"/>',\n columns: '<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"/><path d=\"M12 3v18\"/>',\n maximize:\n '<path d=\"M8 3H5a2 2 0 0 0-2 2v3M21 8V5a2 2 0 0 0-2-2h-3M3 16v3a2 2 0 0 0 2 2h3M16 21h3a2 2 0 0 0 2-2v-3\"/>',\n \"zoom-in\": '<circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.3-4.3M11 8v6M8 11h6\"/>',\n book: '<path d=\"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20\"/>',\n server:\n '<rect width=\"20\" height=\"8\" x=\"2\" y=\"2\" rx=\"2\"/><rect width=\"20\" height=\"8\" x=\"2\" y=\"14\" rx=\"2\"/><path d=\"M6 6h.01M6 18h.01\"/>',\n} as const;\n\n/** Every icon name available in the sprite. */\nexport type IconName = keyof typeof ICON_PATHS;\n"],"mappings":";;;;;;;;;AAKA,MAAa,aAAa;CACxB,OAAO;CACP,GAAG;CACH,MAAM;CACN,gBAAgB;CAChB,cAAc;CACd,iBAAiB;CACjB,gBAAgB;CAChB,iBAAiB;CACjB,eAAe;CACf,QACE;CACF,QAAQ;CACR,OACE;CACF,cACE;CACF,cAAc;CACd,UAAU;CACV,UACE;CACF,KAAK;CACL,UACE;CACF,OACE;CACF,MAAM;CACN,KAAK;CACL,MAAM;CACN,SACE;CACF,MAAM;CACN,UAAU;CACV,QACE;CACF,QAAQ;CACR,KAAK;CACL,MAAM;CACN,SAAS;CACT,MAAM;CACN,mBACE;CACF,WAAW;CACX,iBACE;CACF,MAAM;CACN,gBAAgB;CAChB,YAAY;CACZ,kBACE;CACF,MAAM;CACN,OAAO;CACP,QAAQ;CACR,kBAAkB;CAClB,SAAS;CACT,UACE;CACF,WAAW;CACX,MAAM;CACN,QACE;AACJ"}
@@ -0,0 +1,82 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { css } from "./css.mjs";
5
+ import { Button } from "./buttons.mjs";
6
+ import { Meta } from "./feedback.mjs";
7
+ import { jsx, jsxs } from "hono/jsx/jsx-runtime";
8
+ //#region src/ui/preview-embed.tsx
9
+ /**
10
+ * Sandbox of the embedded story frame (ADR 0006, decision 9). Scripts only: no `allow-same-origin`,
11
+ * so a customer's Storybook never runs with the app's origin and its cookies, and no top navigation,
12
+ * forms or popups.
13
+ */
14
+ const PREVIEW_SANDBOX = "allow-scripts";
15
+ const previewHost = css`
16
+ /* preview-embed */
17
+ display: grid;
18
+ min-height: 12rem;
19
+ border: 1px solid var(--border-subtle);
20
+ border-radius: var(--radius);
21
+ background: var(--surface-muted);
22
+ overflow: hidden;
23
+ `;
24
+ const previewIdle = css`
25
+ /* preview-embed-idle */
26
+ display: grid;
27
+ gap: 0.5rem;
28
+ place-items: center;
29
+ align-content: center;
30
+ padding: 2rem 1rem;
31
+ text-align: center;
32
+ `;
33
+ const previewFrame = css`
34
+ /* preview-embed-frame */
35
+ display: block;
36
+ width: 100%;
37
+ height: 28rem;
38
+ border: 0;
39
+ background: var(--surface-card);
40
+ `;
41
+ /**
42
+ * A published story in a sandboxed frame that loads on click. The frame is inert markup (a
43
+ * `<template>`) until the visitor asks for it, so opening a page never runs the customer's
44
+ * Storybook; the shell script moves the frame in on click and, for a signed URL, re-points it
45
+ * to a fresh token before the current one expires.
46
+ */
47
+ const PreviewEmbed = ({ src, title, refresh }) => /* @__PURE__ */ jsxs("div", {
48
+ class: previewHost,
49
+ "data-preview-embed": true,
50
+ ...refresh ? {
51
+ "data-preview-refresh-url": refresh.url,
52
+ "data-preview-path": refresh.path,
53
+ "data-preview-refresh-seconds": String(refresh.seconds)
54
+ } : {},
55
+ children: [/* @__PURE__ */ jsxs("div", {
56
+ class: previewIdle,
57
+ "data-preview-idle": true,
58
+ children: [/* @__PURE__ */ jsx(Button, {
59
+ variant: "secondary",
60
+ size: "sm",
61
+ icon: "image",
62
+ "data-preview-load": true,
63
+ children: "Load preview"
64
+ }), /* @__PURE__ */ jsx(Meta, {
65
+ as: "span",
66
+ children: "Runs the published story in a sandboxed frame, only when you ask."
67
+ })]
68
+ }), /* @__PURE__ */ jsx("template", {
69
+ "data-preview-template": true,
70
+ children: /* @__PURE__ */ jsx("iframe", {
71
+ class: previewFrame,
72
+ src,
73
+ title,
74
+ sandbox: PREVIEW_SANDBOX,
75
+ referrerpolicy: "no-referrer"
76
+ })
77
+ })]
78
+ });
79
+ //#endregion
80
+ export { PREVIEW_SANDBOX, PreviewEmbed };
81
+
82
+ //# sourceMappingURL=preview-embed.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preview-embed.mjs","names":[],"sources":["../../src/ui/preview-embed.tsx"],"sourcesContent":["import type { FC } from \"hono/jsx\";\nimport { Button } from \"./buttons.tsx\";\nimport { css } from \"./css.ts\";\nimport { Meta } from \"./feedback.tsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\n/**\n * Sandbox of the embedded story frame (ADR 0006, decision 9). Scripts only: no `allow-same-origin`,\n * so a customer's Storybook never runs with the app's origin and its cookies, and no top navigation,\n * forms or popups.\n */\nexport const PREVIEW_SANDBOX = \"allow-scripts\";\n\nconst previewHost = css`\n /* preview-embed */\n display: grid;\n min-height: 12rem;\n border: 1px solid var(--border-subtle);\n border-radius: var(--radius);\n background: var(--surface-muted);\n overflow: hidden;\n`;\n\nconst previewIdle = css`\n /* preview-embed-idle */\n display: grid;\n gap: 0.5rem;\n place-items: center;\n align-content: center;\n padding: 2rem 1rem;\n text-align: center;\n`;\n\nconst previewFrame = css`\n /* preview-embed-frame */\n display: block;\n width: 100%;\n height: 28rem;\n border: 0;\n background: var(--surface-card);\n`;\n\n/**\n * A published story in a sandboxed frame that loads on click. The frame is inert markup (a\n * `<template>`) until the visitor asks for it, so opening a page never runs the customer's\n * Storybook; the shell script moves the frame in on click and, for a signed URL, re-points it\n * to a fresh token before the current one expires.\n */\nexport const PreviewEmbed: FC<{\n src: string;\n title: string;\n /** For a frame under a signed URL: where to ask for a fresh root, the path after it, how often. */\n refresh?: { url: string; path: string; seconds: number };\n}> = ({ src, title, refresh }) => (\n <div\n class={previewHost}\n data-preview-embed\n {...(refresh\n ? {\n \"data-preview-refresh-url\": refresh.url,\n \"data-preview-path\": refresh.path,\n \"data-preview-refresh-seconds\": String(refresh.seconds),\n }\n : {})}\n >\n <div class={previewIdle} data-preview-idle>\n <Button variant=\"secondary\" size=\"sm\" icon=\"image\" data-preview-load>\n Load preview\n </Button>\n <Meta as=\"span\">Runs the published story in a sandboxed frame, only when you ask.</Meta>\n </div>\n <template data-preview-template>\n <iframe\n class={previewFrame}\n src={src}\n title={title}\n sandbox={PREVIEW_SANDBOX}\n referrerpolicy=\"no-referrer\"\n />\n </template>\n </div>\n);\n"],"mappings":";;;;;;;;;;;;;AAYA,MAAa,kBAAkB;AAE/B,MAAM,cAAc,GAAG;;;;;;;;;AAUvB,MAAM,cAAc,GAAG;;;;;;;;;AAUvB,MAAM,eAAe,GAAG;;;;;;;;;;;;;;AAexB,MAAa,gBAKP,EAAE,KAAK,OAAO,cAClB,qBAAC,OAAD;CACE,OAAO;CACP,sBAAA;CACA,GAAK,UACD;EACE,4BAA4B,QAAQ;EACpC,qBAAqB,QAAQ;EAC7B,gCAAgC,OAAO,QAAQ,OAAO;CACxD,IACA,CAAC;CATP,UAAA,CAWE,qBAAC,OAAD;EAAK,OAAO;EAAa,qBAAA;EAAzB,UAAA,CACE,oBAAC,QAAD;GAAQ,SAAQ;GAAY,MAAK;GAAK,MAAK;GAAQ,qBAAA;GAAkB,UAAA;EAE7D,CAAA,GACR,oBAAC,MAAD;GAAM,IAAG;GAAO,UAAA;EAAuE,CAAA,CACpF;CACL,CAAA,GAAA,oBAAC,YAAD;EAAU,yBAAA;EACR,UAAA,oBAAC,UAAD;GACE,OAAO;GACF;GACE;GACP,SAAS;GACT,gBAAe;EAChB,CAAA;CACO,CAAA,CACP"}
@@ -12,6 +12,27 @@ function csrfPart() {
12
12
  if(meta){ e.detail.headers['x-csrf-token']=meta.getAttribute('content'); }
13
13
  });`;
14
14
  }
15
+ /**
16
+ * System page: report the origin the admin opened it with, so a deployment learns its public URL
17
+ * without a redeploy (ADR 0007, decision 6). The server keeps the first report and answers 201 only
18
+ * when it stored a new one, which is when the page reloads to show it.
19
+ */
20
+ function originPart() {
21
+ return `
22
+ function reportOrigin(){
23
+ var el=document.querySelector('[data-report-origin]');
24
+ if(!el || el.getAttribute('data-reported')) return;
25
+ el.setAttribute('data-reported','1');
26
+ var meta=document.querySelector('meta[name="csrf-token"]');
27
+ fetch(el.getAttribute('data-report-origin'),{
28
+ method:'POST',
29
+ credentials:'same-origin',
30
+ headers:{'content-type':'application/json','x-csrf-token':meta ? meta.getAttribute('content') : ''},
31
+ body:JSON.stringify({origin:location.origin})
32
+ }).then(function(r){ if(r.status===201) location.reload(); }).catch(function(){});
33
+ }
34
+ inits.push(reportOrigin);`;
35
+ }
15
36
  /** Theme menu: apply, persist, and mark the current choice. */
16
37
  function themePart() {
17
38
  return `
@@ -206,6 +227,67 @@ function filterPart() {
206
227
  scope.querySelectorAll('[data-filter-empty]').forEach(function(el){ el.hidden=shown>0; });
207
228
  });`;
208
229
  }
230
+ /**
231
+ * Click-to-load story previews: move the inert frame out of its template (nothing runs before). A
232
+ * frame under a signed URL first asks for a fresh root, so a page left open for hours does not mount
233
+ * an expired token; it keeps the rendered one when that request fails. Afterwards the root is renewed
234
+ * every `data-preview-refresh-seconds`; a failed renewal is retried a few times a minute apart.
235
+ */
236
+ function previewPart() {
237
+ return `
238
+ document.addEventListener('click',function(e){
239
+ var btn=e.target instanceof Element ? e.target.closest('[data-preview-load]') : null;
240
+ var host=btn ? btn.closest('[data-preview-embed]') : null;
241
+ var tpl=host ? host.querySelector('template[data-preview-template]') : null;
242
+ if(!host||!tpl||host.getAttribute('data-preview-loading')) return;
243
+ host.setAttribute('data-preview-loading','1');
244
+ var idle=host.querySelector('[data-preview-idle]');
245
+ var path=host.getAttribute('data-preview-path');
246
+ var fresh=fetchPreviewBase(host).catch(function(){ return null; });
247
+ fresh.then(function(base){
248
+ var content=document.importNode(tpl.content,true);
249
+ var frame=content.querySelector('iframe');
250
+ if(frame&&base&&path) frame.src=base+path;
251
+ if(idle) idle.hidden=true;
252
+ host.appendChild(content);
253
+ tpl.remove();
254
+ keepPreviewFresh(host);
255
+ });
256
+ });`;
257
+ }
258
+ /** Fetch a fresh signed root for a preview host and keep it renewed (see {@link previewPart}). */
259
+ function previewRefreshPart() {
260
+ return `
261
+ function fetchPreviewBase(host){
262
+ var url=host.getAttribute('data-preview-refresh-url');
263
+ if(!url) return Promise.resolve(null);
264
+ return fetch(url,{headers:{accept:'application/json'}}).then(function(r){
265
+ if(!r.ok) throw new Error('preview-token');
266
+ return r.json();
267
+ }).then(function(body){ return body&&body.base ? body.base : null; });
268
+ }
269
+ function keepPreviewFresh(host){
270
+ var url=host.getAttribute('data-preview-refresh-url');
271
+ var path=host.getAttribute('data-preview-path');
272
+ var seconds=Number(host.getAttribute('data-preview-refresh-seconds'));
273
+ if(!url||!path||!(seconds>0)) return;
274
+ var RETRY_MS=60000, MAX_RETRIES=3;
275
+ function renew(tries){
276
+ var frame=host.querySelector('iframe');
277
+ if(!host.isConnected||!frame) return;
278
+ fetchPreviewBase(host).then(function(base){
279
+ if(base) frame.src=base+path;
280
+ else throw new Error('preview-token');
281
+ }).catch(function(){
282
+ if(tries<MAX_RETRIES) setTimeout(function(){ renew(tries+1); },RETRY_MS);
283
+ });
284
+ }
285
+ var timer=setInterval(function(){
286
+ if(!host.isConnected||!host.querySelector('iframe')){ clearInterval(timer); return; }
287
+ renew(0);
288
+ },seconds*1000);
289
+ }`;
290
+ }
209
291
  /** Thin top progress bar while an HTMX request (e.g. snapshot navigation) is in flight. */
210
292
  function loadingPart() {
211
293
  return `
@@ -237,6 +319,7 @@ function clientScript() {
237
319
  if(!shell) return;
238
320
  var inits=[];${[
239
321
  csrfPart(),
322
+ originPart(),
240
323
  themePart(),
241
324
  sidebarPart(),
242
325
  dropdownPart(),
@@ -244,6 +327,8 @@ function clientScript() {
244
327
  formPart(),
245
328
  copyPart(),
246
329
  filterPart(),
330
+ previewPart(),
331
+ previewRefreshPart(),
247
332
  loadingPart(),
248
333
  focusPart(),
249
334
  reviewScript()
@@ -1 +1 @@
1
- {"version":3,"file":"client-script.mjs","names":[],"sources":["../../../src/ui/shell/client-script.ts"],"sourcesContent":["import { iconSpriteHref } from \"../icons/sprite.ts\";\nimport { reviewScript } from \"./review-script.ts\";\n\n/*\n * The page-end script is re-executed whenever HTMX swaps `body` (review\n * navigation does). So it is split in two: listeners are bound ONCE on\n * `document` and look elements up lazily; `inits` are re-run per render for\n * anything that needs wiring to freshly swapped elements.\n */\n\n/** Attach the CSRF token to every HTMX request. */\nfunction csrfPart(): string {\n return `\n document.addEventListener('htmx:configRequest',function(e){\n var meta=document.querySelector('meta[name=\"csrf-token\"]');\n if(meta){ e.detail.headers['x-csrf-token']=meta.getAttribute('content'); }\n });`;\n}\n\n/** Theme menu: apply, persist, and mark the current choice. */\nfunction themePart(): string {\n return `\n function markTheme(){\n var cur=document.documentElement.getAttribute('data-theme')||'system';\n document.querySelectorAll('[data-theme-set]').forEach(function(item){\n item.setAttribute('aria-current', item.getAttribute('data-theme-set')===cur ? 'true' : 'false');\n });\n }\n inits.push(markTheme);\n document.addEventListener('click',function(e){\n var item=e.target instanceof Element ? e.target.closest('[data-theme-set]') : null;\n if(!item) return;\n var next=item.getAttribute('data-theme-set');\n shell.applyTheme(next);\n shell.writeCookie(shell.themeCookie,next);\n markTheme();\n });`;\n}\n\n/** Desktop rail collapse (persisted) and the mobile drawer. */\nfunction sidebarPart(): string {\n return `\n function setDrawer(open){\n var sidebar=document.getElementById('sidebar');\n if(!sidebar) return;\n sidebar.classList.toggle('sidebar--open',open);\n var backdrop=document.querySelector('[data-sidebar-backdrop]');\n if(backdrop){ backdrop.hidden=!open; }\n var btn=document.querySelector('[data-sidebar-toggle]');\n if(btn){ btn.setAttribute('aria-expanded',open?'true':'false'); }\n }\n document.addEventListener('click',function(e){\n var t=e.target instanceof Element ? e.target : null;\n if(!t) return;\n if(t.closest('[data-sidebar-toggle]')){\n var sidebar=document.getElementById('sidebar');\n setDrawer(!(sidebar&&sidebar.classList.contains('sidebar--open')));\n } else if(t.closest('[data-sidebar-backdrop]')){\n setDrawer(false);\n } else if(t.closest('[data-sidebar-collapse]')){\n var next=document.documentElement.getAttribute('data-sidebar')==='rail' ? 'full' : 'rail';\n shell.applySidebar(next);\n shell.writeCookie(shell.sidebarCookie,next);\n }\n });\n document.addEventListener('keydown',function(e){\n var sidebar=document.getElementById('sidebar');\n if(e.key==='Escape'&&sidebar&&sidebar.classList.contains('sidebar--open')){\n setDrawer(false);\n var btn=document.querySelector('[data-sidebar-toggle]');\n if(btn) btn.focus();\n }\n });`;\n}\n\n/** Native <details> dropdowns: close on outside click, Escape, or choosing an item. */\nfunction dropdownPart(): string {\n return `\n document.addEventListener('click',function(e){\n var target=e.target instanceof Element ? e.target : null;\n var owner=target ? target.closest('details[data-dropdown]') : null;\n document.querySelectorAll('details[data-dropdown][open]').forEach(function(d){\n if(d!==owner) d.removeAttribute('open');\n });\n if(owner&&target&&target.closest('[role=\"menuitem\"]')){ owner.removeAttribute('open'); }\n });\n document.addEventListener('keydown',function(e){\n if(e.key!=='Escape') return;\n var open=document.querySelector('details[data-dropdown][open]');\n if(!open) return;\n open.removeAttribute('open');\n var summary=open.querySelector('summary');\n if(summary) summary.focus();\n });`;\n}\n\n/** Build and show one toast element (text only, never HTML). */\nfunction toastRenderPart(): string {\n return `\n var toastIcons={success:'check-circle',danger:'x-circle',warning:'alert-triangle',info:'info'};\n function toastIcon(tone){\n var ns='http://www.w3.org/2000/svg';\n var svg=document.createElementNS(ns,'svg');\n var attrs={viewBox:'0 0 24 24',width:'18',height:'18','aria-hidden':'true',fill:'none',\n stroke:'currentColor','stroke-width':'2','stroke-linecap':'round','stroke-linejoin':'round'};\n Object.keys(attrs).forEach(function(name){ svg.setAttribute(name,attrs[name]); });\n var use=document.createElementNS(ns,'use');\n use.setAttribute('href','${iconSpriteHref}#i-'+toastIcons[tone]);\n svg.appendChild(use);\n return svg;\n }\n function toast(detail){\n var region=document.querySelector('[data-toast-region]');\n if(!region||!detail||!detail.message) return;\n var tone=toastIcons[detail.tone] ? detail.tone : 'info';\n var el=document.createElement('div');\n el.className='toast';\n el.setAttribute('data-tone',tone);\n el.setAttribute('role',tone==='danger'?'alert':'status');\n var text=document.createElement('span');\n text.textContent=String(detail.message);\n el.appendChild(toastIcon(tone));\n el.appendChild(text);\n region.appendChild(el);\n setTimeout(function(){ el.remove(); },tone==='danger'?8000:4000);\n }`;\n}\n\n/**\n * Toasts. Sources: an HX-Trigger(-After-Swap) \"showToast\" event, and a toast\n * queued in sessionStorage by a form with data-toast (review actions reload\n * or redirect, so the message has to survive the navigation).\n */\nfunction toastPart(): string {\n return `${toastRenderPart()}\n var TOAST_KEY='ss_toast';\n function queueToast(detail){ try{ sessionStorage.setItem(TOAST_KEY,JSON.stringify(detail)); }catch(_){} }\n function clearToast(){ try{ sessionStorage.removeItem(TOAST_KEY); }catch(_){} }\n function flushQueued(){\n try{\n var raw=sessionStorage.getItem(TOAST_KEY);\n if(!raw) return;\n sessionStorage.removeItem(TOAST_KEY);\n toast(JSON.parse(raw));\n }catch(_){}\n }\n function flushFlash(){\n var raw=shell.readCookie('storyshelf_flash');\n if(!raw) return;\n document.cookie='storyshelf_flash=; Path=/; Max-Age=0; SameSite=Lax';\n try{ toast(JSON.parse(raw)); }catch(_){}\n }\n function flushToast(){ flushQueued(); flushFlash(); }\n inits.push(flushToast);\n document.addEventListener('htmx:beforeRequest',function(e){\n var form=e.detail && e.detail.elt && e.detail.elt.closest ? e.detail.elt.closest('[data-toast]') : null;\n if(form){ queueToast({message:form.getAttribute('data-toast'),tone:form.getAttribute('data-toast-tone')||'success'}); }\n });\n function failed(){ clearToast(); toast({message:'Something went wrong. Please try again.',tone:'danger'}); }\n document.addEventListener('htmx:responseError',failed);\n document.addEventListener('htmx:sendError',failed);`;\n}\n\n/**\n * Forms and body swaps. HTMX does not swap 4xx responses, but this app answers validation\n * failures with the re-rendered page (400/409/422) so field errors show.\n * \\`data-confirm\\` asks before submitting plain (non-HTMX) forms.\n */\nfunction formPart(): string {\n return `\n document.addEventListener('htmx:beforeSwap',function(e){\n // A body swap only replaces <body>, so classes first used by the new page\n // would be unstyled: adopt the stylesheet the server collected for it.\n var match=/<style id=\"storyshelf-css\">([\\\\s\\\\S]*?)<\\\\/style>/.exec(e.detail.serverResponse||'');\n var sheet=document.getElementById('storyshelf-css');\n if(match&&sheet&&e.detail.target===document.body){ sheet.textContent=match[1]; }\n var status=e.detail.xhr ? e.detail.xhr.status : 0;\n if(status===400||status===409||status===422){\n e.detail.shouldSwap=true;\n e.detail.isError=false;\n clearToast();\n }\n });\n document.addEventListener('submit',function(e){\n var form=e.target instanceof Element ? e.target.closest('form[data-confirm]') : null;\n if(form&&!window.confirm(form.getAttribute('data-confirm')||'Are you sure?')){ e.preventDefault(); }\n });`;\n}\n\n/** Copy buttons: `data-copy` holds the text; confirm with a toast. */\nfunction copyPart(): string {\n return `\n document.addEventListener('click',function(e){\n var btn=e.target instanceof Element ? e.target.closest('[data-copy]') : null;\n if(!btn||!navigator.clipboard) return;\n navigator.clipboard.writeText(btn.getAttribute('data-copy')||'').then(function(){\n toast({message:'Copied to clipboard',tone:'success'});\n },function(){\n toast({message:'Could not copy',tone:'danger'});\n });\n });`;\n}\n\n/** Client-side list filtering for `[data-filter-input]` boxes. */\nfunction filterPart(): string {\n return `\n document.addEventListener('input',function(e){\n var input=e.target instanceof Element ? e.target.closest('[data-filter-input]') : null;\n if(!input) return;\n var scope=input.closest('[data-filter-scope]')||document;\n var query=input.value.trim().toLowerCase();\n var shown=0;\n scope.querySelectorAll('[data-filter-item]').forEach(function(item){\n var match=!query||(item.getAttribute('data-filter-text')||'').toLowerCase().indexOf(query)>-1;\n item.hidden=!match;\n if(match) shown++;\n });\n scope.querySelectorAll('[data-filter-empty]').forEach(function(el){ el.hidden=shown>0; });\n });`;\n}\n\n/** Thin top progress bar while an HTMX request (e.g. snapshot navigation) is in flight. */\nfunction loadingPart(): string {\n return `\n var pending=0;\n function setLoading(delta){\n pending=Math.max(0,pending+delta);\n document.documentElement.classList.toggle('ss-loading',pending>0);\n }\n document.addEventListener('htmx:beforeRequest',function(){ setLoading(1); });\n document.addEventListener('htmx:afterRequest',function(){ setLoading(-1); });`;\n}\n\n/** Keep keyboard focus sensible after HTMX swaps. */\nfunction focusPart(): string {\n return `\n document.addEventListener('htmx:afterSwap',function(e){\n var target=e.detail && e.detail.target;\n if(target&&target.querySelector&&target!==document.body){\n var focusable=target.querySelector('[autofocus], input, select, textarea, button');\n if(focusable) try{ focusable.focus(); }catch(_){}\n }\n });`;\n}\n\n/** Page-end script wiring up the shell's interactive behavior (idempotent). */\nexport function clientScript(): string {\n const parts = [\n csrfPart(),\n themePart(),\n sidebarPart(),\n dropdownPart(),\n toastPart(),\n formPart(),\n copyPart(),\n filterPart(),\n loadingPart(),\n focusPart(),\n reviewScript(),\n ];\n return `\n (function(){\n if(window.__ssInit){ window.__ssInit(); return; }\n var shell=window.__storyshelfShell;\n if(!shell) return;\n var inits=[];${parts.join(\"\\n\")}\n window.__ssInit=function(){ inits.forEach(function(fn){ fn(); }); };\n window.__ssInit();\n })();\n `.trim();\n}\n"],"mappings":";;;;;;;AAWA,SAAS,WAAmB;CAC1B,OAAO;;;;;AAKT;;AAGA,SAAS,YAAoB;CAC3B,OAAO;;;;;;;;;;;;;;;;AAgBT;;AAGA,SAAS,cAAsB;CAC7B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCT;;AAGA,SAAS,eAAuB;CAC9B,OAAO;;;;;;;;;;;;;;;;;AAiBT;;AAGA,SAAS,kBAA0B;CACjC,OAAO;;;;;;;;;iCASwB,eAAe;;;;;;;;;;;;;;;;;;;AAmBhD;;;;;;AAOA,SAAS,YAAoB;CAC3B,OAAO,GAAG,gBAAgB,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2B9B;;;;;;AAOA,SAAS,WAAmB;CAC1B,OAAO;;;;;;;;;;;;;;;;;;AAkBT;;AAGA,SAAS,WAAmB;CAC1B,OAAO;;;;;;;;;;AAUT;;AAGA,SAAS,aAAqB;CAC5B,OAAO;;;;;;;;;;;;;;AAcT;;AAGA,SAAS,cAAsB;CAC7B,OAAO;;;;;;;;AAQT;;AAGA,SAAS,YAAoB;CAC3B,OAAO;;;;;;;;AAQT;;AAGA,SAAgB,eAAuB;CAcrC,OAAO;;;;;mBAKU;EAjBf,SAAS;EACT,UAAU;EACV,YAAY;EACZ,aAAa;EACb,UAAU;EACV,SAAS;EACT,SAAS;EACT,WAAW;EACX,YAAY;EACZ,UAAU;EACV,aAAa;CAOM,CAAC,CAAC,KAAK,IAAI,EAAE;;;;IAIhC,KAAK;AACT"}
1
+ {"version":3,"file":"client-script.mjs","names":[],"sources":["../../../src/ui/shell/client-script.ts"],"sourcesContent":["import { iconSpriteHref } from \"../icons/sprite.ts\";\nimport { reviewScript } from \"./review-script.ts\";\n\n/*\n * The page-end script is re-executed whenever HTMX swaps `body` (review\n * navigation does). So it is split in two: listeners are bound ONCE on\n * `document` and look elements up lazily; `inits` are re-run per render for\n * anything that needs wiring to freshly swapped elements.\n */\n\n/** Attach the CSRF token to every HTMX request. */\nfunction csrfPart(): string {\n return `\n document.addEventListener('htmx:configRequest',function(e){\n var meta=document.querySelector('meta[name=\"csrf-token\"]');\n if(meta){ e.detail.headers['x-csrf-token']=meta.getAttribute('content'); }\n });`;\n}\n\n/**\n * System page: report the origin the admin opened it with, so a deployment learns its public URL\n * without a redeploy (ADR 0007, decision 6). The server keeps the first report and answers 201 only\n * when it stored a new one, which is when the page reloads to show it.\n */\nfunction originPart(): string {\n return `\n function reportOrigin(){\n var el=document.querySelector('[data-report-origin]');\n if(!el || el.getAttribute('data-reported')) return;\n el.setAttribute('data-reported','1');\n var meta=document.querySelector('meta[name=\"csrf-token\"]');\n fetch(el.getAttribute('data-report-origin'),{\n method:'POST',\n credentials:'same-origin',\n headers:{'content-type':'application/json','x-csrf-token':meta ? meta.getAttribute('content') : ''},\n body:JSON.stringify({origin:location.origin})\n }).then(function(r){ if(r.status===201) location.reload(); }).catch(function(){});\n }\n inits.push(reportOrigin);`;\n}\n\n/** Theme menu: apply, persist, and mark the current choice. */\nfunction themePart(): string {\n return `\n function markTheme(){\n var cur=document.documentElement.getAttribute('data-theme')||'system';\n document.querySelectorAll('[data-theme-set]').forEach(function(item){\n item.setAttribute('aria-current', item.getAttribute('data-theme-set')===cur ? 'true' : 'false');\n });\n }\n inits.push(markTheme);\n document.addEventListener('click',function(e){\n var item=e.target instanceof Element ? e.target.closest('[data-theme-set]') : null;\n if(!item) return;\n var next=item.getAttribute('data-theme-set');\n shell.applyTheme(next);\n shell.writeCookie(shell.themeCookie,next);\n markTheme();\n });`;\n}\n\n/** Desktop rail collapse (persisted) and the mobile drawer. */\nfunction sidebarPart(): string {\n return `\n function setDrawer(open){\n var sidebar=document.getElementById('sidebar');\n if(!sidebar) return;\n sidebar.classList.toggle('sidebar--open',open);\n var backdrop=document.querySelector('[data-sidebar-backdrop]');\n if(backdrop){ backdrop.hidden=!open; }\n var btn=document.querySelector('[data-sidebar-toggle]');\n if(btn){ btn.setAttribute('aria-expanded',open?'true':'false'); }\n }\n document.addEventListener('click',function(e){\n var t=e.target instanceof Element ? e.target : null;\n if(!t) return;\n if(t.closest('[data-sidebar-toggle]')){\n var sidebar=document.getElementById('sidebar');\n setDrawer(!(sidebar&&sidebar.classList.contains('sidebar--open')));\n } else if(t.closest('[data-sidebar-backdrop]')){\n setDrawer(false);\n } else if(t.closest('[data-sidebar-collapse]')){\n var next=document.documentElement.getAttribute('data-sidebar')==='rail' ? 'full' : 'rail';\n shell.applySidebar(next);\n shell.writeCookie(shell.sidebarCookie,next);\n }\n });\n document.addEventListener('keydown',function(e){\n var sidebar=document.getElementById('sidebar');\n if(e.key==='Escape'&&sidebar&&sidebar.classList.contains('sidebar--open')){\n setDrawer(false);\n var btn=document.querySelector('[data-sidebar-toggle]');\n if(btn) btn.focus();\n }\n });`;\n}\n\n/** Native <details> dropdowns: close on outside click, Escape, or choosing an item. */\nfunction dropdownPart(): string {\n return `\n document.addEventListener('click',function(e){\n var target=e.target instanceof Element ? e.target : null;\n var owner=target ? target.closest('details[data-dropdown]') : null;\n document.querySelectorAll('details[data-dropdown][open]').forEach(function(d){\n if(d!==owner) d.removeAttribute('open');\n });\n if(owner&&target&&target.closest('[role=\"menuitem\"]')){ owner.removeAttribute('open'); }\n });\n document.addEventListener('keydown',function(e){\n if(e.key!=='Escape') return;\n var open=document.querySelector('details[data-dropdown][open]');\n if(!open) return;\n open.removeAttribute('open');\n var summary=open.querySelector('summary');\n if(summary) summary.focus();\n });`;\n}\n\n/** Build and show one toast element (text only, never HTML). */\nfunction toastRenderPart(): string {\n return `\n var toastIcons={success:'check-circle',danger:'x-circle',warning:'alert-triangle',info:'info'};\n function toastIcon(tone){\n var ns='http://www.w3.org/2000/svg';\n var svg=document.createElementNS(ns,'svg');\n var attrs={viewBox:'0 0 24 24',width:'18',height:'18','aria-hidden':'true',fill:'none',\n stroke:'currentColor','stroke-width':'2','stroke-linecap':'round','stroke-linejoin':'round'};\n Object.keys(attrs).forEach(function(name){ svg.setAttribute(name,attrs[name]); });\n var use=document.createElementNS(ns,'use');\n use.setAttribute('href','${iconSpriteHref}#i-'+toastIcons[tone]);\n svg.appendChild(use);\n return svg;\n }\n function toast(detail){\n var region=document.querySelector('[data-toast-region]');\n if(!region||!detail||!detail.message) return;\n var tone=toastIcons[detail.tone] ? detail.tone : 'info';\n var el=document.createElement('div');\n el.className='toast';\n el.setAttribute('data-tone',tone);\n el.setAttribute('role',tone==='danger'?'alert':'status');\n var text=document.createElement('span');\n text.textContent=String(detail.message);\n el.appendChild(toastIcon(tone));\n el.appendChild(text);\n region.appendChild(el);\n setTimeout(function(){ el.remove(); },tone==='danger'?8000:4000);\n }`;\n}\n\n/**\n * Toasts. Sources: an HX-Trigger(-After-Swap) \"showToast\" event, and a toast\n * queued in sessionStorage by a form with data-toast (review actions reload\n * or redirect, so the message has to survive the navigation).\n */\nfunction toastPart(): string {\n return `${toastRenderPart()}\n var TOAST_KEY='ss_toast';\n function queueToast(detail){ try{ sessionStorage.setItem(TOAST_KEY,JSON.stringify(detail)); }catch(_){} }\n function clearToast(){ try{ sessionStorage.removeItem(TOAST_KEY); }catch(_){} }\n function flushQueued(){\n try{\n var raw=sessionStorage.getItem(TOAST_KEY);\n if(!raw) return;\n sessionStorage.removeItem(TOAST_KEY);\n toast(JSON.parse(raw));\n }catch(_){}\n }\n function flushFlash(){\n var raw=shell.readCookie('storyshelf_flash');\n if(!raw) return;\n document.cookie='storyshelf_flash=; Path=/; Max-Age=0; SameSite=Lax';\n try{ toast(JSON.parse(raw)); }catch(_){}\n }\n function flushToast(){ flushQueued(); flushFlash(); }\n inits.push(flushToast);\n document.addEventListener('htmx:beforeRequest',function(e){\n var form=e.detail && e.detail.elt && e.detail.elt.closest ? e.detail.elt.closest('[data-toast]') : null;\n if(form){ queueToast({message:form.getAttribute('data-toast'),tone:form.getAttribute('data-toast-tone')||'success'}); }\n });\n function failed(){ clearToast(); toast({message:'Something went wrong. Please try again.',tone:'danger'}); }\n document.addEventListener('htmx:responseError',failed);\n document.addEventListener('htmx:sendError',failed);`;\n}\n\n/**\n * Forms and body swaps. HTMX does not swap 4xx responses, but this app answers validation\n * failures with the re-rendered page (400/409/422) so field errors show.\n * \\`data-confirm\\` asks before submitting plain (non-HTMX) forms.\n */\nfunction formPart(): string {\n return `\n document.addEventListener('htmx:beforeSwap',function(e){\n // A body swap only replaces <body>, so classes first used by the new page\n // would be unstyled: adopt the stylesheet the server collected for it.\n var match=/<style id=\"storyshelf-css\">([\\\\s\\\\S]*?)<\\\\/style>/.exec(e.detail.serverResponse||'');\n var sheet=document.getElementById('storyshelf-css');\n if(match&&sheet&&e.detail.target===document.body){ sheet.textContent=match[1]; }\n var status=e.detail.xhr ? e.detail.xhr.status : 0;\n if(status===400||status===409||status===422){\n e.detail.shouldSwap=true;\n e.detail.isError=false;\n clearToast();\n }\n });\n document.addEventListener('submit',function(e){\n var form=e.target instanceof Element ? e.target.closest('form[data-confirm]') : null;\n if(form&&!window.confirm(form.getAttribute('data-confirm')||'Are you sure?')){ e.preventDefault(); }\n });`;\n}\n\n/** Copy buttons: `data-copy` holds the text; confirm with a toast. */\nfunction copyPart(): string {\n return `\n document.addEventListener('click',function(e){\n var btn=e.target instanceof Element ? e.target.closest('[data-copy]') : null;\n if(!btn||!navigator.clipboard) return;\n navigator.clipboard.writeText(btn.getAttribute('data-copy')||'').then(function(){\n toast({message:'Copied to clipboard',tone:'success'});\n },function(){\n toast({message:'Could not copy',tone:'danger'});\n });\n });`;\n}\n\n/** Client-side list filtering for `[data-filter-input]` boxes. */\nfunction filterPart(): string {\n return `\n document.addEventListener('input',function(e){\n var input=e.target instanceof Element ? e.target.closest('[data-filter-input]') : null;\n if(!input) return;\n var scope=input.closest('[data-filter-scope]')||document;\n var query=input.value.trim().toLowerCase();\n var shown=0;\n scope.querySelectorAll('[data-filter-item]').forEach(function(item){\n var match=!query||(item.getAttribute('data-filter-text')||'').toLowerCase().indexOf(query)>-1;\n item.hidden=!match;\n if(match) shown++;\n });\n scope.querySelectorAll('[data-filter-empty]').forEach(function(el){ el.hidden=shown>0; });\n });`;\n}\n\n/**\n * Click-to-load story previews: move the inert frame out of its template (nothing runs before). A\n * frame under a signed URL first asks for a fresh root, so a page left open for hours does not mount\n * an expired token; it keeps the rendered one when that request fails. Afterwards the root is renewed\n * every `data-preview-refresh-seconds`; a failed renewal is retried a few times a minute apart.\n */\nfunction previewPart(): string {\n return `\n document.addEventListener('click',function(e){\n var btn=e.target instanceof Element ? e.target.closest('[data-preview-load]') : null;\n var host=btn ? btn.closest('[data-preview-embed]') : null;\n var tpl=host ? host.querySelector('template[data-preview-template]') : null;\n if(!host||!tpl||host.getAttribute('data-preview-loading')) return;\n host.setAttribute('data-preview-loading','1');\n var idle=host.querySelector('[data-preview-idle]');\n var path=host.getAttribute('data-preview-path');\n var fresh=fetchPreviewBase(host).catch(function(){ return null; });\n fresh.then(function(base){\n var content=document.importNode(tpl.content,true);\n var frame=content.querySelector('iframe');\n if(frame&&base&&path) frame.src=base+path;\n if(idle) idle.hidden=true;\n host.appendChild(content);\n tpl.remove();\n keepPreviewFresh(host);\n });\n });`;\n}\n\n/** Fetch a fresh signed root for a preview host and keep it renewed (see {@link previewPart}). */\nfunction previewRefreshPart(): string {\n return `\n function fetchPreviewBase(host){\n var url=host.getAttribute('data-preview-refresh-url');\n if(!url) return Promise.resolve(null);\n return fetch(url,{headers:{accept:'application/json'}}).then(function(r){\n if(!r.ok) throw new Error('preview-token');\n return r.json();\n }).then(function(body){ return body&&body.base ? body.base : null; });\n }\n function keepPreviewFresh(host){\n var url=host.getAttribute('data-preview-refresh-url');\n var path=host.getAttribute('data-preview-path');\n var seconds=Number(host.getAttribute('data-preview-refresh-seconds'));\n if(!url||!path||!(seconds>0)) return;\n var RETRY_MS=60000, MAX_RETRIES=3;\n function renew(tries){\n var frame=host.querySelector('iframe');\n if(!host.isConnected||!frame) return;\n fetchPreviewBase(host).then(function(base){\n if(base) frame.src=base+path;\n else throw new Error('preview-token');\n }).catch(function(){\n if(tries<MAX_RETRIES) setTimeout(function(){ renew(tries+1); },RETRY_MS);\n });\n }\n var timer=setInterval(function(){\n if(!host.isConnected||!host.querySelector('iframe')){ clearInterval(timer); return; }\n renew(0);\n },seconds*1000);\n }`;\n}\n\n/** Thin top progress bar while an HTMX request (e.g. snapshot navigation) is in flight. */\nfunction loadingPart(): string {\n return `\n var pending=0;\n function setLoading(delta){\n pending=Math.max(0,pending+delta);\n document.documentElement.classList.toggle('ss-loading',pending>0);\n }\n document.addEventListener('htmx:beforeRequest',function(){ setLoading(1); });\n document.addEventListener('htmx:afterRequest',function(){ setLoading(-1); });`;\n}\n\n/** Keep keyboard focus sensible after HTMX swaps. */\nfunction focusPart(): string {\n return `\n document.addEventListener('htmx:afterSwap',function(e){\n var target=e.detail && e.detail.target;\n if(target&&target.querySelector&&target!==document.body){\n var focusable=target.querySelector('[autofocus], input, select, textarea, button');\n if(focusable) try{ focusable.focus(); }catch(_){}\n }\n });`;\n}\n\n/** Page-end script wiring up the shell's interactive behavior (idempotent). */\nexport function clientScript(): string {\n const parts = [\n csrfPart(),\n originPart(),\n themePart(),\n sidebarPart(),\n dropdownPart(),\n toastPart(),\n formPart(),\n copyPart(),\n filterPart(),\n previewPart(),\n previewRefreshPart(),\n loadingPart(),\n focusPart(),\n reviewScript(),\n ];\n return `\n (function(){\n if(window.__ssInit){ window.__ssInit(); return; }\n var shell=window.__storyshelfShell;\n if(!shell) return;\n var inits=[];${parts.join(\"\\n\")}\n window.__ssInit=function(){ inits.forEach(function(fn){ fn(); }); };\n window.__ssInit();\n })();\n `.trim();\n}\n"],"mappings":";;;;;;;AAWA,SAAS,WAAmB;CAC1B,OAAO;;;;;AAKT;;;;;;AAOA,SAAS,aAAqB;CAC5B,OAAO;;;;;;;;;;;;;;AAcT;;AAGA,SAAS,YAAoB;CAC3B,OAAO;;;;;;;;;;;;;;;;AAgBT;;AAGA,SAAS,cAAsB;CAC7B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCT;;AAGA,SAAS,eAAuB;CAC9B,OAAO;;;;;;;;;;;;;;;;;AAiBT;;AAGA,SAAS,kBAA0B;CACjC,OAAO;;;;;;;;;iCASwB,eAAe;;;;;;;;;;;;;;;;;;;AAmBhD;;;;;;AAOA,SAAS,YAAoB;CAC3B,OAAO,GAAG,gBAAgB,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2B9B;;;;;;AAOA,SAAS,WAAmB;CAC1B,OAAO;;;;;;;;;;;;;;;;;;AAkBT;;AAGA,SAAS,WAAmB;CAC1B,OAAO;;;;;;;;;;AAUT;;AAGA,SAAS,aAAqB;CAC5B,OAAO;;;;;;;;;;;;;;AAcT;;;;;;;AAQA,SAAS,cAAsB;CAC7B,OAAO;;;;;;;;;;;;;;;;;;;;AAoBT;;AAGA,SAAS,qBAA6B;CACpC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BT;;AAGA,SAAS,cAAsB;CAC7B,OAAO;;;;;;;;AAQT;;AAGA,SAAS,YAAoB;CAC3B,OAAO;;;;;;;;AAQT;;AAGA,SAAgB,eAAuB;CAiBrC,OAAO;;;;;mBAKU;EApBf,SAAS;EACT,WAAW;EACX,UAAU;EACV,YAAY;EACZ,aAAa;EACb,UAAU;EACV,SAAS;EACT,SAAS;EACT,WAAW;EACX,YAAY;EACZ,mBAAmB;EACnB,YAAY;EACZ,UAAU;EACV,aAAa;CAOM,CAAC,CAAC,KAAK,IAAI,EAAE;;;;IAIhC,KAAK;AACT"}
@@ -2,8 +2,17 @@ import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
4
  //#region src/ui/shell/nav.ts
5
- /** Entries shown for the current project. */
6
- function projectNavItems(urls, slug) {
5
+ /**
6
+ * Entries shown for the current project. Entries for `testing` routes (Jobs) drop out on a
7
+ * hosting-only instance, where those routes answer 404.
8
+ */
9
+ function projectNavItems(urls, slug, testing = true) {
10
+ const jobs = {
11
+ key: "jobs",
12
+ label: "Jobs",
13
+ href: urls.jobs(slug),
14
+ icon: "activity"
15
+ };
7
16
  return [
8
17
  {
9
18
  key: "library",
@@ -17,12 +26,7 @@ function projectNavItems(urls, slug) {
17
26
  href: urls.buildsList(slug),
18
27
  icon: "layers"
19
28
  },
20
- {
21
- key: "jobs",
22
- label: "Jobs",
23
- href: urls.jobs(slug),
24
- icon: "activity"
25
- },
29
+ ...testing ? [jobs] : [],
26
30
  {
27
31
  key: "labels",
28
32
  label: "Labels",
@@ -1 +1 @@
1
- {"version":3,"file":"nav.mjs","names":[],"sources":["../../../src/ui/shell/nav.ts"],"sourcesContent":["import type { IconName } from \"../icons/paths.ts\";\n\n/** Which nav entry is active and which project (if any) the page belongs to. */\nexport interface NavConfig {\n active?: string;\n projectSlug?: string;\n projectName?: string;\n /** Levels below the section, ending with the current page (e.g. a build, then \"Review\"). */\n trail?: NavCrumb[];\n}\n\n/** One breadcrumb level; the last trail item is the current page (it needs no `href`). */\nexport interface NavCrumb {\n label: string;\n href?: string;\n}\n\n/** One sidebar entry. */\nexport interface NavItem {\n key: string;\n label: string;\n href: string;\n icon: IconName;\n /** Opens in a new tab (raw API output). */\n external?: boolean;\n}\n\n/** Subset of the URL builder the nav needs (keeps this module pure). */\nexport interface NavUrls {\n projects(): string;\n library(slug: string): string;\n buildsList(slug: string): string;\n jobs(slug: string): string;\n labels(slug: string): string;\n settings(slug: string): string;\n}\n\n/** Entries shown for the current project. */\nexport function projectNavItems(urls: NavUrls, slug: string): NavItem[] {\n return [\n { key: \"library\", label: \"Library\", href: urls.library(slug), icon: \"image\" },\n { key: \"builds\", label: \"Builds\", href: urls.buildsList(slug), icon: \"layers\" },\n { key: \"jobs\", label: \"Jobs\", href: urls.jobs(slug), icon: \"activity\" },\n { key: \"labels\", label: \"Labels\", href: urls.labels(slug), icon: \"tag\" },\n { key: \"settings\", label: \"Settings\", href: urls.settings(slug), icon: \"settings\" },\n ];\n}\n\n/** Developer entries (API reference). */\nexport function developerNavItems(): NavItem[] {\n return [\n { key: \"api-docs\", label: \"API docs\", href: \"/api/v1/docs\", icon: \"book\" },\n {\n key: \"openapi\",\n label: \"OpenAPI spec\",\n href: \"/api/v1/openapi.json\",\n icon: \"code\",\n external: true,\n },\n ];\n}\n\n/** Administration entries (admins only). */\nexport function adminNavItems(): NavItem[] {\n return [{ key: \"admin\", label: \"System\", href: \"/admin\", icon: \"server\" }];\n}\n\n/**\n * Breadcrumbs after the project picker: the *ancestors* of the current page.\n * The page itself is named by its heading, so it is never repeated here —\n * the section plus `nav.trail` describe where the page is, and the last item\n * (the current page) is dropped. Every returned crumb links somewhere, and\n * the last one is the page's parent.\n */\nexport function breadcrumbs(nav: NavConfig | undefined, urls: NavUrls): NavCrumb[] {\n const section = sectionCrumb(nav, urls);\n const path = section ? [section, ...(nav?.trail ?? [])] : [...(nav?.trail ?? [])];\n return path.slice(0, -1).filter((crumb) => crumb.href !== undefined);\n}\n\nfunction sectionCrumb(nav: NavConfig | undefined, urls: NavUrls): NavCrumb | null {\n const slug = nav?.projectSlug;\n const table: Record<string, NavCrumb | undefined> = {\n library: slug ? { label: \"Library\", href: urls.library(slug) } : undefined,\n builds: slug ? { label: \"Builds\", href: urls.buildsList(slug) } : undefined,\n jobs: slug ? { label: \"Jobs\", href: urls.jobs(slug) } : undefined,\n labels: slug ? { label: \"Labels\", href: urls.labels(slug) } : undefined,\n settings: slug ? { label: \"Settings\", href: urls.settings(slug) } : undefined,\n admin: { label: \"System\", href: \"/admin\" },\n profile: { label: \"Profile\", href: \"/profile\" },\n };\n return table[nav?.active ?? \"\"] ?? null;\n}\n"],"mappings":";;;;;AAsCA,SAAgB,gBAAgB,MAAe,MAAyB;CACtE,OAAO;EACL;GAAE,KAAK;GAAW,OAAO;GAAW,MAAM,KAAK,QAAQ,IAAI;GAAG,MAAM;EAAQ;EAC5E;GAAE,KAAK;GAAU,OAAO;GAAU,MAAM,KAAK,WAAW,IAAI;GAAG,MAAM;EAAS;EAC9E;GAAE,KAAK;GAAQ,OAAO;GAAQ,MAAM,KAAK,KAAK,IAAI;GAAG,MAAM;EAAW;EACtE;GAAE,KAAK;GAAU,OAAO;GAAU,MAAM,KAAK,OAAO,IAAI;GAAG,MAAM;EAAM;EACvE;GAAE,KAAK;GAAY,OAAO;GAAY,MAAM,KAAK,SAAS,IAAI;GAAG,MAAM;EAAW;CACpF;AACF;;AAGA,SAAgB,oBAA+B;CAC7C,OAAO,CACL;EAAE,KAAK;EAAY,OAAO;EAAY,MAAM;EAAgB,MAAM;CAAO,GACzE;EACE,KAAK;EACL,OAAO;EACP,MAAM;EACN,MAAM;EACN,UAAU;CACZ,CACF;AACF;;AAGA,SAAgB,gBAA2B;CACzC,OAAO,CAAC;EAAE,KAAK;EAAS,OAAO;EAAU,MAAM;EAAU,MAAM;CAAS,CAAC;AAC3E;;;;;;;;AASA,SAAgB,YAAY,KAA4B,MAA2B;CACjF,MAAM,UAAU,aAAa,KAAK,IAAI;CAEtC,QADa,UAAU,CAAC,SAAS,GAAI,KAAK,SAAS,CAAC,CAAE,IAAI,CAAC,GAAI,KAAK,SAAS,CAAC,CAAE,EAAA,CACpE,MAAM,GAAG,EAAE,CAAC,CAAC,QAAQ,UAAU,MAAM,SAAS,KAAA,CAAS;AACrE;AAEA,SAAS,aAAa,KAA4B,MAAgC;CAChF,MAAM,OAAO,KAAK;CAUlB,OAAO;EARL,SAAS,OAAO;GAAE,OAAO;GAAW,MAAM,KAAK,QAAQ,IAAI;EAAE,IAAI,KAAA;EACjE,QAAQ,OAAO;GAAE,OAAO;GAAU,MAAM,KAAK,WAAW,IAAI;EAAE,IAAI,KAAA;EAClE,MAAM,OAAO;GAAE,OAAO;GAAQ,MAAM,KAAK,KAAK,IAAI;EAAE,IAAI,KAAA;EACxD,QAAQ,OAAO;GAAE,OAAO;GAAU,MAAM,KAAK,OAAO,IAAI;EAAE,IAAI,KAAA;EAC9D,UAAU,OAAO;GAAE,OAAO;GAAY,MAAM,KAAK,SAAS,IAAI;EAAE,IAAI,KAAA;EACpE,OAAO;GAAE,OAAO;GAAU,MAAM;EAAS;EACzC,SAAS;GAAE,OAAO;GAAW,MAAM;EAAW;CAErC,EAAE,KAAK,UAAU,OAAO;AACrC"}
1
+ {"version":3,"file":"nav.mjs","names":[],"sources":["../../../src/ui/shell/nav.ts"],"sourcesContent":["import type { IconName } from \"../icons/paths.ts\";\n\n/** Which nav entry is active and which project (if any) the page belongs to. */\nexport interface NavConfig {\n active?: string;\n projectSlug?: string;\n projectName?: string;\n /** Levels below the section, ending with the current page (e.g. a build, then \"Review\"). */\n trail?: NavCrumb[];\n}\n\n/** One breadcrumb level; the last trail item is the current page (it needs no `href`). */\nexport interface NavCrumb {\n label: string;\n href?: string;\n}\n\n/** One sidebar entry. */\nexport interface NavItem {\n key: string;\n label: string;\n href: string;\n icon: IconName;\n /** Opens in a new tab (raw API output). */\n external?: boolean;\n}\n\n/** Subset of the URL builder the nav needs (keeps this module pure). */\nexport interface NavUrls {\n projects(): string;\n library(slug: string): string;\n buildsList(slug: string): string;\n jobs(slug: string): string;\n labels(slug: string): string;\n settings(slug: string): string;\n}\n\n/**\n * Entries shown for the current project. Entries for `testing` routes (Jobs) drop out on a\n * hosting-only instance, where those routes answer 404.\n */\nexport function projectNavItems(urls: NavUrls, slug: string, testing = true): NavItem[] {\n const jobs: NavItem = { key: \"jobs\", label: \"Jobs\", href: urls.jobs(slug), icon: \"activity\" };\n return [\n { key: \"library\", label: \"Library\", href: urls.library(slug), icon: \"image\" },\n { key: \"builds\", label: \"Builds\", href: urls.buildsList(slug), icon: \"layers\" },\n ...(testing ? [jobs] : []),\n { key: \"labels\", label: \"Labels\", href: urls.labels(slug), icon: \"tag\" },\n { key: \"settings\", label: \"Settings\", href: urls.settings(slug), icon: \"settings\" },\n ];\n}\n\n/** Developer entries (API reference). */\nexport function developerNavItems(): NavItem[] {\n return [\n { key: \"api-docs\", label: \"API docs\", href: \"/api/v1/docs\", icon: \"book\" },\n {\n key: \"openapi\",\n label: \"OpenAPI spec\",\n href: \"/api/v1/openapi.json\",\n icon: \"code\",\n external: true,\n },\n ];\n}\n\n/** Administration entries (admins only). */\nexport function adminNavItems(): NavItem[] {\n return [{ key: \"admin\", label: \"System\", href: \"/admin\", icon: \"server\" }];\n}\n\n/**\n * Breadcrumbs after the project picker: the *ancestors* of the current page.\n * The page itself is named by its heading, so it is never repeated here —\n * the section plus `nav.trail` describe where the page is, and the last item\n * (the current page) is dropped. Every returned crumb links somewhere, and\n * the last one is the page's parent.\n */\nexport function breadcrumbs(nav: NavConfig | undefined, urls: NavUrls): NavCrumb[] {\n const section = sectionCrumb(nav, urls);\n const path = section ? [section, ...(nav?.trail ?? [])] : [...(nav?.trail ?? [])];\n return path.slice(0, -1).filter((crumb) => crumb.href !== undefined);\n}\n\nfunction sectionCrumb(nav: NavConfig | undefined, urls: NavUrls): NavCrumb | null {\n const slug = nav?.projectSlug;\n const table: Record<string, NavCrumb | undefined> = {\n library: slug ? { label: \"Library\", href: urls.library(slug) } : undefined,\n builds: slug ? { label: \"Builds\", href: urls.buildsList(slug) } : undefined,\n jobs: slug ? { label: \"Jobs\", href: urls.jobs(slug) } : undefined,\n labels: slug ? { label: \"Labels\", href: urls.labels(slug) } : undefined,\n settings: slug ? { label: \"Settings\", href: urls.settings(slug) } : undefined,\n admin: { label: \"System\", href: \"/admin\" },\n profile: { label: \"Profile\", href: \"/profile\" },\n };\n return table[nav?.active ?? \"\"] ?? null;\n}\n"],"mappings":";;;;;;;;AAyCA,SAAgB,gBAAgB,MAAe,MAAc,UAAU,MAAiB;CACtF,MAAM,OAAgB;EAAE,KAAK;EAAQ,OAAO;EAAQ,MAAM,KAAK,KAAK,IAAI;EAAG,MAAM;CAAW;CAC5F,OAAO;EACL;GAAE,KAAK;GAAW,OAAO;GAAW,MAAM,KAAK,QAAQ,IAAI;GAAG,MAAM;EAAQ;EAC5E;GAAE,KAAK;GAAU,OAAO;GAAU,MAAM,KAAK,WAAW,IAAI;GAAG,MAAM;EAAS;EAC9E,GAAI,UAAU,CAAC,IAAI,IAAI,CAAC;EACxB;GAAE,KAAK;GAAU,OAAO;GAAU,MAAM,KAAK,OAAO,IAAI;GAAG,MAAM;EAAM;EACvE;GAAE,KAAK;GAAY,OAAO;GAAY,MAAM,KAAK,SAAS,IAAI;GAAG,MAAM;EAAW;CACpF;AACF;;AAGA,SAAgB,oBAA+B;CAC7C,OAAO,CACL;EAAE,KAAK;EAAY,OAAO;EAAY,MAAM;EAAgB,MAAM;CAAO,GACzE;EACE,KAAK;EACL,OAAO;EACP,MAAM;EACN,MAAM;EACN,UAAU;CACZ,CACF;AACF;;AAGA,SAAgB,gBAA2B;CACzC,OAAO,CAAC;EAAE,KAAK;EAAS,OAAO;EAAU,MAAM;EAAU,MAAM;CAAS,CAAC;AAC3E;;;;;;;;AASA,SAAgB,YAAY,KAA4B,MAA2B;CACjF,MAAM,UAAU,aAAa,KAAK,IAAI;CAEtC,QADa,UAAU,CAAC,SAAS,GAAI,KAAK,SAAS,CAAC,CAAE,IAAI,CAAC,GAAI,KAAK,SAAS,CAAC,CAAE,EAAA,CACpE,MAAM,GAAG,EAAE,CAAC,CAAC,QAAQ,UAAU,MAAM,SAAS,KAAA,CAAS;AACrE;AAEA,SAAS,aAAa,KAA4B,MAAgC;CAChF,MAAM,OAAO,KAAK;CAUlB,OAAO;EARL,SAAS,OAAO;GAAE,OAAO;GAAW,MAAM,KAAK,QAAQ,IAAI;EAAE,IAAI,KAAA;EACjE,QAAQ,OAAO;GAAE,OAAO;GAAU,MAAM,KAAK,WAAW,IAAI;EAAE,IAAI,KAAA;EAClE,MAAM,OAAO;GAAE,OAAO;GAAQ,MAAM,KAAK,KAAK,IAAI;EAAE,IAAI,KAAA;EACxD,QAAQ,OAAO;GAAE,OAAO;GAAU,MAAM,KAAK,OAAO,IAAI;EAAE,IAAI,KAAA;EAC9D,UAAU,OAAO;GAAE,OAAO;GAAY,MAAM,KAAK,SAAS,IAAI;EAAE,IAAI,KAAA;EACpE,OAAO;GAAE,OAAO;GAAU,MAAM;EAAS;EACzC,SAAS;GAAE,OAAO;GAAW,MAAM;EAAW;CAErC,EAAE,KAAK,UAAU,OAAO;AACrC"}
@@ -166,7 +166,15 @@ function keyboardShortcuts() {
166
166
  if(el&&!el.disabled){ el.click(); return true; }
167
167
  return false;
168
168
  }
169
+ function stepBrowser(dir){
170
+ var tabs=[].slice.call(document.querySelectorAll('[data-browser-tabs] a'));
171
+ var at=tabs.findIndex(function(a){ return a.getAttribute('aria-current')==='page'; });
172
+ var target=at===-1?null:tabs[at+dir];
173
+ if(target){ target.click(); return true; }
174
+ return false;
175
+ }
169
176
  function handleKey(key,stage){
177
+ if(key==='['||key===']') return stepBrowser(key===']'?1:-1);
170
178
  if(key==='j'||key==='ArrowRight') return press1('[data-snap-next]');
171
179
  if(key==='k'||key==='ArrowLeft') return press1('[data-snap-prev]');
172
180
  if(key==='a'||key==='A') return press1('[data-approve]');
@@ -1 +1 @@
1
- {"version":3,"file":"review-script.mjs","names":[],"sources":["../../../src/ui/shell/review-script.ts"],"sourcesContent":["/**\n * Review workspace behavior: comparison modes, zoom, synchronized scrolling,\n * list filters, keyboard review, and the shortcuts dialog. Runs inside the\n * shell's once-only closure (`inits`, `shell` are in scope).\n */\nexport function reviewScript(): string {\n return [stateHelpers(), stageSetup(), wipeDrag(), clickControls(), keyboardShortcuts()].join(\n \"\\n\",\n );\n}\n\n/** Mode/zoom state on the stage root, remembered across snapshots. */\nfunction stateHelpers(): string {\n return `\n var MODES=['split','swipe','onion','diff','flip'];\n var ZOOMS=['fit','100','200','400'];\n function remember(key,value){ try{ localStorage.setItem(key,value); }catch(_){} }\n function recall(key){ try{ return localStorage.getItem(key); }catch(_){ return null; } }\n function press(group,value){\n if(!group) return;\n group.querySelectorAll('[data-view-value]').forEach(function(b){\n b.setAttribute('aria-pressed', b.getAttribute('data-view-value')===value ? 'true' : 'false');\n });\n }\n function setMode(stage,mode,persist){\n stage.setAttribute('data-view',mode);\n press(stage.querySelector('[data-compare-mode]'),mode);\n if(persist) remember('ss_compare_mode',mode);\n }\n function setZoom(stage,zoom,persist){\n stage.setAttribute('data-zoom',zoom);\n press(stage.querySelector('[data-compare-zoom]'),zoom);\n if(persist) remember('ss_compare_zoom',zoom);\n }\n function flip(stage){\n stage.setAttribute('data-flip', stage.getAttribute('data-flip')==='baseline' ? 'current' : 'baseline');\n }\n function nextZoom(stage){\n var cur=stage.getAttribute('data-zoom')||'fit';\n setZoom(stage,ZOOMS[(ZOOMS.indexOf(cur)+1)%ZOOMS.length],true);\n }\n function stepZoom(stage,delta){\n var cur=ZOOMS.indexOf(stage.getAttribute('data-zoom')||'fit');\n var next=Math.max(0,Math.min(ZOOMS.length-1,cur+delta));\n setZoom(stage,ZOOMS[next],true);\n }`;\n}\n\n/** Per-render wiring of a freshly swapped-in comparison stage. */\nfunction stageSetup(): string {\n return `\n function syncScroll(stage){\n var panes=Array.from(stage.querySelectorAll('.stage__pane'));\n var lock=false;\n panes.forEach(function(pane){\n pane.addEventListener('scroll',function(){\n if(lock) return;\n lock=true;\n panes.forEach(function(other){\n if(other!==pane){ other.scrollTop=pane.scrollTop; other.scrollLeft=pane.scrollLeft; }\n });\n requestAnimationFrame(function(){ lock=false; });\n });\n });\n }\n function bindRange(stage,selector,prop,scale){\n var input=stage.querySelector(selector);\n if(!input) return;\n input.addEventListener('input',function(){ stage.style.setProperty(prop,String(input.value*scale)+(scale===1?'%':'')); });\n }\n function initStage(stage){\n if(stage.getAttribute('data-ready')) return;\n stage.setAttribute('data-ready','1');\n var mode=recall('ss_compare_mode');\n if(MODES.indexOf(mode)>-1) setMode(stage,mode,false);\n var zoom=recall('ss_compare_zoom');\n if(ZOOMS.indexOf(zoom)>-1) setZoom(stage,zoom,false);\n bindRange(stage,'[data-compare-swipe]','--swipe',1);\n bindRange(stage,'[data-compare-onion]','--onion',0.01);\n var grid=stage.querySelector('.stage__grid');\n if(grid){ grid.addEventListener('click',function(){ if(stage.getAttribute('data-view')==='flip'){ flip(stage); } }); }\n syncScroll(stage);\n }\n inits.push(function(){ document.querySelectorAll('[data-compare]').forEach(initStage); });`;\n}\n\n/**\n * Drag the wipe handle to move the swipe divider. Uses pointer capture so the\n * drag keeps working when the pointer leaves the handle, and mirrors the value\n * into the (accessible) range slider.\n */\nfunction wipeDrag(): string {\n return `\n function wipePercent(stage,clientX){\n var pane=stage.querySelector('.stage__pane[data-pane=\"current\"]');\n if(!pane) return 50;\n var rect=pane.getBoundingClientRect();\n return Math.max(0,Math.min(100,(clientX-rect.left)/rect.width*100));\n }\n function setWipe(stage,percent){\n stage.style.setProperty('--swipe',percent+'%');\n var range=stage.querySelector('[data-compare-swipe]');\n if(range){ range.value=String(Math.round(percent)); }\n }\n function bindKnob(stage){\n var knob=stage.querySelector('[data-compare-knob]');\n if(!knob||knob.getAttribute('data-bound')) return;\n knob.setAttribute('data-bound','1');\n knob.addEventListener('pointerdown',function(e){\n e.preventDefault();\n knob.setPointerCapture(e.pointerId);\n setWipe(stage,wipePercent(stage,e.clientX));\n function move(ev){ setWipe(stage,wipePercent(stage,ev.clientX)); }\n function stop(){\n knob.removeEventListener('pointermove',move);\n knob.removeEventListener('pointerup',stop);\n knob.removeEventListener('pointercancel',stop);\n }\n knob.addEventListener('pointermove',move);\n knob.addEventListener('pointerup',stop);\n knob.addEventListener('pointercancel',stop);\n });\n }\n inits.push(function(){ document.querySelectorAll('[data-compare]').forEach(bindKnob); });`;\n}\n\n/** Delegated clicks: mode/zoom/filter segments and the shortcuts button. */\nfunction clickControls(): string {\n return `\n function openShortcuts(){\n var dialog=document.querySelector('dialog[data-shortcuts]');\n if(dialog&&typeof dialog.showModal==='function'&&!dialog.open){ dialog.showModal(); }\n }\n function chooseSegment(btn){\n var value=btn.getAttribute('data-view-value');\n var stage=btn.closest('[data-compare]');\n if(stage&&btn.closest('[data-compare-mode]')){ setMode(stage,value,true); }\n else if(stage&&btn.closest('[data-compare-zoom]')){ setZoom(stage,value,true); }\n else if(btn.closest('[data-filter-switch]')){\n var list=document.querySelector('[data-diff-nav]');\n if(list){ list.setAttribute('data-filter',value); }\n press(btn.closest('[data-filter-switch]'),value);\n }\n }\n document.addEventListener('click',function(e){\n var t=e.target instanceof Element ? e.target : null;\n if(!t) return;\n var btn=t.closest('[data-view-value]');\n if(btn){ chooseSegment(btn); }\n if(t.closest('[data-shortcuts-open]')){ openShortcuts(); }\n });`;\n}\n\n/** Global review shortcuts; ignored while typing or with modifier keys held. */\nfunction keyboardShortcuts(): string {\n return `\n function typing(target){\n return target instanceof HTMLElement &&\n (target.tagName==='INPUT'||target.tagName==='TEXTAREA'||target.tagName==='SELECT'||target.isContentEditable);\n }\n function press1(selector){\n var el=document.querySelector(selector);\n if(el&&!el.disabled){ el.click(); return true; }\n return false;\n }\n function handleKey(key,stage){\n if(key==='j'||key==='ArrowRight') return press1('[data-snap-next]');\n if(key==='k'||key==='ArrowLeft') return press1('[data-snap-prev]');\n if(key==='a'||key==='A') return press1('[data-approve]');\n if(key==='r'||key==='R') return press1('[data-reject]');\n if(key>='1'&&key<='5'){ setMode(stage,MODES[Number(key)-1],true); return true; }\n if(key==='f'){ nextZoom(stage); return true; }\n if(key==='+'||key==='='){ stepZoom(stage,1); return true; }\n if(key==='-'||key==='_'){ stepZoom(stage,-1); return true; }\n if(key==='t'){\n if(stage.getAttribute('data-view')==='flip'){ flip(stage); } else { setMode(stage,'flip',true); }\n return true;\n }\n if(key==='?'){ openShortcuts(); return true; }\n return false;\n }\n document.addEventListener('keydown',function(e){\n if(e.metaKey||e.ctrlKey||e.altKey||typing(e.target)) return;\n var stage=document.querySelector('[data-compare]');\n if(!stage||document.querySelector('dialog[open]')) return;\n if(handleKey(e.key,stage)){ e.preventDefault(); }\n });`;\n}\n"],"mappings":";;;;;;;;;AAKA,SAAgB,eAAuB;CACrC,OAAO;EAAC,aAAa;EAAG,WAAW;EAAG,SAAS;EAAG,cAAc;EAAG,kBAAkB;CAAC,CAAC,CAAC,KACtF,IACF;AACF;;AAGA,SAAS,eAAuB;CAC9B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCT;;AAGA,SAAS,aAAqB;CAC5B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCT;;;;;;AAOA,SAAS,WAAmB;CAC1B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCT;;AAGA,SAAS,gBAAwB;CAC/B,OAAO;;;;;;;;;;;;;;;;;;;;;;;AAuBT;;AAGA,SAAS,oBAA4B;CACnC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCT"}
1
+ {"version":3,"file":"review-script.mjs","names":[],"sources":["../../../src/ui/shell/review-script.ts"],"sourcesContent":["/**\n * Review workspace behavior: comparison modes, zoom, synchronized scrolling,\n * list filters, keyboard review, and the shortcuts dialog. Runs inside the\n * shell's once-only closure (`inits`, `shell` are in scope).\n */\nexport function reviewScript(): string {\n return [stateHelpers(), stageSetup(), wipeDrag(), clickControls(), keyboardShortcuts()].join(\n \"\\n\",\n );\n}\n\n/** Mode/zoom state on the stage root, remembered across snapshots. */\nfunction stateHelpers(): string {\n return `\n var MODES=['split','swipe','onion','diff','flip'];\n var ZOOMS=['fit','100','200','400'];\n function remember(key,value){ try{ localStorage.setItem(key,value); }catch(_){} }\n function recall(key){ try{ return localStorage.getItem(key); }catch(_){ return null; } }\n function press(group,value){\n if(!group) return;\n group.querySelectorAll('[data-view-value]').forEach(function(b){\n b.setAttribute('aria-pressed', b.getAttribute('data-view-value')===value ? 'true' : 'false');\n });\n }\n function setMode(stage,mode,persist){\n stage.setAttribute('data-view',mode);\n press(stage.querySelector('[data-compare-mode]'),mode);\n if(persist) remember('ss_compare_mode',mode);\n }\n function setZoom(stage,zoom,persist){\n stage.setAttribute('data-zoom',zoom);\n press(stage.querySelector('[data-compare-zoom]'),zoom);\n if(persist) remember('ss_compare_zoom',zoom);\n }\n function flip(stage){\n stage.setAttribute('data-flip', stage.getAttribute('data-flip')==='baseline' ? 'current' : 'baseline');\n }\n function nextZoom(stage){\n var cur=stage.getAttribute('data-zoom')||'fit';\n setZoom(stage,ZOOMS[(ZOOMS.indexOf(cur)+1)%ZOOMS.length],true);\n }\n function stepZoom(stage,delta){\n var cur=ZOOMS.indexOf(stage.getAttribute('data-zoom')||'fit');\n var next=Math.max(0,Math.min(ZOOMS.length-1,cur+delta));\n setZoom(stage,ZOOMS[next],true);\n }`;\n}\n\n/** Per-render wiring of a freshly swapped-in comparison stage. */\nfunction stageSetup(): string {\n return `\n function syncScroll(stage){\n var panes=Array.from(stage.querySelectorAll('.stage__pane'));\n var lock=false;\n panes.forEach(function(pane){\n pane.addEventListener('scroll',function(){\n if(lock) return;\n lock=true;\n panes.forEach(function(other){\n if(other!==pane){ other.scrollTop=pane.scrollTop; other.scrollLeft=pane.scrollLeft; }\n });\n requestAnimationFrame(function(){ lock=false; });\n });\n });\n }\n function bindRange(stage,selector,prop,scale){\n var input=stage.querySelector(selector);\n if(!input) return;\n input.addEventListener('input',function(){ stage.style.setProperty(prop,String(input.value*scale)+(scale===1?'%':'')); });\n }\n function initStage(stage){\n if(stage.getAttribute('data-ready')) return;\n stage.setAttribute('data-ready','1');\n var mode=recall('ss_compare_mode');\n if(MODES.indexOf(mode)>-1) setMode(stage,mode,false);\n var zoom=recall('ss_compare_zoom');\n if(ZOOMS.indexOf(zoom)>-1) setZoom(stage,zoom,false);\n bindRange(stage,'[data-compare-swipe]','--swipe',1);\n bindRange(stage,'[data-compare-onion]','--onion',0.01);\n var grid=stage.querySelector('.stage__grid');\n if(grid){ grid.addEventListener('click',function(){ if(stage.getAttribute('data-view')==='flip'){ flip(stage); } }); }\n syncScroll(stage);\n }\n inits.push(function(){ document.querySelectorAll('[data-compare]').forEach(initStage); });`;\n}\n\n/**\n * Drag the wipe handle to move the swipe divider. Uses pointer capture so the\n * drag keeps working when the pointer leaves the handle, and mirrors the value\n * into the (accessible) range slider.\n */\nfunction wipeDrag(): string {\n return `\n function wipePercent(stage,clientX){\n var pane=stage.querySelector('.stage__pane[data-pane=\"current\"]');\n if(!pane) return 50;\n var rect=pane.getBoundingClientRect();\n return Math.max(0,Math.min(100,(clientX-rect.left)/rect.width*100));\n }\n function setWipe(stage,percent){\n stage.style.setProperty('--swipe',percent+'%');\n var range=stage.querySelector('[data-compare-swipe]');\n if(range){ range.value=String(Math.round(percent)); }\n }\n function bindKnob(stage){\n var knob=stage.querySelector('[data-compare-knob]');\n if(!knob||knob.getAttribute('data-bound')) return;\n knob.setAttribute('data-bound','1');\n knob.addEventListener('pointerdown',function(e){\n e.preventDefault();\n knob.setPointerCapture(e.pointerId);\n setWipe(stage,wipePercent(stage,e.clientX));\n function move(ev){ setWipe(stage,wipePercent(stage,ev.clientX)); }\n function stop(){\n knob.removeEventListener('pointermove',move);\n knob.removeEventListener('pointerup',stop);\n knob.removeEventListener('pointercancel',stop);\n }\n knob.addEventListener('pointermove',move);\n knob.addEventListener('pointerup',stop);\n knob.addEventListener('pointercancel',stop);\n });\n }\n inits.push(function(){ document.querySelectorAll('[data-compare]').forEach(bindKnob); });`;\n}\n\n/** Delegated clicks: mode/zoom/filter segments and the shortcuts button. */\nfunction clickControls(): string {\n return `\n function openShortcuts(){\n var dialog=document.querySelector('dialog[data-shortcuts]');\n if(dialog&&typeof dialog.showModal==='function'&&!dialog.open){ dialog.showModal(); }\n }\n function chooseSegment(btn){\n var value=btn.getAttribute('data-view-value');\n var stage=btn.closest('[data-compare]');\n if(stage&&btn.closest('[data-compare-mode]')){ setMode(stage,value,true); }\n else if(stage&&btn.closest('[data-compare-zoom]')){ setZoom(stage,value,true); }\n else if(btn.closest('[data-filter-switch]')){\n var list=document.querySelector('[data-diff-nav]');\n if(list){ list.setAttribute('data-filter',value); }\n press(btn.closest('[data-filter-switch]'),value);\n }\n }\n document.addEventListener('click',function(e){\n var t=e.target instanceof Element ? e.target : null;\n if(!t) return;\n var btn=t.closest('[data-view-value]');\n if(btn){ chooseSegment(btn); }\n if(t.closest('[data-shortcuts-open]')){ openShortcuts(); }\n });`;\n}\n\n/** Global review shortcuts; ignored while typing or with modifier keys held. */\nfunction keyboardShortcuts(): string {\n return `\n function typing(target){\n return target instanceof HTMLElement &&\n (target.tagName==='INPUT'||target.tagName==='TEXTAREA'||target.tagName==='SELECT'||target.isContentEditable);\n }\n function press1(selector){\n var el=document.querySelector(selector);\n if(el&&!el.disabled){ el.click(); return true; }\n return false;\n }\n function stepBrowser(dir){\n var tabs=[].slice.call(document.querySelectorAll('[data-browser-tabs] a'));\n var at=tabs.findIndex(function(a){ return a.getAttribute('aria-current')==='page'; });\n var target=at===-1?null:tabs[at+dir];\n if(target){ target.click(); return true; }\n return false;\n }\n function handleKey(key,stage){\n if(key==='['||key===']') return stepBrowser(key===']'?1:-1);\n if(key==='j'||key==='ArrowRight') return press1('[data-snap-next]');\n if(key==='k'||key==='ArrowLeft') return press1('[data-snap-prev]');\n if(key==='a'||key==='A') return press1('[data-approve]');\n if(key==='r'||key==='R') return press1('[data-reject]');\n if(key>='1'&&key<='5'){ setMode(stage,MODES[Number(key)-1],true); return true; }\n if(key==='f'){ nextZoom(stage); return true; }\n if(key==='+'||key==='='){ stepZoom(stage,1); return true; }\n if(key==='-'||key==='_'){ stepZoom(stage,-1); return true; }\n if(key==='t'){\n if(stage.getAttribute('data-view')==='flip'){ flip(stage); } else { setMode(stage,'flip',true); }\n return true;\n }\n if(key==='?'){ openShortcuts(); return true; }\n return false;\n }\n document.addEventListener('keydown',function(e){\n if(e.metaKey||e.ctrlKey||e.altKey||typing(e.target)) return;\n var stage=document.querySelector('[data-compare]');\n if(!stage||document.querySelector('dialog[open]')) return;\n if(handleKey(e.key,stage)){ e.preventDefault(); }\n });`;\n}\n"],"mappings":";;;;;;;;;AAKA,SAAgB,eAAuB;CACrC,OAAO;EAAC,aAAa;EAAG,WAAW;EAAG,SAAS;EAAG,cAAc;EAAG,kBAAkB;CAAC,CAAC,CAAC,KACtF,IACF;AACF;;AAGA,SAAS,eAAuB;CAC9B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCT;;AAGA,SAAS,aAAqB;CAC5B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCT;;;;;;AAOA,SAAS,WAAmB;CAC1B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCT;;AAGA,SAAS,gBAAwB;CAC/B,OAAO;;;;;;;;;;;;;;;;;;;;;;;AAuBT;;AAGA,SAAS,oBAA4B;CACnC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCT"}
@@ -253,7 +253,7 @@ const Brand = ({ name, logo, href }) => {
253
253
  };
254
254
  /** Primary navigation: brand, workspace, project, developer, and admin entries. */
255
255
  const Sidebar = ({ nav, name, logo }) => {
256
- const { user, authEnabled, config } = getStore();
256
+ const { user, authEnabled, config, testing } = getStore();
257
257
  const urls = createUrlBuilder("/", config.publishedBaseDomain);
258
258
  const showAdmin = !authEnabled || user?.role === "admin";
259
259
  const projects = {
@@ -282,7 +282,7 @@ const Sidebar = ({ nav, name, logo }) => {
282
282
  }),
283
283
  nav?.projectSlug ? /* @__PURE__ */ jsx(NavSection, {
284
284
  title: "Project",
285
- items: projectNavItems(urls, nav.projectSlug),
285
+ items: projectNavItems(urls, nav.projectSlug, testing),
286
286
  active: nav.active
287
287
  }) : null,
288
288
  /* @__PURE__ */ jsx(NavSection, {
@@ -1 +1 @@
1
- {"version":3,"file":"sidebar.mjs","names":[],"sources":["../../../src/ui/shell/sidebar.tsx"],"sourcesContent":["import { createUrlBuilder, safeImageUrl } from \"@storyshelf/core/urls\";\nimport type { FC } from \"hono/jsx\";\nimport { getStore } from \"../../store.ts\";\nimport { css } from \"../css.ts\";\nimport { Icon } from \"../icons/icon.tsx\";\nimport { ThemeMenu, UserMenu } from \"./account.tsx\";\nimport {\n adminNavItems,\n developerNavItems,\n projectNavItems,\n type NavConfig,\n type NavItem,\n} from \"./nav.ts\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\n/**\n * Left rail. Collapses to icons only on desktop (`html[data-sidebar=\"rail\"]`)\n * and becomes an off-canvas drawer on small screens.\n */\nconst shellSidebar = css`\n /* shell-sidebar */\n position: sticky;\n top: 0;\n height: 100vh;\n display: flex;\n flex-direction: column;\n gap: var(--space-2);\n padding: var(--space-3) var(--space-2);\n background: var(--sidebar-bg);\n border-right: 1px solid var(--border);\n .sidebar__brand {\n display: flex;\n align-items: center;\n gap: 0.6rem;\n min-height: 40px;\n padding: 0 var(--space-2);\n color: var(--text-primary);\n font-weight: 650;\n font-size: var(--text-lg);\n letter-spacing: -0.01em;\n text-decoration: none;\n }\n .sidebar__brand:hover {\n text-decoration: none;\n }\n .sidebar__mark {\n flex: none;\n display: inline-grid;\n place-items: center;\n width: 28px;\n height: 28px;\n border-radius: var(--radius);\n background: var(--accent);\n color: var(--accent-contrast);\n }\n .sidebar__logo {\n flex: none;\n border-radius: var(--radius-sm);\n object-fit: contain;\n }\n .sidebar__nav {\n display: flex;\n flex-direction: column;\n gap: 2px;\n flex: 1;\n min-height: 0;\n overflow-x: hidden;\n overflow-y: auto;\n }\n .sidebar__section {\n margin: var(--space-4) 0 var(--space-1);\n padding: 0 var(--space-3);\n color: var(--text-secondary);\n font-size: 0.68rem;\n font-weight: 600;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n white-space: nowrap;\n }\n .sidebar__link {\n display: flex;\n align-items: center;\n gap: 0.65rem;\n min-height: 38px;\n padding: 0.4rem var(--space-3);\n border-radius: var(--radius);\n color: var(--text-secondary);\n font-size: var(--text-base);\n font-weight: 500;\n text-decoration: none;\n white-space: nowrap;\n transition:\n background var(--dur-fast) var(--ease),\n color var(--dur-fast) var(--ease);\n }\n .sidebar__link:hover {\n background: var(--surface-muted);\n color: var(--text-primary);\n text-decoration: none;\n }\n .sidebar__link--active {\n background: var(--accent-subtle);\n color: var(--accent-fg);\n font-weight: 600;\n }\n .sidebar__link--active:hover {\n background: var(--accent-subtle);\n color: var(--accent-fg);\n }\n .sidebar__link--active svg {\n color: var(--accent);\n }\n .sidebar__ext {\n margin-left: auto;\n opacity: 0.6;\n }\n .sidebar__footer {\n display: grid;\n gap: var(--space-1);\n padding-top: var(--space-2);\n border-top: 1px solid var(--border-subtle);\n }\n .sidebar__tools {\n display: flex;\n align-items: center;\n gap: var(--space-1);\n }\n .sidebar__tools > details {\n flex: none;\n width: auto;\n }\n .sidebar__tools .sidebar__collapse {\n flex: 1;\n width: auto;\n }\n .sidebar__collapse {\n appearance: none;\n display: flex;\n align-items: center;\n gap: 0.65rem;\n min-height: 36px;\n padding: 0.4rem var(--space-3);\n border: 0;\n border-radius: var(--radius);\n background: transparent;\n color: var(--text-secondary);\n font: inherit;\n font-size: var(--text-sm);\n cursor: pointer;\n }\n .sidebar__collapse:hover {\n background: var(--surface-muted);\n color: var(--text-primary);\n }\n @media (min-width: 881px) {\n html[data-sidebar=\"rail\"] & .sidebar__label,\n html[data-sidebar=\"rail\"] & .sidebar__section,\n html[data-sidebar=\"rail\"] & .sidebar__ext {\n display: none;\n }\n html[data-sidebar=\"rail\"] & .sidebar__link,\n html[data-sidebar=\"rail\"] & .sidebar__collapse,\n html[data-sidebar=\"rail\"] & .sidebar__brand {\n justify-content: center;\n padding-inline: 0;\n }\n html[data-sidebar=\"rail\"] & .sidebar__collapse svg {\n transform: rotate(180deg);\n }\n html[data-sidebar=\"rail\"] & .sidebar__footer summary {\n justify-content: center;\n }\n html[data-sidebar=\"rail\"] & .sidebar__footer summary > svg:last-child {\n display: none;\n }\n html[data-sidebar=\"rail\"] & .sidebar__tools {\n flex-direction: column;\n }\n }\n @media (max-width: 880px) {\n & {\n position: fixed;\n inset: 0 auto 0 0;\n z-index: 60;\n width: min(280px, 85vw);\n height: 100dvh;\n transform: translateX(-100%);\n transition: transform var(--dur-base) var(--ease);\n box-shadow: var(--shadow-3);\n }\n &.sidebar--open {\n transform: translateX(0);\n }\n & .sidebar__collapse {\n display: none;\n }\n }\n`;\n\nconst NavLink: FC<{ item: NavItem; active?: string }> = ({ item, active }) => {\n const current = active === item.key;\n return (\n <a\n class={`sidebar__link${current ? \" sidebar__link--active\" : \"\"}`}\n href={item.href}\n title={item.label}\n aria-current={current ? \"page\" : undefined}\n target={item.external ? \"_blank\" : undefined}\n rel={item.external ? \"noopener\" : undefined}\n >\n <Icon name={item.icon} />\n <span class=\"sidebar__label\">{item.label}</span>\n {item.external ? (\n <span class=\"sidebar__ext\">\n <Icon name=\"external-link\" size=\"sm\" />\n </span>\n ) : null}\n </a>\n );\n};\n\nconst NavSection: FC<{ title: string; items: NavItem[]; active?: string }> = ({\n title,\n items,\n active,\n}) => {\n return (\n <>\n <div class=\"sidebar__section\">{title}</div>\n {items.map((item) => (\n <NavLink key={item.key} item={item} active={active} />\n ))}\n </>\n );\n};\n\nconst Brand: FC<{ name: string; logo?: string; href: string }> = ({ name, logo, href }) => {\n const safeLogo = safeImageUrl(logo);\n return (\n <a class=\"sidebar__brand\" href={href} title={name}>\n {safeLogo ? (\n <img class=\"sidebar__logo\" src={safeLogo} alt=\"\" width=\"28\" height=\"28\" />\n ) : (\n <span class=\"sidebar__mark\" aria-hidden=\"true\">\n <Icon name=\"layers\" size=\"sm\" />\n </span>\n )}\n <span class=\"sidebar__label\">{name}</span>\n </a>\n );\n};\n\n/** Primary navigation: brand, workspace, project, developer, and admin entries. */\nexport const Sidebar: FC<{ nav?: NavConfig; name: string; logo?: string }> = ({\n nav,\n name,\n logo,\n}) => {\n const { user, authEnabled, config } = getStore();\n const urls = createUrlBuilder(\"/\", config.publishedBaseDomain);\n const showAdmin = !authEnabled || user?.role === \"admin\";\n const projects: NavItem = {\n key: \"projects\",\n label: \"Projects\",\n href: urls.projects(),\n icon: \"folder\",\n };\n return (\n <aside id=\"sidebar\" class={shellSidebar} aria-label=\"Primary\" data-sidebar>\n <Brand name={name} logo={logo} href={urls.projects()} />\n <nav class=\"sidebar__nav\">\n <NavLink item={projects} active={nav?.active} />\n {nav?.projectSlug ? (\n <NavSection\n title=\"Project\"\n items={projectNavItems(urls, nav.projectSlug)}\n active={nav.active}\n />\n ) : null}\n <NavSection title=\"Developer\" items={developerNavItems()} active={nav?.active} />\n {showAdmin ? (\n <NavSection title=\"Administration\" items={adminNavItems()} active={nav?.active} />\n ) : null}\n </nav>\n <div class=\"sidebar__footer\">\n <UserMenu />\n <div class=\"sidebar__tools\">\n <ThemeMenu />\n <button\n class=\"sidebar__collapse\"\n type=\"button\"\n data-sidebar-collapse\n aria-label=\"Collapse or expand sidebar\"\n title=\"Collapse or expand sidebar\"\n >\n <Icon name=\"chevrons-left\" />\n <span class=\"sidebar__label\">Collapse</span>\n </button>\n </div>\n </div>\n </aside>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"sidebar.mjs","names":[],"sources":["../../../src/ui/shell/sidebar.tsx"],"sourcesContent":["import { createUrlBuilder, safeImageUrl } from \"@storyshelf/core/urls\";\nimport type { FC } from \"hono/jsx\";\nimport { getStore } from \"../../store.ts\";\nimport { css } from \"../css.ts\";\nimport { Icon } from \"../icons/icon.tsx\";\nimport { ThemeMenu, UserMenu } from \"./account.tsx\";\nimport {\n adminNavItems,\n developerNavItems,\n projectNavItems,\n type NavConfig,\n type NavItem,\n} from \"./nav.ts\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\n/**\n * Left rail. Collapses to icons only on desktop (`html[data-sidebar=\"rail\"]`)\n * and becomes an off-canvas drawer on small screens.\n */\nconst shellSidebar = css`\n /* shell-sidebar */\n position: sticky;\n top: 0;\n height: 100vh;\n display: flex;\n flex-direction: column;\n gap: var(--space-2);\n padding: var(--space-3) var(--space-2);\n background: var(--sidebar-bg);\n border-right: 1px solid var(--border);\n .sidebar__brand {\n display: flex;\n align-items: center;\n gap: 0.6rem;\n min-height: 40px;\n padding: 0 var(--space-2);\n color: var(--text-primary);\n font-weight: 650;\n font-size: var(--text-lg);\n letter-spacing: -0.01em;\n text-decoration: none;\n }\n .sidebar__brand:hover {\n text-decoration: none;\n }\n .sidebar__mark {\n flex: none;\n display: inline-grid;\n place-items: center;\n width: 28px;\n height: 28px;\n border-radius: var(--radius);\n background: var(--accent);\n color: var(--accent-contrast);\n }\n .sidebar__logo {\n flex: none;\n border-radius: var(--radius-sm);\n object-fit: contain;\n }\n .sidebar__nav {\n display: flex;\n flex-direction: column;\n gap: 2px;\n flex: 1;\n min-height: 0;\n overflow-x: hidden;\n overflow-y: auto;\n }\n .sidebar__section {\n margin: var(--space-4) 0 var(--space-1);\n padding: 0 var(--space-3);\n color: var(--text-secondary);\n font-size: 0.68rem;\n font-weight: 600;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n white-space: nowrap;\n }\n .sidebar__link {\n display: flex;\n align-items: center;\n gap: 0.65rem;\n min-height: 38px;\n padding: 0.4rem var(--space-3);\n border-radius: var(--radius);\n color: var(--text-secondary);\n font-size: var(--text-base);\n font-weight: 500;\n text-decoration: none;\n white-space: nowrap;\n transition:\n background var(--dur-fast) var(--ease),\n color var(--dur-fast) var(--ease);\n }\n .sidebar__link:hover {\n background: var(--surface-muted);\n color: var(--text-primary);\n text-decoration: none;\n }\n .sidebar__link--active {\n background: var(--accent-subtle);\n color: var(--accent-fg);\n font-weight: 600;\n }\n .sidebar__link--active:hover {\n background: var(--accent-subtle);\n color: var(--accent-fg);\n }\n .sidebar__link--active svg {\n color: var(--accent);\n }\n .sidebar__ext {\n margin-left: auto;\n opacity: 0.6;\n }\n .sidebar__footer {\n display: grid;\n gap: var(--space-1);\n padding-top: var(--space-2);\n border-top: 1px solid var(--border-subtle);\n }\n .sidebar__tools {\n display: flex;\n align-items: center;\n gap: var(--space-1);\n }\n .sidebar__tools > details {\n flex: none;\n width: auto;\n }\n .sidebar__tools .sidebar__collapse {\n flex: 1;\n width: auto;\n }\n .sidebar__collapse {\n appearance: none;\n display: flex;\n align-items: center;\n gap: 0.65rem;\n min-height: 36px;\n padding: 0.4rem var(--space-3);\n border: 0;\n border-radius: var(--radius);\n background: transparent;\n color: var(--text-secondary);\n font: inherit;\n font-size: var(--text-sm);\n cursor: pointer;\n }\n .sidebar__collapse:hover {\n background: var(--surface-muted);\n color: var(--text-primary);\n }\n @media (min-width: 881px) {\n html[data-sidebar=\"rail\"] & .sidebar__label,\n html[data-sidebar=\"rail\"] & .sidebar__section,\n html[data-sidebar=\"rail\"] & .sidebar__ext {\n display: none;\n }\n html[data-sidebar=\"rail\"] & .sidebar__link,\n html[data-sidebar=\"rail\"] & .sidebar__collapse,\n html[data-sidebar=\"rail\"] & .sidebar__brand {\n justify-content: center;\n padding-inline: 0;\n }\n html[data-sidebar=\"rail\"] & .sidebar__collapse svg {\n transform: rotate(180deg);\n }\n html[data-sidebar=\"rail\"] & .sidebar__footer summary {\n justify-content: center;\n }\n html[data-sidebar=\"rail\"] & .sidebar__footer summary > svg:last-child {\n display: none;\n }\n html[data-sidebar=\"rail\"] & .sidebar__tools {\n flex-direction: column;\n }\n }\n @media (max-width: 880px) {\n & {\n position: fixed;\n inset: 0 auto 0 0;\n z-index: 60;\n width: min(280px, 85vw);\n height: 100dvh;\n transform: translateX(-100%);\n transition: transform var(--dur-base) var(--ease);\n box-shadow: var(--shadow-3);\n }\n &.sidebar--open {\n transform: translateX(0);\n }\n & .sidebar__collapse {\n display: none;\n }\n }\n`;\n\nconst NavLink: FC<{ item: NavItem; active?: string }> = ({ item, active }) => {\n const current = active === item.key;\n return (\n <a\n class={`sidebar__link${current ? \" sidebar__link--active\" : \"\"}`}\n href={item.href}\n title={item.label}\n aria-current={current ? \"page\" : undefined}\n target={item.external ? \"_blank\" : undefined}\n rel={item.external ? \"noopener\" : undefined}\n >\n <Icon name={item.icon} />\n <span class=\"sidebar__label\">{item.label}</span>\n {item.external ? (\n <span class=\"sidebar__ext\">\n <Icon name=\"external-link\" size=\"sm\" />\n </span>\n ) : null}\n </a>\n );\n};\n\nconst NavSection: FC<{ title: string; items: NavItem[]; active?: string }> = ({\n title,\n items,\n active,\n}) => {\n return (\n <>\n <div class=\"sidebar__section\">{title}</div>\n {items.map((item) => (\n <NavLink key={item.key} item={item} active={active} />\n ))}\n </>\n );\n};\n\nconst Brand: FC<{ name: string; logo?: string; href: string }> = ({ name, logo, href }) => {\n const safeLogo = safeImageUrl(logo);\n return (\n <a class=\"sidebar__brand\" href={href} title={name}>\n {safeLogo ? (\n <img class=\"sidebar__logo\" src={safeLogo} alt=\"\" width=\"28\" height=\"28\" />\n ) : (\n <span class=\"sidebar__mark\" aria-hidden=\"true\">\n <Icon name=\"layers\" size=\"sm\" />\n </span>\n )}\n <span class=\"sidebar__label\">{name}</span>\n </a>\n );\n};\n\n/** Primary navigation: brand, workspace, project, developer, and admin entries. */\nexport const Sidebar: FC<{ nav?: NavConfig; name: string; logo?: string }> = ({\n nav,\n name,\n logo,\n}) => {\n const { user, authEnabled, config, testing } = getStore();\n const urls = createUrlBuilder(\"/\", config.publishedBaseDomain);\n const showAdmin = !authEnabled || user?.role === \"admin\";\n const projects: NavItem = {\n key: \"projects\",\n label: \"Projects\",\n href: urls.projects(),\n icon: \"folder\",\n };\n return (\n <aside id=\"sidebar\" class={shellSidebar} aria-label=\"Primary\" data-sidebar>\n <Brand name={name} logo={logo} href={urls.projects()} />\n <nav class=\"sidebar__nav\">\n <NavLink item={projects} active={nav?.active} />\n {nav?.projectSlug ? (\n <NavSection\n title=\"Project\"\n items={projectNavItems(urls, nav.projectSlug, testing)}\n active={nav.active}\n />\n ) : null}\n <NavSection title=\"Developer\" items={developerNavItems()} active={nav?.active} />\n {showAdmin ? (\n <NavSection title=\"Administration\" items={adminNavItems()} active={nav?.active} />\n ) : null}\n </nav>\n <div class=\"sidebar__footer\">\n <UserMenu />\n <div class=\"sidebar__tools\">\n <ThemeMenu />\n <button\n class=\"sidebar__collapse\"\n type=\"button\"\n data-sidebar-collapse\n aria-label=\"Collapse or expand sidebar\"\n title=\"Collapse or expand sidebar\"\n >\n <Icon name=\"chevrons-left\" />\n <span class=\"sidebar__label\">Collapse</span>\n </button>\n </div>\n </div>\n </aside>\n );\n};\n"],"mappings":";;;;;;;;;;;;;;;AAoBA,MAAM,eAAe,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoLxB,MAAM,WAAmD,EAAE,MAAM,aAAa;CAC5E,MAAM,UAAU,WAAW,KAAK;CAChC,OACE,qBAAC,KAAD;EACE,OAAO,gBAAgB,UAAU,2BAA2B;EAC5D,MAAM,KAAK;EACX,OAAO,KAAK;EACZ,gBAAc,UAAU,SAAS,KAAA;EACjC,QAAQ,KAAK,WAAW,WAAW,KAAA;EACnC,KAAK,KAAK,WAAW,aAAa,KAAA;EANpC,UAAA;GAQE,oBAAC,MAAD,EAAM,MAAM,KAAK,KAAO,CAAA;GACxB,oBAAC,QAAD;IAAM,OAAM;IAAkB,UAAA,KAAK;GAAY,CAAA;GAC9C,KAAK,WACJ,oBAAC,QAAD;IAAM,OAAM;IACV,UAAA,oBAAC,MAAD;KAAM,MAAK;KAAgB,MAAK;IAAM,CAAA;GAClC,CAAA,IACJ;EACH;;AAEP;AAEA,MAAM,cAAwE,EAC5E,OACA,OACA,aACI;CACJ,OACE,qBAAA,UAAA,EAAA,UAAA,CACE,oBAAC,OAAD;EAAK,OAAM;EAAoB,UAAA;CAAW,CAAA,GACzC,MAAM,KAAK,SACV,oBAAC,SAAD;EAA8B;EAAc;CAAS,GAAvC,KAAK,GAAkC,CACtD,CACD,EAAA,CAAA;AAEN;AAEA,MAAM,SAA4D,EAAE,MAAM,MAAM,WAAW;CACzF,MAAM,WAAW,aAAa,IAAI;CAClC,OACE,qBAAC,KAAD;EAAG,OAAM;EAAuB;EAAM,OAAO;EAA7C,UAAA,CACG,WACC,oBAAC,OAAD;GAAK,OAAM;GAAgB,KAAK;GAAU,KAAI;GAAG,OAAM;GAAK,QAAO;EAAM,CAAA,IAEzE,oBAAC,QAAD;GAAM,OAAM;GAAgB,eAAY;GACtC,UAAA,oBAAC,MAAD;IAAM,MAAK;IAAS,MAAK;GAAM,CAAA;EAC3B,CAAA,GAER,oBAAC,QAAD;GAAM,OAAM;GAAkB,UAAA;EAAW,CAAA,CACxC;;AAEP;;AAGA,MAAa,WAAiE,EAC5E,KACA,MACA,WACI;CACJ,MAAM,EAAE,MAAM,aAAa,QAAQ,YAAY,SAAS;CACxD,MAAM,OAAO,iBAAiB,KAAK,OAAO,mBAAmB;CAC7D,MAAM,YAAY,CAAC,eAAe,MAAM,SAAS;CACjD,MAAM,WAAoB;EACxB,KAAK;EACL,OAAO;EACP,MAAM,KAAK,SAAS;EACpB,MAAM;CACR;CACA,OACE,qBAAC,SAAD;EAAO,IAAG;EAAU,OAAO;EAAc,cAAW;EAAU,gBAAA;EAA9D,UAAA;GACE,oBAAC,OAAD;IAAa;IAAY;IAAM,MAAM,KAAK,SAAS;GAAI,CAAA;GACvD,qBAAC,OAAD;IAAK,OAAM;IAAX,UAAA;KACE,oBAAC,SAAD;MAAS,MAAM;MAAU,QAAQ,KAAK;KAAS,CAAA;KAC9C,KAAK,cACJ,oBAAC,YAAD;MACE,OAAM;MACN,OAAO,gBAAgB,MAAM,IAAI,aAAa,OAAO;MACrD,QAAQ,IAAI;KACb,CAAA,IACC;KACJ,oBAAC,YAAD;MAAY,OAAM;MAAY,OAAO,kBAAkB;MAAG,QAAQ,KAAK;KAAS,CAAA;KAC/E,YACC,oBAAC,YAAD;MAAY,OAAM;MAAiB,OAAO,cAAc;MAAG,QAAQ,KAAK;KAAS,CAAA,IAC/E;IACD;;GACL,qBAAC,OAAD;IAAK,OAAM;IAAX,UAAA,CACE,oBAAC,UAAD,CAAW,CAAA,GACX,qBAAC,OAAD;KAAK,OAAM;KAAX,UAAA,CACE,oBAAC,WAAD,CAAY,CAAA,GACZ,qBAAC,UAAD;MACE,OAAM;MACN,MAAK;MACL,yBAAA;MACA,cAAW;MACX,OAAM;MALR,UAAA,CAOE,oBAAC,MAAD,EAAM,MAAK,gBAAiB,CAAA,GAC5B,oBAAC,QAAD;OAAM,OAAM;OAAiB,UAAA;MAAc,CAAA,CACrC;KACL,CAAA,CAAA;IACF,CAAA,CAAA;;EACA;;AAEX"}