@storyshelf/app 0.6.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (429) hide show
  1. package/README.md +42 -4
  2. package/dist/app-types.d.mts +2 -6
  3. package/dist/asset-manifest.mjs +14 -7
  4. package/dist/asset-manifest.mjs.map +1 -1
  5. package/dist/assets/{htmx.min.js → htmx.mjs} +9 -1
  6. package/dist/assets/htmx.mjs.map +1 -0
  7. package/dist/assets/mark.mjs +9 -0
  8. package/dist/assets/mark.mjs.map +1 -0
  9. package/dist/boot-tasks.mjs +79 -0
  10. package/dist/boot-tasks.mjs.map +1 -0
  11. package/dist/build-state-flags.mjs +17 -0
  12. package/dist/build-state-flags.mjs.map +1 -0
  13. package/dist/build-state-setup.mjs +74 -0
  14. package/dist/build-state-setup.mjs.map +1 -0
  15. package/dist/capture-alerts.mjs +45 -0
  16. package/dist/capture-alerts.mjs.map +1 -0
  17. package/dist/capture-process.mjs +58 -0
  18. package/dist/capture-process.mjs.map +1 -0
  19. package/dist/capture-setup.mjs +96 -119
  20. package/dist/capture-setup.mjs.map +1 -1
  21. package/dist/connections.mjs +33 -0
  22. package/dist/connections.mjs.map +1 -0
  23. package/dist/consumer-setup.mjs +57 -0
  24. package/dist/consumer-setup.mjs.map +1 -0
  25. package/dist/credential-keys.mjs +1 -2
  26. package/dist/credentials.mjs +1 -2
  27. package/dist/git-status.mjs +45 -0
  28. package/dist/git-status.mjs.map +1 -0
  29. package/dist/hidden-data.mjs +23 -0
  30. package/dist/hidden-data.mjs.map +1 -0
  31. package/dist/index.d.mts +3 -5
  32. package/dist/index.mjs +204 -127
  33. package/dist/index.mjs.map +1 -1
  34. package/dist/insights/budget.mjs +48 -0
  35. package/dist/insights/budget.mjs.map +1 -0
  36. package/dist/insights/deps.mjs +20 -0
  37. package/dist/insights/deps.mjs.map +1 -0
  38. package/dist/insights/errors.mjs +21 -0
  39. package/dist/insights/errors.mjs.map +1 -0
  40. package/dist/insights/health-panel.mjs +39 -0
  41. package/dist/insights/health-panel.mjs.map +1 -0
  42. package/dist/insights/job.mjs +102 -0
  43. package/dist/insights/job.mjs.map +1 -0
  44. package/dist/insights/load-health.mjs +58 -0
  45. package/dist/insights/load-health.mjs.map +1 -0
  46. package/dist/insights/load-triage.mjs +97 -0
  47. package/dist/insights/load-triage.mjs.map +1 -0
  48. package/dist/insights/panel.mjs +27 -0
  49. package/dist/insights/panel.mjs.map +1 -0
  50. package/dist/insights/request.mjs +112 -0
  51. package/dist/insights/request.mjs.map +1 -0
  52. package/dist/insights/stale.mjs +20 -0
  53. package/dist/insights/stale.mjs.map +1 -0
  54. package/dist/insights/usage-report.mjs +59 -0
  55. package/dist/insights/usage-report.mjs.map +1 -0
  56. package/dist/insights/view.mjs +47 -0
  57. package/dist/insights/view.mjs.map +1 -0
  58. package/dist/job-canceller.mjs +13 -0
  59. package/dist/job-canceller.mjs.map +1 -0
  60. package/dist/job-queue.mjs +50 -0
  61. package/dist/job-queue.mjs.map +1 -0
  62. package/dist/jobs-setup.mjs +131 -0
  63. package/dist/jobs-setup.mjs.map +1 -0
  64. package/dist/lifecycle.mjs +93 -22
  65. package/dist/lifecycle.mjs.map +1 -1
  66. package/dist/middleware/auth-gate.mjs +5 -4
  67. package/dist/middleware/auth-gate.mjs.map +1 -1
  68. package/dist/middleware/csrf.mjs +1 -2
  69. package/dist/middleware/rate-limit.mjs +42 -35
  70. package/dist/middleware/rate-limit.mjs.map +1 -1
  71. package/dist/middleware/request-log.mjs +4 -4
  72. package/dist/middleware/request-log.mjs.map +1 -1
  73. package/dist/middleware/require-review.mjs +50 -0
  74. package/dist/middleware/require-review.mjs.map +1 -0
  75. package/dist/middleware/require-testing.mjs +40 -0
  76. package/dist/middleware/require-testing.mjs.map +1 -0
  77. package/dist/middleware/server-timing.mjs +1 -2
  78. package/dist/middleware/setup-gate.mjs +1 -2
  79. package/dist/middleware/setup-gate.mjs.map +1 -1
  80. package/dist/middleware/store-scope.mjs +13 -4
  81. package/dist/middleware/store-scope.mjs.map +1 -1
  82. package/dist/notifications-pause.mjs +32 -0
  83. package/dist/notifications-pause.mjs.map +1 -0
  84. package/dist/notify.d.mts +3 -4
  85. package/dist/notify.mjs +79 -41
  86. package/dist/notify.mjs.map +1 -1
  87. package/dist/pages/a11y-violations.mjs +115 -0
  88. package/dist/pages/a11y-violations.mjs.map +1 -0
  89. package/dist/pages/admin-ai.mjs +77 -0
  90. package/dist/pages/admin-ai.mjs.map +1 -0
  91. package/dist/pages/admin-system-origin.mjs +132 -0
  92. package/dist/pages/admin-system-origin.mjs.map +1 -0
  93. package/dist/pages/admin-system-targets.mjs +52 -0
  94. package/dist/pages/admin-system-targets.mjs.map +1 -0
  95. package/dist/pages/admin-system.mjs +64 -15
  96. package/dist/pages/admin-system.mjs.map +1 -1
  97. package/dist/pages/build-detail.mjs +105 -78
  98. package/dist/pages/build-detail.mjs.map +1 -1
  99. package/dist/pages/build-diff-actions.mjs +88 -35
  100. package/dist/pages/build-diff-actions.mjs.map +1 -1
  101. package/dist/pages/build-diff-comments.mjs +17 -16
  102. package/dist/pages/build-diff-comments.mjs.map +1 -1
  103. package/dist/pages/build-diff-default.mjs +53 -0
  104. package/dist/pages/build-diff-default.mjs.map +1 -0
  105. package/dist/pages/build-diff-environment.mjs +81 -0
  106. package/dist/pages/build-diff-environment.mjs.map +1 -0
  107. package/dist/pages/build-diff-header.mjs +23 -6
  108. package/dist/pages/build-diff-header.mjs.map +1 -1
  109. package/dist/pages/build-diff-insight.mjs +112 -0
  110. package/dist/pages/build-diff-insight.mjs.map +1 -0
  111. package/dist/pages/build-diff-model.mjs +74 -12
  112. package/dist/pages/build-diff-model.mjs.map +1 -1
  113. package/dist/pages/build-diff-nav.mjs +29 -14
  114. package/dist/pages/build-diff-nav.mjs.map +1 -1
  115. package/dist/pages/build-diff-reference.mjs +84 -0
  116. package/dist/pages/build-diff-reference.mjs.map +1 -0
  117. package/dist/pages/build-diff-shortcuts.mjs +6 -3
  118. package/dist/pages/build-diff-shortcuts.mjs.map +1 -1
  119. package/dist/pages/build-diff-stale.mjs +1 -2
  120. package/dist/pages/build-diff-usage.mjs +62 -0
  121. package/dist/pages/build-diff-usage.mjs.map +1 -0
  122. package/dist/pages/build-diff-viewer.mjs +97 -14
  123. package/dist/pages/build-diff-viewer.mjs.map +1 -1
  124. package/dist/pages/build-diff.mjs +37 -22
  125. package/dist/pages/build-diff.mjs.map +1 -1
  126. package/dist/pages/build-tests.mjs +73 -0
  127. package/dist/pages/build-tests.mjs.map +1 -0
  128. package/dist/pages/component-detail.mjs +248 -0
  129. package/dist/pages/component-detail.mjs.map +1 -0
  130. package/dist/pages/component-stories.mjs +167 -0
  131. package/dist/pages/component-stories.mjs.map +1 -0
  132. package/dist/pages/compute-jobs.mjs +14 -28
  133. package/dist/pages/compute-jobs.mjs.map +1 -1
  134. package/dist/pages/design-links.mjs +181 -0
  135. package/dist/pages/design-links.mjs.map +1 -0
  136. package/dist/pages/environment-change.mjs +52 -0
  137. package/dist/pages/environment-change.mjs.map +1 -0
  138. package/dist/pages/health-panel.mjs +125 -0
  139. package/dist/pages/health-panel.mjs.map +1 -0
  140. package/dist/pages/invite.mjs +1 -2
  141. package/dist/pages/label-detail.mjs +1 -2
  142. package/dist/pages/library-grid.mjs +104 -0
  143. package/dist/pages/library-grid.mjs.map +1 -0
  144. package/dist/pages/library-model.mjs +46 -0
  145. package/dist/pages/library-model.mjs.map +1 -0
  146. package/dist/pages/library-snapshots.mjs +129 -0
  147. package/dist/pages/library-snapshots.mjs.map +1 -0
  148. package/dist/pages/library.mjs +113 -220
  149. package/dist/pages/library.mjs.map +1 -1
  150. package/dist/pages/login.mjs +1 -2
  151. package/dist/pages/notifications-paused.mjs +21 -0
  152. package/dist/pages/notifications-paused.mjs.map +1 -0
  153. package/dist/pages/passkey-ceremony.mjs +1 -2
  154. package/dist/pages/profile.mjs +54 -50
  155. package/dist/pages/profile.mjs.map +1 -1
  156. package/dist/pages/project-builds-table.mjs +3 -4
  157. package/dist/pages/project-builds-table.mjs.map +1 -1
  158. package/dist/pages/project-builds.mjs +52 -23
  159. package/dist/pages/project-builds.mjs.map +1 -1
  160. package/dist/pages/project-create.mjs +1 -2
  161. package/dist/pages/project-settings.mjs +52 -14
  162. package/dist/pages/project-settings.mjs.map +1 -1
  163. package/dist/pages/projects-card.mjs +15 -3
  164. package/dist/pages/projects-card.mjs.map +1 -1
  165. package/dist/pages/projects.mjs +7 -4
  166. package/dist/pages/projects.mjs.map +1 -1
  167. package/dist/pages/review-changeset.mjs +115 -0
  168. package/dist/pages/review-changeset.mjs.map +1 -0
  169. package/dist/pages/review-comments.mjs +102 -0
  170. package/dist/pages/review-comments.mjs.map +1 -0
  171. package/dist/pages/review-verdict.mjs +101 -0
  172. package/dist/pages/review-verdict.mjs.map +1 -0
  173. package/dist/pages/root.mjs +1 -2
  174. package/dist/pages/settings-ai.mjs +68 -0
  175. package/dist/pages/settings-ai.mjs.map +1 -0
  176. package/dist/pages/settings-capture.mjs +123 -0
  177. package/dist/pages/settings-capture.mjs.map +1 -0
  178. package/dist/pages/settings-design-credential.mjs +98 -0
  179. package/dist/pages/settings-design-credential.mjs.map +1 -0
  180. package/dist/pages/settings-design-links.mjs +178 -0
  181. package/dist/pages/settings-design-links.mjs.map +1 -0
  182. package/dist/pages/settings-design.mjs +47 -0
  183. package/dist/pages/settings-design.mjs.map +1 -0
  184. package/dist/pages/settings-general.mjs +47 -23
  185. package/dist/pages/settings-general.mjs.map +1 -1
  186. package/dist/pages/settings-labels.mjs +44 -4
  187. package/dist/pages/settings-labels.mjs.map +1 -1
  188. package/dist/pages/settings-links.mjs +111 -0
  189. package/dist/pages/settings-links.mjs.map +1 -0
  190. package/dist/pages/settings-members.mjs +1 -2
  191. package/dist/pages/settings-notifications-git.mjs +127 -0
  192. package/dist/pages/settings-notifications-git.mjs.map +1 -0
  193. package/dist/pages/settings-notifications.mjs +80 -44
  194. package/dist/pages/settings-notifications.mjs.map +1 -1
  195. package/dist/pages/settings-tests.mjs +34 -4
  196. package/dist/pages/settings-tests.mjs.map +1 -1
  197. package/dist/pages/settings-tokens.mjs +1 -2
  198. package/dist/pages/settings-webhooks.mjs +1 -2
  199. package/dist/pages/storybook.mjs +1 -2
  200. package/dist/pages/target-choices.mjs +38 -0
  201. package/dist/pages/target-choices.mjs.map +1 -0
  202. package/dist/publish-setup.mjs +88 -0
  203. package/dist/publish-setup.mjs.map +1 -0
  204. package/dist/retention-timer.mjs +12 -12
  205. package/dist/retention-timer.mjs.map +1 -1
  206. package/dist/review-events.mjs +47 -0
  207. package/dist/review-events.mjs.map +1 -0
  208. package/dist/routers/admin-pages.mjs +46 -5
  209. package/dist/routers/admin-pages.mjs.map +1 -1
  210. package/dist/routers/admin-public-origin.mjs +92 -0
  211. package/dist/routers/admin-public-origin.mjs.map +1 -0
  212. package/dist/routers/admin.mjs +8 -4
  213. package/dist/routers/admin.mjs.map +1 -1
  214. package/dist/routers/assets.mjs +10 -4
  215. package/dist/routers/assets.mjs.map +1 -1
  216. package/dist/routers/attempts.mjs +1 -2
  217. package/dist/routers/auth-sync.mjs +1 -2
  218. package/dist/routers/auth.mjs +2 -3
  219. package/dist/routers/baseline-guard.mjs +4 -5
  220. package/dist/routers/baseline-guard.mjs.map +1 -1
  221. package/dist/routers/baseline-reference.mjs +72 -0
  222. package/dist/routers/baseline-reference.mjs.map +1 -0
  223. package/dist/routers/baselines.handlers.mjs +63 -0
  224. package/dist/routers/baselines.handlers.mjs.map +1 -0
  225. package/dist/routers/baselines.mjs +124 -0
  226. package/dist/routers/baselines.mjs.map +1 -0
  227. package/dist/routers/build-cancel.mjs +67 -0
  228. package/dist/routers/build-cancel.mjs.map +1 -0
  229. package/dist/routers/build-changeset.mjs +69 -0
  230. package/dist/routers/build-changeset.mjs.map +1 -0
  231. package/dist/routers/build-reviews.handlers.mjs +85 -0
  232. package/dist/routers/build-reviews.handlers.mjs.map +1 -0
  233. package/dist/routers/build-reviews.mjs +73 -0
  234. package/dist/routers/build-reviews.mjs.map +1 -0
  235. package/dist/routers/builds.handlers.mjs +65 -60
  236. package/dist/routers/builds.handlers.mjs.map +1 -1
  237. package/dist/routers/builds.mjs +77 -19
  238. package/dist/routers/builds.mjs.map +1 -1
  239. package/dist/routers/capture-cache.mjs +29 -0
  240. package/dist/routers/capture-cache.mjs.map +1 -0
  241. package/dist/routers/capture-config.mjs +34 -0
  242. package/dist/routers/capture-config.mjs.map +1 -0
  243. package/dist/routers/comments.mjs +42 -17
  244. package/dist/routers/comments.mjs.map +1 -1
  245. package/dist/routers/comparison-image.mjs +29 -0
  246. package/dist/routers/comparison-image.mjs.map +1 -0
  247. package/dist/routers/component-catalog.handlers.mjs +70 -0
  248. package/dist/routers/component-catalog.handlers.mjs.map +1 -0
  249. package/dist/routers/component.handlers.mjs +120 -0
  250. package/dist/routers/component.handlers.mjs.map +1 -0
  251. package/dist/routers/connections.mjs +117 -0
  252. package/dist/routers/connections.mjs.map +1 -0
  253. package/dist/routers/design-connect.handlers.mjs +43 -0
  254. package/dist/routers/design-connect.handlers.mjs.map +1 -0
  255. package/dist/routers/design.handlers.mjs +113 -0
  256. package/dist/routers/design.handlers.mjs.map +1 -0
  257. package/dist/routers/flash.mjs +1 -2
  258. package/dist/routers/health-report.mjs +3 -5
  259. package/dist/routers/health-report.mjs.map +1 -1
  260. package/dist/routers/health.mjs +5 -5
  261. package/dist/routers/health.mjs.map +1 -1
  262. package/dist/routers/helpers.mjs +20 -5
  263. package/dist/routers/helpers.mjs.map +1 -1
  264. package/dist/routers/htmx.mjs +1 -2
  265. package/dist/routers/insights-ui.mjs +63 -0
  266. package/dist/routers/insights-ui.mjs.map +1 -0
  267. package/dist/routers/insights.handlers.mjs +44 -0
  268. package/dist/routers/insights.handlers.mjs.map +1 -0
  269. package/dist/routers/insights.mjs +188 -0
  270. package/dist/routers/insights.mjs.map +1 -0
  271. package/dist/routers/labels.mjs +1 -2
  272. package/dist/routers/links.handlers.mjs +79 -0
  273. package/dist/routers/links.handlers.mjs.map +1 -0
  274. package/dist/routers/links.mjs +138 -0
  275. package/dist/routers/links.mjs.map +1 -0
  276. package/dist/routers/media.mjs +30 -7
  277. package/dist/routers/media.mjs.map +1 -1
  278. package/dist/routers/members.mjs +3 -3
  279. package/dist/routers/members.mjs.map +1 -1
  280. package/dist/routers/notification-channels.mjs +18 -25
  281. package/dist/routers/notification-channels.mjs.map +1 -1
  282. package/dist/routers/notification-subscriptions.mjs +1 -2
  283. package/dist/routers/oauth-state.mjs +44 -0
  284. package/dist/routers/oauth-state.mjs.map +1 -0
  285. package/dist/routers/preview-base.mjs +37 -0
  286. package/dist/routers/preview-base.mjs.map +1 -0
  287. package/dist/routers/preview-token.mjs +50 -0
  288. package/dist/routers/preview-token.mjs.map +1 -0
  289. package/dist/routers/profile.mjs +4 -3
  290. package/dist/routers/profile.mjs.map +1 -1
  291. package/dist/routers/projects.mjs +18 -3
  292. package/dist/routers/projects.mjs.map +1 -1
  293. package/dist/routers/publish-upload.mjs +85 -0
  294. package/dist/routers/publish-upload.mjs.map +1 -0
  295. package/dist/routers/published-access.mjs +51 -0
  296. package/dist/routers/published-access.mjs.map +1 -0
  297. package/dist/routers/published-compression.mjs +109 -0
  298. package/dist/routers/published-compression.mjs.map +1 -0
  299. package/dist/routers/published-files.mjs +69 -0
  300. package/dist/routers/published-files.mjs.map +1 -0
  301. package/dist/routers/review-forms.mjs +71 -0
  302. package/dist/routers/review-forms.mjs.map +1 -0
  303. package/dist/routers/review-next.mjs +1 -2
  304. package/dist/routers/review-section.handlers.mjs +84 -0
  305. package/dist/routers/review-section.handlers.mjs.map +1 -0
  306. package/dist/routers/review-switch-off.mjs +28 -0
  307. package/dist/routers/review-switch-off.mjs.map +1 -0
  308. package/dist/routers/schemas.mjs +141 -24
  309. package/dist/routers/schemas.mjs.map +1 -1
  310. package/dist/routers/scoped-models.mjs +21 -0
  311. package/dist/routers/scoped-models.mjs.map +1 -0
  312. package/dist/routers/settings-ai.mjs +34 -0
  313. package/dist/routers/settings-ai.mjs.map +1 -0
  314. package/dist/routers/settings-capture.mjs +76 -0
  315. package/dist/routers/settings-capture.mjs.map +1 -0
  316. package/dist/routers/settings-design.mjs +102 -0
  317. package/dist/routers/settings-design.mjs.map +1 -0
  318. package/dist/routers/settings-general.mjs +56 -12
  319. package/dist/routers/settings-general.mjs.map +1 -1
  320. package/dist/routers/settings-git-channel.mjs +76 -0
  321. package/dist/routers/settings-git-channel.mjs.map +1 -0
  322. package/dist/routers/settings-labels.mjs +28 -3
  323. package/dist/routers/settings-labels.mjs.map +1 -1
  324. package/dist/routers/settings-links.mjs +62 -0
  325. package/dist/routers/settings-links.mjs.map +1 -0
  326. package/dist/routers/settings-members.mjs +5 -9
  327. package/dist/routers/settings-members.mjs.map +1 -1
  328. package/dist/routers/settings-notifications.mjs +16 -12
  329. package/dist/routers/settings-notifications.mjs.map +1 -1
  330. package/dist/routers/settings-tokens.mjs +3 -7
  331. package/dist/routers/settings-tokens.mjs.map +1 -1
  332. package/dist/routers/settings-webhooks.mjs +8 -26
  333. package/dist/routers/settings-webhooks.mjs.map +1 -1
  334. package/dist/routers/settings.handlers.mjs +108 -28
  335. package/dist/routers/settings.handlers.mjs.map +1 -1
  336. package/dist/routers/settings.mjs +17 -6
  337. package/dist/routers/settings.mjs.map +1 -1
  338. package/dist/routers/signed-storybook.mjs +55 -0
  339. package/dist/routers/signed-storybook.mjs.map +1 -0
  340. package/dist/routers/snapshot-groups.mjs +62 -0
  341. package/dist/routers/snapshot-groups.mjs.map +1 -0
  342. package/dist/routers/snapshots.mjs +104 -7
  343. package/dist/routers/snapshots.mjs.map +1 -1
  344. package/dist/routers/storybook.mjs +57 -104
  345. package/dist/routers/storybook.mjs.map +1 -1
  346. package/dist/routers/tokens.mjs +3 -7
  347. package/dist/routers/tokens.mjs.map +1 -1
  348. package/dist/routers/ui.mjs +38 -7
  349. package/dist/routers/ui.mjs.map +1 -1
  350. package/dist/routers/webhooks.mjs +6 -13
  351. package/dist/routers/webhooks.mjs.map +1 -1
  352. package/dist/routers/well-known.mjs +12 -6
  353. package/dist/routers/well-known.mjs.map +1 -1
  354. package/dist/routes/api-routes.mjs +92 -0
  355. package/dist/routes/api-routes.mjs.map +1 -0
  356. package/dist/routes/classification.mjs +37 -0
  357. package/dist/routes/classification.mjs.map +1 -0
  358. package/dist/routes/openapi-classes.mjs +21 -0
  359. package/dist/routes/openapi-classes.mjs.map +1 -0
  360. package/dist/routes/page-routes.mjs +103 -0
  361. package/dist/routes/page-routes.mjs.map +1 -0
  362. package/dist/runtime.mjs +19 -15
  363. package/dist/runtime.mjs.map +1 -1
  364. package/dist/serverless.mjs +4 -0
  365. package/dist/store.mjs +20 -3
  366. package/dist/store.mjs.map +1 -1
  367. package/dist/targets.mjs +45 -0
  368. package/dist/targets.mjs.map +1 -0
  369. package/dist/ui/avatar.mjs +1 -2
  370. package/dist/ui/buttons.mjs +17 -5
  371. package/dist/ui/buttons.mjs.map +1 -1
  372. package/dist/ui/code-block.mjs +1 -2
  373. package/dist/ui/compare-stage.mjs +86 -54
  374. package/dist/ui/compare-stage.mjs.map +1 -1
  375. package/dist/ui/csrf-field.mjs +1 -2
  376. package/dist/ui/css.mjs +1 -2
  377. package/dist/ui/design-reference.mjs +96 -0
  378. package/dist/ui/design-reference.mjs.map +1 -0
  379. package/dist/ui/document.mjs +2 -4
  380. package/dist/ui/document.mjs.map +1 -1
  381. package/dist/ui/dropdown.mjs +1 -2
  382. package/dist/ui/feedback.mjs +1 -2
  383. package/dist/ui/filter-input.mjs +1 -2
  384. package/dist/ui/forms.mjs +7 -7
  385. package/dist/ui/forms.mjs.map +1 -1
  386. package/dist/ui/icons/icon.mjs +1 -2
  387. package/dist/ui/icons/paths.mjs +2 -2
  388. package/dist/ui/icons/paths.mjs.map +1 -1
  389. package/dist/ui/icons/sprite.mjs +1 -2
  390. package/dist/ui/kbd.mjs +1 -2
  391. package/dist/ui/layout.mjs +1 -2
  392. package/dist/ui/preview-embed.mjs +81 -0
  393. package/dist/ui/preview-embed.mjs.map +1 -0
  394. package/dist/ui/progress.mjs +1 -2
  395. package/dist/ui/segmented.mjs +1 -2
  396. package/dist/ui/shell/account.mjs +1 -2
  397. package/dist/ui/shell/bare.mjs +1 -2
  398. package/dist/ui/shell/boot-script.mjs +1 -2
  399. package/dist/ui/shell/client-script.mjs +86 -2
  400. package/dist/ui/shell/client-script.mjs.map +1 -1
  401. package/dist/ui/shell/nav.mjs +13 -10
  402. package/dist/ui/shell/nav.mjs.map +1 -1
  403. package/dist/ui/shell/review-script.mjs +25 -3
  404. package/dist/ui/shell/review-script.mjs.map +1 -1
  405. package/dist/ui/shell/sidebar.mjs +3 -4
  406. package/dist/ui/shell/sidebar.mjs.map +1 -1
  407. package/dist/ui/shell/topbar.mjs +1 -2
  408. package/dist/ui/stacks.mjs +1 -2
  409. package/dist/ui/styles/base.mjs +1 -2
  410. package/dist/ui/styles/behavior.mjs +1 -2
  411. package/dist/ui/styles/review-layout.mjs +1 -2
  412. package/dist/ui/styles/review.mjs +1 -2
  413. package/dist/ui/styles/tokens.mjs +1 -2
  414. package/dist/ui/styles.mjs +1 -2
  415. package/dist/ui/subnav.mjs +1 -2
  416. package/dist/ui/table.mjs +1 -2
  417. package/dist/ui/theme.mjs +1 -2
  418. package/dist/ui/thumbnail.mjs +1 -2
  419. package/dist/ui/time.mjs +1 -2
  420. package/package.json +14 -12
  421. package/dist/asset-paths.mjs +0 -23
  422. package/dist/asset-paths.mjs.map +0 -1
  423. package/dist/assets/mark.svg +0 -1
  424. package/dist/pages/settings-status.mjs +0 -137
  425. package/dist/pages/settings-status.mjs.map +0 -1
  426. package/dist/routers/settings-status.mjs +0 -77
  427. package/dist/routers/settings-status.mjs.map +0 -1
  428. package/dist/routers/status-configs.mjs +0 -108
  429. package/dist/routers/status-configs.mjs.map +0 -1
