@storyshelf/app 0.6.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -4
- package/dist/app-types.d.mts +2 -6
- package/dist/asset-manifest.mjs +14 -7
- package/dist/asset-manifest.mjs.map +1 -1
- package/dist/assets/{htmx.min.js → htmx.mjs} +9 -1
- package/dist/assets/htmx.mjs.map +1 -0
- package/dist/assets/mark.mjs +9 -0
- package/dist/assets/mark.mjs.map +1 -0
- package/dist/boot-tasks.mjs +79 -0
- package/dist/boot-tasks.mjs.map +1 -0
- package/dist/build-state-flags.mjs +17 -0
- package/dist/build-state-flags.mjs.map +1 -0
- package/dist/build-state-setup.mjs +74 -0
- package/dist/build-state-setup.mjs.map +1 -0
- package/dist/capture-alerts.mjs +45 -0
- package/dist/capture-alerts.mjs.map +1 -0
- package/dist/capture-process.mjs +58 -0
- package/dist/capture-process.mjs.map +1 -0
- package/dist/capture-setup.mjs +96 -119
- package/dist/capture-setup.mjs.map +1 -1
- package/dist/connections.mjs +33 -0
- package/dist/connections.mjs.map +1 -0
- package/dist/consumer-setup.mjs +57 -0
- package/dist/consumer-setup.mjs.map +1 -0
- package/dist/credential-keys.mjs +1 -2
- package/dist/credentials.mjs +1 -2
- package/dist/git-status.mjs +45 -0
- package/dist/git-status.mjs.map +1 -0
- package/dist/hidden-data.mjs +23 -0
- package/dist/hidden-data.mjs.map +1 -0
- package/dist/index.d.mts +3 -5
- package/dist/index.mjs +204 -127
- package/dist/index.mjs.map +1 -1
- package/dist/insights/budget.mjs +48 -0
- package/dist/insights/budget.mjs.map +1 -0
- package/dist/insights/deps.mjs +20 -0
- package/dist/insights/deps.mjs.map +1 -0
- package/dist/insights/errors.mjs +21 -0
- package/dist/insights/errors.mjs.map +1 -0
- package/dist/insights/health-panel.mjs +39 -0
- package/dist/insights/health-panel.mjs.map +1 -0
- package/dist/insights/job.mjs +102 -0
- package/dist/insights/job.mjs.map +1 -0
- package/dist/insights/load-health.mjs +58 -0
- package/dist/insights/load-health.mjs.map +1 -0
- package/dist/insights/load-triage.mjs +97 -0
- package/dist/insights/load-triage.mjs.map +1 -0
- package/dist/insights/panel.mjs +27 -0
- package/dist/insights/panel.mjs.map +1 -0
- package/dist/insights/request.mjs +112 -0
- package/dist/insights/request.mjs.map +1 -0
- package/dist/insights/stale.mjs +20 -0
- package/dist/insights/stale.mjs.map +1 -0
- package/dist/insights/usage-report.mjs +59 -0
- package/dist/insights/usage-report.mjs.map +1 -0
- package/dist/insights/view.mjs +47 -0
- package/dist/insights/view.mjs.map +1 -0
- package/dist/job-canceller.mjs +13 -0
- package/dist/job-canceller.mjs.map +1 -0
- package/dist/job-queue.mjs +50 -0
- package/dist/job-queue.mjs.map +1 -0
- package/dist/jobs-setup.mjs +131 -0
- package/dist/jobs-setup.mjs.map +1 -0
- package/dist/lifecycle.mjs +93 -22
- package/dist/lifecycle.mjs.map +1 -1
- package/dist/middleware/auth-gate.mjs +5 -4
- package/dist/middleware/auth-gate.mjs.map +1 -1
- package/dist/middleware/csrf.mjs +1 -2
- package/dist/middleware/rate-limit.mjs +42 -35
- package/dist/middleware/rate-limit.mjs.map +1 -1
- package/dist/middleware/request-log.mjs +4 -4
- package/dist/middleware/request-log.mjs.map +1 -1
- package/dist/middleware/require-review.mjs +50 -0
- package/dist/middleware/require-review.mjs.map +1 -0
- package/dist/middleware/require-testing.mjs +40 -0
- package/dist/middleware/require-testing.mjs.map +1 -0
- package/dist/middleware/server-timing.mjs +1 -2
- package/dist/middleware/setup-gate.mjs +1 -2
- package/dist/middleware/setup-gate.mjs.map +1 -1
- package/dist/middleware/store-scope.mjs +13 -4
- package/dist/middleware/store-scope.mjs.map +1 -1
- package/dist/notifications-pause.mjs +32 -0
- package/dist/notifications-pause.mjs.map +1 -0
- package/dist/notify.d.mts +3 -4
- package/dist/notify.mjs +79 -41
- package/dist/notify.mjs.map +1 -1
- package/dist/pages/a11y-violations.mjs +115 -0
- package/dist/pages/a11y-violations.mjs.map +1 -0
- package/dist/pages/admin-ai.mjs +77 -0
- package/dist/pages/admin-ai.mjs.map +1 -0
- package/dist/pages/admin-system-origin.mjs +132 -0
- package/dist/pages/admin-system-origin.mjs.map +1 -0
- package/dist/pages/admin-system-targets.mjs +52 -0
- package/dist/pages/admin-system-targets.mjs.map +1 -0
- package/dist/pages/admin-system.mjs +64 -15
- package/dist/pages/admin-system.mjs.map +1 -1
- package/dist/pages/build-detail.mjs +105 -78
- package/dist/pages/build-detail.mjs.map +1 -1
- package/dist/pages/build-diff-actions.mjs +88 -35
- package/dist/pages/build-diff-actions.mjs.map +1 -1
- package/dist/pages/build-diff-comments.mjs +17 -16
- package/dist/pages/build-diff-comments.mjs.map +1 -1
- package/dist/pages/build-diff-default.mjs +53 -0
- package/dist/pages/build-diff-default.mjs.map +1 -0
- package/dist/pages/build-diff-environment.mjs +81 -0
- package/dist/pages/build-diff-environment.mjs.map +1 -0
- package/dist/pages/build-diff-header.mjs +23 -6
- package/dist/pages/build-diff-header.mjs.map +1 -1
- package/dist/pages/build-diff-insight.mjs +112 -0
- package/dist/pages/build-diff-insight.mjs.map +1 -0
- package/dist/pages/build-diff-model.mjs +74 -12
- package/dist/pages/build-diff-model.mjs.map +1 -1
- package/dist/pages/build-diff-nav.mjs +29 -14
- package/dist/pages/build-diff-nav.mjs.map +1 -1
- package/dist/pages/build-diff-reference.mjs +84 -0
- package/dist/pages/build-diff-reference.mjs.map +1 -0
- package/dist/pages/build-diff-shortcuts.mjs +6 -3
- package/dist/pages/build-diff-shortcuts.mjs.map +1 -1
- package/dist/pages/build-diff-stale.mjs +1 -2
- package/dist/pages/build-diff-usage.mjs +62 -0
- package/dist/pages/build-diff-usage.mjs.map +1 -0
- package/dist/pages/build-diff-viewer.mjs +97 -14
- package/dist/pages/build-diff-viewer.mjs.map +1 -1
- package/dist/pages/build-diff.mjs +37 -22
- package/dist/pages/build-diff.mjs.map +1 -1
- package/dist/pages/build-tests.mjs +73 -0
- package/dist/pages/build-tests.mjs.map +1 -0
- package/dist/pages/component-detail.mjs +248 -0
- package/dist/pages/component-detail.mjs.map +1 -0
- package/dist/pages/component-stories.mjs +167 -0
- package/dist/pages/component-stories.mjs.map +1 -0
- package/dist/pages/compute-jobs.mjs +14 -28
- package/dist/pages/compute-jobs.mjs.map +1 -1
- package/dist/pages/design-links.mjs +181 -0
- package/dist/pages/design-links.mjs.map +1 -0
- package/dist/pages/environment-change.mjs +52 -0
- package/dist/pages/environment-change.mjs.map +1 -0
- package/dist/pages/health-panel.mjs +125 -0
- package/dist/pages/health-panel.mjs.map +1 -0
- package/dist/pages/invite.mjs +1 -2
- package/dist/pages/label-detail.mjs +1 -2
- package/dist/pages/library-grid.mjs +104 -0
- package/dist/pages/library-grid.mjs.map +1 -0
- package/dist/pages/library-model.mjs +46 -0
- package/dist/pages/library-model.mjs.map +1 -0
- package/dist/pages/library-snapshots.mjs +129 -0
- package/dist/pages/library-snapshots.mjs.map +1 -0
- package/dist/pages/library.mjs +113 -220
- package/dist/pages/library.mjs.map +1 -1
- package/dist/pages/login.mjs +1 -2
- package/dist/pages/notifications-paused.mjs +21 -0
- package/dist/pages/notifications-paused.mjs.map +1 -0
- package/dist/pages/passkey-ceremony.mjs +1 -2
- package/dist/pages/profile.mjs +54 -50
- package/dist/pages/profile.mjs.map +1 -1
- package/dist/pages/project-builds-table.mjs +3 -4
- package/dist/pages/project-builds-table.mjs.map +1 -1
- package/dist/pages/project-builds.mjs +52 -23
- package/dist/pages/project-builds.mjs.map +1 -1
- package/dist/pages/project-create.mjs +1 -2
- package/dist/pages/project-settings.mjs +52 -14
- package/dist/pages/project-settings.mjs.map +1 -1
- package/dist/pages/projects-card.mjs +15 -3
- package/dist/pages/projects-card.mjs.map +1 -1
- package/dist/pages/projects.mjs +7 -4
- package/dist/pages/projects.mjs.map +1 -1
- package/dist/pages/review-changeset.mjs +115 -0
- package/dist/pages/review-changeset.mjs.map +1 -0
- package/dist/pages/review-comments.mjs +102 -0
- package/dist/pages/review-comments.mjs.map +1 -0
- package/dist/pages/review-verdict.mjs +101 -0
- package/dist/pages/review-verdict.mjs.map +1 -0
- package/dist/pages/root.mjs +1 -2
- package/dist/pages/settings-ai.mjs +68 -0
- package/dist/pages/settings-ai.mjs.map +1 -0
- package/dist/pages/settings-capture.mjs +123 -0
- package/dist/pages/settings-capture.mjs.map +1 -0
- package/dist/pages/settings-design-credential.mjs +98 -0
- package/dist/pages/settings-design-credential.mjs.map +1 -0
- package/dist/pages/settings-design-links.mjs +178 -0
- package/dist/pages/settings-design-links.mjs.map +1 -0
- package/dist/pages/settings-design.mjs +47 -0
- package/dist/pages/settings-design.mjs.map +1 -0
- package/dist/pages/settings-general.mjs +47 -23
- package/dist/pages/settings-general.mjs.map +1 -1
- package/dist/pages/settings-labels.mjs +44 -4
- package/dist/pages/settings-labels.mjs.map +1 -1
- package/dist/pages/settings-links.mjs +111 -0
- package/dist/pages/settings-links.mjs.map +1 -0
- package/dist/pages/settings-members.mjs +1 -2
- package/dist/pages/settings-notifications-git.mjs +127 -0
- package/dist/pages/settings-notifications-git.mjs.map +1 -0
- package/dist/pages/settings-notifications.mjs +80 -44
- package/dist/pages/settings-notifications.mjs.map +1 -1
- package/dist/pages/settings-tests.mjs +34 -4
- package/dist/pages/settings-tests.mjs.map +1 -1
- package/dist/pages/settings-tokens.mjs +1 -2
- package/dist/pages/settings-webhooks.mjs +1 -2
- package/dist/pages/storybook.mjs +1 -2
- package/dist/pages/target-choices.mjs +38 -0
- package/dist/pages/target-choices.mjs.map +1 -0
- package/dist/publish-setup.mjs +88 -0
- package/dist/publish-setup.mjs.map +1 -0
- package/dist/retention-timer.mjs +12 -12
- package/dist/retention-timer.mjs.map +1 -1
- package/dist/review-events.mjs +47 -0
- package/dist/review-events.mjs.map +1 -0
- package/dist/routers/admin-pages.mjs +46 -5
- package/dist/routers/admin-pages.mjs.map +1 -1
- package/dist/routers/admin-public-origin.mjs +92 -0
- package/dist/routers/admin-public-origin.mjs.map +1 -0
- package/dist/routers/admin.mjs +8 -4
- package/dist/routers/admin.mjs.map +1 -1
- package/dist/routers/assets.mjs +10 -4
- package/dist/routers/assets.mjs.map +1 -1
- package/dist/routers/attempts.mjs +1 -2
- package/dist/routers/auth-sync.mjs +1 -2
- package/dist/routers/auth.mjs +2 -3
- package/dist/routers/baseline-guard.mjs +4 -5
- package/dist/routers/baseline-guard.mjs.map +1 -1
- package/dist/routers/baseline-reference.mjs +72 -0
- package/dist/routers/baseline-reference.mjs.map +1 -0
- package/dist/routers/baselines.handlers.mjs +63 -0
- package/dist/routers/baselines.handlers.mjs.map +1 -0
- package/dist/routers/baselines.mjs +124 -0
- package/dist/routers/baselines.mjs.map +1 -0
- package/dist/routers/build-cancel.mjs +67 -0
- package/dist/routers/build-cancel.mjs.map +1 -0
- package/dist/routers/build-changeset.mjs +69 -0
- package/dist/routers/build-changeset.mjs.map +1 -0
- package/dist/routers/build-reviews.handlers.mjs +85 -0
- package/dist/routers/build-reviews.handlers.mjs.map +1 -0
- package/dist/routers/build-reviews.mjs +73 -0
- package/dist/routers/build-reviews.mjs.map +1 -0
- package/dist/routers/builds.handlers.mjs +65 -60
- package/dist/routers/builds.handlers.mjs.map +1 -1
- package/dist/routers/builds.mjs +77 -19
- package/dist/routers/builds.mjs.map +1 -1
- package/dist/routers/capture-cache.mjs +29 -0
- package/dist/routers/capture-cache.mjs.map +1 -0
- package/dist/routers/capture-config.mjs +34 -0
- package/dist/routers/capture-config.mjs.map +1 -0
- package/dist/routers/comments.mjs +42 -17
- package/dist/routers/comments.mjs.map +1 -1
- package/dist/routers/comparison-image.mjs +29 -0
- package/dist/routers/comparison-image.mjs.map +1 -0
- package/dist/routers/component-catalog.handlers.mjs +70 -0
- package/dist/routers/component-catalog.handlers.mjs.map +1 -0
- package/dist/routers/component.handlers.mjs +120 -0
- package/dist/routers/component.handlers.mjs.map +1 -0
- package/dist/routers/connections.mjs +117 -0
- package/dist/routers/connections.mjs.map +1 -0
- package/dist/routers/design-connect.handlers.mjs +43 -0
- package/dist/routers/design-connect.handlers.mjs.map +1 -0
- package/dist/routers/design.handlers.mjs +113 -0
- package/dist/routers/design.handlers.mjs.map +1 -0
- package/dist/routers/flash.mjs +1 -2
- package/dist/routers/health-report.mjs +3 -5
- package/dist/routers/health-report.mjs.map +1 -1
- package/dist/routers/health.mjs +5 -5
- package/dist/routers/health.mjs.map +1 -1
- package/dist/routers/helpers.mjs +20 -5
- package/dist/routers/helpers.mjs.map +1 -1
- package/dist/routers/htmx.mjs +1 -2
- package/dist/routers/insights-ui.mjs +63 -0
- package/dist/routers/insights-ui.mjs.map +1 -0
- package/dist/routers/insights.handlers.mjs +44 -0
- package/dist/routers/insights.handlers.mjs.map +1 -0
- package/dist/routers/insights.mjs +188 -0
- package/dist/routers/insights.mjs.map +1 -0
- package/dist/routers/labels.mjs +1 -2
- package/dist/routers/links.handlers.mjs +79 -0
- package/dist/routers/links.handlers.mjs.map +1 -0
- package/dist/routers/links.mjs +138 -0
- package/dist/routers/links.mjs.map +1 -0
- package/dist/routers/media.mjs +30 -7
- package/dist/routers/media.mjs.map +1 -1
- package/dist/routers/members.mjs +3 -3
- package/dist/routers/members.mjs.map +1 -1
- package/dist/routers/notification-channels.mjs +18 -25
- package/dist/routers/notification-channels.mjs.map +1 -1
- package/dist/routers/notification-subscriptions.mjs +1 -2
- package/dist/routers/oauth-state.mjs +44 -0
- package/dist/routers/oauth-state.mjs.map +1 -0
- package/dist/routers/preview-base.mjs +37 -0
- package/dist/routers/preview-base.mjs.map +1 -0
- package/dist/routers/preview-token.mjs +50 -0
- package/dist/routers/preview-token.mjs.map +1 -0
- package/dist/routers/profile.mjs +4 -3
- package/dist/routers/profile.mjs.map +1 -1
- package/dist/routers/projects.mjs +18 -3
- package/dist/routers/projects.mjs.map +1 -1
- package/dist/routers/publish-upload.mjs +85 -0
- package/dist/routers/publish-upload.mjs.map +1 -0
- package/dist/routers/published-access.mjs +51 -0
- package/dist/routers/published-access.mjs.map +1 -0
- package/dist/routers/published-compression.mjs +109 -0
- package/dist/routers/published-compression.mjs.map +1 -0
- package/dist/routers/published-files.mjs +69 -0
- package/dist/routers/published-files.mjs.map +1 -0
- package/dist/routers/review-forms.mjs +71 -0
- package/dist/routers/review-forms.mjs.map +1 -0
- package/dist/routers/review-next.mjs +1 -2
- package/dist/routers/review-section.handlers.mjs +84 -0
- package/dist/routers/review-section.handlers.mjs.map +1 -0
- package/dist/routers/review-switch-off.mjs +28 -0
- package/dist/routers/review-switch-off.mjs.map +1 -0
- package/dist/routers/schemas.mjs +141 -24
- package/dist/routers/schemas.mjs.map +1 -1
- package/dist/routers/scoped-models.mjs +21 -0
- package/dist/routers/scoped-models.mjs.map +1 -0
- package/dist/routers/settings-ai.mjs +34 -0
- package/dist/routers/settings-ai.mjs.map +1 -0
- package/dist/routers/settings-capture.mjs +76 -0
- package/dist/routers/settings-capture.mjs.map +1 -0
- package/dist/routers/settings-design.mjs +102 -0
- package/dist/routers/settings-design.mjs.map +1 -0
- package/dist/routers/settings-general.mjs +56 -12
- package/dist/routers/settings-general.mjs.map +1 -1
- package/dist/routers/settings-git-channel.mjs +76 -0
- package/dist/routers/settings-git-channel.mjs.map +1 -0
- package/dist/routers/settings-labels.mjs +28 -3
- package/dist/routers/settings-labels.mjs.map +1 -1
- package/dist/routers/settings-links.mjs +62 -0
- package/dist/routers/settings-links.mjs.map +1 -0
- package/dist/routers/settings-members.mjs +5 -9
- package/dist/routers/settings-members.mjs.map +1 -1
- package/dist/routers/settings-notifications.mjs +16 -12
- package/dist/routers/settings-notifications.mjs.map +1 -1
- package/dist/routers/settings-tokens.mjs +3 -7
- package/dist/routers/settings-tokens.mjs.map +1 -1
- package/dist/routers/settings-webhooks.mjs +8 -26
- package/dist/routers/settings-webhooks.mjs.map +1 -1
- package/dist/routers/settings.handlers.mjs +108 -28
- package/dist/routers/settings.handlers.mjs.map +1 -1
- package/dist/routers/settings.mjs +17 -6
- package/dist/routers/settings.mjs.map +1 -1
- package/dist/routers/signed-storybook.mjs +55 -0
- package/dist/routers/signed-storybook.mjs.map +1 -0
- package/dist/routers/snapshot-groups.mjs +62 -0
- package/dist/routers/snapshot-groups.mjs.map +1 -0
- package/dist/routers/snapshots.mjs +104 -7
- package/dist/routers/snapshots.mjs.map +1 -1
- package/dist/routers/storybook.mjs +57 -104
- package/dist/routers/storybook.mjs.map +1 -1
- package/dist/routers/tokens.mjs +3 -7
- package/dist/routers/tokens.mjs.map +1 -1
- package/dist/routers/ui.mjs +38 -7
- package/dist/routers/ui.mjs.map +1 -1
- package/dist/routers/webhooks.mjs +6 -13
- package/dist/routers/webhooks.mjs.map +1 -1
- package/dist/routers/well-known.mjs +12 -6
- package/dist/routers/well-known.mjs.map +1 -1
- package/dist/routes/api-routes.mjs +92 -0
- package/dist/routes/api-routes.mjs.map +1 -0
- package/dist/routes/classification.mjs +37 -0
- package/dist/routes/classification.mjs.map +1 -0
- package/dist/routes/openapi-classes.mjs +21 -0
- package/dist/routes/openapi-classes.mjs.map +1 -0
- package/dist/routes/page-routes.mjs +103 -0
- package/dist/routes/page-routes.mjs.map +1 -0
- package/dist/runtime.mjs +19 -15
- package/dist/runtime.mjs.map +1 -1
- package/dist/serverless.mjs +4 -0
- package/dist/store.mjs +20 -3
- package/dist/store.mjs.map +1 -1
- package/dist/targets.mjs +45 -0
- package/dist/targets.mjs.map +1 -0
- package/dist/ui/avatar.mjs +1 -2
- package/dist/ui/buttons.mjs +17 -5
- package/dist/ui/buttons.mjs.map +1 -1
- package/dist/ui/code-block.mjs +1 -2
- package/dist/ui/compare-stage.mjs +86 -54
- package/dist/ui/compare-stage.mjs.map +1 -1
- package/dist/ui/csrf-field.mjs +1 -2
- package/dist/ui/css.mjs +1 -2
- package/dist/ui/design-reference.mjs +96 -0
- package/dist/ui/design-reference.mjs.map +1 -0
- package/dist/ui/document.mjs +2 -4
- package/dist/ui/document.mjs.map +1 -1
- package/dist/ui/dropdown.mjs +1 -2
- package/dist/ui/feedback.mjs +1 -2
- package/dist/ui/filter-input.mjs +1 -2
- package/dist/ui/forms.mjs +7 -7
- package/dist/ui/forms.mjs.map +1 -1
- package/dist/ui/icons/icon.mjs +1 -2
- package/dist/ui/icons/paths.mjs +2 -2
- package/dist/ui/icons/paths.mjs.map +1 -1
- package/dist/ui/icons/sprite.mjs +1 -2
- package/dist/ui/kbd.mjs +1 -2
- package/dist/ui/layout.mjs +1 -2
- package/dist/ui/preview-embed.mjs +81 -0
- package/dist/ui/preview-embed.mjs.map +1 -0
- package/dist/ui/progress.mjs +1 -2
- package/dist/ui/segmented.mjs +1 -2
- package/dist/ui/shell/account.mjs +1 -2
- package/dist/ui/shell/bare.mjs +1 -2
- package/dist/ui/shell/boot-script.mjs +1 -2
- package/dist/ui/shell/client-script.mjs +86 -2
- package/dist/ui/shell/client-script.mjs.map +1 -1
- package/dist/ui/shell/nav.mjs +13 -10
- package/dist/ui/shell/nav.mjs.map +1 -1
- package/dist/ui/shell/review-script.mjs +25 -3
- package/dist/ui/shell/review-script.mjs.map +1 -1
- package/dist/ui/shell/sidebar.mjs +3 -4
- package/dist/ui/shell/sidebar.mjs.map +1 -1
- package/dist/ui/shell/topbar.mjs +1 -2
- package/dist/ui/stacks.mjs +1 -2
- package/dist/ui/styles/base.mjs +1 -2
- package/dist/ui/styles/behavior.mjs +1 -2
- package/dist/ui/styles/review-layout.mjs +1 -2
- package/dist/ui/styles/review.mjs +1 -2
- package/dist/ui/styles/tokens.mjs +1 -2
- package/dist/ui/styles.mjs +1 -2
- package/dist/ui/subnav.mjs +1 -2
- package/dist/ui/table.mjs +1 -2
- package/dist/ui/theme.mjs +1 -2
- package/dist/ui/thumbnail.mjs +1 -2
- package/dist/ui/time.mjs +1 -2
- package/package.json +14 -12
- 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
|
@@ -3,7 +3,7 @@ import "node:url";
|
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { iconSpriteHref } from "../icons/sprite.mjs";
|
|
5
5
|
import { reviewScript } from "./review-script.mjs";
|
|
6
|
-
|
|
6
|
+
// src/ui/shell/client-script.ts
|
|
7
7
|
/** Attach the CSRF token to every HTMX request. */
|
|
8
8
|
function csrfPart() {
|
|
9
9
|
return `
|
|
@@ -12,6 +12,27 @@ function csrfPart() {
|
|
|
12
12
|
if(meta){ e.detail.headers['x-csrf-token']=meta.getAttribute('content'); }
|
|
13
13
|
});`;
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* System page: report the origin the admin opened it with, so a deployment learns its public URL
|
|
17
|
+
* without a redeploy (ADR 0007, decision 6). The server keeps the first report and answers 201 only
|
|
18
|
+
* when it stored a new one, which is when the page reloads to show it.
|
|
19
|
+
*/
|
|
20
|
+
function originPart() {
|
|
21
|
+
return `
|
|
22
|
+
function reportOrigin(){
|
|
23
|
+
var el=document.querySelector('[data-report-origin]');
|
|
24
|
+
if(!el || el.getAttribute('data-reported')) return;
|
|
25
|
+
el.setAttribute('data-reported','1');
|
|
26
|
+
var meta=document.querySelector('meta[name="csrf-token"]');
|
|
27
|
+
fetch(el.getAttribute('data-report-origin'),{
|
|
28
|
+
method:'POST',
|
|
29
|
+
credentials:'same-origin',
|
|
30
|
+
headers:{'content-type':'application/json','x-csrf-token':meta ? meta.getAttribute('content') : ''},
|
|
31
|
+
body:JSON.stringify({origin:location.origin})
|
|
32
|
+
}).then(function(r){ if(r.status===201) location.reload(); }).catch(function(){});
|
|
33
|
+
}
|
|
34
|
+
inits.push(reportOrigin);`;
|
|
35
|
+
}
|
|
15
36
|
/** Theme menu: apply, persist, and mark the current choice. */
|
|
16
37
|
function themePart() {
|
|
17
38
|
return `
|
|
@@ -206,6 +227,67 @@ function filterPart() {
|
|
|
206
227
|
scope.querySelectorAll('[data-filter-empty]').forEach(function(el){ el.hidden=shown>0; });
|
|
207
228
|
});`;
|
|
208
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* Click-to-load story previews: move the inert frame out of its template (nothing runs before). A
|
|
232
|
+
* frame under a signed URL first asks for a fresh root, so a page left open for hours does not mount
|
|
233
|
+
* an expired token; it keeps the rendered one when that request fails. Afterwards the root is renewed
|
|
234
|
+
* every `data-preview-refresh-seconds`; a failed renewal is retried a few times a minute apart.
|
|
235
|
+
*/
|
|
236
|
+
function previewPart() {
|
|
237
|
+
return `
|
|
238
|
+
document.addEventListener('click',function(e){
|
|
239
|
+
var btn=e.target instanceof Element ? e.target.closest('[data-preview-load]') : null;
|
|
240
|
+
var host=btn ? btn.closest('[data-preview-embed]') : null;
|
|
241
|
+
var tpl=host ? host.querySelector('template[data-preview-template]') : null;
|
|
242
|
+
if(!host||!tpl||host.getAttribute('data-preview-loading')) return;
|
|
243
|
+
host.setAttribute('data-preview-loading','1');
|
|
244
|
+
var idle=host.querySelector('[data-preview-idle]');
|
|
245
|
+
var path=host.getAttribute('data-preview-path');
|
|
246
|
+
var fresh=fetchPreviewBase(host).catch(function(){ return null; });
|
|
247
|
+
fresh.then(function(base){
|
|
248
|
+
var content=document.importNode(tpl.content,true);
|
|
249
|
+
var frame=content.querySelector('iframe');
|
|
250
|
+
if(frame&&base&&path) frame.src=base+path;
|
|
251
|
+
if(idle) idle.hidden=true;
|
|
252
|
+
host.appendChild(content);
|
|
253
|
+
tpl.remove();
|
|
254
|
+
keepPreviewFresh(host);
|
|
255
|
+
});
|
|
256
|
+
});`;
|
|
257
|
+
}
|
|
258
|
+
/** Fetch a fresh signed root for a preview host and keep it renewed (see {@link previewPart}). */
|
|
259
|
+
function previewRefreshPart() {
|
|
260
|
+
return `
|
|
261
|
+
function fetchPreviewBase(host){
|
|
262
|
+
var url=host.getAttribute('data-preview-refresh-url');
|
|
263
|
+
if(!url) return Promise.resolve(null);
|
|
264
|
+
return fetch(url,{headers:{accept:'application/json'}}).then(function(r){
|
|
265
|
+
if(!r.ok) throw new Error('preview-token');
|
|
266
|
+
return r.json();
|
|
267
|
+
}).then(function(body){ return body&&body.base ? body.base : null; });
|
|
268
|
+
}
|
|
269
|
+
function keepPreviewFresh(host){
|
|
270
|
+
var url=host.getAttribute('data-preview-refresh-url');
|
|
271
|
+
var path=host.getAttribute('data-preview-path');
|
|
272
|
+
var seconds=Number(host.getAttribute('data-preview-refresh-seconds'));
|
|
273
|
+
if(!url||!path||!(seconds>0)) return;
|
|
274
|
+
var RETRY_MS=60000, MAX_RETRIES=3;
|
|
275
|
+
function renew(tries){
|
|
276
|
+
var frame=host.querySelector('iframe');
|
|
277
|
+
if(!host.isConnected||!frame) return;
|
|
278
|
+
fetchPreviewBase(host).then(function(base){
|
|
279
|
+
if(base) frame.src=base+path;
|
|
280
|
+
else throw new Error('preview-token');
|
|
281
|
+
}).catch(function(){
|
|
282
|
+
if(tries<MAX_RETRIES) setTimeout(function(){ renew(tries+1); },RETRY_MS);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
var timer=setInterval(function(){
|
|
286
|
+
if(!host.isConnected||!host.querySelector('iframe')){ clearInterval(timer); return; }
|
|
287
|
+
renew(0);
|
|
288
|
+
},seconds*1000);
|
|
289
|
+
}`;
|
|
290
|
+
}
|
|
209
291
|
/** Thin top progress bar while an HTMX request (e.g. snapshot navigation) is in flight. */
|
|
210
292
|
function loadingPart() {
|
|
211
293
|
return `
|
|
@@ -237,6 +319,7 @@ function clientScript() {
|
|
|
237
319
|
if(!shell) return;
|
|
238
320
|
var inits=[];${[
|
|
239
321
|
csrfPart(),
|
|
322
|
+
originPart(),
|
|
240
323
|
themePart(),
|
|
241
324
|
sidebarPart(),
|
|
242
325
|
dropdownPart(),
|
|
@@ -244,6 +327,8 @@ function clientScript() {
|
|
|
244
327
|
formPart(),
|
|
245
328
|
copyPart(),
|
|
246
329
|
filterPart(),
|
|
330
|
+
previewPart(),
|
|
331
|
+
previewRefreshPart(),
|
|
247
332
|
loadingPart(),
|
|
248
333
|
focusPart(),
|
|
249
334
|
reviewScript()
|
|
@@ -253,7 +338,6 @@ function clientScript() {
|
|
|
253
338
|
})();
|
|
254
339
|
`.trim();
|
|
255
340
|
}
|
|
256
|
-
//#endregion
|
|
257
341
|
export { clientScript };
|
|
258
342
|
|
|
259
343
|
//# sourceMappingURL=client-script.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client-script.mjs","names":[],"sources":["../../../src/ui/shell/client-script.ts"],"sourcesContent":["import { iconSpriteHref } from \"../icons/sprite.ts\";\nimport { reviewScript } from \"./review-script.ts\";\n\n/*\n * The page-end script is re-executed whenever HTMX swaps `body` (review\n * navigation does). So it is split in two: listeners are bound ONCE on\n * `document` and look elements up lazily; `inits` are re-run per render for\n * anything that needs wiring to freshly swapped elements.\n */\n\n/** Attach the CSRF token to every HTMX request. */\nfunction csrfPart(): string {\n return `\n document.addEventListener('htmx:configRequest',function(e){\n var meta=document.querySelector('meta[name=\"csrf-token\"]');\n if(meta){ e.detail.headers['x-csrf-token']=meta.getAttribute('content'); }\n });`;\n}\n\n/** Theme menu: apply, persist, and mark the current choice. */\nfunction themePart(): string {\n return `\n function markTheme(){\n var cur=document.documentElement.getAttribute('data-theme')||'system';\n document.querySelectorAll('[data-theme-set]').forEach(function(item){\n item.setAttribute('aria-current', item.getAttribute('data-theme-set')===cur ? 'true' : 'false');\n });\n }\n inits.push(markTheme);\n document.addEventListener('click',function(e){\n var item=e.target instanceof Element ? e.target.closest('[data-theme-set]') : null;\n if(!item) return;\n var next=item.getAttribute('data-theme-set');\n shell.applyTheme(next);\n shell.writeCookie(shell.themeCookie,next);\n markTheme();\n });`;\n}\n\n/** Desktop rail collapse (persisted) and the mobile drawer. */\nfunction sidebarPart(): string {\n return `\n function setDrawer(open){\n var sidebar=document.getElementById('sidebar');\n if(!sidebar) return;\n sidebar.classList.toggle('sidebar--open',open);\n var backdrop=document.querySelector('[data-sidebar-backdrop]');\n if(backdrop){ backdrop.hidden=!open; }\n var btn=document.querySelector('[data-sidebar-toggle]');\n if(btn){ btn.setAttribute('aria-expanded',open?'true':'false'); }\n }\n document.addEventListener('click',function(e){\n var t=e.target instanceof Element ? e.target : null;\n if(!t) return;\n if(t.closest('[data-sidebar-toggle]')){\n var sidebar=document.getElementById('sidebar');\n setDrawer(!(sidebar&&sidebar.classList.contains('sidebar--open')));\n } else if(t.closest('[data-sidebar-backdrop]')){\n setDrawer(false);\n } else if(t.closest('[data-sidebar-collapse]')){\n var next=document.documentElement.getAttribute('data-sidebar')==='rail' ? 'full' : 'rail';\n shell.applySidebar(next);\n shell.writeCookie(shell.sidebarCookie,next);\n }\n });\n document.addEventListener('keydown',function(e){\n var sidebar=document.getElementById('sidebar');\n if(e.key==='Escape'&&sidebar&&sidebar.classList.contains('sidebar--open')){\n setDrawer(false);\n var btn=document.querySelector('[data-sidebar-toggle]');\n if(btn) btn.focus();\n }\n });`;\n}\n\n/** Native <details> dropdowns: close on outside click, Escape, or choosing an item. */\nfunction dropdownPart(): string {\n return `\n document.addEventListener('click',function(e){\n var target=e.target instanceof Element ? e.target : null;\n var owner=target ? target.closest('details[data-dropdown]') : null;\n document.querySelectorAll('details[data-dropdown][open]').forEach(function(d){\n if(d!==owner) d.removeAttribute('open');\n });\n if(owner&&target&&target.closest('[role=\"menuitem\"]')){ owner.removeAttribute('open'); }\n });\n document.addEventListener('keydown',function(e){\n if(e.key!=='Escape') return;\n var open=document.querySelector('details[data-dropdown][open]');\n if(!open) return;\n open.removeAttribute('open');\n var summary=open.querySelector('summary');\n if(summary) summary.focus();\n });`;\n}\n\n/** Build and show one toast element (text only, never HTML). */\nfunction toastRenderPart(): string {\n return `\n var toastIcons={success:'check-circle',danger:'x-circle',warning:'alert-triangle',info:'info'};\n function toastIcon(tone){\n var ns='http://www.w3.org/2000/svg';\n var svg=document.createElementNS(ns,'svg');\n var attrs={viewBox:'0 0 24 24',width:'18',height:'18','aria-hidden':'true',fill:'none',\n stroke:'currentColor','stroke-width':'2','stroke-linecap':'round','stroke-linejoin':'round'};\n Object.keys(attrs).forEach(function(name){ svg.setAttribute(name,attrs[name]); });\n var use=document.createElementNS(ns,'use');\n use.setAttribute('href','${iconSpriteHref}#i-'+toastIcons[tone]);\n svg.appendChild(use);\n return svg;\n }\n function toast(detail){\n var region=document.querySelector('[data-toast-region]');\n if(!region||!detail||!detail.message) return;\n var tone=toastIcons[detail.tone] ? detail.tone : 'info';\n var el=document.createElement('div');\n el.className='toast';\n el.setAttribute('data-tone',tone);\n el.setAttribute('role',tone==='danger'?'alert':'status');\n var text=document.createElement('span');\n text.textContent=String(detail.message);\n el.appendChild(toastIcon(tone));\n el.appendChild(text);\n region.appendChild(el);\n setTimeout(function(){ el.remove(); },tone==='danger'?8000:4000);\n }`;\n}\n\n/**\n * Toasts. Sources: an HX-Trigger(-After-Swap) \"showToast\" event, and a toast\n * queued in sessionStorage by a form with data-toast (review actions reload\n * or redirect, so the message has to survive the navigation).\n */\nfunction toastPart(): string {\n return `${toastRenderPart()}\n var TOAST_KEY='ss_toast';\n function queueToast(detail){ try{ sessionStorage.setItem(TOAST_KEY,JSON.stringify(detail)); }catch(_){} }\n function clearToast(){ try{ sessionStorage.removeItem(TOAST_KEY); }catch(_){} }\n function flushQueued(){\n try{\n var raw=sessionStorage.getItem(TOAST_KEY);\n if(!raw) return;\n sessionStorage.removeItem(TOAST_KEY);\n toast(JSON.parse(raw));\n }catch(_){}\n }\n function flushFlash(){\n var raw=shell.readCookie('storyshelf_flash');\n if(!raw) return;\n document.cookie='storyshelf_flash=; Path=/; Max-Age=0; SameSite=Lax';\n try{ toast(JSON.parse(raw)); }catch(_){}\n }\n function flushToast(){ flushQueued(); flushFlash(); }\n inits.push(flushToast);\n document.addEventListener('htmx:beforeRequest',function(e){\n var form=e.detail && e.detail.elt && e.detail.elt.closest ? e.detail.elt.closest('[data-toast]') : null;\n if(form){ queueToast({message:form.getAttribute('data-toast'),tone:form.getAttribute('data-toast-tone')||'success'}); }\n });\n function failed(){ clearToast(); toast({message:'Something went wrong. Please try again.',tone:'danger'}); }\n document.addEventListener('htmx:responseError',failed);\n document.addEventListener('htmx:sendError',failed);`;\n}\n\n/**\n * Forms and body swaps. HTMX does not swap 4xx responses, but this app answers validation\n * failures with the re-rendered page (400/409/422) so field errors show.\n * \\`data-confirm\\` asks before submitting plain (non-HTMX) forms.\n */\nfunction formPart(): string {\n return `\n document.addEventListener('htmx:beforeSwap',function(e){\n // A body swap only replaces <body>, so classes first used by the new page\n // would be unstyled: adopt the stylesheet the server collected for it.\n var match=/<style id=\"storyshelf-css\">([\\\\s\\\\S]*?)<\\\\/style>/.exec(e.detail.serverResponse||'');\n var sheet=document.getElementById('storyshelf-css');\n if(match&&sheet&&e.detail.target===document.body){ sheet.textContent=match[1]; }\n var status=e.detail.xhr ? e.detail.xhr.status : 0;\n if(status===400||status===409||status===422){\n e.detail.shouldSwap=true;\n e.detail.isError=false;\n clearToast();\n }\n });\n document.addEventListener('submit',function(e){\n var form=e.target instanceof Element ? e.target.closest('form[data-confirm]') : null;\n if(form&&!window.confirm(form.getAttribute('data-confirm')||'Are you sure?')){ e.preventDefault(); }\n });`;\n}\n\n/** Copy buttons: `data-copy` holds the text; confirm with a toast. */\nfunction copyPart(): string {\n return `\n document.addEventListener('click',function(e){\n var btn=e.target instanceof Element ? e.target.closest('[data-copy]') : null;\n if(!btn||!navigator.clipboard) return;\n navigator.clipboard.writeText(btn.getAttribute('data-copy')||'').then(function(){\n toast({message:'Copied to clipboard',tone:'success'});\n },function(){\n toast({message:'Could not copy',tone:'danger'});\n });\n });`;\n}\n\n/** Client-side list filtering for `[data-filter-input]` boxes. */\nfunction filterPart(): string {\n return `\n document.addEventListener('input',function(e){\n var input=e.target instanceof Element ? e.target.closest('[data-filter-input]') : null;\n if(!input) return;\n var scope=input.closest('[data-filter-scope]')||document;\n var query=input.value.trim().toLowerCase();\n var shown=0;\n scope.querySelectorAll('[data-filter-item]').forEach(function(item){\n var match=!query||(item.getAttribute('data-filter-text')||'').toLowerCase().indexOf(query)>-1;\n item.hidden=!match;\n if(match) shown++;\n });\n scope.querySelectorAll('[data-filter-empty]').forEach(function(el){ el.hidden=shown>0; });\n });`;\n}\n\n/** Thin top progress bar while an HTMX request (e.g. snapshot navigation) is in flight. */\nfunction loadingPart(): string {\n return `\n var pending=0;\n function setLoading(delta){\n pending=Math.max(0,pending+delta);\n document.documentElement.classList.toggle('ss-loading',pending>0);\n }\n document.addEventListener('htmx:beforeRequest',function(){ setLoading(1); });\n document.addEventListener('htmx:afterRequest',function(){ setLoading(-1); });`;\n}\n\n/** Keep keyboard focus sensible after HTMX swaps. */\nfunction focusPart(): string {\n return `\n document.addEventListener('htmx:afterSwap',function(e){\n var target=e.detail && e.detail.target;\n if(target&&target.querySelector&&target!==document.body){\n var focusable=target.querySelector('[autofocus], input, select, textarea, button');\n if(focusable) try{ focusable.focus(); }catch(_){}\n }\n });`;\n}\n\n/** Page-end script wiring up the shell's interactive behavior (idempotent). */\nexport function clientScript(): string {\n const parts = [\n csrfPart(),\n themePart(),\n sidebarPart(),\n dropdownPart(),\n toastPart(),\n formPart(),\n copyPart(),\n filterPart(),\n loadingPart(),\n focusPart(),\n reviewScript(),\n ];\n return `\n (function(){\n if(window.__ssInit){ window.__ssInit(); return; }\n var shell=window.__storyshelfShell;\n if(!shell) return;\n var inits=[];${parts.join(\"\\n\")}\n window.__ssInit=function(){ inits.forEach(function(fn){ fn(); }); };\n window.__ssInit();\n })();\n `.trim();\n}\n"],"mappings":";;;;;;;AAWA,SAAS,WAAmB;CAC1B,OAAO;;;;;AAKT;;AAGA,SAAS,YAAoB;CAC3B,OAAO;;;;;;;;;;;;;;;;AAgBT;;AAGA,SAAS,cAAsB;CAC7B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCT;;AAGA,SAAS,eAAuB;CAC9B,OAAO;;;;;;;;;;;;;;;;;AAiBT;;AAGA,SAAS,kBAA0B;CACjC,OAAO;;;;;;;;;iCASwB,eAAe;;;;;;;;;;;;;;;;;;;AAmBhD;;;;;;AAOA,SAAS,YAAoB;CAC3B,OAAO,GAAG,gBAAgB,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2B9B;;;;;;AAOA,SAAS,WAAmB;CAC1B,OAAO;;;;;;;;;;;;;;;;;;AAkBT;;AAGA,SAAS,WAAmB;CAC1B,OAAO;;;;;;;;;;AAUT;;AAGA,SAAS,aAAqB;CAC5B,OAAO;;;;;;;;;;;;;;AAcT;;AAGA,SAAS,cAAsB;CAC7B,OAAO;;;;;;;;AAQT;;AAGA,SAAS,YAAoB;CAC3B,OAAO;;;;;;;;AAQT;;AAGA,SAAgB,eAAuB;CAcrC,OAAO;;;;;mBAKU;EAjBf,SAAS;EACT,UAAU;EACV,YAAY;EACZ,aAAa;EACb,UAAU;EACV,SAAS;EACT,SAAS;EACT,WAAW;EACX,YAAY;EACZ,UAAU;EACV,aAAa;CAOM,CAAC,CAAC,KAAK,IAAI,EAAE;;;;IAIhC,KAAK;AACT"}
|
|
1
|
+
{"version":3,"file":"client-script.mjs","names":[],"sources":["../../../src/ui/shell/client-script.ts"],"sourcesContent":["import { iconSpriteHref } from \"../icons/sprite.ts\";\nimport { reviewScript } from \"./review-script.ts\";\n\n/*\n * The page-end script is re-executed whenever HTMX swaps `body` (review\n * navigation does). So it is split in two: listeners are bound ONCE on\n * `document` and look elements up lazily; `inits` are re-run per render for\n * anything that needs wiring to freshly swapped elements.\n */\n\n/** Attach the CSRF token to every HTMX request. */\nfunction csrfPart(): string {\n return `\n document.addEventListener('htmx:configRequest',function(e){\n var meta=document.querySelector('meta[name=\"csrf-token\"]');\n if(meta){ e.detail.headers['x-csrf-token']=meta.getAttribute('content'); }\n });`;\n}\n\n/**\n * System page: report the origin the admin opened it with, so a deployment learns its public URL\n * without a redeploy (ADR 0007, decision 6). The server keeps the first report and answers 201 only\n * when it stored a new one, which is when the page reloads to show it.\n */\nfunction originPart(): string {\n return `\n function reportOrigin(){\n var el=document.querySelector('[data-report-origin]');\n if(!el || el.getAttribute('data-reported')) return;\n el.setAttribute('data-reported','1');\n var meta=document.querySelector('meta[name=\"csrf-token\"]');\n fetch(el.getAttribute('data-report-origin'),{\n method:'POST',\n credentials:'same-origin',\n headers:{'content-type':'application/json','x-csrf-token':meta ? meta.getAttribute('content') : ''},\n body:JSON.stringify({origin:location.origin})\n }).then(function(r){ if(r.status===201) location.reload(); }).catch(function(){});\n }\n inits.push(reportOrigin);`;\n}\n\n/** Theme menu: apply, persist, and mark the current choice. */\nfunction themePart(): string {\n return `\n function markTheme(){\n var cur=document.documentElement.getAttribute('data-theme')||'system';\n document.querySelectorAll('[data-theme-set]').forEach(function(item){\n item.setAttribute('aria-current', item.getAttribute('data-theme-set')===cur ? 'true' : 'false');\n });\n }\n inits.push(markTheme);\n document.addEventListener('click',function(e){\n var item=e.target instanceof Element ? e.target.closest('[data-theme-set]') : null;\n if(!item) return;\n var next=item.getAttribute('data-theme-set');\n shell.applyTheme(next);\n shell.writeCookie(shell.themeCookie,next);\n markTheme();\n });`;\n}\n\n/** Desktop rail collapse (persisted) and the mobile drawer. */\nfunction sidebarPart(): string {\n return `\n function setDrawer(open){\n var sidebar=document.getElementById('sidebar');\n if(!sidebar) return;\n sidebar.classList.toggle('sidebar--open',open);\n var backdrop=document.querySelector('[data-sidebar-backdrop]');\n if(backdrop){ backdrop.hidden=!open; }\n var btn=document.querySelector('[data-sidebar-toggle]');\n if(btn){ btn.setAttribute('aria-expanded',open?'true':'false'); }\n }\n document.addEventListener('click',function(e){\n var t=e.target instanceof Element ? e.target : null;\n if(!t) return;\n if(t.closest('[data-sidebar-toggle]')){\n var sidebar=document.getElementById('sidebar');\n setDrawer(!(sidebar&&sidebar.classList.contains('sidebar--open')));\n } else if(t.closest('[data-sidebar-backdrop]')){\n setDrawer(false);\n } else if(t.closest('[data-sidebar-collapse]')){\n var next=document.documentElement.getAttribute('data-sidebar')==='rail' ? 'full' : 'rail';\n shell.applySidebar(next);\n shell.writeCookie(shell.sidebarCookie,next);\n }\n });\n document.addEventListener('keydown',function(e){\n var sidebar=document.getElementById('sidebar');\n if(e.key==='Escape'&&sidebar&&sidebar.classList.contains('sidebar--open')){\n setDrawer(false);\n var btn=document.querySelector('[data-sidebar-toggle]');\n if(btn) btn.focus();\n }\n });`;\n}\n\n/** Native <details> dropdowns: close on outside click, Escape, or choosing an item. */\nfunction dropdownPart(): string {\n return `\n document.addEventListener('click',function(e){\n var target=e.target instanceof Element ? e.target : null;\n var owner=target ? target.closest('details[data-dropdown]') : null;\n document.querySelectorAll('details[data-dropdown][open]').forEach(function(d){\n if(d!==owner) d.removeAttribute('open');\n });\n if(owner&&target&&target.closest('[role=\"menuitem\"]')){ owner.removeAttribute('open'); }\n });\n document.addEventListener('keydown',function(e){\n if(e.key!=='Escape') return;\n var open=document.querySelector('details[data-dropdown][open]');\n if(!open) return;\n open.removeAttribute('open');\n var summary=open.querySelector('summary');\n if(summary) summary.focus();\n });`;\n}\n\n/** Build and show one toast element (text only, never HTML). */\nfunction toastRenderPart(): string {\n return `\n var toastIcons={success:'check-circle',danger:'x-circle',warning:'alert-triangle',info:'info'};\n function toastIcon(tone){\n var ns='http://www.w3.org/2000/svg';\n var svg=document.createElementNS(ns,'svg');\n var attrs={viewBox:'0 0 24 24',width:'18',height:'18','aria-hidden':'true',fill:'none',\n stroke:'currentColor','stroke-width':'2','stroke-linecap':'round','stroke-linejoin':'round'};\n Object.keys(attrs).forEach(function(name){ svg.setAttribute(name,attrs[name]); });\n var use=document.createElementNS(ns,'use');\n use.setAttribute('href','${iconSpriteHref}#i-'+toastIcons[tone]);\n svg.appendChild(use);\n return svg;\n }\n function toast(detail){\n var region=document.querySelector('[data-toast-region]');\n if(!region||!detail||!detail.message) return;\n var tone=toastIcons[detail.tone] ? detail.tone : 'info';\n var el=document.createElement('div');\n el.className='toast';\n el.setAttribute('data-tone',tone);\n el.setAttribute('role',tone==='danger'?'alert':'status');\n var text=document.createElement('span');\n text.textContent=String(detail.message);\n el.appendChild(toastIcon(tone));\n el.appendChild(text);\n region.appendChild(el);\n setTimeout(function(){ el.remove(); },tone==='danger'?8000:4000);\n }`;\n}\n\n/**\n * Toasts. Sources: an HX-Trigger(-After-Swap) \"showToast\" event, and a toast\n * queued in sessionStorage by a form with data-toast (review actions reload\n * or redirect, so the message has to survive the navigation).\n */\nfunction toastPart(): string {\n return `${toastRenderPart()}\n var TOAST_KEY='ss_toast';\n function queueToast(detail){ try{ sessionStorage.setItem(TOAST_KEY,JSON.stringify(detail)); }catch(_){} }\n function clearToast(){ try{ sessionStorage.removeItem(TOAST_KEY); }catch(_){} }\n function flushQueued(){\n try{\n var raw=sessionStorage.getItem(TOAST_KEY);\n if(!raw) return;\n sessionStorage.removeItem(TOAST_KEY);\n toast(JSON.parse(raw));\n }catch(_){}\n }\n function flushFlash(){\n var raw=shell.readCookie('storyshelf_flash');\n if(!raw) return;\n document.cookie='storyshelf_flash=; Path=/; Max-Age=0; SameSite=Lax';\n try{ toast(JSON.parse(raw)); }catch(_){}\n }\n function flushToast(){ flushQueued(); flushFlash(); }\n inits.push(flushToast);\n document.addEventListener('htmx:beforeRequest',function(e){\n var form=e.detail && e.detail.elt && e.detail.elt.closest ? e.detail.elt.closest('[data-toast]') : null;\n if(form){ queueToast({message:form.getAttribute('data-toast'),tone:form.getAttribute('data-toast-tone')||'success'}); }\n });\n function failed(){ clearToast(); toast({message:'Something went wrong. Please try again.',tone:'danger'}); }\n document.addEventListener('htmx:responseError',failed);\n document.addEventListener('htmx:sendError',failed);`;\n}\n\n/**\n * Forms and body swaps. HTMX does not swap 4xx responses, but this app answers validation\n * failures with the re-rendered page (400/409/422) so field errors show.\n * \\`data-confirm\\` asks before submitting plain (non-HTMX) forms.\n */\nfunction formPart(): string {\n return `\n document.addEventListener('htmx:beforeSwap',function(e){\n // A body swap only replaces <body>, so classes first used by the new page\n // would be unstyled: adopt the stylesheet the server collected for it.\n var match=/<style id=\"storyshelf-css\">([\\\\s\\\\S]*?)<\\\\/style>/.exec(e.detail.serverResponse||'');\n var sheet=document.getElementById('storyshelf-css');\n if(match&&sheet&&e.detail.target===document.body){ sheet.textContent=match[1]; }\n var status=e.detail.xhr ? e.detail.xhr.status : 0;\n if(status===400||status===409||status===422){\n e.detail.shouldSwap=true;\n e.detail.isError=false;\n clearToast();\n }\n });\n document.addEventListener('submit',function(e){\n var form=e.target instanceof Element ? e.target.closest('form[data-confirm]') : null;\n if(form&&!window.confirm(form.getAttribute('data-confirm')||'Are you sure?')){ e.preventDefault(); }\n });`;\n}\n\n/** Copy buttons: `data-copy` holds the text; confirm with a toast. */\nfunction copyPart(): string {\n return `\n document.addEventListener('click',function(e){\n var btn=e.target instanceof Element ? e.target.closest('[data-copy]') : null;\n if(!btn||!navigator.clipboard) return;\n navigator.clipboard.writeText(btn.getAttribute('data-copy')||'').then(function(){\n toast({message:'Copied to clipboard',tone:'success'});\n },function(){\n toast({message:'Could not copy',tone:'danger'});\n });\n });`;\n}\n\n/** Client-side list filtering for `[data-filter-input]` boxes. */\nfunction filterPart(): string {\n return `\n document.addEventListener('input',function(e){\n var input=e.target instanceof Element ? e.target.closest('[data-filter-input]') : null;\n if(!input) return;\n var scope=input.closest('[data-filter-scope]')||document;\n var query=input.value.trim().toLowerCase();\n var shown=0;\n scope.querySelectorAll('[data-filter-item]').forEach(function(item){\n var match=!query||(item.getAttribute('data-filter-text')||'').toLowerCase().indexOf(query)>-1;\n item.hidden=!match;\n if(match) shown++;\n });\n scope.querySelectorAll('[data-filter-empty]').forEach(function(el){ el.hidden=shown>0; });\n });`;\n}\n\n/**\n * Click-to-load story previews: move the inert frame out of its template (nothing runs before). A\n * frame under a signed URL first asks for a fresh root, so a page left open for hours does not mount\n * an expired token; it keeps the rendered one when that request fails. Afterwards the root is renewed\n * every `data-preview-refresh-seconds`; a failed renewal is retried a few times a minute apart.\n */\nfunction previewPart(): string {\n return `\n document.addEventListener('click',function(e){\n var btn=e.target instanceof Element ? e.target.closest('[data-preview-load]') : null;\n var host=btn ? btn.closest('[data-preview-embed]') : null;\n var tpl=host ? host.querySelector('template[data-preview-template]') : null;\n if(!host||!tpl||host.getAttribute('data-preview-loading')) return;\n host.setAttribute('data-preview-loading','1');\n var idle=host.querySelector('[data-preview-idle]');\n var path=host.getAttribute('data-preview-path');\n var fresh=fetchPreviewBase(host).catch(function(){ return null; });\n fresh.then(function(base){\n var content=document.importNode(tpl.content,true);\n var frame=content.querySelector('iframe');\n if(frame&&base&&path) frame.src=base+path;\n if(idle) idle.hidden=true;\n host.appendChild(content);\n tpl.remove();\n keepPreviewFresh(host);\n });\n });`;\n}\n\n/** Fetch a fresh signed root for a preview host and keep it renewed (see {@link previewPart}). */\nfunction previewRefreshPart(): string {\n return `\n function fetchPreviewBase(host){\n var url=host.getAttribute('data-preview-refresh-url');\n if(!url) return Promise.resolve(null);\n return fetch(url,{headers:{accept:'application/json'}}).then(function(r){\n if(!r.ok) throw new Error('preview-token');\n return r.json();\n }).then(function(body){ return body&&body.base ? body.base : null; });\n }\n function keepPreviewFresh(host){\n var url=host.getAttribute('data-preview-refresh-url');\n var path=host.getAttribute('data-preview-path');\n var seconds=Number(host.getAttribute('data-preview-refresh-seconds'));\n if(!url||!path||!(seconds>0)) return;\n var RETRY_MS=60000, MAX_RETRIES=3;\n function renew(tries){\n var frame=host.querySelector('iframe');\n if(!host.isConnected||!frame) return;\n fetchPreviewBase(host).then(function(base){\n if(base) frame.src=base+path;\n else throw new Error('preview-token');\n }).catch(function(){\n if(tries<MAX_RETRIES) setTimeout(function(){ renew(tries+1); },RETRY_MS);\n });\n }\n var timer=setInterval(function(){\n if(!host.isConnected||!host.querySelector('iframe')){ clearInterval(timer); return; }\n renew(0);\n },seconds*1000);\n }`;\n}\n\n/** Thin top progress bar while an HTMX request (e.g. snapshot navigation) is in flight. */\nfunction loadingPart(): string {\n return `\n var pending=0;\n function setLoading(delta){\n pending=Math.max(0,pending+delta);\n document.documentElement.classList.toggle('ss-loading',pending>0);\n }\n document.addEventListener('htmx:beforeRequest',function(){ setLoading(1); });\n document.addEventListener('htmx:afterRequest',function(){ setLoading(-1); });`;\n}\n\n/** Keep keyboard focus sensible after HTMX swaps. */\nfunction focusPart(): string {\n return `\n document.addEventListener('htmx:afterSwap',function(e){\n var target=e.detail && e.detail.target;\n if(target&&target.querySelector&&target!==document.body){\n var focusable=target.querySelector('[autofocus], input, select, textarea, button');\n if(focusable) try{ focusable.focus(); }catch(_){}\n }\n });`;\n}\n\n/** Page-end script wiring up the shell's interactive behavior (idempotent). */\nexport function clientScript(): string {\n const parts = [\n csrfPart(),\n originPart(),\n themePart(),\n sidebarPart(),\n dropdownPart(),\n toastPart(),\n formPart(),\n copyPart(),\n filterPart(),\n previewPart(),\n previewRefreshPart(),\n loadingPart(),\n focusPart(),\n reviewScript(),\n ];\n return `\n (function(){\n if(window.__ssInit){ window.__ssInit(); return; }\n var shell=window.__storyshelfShell;\n if(!shell) return;\n var inits=[];${parts.join(\"\\n\")}\n window.__ssInit=function(){ inits.forEach(function(fn){ fn(); }); };\n window.__ssInit();\n })();\n `.trim();\n}\n"],"mappings":";;;;;;;AAWA,SAAS,WAAmB;CAC1B,OAAO;;;;;AAKT;;;;;;AAOA,SAAS,aAAqB;CAC5B,OAAO;;;;;;;;;;;;;;AAcT;;AAGA,SAAS,YAAoB;CAC3B,OAAO;;;;;;;;;;;;;;;;AAgBT;;AAGA,SAAS,cAAsB;CAC7B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCT;;AAGA,SAAS,eAAuB;CAC9B,OAAO;;;;;;;;;;;;;;;;;AAiBT;;AAGA,SAAS,kBAA0B;CACjC,OAAO;;;;;;;;;iCASwB,eAAe;;;;;;;;;;;;;;;;;;;AAmBhD;;;;;;AAOA,SAAS,YAAoB;CAC3B,OAAO,GAAG,gBAAgB,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2B9B;;;;;;AAOA,SAAS,WAAmB;CAC1B,OAAO;;;;;;;;;;;;;;;;;;AAkBT;;AAGA,SAAS,WAAmB;CAC1B,OAAO;;;;;;;;;;AAUT;;AAGA,SAAS,aAAqB;CAC5B,OAAO;;;;;;;;;;;;;;AAcT;;;;;;;AAQA,SAAS,cAAsB;CAC7B,OAAO;;;;;;;;;;;;;;;;;;;;AAoBT;;AAGA,SAAS,qBAA6B;CACpC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BT;;AAGA,SAAS,cAAsB;CAC7B,OAAO;;;;;;;;AAQT;;AAGA,SAAS,YAAoB;CAC3B,OAAO;;;;;;;;AAQT;;AAGA,SAAgB,eAAuB;CAiBrC,OAAO;;;;;mBAKU;EApBf,SAAS;EACT,WAAW;EACX,UAAU;EACV,YAAY;EACZ,aAAa;EACb,UAAU;EACV,SAAS;EACT,SAAS;EACT,WAAW;EACX,YAAY;EACZ,mBAAmB;EACnB,YAAY;EACZ,UAAU;EACV,aAAa;CAOM,CAAC,CAAC,KAAK,IAAI,EAAE;;;;IAIhC,KAAK;AACT"}
|
package/dist/ui/shell/nav.mjs
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
|
|
4
|
+
// src/ui/shell/nav.ts
|
|
5
|
+
/**
|
|
6
|
+
* Entries shown for the current project. Entries for `testing` routes (Jobs) drop out on a
|
|
7
|
+
* hosting-only instance, where those routes answer 404.
|
|
8
|
+
*/
|
|
9
|
+
function projectNavItems(urls, slug, testing = true) {
|
|
10
|
+
const jobs = {
|
|
11
|
+
key: "jobs",
|
|
12
|
+
label: "Jobs",
|
|
13
|
+
href: urls.jobs(slug),
|
|
14
|
+
icon: "activity"
|
|
15
|
+
};
|
|
7
16
|
return [
|
|
8
17
|
{
|
|
9
18
|
key: "library",
|
|
@@ -17,12 +26,7 @@ function projectNavItems(urls, slug) {
|
|
|
17
26
|
href: urls.buildsList(slug),
|
|
18
27
|
icon: "layers"
|
|
19
28
|
},
|
|
20
|
-
|
|
21
|
-
key: "jobs",
|
|
22
|
-
label: "Jobs",
|
|
23
|
-
href: urls.jobs(slug),
|
|
24
|
-
icon: "activity"
|
|
25
|
-
},
|
|
29
|
+
...testing ? [jobs] : [],
|
|
26
30
|
{
|
|
27
31
|
key: "labels",
|
|
28
32
|
label: "Labels",
|
|
@@ -105,7 +109,6 @@ function sectionCrumb(nav, urls) {
|
|
|
105
109
|
}
|
|
106
110
|
}[nav?.active ?? ""] ?? null;
|
|
107
111
|
}
|
|
108
|
-
//#endregion
|
|
109
112
|
export { adminNavItems, breadcrumbs, developerNavItems, projectNavItems };
|
|
110
113
|
|
|
111
114
|
//# sourceMappingURL=nav.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nav.mjs","names":[],"sources":["../../../src/ui/shell/nav.ts"],"sourcesContent":["import type { IconName } from \"../icons/paths.ts\";\n\n/** Which nav entry is active and which project (if any) the page belongs to. */\nexport interface NavConfig {\n active?: string;\n projectSlug?: string;\n projectName?: string;\n /** Levels below the section, ending with the current page (e.g. a build, then \"Review\"). */\n trail?: NavCrumb[];\n}\n\n/** One breadcrumb level; the last trail item is the current page (it needs no `href`). */\nexport interface NavCrumb {\n label: string;\n href?: string;\n}\n\n/** One sidebar entry. */\nexport interface NavItem {\n key: string;\n label: string;\n href: string;\n icon: IconName;\n /** Opens in a new tab (raw API output). */\n external?: boolean;\n}\n\n/** Subset of the URL builder the nav needs (keeps this module pure). */\nexport interface NavUrls {\n projects(): string;\n library(slug: string): string;\n buildsList(slug: string): string;\n jobs(slug: string): string;\n labels(slug: string): string;\n settings(slug: string): string;\n}\n\n
|
|
1
|
+
{"version":3,"file":"nav.mjs","names":[],"sources":["../../../src/ui/shell/nav.ts"],"sourcesContent":["import type { IconName } from \"../icons/paths.ts\";\n\n/** Which nav entry is active and which project (if any) the page belongs to. */\nexport interface NavConfig {\n active?: string;\n projectSlug?: string;\n projectName?: string;\n /** Levels below the section, ending with the current page (e.g. a build, then \"Review\"). */\n trail?: NavCrumb[];\n}\n\n/** One breadcrumb level; the last trail item is the current page (it needs no `href`). */\nexport interface NavCrumb {\n label: string;\n href?: string;\n}\n\n/** One sidebar entry. */\nexport interface NavItem {\n key: string;\n label: string;\n href: string;\n icon: IconName;\n /** Opens in a new tab (raw API output). */\n external?: boolean;\n}\n\n/** Subset of the URL builder the nav needs (keeps this module pure). */\nexport interface NavUrls {\n projects(): string;\n library(slug: string): string;\n buildsList(slug: string): string;\n jobs(slug: string): string;\n labels(slug: string): string;\n settings(slug: string): string;\n}\n\n/**\n * Entries shown for the current project. Entries for `testing` routes (Jobs) drop out on a\n * hosting-only instance, where those routes answer 404.\n */\nexport function projectNavItems(urls: NavUrls, slug: string, testing = true): NavItem[] {\n const jobs: NavItem = { key: \"jobs\", label: \"Jobs\", href: urls.jobs(slug), icon: \"activity\" };\n return [\n { key: \"library\", label: \"Library\", href: urls.library(slug), icon: \"image\" },\n { key: \"builds\", label: \"Builds\", href: urls.buildsList(slug), icon: \"layers\" },\n ...(testing ? [jobs] : []),\n { key: \"labels\", label: \"Labels\", href: urls.labels(slug), icon: \"tag\" },\n { key: \"settings\", label: \"Settings\", href: urls.settings(slug), icon: \"settings\" },\n ];\n}\n\n/** Developer entries (API reference). */\nexport function developerNavItems(): NavItem[] {\n return [\n { key: \"api-docs\", label: \"API docs\", href: \"/api/v1/docs\", icon: \"book\" },\n {\n key: \"openapi\",\n label: \"OpenAPI spec\",\n href: \"/api/v1/openapi.json\",\n icon: \"code\",\n external: true,\n },\n ];\n}\n\n/** Administration entries (admins only). */\nexport function adminNavItems(): NavItem[] {\n return [{ key: \"admin\", label: \"System\", href: \"/admin\", icon: \"server\" }];\n}\n\n/**\n * Breadcrumbs after the project picker: the *ancestors* of the current page.\n * The page itself is named by its heading, so it is never repeated here —\n * the section plus `nav.trail` describe where the page is, and the last item\n * (the current page) is dropped. Every returned crumb links somewhere, and\n * the last one is the page's parent.\n */\nexport function breadcrumbs(nav: NavConfig | undefined, urls: NavUrls): NavCrumb[] {\n const section = sectionCrumb(nav, urls);\n const path = section ? [section, ...(nav?.trail ?? [])] : [...(nav?.trail ?? [])];\n return path.slice(0, -1).filter((crumb) => crumb.href !== undefined);\n}\n\nfunction sectionCrumb(nav: NavConfig | undefined, urls: NavUrls): NavCrumb | null {\n const slug = nav?.projectSlug;\n const table: Record<string, NavCrumb | undefined> = {\n library: slug ? { label: \"Library\", href: urls.library(slug) } : undefined,\n builds: slug ? { label: \"Builds\", href: urls.buildsList(slug) } : undefined,\n jobs: slug ? { label: \"Jobs\", href: urls.jobs(slug) } : undefined,\n labels: slug ? { label: \"Labels\", href: urls.labels(slug) } : undefined,\n settings: slug ? { label: \"Settings\", href: urls.settings(slug) } : undefined,\n admin: { label: \"System\", href: \"/admin\" },\n profile: { label: \"Profile\", href: \"/profile\" },\n };\n return table[nav?.active ?? \"\"] ?? null;\n}\n"],"mappings":";;;;;;;;AAyCA,SAAgB,gBAAgB,MAAe,MAAc,UAAU,MAAiB;CACtF,MAAM,OAAgB;EAAE,KAAK;EAAQ,OAAO;EAAQ,MAAM,KAAK,KAAK,IAAI;EAAG,MAAM;CAAW;CAC5F,OAAO;EACL;GAAE,KAAK;GAAW,OAAO;GAAW,MAAM,KAAK,QAAQ,IAAI;GAAG,MAAM;EAAQ;EAC5E;GAAE,KAAK;GAAU,OAAO;GAAU,MAAM,KAAK,WAAW,IAAI;GAAG,MAAM;EAAS;EAC9E,GAAI,UAAU,CAAC,IAAI,IAAI,CAAC;EACxB;GAAE,KAAK;GAAU,OAAO;GAAU,MAAM,KAAK,OAAO,IAAI;GAAG,MAAM;EAAM;EACvE;GAAE,KAAK;GAAY,OAAO;GAAY,MAAM,KAAK,SAAS,IAAI;GAAG,MAAM;EAAW;CACpF;AACF;;AAGA,SAAgB,oBAA+B;CAC7C,OAAO,CACL;EAAE,KAAK;EAAY,OAAO;EAAY,MAAM;EAAgB,MAAM;CAAO,GACzE;EACE,KAAK;EACL,OAAO;EACP,MAAM;EACN,MAAM;EACN,UAAU;CACZ,CACF;AACF;;AAGA,SAAgB,gBAA2B;CACzC,OAAO,CAAC;EAAE,KAAK;EAAS,OAAO;EAAU,MAAM;EAAU,MAAM;CAAS,CAAC;AAC3E;;;;;;;;AASA,SAAgB,YAAY,KAA4B,MAA2B;CACjF,MAAM,UAAU,aAAa,KAAK,IAAI;CAEtC,QADa,UAAU,CAAC,SAAS,GAAI,KAAK,SAAS,CAAC,CAAE,IAAI,CAAC,GAAI,KAAK,SAAS,CAAC,CAAE,EAAA,CACpE,MAAM,GAAG,EAAE,CAAC,CAAC,QAAQ,UAAU,MAAM,SAAS,KAAA,CAAS;AACrE;AAEA,SAAS,aAAa,KAA4B,MAAgC;CAChF,MAAM,OAAO,KAAK;CAUlB,OAAO;EARL,SAAS,OAAO;GAAE,OAAO;GAAW,MAAM,KAAK,QAAQ,IAAI;EAAE,IAAI,KAAA;EACjE,QAAQ,OAAO;GAAE,OAAO;GAAU,MAAM,KAAK,WAAW,IAAI;EAAE,IAAI,KAAA;EAClE,MAAM,OAAO;GAAE,OAAO;GAAQ,MAAM,KAAK,KAAK,IAAI;EAAE,IAAI,KAAA;EACxD,QAAQ,OAAO;GAAE,OAAO;GAAU,MAAM,KAAK,OAAO,IAAI;EAAE,IAAI,KAAA;EAC9D,UAAU,OAAO;GAAE,OAAO;GAAY,MAAM,KAAK,SAAS,IAAI;EAAE,IAAI,KAAA;EACpE,OAAO;GAAE,OAAO;GAAU,MAAM;EAAS;EACzC,SAAS;GAAE,OAAO;GAAW,MAAM;EAAW;CAErC,EAAE,KAAK,UAAU,OAAO;AACrC"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
-
|
|
4
|
+
// src/ui/shell/review-script.ts
|
|
5
5
|
/**
|
|
6
6
|
* Review workspace behavior: comparison modes, zoom, synchronized scrolling,
|
|
7
7
|
* list filters, keyboard review, and the shortcuts dialog. Runs inside the
|
|
@@ -29,12 +29,18 @@ function stateHelpers() {
|
|
|
29
29
|
b.setAttribute('aria-pressed', b.getAttribute('data-view-value')===value ? 'true' : 'false');
|
|
30
30
|
});
|
|
31
31
|
}
|
|
32
|
+
function allows(stage,attr,value){
|
|
33
|
+
var list=stage.getAttribute(attr);
|
|
34
|
+
return !list || list.split(',').indexOf(value)>-1;
|
|
35
|
+
}
|
|
32
36
|
function setMode(stage,mode,persist){
|
|
37
|
+
if(!allows(stage,'data-modes',mode)) return;
|
|
33
38
|
stage.setAttribute('data-view',mode);
|
|
34
39
|
press(stage.querySelector('[data-compare-mode]'),mode);
|
|
35
40
|
if(persist) remember('ss_compare_mode',mode);
|
|
36
41
|
}
|
|
37
42
|
function setZoom(stage,zoom,persist){
|
|
43
|
+
if(!allows(stage,'data-zooms',zoom)) return;
|
|
38
44
|
stage.setAttribute('data-zoom',zoom);
|
|
39
45
|
press(stage.querySelector('[data-compare-zoom]'),zoom);
|
|
40
46
|
if(persist) remember('ss_compare_zoom',zoom);
|
|
@@ -135,11 +141,20 @@ function clickControls() {
|
|
|
135
141
|
var dialog=document.querySelector('dialog[data-shortcuts]');
|
|
136
142
|
if(dialog&&typeof dialog.showModal==='function'&&!dialog.open){ dialog.showModal(); }
|
|
137
143
|
}
|
|
144
|
+
function chooseSource(btn,value){
|
|
145
|
+
var root=btn.closest('[data-compare-sources]');
|
|
146
|
+
if(!root) return;
|
|
147
|
+
root.querySelectorAll('[data-compare-source]').forEach(function(pane){
|
|
148
|
+
pane.hidden = pane.getAttribute('data-compare-source')!==value;
|
|
149
|
+
});
|
|
150
|
+
press(btn.closest('[data-compare-source-switch]'),value);
|
|
151
|
+
}
|
|
138
152
|
function chooseSegment(btn){
|
|
139
153
|
var value=btn.getAttribute('data-view-value');
|
|
140
154
|
var stage=btn.closest('[data-compare]');
|
|
141
155
|
if(stage&&btn.closest('[data-compare-mode]')){ setMode(stage,value,true); }
|
|
142
156
|
else if(stage&&btn.closest('[data-compare-zoom]')){ setZoom(stage,value,true); }
|
|
157
|
+
else if(btn.closest('[data-compare-source-switch]')){ chooseSource(btn,value); }
|
|
143
158
|
else if(btn.closest('[data-filter-switch]')){
|
|
144
159
|
var list=document.querySelector('[data-diff-nav]');
|
|
145
160
|
if(list){ list.setAttribute('data-filter',value); }
|
|
@@ -166,7 +181,15 @@ function keyboardShortcuts() {
|
|
|
166
181
|
if(el&&!el.disabled){ el.click(); return true; }
|
|
167
182
|
return false;
|
|
168
183
|
}
|
|
184
|
+
function stepBrowser(dir){
|
|
185
|
+
var tabs=[].slice.call(document.querySelectorAll('[data-browser-tabs] a'));
|
|
186
|
+
var at=tabs.findIndex(function(a){ return a.getAttribute('aria-current')==='page'; });
|
|
187
|
+
var target=at===-1?null:tabs[at+dir];
|
|
188
|
+
if(target){ target.click(); return true; }
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
169
191
|
function handleKey(key,stage){
|
|
192
|
+
if(key==='['||key===']') return stepBrowser(key===']'?1:-1);
|
|
170
193
|
if(key==='j'||key==='ArrowRight') return press1('[data-snap-next]');
|
|
171
194
|
if(key==='k'||key==='ArrowLeft') return press1('[data-snap-prev]');
|
|
172
195
|
if(key==='a'||key==='A') return press1('[data-approve]');
|
|
@@ -184,12 +207,11 @@ function keyboardShortcuts() {
|
|
|
184
207
|
}
|
|
185
208
|
document.addEventListener('keydown',function(e){
|
|
186
209
|
if(e.metaKey||e.ctrlKey||e.altKey||typing(e.target)) return;
|
|
187
|
-
var stage=document.querySelector('[data-compare]');
|
|
210
|
+
var stage=document.querySelector('[data-compare-source]:not([hidden]) [data-compare]')||document.querySelector('[data-compare]');
|
|
188
211
|
if(!stage||document.querySelector('dialog[open]')) return;
|
|
189
212
|
if(handleKey(e.key,stage)){ e.preventDefault(); }
|
|
190
213
|
});`;
|
|
191
214
|
}
|
|
192
|
-
//#endregion
|
|
193
215
|
export { reviewScript };
|
|
194
216
|
|
|
195
217
|
//# sourceMappingURL=review-script.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"review-script.mjs","names":[],"sources":["../../../src/ui/shell/review-script.ts"],"sourcesContent":["/**\n * Review workspace behavior: comparison modes, zoom, synchronized scrolling,\n * list filters, keyboard review, and the shortcuts dialog. Runs inside the\n * shell's once-only closure (`inits`, `shell` are in scope).\n */\nexport function reviewScript(): string {\n return [stateHelpers(), stageSetup(), wipeDrag(), clickControls(), keyboardShortcuts()].join(\n \"\\n\",\n );\n}\n\n/** Mode/zoom state on the stage root, remembered across snapshots. */\nfunction stateHelpers(): string {\n return `\n var MODES=['split','swipe','onion','diff','flip'];\n var ZOOMS=['fit','100','200','400'];\n function remember(key,value){ try{ localStorage.setItem(key,value); }catch(_){} }\n function recall(key){ try{ return localStorage.getItem(key); }catch(_){ return null; } }\n function press(group,value){\n if(!group) return;\n group.querySelectorAll('[data-view-value]').forEach(function(b){\n b.setAttribute('aria-pressed', b.getAttribute('data-view-value')===value ? 'true' : 'false');\n });\n }\n function setMode(stage,mode,persist){\n stage.setAttribute('data-view',mode);\n press(stage.querySelector('[data-compare-mode]'),mode);\n if(persist) remember('ss_compare_mode',mode);\n }\n function setZoom(stage,zoom,persist){\n stage.setAttribute('data-zoom',zoom);\n press(stage.querySelector('[data-compare-zoom]'),zoom);\n if(persist) remember('ss_compare_zoom',zoom);\n }\n function flip(stage){\n stage.setAttribute('data-flip', stage.getAttribute('data-flip')==='baseline' ? 'current' : 'baseline');\n }\n function nextZoom(stage){\n var cur=stage.getAttribute('data-zoom')||'fit';\n setZoom(stage,ZOOMS[(ZOOMS.indexOf(cur)+1)%ZOOMS.length],true);\n }\n function stepZoom(stage,delta){\n var cur=ZOOMS.indexOf(stage.getAttribute('data-zoom')||'fit');\n var next=Math.max(0,Math.min(ZOOMS.length-1,cur+delta));\n setZoom(stage,ZOOMS[next],true);\n }`;\n}\n\n/** Per-render wiring of a freshly swapped-in comparison stage. */\nfunction stageSetup(): string {\n return `\n function syncScroll(stage){\n var panes=Array.from(stage.querySelectorAll('.stage__pane'));\n var lock=false;\n panes.forEach(function(pane){\n pane.addEventListener('scroll',function(){\n if(lock) return;\n lock=true;\n panes.forEach(function(other){\n if(other!==pane){ other.scrollTop=pane.scrollTop; other.scrollLeft=pane.scrollLeft; }\n });\n requestAnimationFrame(function(){ lock=false; });\n });\n });\n }\n function bindRange(stage,selector,prop,scale){\n var input=stage.querySelector(selector);\n if(!input) return;\n input.addEventListener('input',function(){ stage.style.setProperty(prop,String(input.value*scale)+(scale===1?'%':'')); });\n }\n function initStage(stage){\n if(stage.getAttribute('data-ready')) return;\n stage.setAttribute('data-ready','1');\n var mode=recall('ss_compare_mode');\n if(MODES.indexOf(mode)>-1) setMode(stage,mode,false);\n var zoom=recall('ss_compare_zoom');\n if(ZOOMS.indexOf(zoom)>-1) setZoom(stage,zoom,false);\n bindRange(stage,'[data-compare-swipe]','--swipe',1);\n bindRange(stage,'[data-compare-onion]','--onion',0.01);\n var grid=stage.querySelector('.stage__grid');\n if(grid){ grid.addEventListener('click',function(){ if(stage.getAttribute('data-view')==='flip'){ flip(stage); } }); }\n syncScroll(stage);\n }\n inits.push(function(){ document.querySelectorAll('[data-compare]').forEach(initStage); });`;\n}\n\n/**\n * Drag the wipe handle to move the swipe divider. Uses pointer capture so the\n * drag keeps working when the pointer leaves the handle, and mirrors the value\n * into the (accessible) range slider.\n */\nfunction wipeDrag(): string {\n return `\n function wipePercent(stage,clientX){\n var pane=stage.querySelector('.stage__pane[data-pane=\"current\"]');\n if(!pane) return 50;\n var rect=pane.getBoundingClientRect();\n return Math.max(0,Math.min(100,(clientX-rect.left)/rect.width*100));\n }\n function setWipe(stage,percent){\n stage.style.setProperty('--swipe',percent+'%');\n var range=stage.querySelector('[data-compare-swipe]');\n if(range){ range.value=String(Math.round(percent)); }\n }\n function bindKnob(stage){\n var knob=stage.querySelector('[data-compare-knob]');\n if(!knob||knob.getAttribute('data-bound')) return;\n knob.setAttribute('data-bound','1');\n knob.addEventListener('pointerdown',function(e){\n e.preventDefault();\n knob.setPointerCapture(e.pointerId);\n setWipe(stage,wipePercent(stage,e.clientX));\n function move(ev){ setWipe(stage,wipePercent(stage,ev.clientX)); }\n function stop(){\n knob.removeEventListener('pointermove',move);\n knob.removeEventListener('pointerup',stop);\n knob.removeEventListener('pointercancel',stop);\n }\n knob.addEventListener('pointermove',move);\n knob.addEventListener('pointerup',stop);\n knob.addEventListener('pointercancel',stop);\n });\n }\n inits.push(function(){ document.querySelectorAll('[data-compare]').forEach(bindKnob); });`;\n}\n\n/** Delegated clicks: mode/zoom/filter segments and the shortcuts button. */\nfunction clickControls(): string {\n return `\n function openShortcuts(){\n var dialog=document.querySelector('dialog[data-shortcuts]');\n if(dialog&&typeof dialog.showModal==='function'&&!dialog.open){ dialog.showModal(); }\n }\n function chooseSegment(btn){\n var value=btn.getAttribute('data-view-value');\n var stage=btn.closest('[data-compare]');\n if(stage&&btn.closest('[data-compare-mode]')){ setMode(stage,value,true); }\n else if(stage&&btn.closest('[data-compare-zoom]')){ setZoom(stage,value,true); }\n else if(btn.closest('[data-filter-switch]')){\n var list=document.querySelector('[data-diff-nav]');\n if(list){ list.setAttribute('data-filter',value); }\n press(btn.closest('[data-filter-switch]'),value);\n }\n }\n document.addEventListener('click',function(e){\n var t=e.target instanceof Element ? e.target : null;\n if(!t) return;\n var btn=t.closest('[data-view-value]');\n if(btn){ chooseSegment(btn); }\n if(t.closest('[data-shortcuts-open]')){ openShortcuts(); }\n });`;\n}\n\n/** Global review shortcuts; ignored while typing or with modifier keys held. */\nfunction keyboardShortcuts(): string {\n return `\n function typing(target){\n return target instanceof HTMLElement &&\n (target.tagName==='INPUT'||target.tagName==='TEXTAREA'||target.tagName==='SELECT'||target.isContentEditable);\n }\n function press1(selector){\n var el=document.querySelector(selector);\n if(el&&!el.disabled){ el.click(); return true; }\n return false;\n }\n function handleKey(key,stage){\n if(key==='j'||key==='ArrowRight') return press1('[data-snap-next]');\n if(key==='k'||key==='ArrowLeft') return press1('[data-snap-prev]');\n if(key==='a'||key==='A') return press1('[data-approve]');\n if(key==='r'||key==='R') return press1('[data-reject]');\n if(key>='1'&&key<='5'){ setMode(stage,MODES[Number(key)-1],true); return true; }\n if(key==='f'){ nextZoom(stage); return true; }\n if(key==='+'||key==='='){ stepZoom(stage,1); return true; }\n if(key==='-'||key==='_'){ stepZoom(stage,-1); return true; }\n if(key==='t'){\n if(stage.getAttribute('data-view')==='flip'){ flip(stage); } else { setMode(stage,'flip',true); }\n return true;\n }\n if(key==='?'){ openShortcuts(); return true; }\n return false;\n }\n document.addEventListener('keydown',function(e){\n if(e.metaKey||e.ctrlKey||e.altKey||typing(e.target)) return;\n var stage=document.querySelector('[data-compare]');\n if(!stage||document.querySelector('dialog[open]')) return;\n if(handleKey(e.key,stage)){ e.preventDefault(); }\n });`;\n}\n"],"mappings":";;;;;;;;;AAKA,SAAgB,eAAuB;CACrC,OAAO;EAAC,aAAa;EAAG,WAAW;EAAG,SAAS;EAAG,cAAc;EAAG,kBAAkB;CAAC,CAAC,CAAC,KACtF,IACF;AACF;;AAGA,SAAS,eAAuB;CAC9B,OAAO
|
|
1
|
+
{"version":3,"file":"review-script.mjs","names":[],"sources":["../../../src/ui/shell/review-script.ts"],"sourcesContent":["/**\n * Review workspace behavior: comparison modes, zoom, synchronized scrolling,\n * list filters, keyboard review, and the shortcuts dialog. Runs inside the\n * shell's once-only closure (`inits`, `shell` are in scope).\n */\nexport function reviewScript(): string {\n return [stateHelpers(), stageSetup(), wipeDrag(), clickControls(), keyboardShortcuts()].join(\n \"\\n\",\n );\n}\n\n/** Mode/zoom state on the stage root, remembered across snapshots. */\nfunction stateHelpers(): string {\n return `\n var MODES=['split','swipe','onion','diff','flip'];\n var ZOOMS=['fit','100','200','400'];\n function remember(key,value){ try{ localStorage.setItem(key,value); }catch(_){} }\n function recall(key){ try{ return localStorage.getItem(key); }catch(_){ return null; } }\n function press(group,value){\n if(!group) return;\n group.querySelectorAll('[data-view-value]').forEach(function(b){\n b.setAttribute('aria-pressed', b.getAttribute('data-view-value')===value ? 'true' : 'false');\n });\n }\n function allows(stage,attr,value){\n var list=stage.getAttribute(attr);\n return !list || list.split(',').indexOf(value)>-1;\n }\n function setMode(stage,mode,persist){\n if(!allows(stage,'data-modes',mode)) return;\n stage.setAttribute('data-view',mode);\n press(stage.querySelector('[data-compare-mode]'),mode);\n if(persist) remember('ss_compare_mode',mode);\n }\n function setZoom(stage,zoom,persist){\n if(!allows(stage,'data-zooms',zoom)) return;\n stage.setAttribute('data-zoom',zoom);\n press(stage.querySelector('[data-compare-zoom]'),zoom);\n if(persist) remember('ss_compare_zoom',zoom);\n }\n function flip(stage){\n stage.setAttribute('data-flip', stage.getAttribute('data-flip')==='baseline' ? 'current' : 'baseline');\n }\n function nextZoom(stage){\n var cur=stage.getAttribute('data-zoom')||'fit';\n setZoom(stage,ZOOMS[(ZOOMS.indexOf(cur)+1)%ZOOMS.length],true);\n }\n function stepZoom(stage,delta){\n var cur=ZOOMS.indexOf(stage.getAttribute('data-zoom')||'fit');\n var next=Math.max(0,Math.min(ZOOMS.length-1,cur+delta));\n setZoom(stage,ZOOMS[next],true);\n }`;\n}\n\n/** Per-render wiring of a freshly swapped-in comparison stage. */\nfunction stageSetup(): string {\n return `\n function syncScroll(stage){\n var panes=Array.from(stage.querySelectorAll('.stage__pane'));\n var lock=false;\n panes.forEach(function(pane){\n pane.addEventListener('scroll',function(){\n if(lock) return;\n lock=true;\n panes.forEach(function(other){\n if(other!==pane){ other.scrollTop=pane.scrollTop; other.scrollLeft=pane.scrollLeft; }\n });\n requestAnimationFrame(function(){ lock=false; });\n });\n });\n }\n function bindRange(stage,selector,prop,scale){\n var input=stage.querySelector(selector);\n if(!input) return;\n input.addEventListener('input',function(){ stage.style.setProperty(prop,String(input.value*scale)+(scale===1?'%':'')); });\n }\n function initStage(stage){\n if(stage.getAttribute('data-ready')) return;\n stage.setAttribute('data-ready','1');\n var mode=recall('ss_compare_mode');\n if(MODES.indexOf(mode)>-1) setMode(stage,mode,false);\n var zoom=recall('ss_compare_zoom');\n if(ZOOMS.indexOf(zoom)>-1) setZoom(stage,zoom,false);\n bindRange(stage,'[data-compare-swipe]','--swipe',1);\n bindRange(stage,'[data-compare-onion]','--onion',0.01);\n var grid=stage.querySelector('.stage__grid');\n if(grid){ grid.addEventListener('click',function(){ if(stage.getAttribute('data-view')==='flip'){ flip(stage); } }); }\n syncScroll(stage);\n }\n inits.push(function(){ document.querySelectorAll('[data-compare]').forEach(initStage); });`;\n}\n\n/**\n * Drag the wipe handle to move the swipe divider. Uses pointer capture so the\n * drag keeps working when the pointer leaves the handle, and mirrors the value\n * into the (accessible) range slider.\n */\nfunction wipeDrag(): string {\n return `\n function wipePercent(stage,clientX){\n var pane=stage.querySelector('.stage__pane[data-pane=\"current\"]');\n if(!pane) return 50;\n var rect=pane.getBoundingClientRect();\n return Math.max(0,Math.min(100,(clientX-rect.left)/rect.width*100));\n }\n function setWipe(stage,percent){\n stage.style.setProperty('--swipe',percent+'%');\n var range=stage.querySelector('[data-compare-swipe]');\n if(range){ range.value=String(Math.round(percent)); }\n }\n function bindKnob(stage){\n var knob=stage.querySelector('[data-compare-knob]');\n if(!knob||knob.getAttribute('data-bound')) return;\n knob.setAttribute('data-bound','1');\n knob.addEventListener('pointerdown',function(e){\n e.preventDefault();\n knob.setPointerCapture(e.pointerId);\n setWipe(stage,wipePercent(stage,e.clientX));\n function move(ev){ setWipe(stage,wipePercent(stage,ev.clientX)); }\n function stop(){\n knob.removeEventListener('pointermove',move);\n knob.removeEventListener('pointerup',stop);\n knob.removeEventListener('pointercancel',stop);\n }\n knob.addEventListener('pointermove',move);\n knob.addEventListener('pointerup',stop);\n knob.addEventListener('pointercancel',stop);\n });\n }\n inits.push(function(){ document.querySelectorAll('[data-compare]').forEach(bindKnob); });`;\n}\n\n/** Delegated clicks: mode/zoom/filter segments and the shortcuts button. */\nfunction clickControls(): string {\n return `\n function openShortcuts(){\n var dialog=document.querySelector('dialog[data-shortcuts]');\n if(dialog&&typeof dialog.showModal==='function'&&!dialog.open){ dialog.showModal(); }\n }\n function chooseSource(btn,value){\n var root=btn.closest('[data-compare-sources]');\n if(!root) return;\n root.querySelectorAll('[data-compare-source]').forEach(function(pane){\n pane.hidden = pane.getAttribute('data-compare-source')!==value;\n });\n press(btn.closest('[data-compare-source-switch]'),value);\n }\n function chooseSegment(btn){\n var value=btn.getAttribute('data-view-value');\n var stage=btn.closest('[data-compare]');\n if(stage&&btn.closest('[data-compare-mode]')){ setMode(stage,value,true); }\n else if(stage&&btn.closest('[data-compare-zoom]')){ setZoom(stage,value,true); }\n else if(btn.closest('[data-compare-source-switch]')){ chooseSource(btn,value); }\n else if(btn.closest('[data-filter-switch]')){\n var list=document.querySelector('[data-diff-nav]');\n if(list){ list.setAttribute('data-filter',value); }\n press(btn.closest('[data-filter-switch]'),value);\n }\n }\n document.addEventListener('click',function(e){\n var t=e.target instanceof Element ? e.target : null;\n if(!t) return;\n var btn=t.closest('[data-view-value]');\n if(btn){ chooseSegment(btn); }\n if(t.closest('[data-shortcuts-open]')){ openShortcuts(); }\n });`;\n}\n\n/** Global review shortcuts; ignored while typing or with modifier keys held. */\nfunction keyboardShortcuts(): string {\n return `\n function typing(target){\n return target instanceof HTMLElement &&\n (target.tagName==='INPUT'||target.tagName==='TEXTAREA'||target.tagName==='SELECT'||target.isContentEditable);\n }\n function press1(selector){\n var el=document.querySelector(selector);\n if(el&&!el.disabled){ el.click(); return true; }\n return false;\n }\n function stepBrowser(dir){\n var tabs=[].slice.call(document.querySelectorAll('[data-browser-tabs] a'));\n var at=tabs.findIndex(function(a){ return a.getAttribute('aria-current')==='page'; });\n var target=at===-1?null:tabs[at+dir];\n if(target){ target.click(); return true; }\n return false;\n }\n function handleKey(key,stage){\n if(key==='['||key===']') return stepBrowser(key===']'?1:-1);\n if(key==='j'||key==='ArrowRight') return press1('[data-snap-next]');\n if(key==='k'||key==='ArrowLeft') return press1('[data-snap-prev]');\n if(key==='a'||key==='A') return press1('[data-approve]');\n if(key==='r'||key==='R') return press1('[data-reject]');\n if(key>='1'&&key<='5'){ setMode(stage,MODES[Number(key)-1],true); return true; }\n if(key==='f'){ nextZoom(stage); return true; }\n if(key==='+'||key==='='){ stepZoom(stage,1); return true; }\n if(key==='-'||key==='_'){ stepZoom(stage,-1); return true; }\n if(key==='t'){\n if(stage.getAttribute('data-view')==='flip'){ flip(stage); } else { setMode(stage,'flip',true); }\n return true;\n }\n if(key==='?'){ openShortcuts(); return true; }\n return false;\n }\n document.addEventListener('keydown',function(e){\n if(e.metaKey||e.ctrlKey||e.altKey||typing(e.target)) return;\n var stage=document.querySelector('[data-compare-source]:not([hidden]) [data-compare]')||document.querySelector('[data-compare]');\n if(!stage||document.querySelector('dialog[open]')) return;\n if(handleKey(e.key,stage)){ e.preventDefault(); }\n });`;\n}\n"],"mappings":";;;;;;;;;AAKA,SAAgB,eAAuB;CACrC,OAAO;EAAC,aAAa;EAAG,WAAW;EAAG,SAAS;EAAG,cAAc;EAAG,kBAAkB;CAAC,CAAC,CAAC,KACtF,IACF;AACF;;AAGA,SAAS,eAAuB;CAC9B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCT;;AAGA,SAAS,aAAqB;CAC5B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCT;;;;;;AAOA,SAAS,WAAmB;CAC1B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCT;;AAGA,SAAS,gBAAwB;CAC/B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCT;;AAGA,SAAS,oBAA4B;CACnC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCT"}
|
|
@@ -8,7 +8,7 @@ import { ThemeMenu, UserMenu } from "./account.mjs";
|
|
|
8
8
|
import { adminNavItems, developerNavItems, projectNavItems } from "./nav.mjs";
|
|
9
9
|
import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
10
10
|
import { createUrlBuilder, safeImageUrl } from "@storyshelf/core/urls";
|
|
11
|
-
|
|
11
|
+
// src/ui/shell/sidebar.tsx
|
|
12
12
|
/**
|
|
13
13
|
* Left rail. Collapses to icons only on desktop (`html[data-sidebar="rail"]`)
|
|
14
14
|
* and becomes an off-canvas drawer on small screens.
|
|
@@ -253,7 +253,7 @@ const Brand = ({ name, logo, href }) => {
|
|
|
253
253
|
};
|
|
254
254
|
/** Primary navigation: brand, workspace, project, developer, and admin entries. */
|
|
255
255
|
const Sidebar = ({ nav, name, logo }) => {
|
|
256
|
-
const { user, authEnabled, config } = getStore();
|
|
256
|
+
const { user, authEnabled, config, testing } = getStore();
|
|
257
257
|
const urls = createUrlBuilder("/", config.publishedBaseDomain);
|
|
258
258
|
const showAdmin = !authEnabled || user?.role === "admin";
|
|
259
259
|
const projects = {
|
|
@@ -282,7 +282,7 @@ const Sidebar = ({ nav, name, logo }) => {
|
|
|
282
282
|
}),
|
|
283
283
|
nav?.projectSlug ? /* @__PURE__ */ jsx(NavSection, {
|
|
284
284
|
title: "Project",
|
|
285
|
-
items: projectNavItems(urls, nav.projectSlug),
|
|
285
|
+
items: projectNavItems(urls, nav.projectSlug, testing),
|
|
286
286
|
active: nav.active
|
|
287
287
|
}) : null,
|
|
288
288
|
/* @__PURE__ */ jsx(NavSection, {
|
|
@@ -317,7 +317,6 @@ const Sidebar = ({ nav, name, logo }) => {
|
|
|
317
317
|
]
|
|
318
318
|
});
|
|
319
319
|
};
|
|
320
|
-
//#endregion
|
|
321
320
|
export { Sidebar };
|
|
322
321
|
|
|
323
322
|
//# sourceMappingURL=sidebar.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sidebar.mjs","names":[],"sources":["../../../src/ui/shell/sidebar.tsx"],"sourcesContent":["import { createUrlBuilder, safeImageUrl } from \"@storyshelf/core/urls\";\nimport type { FC } from \"hono/jsx\";\nimport { getStore } from \"../../store.ts\";\nimport { css } from \"../css.ts\";\nimport { Icon } from \"../icons/icon.tsx\";\nimport { ThemeMenu, UserMenu } from \"./account.tsx\";\nimport {\n adminNavItems,\n developerNavItems,\n projectNavItems,\n type NavConfig,\n type NavItem,\n} from \"./nav.ts\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\n/**\n * Left rail. Collapses to icons only on desktop (`html[data-sidebar=\"rail\"]`)\n * and becomes an off-canvas drawer on small screens.\n */\nconst shellSidebar = css`\n /* shell-sidebar */\n position: sticky;\n top: 0;\n height: 100vh;\n display: flex;\n flex-direction: column;\n gap: var(--space-2);\n padding: var(--space-3) var(--space-2);\n background: var(--sidebar-bg);\n border-right: 1px solid var(--border);\n .sidebar__brand {\n display: flex;\n align-items: center;\n gap: 0.6rem;\n min-height: 40px;\n padding: 0 var(--space-2);\n color: var(--text-primary);\n font-weight: 650;\n font-size: var(--text-lg);\n letter-spacing: -0.01em;\n text-decoration: none;\n }\n .sidebar__brand:hover {\n text-decoration: none;\n }\n .sidebar__mark {\n flex: none;\n display: inline-grid;\n place-items: center;\n width: 28px;\n height: 28px;\n border-radius: var(--radius);\n background: var(--accent);\n color: var(--accent-contrast);\n }\n .sidebar__logo {\n flex: none;\n border-radius: var(--radius-sm);\n object-fit: contain;\n }\n .sidebar__nav {\n display: flex;\n flex-direction: column;\n gap: 2px;\n flex: 1;\n min-height: 0;\n overflow-x: hidden;\n overflow-y: auto;\n }\n .sidebar__section {\n margin: var(--space-4) 0 var(--space-1);\n padding: 0 var(--space-3);\n color: var(--text-secondary);\n font-size: 0.68rem;\n font-weight: 600;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n white-space: nowrap;\n }\n .sidebar__link {\n display: flex;\n align-items: center;\n gap: 0.65rem;\n min-height: 38px;\n padding: 0.4rem var(--space-3);\n border-radius: var(--radius);\n color: var(--text-secondary);\n font-size: var(--text-base);\n font-weight: 500;\n text-decoration: none;\n white-space: nowrap;\n transition:\n background var(--dur-fast) var(--ease),\n color var(--dur-fast) var(--ease);\n }\n .sidebar__link:hover {\n background: var(--surface-muted);\n color: var(--text-primary);\n text-decoration: none;\n }\n .sidebar__link--active {\n background: var(--accent-subtle);\n color: var(--accent-fg);\n font-weight: 600;\n }\n .sidebar__link--active:hover {\n background: var(--accent-subtle);\n color: var(--accent-fg);\n }\n .sidebar__link--active svg {\n color: var(--accent);\n }\n .sidebar__ext {\n margin-left: auto;\n opacity: 0.6;\n }\n .sidebar__footer {\n display: grid;\n gap: var(--space-1);\n padding-top: var(--space-2);\n border-top: 1px solid var(--border-subtle);\n }\n .sidebar__tools {\n display: flex;\n align-items: center;\n gap: var(--space-1);\n }\n .sidebar__tools > details {\n flex: none;\n width: auto;\n }\n .sidebar__tools .sidebar__collapse {\n flex: 1;\n width: auto;\n }\n .sidebar__collapse {\n appearance: none;\n display: flex;\n align-items: center;\n gap: 0.65rem;\n min-height: 36px;\n padding: 0.4rem var(--space-3);\n border: 0;\n border-radius: var(--radius);\n background: transparent;\n color: var(--text-secondary);\n font: inherit;\n font-size: var(--text-sm);\n cursor: pointer;\n }\n .sidebar__collapse:hover {\n background: var(--surface-muted);\n color: var(--text-primary);\n }\n @media (min-width: 881px) {\n html[data-sidebar=\"rail\"] & .sidebar__label,\n html[data-sidebar=\"rail\"] & .sidebar__section,\n html[data-sidebar=\"rail\"] & .sidebar__ext {\n display: none;\n }\n html[data-sidebar=\"rail\"] & .sidebar__link,\n html[data-sidebar=\"rail\"] & .sidebar__collapse,\n html[data-sidebar=\"rail\"] & .sidebar__brand {\n justify-content: center;\n padding-inline: 0;\n }\n html[data-sidebar=\"rail\"] & .sidebar__collapse svg {\n transform: rotate(180deg);\n }\n html[data-sidebar=\"rail\"] & .sidebar__footer summary {\n justify-content: center;\n }\n html[data-sidebar=\"rail\"] & .sidebar__footer summary > svg:last-child {\n display: none;\n }\n html[data-sidebar=\"rail\"] & .sidebar__tools {\n flex-direction: column;\n }\n }\n @media (max-width: 880px) {\n & {\n position: fixed;\n inset: 0 auto 0 0;\n z-index: 60;\n width: min(280px, 85vw);\n height: 100dvh;\n transform: translateX(-100%);\n transition: transform var(--dur-base) var(--ease);\n box-shadow: var(--shadow-3);\n }\n &.sidebar--open {\n transform: translateX(0);\n }\n & .sidebar__collapse {\n display: none;\n }\n }\n`;\n\nconst NavLink: FC<{ item: NavItem; active?: string }> = ({ item, active }) => {\n const current = active === item.key;\n return (\n <a\n class={`sidebar__link${current ? \" sidebar__link--active\" : \"\"}`}\n href={item.href}\n title={item.label}\n aria-current={current ? \"page\" : undefined}\n target={item.external ? \"_blank\" : undefined}\n rel={item.external ? \"noopener\" : undefined}\n >\n <Icon name={item.icon} />\n <span class=\"sidebar__label\">{item.label}</span>\n {item.external ? (\n <span class=\"sidebar__ext\">\n <Icon name=\"external-link\" size=\"sm\" />\n </span>\n ) : null}\n </a>\n );\n};\n\nconst NavSection: FC<{ title: string; items: NavItem[]; active?: string }> = ({\n title,\n items,\n active,\n}) => {\n return (\n <>\n <div class=\"sidebar__section\">{title}</div>\n {items.map((item) => (\n <NavLink key={item.key} item={item} active={active} />\n ))}\n </>\n );\n};\n\nconst Brand: FC<{ name: string; logo?: string; href: string }> = ({ name, logo, href }) => {\n const safeLogo = safeImageUrl(logo);\n return (\n <a class=\"sidebar__brand\" href={href} title={name}>\n {safeLogo ? (\n <img class=\"sidebar__logo\" src={safeLogo} alt=\"\" width=\"28\" height=\"28\" />\n ) : (\n <span class=\"sidebar__mark\" aria-hidden=\"true\">\n <Icon name=\"layers\" size=\"sm\" />\n </span>\n )}\n <span class=\"sidebar__label\">{name}</span>\n </a>\n );\n};\n\n/** Primary navigation: brand, workspace, project, developer, and admin entries. */\nexport const Sidebar: FC<{ nav?: NavConfig; name: string; logo?: string }> = ({\n nav,\n name,\n logo,\n}) => {\n const { user, authEnabled, config } = getStore();\n const urls = createUrlBuilder(\"/\", config.publishedBaseDomain);\n const showAdmin = !authEnabled || user?.role === \"admin\";\n const projects: NavItem = {\n key: \"projects\",\n label: \"Projects\",\n href: urls.projects(),\n icon: \"folder\",\n };\n return (\n <aside id=\"sidebar\" class={shellSidebar} aria-label=\"Primary\" data-sidebar>\n <Brand name={name} logo={logo} href={urls.projects()} />\n <nav class=\"sidebar__nav\">\n <NavLink item={projects} active={nav?.active} />\n {nav?.projectSlug ? (\n <NavSection\n title=\"Project\"\n items={projectNavItems(urls, nav.projectSlug)}\n active={nav.active}\n />\n ) : null}\n <NavSection title=\"Developer\" items={developerNavItems()} active={nav?.active} />\n {showAdmin ? (\n <NavSection title=\"Administration\" items={adminNavItems()} active={nav?.active} />\n ) : null}\n </nav>\n <div class=\"sidebar__footer\">\n <UserMenu />\n <div class=\"sidebar__tools\">\n <ThemeMenu />\n <button\n class=\"sidebar__collapse\"\n type=\"button\"\n data-sidebar-collapse\n aria-label=\"Collapse or expand sidebar\"\n title=\"Collapse or expand sidebar\"\n >\n <Icon name=\"chevrons-left\" />\n <span class=\"sidebar__label\">Collapse</span>\n </button>\n </div>\n </div>\n </aside>\n );\n};\n"],"mappings":"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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/dist/ui/shell/topbar.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { breadcrumbs } from "./nav.mjs";
|
|
|
10
10
|
import { ProjectModel } from "@storyshelf/core/models";
|
|
11
11
|
import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
12
12
|
import { createUrlBuilder } from "@storyshelf/core/urls";
|
|
13
|
-
|
|
13
|
+
// src/ui/shell/topbar.tsx
|
|
14
14
|
/** Slim, translucent bar: project picker plus breadcrumbs (account lives in the sidebar). */
|
|
15
15
|
const shellTopbar = css`
|
|
16
16
|
/* shell-topbar */
|
|
@@ -262,7 +262,6 @@ const ToastRegion = () => {
|
|
|
262
262
|
"aria-atomic": "false"
|
|
263
263
|
});
|
|
264
264
|
};
|
|
265
|
-
//#endregion
|
|
266
265
|
export { ToastRegion, TopBar };
|
|
267
266
|
|
|
268
267
|
//# sourceMappingURL=topbar.mjs.map
|
package/dist/ui/stacks.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import "node:url";
|
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { css } from "./css.mjs";
|
|
5
5
|
import { jsx } from "hono/jsx/jsx-runtime";
|
|
6
|
-
|
|
6
|
+
// src/ui/stacks.tsx
|
|
7
7
|
const hGaps = {
|
|
8
8
|
sm: "0.5rem",
|
|
9
9
|
md: "0.75rem",
|
|
@@ -76,7 +76,6 @@ const VStack = ({ gap = "md", align = "stretch", children }) => {
|
|
|
76
76
|
children
|
|
77
77
|
});
|
|
78
78
|
};
|
|
79
|
-
//#endregion
|
|
80
79
|
export { HStack, VStack };
|
|
81
80
|
|
|
82
81
|
//# sourceMappingURL=stacks.mjs.map
|
package/dist/ui/styles/base.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
-
|
|
4
|
+
// src/ui/styles/base.ts
|
|
5
5
|
/** Base reset, typography, focus ring, and shared utilities. */
|
|
6
6
|
function baseCss() {
|
|
7
7
|
return `${resetCss()}\n${utilityCss()}`;
|
|
@@ -35,7 +35,6 @@ function utilityCss() {
|
|
|
35
35
|
.mb-1 { margin-bottom: 1rem; }
|
|
36
36
|
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }`;
|
|
37
37
|
}
|
|
38
|
-
//#endregion
|
|
39
38
|
export { baseCss };
|
|
40
39
|
|
|
41
40
|
//# sourceMappingURL=base.mjs.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
-
|
|
4
|
+
// src/ui/styles/behavior.ts
|
|
5
5
|
/** Behavioral CSS: HTMX indicators, skeletons, tables, stats. */
|
|
6
6
|
function behaviorCss() {
|
|
7
7
|
return `${htmxCss()}\n${dataCss()}\n${chromeCss()}`;
|
|
@@ -37,7 +37,6 @@ function chromeCss() {
|
|
|
37
37
|
[data-theme-icon] { display: none; }
|
|
38
38
|
[data-theme="light"] [data-theme-icon="light"], [data-theme="dark"] [data-theme-icon="dark"], [data-theme="system"] [data-theme-icon="system"] { display: inline-flex; }`;
|
|
39
39
|
}
|
|
40
|
-
//#endregion
|
|
41
40
|
export { behaviorCss };
|
|
42
41
|
|
|
43
42
|
//# sourceMappingURL=behavior.mjs.map
|
|
@@ -2,7 +2,7 @@ import "node:path";
|
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { css } from "../css.mjs";
|
|
5
|
-
|
|
5
|
+
// src/ui/styles/review-layout.ts
|
|
6
6
|
/**
|
|
7
7
|
* Review workspace layout styles: header, filmstrip, canvas column, action
|
|
8
8
|
* bar, and shortcuts dialog. Shared by the `build-diff-*` page sections, so
|
|
@@ -341,7 +341,6 @@ const shortcutsDialog = css`
|
|
|
341
341
|
color: var(--text-secondary);
|
|
342
342
|
}
|
|
343
343
|
`;
|
|
344
|
-
//#endregion
|
|
345
344
|
export { actionBar, actionGroup, actionHints, actionPosition, canvasColumn, filmBody, filmItem, filmItemActive, filmStatus, filmSub, filmTitle, filmstrip, filmstripHead, filmstripList, headerActions, headerMain, headerMeta, headerProgress, headerSide, headerTitle, reviewHeader, shortcutsDialog, statusDots, viewerHead, viewerMeta, viewerTitle, workspace };
|
|
346
345
|
|
|
347
346
|
//# sourceMappingURL=review-layout.mjs.map
|
|
@@ -2,7 +2,7 @@ import "node:path";
|
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { css } from "../css.mjs";
|
|
5
|
-
|
|
5
|
+
// src/ui/styles/review.ts
|
|
6
6
|
/**
|
|
7
7
|
* Snapshot-card and comment styles shared by the build detail, library, and
|
|
8
8
|
* review pages (the review workspace layout lives in `review-layout.ts`).
|
|
@@ -80,7 +80,6 @@ const reviewCommentActions = css`
|
|
|
80
80
|
/* review-comment-actions */
|
|
81
81
|
margin-top: 0.5rem;
|
|
82
82
|
`;
|
|
83
|
-
//#endregion
|
|
84
83
|
export { diffPaneImg, reviewComment, reviewCommentActions, reviewCommentBody, reviewCommentHead, snapshotCard, snapshotCardBody, snapshotCardHead, snapshotCardMeta, snapshotGrid };
|
|
85
84
|
|
|
86
85
|
//# sourceMappingURL=review.mjs.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
-
|
|
4
|
+
// src/ui/styles/tokens.ts
|
|
5
5
|
/**
|
|
6
6
|
* CSS variable tokens: brand-driven colors (per theme), theme-independent
|
|
7
7
|
* scales (type, space, radius, motion), and tokens derived from the brand
|
|
@@ -126,7 +126,6 @@ function derivedVars(fb) {
|
|
|
126
126
|
--status-rejected-border: ${mix("--status-rejected", "32%", "--border")};
|
|
127
127
|
--status-info-border: ${mix("--status-info", "32%", "--border")};`;
|
|
128
128
|
}
|
|
129
|
-
//#endregion
|
|
130
129
|
export { tokenCss };
|
|
131
130
|
|
|
132
131
|
//# sourceMappingURL=tokens.mjs.map
|