@storyshelf/app 0.6.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -4
- package/dist/app-types.d.mts +1 -3
- package/dist/asset-manifest.mjs +13 -5
- package/dist/asset-manifest.mjs.map +1 -1
- package/dist/assets/{htmx.min.js → htmx.mjs} +10 -1
- package/dist/assets/htmx.mjs.map +1 -0
- package/dist/assets/mark.mjs +10 -0
- package/dist/assets/mark.mjs.map +1 -0
- package/dist/boot-tasks.mjs +80 -0
- package/dist/boot-tasks.mjs.map +1 -0
- package/dist/build-state-flags.mjs +18 -0
- package/dist/build-state-flags.mjs.map +1 -0
- package/dist/build-state-setup.mjs +75 -0
- package/dist/build-state-setup.mjs.map +1 -0
- package/dist/capture-alerts.mjs +46 -0
- package/dist/capture-alerts.mjs.map +1 -0
- package/dist/capture-process.mjs +59 -0
- package/dist/capture-process.mjs.map +1 -0
- package/dist/capture-setup.mjs +95 -117
- package/dist/capture-setup.mjs.map +1 -1
- package/dist/connections.mjs +34 -0
- package/dist/connections.mjs.map +1 -0
- package/dist/consumer-setup.mjs +58 -0
- package/dist/consumer-setup.mjs.map +1 -0
- package/dist/git-status.mjs +46 -0
- package/dist/git-status.mjs.map +1 -0
- package/dist/hidden-data.mjs +24 -0
- package/dist/hidden-data.mjs.map +1 -0
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +203 -125
- package/dist/index.mjs.map +1 -1
- package/dist/insights/budget.mjs +49 -0
- package/dist/insights/budget.mjs.map +1 -0
- package/dist/insights/deps.mjs +21 -0
- package/dist/insights/deps.mjs.map +1 -0
- package/dist/insights/errors.mjs +22 -0
- package/dist/insights/errors.mjs.map +1 -0
- package/dist/insights/health-panel.mjs +40 -0
- package/dist/insights/health-panel.mjs.map +1 -0
- package/dist/insights/job.mjs +103 -0
- package/dist/insights/job.mjs.map +1 -0
- package/dist/insights/load-health.mjs +59 -0
- package/dist/insights/load-health.mjs.map +1 -0
- package/dist/insights/load-triage.mjs +98 -0
- package/dist/insights/load-triage.mjs.map +1 -0
- package/dist/insights/panel.mjs +28 -0
- package/dist/insights/panel.mjs.map +1 -0
- package/dist/insights/request.mjs +113 -0
- package/dist/insights/request.mjs.map +1 -0
- package/dist/insights/stale.mjs +21 -0
- package/dist/insights/stale.mjs.map +1 -0
- package/dist/insights/usage-report.mjs +60 -0
- package/dist/insights/usage-report.mjs.map +1 -0
- package/dist/insights/view.mjs +48 -0
- package/dist/insights/view.mjs.map +1 -0
- package/dist/job-canceller.mjs +14 -0
- package/dist/job-canceller.mjs.map +1 -0
- package/dist/job-queue.mjs +51 -0
- package/dist/job-queue.mjs.map +1 -0
- package/dist/jobs-setup.mjs +132 -0
- package/dist/jobs-setup.mjs.map +1 -0
- package/dist/lifecycle.mjs +91 -19
- package/dist/lifecycle.mjs.map +1 -1
- package/dist/middleware/auth-gate.mjs +4 -2
- package/dist/middleware/auth-gate.mjs.map +1 -1
- package/dist/middleware/rate-limit.mjs +41 -33
- package/dist/middleware/rate-limit.mjs.map +1 -1
- package/dist/middleware/request-log.mjs +3 -2
- package/dist/middleware/request-log.mjs.map +1 -1
- package/dist/middleware/require-review.mjs +51 -0
- package/dist/middleware/require-review.mjs.map +1 -0
- package/dist/middleware/require-testing.mjs +41 -0
- package/dist/middleware/require-testing.mjs.map +1 -0
- package/dist/middleware/setup-gate.mjs.map +1 -1
- package/dist/middleware/store-scope.mjs +12 -2
- package/dist/middleware/store-scope.mjs.map +1 -1
- package/dist/notifications-pause.mjs +33 -0
- package/dist/notifications-pause.mjs.map +1 -0
- package/dist/notify.d.mts +2 -1
- package/dist/notify.mjs +78 -39
- package/dist/notify.mjs.map +1 -1
- package/dist/pages/a11y-violations.mjs +116 -0
- package/dist/pages/a11y-violations.mjs.map +1 -0
- package/dist/pages/admin-ai.mjs +78 -0
- package/dist/pages/admin-ai.mjs.map +1 -0
- package/dist/pages/admin-system-origin.mjs +123 -0
- package/dist/pages/admin-system-origin.mjs.map +1 -0
- package/dist/pages/admin-system-targets.mjs +53 -0
- package/dist/pages/admin-system-targets.mjs.map +1 -0
- package/dist/pages/admin-system.mjs +63 -13
- package/dist/pages/admin-system.mjs.map +1 -1
- package/dist/pages/build-detail.mjs +104 -76
- package/dist/pages/build-detail.mjs.map +1 -1
- package/dist/pages/build-diff-actions.mjs +87 -33
- package/dist/pages/build-diff-actions.mjs.map +1 -1
- package/dist/pages/build-diff-comments.mjs +16 -14
- package/dist/pages/build-diff-comments.mjs.map +1 -1
- package/dist/pages/build-diff-environment.mjs +82 -0
- package/dist/pages/build-diff-environment.mjs.map +1 -0
- package/dist/pages/build-diff-header.mjs +22 -4
- package/dist/pages/build-diff-header.mjs.map +1 -1
- package/dist/pages/build-diff-insight.mjs +113 -0
- package/dist/pages/build-diff-insight.mjs.map +1 -0
- package/dist/pages/build-diff-model.mjs +55 -10
- package/dist/pages/build-diff-model.mjs.map +1 -1
- package/dist/pages/build-diff-nav.mjs +28 -12
- package/dist/pages/build-diff-nav.mjs.map +1 -1
- package/dist/pages/build-diff-shortcuts.mjs +5 -1
- package/dist/pages/build-diff-shortcuts.mjs.map +1 -1
- package/dist/pages/build-diff-usage.mjs +63 -0
- package/dist/pages/build-diff-usage.mjs.map +1 -0
- package/dist/pages/build-diff-viewer.mjs +62 -5
- package/dist/pages/build-diff-viewer.mjs.map +1 -1
- package/dist/pages/build-diff.mjs +34 -13
- package/dist/pages/build-diff.mjs.map +1 -1
- package/dist/pages/build-tests.mjs +74 -0
- package/dist/pages/build-tests.mjs.map +1 -0
- package/dist/pages/component-detail.mjs +249 -0
- package/dist/pages/component-detail.mjs.map +1 -0
- package/dist/pages/component-stories.mjs +168 -0
- package/dist/pages/component-stories.mjs.map +1 -0
- package/dist/pages/compute-jobs.mjs +13 -26
- package/dist/pages/compute-jobs.mjs.map +1 -1
- package/dist/pages/design-links.mjs +182 -0
- package/dist/pages/design-links.mjs.map +1 -0
- package/dist/pages/environment-change.mjs +53 -0
- package/dist/pages/environment-change.mjs.map +1 -0
- package/dist/pages/health-panel.mjs +126 -0
- package/dist/pages/health-panel.mjs.map +1 -0
- package/dist/pages/library-grid.mjs +105 -0
- package/dist/pages/library-grid.mjs.map +1 -0
- package/dist/pages/library-model.mjs +47 -0
- package/dist/pages/library-model.mjs.map +1 -0
- package/dist/pages/library-snapshots.mjs +130 -0
- package/dist/pages/library-snapshots.mjs.map +1 -0
- package/dist/pages/library.mjs +112 -218
- package/dist/pages/library.mjs.map +1 -1
- package/dist/pages/notifications-paused.mjs +22 -0
- package/dist/pages/notifications-paused.mjs.map +1 -0
- package/dist/pages/profile.mjs +53 -48
- package/dist/pages/profile.mjs.map +1 -1
- package/dist/pages/project-builds-table.mjs +2 -2
- package/dist/pages/project-builds-table.mjs.map +1 -1
- package/dist/pages/project-builds.mjs +51 -21
- package/dist/pages/project-builds.mjs.map +1 -1
- package/dist/pages/project-settings.mjs +51 -12
- package/dist/pages/project-settings.mjs.map +1 -1
- package/dist/pages/projects-card.mjs +14 -1
- package/dist/pages/projects-card.mjs.map +1 -1
- package/dist/pages/projects.mjs +6 -2
- package/dist/pages/projects.mjs.map +1 -1
- package/dist/pages/review-changeset.mjs +116 -0
- package/dist/pages/review-changeset.mjs.map +1 -0
- package/dist/pages/review-comments.mjs +103 -0
- package/dist/pages/review-comments.mjs.map +1 -0
- package/dist/pages/review-verdict.mjs +102 -0
- package/dist/pages/review-verdict.mjs.map +1 -0
- package/dist/pages/settings-ai.mjs +69 -0
- package/dist/pages/settings-ai.mjs.map +1 -0
- package/dist/pages/settings-capture.mjs +124 -0
- package/dist/pages/settings-capture.mjs.map +1 -0
- package/dist/pages/settings-design-credential.mjs +99 -0
- package/dist/pages/settings-design-credential.mjs.map +1 -0
- package/dist/pages/settings-design-links.mjs +179 -0
- package/dist/pages/settings-design-links.mjs.map +1 -0
- package/dist/pages/settings-design.mjs +48 -0
- package/dist/pages/settings-design.mjs.map +1 -0
- package/dist/pages/settings-general.mjs +46 -21
- package/dist/pages/settings-general.mjs.map +1 -1
- package/dist/pages/settings-labels.mjs +43 -2
- package/dist/pages/settings-labels.mjs.map +1 -1
- package/dist/pages/settings-links.mjs +112 -0
- package/dist/pages/settings-links.mjs.map +1 -0
- package/dist/pages/settings-notifications-git.mjs +128 -0
- package/dist/pages/settings-notifications-git.mjs.map +1 -0
- package/dist/pages/settings-notifications.mjs +79 -42
- package/dist/pages/settings-notifications.mjs.map +1 -1
- package/dist/pages/settings-tests.mjs +33 -2
- package/dist/pages/settings-tests.mjs.map +1 -1
- package/dist/pages/target-choices.mjs +39 -0
- package/dist/pages/target-choices.mjs.map +1 -0
- package/dist/publish-setup.mjs +89 -0
- package/dist/publish-setup.mjs.map +1 -0
- package/dist/retention-timer.mjs +2 -1
- package/dist/retention-timer.mjs.map +1 -1
- package/dist/review-events.mjs +48 -0
- package/dist/review-events.mjs.map +1 -0
- package/dist/routers/admin-pages.mjs +45 -3
- package/dist/routers/admin-pages.mjs.map +1 -1
- package/dist/routers/admin-public-origin.mjs +85 -0
- package/dist/routers/admin-public-origin.mjs.map +1 -0
- package/dist/routers/assets.mjs +9 -2
- package/dist/routers/assets.mjs.map +1 -1
- package/dist/routers/auth.mjs +1 -1
- package/dist/routers/baseline-guard.mjs +2 -2
- package/dist/routers/baseline-guard.mjs.map +1 -1
- package/dist/routers/baselines.handlers.mjs +64 -0
- package/dist/routers/baselines.handlers.mjs.map +1 -0
- package/dist/routers/baselines.mjs +125 -0
- package/dist/routers/baselines.mjs.map +1 -0
- package/dist/routers/build-cancel.mjs +68 -0
- package/dist/routers/build-cancel.mjs.map +1 -0
- package/dist/routers/build-changeset.mjs +70 -0
- package/dist/routers/build-changeset.mjs.map +1 -0
- package/dist/routers/build-reviews.handlers.mjs +86 -0
- package/dist/routers/build-reviews.handlers.mjs.map +1 -0
- package/dist/routers/build-reviews.mjs +74 -0
- package/dist/routers/build-reviews.mjs.map +1 -0
- package/dist/routers/builds.handlers.mjs +46 -55
- package/dist/routers/builds.handlers.mjs.map +1 -1
- package/dist/routers/builds.mjs +71 -17
- package/dist/routers/builds.mjs.map +1 -1
- package/dist/routers/capture-config.mjs +35 -0
- package/dist/routers/capture-config.mjs.map +1 -0
- package/dist/routers/comments.mjs +41 -15
- package/dist/routers/comments.mjs.map +1 -1
- package/dist/routers/component-catalog.handlers.mjs +71 -0
- package/dist/routers/component-catalog.handlers.mjs.map +1 -0
- package/dist/routers/component.handlers.mjs +121 -0
- package/dist/routers/component.handlers.mjs.map +1 -0
- package/dist/routers/connections.mjs +118 -0
- package/dist/routers/connections.mjs.map +1 -0
- package/dist/routers/design-connect.handlers.mjs +44 -0
- package/dist/routers/design-connect.handlers.mjs.map +1 -0
- package/dist/routers/design.handlers.mjs +114 -0
- package/dist/routers/design.handlers.mjs.map +1 -0
- package/dist/routers/health-report.mjs +2 -3
- package/dist/routers/health-report.mjs.map +1 -1
- package/dist/routers/health.mjs +4 -3
- package/dist/routers/health.mjs.map +1 -1
- package/dist/routers/helpers.mjs +19 -3
- package/dist/routers/helpers.mjs.map +1 -1
- package/dist/routers/insights-ui.mjs +64 -0
- package/dist/routers/insights-ui.mjs.map +1 -0
- package/dist/routers/insights.handlers.mjs +45 -0
- package/dist/routers/insights.handlers.mjs.map +1 -0
- package/dist/routers/insights.mjs +189 -0
- package/dist/routers/insights.mjs.map +1 -0
- package/dist/routers/links.handlers.mjs +80 -0
- package/dist/routers/links.handlers.mjs.map +1 -0
- package/dist/routers/links.mjs +139 -0
- package/dist/routers/links.mjs.map +1 -0
- package/dist/routers/media.mjs +18 -4
- package/dist/routers/media.mjs.map +1 -1
- package/dist/routers/members.mjs +2 -1
- package/dist/routers/members.mjs.map +1 -1
- package/dist/routers/notification-channels.mjs +17 -23
- package/dist/routers/notification-channels.mjs.map +1 -1
- package/dist/routers/oauth-state.mjs +45 -0
- package/dist/routers/oauth-state.mjs.map +1 -0
- package/dist/routers/preview-base.mjs +38 -0
- package/dist/routers/preview-base.mjs.map +1 -0
- package/dist/routers/preview-token.mjs +51 -0
- package/dist/routers/preview-token.mjs.map +1 -0
- package/dist/routers/profile.mjs +3 -1
- package/dist/routers/profile.mjs.map +1 -1
- package/dist/routers/projects.mjs +17 -1
- package/dist/routers/projects.mjs.map +1 -1
- package/dist/routers/publish-upload.mjs +78 -0
- package/dist/routers/publish-upload.mjs.map +1 -0
- package/dist/routers/published-access.mjs +52 -0
- package/dist/routers/published-access.mjs.map +1 -0
- package/dist/routers/published-files.mjs +52 -0
- package/dist/routers/published-files.mjs.map +1 -0
- package/dist/routers/review-forms.mjs +72 -0
- package/dist/routers/review-forms.mjs.map +1 -0
- package/dist/routers/review-section.handlers.mjs +85 -0
- package/dist/routers/review-section.handlers.mjs.map +1 -0
- package/dist/routers/review-switch-off.mjs +29 -0
- package/dist/routers/review-switch-off.mjs.map +1 -0
- package/dist/routers/schemas.mjs +140 -22
- package/dist/routers/schemas.mjs.map +1 -1
- package/dist/routers/scoped-models.mjs +22 -0
- package/dist/routers/scoped-models.mjs.map +1 -0
- package/dist/routers/settings-ai.mjs +35 -0
- package/dist/routers/settings-ai.mjs.map +1 -0
- package/dist/routers/settings-capture.mjs +77 -0
- package/dist/routers/settings-capture.mjs.map +1 -0
- package/dist/routers/settings-design.mjs +103 -0
- package/dist/routers/settings-design.mjs.map +1 -0
- package/dist/routers/settings-general.mjs +55 -10
- package/dist/routers/settings-general.mjs.map +1 -1
- package/dist/routers/settings-git-channel.mjs +77 -0
- package/dist/routers/settings-git-channel.mjs.map +1 -0
- package/dist/routers/settings-labels.mjs +27 -1
- package/dist/routers/settings-labels.mjs.map +1 -1
- package/dist/routers/settings-links.mjs +63 -0
- package/dist/routers/settings-links.mjs.map +1 -0
- package/dist/routers/settings-members.mjs +4 -7
- package/dist/routers/settings-members.mjs.map +1 -1
- package/dist/routers/settings-notifications.mjs +15 -10
- package/dist/routers/settings-notifications.mjs.map +1 -1
- package/dist/routers/settings-tokens.mjs +2 -5
- package/dist/routers/settings-tokens.mjs.map +1 -1
- package/dist/routers/settings-webhooks.mjs +7 -24
- package/dist/routers/settings-webhooks.mjs.map +1 -1
- package/dist/routers/settings.handlers.mjs +107 -26
- package/dist/routers/settings.handlers.mjs.map +1 -1
- package/dist/routers/settings.mjs +16 -4
- package/dist/routers/settings.mjs.map +1 -1
- package/dist/routers/signed-storybook.mjs +53 -0
- package/dist/routers/signed-storybook.mjs.map +1 -0
- package/dist/routers/snapshot-groups.mjs +63 -0
- package/dist/routers/snapshot-groups.mjs.map +1 -0
- package/dist/routers/snapshots.mjs +103 -5
- package/dist/routers/snapshots.mjs.map +1 -1
- package/dist/routers/storybook.mjs +37 -88
- package/dist/routers/storybook.mjs.map +1 -1
- package/dist/routers/tokens.mjs +2 -5
- package/dist/routers/tokens.mjs.map +1 -1
- package/dist/routers/ui.mjs +34 -5
- package/dist/routers/ui.mjs.map +1 -1
- package/dist/routers/webhooks.mjs +5 -11
- package/dist/routers/webhooks.mjs.map +1 -1
- package/dist/routers/well-known.mjs +11 -4
- package/dist/routers/well-known.mjs.map +1 -1
- package/dist/routes/api-routes.mjs +93 -0
- package/dist/routes/api-routes.mjs.map +1 -0
- package/dist/routes/classification.mjs +38 -0
- package/dist/routes/classification.mjs.map +1 -0
- package/dist/routes/openapi-classes.mjs +22 -0
- package/dist/routes/openapi-classes.mjs.map +1 -0
- package/dist/routes/page-routes.mjs +104 -0
- package/dist/routes/page-routes.mjs.map +1 -0
- package/dist/runtime.mjs +18 -13
- package/dist/runtime.mjs.map +1 -1
- package/dist/serverless.mjs +5 -0
- package/dist/store.mjs +19 -1
- package/dist/store.mjs.map +1 -1
- package/dist/targets.mjs +46 -0
- package/dist/targets.mjs.map +1 -0
- package/dist/ui/buttons.mjs +16 -3
- package/dist/ui/buttons.mjs.map +1 -1
- package/dist/ui/design-reference.mjs +97 -0
- package/dist/ui/design-reference.mjs.map +1 -0
- package/dist/ui/document.mjs +1 -2
- package/dist/ui/document.mjs.map +1 -1
- package/dist/ui/forms.mjs +6 -5
- package/dist/ui/forms.mjs.map +1 -1
- package/dist/ui/icons/paths.mjs +1 -0
- package/dist/ui/icons/paths.mjs.map +1 -1
- package/dist/ui/preview-embed.mjs +82 -0
- package/dist/ui/preview-embed.mjs.map +1 -0
- package/dist/ui/shell/client-script.mjs +85 -0
- package/dist/ui/shell/client-script.mjs.map +1 -1
- package/dist/ui/shell/nav.mjs +12 -8
- package/dist/ui/shell/nav.mjs.map +1 -1
- package/dist/ui/shell/review-script.mjs +8 -0
- package/dist/ui/shell/review-script.mjs.map +1 -1
- package/dist/ui/shell/sidebar.mjs +2 -2
- package/dist/ui/shell/sidebar.mjs.map +1 -1
- package/package.json +11 -9
- package/dist/asset-paths.mjs +0 -23
- package/dist/asset-paths.mjs.map +0 -1
- package/dist/assets/mark.svg +0 -1
- package/dist/pages/settings-status.mjs +0 -137
- package/dist/pages/settings-status.mjs.map +0 -1
- package/dist/routers/settings-status.mjs +0 -77
- package/dist/routers/settings-status.mjs.map +0 -1
- package/dist/routers/status-configs.mjs +0 -108
- package/dist/routers/status-configs.mjs.map +0 -1
|
@@ -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"}
|
package/dist/ui/shell/nav.mjs
CHANGED
|
@@ -2,8 +2,17 @@ import "node:path";
|
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
4
|
//#region src/ui/shell/nav.ts
|
|
5
|
-
/**
|
|
6
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Entries shown for the current project. Entries for `testing` routes (Jobs) drop out on a
|
|
7
|
+
* hosting-only instance, where those routes answer 404.
|
|
8
|
+
*/
|
|
9
|
+
function projectNavItems(urls, slug, testing = true) {
|
|
10
|
+
const jobs = {
|
|
11
|
+
key: "jobs",
|
|
12
|
+
label: "Jobs",
|
|
13
|
+
href: urls.jobs(slug),
|
|
14
|
+
icon: "activity"
|
|
15
|
+
};
|
|
7
16
|
return [
|
|
8
17
|
{
|
|
9
18
|
key: "library",
|
|
@@ -17,12 +26,7 @@ function projectNavItems(urls, slug) {
|
|
|
17
26
|
href: urls.buildsList(slug),
|
|
18
27
|
icon: "layers"
|
|
19
28
|
},
|
|
20
|
-
|
|
21
|
-
key: "jobs",
|
|
22
|
-
label: "Jobs",
|
|
23
|
-
href: urls.jobs(slug),
|
|
24
|
-
icon: "activity"
|
|
25
|
-
},
|
|
29
|
+
...testing ? [jobs] : [],
|
|
26
30
|
{
|
|
27
31
|
key: "labels",
|
|
28
32
|
label: "Labels",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nav.mjs","names":[],"sources":["../../../src/ui/shell/nav.ts"],"sourcesContent":["import type { IconName } from \"../icons/paths.ts\";\n\n/** Which nav entry is active and which project (if any) the page belongs to. */\nexport interface NavConfig {\n active?: string;\n projectSlug?: string;\n projectName?: string;\n /** Levels below the section, ending with the current page (e.g. a build, then \"Review\"). */\n trail?: NavCrumb[];\n}\n\n/** One breadcrumb level; the last trail item is the current page (it needs no `href`). */\nexport interface NavCrumb {\n label: string;\n href?: string;\n}\n\n/** One sidebar entry. */\nexport interface NavItem {\n key: string;\n label: string;\n href: string;\n icon: IconName;\n /** Opens in a new tab (raw API output). */\n external?: boolean;\n}\n\n/** Subset of the URL builder the nav needs (keeps this module pure). */\nexport interface NavUrls {\n projects(): string;\n library(slug: string): string;\n buildsList(slug: string): string;\n jobs(slug: string): string;\n labels(slug: string): string;\n settings(slug: string): string;\n}\n\n
|
|
1
|
+
{"version":3,"file":"nav.mjs","names":[],"sources":["../../../src/ui/shell/nav.ts"],"sourcesContent":["import type { IconName } from \"../icons/paths.ts\";\n\n/** Which nav entry is active and which project (if any) the page belongs to. */\nexport interface NavConfig {\n active?: string;\n projectSlug?: string;\n projectName?: string;\n /** Levels below the section, ending with the current page (e.g. a build, then \"Review\"). */\n trail?: NavCrumb[];\n}\n\n/** One breadcrumb level; the last trail item is the current page (it needs no `href`). */\nexport interface NavCrumb {\n label: string;\n href?: string;\n}\n\n/** One sidebar entry. */\nexport interface NavItem {\n key: string;\n label: string;\n href: string;\n icon: IconName;\n /** Opens in a new tab (raw API output). */\n external?: boolean;\n}\n\n/** Subset of the URL builder the nav needs (keeps this module pure). */\nexport interface NavUrls {\n projects(): string;\n library(slug: string): string;\n buildsList(slug: string): string;\n jobs(slug: string): string;\n labels(slug: string): string;\n settings(slug: string): string;\n}\n\n/**\n * Entries shown for the current project. Entries for `testing` routes (Jobs) drop out on a\n * hosting-only instance, where those routes answer 404.\n */\nexport function projectNavItems(urls: NavUrls, slug: string, testing = true): NavItem[] {\n const jobs: NavItem = { key: \"jobs\", label: \"Jobs\", href: urls.jobs(slug), icon: \"activity\" };\n return [\n { key: \"library\", label: \"Library\", href: urls.library(slug), icon: \"image\" },\n { key: \"builds\", label: \"Builds\", href: urls.buildsList(slug), icon: \"layers\" },\n ...(testing ? [jobs] : []),\n { key: \"labels\", label: \"Labels\", href: urls.labels(slug), icon: \"tag\" },\n { key: \"settings\", label: \"Settings\", href: urls.settings(slug), icon: \"settings\" },\n ];\n}\n\n/** Developer entries (API reference). */\nexport function developerNavItems(): NavItem[] {\n return [\n { key: \"api-docs\", label: \"API docs\", href: \"/api/v1/docs\", icon: \"book\" },\n {\n key: \"openapi\",\n label: \"OpenAPI spec\",\n href: \"/api/v1/openapi.json\",\n icon: \"code\",\n external: true,\n },\n ];\n}\n\n/** Administration entries (admins only). */\nexport function adminNavItems(): NavItem[] {\n return [{ key: \"admin\", label: \"System\", href: \"/admin\", icon: \"server\" }];\n}\n\n/**\n * Breadcrumbs after the project picker: the *ancestors* of the current page.\n * The page itself is named by its heading, so it is never repeated here —\n * the section plus `nav.trail` describe where the page is, and the last item\n * (the current page) is dropped. Every returned crumb links somewhere, and\n * the last one is the page's parent.\n */\nexport function breadcrumbs(nav: NavConfig | undefined, urls: NavUrls): NavCrumb[] {\n const section = sectionCrumb(nav, urls);\n const path = section ? [section, ...(nav?.trail ?? [])] : [...(nav?.trail ?? [])];\n return path.slice(0, -1).filter((crumb) => crumb.href !== undefined);\n}\n\nfunction sectionCrumb(nav: NavConfig | undefined, urls: NavUrls): NavCrumb | null {\n const slug = nav?.projectSlug;\n const table: Record<string, NavCrumb | undefined> = {\n library: slug ? { label: \"Library\", href: urls.library(slug) } : undefined,\n builds: slug ? { label: \"Builds\", href: urls.buildsList(slug) } : undefined,\n jobs: slug ? { label: \"Jobs\", href: urls.jobs(slug) } : undefined,\n labels: slug ? { label: \"Labels\", href: urls.labels(slug) } : undefined,\n settings: slug ? { label: \"Settings\", href: urls.settings(slug) } : undefined,\n admin: { label: \"System\", href: \"/admin\" },\n profile: { label: \"Profile\", href: \"/profile\" },\n };\n return table[nav?.active ?? \"\"] ?? null;\n}\n"],"mappings":";;;;;;;;AAyCA,SAAgB,gBAAgB,MAAe,MAAc,UAAU,MAAiB;CACtF,MAAM,OAAgB;EAAE,KAAK;EAAQ,OAAO;EAAQ,MAAM,KAAK,KAAK,IAAI;EAAG,MAAM;CAAW;CAC5F,OAAO;EACL;GAAE,KAAK;GAAW,OAAO;GAAW,MAAM,KAAK,QAAQ,IAAI;GAAG,MAAM;EAAQ;EAC5E;GAAE,KAAK;GAAU,OAAO;GAAU,MAAM,KAAK,WAAW,IAAI;GAAG,MAAM;EAAS;EAC9E,GAAI,UAAU,CAAC,IAAI,IAAI,CAAC;EACxB;GAAE,KAAK;GAAU,OAAO;GAAU,MAAM,KAAK,OAAO,IAAI;GAAG,MAAM;EAAM;EACvE;GAAE,KAAK;GAAY,OAAO;GAAY,MAAM,KAAK,SAAS,IAAI;GAAG,MAAM;EAAW;CACpF;AACF;;AAGA,SAAgB,oBAA+B;CAC7C,OAAO,CACL;EAAE,KAAK;EAAY,OAAO;EAAY,MAAM;EAAgB,MAAM;CAAO,GACzE;EACE,KAAK;EACL,OAAO;EACP,MAAM;EACN,MAAM;EACN,UAAU;CACZ,CACF;AACF;;AAGA,SAAgB,gBAA2B;CACzC,OAAO,CAAC;EAAE,KAAK;EAAS,OAAO;EAAU,MAAM;EAAU,MAAM;CAAS,CAAC;AAC3E;;;;;;;;AASA,SAAgB,YAAY,KAA4B,MAA2B;CACjF,MAAM,UAAU,aAAa,KAAK,IAAI;CAEtC,QADa,UAAU,CAAC,SAAS,GAAI,KAAK,SAAS,CAAC,CAAE,IAAI,CAAC,GAAI,KAAK,SAAS,CAAC,CAAE,EAAA,CACpE,MAAM,GAAG,EAAE,CAAC,CAAC,QAAQ,UAAU,MAAM,SAAS,KAAA,CAAS;AACrE;AAEA,SAAS,aAAa,KAA4B,MAAgC;CAChF,MAAM,OAAO,KAAK;CAUlB,OAAO;EARL,SAAS,OAAO;GAAE,OAAO;GAAW,MAAM,KAAK,QAAQ,IAAI;EAAE,IAAI,KAAA;EACjE,QAAQ,OAAO;GAAE,OAAO;GAAU,MAAM,KAAK,WAAW,IAAI;EAAE,IAAI,KAAA;EAClE,MAAM,OAAO;GAAE,OAAO;GAAQ,MAAM,KAAK,KAAK,IAAI;EAAE,IAAI,KAAA;EACxD,QAAQ,OAAO;GAAE,OAAO;GAAU,MAAM,KAAK,OAAO,IAAI;EAAE,IAAI,KAAA;EAC9D,UAAU,OAAO;GAAE,OAAO;GAAY,MAAM,KAAK,SAAS,IAAI;EAAE,IAAI,KAAA;EACpE,OAAO;GAAE,OAAO;GAAU,MAAM;EAAS;EACzC,SAAS;GAAE,OAAO;GAAW,MAAM;EAAW;CAErC,EAAE,KAAK,UAAU,OAAO;AACrC"}
|
|
@@ -166,7 +166,15 @@ function keyboardShortcuts() {
|
|
|
166
166
|
if(el&&!el.disabled){ el.click(); return true; }
|
|
167
167
|
return false;
|
|
168
168
|
}
|
|
169
|
+
function stepBrowser(dir){
|
|
170
|
+
var tabs=[].slice.call(document.querySelectorAll('[data-browser-tabs] a'));
|
|
171
|
+
var at=tabs.findIndex(function(a){ return a.getAttribute('aria-current')==='page'; });
|
|
172
|
+
var target=at===-1?null:tabs[at+dir];
|
|
173
|
+
if(target){ target.click(); return true; }
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
169
176
|
function handleKey(key,stage){
|
|
177
|
+
if(key==='['||key===']') return stepBrowser(key===']'?1:-1);
|
|
170
178
|
if(key==='j'||key==='ArrowRight') return press1('[data-snap-next]');
|
|
171
179
|
if(key==='k'||key==='ArrowLeft') return press1('[data-snap-prev]');
|
|
172
180
|
if(key==='a'||key==='A') return press1('[data-approve]');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"review-script.mjs","names":[],"sources":["../../../src/ui/shell/review-script.ts"],"sourcesContent":["/**\n * Review workspace behavior: comparison modes, zoom, synchronized scrolling,\n * list filters, keyboard review, and the shortcuts dialog. Runs inside the\n * shell's once-only closure (`inits`, `shell` are in scope).\n */\nexport function reviewScript(): string {\n return [stateHelpers(), stageSetup(), wipeDrag(), clickControls(), keyboardShortcuts()].join(\n \"\\n\",\n );\n}\n\n/** Mode/zoom state on the stage root, remembered across snapshots. */\nfunction stateHelpers(): string {\n return `\n var MODES=['split','swipe','onion','diff','flip'];\n var ZOOMS=['fit','100','200','400'];\n function remember(key,value){ try{ localStorage.setItem(key,value); }catch(_){} }\n function recall(key){ try{ return localStorage.getItem(key); }catch(_){ return null; } }\n function press(group,value){\n if(!group) return;\n group.querySelectorAll('[data-view-value]').forEach(function(b){\n b.setAttribute('aria-pressed', b.getAttribute('data-view-value')===value ? 'true' : 'false');\n });\n }\n function setMode(stage,mode,persist){\n stage.setAttribute('data-view',mode);\n press(stage.querySelector('[data-compare-mode]'),mode);\n if(persist) remember('ss_compare_mode',mode);\n }\n function setZoom(stage,zoom,persist){\n stage.setAttribute('data-zoom',zoom);\n press(stage.querySelector('[data-compare-zoom]'),zoom);\n if(persist) remember('ss_compare_zoom',zoom);\n }\n function flip(stage){\n stage.setAttribute('data-flip', stage.getAttribute('data-flip')==='baseline' ? 'current' : 'baseline');\n }\n function nextZoom(stage){\n var cur=stage.getAttribute('data-zoom')||'fit';\n setZoom(stage,ZOOMS[(ZOOMS.indexOf(cur)+1)%ZOOMS.length],true);\n }\n function stepZoom(stage,delta){\n var cur=ZOOMS.indexOf(stage.getAttribute('data-zoom')||'fit');\n var next=Math.max(0,Math.min(ZOOMS.length-1,cur+delta));\n setZoom(stage,ZOOMS[next],true);\n }`;\n}\n\n/** Per-render wiring of a freshly swapped-in comparison stage. */\nfunction stageSetup(): string {\n return `\n function syncScroll(stage){\n var panes=Array.from(stage.querySelectorAll('.stage__pane'));\n var lock=false;\n panes.forEach(function(pane){\n pane.addEventListener('scroll',function(){\n if(lock) return;\n lock=true;\n panes.forEach(function(other){\n if(other!==pane){ other.scrollTop=pane.scrollTop; other.scrollLeft=pane.scrollLeft; }\n });\n requestAnimationFrame(function(){ lock=false; });\n });\n });\n }\n function bindRange(stage,selector,prop,scale){\n var input=stage.querySelector(selector);\n if(!input) return;\n input.addEventListener('input',function(){ stage.style.setProperty(prop,String(input.value*scale)+(scale===1?'%':'')); });\n }\n function initStage(stage){\n if(stage.getAttribute('data-ready')) return;\n stage.setAttribute('data-ready','1');\n var mode=recall('ss_compare_mode');\n if(MODES.indexOf(mode)>-1) setMode(stage,mode,false);\n var zoom=recall('ss_compare_zoom');\n if(ZOOMS.indexOf(zoom)>-1) setZoom(stage,zoom,false);\n bindRange(stage,'[data-compare-swipe]','--swipe',1);\n bindRange(stage,'[data-compare-onion]','--onion',0.01);\n var grid=stage.querySelector('.stage__grid');\n if(grid){ grid.addEventListener('click',function(){ if(stage.getAttribute('data-view')==='flip'){ flip(stage); } }); }\n syncScroll(stage);\n }\n inits.push(function(){ document.querySelectorAll('[data-compare]').forEach(initStage); });`;\n}\n\n/**\n * Drag the wipe handle to move the swipe divider. Uses pointer capture so the\n * drag keeps working when the pointer leaves the handle, and mirrors the value\n * into the (accessible) range slider.\n */\nfunction wipeDrag(): string {\n return `\n function wipePercent(stage,clientX){\n var pane=stage.querySelector('.stage__pane[data-pane=\"current\"]');\n if(!pane) return 50;\n var rect=pane.getBoundingClientRect();\n return Math.max(0,Math.min(100,(clientX-rect.left)/rect.width*100));\n }\n function setWipe(stage,percent){\n stage.style.setProperty('--swipe',percent+'%');\n var range=stage.querySelector('[data-compare-swipe]');\n if(range){ range.value=String(Math.round(percent)); }\n }\n function bindKnob(stage){\n var knob=stage.querySelector('[data-compare-knob]');\n if(!knob||knob.getAttribute('data-bound')) return;\n knob.setAttribute('data-bound','1');\n knob.addEventListener('pointerdown',function(e){\n e.preventDefault();\n knob.setPointerCapture(e.pointerId);\n setWipe(stage,wipePercent(stage,e.clientX));\n function move(ev){ setWipe(stage,wipePercent(stage,ev.clientX)); }\n function stop(){\n knob.removeEventListener('pointermove',move);\n knob.removeEventListener('pointerup',stop);\n knob.removeEventListener('pointercancel',stop);\n }\n knob.addEventListener('pointermove',move);\n knob.addEventListener('pointerup',stop);\n knob.addEventListener('pointercancel',stop);\n });\n }\n inits.push(function(){ document.querySelectorAll('[data-compare]').forEach(bindKnob); });`;\n}\n\n/** Delegated clicks: mode/zoom/filter segments and the shortcuts button. */\nfunction clickControls(): string {\n return `\n function openShortcuts(){\n var dialog=document.querySelector('dialog[data-shortcuts]');\n if(dialog&&typeof dialog.showModal==='function'&&!dialog.open){ dialog.showModal(); }\n }\n function chooseSegment(btn){\n var value=btn.getAttribute('data-view-value');\n var stage=btn.closest('[data-compare]');\n if(stage&&btn.closest('[data-compare-mode]')){ setMode(stage,value,true); }\n else if(stage&&btn.closest('[data-compare-zoom]')){ setZoom(stage,value,true); }\n else if(btn.closest('[data-filter-switch]')){\n var list=document.querySelector('[data-diff-nav]');\n if(list){ list.setAttribute('data-filter',value); }\n press(btn.closest('[data-filter-switch]'),value);\n }\n }\n document.addEventListener('click',function(e){\n var t=e.target instanceof Element ? e.target : null;\n if(!t) return;\n var btn=t.closest('[data-view-value]');\n if(btn){ chooseSegment(btn); }\n if(t.closest('[data-shortcuts-open]')){ openShortcuts(); }\n });`;\n}\n\n/** Global review shortcuts; ignored while typing or with modifier keys held. */\nfunction keyboardShortcuts(): string {\n return `\n function typing(target){\n return target instanceof HTMLElement &&\n (target.tagName==='INPUT'||target.tagName==='TEXTAREA'||target.tagName==='SELECT'||target.isContentEditable);\n }\n function press1(selector){\n var el=document.querySelector(selector);\n if(el&&!el.disabled){ el.click(); return true; }\n return false;\n }\n function handleKey(key,stage){\n if(key==='j'||key==='ArrowRight') return press1('[data-snap-next]');\n if(key==='k'||key==='ArrowLeft') return press1('[data-snap-prev]');\n if(key==='a'||key==='A') return press1('[data-approve]');\n if(key==='r'||key==='R') return press1('[data-reject]');\n if(key>='1'&&key<='5'){ setMode(stage,MODES[Number(key)-1],true); return true; }\n if(key==='f'){ nextZoom(stage); return true; }\n if(key==='+'||key==='='){ stepZoom(stage,1); return true; }\n if(key==='-'||key==='_'){ stepZoom(stage,-1); return true; }\n if(key==='t'){\n if(stage.getAttribute('data-view')==='flip'){ flip(stage); } else { setMode(stage,'flip',true); }\n return true;\n }\n if(key==='?'){ openShortcuts(); return true; }\n return false;\n }\n document.addEventListener('keydown',function(e){\n if(e.metaKey||e.ctrlKey||e.altKey||typing(e.target)) return;\n var stage=document.querySelector('[data-compare]');\n if(!stage||document.querySelector('dialog[open]')) return;\n if(handleKey(e.key,stage)){ e.preventDefault(); }\n });`;\n}\n"],"mappings":";;;;;;;;;AAKA,SAAgB,eAAuB;CACrC,OAAO;EAAC,aAAa;EAAG,WAAW;EAAG,SAAS;EAAG,cAAc;EAAG,kBAAkB;CAAC,CAAC,CAAC,KACtF,IACF;AACF;;AAGA,SAAS,eAAuB;CAC9B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCT;;AAGA,SAAS,aAAqB;CAC5B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCT;;;;;;AAOA,SAAS,WAAmB;CAC1B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCT;;AAGA,SAAS,gBAAwB;CAC/B,OAAO;;;;;;;;;;;;;;;;;;;;;;;AAuBT;;AAGA,SAAS,oBAA4B;CACnC,OAAO
|
|
1
|
+
{"version":3,"file":"review-script.mjs","names":[],"sources":["../../../src/ui/shell/review-script.ts"],"sourcesContent":["/**\n * Review workspace behavior: comparison modes, zoom, synchronized scrolling,\n * list filters, keyboard review, and the shortcuts dialog. Runs inside the\n * shell's once-only closure (`inits`, `shell` are in scope).\n */\nexport function reviewScript(): string {\n return [stateHelpers(), stageSetup(), wipeDrag(), clickControls(), keyboardShortcuts()].join(\n \"\\n\",\n );\n}\n\n/** Mode/zoom state on the stage root, remembered across snapshots. */\nfunction stateHelpers(): string {\n return `\n var MODES=['split','swipe','onion','diff','flip'];\n var ZOOMS=['fit','100','200','400'];\n function remember(key,value){ try{ localStorage.setItem(key,value); }catch(_){} }\n function recall(key){ try{ return localStorage.getItem(key); }catch(_){ return null; } }\n function press(group,value){\n if(!group) return;\n group.querySelectorAll('[data-view-value]').forEach(function(b){\n b.setAttribute('aria-pressed', b.getAttribute('data-view-value')===value ? 'true' : 'false');\n });\n }\n function setMode(stage,mode,persist){\n stage.setAttribute('data-view',mode);\n press(stage.querySelector('[data-compare-mode]'),mode);\n if(persist) remember('ss_compare_mode',mode);\n }\n function setZoom(stage,zoom,persist){\n stage.setAttribute('data-zoom',zoom);\n press(stage.querySelector('[data-compare-zoom]'),zoom);\n if(persist) remember('ss_compare_zoom',zoom);\n }\n function flip(stage){\n stage.setAttribute('data-flip', stage.getAttribute('data-flip')==='baseline' ? 'current' : 'baseline');\n }\n function nextZoom(stage){\n var cur=stage.getAttribute('data-zoom')||'fit';\n setZoom(stage,ZOOMS[(ZOOMS.indexOf(cur)+1)%ZOOMS.length],true);\n }\n function stepZoom(stage,delta){\n var cur=ZOOMS.indexOf(stage.getAttribute('data-zoom')||'fit');\n var next=Math.max(0,Math.min(ZOOMS.length-1,cur+delta));\n setZoom(stage,ZOOMS[next],true);\n }`;\n}\n\n/** Per-render wiring of a freshly swapped-in comparison stage. */\nfunction stageSetup(): string {\n return `\n function syncScroll(stage){\n var panes=Array.from(stage.querySelectorAll('.stage__pane'));\n var lock=false;\n panes.forEach(function(pane){\n pane.addEventListener('scroll',function(){\n if(lock) return;\n lock=true;\n panes.forEach(function(other){\n if(other!==pane){ other.scrollTop=pane.scrollTop; other.scrollLeft=pane.scrollLeft; }\n });\n requestAnimationFrame(function(){ lock=false; });\n });\n });\n }\n function bindRange(stage,selector,prop,scale){\n var input=stage.querySelector(selector);\n if(!input) return;\n input.addEventListener('input',function(){ stage.style.setProperty(prop,String(input.value*scale)+(scale===1?'%':'')); });\n }\n function initStage(stage){\n if(stage.getAttribute('data-ready')) return;\n stage.setAttribute('data-ready','1');\n var mode=recall('ss_compare_mode');\n if(MODES.indexOf(mode)>-1) setMode(stage,mode,false);\n var zoom=recall('ss_compare_zoom');\n if(ZOOMS.indexOf(zoom)>-1) setZoom(stage,zoom,false);\n bindRange(stage,'[data-compare-swipe]','--swipe',1);\n bindRange(stage,'[data-compare-onion]','--onion',0.01);\n var grid=stage.querySelector('.stage__grid');\n if(grid){ grid.addEventListener('click',function(){ if(stage.getAttribute('data-view')==='flip'){ flip(stage); } }); }\n syncScroll(stage);\n }\n inits.push(function(){ document.querySelectorAll('[data-compare]').forEach(initStage); });`;\n}\n\n/**\n * Drag the wipe handle to move the swipe divider. Uses pointer capture so the\n * drag keeps working when the pointer leaves the handle, and mirrors the value\n * into the (accessible) range slider.\n */\nfunction wipeDrag(): string {\n return `\n function wipePercent(stage,clientX){\n var pane=stage.querySelector('.stage__pane[data-pane=\"current\"]');\n if(!pane) return 50;\n var rect=pane.getBoundingClientRect();\n return Math.max(0,Math.min(100,(clientX-rect.left)/rect.width*100));\n }\n function setWipe(stage,percent){\n stage.style.setProperty('--swipe',percent+'%');\n var range=stage.querySelector('[data-compare-swipe]');\n if(range){ range.value=String(Math.round(percent)); }\n }\n function bindKnob(stage){\n var knob=stage.querySelector('[data-compare-knob]');\n if(!knob||knob.getAttribute('data-bound')) return;\n knob.setAttribute('data-bound','1');\n knob.addEventListener('pointerdown',function(e){\n e.preventDefault();\n knob.setPointerCapture(e.pointerId);\n setWipe(stage,wipePercent(stage,e.clientX));\n function move(ev){ setWipe(stage,wipePercent(stage,ev.clientX)); }\n function stop(){\n knob.removeEventListener('pointermove',move);\n knob.removeEventListener('pointerup',stop);\n knob.removeEventListener('pointercancel',stop);\n }\n knob.addEventListener('pointermove',move);\n knob.addEventListener('pointerup',stop);\n knob.addEventListener('pointercancel',stop);\n });\n }\n inits.push(function(){ document.querySelectorAll('[data-compare]').forEach(bindKnob); });`;\n}\n\n/** Delegated clicks: mode/zoom/filter segments and the shortcuts button. */\nfunction clickControls(): string {\n return `\n function openShortcuts(){\n var dialog=document.querySelector('dialog[data-shortcuts]');\n if(dialog&&typeof dialog.showModal==='function'&&!dialog.open){ dialog.showModal(); }\n }\n function chooseSegment(btn){\n var value=btn.getAttribute('data-view-value');\n var stage=btn.closest('[data-compare]');\n if(stage&&btn.closest('[data-compare-mode]')){ setMode(stage,value,true); }\n else if(stage&&btn.closest('[data-compare-zoom]')){ setZoom(stage,value,true); }\n else if(btn.closest('[data-filter-switch]')){\n var list=document.querySelector('[data-diff-nav]');\n if(list){ list.setAttribute('data-filter',value); }\n press(btn.closest('[data-filter-switch]'),value);\n }\n }\n document.addEventListener('click',function(e){\n var t=e.target instanceof Element ? e.target : null;\n if(!t) return;\n var btn=t.closest('[data-view-value]');\n if(btn){ chooseSegment(btn); }\n if(t.closest('[data-shortcuts-open]')){ openShortcuts(); }\n });`;\n}\n\n/** Global review shortcuts; ignored while typing or with modifier keys held. */\nfunction keyboardShortcuts(): string {\n return `\n function typing(target){\n return target instanceof HTMLElement &&\n (target.tagName==='INPUT'||target.tagName==='TEXTAREA'||target.tagName==='SELECT'||target.isContentEditable);\n }\n function press1(selector){\n var el=document.querySelector(selector);\n if(el&&!el.disabled){ el.click(); return true; }\n return false;\n }\n function stepBrowser(dir){\n var tabs=[].slice.call(document.querySelectorAll('[data-browser-tabs] a'));\n var at=tabs.findIndex(function(a){ return a.getAttribute('aria-current')==='page'; });\n var target=at===-1?null:tabs[at+dir];\n if(target){ target.click(); return true; }\n return false;\n }\n function handleKey(key,stage){\n if(key==='['||key===']') return stepBrowser(key===']'?1:-1);\n if(key==='j'||key==='ArrowRight') return press1('[data-snap-next]');\n if(key==='k'||key==='ArrowLeft') return press1('[data-snap-prev]');\n if(key==='a'||key==='A') return press1('[data-approve]');\n if(key==='r'||key==='R') return press1('[data-reject]');\n if(key>='1'&&key<='5'){ setMode(stage,MODES[Number(key)-1],true); return true; }\n if(key==='f'){ nextZoom(stage); return true; }\n if(key==='+'||key==='='){ stepZoom(stage,1); return true; }\n if(key==='-'||key==='_'){ stepZoom(stage,-1); return true; }\n if(key==='t'){\n if(stage.getAttribute('data-view')==='flip'){ flip(stage); } else { setMode(stage,'flip',true); }\n return true;\n }\n if(key==='?'){ openShortcuts(); return true; }\n return false;\n }\n document.addEventListener('keydown',function(e){\n if(e.metaKey||e.ctrlKey||e.altKey||typing(e.target)) return;\n var stage=document.querySelector('[data-compare]');\n if(!stage||document.querySelector('dialog[open]')) return;\n if(handleKey(e.key,stage)){ e.preventDefault(); }\n });`;\n}\n"],"mappings":";;;;;;;;;AAKA,SAAgB,eAAuB;CACrC,OAAO;EAAC,aAAa;EAAG,WAAW;EAAG,SAAS;EAAG,cAAc;EAAG,kBAAkB;CAAC,CAAC,CAAC,KACtF,IACF;AACF;;AAGA,SAAS,eAAuB;CAC9B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCT;;AAGA,SAAS,aAAqB;CAC5B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCT;;;;;;AAOA,SAAS,WAAmB;CAC1B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCT;;AAGA,SAAS,gBAAwB;CAC/B,OAAO;;;;;;;;;;;;;;;;;;;;;;;AAuBT;;AAGA,SAAS,oBAA4B;CACnC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCT"}
|
|
@@ -253,7 +253,7 @@ const Brand = ({ name, logo, href }) => {
|
|
|
253
253
|
};
|
|
254
254
|
/** Primary navigation: brand, workspace, project, developer, and admin entries. */
|
|
255
255
|
const Sidebar = ({ nav, name, logo }) => {
|
|
256
|
-
const { user, authEnabled, config } = getStore();
|
|
256
|
+
const { user, authEnabled, config, testing } = getStore();
|
|
257
257
|
const urls = createUrlBuilder("/", config.publishedBaseDomain);
|
|
258
258
|
const showAdmin = !authEnabled || user?.role === "admin";
|
|
259
259
|
const projects = {
|
|
@@ -282,7 +282,7 @@ const Sidebar = ({ nav, name, logo }) => {
|
|
|
282
282
|
}),
|
|
283
283
|
nav?.projectSlug ? /* @__PURE__ */ jsx(NavSection, {
|
|
284
284
|
title: "Project",
|
|
285
|
-
items: projectNavItems(urls, nav.projectSlug),
|
|
285
|
+
items: projectNavItems(urls, nav.projectSlug, testing),
|
|
286
286
|
active: nav.active
|
|
287
287
|
}) : null,
|
|
288
288
|
/* @__PURE__ */ jsx(NavSection, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sidebar.mjs","names":[],"sources":["../../../src/ui/shell/sidebar.tsx"],"sourcesContent":["import { createUrlBuilder, safeImageUrl } from \"@storyshelf/core/urls\";\nimport type { FC } from \"hono/jsx\";\nimport { getStore } from \"../../store.ts\";\nimport { css } from \"../css.ts\";\nimport { Icon } from \"../icons/icon.tsx\";\nimport { ThemeMenu, UserMenu } from \"./account.tsx\";\nimport {\n adminNavItems,\n developerNavItems,\n projectNavItems,\n type NavConfig,\n type NavItem,\n} from \"./nav.ts\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\n/**\n * Left rail. Collapses to icons only on desktop (`html[data-sidebar=\"rail\"]`)\n * and becomes an off-canvas drawer on small screens.\n */\nconst shellSidebar = css`\n /* shell-sidebar */\n position: sticky;\n top: 0;\n height: 100vh;\n display: flex;\n flex-direction: column;\n gap: var(--space-2);\n padding: var(--space-3) var(--space-2);\n background: var(--sidebar-bg);\n border-right: 1px solid var(--border);\n .sidebar__brand {\n display: flex;\n align-items: center;\n gap: 0.6rem;\n min-height: 40px;\n padding: 0 var(--space-2);\n color: var(--text-primary);\n font-weight: 650;\n font-size: var(--text-lg);\n letter-spacing: -0.01em;\n text-decoration: none;\n }\n .sidebar__brand:hover {\n text-decoration: none;\n }\n .sidebar__mark {\n flex: none;\n display: inline-grid;\n place-items: center;\n width: 28px;\n height: 28px;\n border-radius: var(--radius);\n background: var(--accent);\n color: var(--accent-contrast);\n }\n .sidebar__logo {\n flex: none;\n border-radius: var(--radius-sm);\n object-fit: contain;\n }\n .sidebar__nav {\n display: flex;\n flex-direction: column;\n gap: 2px;\n flex: 1;\n min-height: 0;\n overflow-x: hidden;\n overflow-y: auto;\n }\n .sidebar__section {\n margin: var(--space-4) 0 var(--space-1);\n padding: 0 var(--space-3);\n color: var(--text-secondary);\n font-size: 0.68rem;\n font-weight: 600;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n white-space: nowrap;\n }\n .sidebar__link {\n display: flex;\n align-items: center;\n gap: 0.65rem;\n min-height: 38px;\n padding: 0.4rem var(--space-3);\n border-radius: var(--radius);\n color: var(--text-secondary);\n font-size: var(--text-base);\n font-weight: 500;\n text-decoration: none;\n white-space: nowrap;\n transition:\n background var(--dur-fast) var(--ease),\n color var(--dur-fast) var(--ease);\n }\n .sidebar__link:hover {\n background: var(--surface-muted);\n color: var(--text-primary);\n text-decoration: none;\n }\n .sidebar__link--active {\n background: var(--accent-subtle);\n color: var(--accent-fg);\n font-weight: 600;\n }\n .sidebar__link--active:hover {\n background: var(--accent-subtle);\n color: var(--accent-fg);\n }\n .sidebar__link--active svg {\n color: var(--accent);\n }\n .sidebar__ext {\n margin-left: auto;\n opacity: 0.6;\n }\n .sidebar__footer {\n display: grid;\n gap: var(--space-1);\n padding-top: var(--space-2);\n border-top: 1px solid var(--border-subtle);\n }\n .sidebar__tools {\n display: flex;\n align-items: center;\n gap: var(--space-1);\n }\n .sidebar__tools > details {\n flex: none;\n width: auto;\n }\n .sidebar__tools .sidebar__collapse {\n flex: 1;\n width: auto;\n }\n .sidebar__collapse {\n appearance: none;\n display: flex;\n align-items: center;\n gap: 0.65rem;\n min-height: 36px;\n padding: 0.4rem var(--space-3);\n border: 0;\n border-radius: var(--radius);\n background: transparent;\n color: var(--text-secondary);\n font: inherit;\n font-size: var(--text-sm);\n cursor: pointer;\n }\n .sidebar__collapse:hover {\n background: var(--surface-muted);\n color: var(--text-primary);\n }\n @media (min-width: 881px) {\n html[data-sidebar=\"rail\"] & .sidebar__label,\n html[data-sidebar=\"rail\"] & .sidebar__section,\n html[data-sidebar=\"rail\"] & .sidebar__ext {\n display: none;\n }\n html[data-sidebar=\"rail\"] & .sidebar__link,\n html[data-sidebar=\"rail\"] & .sidebar__collapse,\n html[data-sidebar=\"rail\"] & .sidebar__brand {\n justify-content: center;\n padding-inline: 0;\n }\n html[data-sidebar=\"rail\"] & .sidebar__collapse svg {\n transform: rotate(180deg);\n }\n html[data-sidebar=\"rail\"] & .sidebar__footer summary {\n justify-content: center;\n }\n html[data-sidebar=\"rail\"] & .sidebar__footer summary > svg:last-child {\n display: none;\n }\n html[data-sidebar=\"rail\"] & .sidebar__tools {\n flex-direction: column;\n }\n }\n @media (max-width: 880px) {\n & {\n position: fixed;\n inset: 0 auto 0 0;\n z-index: 60;\n width: min(280px, 85vw);\n height: 100dvh;\n transform: translateX(-100%);\n transition: transform var(--dur-base) var(--ease);\n box-shadow: var(--shadow-3);\n }\n &.sidebar--open {\n transform: translateX(0);\n }\n & .sidebar__collapse {\n display: none;\n }\n }\n`;\n\nconst NavLink: FC<{ item: NavItem; active?: string }> = ({ item, active }) => {\n const current = active === item.key;\n return (\n <a\n class={`sidebar__link${current ? \" sidebar__link--active\" : \"\"}`}\n href={item.href}\n title={item.label}\n aria-current={current ? \"page\" : undefined}\n target={item.external ? \"_blank\" : undefined}\n rel={item.external ? \"noopener\" : undefined}\n >\n <Icon name={item.icon} />\n <span class=\"sidebar__label\">{item.label}</span>\n {item.external ? (\n <span class=\"sidebar__ext\">\n <Icon name=\"external-link\" size=\"sm\" />\n </span>\n ) : null}\n </a>\n );\n};\n\nconst NavSection: FC<{ title: string; items: NavItem[]; active?: string }> = ({\n title,\n items,\n active,\n}) => {\n return (\n <>\n <div class=\"sidebar__section\">{title}</div>\n {items.map((item) => (\n <NavLink key={item.key} item={item} active={active} />\n ))}\n </>\n );\n};\n\nconst Brand: FC<{ name: string; logo?: string; href: string }> = ({ name, logo, href }) => {\n const safeLogo = safeImageUrl(logo);\n return (\n <a class=\"sidebar__brand\" href={href} title={name}>\n {safeLogo ? (\n <img class=\"sidebar__logo\" src={safeLogo} alt=\"\" width=\"28\" height=\"28\" />\n ) : (\n <span class=\"sidebar__mark\" aria-hidden=\"true\">\n <Icon name=\"layers\" size=\"sm\" />\n </span>\n )}\n <span class=\"sidebar__label\">{name}</span>\n </a>\n );\n};\n\n/** Primary navigation: brand, workspace, project, developer, and admin entries. */\nexport const Sidebar: FC<{ nav?: NavConfig; name: string; logo?: string }> = ({\n nav,\n name,\n logo,\n}) => {\n const { user, authEnabled, config } = getStore();\n const urls = createUrlBuilder(\"/\", config.publishedBaseDomain);\n const showAdmin = !authEnabled || user?.role === \"admin\";\n const projects: NavItem = {\n key: \"projects\",\n label: \"Projects\",\n href: urls.projects(),\n icon: \"folder\",\n };\n return (\n <aside id=\"sidebar\" class={shellSidebar} aria-label=\"Primary\" data-sidebar>\n <Brand name={name} logo={logo} href={urls.projects()} />\n <nav class=\"sidebar__nav\">\n <NavLink item={projects} active={nav?.active} />\n {nav?.projectSlug ? (\n <NavSection\n title=\"Project\"\n items={projectNavItems(urls, nav.projectSlug)}\n active={nav.active}\n />\n ) : null}\n <NavSection title=\"Developer\" items={developerNavItems()} active={nav?.active} />\n {showAdmin ? (\n <NavSection title=\"Administration\" items={adminNavItems()} active={nav?.active} />\n ) : null}\n </nav>\n <div class=\"sidebar__footer\">\n <UserMenu />\n <div class=\"sidebar__tools\">\n <ThemeMenu />\n <button\n class=\"sidebar__collapse\"\n type=\"button\"\n data-sidebar-collapse\n aria-label=\"Collapse or expand sidebar\"\n title=\"Collapse or expand sidebar\"\n >\n <Icon name=\"chevrons-left\" />\n <span class=\"sidebar__label\">Collapse</span>\n </button>\n </div>\n </div>\n </aside>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"sidebar.mjs","names":[],"sources":["../../../src/ui/shell/sidebar.tsx"],"sourcesContent":["import { createUrlBuilder, safeImageUrl } from \"@storyshelf/core/urls\";\nimport type { FC } from \"hono/jsx\";\nimport { getStore } from \"../../store.ts\";\nimport { css } from \"../css.ts\";\nimport { Icon } from \"../icons/icon.tsx\";\nimport { ThemeMenu, UserMenu } from \"./account.tsx\";\nimport {\n adminNavItems,\n developerNavItems,\n projectNavItems,\n type NavConfig,\n type NavItem,\n} from \"./nav.ts\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\n/**\n * Left rail. Collapses to icons only on desktop (`html[data-sidebar=\"rail\"]`)\n * and becomes an off-canvas drawer on small screens.\n */\nconst shellSidebar = css`\n /* shell-sidebar */\n position: sticky;\n top: 0;\n height: 100vh;\n display: flex;\n flex-direction: column;\n gap: var(--space-2);\n padding: var(--space-3) var(--space-2);\n background: var(--sidebar-bg);\n border-right: 1px solid var(--border);\n .sidebar__brand {\n display: flex;\n align-items: center;\n gap: 0.6rem;\n min-height: 40px;\n padding: 0 var(--space-2);\n color: var(--text-primary);\n font-weight: 650;\n font-size: var(--text-lg);\n letter-spacing: -0.01em;\n text-decoration: none;\n }\n .sidebar__brand:hover {\n text-decoration: none;\n }\n .sidebar__mark {\n flex: none;\n display: inline-grid;\n place-items: center;\n width: 28px;\n height: 28px;\n border-radius: var(--radius);\n background: var(--accent);\n color: var(--accent-contrast);\n }\n .sidebar__logo {\n flex: none;\n border-radius: var(--radius-sm);\n object-fit: contain;\n }\n .sidebar__nav {\n display: flex;\n flex-direction: column;\n gap: 2px;\n flex: 1;\n min-height: 0;\n overflow-x: hidden;\n overflow-y: auto;\n }\n .sidebar__section {\n margin: var(--space-4) 0 var(--space-1);\n padding: 0 var(--space-3);\n color: var(--text-secondary);\n font-size: 0.68rem;\n font-weight: 600;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n white-space: nowrap;\n }\n .sidebar__link {\n display: flex;\n align-items: center;\n gap: 0.65rem;\n min-height: 38px;\n padding: 0.4rem var(--space-3);\n border-radius: var(--radius);\n color: var(--text-secondary);\n font-size: var(--text-base);\n font-weight: 500;\n text-decoration: none;\n white-space: nowrap;\n transition:\n background var(--dur-fast) var(--ease),\n color var(--dur-fast) var(--ease);\n }\n .sidebar__link:hover {\n background: var(--surface-muted);\n color: var(--text-primary);\n text-decoration: none;\n }\n .sidebar__link--active {\n background: var(--accent-subtle);\n color: var(--accent-fg);\n font-weight: 600;\n }\n .sidebar__link--active:hover {\n background: var(--accent-subtle);\n color: var(--accent-fg);\n }\n .sidebar__link--active svg {\n color: var(--accent);\n }\n .sidebar__ext {\n margin-left: auto;\n opacity: 0.6;\n }\n .sidebar__footer {\n display: grid;\n gap: var(--space-1);\n padding-top: var(--space-2);\n border-top: 1px solid var(--border-subtle);\n }\n .sidebar__tools {\n display: flex;\n align-items: center;\n gap: var(--space-1);\n }\n .sidebar__tools > details {\n flex: none;\n width: auto;\n }\n .sidebar__tools .sidebar__collapse {\n flex: 1;\n width: auto;\n }\n .sidebar__collapse {\n appearance: none;\n display: flex;\n align-items: center;\n gap: 0.65rem;\n min-height: 36px;\n padding: 0.4rem var(--space-3);\n border: 0;\n border-radius: var(--radius);\n background: transparent;\n color: var(--text-secondary);\n font: inherit;\n font-size: var(--text-sm);\n cursor: pointer;\n }\n .sidebar__collapse:hover {\n background: var(--surface-muted);\n color: var(--text-primary);\n }\n @media (min-width: 881px) {\n html[data-sidebar=\"rail\"] & .sidebar__label,\n html[data-sidebar=\"rail\"] & .sidebar__section,\n html[data-sidebar=\"rail\"] & .sidebar__ext {\n display: none;\n }\n html[data-sidebar=\"rail\"] & .sidebar__link,\n html[data-sidebar=\"rail\"] & .sidebar__collapse,\n html[data-sidebar=\"rail\"] & .sidebar__brand {\n justify-content: center;\n padding-inline: 0;\n }\n html[data-sidebar=\"rail\"] & .sidebar__collapse svg {\n transform: rotate(180deg);\n }\n html[data-sidebar=\"rail\"] & .sidebar__footer summary {\n justify-content: center;\n }\n html[data-sidebar=\"rail\"] & .sidebar__footer summary > svg:last-child {\n display: none;\n }\n html[data-sidebar=\"rail\"] & .sidebar__tools {\n flex-direction: column;\n }\n }\n @media (max-width: 880px) {\n & {\n position: fixed;\n inset: 0 auto 0 0;\n z-index: 60;\n width: min(280px, 85vw);\n height: 100dvh;\n transform: translateX(-100%);\n transition: transform var(--dur-base) var(--ease);\n box-shadow: var(--shadow-3);\n }\n &.sidebar--open {\n transform: translateX(0);\n }\n & .sidebar__collapse {\n display: none;\n }\n }\n`;\n\nconst NavLink: FC<{ item: NavItem; active?: string }> = ({ item, active }) => {\n const current = active === item.key;\n return (\n <a\n class={`sidebar__link${current ? \" sidebar__link--active\" : \"\"}`}\n href={item.href}\n title={item.label}\n aria-current={current ? \"page\" : undefined}\n target={item.external ? \"_blank\" : undefined}\n rel={item.external ? \"noopener\" : undefined}\n >\n <Icon name={item.icon} />\n <span class=\"sidebar__label\">{item.label}</span>\n {item.external ? (\n <span class=\"sidebar__ext\">\n <Icon name=\"external-link\" size=\"sm\" />\n </span>\n ) : null}\n </a>\n );\n};\n\nconst NavSection: FC<{ title: string; items: NavItem[]; active?: string }> = ({\n title,\n items,\n active,\n}) => {\n return (\n <>\n <div class=\"sidebar__section\">{title}</div>\n {items.map((item) => (\n <NavLink key={item.key} item={item} active={active} />\n ))}\n </>\n );\n};\n\nconst Brand: FC<{ name: string; logo?: string; href: string }> = ({ name, logo, href }) => {\n const safeLogo = safeImageUrl(logo);\n return (\n <a class=\"sidebar__brand\" href={href} title={name}>\n {safeLogo ? (\n <img class=\"sidebar__logo\" src={safeLogo} alt=\"\" width=\"28\" height=\"28\" />\n ) : (\n <span class=\"sidebar__mark\" aria-hidden=\"true\">\n <Icon name=\"layers\" size=\"sm\" />\n </span>\n )}\n <span class=\"sidebar__label\">{name}</span>\n </a>\n );\n};\n\n/** Primary navigation: brand, workspace, project, developer, and admin entries. */\nexport const Sidebar: FC<{ nav?: NavConfig; name: string; logo?: string }> = ({\n nav,\n name,\n logo,\n}) => {\n const { user, authEnabled, config, testing } = getStore();\n const urls = createUrlBuilder(\"/\", config.publishedBaseDomain);\n const showAdmin = !authEnabled || user?.role === \"admin\";\n const projects: NavItem = {\n key: \"projects\",\n label: \"Projects\",\n href: urls.projects(),\n icon: \"folder\",\n };\n return (\n <aside id=\"sidebar\" class={shellSidebar} aria-label=\"Primary\" data-sidebar>\n <Brand name={name} logo={logo} href={urls.projects()} />\n <nav class=\"sidebar__nav\">\n <NavLink item={projects} active={nav?.active} />\n {nav?.projectSlug ? (\n <NavSection\n title=\"Project\"\n items={projectNavItems(urls, nav.projectSlug, testing)}\n active={nav.active}\n />\n ) : null}\n <NavSection title=\"Developer\" items={developerNavItems()} active={nav?.active} />\n {showAdmin ? (\n <NavSection title=\"Administration\" items={adminNavItems()} active={nav?.active} />\n ) : null}\n </nav>\n <div class=\"sidebar__footer\">\n <UserMenu />\n <div class=\"sidebar__tools\">\n <ThemeMenu />\n <button\n class=\"sidebar__collapse\"\n type=\"button\"\n data-sidebar-collapse\n aria-label=\"Collapse or expand sidebar\"\n title=\"Collapse or expand sidebar\"\n >\n <Icon name=\"chevrons-left\" />\n <span class=\"sidebar__label\">Collapse</span>\n </button>\n </div>\n </div>\n </aside>\n );\n};\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@storyshelf/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "StoryShelf app: Hono app, API routes, and server-rendered UI over @storyshelf/core.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"chromatic-alternative",
|
|
@@ -27,9 +27,6 @@
|
|
|
27
27
|
"dist"
|
|
28
28
|
],
|
|
29
29
|
"type": "module",
|
|
30
|
-
"engines": {
|
|
31
|
-
"node": ">=24"
|
|
32
|
-
},
|
|
33
30
|
"sideEffects": false,
|
|
34
31
|
"types": "./dist/index.d.mts",
|
|
35
32
|
"exports": {
|
|
@@ -55,13 +52,14 @@
|
|
|
55
52
|
"prepublishOnly": "nub run build",
|
|
56
53
|
"test": "vitest run",
|
|
57
54
|
"test:coverage": "vitest run --coverage",
|
|
58
|
-
"test:watch": "vitest"
|
|
55
|
+
"test:watch": "vitest",
|
|
56
|
+
"test:flake": "vitest run --repeats=10 --maxWorkers=1 --no-file-parallelism src/jobs.integration.test.ts src/capture-status-fanout.test.ts src/routers/design.integration.test.ts"
|
|
59
57
|
},
|
|
60
58
|
"dependencies": {
|
|
61
59
|
"@hono/swagger-ui": "^0.6.1",
|
|
62
60
|
"@hono/zod-openapi": "^1.1.5",
|
|
63
|
-
"@storyshelf/core": "0.
|
|
64
|
-
"@storyshelf/observability": "0.
|
|
61
|
+
"@storyshelf/core": "0.7.0",
|
|
62
|
+
"@storyshelf/observability": "0.7.0",
|
|
65
63
|
"hono": "^4.11.0",
|
|
66
64
|
"zod": "^4.2.0"
|
|
67
65
|
},
|
|
@@ -70,8 +68,9 @@
|
|
|
70
68
|
"@opentelemetry/context-async-hooks": "^2.11.0",
|
|
71
69
|
"@opentelemetry/core": "^2.11.0",
|
|
72
70
|
"@opentelemetry/sdk-trace-base": "^2.11.0",
|
|
73
|
-
"@storyshelf/auth": "0.
|
|
74
|
-
"@storyshelf/db-sqlite": "0.
|
|
71
|
+
"@storyshelf/auth": "0.7.0",
|
|
72
|
+
"@storyshelf/db-sqlite": "0.7.0",
|
|
73
|
+
"@storyshelf/design-figma": "0.7.0",
|
|
75
74
|
"@types/node": "^26.0.0",
|
|
76
75
|
"@vitest/coverage-v8": "^4.0.0",
|
|
77
76
|
"adm-zip": "0.6.0",
|
|
@@ -83,6 +82,9 @@
|
|
|
83
82
|
"typescript": "^7.0.2",
|
|
84
83
|
"vitest": "^5.0.0"
|
|
85
84
|
},
|
|
85
|
+
"engines": {
|
|
86
|
+
"node": ">=24"
|
|
87
|
+
},
|
|
86
88
|
"jsr": {
|
|
87
89
|
"runtimeCompat": {
|
|
88
90
|
"node": true,
|
package/dist/asset-paths.mjs
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import "node:path";
|
|
2
|
-
import "node:url";
|
|
3
|
-
import.meta.url;
|
|
4
|
-
import { existsSync } from "node:fs";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
-
//#region src/asset-paths.ts
|
|
7
|
-
/**
|
|
8
|
-
* Locate a vendored file under `assets/` for the three layouts the app runs in:
|
|
9
|
-
* - from source / the published library build, `assets/` sits next to the module;
|
|
10
|
-
* - in the bundled Fly server (`dist/server.mjs`) it sits one level up
|
|
11
|
-
* (`/app/assets`, copied by the Dockerfile).
|
|
12
|
-
* Throws with every path tried, since a missing file is a packaging bug.
|
|
13
|
-
*/
|
|
14
|
-
function findAsset(name, from, exists = existsSync) {
|
|
15
|
-
const candidates = [`assets/${name}`, `../assets/${name}`].map((relative) => fileURLToPath(new URL(relative, from)));
|
|
16
|
-
const found = candidates.find((path) => exists(path));
|
|
17
|
-
if (!found) throw new Error(`Vendored asset "${name}" not found. Looked in: ${candidates.join(", ")}`);
|
|
18
|
-
return found;
|
|
19
|
-
}
|
|
20
|
-
//#endregion
|
|
21
|
-
export { findAsset };
|
|
22
|
-
|
|
23
|
-
//# sourceMappingURL=asset-paths.mjs.map
|
package/dist/asset-paths.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"asset-paths.mjs","names":[],"sources":["../src/asset-paths.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport { fileURLToPath } from \"node:url\";\n\n/**\n * Locate a vendored file under `assets/` for the three layouts the app runs in:\n * - from source / the published library build, `assets/` sits next to the module;\n * - in the bundled Fly server (`dist/server.mjs`) it sits one level up\n * (`/app/assets`, copied by the Dockerfile).\n * Throws with every path tried, since a missing file is a packaging bug.\n */\nexport function findAsset(\n name: string,\n from: string | URL,\n exists: (path: string) => boolean = existsSync,\n): string {\n const candidates = [`assets/${name}`, `../assets/${name}`].map((relative) =>\n fileURLToPath(new URL(relative, from)),\n );\n const found = candidates.find((path) => exists(path));\n if (!found) {\n throw new Error(`Vendored asset \"${name}\" not found. Looked in: ${candidates.join(\", \")}`);\n }\n return found;\n}\n"],"mappings":";;;;;;;;;;;;;AAUA,SAAgB,UACd,MACA,MACA,SAAoC,YAC5B;CACR,MAAM,aAAa,CAAC,UAAU,QAAQ,aAAa,MAAM,CAAC,CAAC,KAAK,aAC9D,cAAc,IAAI,IAAI,UAAU,IAAI,CAAC,CACvC;CACA,MAAM,QAAQ,WAAW,MAAM,SAAS,OAAO,IAAI,CAAC;CACpD,IAAI,CAAC,OACH,MAAM,IAAI,MAAM,mBAAmB,KAAK,0BAA0B,WAAW,KAAK,IAAI,GAAG;CAE3F,OAAO;AACT"}
|
package/dist/assets/mark.svg
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg xmlns='http://www.w3.org/2000/svg' width='64' height='64' viewBox='0 0 64 64' role='img' aria-label='StoryShelf'><rect width='64' height='64' rx='14' fill='#1d5fcf'/><path d='M15 11 H49 A4 4 0 0 1 53 15 V15 A2 2 0 0 1 51 17 H13 A2 2 0 0 1 11 15 V15 A4 4 0 0 1 15 11 Z' fill='#ffffff'/><path d='M13 29 H51 A2 2 0 0 1 53 31 V33 A2 2 0 0 1 51 35 H13 A2 2 0 0 1 11 33 V31 A2 2 0 0 1 13 29 Z' fill='#ffffff'/><path d='M13 47 H51 A2 2 0 0 1 53 49 V49 A4 4 0 0 1 49 53 H15 A4 4 0 0 1 11 49 V49 A2 2 0 0 1 13 47 Z' fill='#ffffff'/><path d='M16 19 H16 A2 2 0 0 1 18 21 V27 A2 2 0 0 1 16 29 H16 A2 2 0 0 1 14 27 V21 A2 2 0 0 1 16 19 Z' fill='#ffffff'/><path d='M22 21 H22 A2 2 0 0 1 24 23 V27 A2 2 0 0 1 22 29 H22 A2 2 0 0 1 20 27 V23 A2 2 0 0 1 22 21 Z' fill='#ffffff'/><path d='M28 23 H28 A2 2 0 0 1 30 25 V27 A2 2 0 0 1 28 29 H28 A2 2 0 0 1 26 27 V25 A2 2 0 0 1 28 23 Z' fill='#ffffff'/><path d='M48 37 H48 A2 2 0 0 1 50 39 V45 A2 2 0 0 1 48 47 H48 A2 2 0 0 1 46 45 V39 A2 2 0 0 1 48 37 Z' fill='#ffffff'/><path d='M42 39 H42 A2 2 0 0 1 44 41 V45 A2 2 0 0 1 42 47 H42 A2 2 0 0 1 40 45 V41 A2 2 0 0 1 42 39 Z' fill='#ffffff'/><path d='M36 41 H36 A2 2 0 0 1 38 43 V45 A2 2 0 0 1 36 47 H36 A2 2 0 0 1 34 45 V43 A2 2 0 0 1 36 41 Z' fill='#ffffff'/></svg>
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import "node:path";
|
|
2
|
-
import "node:url";
|
|
3
|
-
import.meta.url;
|
|
4
|
-
import { Button } from "../ui/buttons.mjs";
|
|
5
|
-
import { Alert, Badge, Meta } from "../ui/feedback.mjs";
|
|
6
|
-
import { Field, FormActions, SelectField, TextareaField } from "../ui/forms.mjs";
|
|
7
|
-
import { Card, SectionTitle } from "../ui/layout.mjs";
|
|
8
|
-
import { Table } from "../ui/table.mjs";
|
|
9
|
-
import { csrfField } from "../ui/csrf-field.mjs";
|
|
10
|
-
import { jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
11
|
-
//#region src/pages/settings-status.tsx
|
|
12
|
-
function providerOptions(providers) {
|
|
13
|
-
return providers.map((provider) => ({
|
|
14
|
-
value: provider.metadata.kind,
|
|
15
|
-
label: provider.metadata.name
|
|
16
|
-
}));
|
|
17
|
-
}
|
|
18
|
-
const StatusConfigRow = ({ config, provider, project, isAdmin }) => {
|
|
19
|
-
const rendered = JSON.stringify(config.config);
|
|
20
|
-
return /* @__PURE__ */ jsxs("tr", { children: [
|
|
21
|
-
/* @__PURE__ */ jsx("td", { children: provider?.metadata.name ?? config.provider }),
|
|
22
|
-
/* @__PURE__ */ jsx("td", {
|
|
23
|
-
class: "truncate max-w-cell",
|
|
24
|
-
title: rendered,
|
|
25
|
-
children: /* @__PURE__ */ jsx("code", { children: rendered })
|
|
26
|
-
}),
|
|
27
|
-
/* @__PURE__ */ jsx("td", { children: config.hasToken ? /* @__PURE__ */ jsx(Badge, {
|
|
28
|
-
tone: "success",
|
|
29
|
-
children: "configured"
|
|
30
|
-
}) : /* @__PURE__ */ jsx(Badge, {
|
|
31
|
-
tone: "danger",
|
|
32
|
-
children: "missing"
|
|
33
|
-
}) }),
|
|
34
|
-
/* @__PURE__ */ jsx("td", { children: new Date(config.createdAt).toLocaleDateString() }),
|
|
35
|
-
/* @__PURE__ */ jsx("td", { children: isAdmin ? /* @__PURE__ */ jsxs("form", {
|
|
36
|
-
method: "post",
|
|
37
|
-
action: `/projects/${project.slug}/settings/status/${config.id}/delete`,
|
|
38
|
-
"hx-post": `/projects/${project.slug}/settings/status/${config.id}/delete`,
|
|
39
|
-
"hx-target": "body",
|
|
40
|
-
"hx-confirm": "Remove this git status config?",
|
|
41
|
-
children: [csrfField(), /* @__PURE__ */ jsx(Button, {
|
|
42
|
-
variant: "ghost",
|
|
43
|
-
type: "submit",
|
|
44
|
-
children: "Delete"
|
|
45
|
-
})]
|
|
46
|
-
}) : null })
|
|
47
|
-
] }, config.id);
|
|
48
|
-
};
|
|
49
|
-
const StatusCreateCard = ({ project, providers, formState }) => {
|
|
50
|
-
return /* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsx(SectionTitle, {
|
|
51
|
-
level: 3,
|
|
52
|
-
children: "Add git provider"
|
|
53
|
-
}), /* @__PURE__ */ jsxs("form", {
|
|
54
|
-
method: "post",
|
|
55
|
-
action: `/projects/${project.slug}/settings/status`,
|
|
56
|
-
"hx-post": `/projects/${project.slug}/settings/status`,
|
|
57
|
-
"hx-target": "body",
|
|
58
|
-
children: [
|
|
59
|
-
csrfField(),
|
|
60
|
-
/* @__PURE__ */ jsx(SelectField, {
|
|
61
|
-
label: "Provider",
|
|
62
|
-
name: "provider",
|
|
63
|
-
options: providerOptions(providers),
|
|
64
|
-
hint: "Integration that posts commit statuses for this project."
|
|
65
|
-
}),
|
|
66
|
-
/* @__PURE__ */ jsx(Field, {
|
|
67
|
-
label: "Token",
|
|
68
|
-
name: "token",
|
|
69
|
-
type: "password",
|
|
70
|
-
required: true,
|
|
71
|
-
placeholder: "ghp_…",
|
|
72
|
-
error: formState?.errors?.["token"],
|
|
73
|
-
hint: "Scoped token for the provider (e.g. a GitHub PAT with repo:status)."
|
|
74
|
-
}),
|
|
75
|
-
/* @__PURE__ */ jsx(TextareaField, {
|
|
76
|
-
label: "Config",
|
|
77
|
-
name: "config",
|
|
78
|
-
rows: 6,
|
|
79
|
-
placeholder: "{\"owner\":\"my-org\",\"repo\":\"my-repo\"}",
|
|
80
|
-
error: formState?.errors?.["config"],
|
|
81
|
-
hint: "JSON configuration for the provider. See the provider documentation for its fields."
|
|
82
|
-
}),
|
|
83
|
-
/* @__PURE__ */ jsx(FormActions, { children: /* @__PURE__ */ jsx(Button, {
|
|
84
|
-
variant: "primary",
|
|
85
|
-
type: "submit",
|
|
86
|
-
icon: "plus",
|
|
87
|
-
children: "Add git provider"
|
|
88
|
-
}) })
|
|
89
|
-
]
|
|
90
|
-
})] });
|
|
91
|
-
};
|
|
92
|
-
function renderCreateSection(project, providers, formState) {
|
|
93
|
-
if (providers.length === 0) return /* @__PURE__ */ jsx(Meta, { children: "No git providers registered on this server." });
|
|
94
|
-
return /* @__PURE__ */ jsx(StatusCreateCard, {
|
|
95
|
-
project,
|
|
96
|
-
providers,
|
|
97
|
-
formState
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
/** Git status settings tab: configured providers plus the add-provider form. */
|
|
101
|
-
function renderSettingsStatus(project, statusConfigs, providers, isAdmin, formState) {
|
|
102
|
-
const byProvider = new Map(providers.map((provider) => [provider.metadata.kind, provider]));
|
|
103
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
104
|
-
class: "grid max-w-form",
|
|
105
|
-
children: [
|
|
106
|
-
formState?.globalError ? /* @__PURE__ */ jsx(Alert, {
|
|
107
|
-
tone: "danger",
|
|
108
|
-
children: formState.globalError
|
|
109
|
-
}) : null,
|
|
110
|
-
/* @__PURE__ */ jsxs(Card, { children: [
|
|
111
|
-
/* @__PURE__ */ jsx(SectionTitle, { children: "Git status" }),
|
|
112
|
-
/* @__PURE__ */ jsx(Meta, { children: "Post commit statuses to a git provider so visual tests show up in your PR checks. Status is reported for each configured provider: pending while capturing, success on approval, failure on rejection or capture errors." }),
|
|
113
|
-
/* @__PURE__ */ jsx(Table, { children: /* @__PURE__ */ jsxs("table", { children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
114
|
-
/* @__PURE__ */ jsx("th", { children: "Provider" }),
|
|
115
|
-
/* @__PURE__ */ jsx("th", { children: "Config" }),
|
|
116
|
-
/* @__PURE__ */ jsx("th", { children: "Token" }),
|
|
117
|
-
/* @__PURE__ */ jsx("th", { children: "Created" }),
|
|
118
|
-
/* @__PURE__ */ jsx("th", { children: /* @__PURE__ */ jsx("span", {
|
|
119
|
-
class: "visually-hidden",
|
|
120
|
-
children: "Actions"
|
|
121
|
-
}) })
|
|
122
|
-
] }) }), /* @__PURE__ */ jsx("tbody", { children: statusConfigs.map((config) => /* @__PURE__ */ jsx(StatusConfigRow, {
|
|
123
|
-
config,
|
|
124
|
-
provider: byProvider.get(config.provider),
|
|
125
|
-
project,
|
|
126
|
-
isAdmin
|
|
127
|
-
}, config.id)) })] }) }),
|
|
128
|
-
statusConfigs.length === 0 ? /* @__PURE__ */ jsx(Meta, { children: "No git providers configured for this project." }) : null
|
|
129
|
-
] }),
|
|
130
|
-
isAdmin ? renderCreateSection(project, providers, formState) : null
|
|
131
|
-
]
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
//#endregion
|
|
135
|
-
export { renderSettingsStatus };
|
|
136
|
-
|
|
137
|
-
//# sourceMappingURL=settings-status.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"settings-status.mjs","names":[],"sources":["../../src/pages/settings-status.tsx"],"sourcesContent":["import type { GitHostProvider } from \"@storyshelf/core/adapter/git-host\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport type { FC } from \"hono/jsx\";\nimport {\n Alert,\n Badge,\n Button,\n Card,\n Field,\n FormActions,\n Meta,\n SectionTitle,\n SelectField,\n Table,\n TextareaField,\n} from \"../ui/components.tsx\";\nimport { csrfField } from \"../ui/csrf-field.tsx\";\n\n/** Git status configuration row as rendered in the status settings tab. */\nexport interface SettingsStatusConfig {\n id: string;\n provider: string;\n config: Record<string, unknown>;\n hasToken: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\n/** Form state for the git status settings tab. */\nexport interface StatusFormState {\n errors?: Record<string, string>;\n globalError?: string;\n}\n\n/* eslint-disable promise-function-async -- JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\nfunction providerOptions(providers: GitHostProvider[]): { value: string; label: string }[] {\n return providers.map((provider) => ({\n value: provider.metadata.kind,\n label: provider.metadata.name,\n }));\n}\n\nconst StatusConfigRow: FC<{\n config: SettingsStatusConfig;\n provider: GitHostProvider | undefined;\n project: Project;\n isAdmin: boolean;\n}> = ({ config, provider, project, isAdmin }) => {\n const rendered = JSON.stringify(config.config);\n return (\n <tr key={config.id}>\n <td>{provider?.metadata.name ?? config.provider}</td>\n <td class=\"truncate max-w-cell\" title={rendered}>\n <code>{rendered}</code>\n </td>\n <td>\n {config.hasToken ? (\n <Badge tone=\"success\">configured</Badge>\n ) : (\n <Badge tone=\"danger\">missing</Badge>\n )}\n </td>\n <td>{new Date(config.createdAt).toLocaleDateString()}</td>\n <td>\n {isAdmin ? (\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/status/${config.id}/delete`}\n hx-post={`/projects/${project.slug}/settings/status/${config.id}/delete`}\n hx-target=\"body\"\n hx-confirm=\"Remove this git status config?\"\n >\n {csrfField()}\n <Button variant=\"ghost\" type=\"submit\">\n Delete\n </Button>\n </form>\n ) : null}\n </td>\n </tr>\n );\n};\n\nconst StatusCreateCard: FC<{\n project: Project;\n providers: GitHostProvider[];\n formState?: StatusFormState;\n}> = ({ project, providers, formState }) => {\n return (\n <Card>\n <SectionTitle level={3}>Add git provider</SectionTitle>\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/status`}\n hx-post={`/projects/${project.slug}/settings/status`}\n hx-target=\"body\"\n >\n {csrfField()}\n <SelectField\n label=\"Provider\"\n name=\"provider\"\n options={providerOptions(providers)}\n hint=\"Integration that posts commit statuses for this project.\"\n />\n <Field\n label=\"Token\"\n name=\"token\"\n type=\"password\"\n required\n placeholder=\"ghp_…\"\n error={formState?.errors?.[\"token\"]}\n hint=\"Scoped token for the provider (e.g. a GitHub PAT with repo:status).\"\n />\n <TextareaField\n label=\"Config\"\n name=\"config\"\n rows={6}\n placeholder='{\"owner\":\"my-org\",\"repo\":\"my-repo\"}'\n error={formState?.errors?.[\"config\"]}\n hint=\"JSON configuration for the provider. See the provider documentation for its fields.\"\n />\n <FormActions>\n <Button variant=\"primary\" type=\"submit\" icon=\"plus\">\n Add git provider\n </Button>\n </FormActions>\n </form>\n </Card>\n );\n};\n\nfunction renderCreateSection(\n project: Project,\n providers: GitHostProvider[],\n formState?: StatusFormState,\n): unknown {\n if (providers.length === 0) {\n return <Meta>No git providers registered on this server.</Meta>;\n }\n return <StatusCreateCard project={project} providers={providers} formState={formState} />;\n}\n\n/** Git status settings tab: configured providers plus the add-provider form. */\n// eslint-disable-next-line max-params -- matches sibling settings render functions\nexport function renderSettingsStatus(\n project: Project,\n statusConfigs: SettingsStatusConfig[],\n providers: GitHostProvider[],\n isAdmin: boolean,\n formState?: StatusFormState,\n): unknown {\n const byProvider = new Map(providers.map((provider) => [provider.metadata.kind, provider]));\n return (\n <div class=\"grid max-w-form\">\n {formState?.globalError ? <Alert tone=\"danger\">{formState.globalError}</Alert> : null}\n\n <Card>\n <SectionTitle>Git status</SectionTitle>\n <Meta>\n Post commit statuses to a git provider so visual tests show up in your PR checks. Status\n is reported for each configured provider: pending while capturing, success on approval,\n failure on rejection or capture errors.\n </Meta>\n <Table>\n <table>\n <thead>\n <tr>\n <th>Provider</th>\n <th>Config</th>\n <th>Token</th>\n <th>Created</th>\n <th>\n <span class=\"visually-hidden\">Actions</span>\n </th>\n </tr>\n </thead>\n <tbody>\n {statusConfigs.map((config) => (\n <StatusConfigRow\n key={config.id}\n config={config}\n provider={byProvider.get(config.provider)}\n project={project}\n isAdmin={isAdmin}\n />\n ))}\n </tbody>\n </table>\n </Table>\n {statusConfigs.length === 0 ? (\n <Meta>No git providers configured for this project.</Meta>\n ) : null}\n </Card>\n\n {isAdmin ? renderCreateSection(project, providers, formState) : null}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import "node:path";
|
|
2
|
-
import "node:url";
|
|
3
|
-
import.meta.url;
|
|
4
|
-
import { credentialKeys } from "../credential-keys.mjs";
|
|
5
|
-
import { getStore } from "../store.mjs";
|
|
6
|
-
import { hxRedirect } from "./htmx.mjs";
|
|
7
|
-
import { flash } from "./flash.mjs";
|
|
8
|
-
import { asString, findProject, renderSettingsPage } from "./settings.handlers.mjs";
|
|
9
|
-
import { StatusConfigModel } from "@storyshelf/core/models";
|
|
10
|
-
//#region src/routers/settings-status.ts
|
|
11
|
-
/** Merge-gate status-config settings (provider configs + tokens, delete). */
|
|
12
|
-
function registerStatusSettings(app) {
|
|
13
|
-
app.get("/projects/:slug/settings/status", async (c) => c.html(await renderSettingsPage(c, "status")));
|
|
14
|
-
app.post("/projects/:slug/settings/status", handleCreateStatus);
|
|
15
|
-
app.post("/projects/:slug/settings/status/:id/delete", handleDeleteStatus);
|
|
16
|
-
}
|
|
17
|
-
async function unknownProviderResponse(c) {
|
|
18
|
-
return c.html(await renderSettingsPage(c, "status", { globalError: "Unknown git provider" }) ?? "", 400);
|
|
19
|
-
}
|
|
20
|
-
async function missingTokenResponse(c) {
|
|
21
|
-
return c.html(await renderSettingsPage(c, "status", { errors: { token: "Token is required" } }) ?? "", 400);
|
|
22
|
-
}
|
|
23
|
-
async function readStatusFields(c) {
|
|
24
|
-
const project = await findProject(c.req.param("slug") ?? "");
|
|
25
|
-
const form = await c.req.formData();
|
|
26
|
-
const provider = getStore().gitHosts.find((p) => p.metadata.kind === asString(form.get("provider")));
|
|
27
|
-
if (!provider) return await unknownProviderResponse(c);
|
|
28
|
-
const token = asString(form.get("token"));
|
|
29
|
-
if (!token) return await missingTokenResponse(c);
|
|
30
|
-
return {
|
|
31
|
-
project,
|
|
32
|
-
provider,
|
|
33
|
-
token,
|
|
34
|
-
configRaw: asString(form.get("config")) ?? "{}"
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
async function handleCreateStatus(c) {
|
|
38
|
-
const fields = await readStatusFields(c);
|
|
39
|
-
if (fields instanceof Response) return fields;
|
|
40
|
-
return await createStatusFromForm(c, fields.project, fields.provider, fields.token, fields.configRaw);
|
|
41
|
-
}
|
|
42
|
-
async function createStatusFromForm(c, project, provider, token, configRaw) {
|
|
43
|
-
const config = parseJsonConfig(configRaw);
|
|
44
|
-
if (config === void 0) return c.html(await renderSettingsPage(c, "status", { errors: { config: "Config must be valid JSON" } }) ?? "", 400);
|
|
45
|
-
const parsed = provider.metadata.schema.safeParse(config);
|
|
46
|
-
if (!parsed.success) return c.html(await renderSettingsPage(c, "status", { errors: { config: parsed.error.message } }) ?? "", 400);
|
|
47
|
-
return await persistStatusConfig(c, project, provider, token, parsed.data);
|
|
48
|
-
}
|
|
49
|
-
async function persistStatusConfig(c, project, provider, token, config) {
|
|
50
|
-
const { db, config: shelfConfig } = getStore();
|
|
51
|
-
await new StatusConfigModel(db, void 0, credentialKeys(shelfConfig)).create(project.id, {
|
|
52
|
-
provider: provider.metadata.kind,
|
|
53
|
-
config,
|
|
54
|
-
token
|
|
55
|
-
});
|
|
56
|
-
flash(c, "Git status config added");
|
|
57
|
-
return hxRedirect(c, `/projects/${project.slug}/settings/status`);
|
|
58
|
-
}
|
|
59
|
-
/** Parse the provider config JSON, or undefined when invalid. */
|
|
60
|
-
function parseJsonConfig(raw) {
|
|
61
|
-
try {
|
|
62
|
-
return JSON.parse(raw);
|
|
63
|
-
} catch {
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
async function handleDeleteStatus(c) {
|
|
68
|
-
const project = await findProject(c.req.param("slug") ?? "");
|
|
69
|
-
const { db, config: shelfConfig } = getStore();
|
|
70
|
-
await new StatusConfigModel(db, void 0, credentialKeys(shelfConfig)).remove(project.id, c.req.param("id") ?? "");
|
|
71
|
-
flash(c, "Git status config removed");
|
|
72
|
-
return hxRedirect(c, `/projects/${project.slug}/settings/status`);
|
|
73
|
-
}
|
|
74
|
-
//#endregion
|
|
75
|
-
export { registerStatusSettings };
|
|
76
|
-
|
|
77
|
-
//# sourceMappingURL=settings-status.mjs.map
|