@@ -3,7 +3,7 @@ import "node:url";
3
3
  import.meta.url;
4
4
  import { iconSpriteHref } from "../icons/sprite.mjs";
5
5
  import { reviewScript } from "./review-script.mjs";
6
- //#region src/ui/shell/client-script.ts
6
+ // src/ui/shell/client-script.ts
7
7
  /** Attach the CSRF token to every HTMX request. */
8
8
  function csrfPart() {
9
9
  return `
@@ -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()
@@ -253,7 +338,6 @@ function clientScript() {
253
338
  })();
254
339
  `.trim();
255
340
  }
256
- //#endregion
257
341
  export { clientScript };
258
342
 
259
343
  //# sourceMappingURL=client-script.mjs.map
@@ -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"}
@@ -1,9 +1,18 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
- //#region src/ui/shell/nav.ts
5
- /** Entries shown for the current project. */
6
- function projectNavItems(urls, slug) {
4
+ // src/ui/shell/nav.ts
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",
@@ -105,7 +109,6 @@ function sectionCrumb(nav, urls) {
105
109
  }
106
110
  }[nav?.active ?? ""] ?? null;
107
111
  }
108
- //#endregion
109
112
  export { adminNavItems, breadcrumbs, developerNavItems, projectNavItems };
110
113
 
111
114
  //# sourceMappingURL=nav.mjs.map
@@ -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"}
@@ -1,7 +1,7 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
- //#region src/ui/shell/review-script.ts
4
+ // src/ui/shell/review-script.ts
5
5
  /**
6
6
  * Review workspace behavior: comparison modes, zoom, synchronized scrolling,
7
7
  * list filters, keyboard review, and the shortcuts dialog. Runs inside the
@@ -29,12 +29,18 @@ function stateHelpers() {
29
29
  b.setAttribute('aria-pressed', b.getAttribute('data-view-value')===value ? 'true' : 'false');
30
30
  });
31
31
  }
32
+ function allows(stage,attr,value){
33
+ var list=stage.getAttribute(attr);
34
+ return !list || list.split(',').indexOf(value)>-1;
35
+ }
32
36
  function setMode(stage,mode,persist){
37
+ if(!allows(stage,'data-modes',mode)) return;
33
38
  stage.setAttribute('data-view',mode);
34
39
  press(stage.querySelector('[data-compare-mode]'),mode);
35
40
  if(persist) remember('ss_compare_mode',mode);
36
41
  }
37
42
  function setZoom(stage,zoom,persist){
43
+ if(!allows(stage,'data-zooms',zoom)) return;
38
44
  stage.setAttribute('data-zoom',zoom);
39
45
  press(stage.querySelector('[data-compare-zoom]'),zoom);
40
46
  if(persist) remember('ss_compare_zoom',zoom);
@@ -135,11 +141,20 @@ function clickControls() {
135
141
  var dialog=document.querySelector('dialog[data-shortcuts]');
136
142
  if(dialog&&typeof dialog.showModal==='function'&&!dialog.open){ dialog.showModal(); }
137
143
  }
144
+ function chooseSource(btn,value){
145
+ var root=btn.closest('[data-compare-sources]');
146
+ if(!root) return;
147
+ root.querySelectorAll('[data-compare-source]').forEach(function(pane){
148
+ pane.hidden = pane.getAttribute('data-compare-source')!==value;
149
+ });
150
+ press(btn.closest('[data-compare-source-switch]'),value);
151
+ }
138
152
  function chooseSegment(btn){
139
153
  var value=btn.getAttribute('data-view-value');
140
154
  var stage=btn.closest('[data-compare]');
141
155
  if(stage&&btn.closest('[data-compare-mode]')){ setMode(stage,value,true); }
142
156
  else if(stage&&btn.closest('[data-compare-zoom]')){ setZoom(stage,value,true); }
157
+ else if(btn.closest('[data-compare-source-switch]')){ chooseSource(btn,value); }
143
158
  else if(btn.closest('[data-filter-switch]')){
144
159
  var list=document.querySelector('[data-diff-nav]');
145
160
  if(list){ list.setAttribute('data-filter',value); }
@@ -166,7 +181,15 @@ function keyboardShortcuts() {
166
181
  if(el&&!el.disabled){ el.click(); return true; }
167
182
  return false;
168
183
  }
184
+ function stepBrowser(dir){
185
+ var tabs=[].slice.call(document.querySelectorAll('[data-browser-tabs] a'));
186
+ var at=tabs.findIndex(function(a){ return a.getAttribute('aria-current')==='page'; });
187
+ var target=at===-1?null:tabs[at+dir];
188
+ if(target){ target.click(); return true; }
189
+ return false;
190
+ }
169
191
  function handleKey(key,stage){
192
+ if(key==='['||key===']') return stepBrowser(key===']'?1:-1);
170
193
  if(key==='j'||key==='ArrowRight') return press1('[data-snap-next]');
171
194
  if(key==='k'||key==='ArrowLeft') return press1('[data-snap-prev]');
172
195
  if(key==='a'||key==='A') return press1('[data-approve]');
@@ -184,12 +207,11 @@ function keyboardShortcuts() {
184
207
  }
185
208
  document.addEventListener('keydown',function(e){
186
209
  if(e.metaKey||e.ctrlKey||e.altKey||typing(e.target)) return;
187
- var stage=document.querySelector('[data-compare]');
210
+ var stage=document.querySelector('[data-compare-source]:not([hidden]) [data-compare]')||document.querySelector('[data-compare]');
188
211
  if(!stage||document.querySelector('dialog[open]')) return;
189
212
  if(handleKey(e.key,stage)){ e.preventDefault(); }
190
213
  });`;
191
214
  }
192
- //#endregion
193
215
  export { reviewScript };
194
216
 
195
217
  //# sourceMappingURL=review-script.mjs.map
@@ -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 allows(stage,attr,value){\n var list=stage.getAttribute(attr);\n return !list || list.split(',').indexOf(value)>-1;\n }\n function setMode(stage,mode,persist){\n if(!allows(stage,'data-modes',mode)) return;\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 if(!allows(stage,'data-zooms',zoom)) return;\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 chooseSource(btn,value){\n var root=btn.closest('[data-compare-sources]');\n if(!root) return;\n root.querySelectorAll('[data-compare-source]').forEach(function(pane){\n pane.hidden = pane.getAttribute('data-compare-source')!==value;\n });\n press(btn.closest('[data-compare-source-switch]'),value);\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-compare-source-switch]')){ chooseSource(btn,value); }\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-source]:not([hidden]) [data-compare]')||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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCT;;AAGA,SAAS,aAAqB;CAC5B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCT;;;;;;AAOA,SAAS,WAAmB;CAC1B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCT;;AAGA,SAAS,gBAAwB;CAC/B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCT;;AAGA,SAAS,oBAA4B;CACnC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCT"}
@@ -8,7 +8,7 @@ import { ThemeMenu, UserMenu } from "./account.mjs";
8
8
  import { adminNavItems, developerNavItems, projectNavItems } from "./nav.mjs";
9
9
  import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
10
10
  import { createUrlBuilder, safeImageUrl } from "@storyshelf/core/urls";
11
- //#region src/ui/shell/sidebar.tsx
11
+ // src/ui/shell/sidebar.tsx
12
12
  /**
13
13
  * Left rail. Collapses to icons only on desktop (`html[data-sidebar="rail"]`)
14
14
  * and becomes an off-canvas drawer on small screens.
@@ -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, {
@@ -317,7 +317,6 @@ const Sidebar = ({ nav, name, logo }) => {
317
317
  ]
318
318
  });
319
319
  };
320
- //#endregion
321
320
  export { Sidebar };
322
321
 
323
322
  //# sourceMappingURL=sidebar.mjs.map
@@ -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":";;;;;;;;;;;;;;;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,WAAW,SAAS;CAC/C,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,WAAW;MAC5C,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"}
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"}
@@ -10,7 +10,7 @@ import { breadcrumbs } from "./nav.mjs";
10
10
  import { ProjectModel } from "@storyshelf/core/models";
11
11
  import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
12
12
  import { createUrlBuilder } from "@storyshelf/core/urls";
13
- //#region src/ui/shell/topbar.tsx
13
+ // src/ui/shell/topbar.tsx
14
14
  /** Slim, translucent bar: project picker plus breadcrumbs (account lives in the sidebar). */
15
15
  const shellTopbar = css`
16
16
  /* shell-topbar */
@@ -262,7 +262,6 @@ const ToastRegion = () => {
262
262
  "aria-atomic": "false"
263
263
  });
264
264
  };
265
- //#endregion
266
265
  export { ToastRegion, TopBar };
267
266
 
268
267
  //# sourceMappingURL=topbar.mjs.map
@@ -3,7 +3,7 @@ import "node:url";
3
3
  import.meta.url;
4
4
  import { css } from "./css.mjs";
5
5
  import { jsx } from "hono/jsx/jsx-runtime";
6
- //#region src/ui/stacks.tsx
6
+ // src/ui/stacks.tsx
7
7
  const hGaps = {
8
8
  sm: "0.5rem",
9
9
  md: "0.75rem",
@@ -76,7 +76,6 @@ const VStack = ({ gap = "md", align = "stretch", children }) => {
76
76
  children
77
77
  });
78
78
  };
79
- //#endregion
80
79
  export { HStack, VStack };
81
80
 
82
81
  //# sourceMappingURL=stacks.mjs.map
@@ -1,7 +1,7 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
- //#region src/ui/styles/base.ts
4
+ // src/ui/styles/base.ts
5
5
  /** Base reset, typography, focus ring, and shared utilities. */
6
6
  function baseCss() {
7
7
  return `${resetCss()}\n${utilityCss()}`;
@@ -35,7 +35,6 @@ function utilityCss() {
35
35
  .mb-1 { margin-bottom: 1rem; }
36
36
  @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }`;
37
37
  }
38
- //#endregion
39
38
  export { baseCss };
40
39
 
41
40
  //# sourceMappingURL=base.mjs.map
@@ -1,7 +1,7 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
- //#region src/ui/styles/behavior.ts
4
+ // src/ui/styles/behavior.ts
5
5
  /** Behavioral CSS: HTMX indicators, skeletons, tables, stats. */
6
6
  function behaviorCss() {
7
7
  return `${htmxCss()}\n${dataCss()}\n${chromeCss()}`;
@@ -37,7 +37,6 @@ function chromeCss() {
37
37
  [data-theme-icon] { display: none; }
38
38
  [data-theme="light"] [data-theme-icon="light"], [data-theme="dark"] [data-theme-icon="dark"], [data-theme="system"] [data-theme-icon="system"] { display: inline-flex; }`;
39
39
  }
40
- //#endregion
41
40
  export { behaviorCss };
42
41
 
43
42
  //# sourceMappingURL=behavior.mjs.map
@@ -2,7 +2,7 @@ import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
4
  import { css } from "../css.mjs";
5
- //#region src/ui/styles/review-layout.ts
5
+ // src/ui/styles/review-layout.ts
6
6
  /**
7
7
  * Review workspace layout styles: header, filmstrip, canvas column, action
8
8
  * bar, and shortcuts dialog. Shared by the `build-diff-*` page sections, so
@@ -341,7 +341,6 @@ const shortcutsDialog = css`
341
341
  color: var(--text-secondary);
342
342
  }
343
343
  `;
344
- //#endregion
345
344
  export { actionBar, actionGroup, actionHints, actionPosition, canvasColumn, filmBody, filmItem, filmItemActive, filmStatus, filmSub, filmTitle, filmstrip, filmstripHead, filmstripList, headerActions, headerMain, headerMeta, headerProgress, headerSide, headerTitle, reviewHeader, shortcutsDialog, statusDots, viewerHead, viewerMeta, viewerTitle, workspace };
346
345
 
347
346
  //# sourceMappingURL=review-layout.mjs.map
@@ -2,7 +2,7 @@ import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
4
  import { css } from "../css.mjs";
5
- //#region src/ui/styles/review.ts
5
+ // src/ui/styles/review.ts
6
6
  /**
7
7
  * Snapshot-card and comment styles shared by the build detail, library, and
8
8
  * review pages (the review workspace layout lives in `review-layout.ts`).
@@ -80,7 +80,6 @@ const reviewCommentActions = css`
80
80
  /* review-comment-actions */
81
81
  margin-top: 0.5rem;
82
82
  `;
83
- //#endregion
84
83
  export { diffPaneImg, reviewComment, reviewCommentActions, reviewCommentBody, reviewCommentHead, snapshotCard, snapshotCardBody, snapshotCardHead, snapshotCardMeta, snapshotGrid };
85
84
 
86
85
  //# sourceMappingURL=review.mjs.map
@@ -1,7 +1,7 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
- //#region src/ui/styles/tokens.ts
4
+ // src/ui/styles/tokens.ts
5
5
  /**
6
6
  * CSS variable tokens: brand-driven colors (per theme), theme-independent
7
7
  * scales (type, space, radius, motion), and tokens derived from the brand
@@ -126,7 +126,6 @@ function derivedVars(fb) {
126
126
  --status-rejected-border: ${mix("--status-rejected", "32%", "--border")};
127
127
  --status-info-border: ${mix("--status-info", "32%", "--border")};`;
128
128
  }
129
- //#endregion
130
129
  export { tokenCss };
131
130
 
132
131
  //# sourceMappingURL=tokens.mjs.map