@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
package/dist/pages/profile.mjs
CHANGED
|
@@ -10,10 +10,11 @@ import { HStack, VStack } from "../ui/stacks.mjs";
|
|
|
10
10
|
import { Table } from "../ui/table.mjs";
|
|
11
11
|
import { csrfField } from "../ui/csrf-field.mjs";
|
|
12
12
|
import { DocumentLayout } from "../ui/document.mjs";
|
|
13
|
+
import { NotificationsPausedNotice } from "./notifications-paused.mjs";
|
|
13
14
|
import { passkeyRegisterScript } from "./passkey-ceremony.mjs";
|
|
14
15
|
import { jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
15
16
|
import { safeImageUrl } from "@storyshelf/core/urls";
|
|
16
|
-
|
|
17
|
+
// src/pages/profile.tsx
|
|
17
18
|
const profileAvatar = css`
|
|
18
19
|
/* profile-avatar */
|
|
19
20
|
width: 64px;
|
|
@@ -297,55 +298,59 @@ function renderProfilePage(state) {
|
|
|
297
298
|
] }, m.projectSlug)) })] })
|
|
298
299
|
})
|
|
299
300
|
] }),
|
|
300
|
-
/* @__PURE__ */ jsxs(Card, { children: [
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
/* @__PURE__ */
|
|
312
|
-
|
|
313
|
-
children: "
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
return /* @__PURE__ */ jsxs("tr", { children: [
|
|
318
|
-
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx("a", {
|
|
319
|
-
href: `/projects/${m.projectSlug}`,
|
|
320
|
-
children: m.projectName
|
|
321
|
-
}) }),
|
|
322
|
-
/* @__PURE__ */ jsx("td", { children: subscribed ? /* @__PURE__ */ jsx(Badge, { children: "Email on" }) : /* @__PURE__ */ jsx(Badge, { children: "Off" }) }),
|
|
323
|
-
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsxs("form", {
|
|
324
|
-
method: "post",
|
|
325
|
-
action: "/profile/notifications",
|
|
326
|
-
children: [
|
|
327
|
-
csrfField(),
|
|
328
|
-
/* @__PURE__ */ jsx("input", {
|
|
329
|
-
type: "hidden",
|
|
330
|
-
name: "slug",
|
|
331
|
-
value: m.projectSlug
|
|
332
|
-
}),
|
|
333
|
-
/* @__PURE__ */ jsx("input", {
|
|
334
|
-
type: "hidden",
|
|
335
|
-
name: "enabled",
|
|
336
|
-
value: subscribed ? "" : "1"
|
|
337
|
-
}),
|
|
338
|
-
/* @__PURE__ */ jsx(Button, {
|
|
339
|
-
variant: "secondary",
|
|
340
|
-
size: "sm",
|
|
341
|
-
type: "submit",
|
|
342
|
-
children: subscribed ? "Disable" : "Enable"
|
|
343
|
-
})
|
|
344
|
-
]
|
|
301
|
+
/* @__PURE__ */ jsxs(Card, { children: [
|
|
302
|
+
/* @__PURE__ */ jsx(PageHeader, {
|
|
303
|
+
title: "Notifications",
|
|
304
|
+
description: "Email alerts per project. Fine-tune topics in each project's settings."
|
|
305
|
+
}),
|
|
306
|
+
/* @__PURE__ */ jsx(NotificationsPausedNotice, { paused: state.notificationsPaused }),
|
|
307
|
+
memberships.length === 0 ? /* @__PURE__ */ jsx("p", {
|
|
308
|
+
class: profileMeta,
|
|
309
|
+
children: "No project memberships."
|
|
310
|
+
}) : /* @__PURE__ */ jsx(Table, {
|
|
311
|
+
dense: true,
|
|
312
|
+
children: /* @__PURE__ */ jsxs("table", { children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
313
|
+
/* @__PURE__ */ jsx("th", { children: "Project" }),
|
|
314
|
+
/* @__PURE__ */ jsx("th", { children: "Status" }),
|
|
315
|
+
/* @__PURE__ */ jsx("th", { children: /* @__PURE__ */ jsx("span", {
|
|
316
|
+
class: "visually-hidden",
|
|
317
|
+
children: "Actions"
|
|
345
318
|
}) })
|
|
346
|
-
] }, m
|
|
347
|
-
|
|
348
|
-
|
|
319
|
+
] }) }), /* @__PURE__ */ jsx("tbody", { children: memberships.map((m) => {
|
|
320
|
+
const subscribed = subscribedSlugs.includes(m.projectSlug);
|
|
321
|
+
return /* @__PURE__ */ jsxs("tr", { children: [
|
|
322
|
+
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx("a", {
|
|
323
|
+
href: `/projects/${m.projectSlug}`,
|
|
324
|
+
children: m.projectName
|
|
325
|
+
}) }),
|
|
326
|
+
/* @__PURE__ */ jsx("td", { children: subscribed ? /* @__PURE__ */ jsx(Badge, { children: "Email on" }) : /* @__PURE__ */ jsx(Badge, { children: "Off" }) }),
|
|
327
|
+
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsxs("form", {
|
|
328
|
+
method: "post",
|
|
329
|
+
action: "/profile/notifications",
|
|
330
|
+
children: [
|
|
331
|
+
csrfField(),
|
|
332
|
+
/* @__PURE__ */ jsx("input", {
|
|
333
|
+
type: "hidden",
|
|
334
|
+
name: "slug",
|
|
335
|
+
value: m.projectSlug
|
|
336
|
+
}),
|
|
337
|
+
/* @__PURE__ */ jsx("input", {
|
|
338
|
+
type: "hidden",
|
|
339
|
+
name: "enabled",
|
|
340
|
+
value: subscribed ? "" : "1"
|
|
341
|
+
}),
|
|
342
|
+
/* @__PURE__ */ jsx(Button, {
|
|
343
|
+
variant: "secondary",
|
|
344
|
+
size: "sm",
|
|
345
|
+
type: "submit",
|
|
346
|
+
children: subscribed ? "Disable" : "Enable"
|
|
347
|
+
})
|
|
348
|
+
]
|
|
349
|
+
}) })
|
|
350
|
+
] }, m.projectSlug);
|
|
351
|
+
}) })] })
|
|
352
|
+
})
|
|
353
|
+
] }),
|
|
349
354
|
/* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs("form", {
|
|
350
355
|
method: "post",
|
|
351
356
|
action: "/auth/logout",
|
|
@@ -358,7 +363,6 @@ function renderProfilePage(state) {
|
|
|
358
363
|
]
|
|
359
364
|
});
|
|
360
365
|
}
|
|
361
|
-
//#endregion
|
|
362
366
|
export { renderProfilePage };
|
|
363
367
|
|
|
364
368
|
//# sourceMappingURL=profile.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"profile.mjs","names":[],"sources":["../../src/pages/profile.tsx"],"sourcesContent":["import type { AuthUser } from \"@storyshelf/core/types\";\nimport { safeImageUrl } from \"@storyshelf/core/urls\";\nimport type { FC } from \"hono/jsx\";\nimport {\n Alert,\n Badge,\n Button,\n Card,\n Field,\n HStack,\n PageHeader,\n Table,\n VStack,\n} from \"../ui/components.tsx\";\nimport { csrfField } from \"../ui/csrf-field.tsx\";\nimport { css } from \"../ui/css.ts\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\nimport { passkeyRegisterScript } from \"./passkey-ceremony.ts\";\n\nexport interface ProfilePageState {\n user: AuthUser;\n dbUser?: {\n email: string;\n name: string;\n displayNameOverride: string | null;\n authProvider: string;\n lastLoginAt: string | null;\n createdAt: string;\n };\n memberships: Array<{ projectName: string; projectSlug: string; role: string; source: string }>;\n subscribedSlugs: string[];\n error?: string;\n isLocal?: boolean;\n security?: {\n sessions: Array<{\n id: string;\n ipAddress: string | null;\n userAgent: string | null;\n createdAt: string;\n expiresAt: string;\n current: boolean;\n }>;\n passkeys: Array<{\n id: string;\n name: string | null;\n deviceType: string;\n backedUp: boolean;\n transports: string | null;\n aaguid: string | null;\n createdAt: string;\n }>;\n passkeysEnabled: boolean;\n hasPassword: boolean;\n };\n}\n\nconst profileAvatar = css`\n /* profile-avatar */\n width: 64px;\n height: 64px;\n border-radius: 999px;\n object-fit: cover;\n`;\n\nconst profileAvatarFallback = css`\n /* profile-avatar-fallback */\n width: 64px;\n height: 64px;\n display: grid;\n place-items: center;\n background: var(--accent);\n color: #fff;\n border-radius: 999px;\n font-size: 1.5rem;\n font-weight: 700;\n`;\n\nconst profileName = css`\n /* profile-name */\n font-weight: 700;\n font-size: 1.2rem;\n`;\n\nconst profileMeta = css`\n /* profile-meta */\n color: var(--text-secondary);\n font-size: 0.85rem;\n`;\n\n/* eslint-disable promise-function-async -- JSX components return HtmlEscapedString */\n\ntype SecurityState = NonNullable<ProfilePageState[\"security\"]>;\ntype PasskeyState = SecurityState[\"passkeys\"][number];\n\n/** Registered passkey rows with per-key remove actions. */\nconst PasskeyTable: FC<{ passkeys: PasskeyState[] }> = ({ passkeys }) => {\n return (\n <Table dense>\n <table>\n <thead>\n <tr>\n <th>Key</th>\n <th>Added</th>\n <th>Status</th>\n </tr>\n </thead>\n <tbody>\n {passkeys.map(\n // oxlint-disable-next-line typescript/promise-function-async -- JSX map is sync\n (key) => (\n <tr key={key.id}>\n <td>\n <div>{key.name ?? \"Unnamed key\"}</div>\n <div class={profileMeta}>{key.deviceType}</div>\n </td>\n <td>{new Date(key.createdAt).toLocaleDateString()}</td>\n <td>\n <HStack>\n {key.backedUp ? <Badge>Synced</Badge> : null}\n <form\n method=\"post\"\n action={`/profile/passkeys/${key.id}/delete`}\n data-confirm=\"Remove this passkey?\"\n >\n <Button variant=\"secondary\" size=\"sm\" type=\"submit\">\n Remove\n </Button>\n </form>\n </HStack>\n </td>\n </tr>\n ),\n )}\n </tbody>\n </table>\n </Table>\n );\n};\n\ntype SessionInfo = SecurityState[\"sessions\"][number];\n\n/** One signed-in device with its revoke action. */\nconst SessionRow: FC<{ session: SessionInfo }> = ({ session }) => {\n return (\n <tr>\n <td>\n <div class=\"truncate\">{session.userAgent ?? \"Unknown device\"}</div>\n <div class={profileMeta}>\n <span class=\"mono\">{session.ipAddress ?? \"—\"}</span>\n </div>\n </td>\n <td>{new Date(session.createdAt).toLocaleString()}</td>\n <td>\n {session.current ? (\n <Badge>This device</Badge>\n ) : (\n <form\n method=\"post\"\n action=\"/profile/sessions/revoke\"\n data-confirm=\"Sign this device out?\"\n >\n <input type=\"hidden\" name=\"sessionId\" value={session.id} />\n <Button variant=\"secondary\" size=\"sm\" type=\"submit\">\n Revoke\n </Button>\n </form>\n )}\n </td>\n </tr>\n );\n};\n\n/** Active device sessions with per-device revoke and sign-out-others. */\nconst SessionsSection: FC<{ security: SecurityState }> = ({ security }) => {\n return (\n <Card>\n <PageHeader title=\"Devices\" description=\"Where you are signed in\" />\n <Table dense>\n <table>\n <thead>\n <tr>\n <th>Device</th>\n <th>Signed in</th>\n <th>Status</th>\n </tr>\n </thead>\n <tbody>\n {security.sessions.map((session) => (\n <SessionRow key={session.id} session={session} />\n ))}\n </tbody>\n </table>\n </Table>\n {security.sessions.length > 1 ? (\n <form\n method=\"post\"\n action=\"/profile/sessions/revoke-others\"\n data-confirm=\"Sign out all other devices?\"\n >\n <Button variant=\"secondary\" size=\"sm\" type=\"submit\">\n Sign out other devices\n </Button>\n </form>\n ) : null}\n </Card>\n );\n};\n\n/** Registered passkeys with delete, register button, and the backup nudge. */\nconst PasskeysSection: FC<{ security: SecurityState }> = ({ security }) => {\n return (\n <Card>\n <PageHeader title=\"Passkeys\" description=\"Sign in without a password using your device\" />\n {security.passkeys.length < 2 ? (\n <Alert title=\"Add a backup key\">\n Register a 2nd key so a lost device doesn't lock you out.\n </Alert>\n ) : null}\n {security.passkeys.length === 0 ? (\n <p class={profileMeta}>No passkeys yet.</p>\n ) : (\n <PasskeyTable passkeys={security.passkeys} />\n )}\n <HStack>\n <Field label=\"Key name\" name=\"keyName\" type=\"text\" placeholder=\"e.g. Security key\" />\n <Button variant=\"primary\" size=\"sm\" type=\"button\" data-passkey-register=\"true\">\n Register a passkey\n </Button>\n </HStack>\n <p class={profileMeta} data-passkey-status=\"true\" aria-live=\"polite\">\n {\"\"}\n </p>\n <script dangerouslySetInnerHTML={{ __html: passkeyRegisterScript() }} />\n </Card>\n );\n};\n\n/** Personal profile page (editable display name, password for local accounts). */\n// oxlint-disable-next-line eslint/max-lines-per-function -- profile page composes several sections, cohesive\nexport function renderProfilePage(state: ProfilePageState): RenderedContent {\n const { user, dbUser, memberships, subscribedSlugs, error, isLocal, security } = state;\n const avatar = safeImageUrl(user.avatarUrl);\n const displayName = dbUser?.displayNameOverride ?? user.name;\n const provider = dbUser?.authProvider ?? user.providerId ?? \"oidc\";\n return (\n <DocumentLayout title=\"Profile\" nav={{ active: \"profile\" }}>\n <PageHeader title=\"Profile\" description=\"Your account and project memberships\" />\n {error ? (\n <Alert tone=\"danger\" title=\"Could not save\">\n {error}\n </Alert>\n ) : null}\n\n <Card>\n <VStack>\n <HStack>\n {avatar ? (\n <img class={profileAvatar} src={avatar} alt=\"\" width=\"64\" height=\"64\" />\n ) : (\n <span class={profileAvatarFallback} aria-hidden=\"true\">\n {user.name.slice(0, 1).toUpperCase()}\n </span>\n )}\n <VStack>\n <div class={profileName}>{displayName}</div>\n <div class={profileMeta}>{user.email}</div>\n <HStack>\n <Badge>{user.role}</Badge>\n <Badge>via {provider}</Badge>\n </HStack>\n </VStack>\n </HStack>\n <div class={profileMeta}>\n Member since {dbUser?.createdAt ? new Date(dbUser.createdAt).toLocaleDateString() : \"—\"}{\" \"}\n · Last login {dbUser?.lastLoginAt ? new Date(dbUser.lastLoginAt).toLocaleString() : \"—\"}\n </div>\n </VStack>\n </Card>\n\n <Card>\n <PageHeader\n title=\"Display name\"\n description=\"Shown in comments and the header. Survives SSO refresh.\"\n />\n <form method=\"post\" action=\"/profile\" novalidate>\n <Field label=\"Display name\" name=\"displayName\" type=\"text\" required value={displayName} />\n <Button variant=\"primary\" type=\"submit\">\n Save name\n </Button>\n </form>\n </Card>\n\n {isLocal ? (\n <Card>\n <PageHeader title=\"Change password\" description=\"For local accounts only\" />\n <form method=\"post\" action=\"/profile/password\" novalidate>\n <Field\n label=\"Current password\"\n name=\"currentPassword\"\n type=\"password\"\n required\n autocomplete=\"current-password\"\n />\n <Field\n label=\"New password\"\n name=\"newPassword\"\n type=\"password\"\n required\n autocomplete=\"new-password\"\n />\n <Field\n label=\"Confirm new password\"\n name=\"confirmPassword\"\n type=\"password\"\n required\n autocomplete=\"new-password\"\n />\n <Button variant=\"primary\" type=\"submit\">\n Change password\n </Button>\n </form>\n </Card>\n ) : null}\n\n {security ? <SessionsSection security={security} /> : null}\n\n {security?.passkeysEnabled ? <PasskeysSection security={security} /> : null}\n\n <Card>\n <PageHeader title=\"Project memberships\" description=\"Your role on each project\" />{\" \"}\n {memberships.length === 0 ? (\n <p class={profileMeta}>No project memberships.</p>\n ) : (\n <Table dense>\n <table>\n <thead>\n <tr>\n <th>Project</th>\n <th>Role</th>\n <th>Source</th>\n </tr>\n </thead>\n <tbody>\n {memberships.map(\n // oxlint-disable-next-line typescript/promise-function-async -- JSX map is sync\n (m) => (\n <tr key={m.projectSlug}>\n <td>\n <a href={`/projects/${m.projectSlug}`}>{m.projectName}</a>\n </td>\n <td>\n <Badge>{m.role}</Badge>\n </td>\n <td>{m.source}</td>\n </tr>\n ),\n )}\n </tbody>\n </table>\n </Table>\n )}\n </Card>\n\n <Card>\n <PageHeader\n title=\"Notifications\"\n description=\"Email alerts per project. Fine-tune topics in each project's settings.\"\n />\n {memberships.length === 0 ? (\n <p class={profileMeta}>No project memberships.</p>\n ) : (\n <Table dense>\n <table>\n <thead>\n <tr>\n <th>Project</th>\n <th>Status</th>\n <th>\n <span class=\"visually-hidden\">Actions</span>\n </th>\n </tr>\n </thead>\n <tbody>\n {memberships.map(\n // oxlint-disable-next-line typescript/promise-function-async -- JSX map is sync\n (m) => {\n const subscribed = subscribedSlugs.includes(m.projectSlug);\n return (\n <tr key={m.projectSlug}>\n <td>\n <a href={`/projects/${m.projectSlug}`}>{m.projectName}</a>\n </td>\n <td>{subscribed ? <Badge>Email on</Badge> : <Badge>Off</Badge>}</td>\n <td>\n <form method=\"post\" action=\"/profile/notifications\">\n {csrfField()}\n <input type=\"hidden\" name=\"slug\" value={m.projectSlug} />\n <input type=\"hidden\" name=\"enabled\" value={subscribed ? \"\" : \"1\"} />\n <Button variant=\"secondary\" size=\"sm\" type=\"submit\">\n {subscribed ? \"Disable\" : \"Enable\"}\n </Button>\n </form>\n </td>\n </tr>\n );\n },\n )}\n </tbody>\n </table>\n </Table>\n )}\n </Card>\n\n <Card>\n <form method=\"post\" action=\"/auth/logout\">\n {csrfField()}\n <Button variant=\"secondary\" type=\"submit\">\n Sign out\n </Button>\n </form>\n </Card>\n </DocumentLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAwDA,MAAM,gBAAgB,GAAG;;;;;;;AAQzB,MAAM,wBAAwB,GAAG;;;;;;;;;;;;AAajC,MAAM,cAAc,GAAG;;;;;AAMvB,MAAM,cAAc,GAAG;;;;;;AAYvB,MAAM,gBAAkD,EAAE,eAAe;CACvE,OACE,oBAAC,OAAD;EAAO,OAAA;EACL,UAAA,qBAAC,SAAD,EAAA,UAAA,CACE,oBAAC,SAAD,EAAA,UACE,qBAAC,MAAD,EAAA,UAAA;GACE,oBAAC,MAAD,EAAA,UAAI,MAAO,CAAA;GACX,oBAAC,MAAD,EAAA,UAAI,QAAS,CAAA;GACb,oBAAC,MAAD,EAAA,UAAI,SAAU,CAAA;EACZ,EAAA,CAAA,EACC,CAAA,GACP,oBAAC,SAAD,EAAA,UACG,SAAS,KAEP,QACC,qBAAC,MAAD,EAAA,UAAA;GACE,qBAAC,MAAD,EAAA,UAAA,CACE,oBAAC,OAAD,EAAA,UAAM,IAAI,QAAQ,cAAmB,CAAA,GACrC,oBAAC,OAAD;IAAK,OAAO;IAAc,UAAA,IAAI;GAAgB,CAAA,CAC5C,EAAA,CAAA;GACJ,oBAAC,MAAD,EAAA,UAAK,IAAI,KAAK,IAAI,SAAS,CAAC,CAAC,mBAAmB,EAAM,CAAA;GACtD,oBAAC,MAAD,EAAA,UACE,qBAAC,QAAD,EAAA,UAAA,CACG,IAAI,WAAW,oBAAC,OAAD,EAAA,UAAO,SAAa,CAAA,IAAI,MACxC,oBAAC,QAAD;IACE,QAAO;IACP,QAAQ,qBAAqB,IAAI,GAAG;IACpC,gBAAa;IAEb,UAAA,oBAAC,QAAD;KAAQ,SAAQ;KAAY,MAAK;KAAK,MAAK;KAAS,UAAA;IAE5C,CAAA;GACJ,CAAA,CACA,EAAA,CAAA,EACN,CAAA;EACF,EAAA,GApBK,IAAI,EAoBT,CAER,EACK,CAAA,CACF,EAAA,CAAA;CACF,CAAA;AAEX;;AAKA,MAAM,cAA4C,EAAE,cAAc;CAChE,OACE,qBAAC,MAAD,EAAA,UAAA;EACE,qBAAC,MAAD,EAAA,UAAA,CACE,oBAAC,OAAD;GAAK,OAAM;GAAY,UAAA,QAAQ,aAAa;EAAsB,CAAA,GAClE,oBAAC,OAAD;GAAK,OAAO;GACV,UAAA,oBAAC,QAAD;IAAM,OAAM;IAAQ,UAAA,QAAQ,aAAa;GAAU,CAAA;EAChD,CAAA,CACH,EAAA,CAAA;EACJ,oBAAC,MAAD,EAAA,UAAK,IAAI,KAAK,QAAQ,SAAS,CAAC,CAAC,eAAe,EAAM,CAAA;EACtD,oBAAC,MAAD,EAAA,UACG,QAAQ,UACP,oBAAC,OAAD,EAAA,UAAO,cAAkB,CAAA,IAEzB,qBAAC,QAAD;GACE,QAAO;GACP,QAAO;GACP,gBAAa;GAHf,UAAA,CAKE,oBAAC,SAAD;IAAO,MAAK;IAAS,MAAK;IAAY,OAAO,QAAQ;GAAK,CAAA,GAC1D,oBAAC,QAAD;IAAQ,SAAQ;IAAY,MAAK;IAAK,MAAK;IAAS,UAAA;GAE5C,CAAA,CACJ;EAEN,CAAA,EAAA,CAAA;CACF,EAAA,CAAA;AAER;;AAGA,MAAM,mBAAoD,EAAE,eAAe;CACzE,OACE,qBAAC,MAAD,EAAA,UAAA;EACE,oBAAC,YAAD;GAAY,OAAM;GAAU,aAAY;EAA2B,CAAA;EACnE,oBAAC,OAAD;GAAO,OAAA;GACL,UAAA,qBAAC,SAAD,EAAA,UAAA,CACE,oBAAC,SAAD,EAAA,UACE,qBAAC,MAAD,EAAA,UAAA;IACE,oBAAC,MAAD,EAAA,UAAI,SAAU,CAAA;IACd,oBAAC,MAAD,EAAA,UAAI,YAAa,CAAA;IACjB,oBAAC,MAAD,EAAA,UAAI,SAAU,CAAA;GACZ,EAAA,CAAA,EACC,CAAA,GACP,oBAAC,SAAD,EAAA,UACG,SAAS,SAAS,KAAK,YACtB,oBAAC,YAAD,EAAsC,QAAU,GAA/B,QAAQ,EAAuB,CACjD,EACI,CAAA,CACF,EAAA,CAAA;EACF,CAAA;EACN,SAAS,SAAS,SAAS,IAC1B,oBAAC,QAAD;GACE,QAAO;GACP,QAAO;GACP,gBAAa;GAEb,UAAA,oBAAC,QAAD;IAAQ,SAAQ;IAAY,MAAK;IAAK,MAAK;IAAS,UAAA;GAE5C,CAAA;EACJ,CAAA,IACJ;CACA,EAAA,CAAA;AAEV;;AAGA,MAAM,mBAAoD,EAAE,eAAe;CACzE,OACE,qBAAC,MAAD,EAAA,UAAA;EACE,oBAAC,YAAD;GAAY,OAAM;GAAW,aAAY;EAAgD,CAAA;EACxF,SAAS,SAAS,SAAS,IAC1B,oBAAC,OAAD;GAAO,OAAM;GAAmB,UAAA;EAEzB,CAAA,IACL;EACH,SAAS,SAAS,WAAW,IAC5B,oBAAC,KAAD;GAAG,OAAO;GAAa,UAAA;EAAmB,CAAA,IAE1C,oBAAC,cAAD,EAAc,UAAU,SAAS,SAAW,CAAA;EAE9C,qBAAC,QAAD,EAAA,UAAA,CACE,oBAAC,OAAD;GAAO,OAAM;GAAW,MAAK;GAAU,MAAK;GAAO,aAAY;EAAqB,CAAA,GACpF,oBAAC,QAAD;GAAQ,SAAQ;GAAU,MAAK;GAAK,MAAK;GAAS,yBAAsB;GAAO,UAAA;EAEvE,CAAA,CACF,EAAA,CAAA;EACR,oBAAC,KAAD;GAAG,OAAO;GAAa,uBAAoB;GAAO,aAAU;GACzD,UAAA;EACA,CAAA;EACH,oBAAC,UAAD,EAAQ,yBAAyB,EAAE,QAAQ,sBAAsB,EAAE,EAAI,CAAA;CACnE,EAAA,CAAA;AAEV;;AAIA,SAAgB,kBAAkB,OAA0C;CAC1E,MAAM,EAAE,MAAM,QAAQ,aAAa,iBAAiB,OAAO,SAAS,aAAa;CACjF,MAAM,SAAS,aAAa,KAAK,SAAS;CAC1C,MAAM,cAAc,QAAQ,uBAAuB,KAAK;CACxD,MAAM,WAAW,QAAQ,gBAAgB,KAAK,cAAc;CAC5D,OACE,qBAAC,gBAAD;EAAgB,OAAM;EAAU,KAAK,EAAE,QAAQ,UAAU;EAAzD,UAAA;GACE,oBAAC,YAAD;IAAY,OAAM;IAAU,aAAY;GAAwC,CAAA;GAC/E,QACC,oBAAC,OAAD;IAAO,MAAK;IAAS,OAAM;IACxB,UAAA;GACI,CAAA,IACL;GAEJ,oBAAC,MAAD,EAAA,UACE,qBAAC,QAAD,EAAA,UAAA,CACE,qBAAC,QAAD,EAAA,UAAA,CACG,SACC,oBAAC,OAAD;IAAK,OAAO;IAAe,KAAK;IAAQ,KAAI;IAAG,OAAM;IAAK,QAAO;GAAM,CAAA,IAEvE,oBAAC,QAAD;IAAM,OAAO;IAAuB,eAAY;IAC7C,UAAA,KAAK,KAAK,MAAM,GAAG,CAAC,CAAC,CAAC,YAAY;GAC/B,CAAA,GAER,qBAAC,QAAD,EAAA,UAAA;IACE,oBAAC,OAAD;KAAK,OAAO;KAAc,UAAA;IAAiB,CAAA;IAC3C,oBAAC,OAAD;KAAK,OAAO;KAAc,UAAA,KAAK;IAAW,CAAA;IAC1C,qBAAC,QAAD,EAAA,UAAA,CACE,oBAAC,OAAD,EAAA,UAAQ,KAAK,KAAY,CAAA,GACzB,qBAAC,OAAD,EAAA,UAAA,CAAO,QAAK,QAAgB,EAAA,CAAA,CACtB,EAAA,CAAA;GACF,EAAA,CAAA,CACF,EAAA,CAAA,GACR,qBAAC,OAAD;IAAK,OAAO;IAAZ,UAAA;KAAyB;KACT,QAAQ,YAAY,IAAI,KAAK,OAAO,SAAS,CAAC,CAAC,mBAAmB,IAAI;KAAK;KAAI;KAC/E,QAAQ,cAAc,IAAI,KAAK,OAAO,WAAW,CAAC,CAAC,eAAe,IAAI;IACjF;GACC,CAAA,CAAA,EAAA,CAAA,EACJ,CAAA;GAEN,qBAAC,MAAD,EAAA,UAAA,CACE,oBAAC,YAAD;IACE,OAAM;IACN,aAAY;GACb,CAAA,GACD,qBAAC,QAAD;IAAM,QAAO;IAAO,QAAO;IAAW,YAAA;IAAtC,UAAA,CACE,oBAAC,OAAD;KAAO,OAAM;KAAe,MAAK;KAAc,MAAK;KAAO,UAAA;KAAS,OAAO;IAAc,CAAA,GACzF,oBAAC,QAAD;KAAQ,SAAQ;KAAU,MAAK;KAAS,UAAA;IAEhC,CAAA,CACJ;GACF,CAAA,CAAA,EAAA,CAAA;GAEL,UACC,qBAAC,MAAD,EAAA,UAAA,CACE,oBAAC,YAAD;IAAY,OAAM;IAAkB,aAAY;GAA2B,CAAA,GAC3E,qBAAC,QAAD;IAAM,QAAO;IAAO,QAAO;IAAoB,YAAA;IAA/C,UAAA;KACE,oBAAC,OAAD;MACE,OAAM;MACN,MAAK;MACL,MAAK;MACL,UAAA;MACA,cAAa;KACd,CAAA;KACD,oBAAC,OAAD;MACE,OAAM;MACN,MAAK;MACL,MAAK;MACL,UAAA;MACA,cAAa;KACd,CAAA;KACD,oBAAC,OAAD;MACE,OAAM;MACN,MAAK;MACL,MAAK;MACL,UAAA;MACA,cAAa;KACd,CAAA;KACD,oBAAC,QAAD;MAAQ,SAAQ;MAAU,MAAK;MAAS,UAAA;KAEhC,CAAA;IACJ;GACF,CAAA,CAAA,EAAA,CAAA,IACJ;GAEH,WAAW,oBAAC,iBAAD,EAA2B,SAAW,CAAA,IAAI;GAErD,UAAU,kBAAkB,oBAAC,iBAAD,EAA2B,SAAW,CAAA,IAAI;GAEvE,qBAAC,MAAD,EAAA,UAAA;IACE,oBAAC,YAAD;KAAY,OAAM;KAAsB,aAAY;IAA6B,CAAA;IAAE;IAClF,YAAY,WAAW,IACtB,oBAAC,KAAD;KAAG,OAAO;KAAa,UAAA;IAA0B,CAAA,IAEjD,oBAAC,OAAD;KAAO,OAAA;KACL,UAAA,qBAAC,SAAD,EAAA,UAAA,CACE,oBAAC,SAAD,EAAA,UACE,qBAAC,MAAD,EAAA,UAAA;MACE,oBAAC,MAAD,EAAA,UAAI,UAAW,CAAA;MACf,oBAAC,MAAD,EAAA,UAAI,OAAQ,CAAA;MACZ,oBAAC,MAAD,EAAA,UAAI,SAAU,CAAA;KACZ,EAAA,CAAA,EACC,CAAA,GACP,oBAAC,SAAD,EAAA,UACG,YAAY,KAEV,MACC,qBAAC,MAAD,EAAA,UAAA;MACE,oBAAC,MAAD,EAAA,UACE,oBAAC,KAAD;OAAG,MAAM,aAAa,EAAE;OAAgB,UAAA,EAAE;MAAe,CAAA,EACvD,CAAA;MACJ,oBAAC,MAAD,EAAA,UACE,oBAAC,OAAD,EAAA,UAAQ,EAAE,KAAY,CAAA,EACpB,CAAA;MACJ,oBAAC,MAAD,EAAA,UAAK,EAAE,OAAW,CAAA;KAChB,EAAA,GARK,EAAE,WAQP,CAER,EACK,CAAA,CACF,EAAA,CAAA;IACF,CAAA;GAEL,EAAA,CAAA;GAEN,qBAAC,MAAD,EAAA,UAAA,CACE,oBAAC,YAAD;IACE,OAAM;IACN,aAAY;GACb,CAAA,GACA,YAAY,WAAW,IACtB,oBAAC,KAAD;IAAG,OAAO;IAAa,UAAA;GAA0B,CAAA,IAEjD,oBAAC,OAAD;IAAO,OAAA;IACL,UAAA,qBAAC,SAAD,EAAA,UAAA,CACE,oBAAC,SAAD,EAAA,UACE,qBAAC,MAAD,EAAA,UAAA;KACE,oBAAC,MAAD,EAAA,UAAI,UAAW,CAAA;KACf,oBAAC,MAAD,EAAA,UAAI,SAAU,CAAA;KACd,oBAAC,MAAD,EAAA,UACE,oBAAC,QAAD;MAAM,OAAM;MAAkB,UAAA;KAAa,CAAA,EACzC,CAAA;IACF,EAAA,CAAA,EACC,CAAA,GACP,oBAAC,SAAD,EAAA,UACG,YAAY,KAEV,MAAM;KACL,MAAM,aAAa,gBAAgB,SAAS,EAAE,WAAW;KACzD,OACE,qBAAC,MAAD,EAAA,UAAA;MACE,oBAAC,MAAD,EAAA,UACE,oBAAC,KAAD;OAAG,MAAM,aAAa,EAAE;OAAgB,UAAA,EAAE;MAAe,CAAA,EACvD,CAAA;MACJ,oBAAC,MAAD,EAAA,UAAK,aAAa,oBAAC,OAAD,EAAA,UAAO,WAAe,CAAA,IAAI,oBAAC,OAAD,EAAA,UAAO,MAAU,CAAA,EAAM,CAAA;MACnE,oBAAC,MAAD,EAAA,UACE,qBAAC,QAAD;OAAM,QAAO;OAAO,QAAO;OAA3B,UAAA;QACG,UAAU;QACX,oBAAC,SAAD;SAAO,MAAK;SAAS,MAAK;SAAO,OAAO,EAAE;QAAc,CAAA;QACxD,oBAAC,SAAD;SAAO,MAAK;SAAS,MAAK;SAAU,OAAO,aAAa,KAAK;QAAM,CAAA;QACnE,oBAAC,QAAD;SAAQ,SAAQ;SAAY,MAAK;SAAK,MAAK;SACxC,UAAA,aAAa,YAAY;QACpB,CAAA;OACJ;MACJ,CAAA,EAAA,CAAA;KACF,EAAA,GAfK,EAAE,WAeP;IAER,CACF,EACK,CAAA,CACF,EAAA,CAAA;GACF,CAAA,CAEL,EAAA,CAAA;GAEN,oBAAC,MAAD,EAAA,UACE,qBAAC,QAAD;IAAM,QAAO;IAAO,QAAO;IAA3B,UAAA,CACG,UAAU,GACX,oBAAC,QAAD;KAAQ,SAAQ;KAAY,MAAK;KAAS,UAAA;IAElC,CAAA,CACJ;GACF,CAAA,EAAA,CAAA;EACQ;;AAEpB"}
|
|
1
|
+
{"version":3,"file":"profile.mjs","names":[],"sources":["../../src/pages/profile.tsx"],"sourcesContent":["import type { AuthUser } from \"@storyshelf/core/types\";\nimport { safeImageUrl } from \"@storyshelf/core/urls\";\nimport type { FC } from \"hono/jsx\";\nimport {\n Alert,\n Badge,\n Button,\n Card,\n Field,\n HStack,\n PageHeader,\n Table,\n VStack,\n} from \"../ui/components.tsx\";\nimport { csrfField } from \"../ui/csrf-field.tsx\";\nimport { css } from \"../ui/css.ts\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\nimport { NotificationsPausedNotice } from \"./notifications-paused.tsx\";\nimport { passkeyRegisterScript } from \"./passkey-ceremony.ts\";\n\nexport interface ProfilePageState {\n user: AuthUser;\n dbUser?: {\n email: string;\n name: string;\n displayNameOverride: string | null;\n authProvider: string;\n lastLoginAt: string | null;\n createdAt: string;\n };\n memberships: Array<{ projectName: string; projectSlug: string; role: string; source: string }>;\n subscribedSlugs: string[];\n error?: string;\n /** Notifiers are wired but no public origin resolves (ADR 0014, decision 11). */\n notificationsPaused?: boolean | undefined;\n isLocal?: boolean;\n security?: {\n sessions: Array<{\n id: string;\n ipAddress: string | null;\n userAgent: string | null;\n createdAt: string;\n expiresAt: string;\n current: boolean;\n }>;\n passkeys: Array<{\n id: string;\n name: string | null;\n deviceType: string;\n backedUp: boolean;\n transports: string | null;\n aaguid: string | null;\n createdAt: string;\n }>;\n passkeysEnabled: boolean;\n hasPassword: boolean;\n };\n}\n\nconst profileAvatar = css`\n /* profile-avatar */\n width: 64px;\n height: 64px;\n border-radius: 999px;\n object-fit: cover;\n`;\n\nconst profileAvatarFallback = css`\n /* profile-avatar-fallback */\n width: 64px;\n height: 64px;\n display: grid;\n place-items: center;\n background: var(--accent);\n color: #fff;\n border-radius: 999px;\n font-size: 1.5rem;\n font-weight: 700;\n`;\n\nconst profileName = css`\n /* profile-name */\n font-weight: 700;\n font-size: 1.2rem;\n`;\n\nconst profileMeta = css`\n /* profile-meta */\n color: var(--text-secondary);\n font-size: 0.85rem;\n`;\n\n/* eslint-disable promise-function-async -- JSX components return HtmlEscapedString */\n\ntype SecurityState = NonNullable<ProfilePageState[\"security\"]>;\ntype PasskeyState = SecurityState[\"passkeys\"][number];\n\n/** Registered passkey rows with per-key remove actions. */\nconst PasskeyTable: FC<{ passkeys: PasskeyState[] }> = ({ passkeys }) => {\n return (\n <Table dense>\n <table>\n <thead>\n <tr>\n <th>Key</th>\n <th>Added</th>\n <th>Status</th>\n </tr>\n </thead>\n <tbody>\n {passkeys.map(\n // oxlint-disable-next-line typescript/promise-function-async -- JSX map is sync\n (key) => (\n <tr key={key.id}>\n <td>\n <div>{key.name ?? \"Unnamed key\"}</div>\n <div class={profileMeta}>{key.deviceType}</div>\n </td>\n <td>{new Date(key.createdAt).toLocaleDateString()}</td>\n <td>\n <HStack>\n {key.backedUp ? <Badge>Synced</Badge> : null}\n <form\n method=\"post\"\n action={`/profile/passkeys/${key.id}/delete`}\n data-confirm=\"Remove this passkey?\"\n >\n <Button variant=\"secondary\" size=\"sm\" type=\"submit\">\n Remove\n </Button>\n </form>\n </HStack>\n </td>\n </tr>\n ),\n )}\n </tbody>\n </table>\n </Table>\n );\n};\n\ntype SessionInfo = SecurityState[\"sessions\"][number];\n\n/** One signed-in device with its revoke action. */\nconst SessionRow: FC<{ session: SessionInfo }> = ({ session }) => {\n return (\n <tr>\n <td>\n <div class=\"truncate\">{session.userAgent ?? \"Unknown device\"}</div>\n <div class={profileMeta}>\n <span class=\"mono\">{session.ipAddress ?? \"—\"}</span>\n </div>\n </td>\n <td>{new Date(session.createdAt).toLocaleString()}</td>\n <td>\n {session.current ? (\n <Badge>This device</Badge>\n ) : (\n <form\n method=\"post\"\n action=\"/profile/sessions/revoke\"\n data-confirm=\"Sign this device out?\"\n >\n <input type=\"hidden\" name=\"sessionId\" value={session.id} />\n <Button variant=\"secondary\" size=\"sm\" type=\"submit\">\n Revoke\n </Button>\n </form>\n )}\n </td>\n </tr>\n );\n};\n\n/** Active device sessions with per-device revoke and sign-out-others. */\nconst SessionsSection: FC<{ security: SecurityState }> = ({ security }) => {\n return (\n <Card>\n <PageHeader title=\"Devices\" description=\"Where you are signed in\" />\n <Table dense>\n <table>\n <thead>\n <tr>\n <th>Device</th>\n <th>Signed in</th>\n <th>Status</th>\n </tr>\n </thead>\n <tbody>\n {security.sessions.map((session) => (\n <SessionRow key={session.id} session={session} />\n ))}\n </tbody>\n </table>\n </Table>\n {security.sessions.length > 1 ? (\n <form\n method=\"post\"\n action=\"/profile/sessions/revoke-others\"\n data-confirm=\"Sign out all other devices?\"\n >\n <Button variant=\"secondary\" size=\"sm\" type=\"submit\">\n Sign out other devices\n </Button>\n </form>\n ) : null}\n </Card>\n );\n};\n\n/** Registered passkeys with delete, register button, and the backup nudge. */\nconst PasskeysSection: FC<{ security: SecurityState }> = ({ security }) => {\n return (\n <Card>\n <PageHeader title=\"Passkeys\" description=\"Sign in without a password using your device\" />\n {security.passkeys.length < 2 ? (\n <Alert title=\"Add a backup key\">\n Register a 2nd key so a lost device doesn't lock you out.\n </Alert>\n ) : null}\n {security.passkeys.length === 0 ? (\n <p class={profileMeta}>No passkeys yet.</p>\n ) : (\n <PasskeyTable passkeys={security.passkeys} />\n )}\n <HStack>\n <Field label=\"Key name\" name=\"keyName\" type=\"text\" placeholder=\"e.g. Security key\" />\n <Button variant=\"primary\" size=\"sm\" type=\"button\" data-passkey-register=\"true\">\n Register a passkey\n </Button>\n </HStack>\n <p class={profileMeta} data-passkey-status=\"true\" aria-live=\"polite\">\n {\"\"}\n </p>\n <script dangerouslySetInnerHTML={{ __html: passkeyRegisterScript() }} />\n </Card>\n );\n};\n\n/** Personal profile page (editable display name, password for local accounts). */\n// oxlint-disable-next-line eslint/max-lines-per-function -- profile page composes several sections, cohesive\nexport function renderProfilePage(state: ProfilePageState): RenderedContent {\n const { user, dbUser, memberships, subscribedSlugs, error, isLocal, security } = state;\n const avatar = safeImageUrl(user.avatarUrl);\n const displayName = dbUser?.displayNameOverride ?? user.name;\n const provider = dbUser?.authProvider ?? user.providerId ?? \"oidc\";\n return (\n <DocumentLayout title=\"Profile\" nav={{ active: \"profile\" }}>\n <PageHeader title=\"Profile\" description=\"Your account and project memberships\" />\n {error ? (\n <Alert tone=\"danger\" title=\"Could not save\">\n {error}\n </Alert>\n ) : null}\n\n <Card>\n <VStack>\n <HStack>\n {avatar ? (\n <img class={profileAvatar} src={avatar} alt=\"\" width=\"64\" height=\"64\" />\n ) : (\n <span class={profileAvatarFallback} aria-hidden=\"true\">\n {user.name.slice(0, 1).toUpperCase()}\n </span>\n )}\n <VStack>\n <div class={profileName}>{displayName}</div>\n <div class={profileMeta}>{user.email}</div>\n <HStack>\n <Badge>{user.role}</Badge>\n <Badge>via {provider}</Badge>\n </HStack>\n </VStack>\n </HStack>\n <div class={profileMeta}>\n Member since {dbUser?.createdAt ? new Date(dbUser.createdAt).toLocaleDateString() : \"—\"}{\" \"}\n · Last login {dbUser?.lastLoginAt ? new Date(dbUser.lastLoginAt).toLocaleString() : \"—\"}\n </div>\n </VStack>\n </Card>\n\n <Card>\n <PageHeader\n title=\"Display name\"\n description=\"Shown in comments and the header. Survives SSO refresh.\"\n />\n <form method=\"post\" action=\"/profile\" novalidate>\n <Field label=\"Display name\" name=\"displayName\" type=\"text\" required value={displayName} />\n <Button variant=\"primary\" type=\"submit\">\n Save name\n </Button>\n </form>\n </Card>\n\n {isLocal ? (\n <Card>\n <PageHeader title=\"Change password\" description=\"For local accounts only\" />\n <form method=\"post\" action=\"/profile/password\" novalidate>\n <Field\n label=\"Current password\"\n name=\"currentPassword\"\n type=\"password\"\n required\n autocomplete=\"current-password\"\n />\n <Field\n label=\"New password\"\n name=\"newPassword\"\n type=\"password\"\n required\n autocomplete=\"new-password\"\n />\n <Field\n label=\"Confirm new password\"\n name=\"confirmPassword\"\n type=\"password\"\n required\n autocomplete=\"new-password\"\n />\n <Button variant=\"primary\" type=\"submit\">\n Change password\n </Button>\n </form>\n </Card>\n ) : null}\n\n {security ? <SessionsSection security={security} /> : null}\n\n {security?.passkeysEnabled ? <PasskeysSection security={security} /> : null}\n\n <Card>\n <PageHeader title=\"Project memberships\" description=\"Your role on each project\" />{\" \"}\n {memberships.length === 0 ? (\n <p class={profileMeta}>No project memberships.</p>\n ) : (\n <Table dense>\n <table>\n <thead>\n <tr>\n <th>Project</th>\n <th>Role</th>\n <th>Source</th>\n </tr>\n </thead>\n <tbody>\n {memberships.map(\n // oxlint-disable-next-line typescript/promise-function-async -- JSX map is sync\n (m) => (\n <tr key={m.projectSlug}>\n <td>\n <a href={`/projects/${m.projectSlug}`}>{m.projectName}</a>\n </td>\n <td>\n <Badge>{m.role}</Badge>\n </td>\n <td>{m.source}</td>\n </tr>\n ),\n )}\n </tbody>\n </table>\n </Table>\n )}\n </Card>\n\n <Card>\n <PageHeader\n title=\"Notifications\"\n description=\"Email alerts per project. Fine-tune topics in each project's settings.\"\n />\n <NotificationsPausedNotice paused={state.notificationsPaused} />\n {memberships.length === 0 ? (\n <p class={profileMeta}>No project memberships.</p>\n ) : (\n <Table dense>\n <table>\n <thead>\n <tr>\n <th>Project</th>\n <th>Status</th>\n <th>\n <span class=\"visually-hidden\">Actions</span>\n </th>\n </tr>\n </thead>\n <tbody>\n {memberships.map(\n // oxlint-disable-next-line typescript/promise-function-async -- JSX map is sync\n (m) => {\n const subscribed = subscribedSlugs.includes(m.projectSlug);\n return (\n <tr key={m.projectSlug}>\n <td>\n <a href={`/projects/${m.projectSlug}`}>{m.projectName}</a>\n </td>\n <td>{subscribed ? <Badge>Email on</Badge> : <Badge>Off</Badge>}</td>\n <td>\n <form method=\"post\" action=\"/profile/notifications\">\n {csrfField()}\n <input type=\"hidden\" name=\"slug\" value={m.projectSlug} />\n <input type=\"hidden\" name=\"enabled\" value={subscribed ? \"\" : \"1\"} />\n <Button variant=\"secondary\" size=\"sm\" type=\"submit\">\n {subscribed ? \"Disable\" : \"Enable\"}\n </Button>\n </form>\n </td>\n </tr>\n );\n },\n )}\n </tbody>\n </table>\n </Table>\n )}\n </Card>\n\n <Card>\n <form method=\"post\" action=\"/auth/logout\">\n {csrfField()}\n <Button variant=\"secondary\" type=\"submit\">\n Sign out\n </Button>\n </form>\n </Card>\n </DocumentLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
-
import { getStore } from "../store.mjs";
|
|
4
|
+
import { getStore, testingEnabled } from "../store.mjs";
|
|
5
5
|
import { css } from "../ui/css.mjs";
|
|
6
6
|
import { Button } from "../ui/buttons.mjs";
|
|
7
7
|
import { Badge, Meta, statusTone } from "../ui/feedback.mjs";
|
|
@@ -11,7 +11,7 @@ import { Table } from "../ui/table.mjs";
|
|
|
11
11
|
import { RelativeTime } from "../ui/time.mjs";
|
|
12
12
|
import { jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
13
13
|
import { createUrlBuilder } from "@storyshelf/core/urls";
|
|
14
|
-
|
|
14
|
+
// src/pages/project-builds-table.tsx
|
|
15
15
|
const buildCell = css`
|
|
16
16
|
/* build-cell */
|
|
17
17
|
display: grid;
|
|
@@ -71,7 +71,7 @@ function BuildSummaryCell({ project, build }) {
|
|
|
71
71
|
}
|
|
72
72
|
function BuildActions({ project, build }) {
|
|
73
73
|
const urls = createUrlBuilder("/", getStore().config.publishedBaseDomain);
|
|
74
|
-
const needsReview = build.status === "reviewing";
|
|
74
|
+
const needsReview = build.status === "reviewing" && testingEnabled();
|
|
75
75
|
return /* @__PURE__ */ jsxs(HStack, {
|
|
76
76
|
wrap: false,
|
|
77
77
|
justify: "end",
|
|
@@ -145,7 +145,6 @@ function BuildsTable({ project, builds }) {
|
|
|
145
145
|
build
|
|
146
146
|
}, build.id)) })] }) });
|
|
147
147
|
}
|
|
148
|
-
//#endregion
|
|
149
148
|
export { BuildsTable };
|
|
150
149
|
|
|
151
150
|
//# sourceMappingURL=project-builds-table.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project-builds-table.mjs","names":[],"sources":["../../src/pages/project-builds-table.tsx"],"sourcesContent":["import type { Build, Project } from \"@storyshelf/core/schema\";\nimport { createUrlBuilder } from \"@storyshelf/core/urls\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport { getStore } from \"../store.ts\";\nimport {\n Avatar,\n Badge,\n Button,\n HStack,\n Meta,\n RelativeTime,\n Table,\n statusTone,\n} from \"../ui/components.tsx\";\nimport { css } from \"../ui/css.ts\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\nconst buildCell = css`\n /* build-cell */\n display: grid;\n gap: 2px;\n min-width: 0;\n max-width: 38ch;\n & strong a {\n color: var(--text-primary);\n }\n & strong a:hover {\n color: var(--accent-fg);\n }\n`;\n\nconst messageLine = css`\n /* build-message */\n overflow: hidden;\n color: var(--text-secondary);\n font-size: var(--text-sm);\n text-overflow: ellipsis;\n white-space: nowrap;\n`;\n\nconst authorCell = css`\n /* build-author */\n display: flex;\n gap: var(--space-2);\n align-items: center;\n white-space: nowrap;\n`;\n\n/** \"4 to review · 1 approved · 12 snapshots\", leaving out zero counts. */\nfunction changeSummary(build: Build): string {\n const parts: string[] = [];\n if (build.changedCount > 0) {\n parts.push(`${build.changedCount} to review`);\n }\n if (build.approvedCount > 0) {\n parts.push(`${build.approvedCount} approved`);\n }\n parts.push(`${build.snapshotCount} ${build.snapshotCount === 1 ? \"snapshot\" : \"snapshots\"}`);\n return parts.join(\" · \");\n}\n\nfunction BuildSummaryCell({\n project,\n build,\n}: {\n project: Project;\n build: Build;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const urls = createUrlBuilder(\"/\", getStore().config.publishedBaseDomain);\n return (\n <div class={buildCell}>\n <strong>\n <a href={urls.build(project.slug, build.id)}>{build.gitBranch}</a>{\" \"}\n <Meta as=\"span\" mono>\n {build.gitSha.slice(0, 7)}\n </Meta>\n </strong>\n <span class={messageLine}>{build.message ?? \"No message\"}</span>\n </div>\n );\n}\n\nfunction BuildActions({\n project,\n build,\n}: {\n project: Project;\n build: Build;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const urls = createUrlBuilder(\"/\", getStore().config.publishedBaseDomain);\n const needsReview = build.status === \"reviewing\";\n return (\n <HStack wrap={false} justify=\"end\">\n <Button\n variant={needsReview ? \"primary\" : \"secondary\"}\n size=\"sm\"\n href={\n needsReview ? urls.buildDiff(project.slug, build.id) : urls.build(project.slug, build.id)\n }\n >\n {needsReview ? \"Review\" : \"View\"}\n </Button>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n icon=\"external-link\"\n href={urls.short(build.id)}\n aria-label=\"View Storybook\"\n title=\"View Storybook\"\n />\n </HStack>\n );\n}\n\nfunction BuildRow({\n project,\n build,\n}: {\n project: Project;\n build: Build;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n return (\n <tr>\n <td>\n <BuildSummaryCell project={project} build={build} />\n </td>\n <td>\n <Badge tone={statusTone(build.status)} icon>\n {build.status}\n </Badge>\n </td>\n <td>\n <Meta as=\"span\">{changeSummary(build)}</Meta>\n </td>\n <td>\n <div class={authorCell}>\n {build.authorName ? <Avatar name={build.authorName} size=\"sm\" /> : null}\n <span>{build.authorName ?? \"—\"}</span>\n </div>\n </td>\n <td class=\"nowrap\">\n <Meta as=\"span\">\n <RelativeTime value={build.createdAt} />\n </Meta>\n </td>\n <td class=\"nowrap\">\n <BuildActions project={project} build={build} />\n </td>\n </tr>\n );\n}\n\n/** Builds as a table: branch/sha, status, change summary, author, age, actions. */\nexport function BuildsTable({\n project,\n builds,\n}: {\n project: Project;\n builds: Build[];\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n return (\n <Table>\n <table>\n <thead>\n <tr>\n <th>Build</th>\n <th>Status</th>\n <th>Changes</th>\n <th>Author</th>\n <th>Created</th>\n <th>\n <span class=\"visually-hidden\">Actions</span>\n </th>\n </tr>\n </thead>\n <tbody>\n {builds.map((build) => (\n <BuildRow key={build.id} project={project} build={build} />\n ))}\n </tbody>\n </table>\n </Table>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;AAkBA,MAAM,YAAY,GAAG;;;;;;;;;;;;;AAcrB,MAAM,cAAc,GAAG;;;;;;;;AASvB,MAAM,aAAa,GAAG;;;;;;;;AAStB,SAAS,cAAc,OAAsB;CAC3C,MAAM,QAAkB,CAAC;CACzB,IAAI,MAAM,eAAe,GACvB,MAAM,KAAK,GAAG,MAAM,aAAa,WAAW;CAE9C,IAAI,MAAM,gBAAgB,GACxB,MAAM,KAAK,GAAG,MAAM,cAAc,UAAU;CAE9C,MAAM,KAAK,GAAG,MAAM,cAAc,GAAG,MAAM,kBAAkB,IAAI,aAAa,aAAa;CAC3F,OAAO,MAAM,KAAK,KAAK;AACzB;AAEA,SAAS,iBAAiB,EACxB,SACA,SAIiD;CACjD,MAAM,OAAO,iBAAiB,KAAK,SAAS,CAAC,CAAC,OAAO,mBAAmB;CACxE,OACE,qBAAC,OAAD;EAAK,OAAO;EAAZ,UAAA,CACE,qBAAC,UAAD,EAAA,UAAA;GACE,oBAAC,KAAD;IAAG,MAAM,KAAK,MAAM,QAAQ,MAAM,MAAM,EAAE;IAAI,UAAA,MAAM;GAAa,CAAA;GAAE;GACnE,oBAAC,MAAD;IAAM,IAAG;IAAO,MAAA;IACb,UAAA,MAAM,OAAO,MAAM,GAAG,CAAC;GACpB,CAAA;EACA,EAAA,CAAA,GACR,oBAAC,QAAD;GAAM,OAAO;GAAc,UAAA,MAAM,WAAW;EAAmB,CAAA,CAC5D;;AAET;AAEA,SAAS,aAAa,EACpB,SACA,SAIiD;CACjD,MAAM,OAAO,iBAAiB,KAAK,SAAS,CAAC,CAAC,OAAO,mBAAmB;
|
|
1
|
+
{"version":3,"file":"project-builds-table.mjs","names":[],"sources":["../../src/pages/project-builds-table.tsx"],"sourcesContent":["import type { Build, Project } from \"@storyshelf/core/schema\";\nimport { createUrlBuilder } from \"@storyshelf/core/urls\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport { getStore, testingEnabled } from \"../store.ts\";\nimport {\n Avatar,\n Badge,\n Button,\n HStack,\n Meta,\n RelativeTime,\n Table,\n statusTone,\n} from \"../ui/components.tsx\";\nimport { css } from \"../ui/css.ts\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\nconst buildCell = css`\n /* build-cell */\n display: grid;\n gap: 2px;\n min-width: 0;\n max-width: 38ch;\n & strong a {\n color: var(--text-primary);\n }\n & strong a:hover {\n color: var(--accent-fg);\n }\n`;\n\nconst messageLine = css`\n /* build-message */\n overflow: hidden;\n color: var(--text-secondary);\n font-size: var(--text-sm);\n text-overflow: ellipsis;\n white-space: nowrap;\n`;\n\nconst authorCell = css`\n /* build-author */\n display: flex;\n gap: var(--space-2);\n align-items: center;\n white-space: nowrap;\n`;\n\n/** \"4 to review · 1 approved · 12 snapshots\", leaving out zero counts. */\nfunction changeSummary(build: Build): string {\n const parts: string[] = [];\n if (build.changedCount > 0) {\n parts.push(`${build.changedCount} to review`);\n }\n if (build.approvedCount > 0) {\n parts.push(`${build.approvedCount} approved`);\n }\n parts.push(`${build.snapshotCount} ${build.snapshotCount === 1 ? \"snapshot\" : \"snapshots\"}`);\n return parts.join(\" · \");\n}\n\nfunction BuildSummaryCell({\n project,\n build,\n}: {\n project: Project;\n build: Build;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const urls = createUrlBuilder(\"/\", getStore().config.publishedBaseDomain);\n return (\n <div class={buildCell}>\n <strong>\n <a href={urls.build(project.slug, build.id)}>{build.gitBranch}</a>{\" \"}\n <Meta as=\"span\" mono>\n {build.gitSha.slice(0, 7)}\n </Meta>\n </strong>\n <span class={messageLine}>{build.message ?? \"No message\"}</span>\n </div>\n );\n}\n\nfunction BuildActions({\n project,\n build,\n}: {\n project: Project;\n build: Build;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const urls = createUrlBuilder(\"/\", getStore().config.publishedBaseDomain);\n // The diff page is a testing route; hosting-only instances link to the build page.\n const needsReview = build.status === \"reviewing\" && testingEnabled();\n return (\n <HStack wrap={false} justify=\"end\">\n <Button\n variant={needsReview ? \"primary\" : \"secondary\"}\n size=\"sm\"\n href={\n needsReview ? urls.buildDiff(project.slug, build.id) : urls.build(project.slug, build.id)\n }\n >\n {needsReview ? \"Review\" : \"View\"}\n </Button>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n icon=\"external-link\"\n href={urls.short(build.id)}\n aria-label=\"View Storybook\"\n title=\"View Storybook\"\n />\n </HStack>\n );\n}\n\nfunction BuildRow({\n project,\n build,\n}: {\n project: Project;\n build: Build;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n return (\n <tr>\n <td>\n <BuildSummaryCell project={project} build={build} />\n </td>\n <td>\n <Badge tone={statusTone(build.status)} icon>\n {build.status}\n </Badge>\n </td>\n <td>\n <Meta as=\"span\">{changeSummary(build)}</Meta>\n </td>\n <td>\n <div class={authorCell}>\n {build.authorName ? <Avatar name={build.authorName} size=\"sm\" /> : null}\n <span>{build.authorName ?? \"—\"}</span>\n </div>\n </td>\n <td class=\"nowrap\">\n <Meta as=\"span\">\n <RelativeTime value={build.createdAt} />\n </Meta>\n </td>\n <td class=\"nowrap\">\n <BuildActions project={project} build={build} />\n </td>\n </tr>\n );\n}\n\n/** Builds as a table: branch/sha, status, change summary, author, age, actions. */\nexport function BuildsTable({\n project,\n builds,\n}: {\n project: Project;\n builds: Build[];\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n return (\n <Table>\n <table>\n <thead>\n <tr>\n <th>Build</th>\n <th>Status</th>\n <th>Changes</th>\n <th>Author</th>\n <th>Created</th>\n <th>\n <span class=\"visually-hidden\">Actions</span>\n </th>\n </tr>\n </thead>\n <tbody>\n {builds.map((build) => (\n <BuildRow key={build.id} project={project} build={build} />\n ))}\n </tbody>\n </table>\n </Table>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,24 +1,27 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
-
import { getStore } from "../store.mjs";
|
|
4
|
+
import { getStore, testingEnabled } from "../store.mjs";
|
|
5
5
|
import { Button } from "../ui/buttons.mjs";
|
|
6
|
-
import { Badge, EmptyState } from "../ui/feedback.mjs";
|
|
6
|
+
import { Badge, EmptyState, Meta, Stat } from "../ui/feedback.mjs";
|
|
7
7
|
import { SelectField } from "../ui/forms.mjs";
|
|
8
|
-
import { PageHeader } from "../ui/layout.mjs";
|
|
8
|
+
import { Card, PageHeader } from "../ui/layout.mjs";
|
|
9
9
|
import { HStack, VStack } from "../ui/stacks.mjs";
|
|
10
10
|
import { Segmented } from "../ui/segmented.mjs";
|
|
11
11
|
import { DocumentLayout } from "../ui/document.mjs";
|
|
12
|
+
import { loadHealthPanel } from "../insights/health-panel.mjs";
|
|
13
|
+
import { HealthSlot } from "./health-panel.mjs";
|
|
12
14
|
import { BuildsTable } from "./project-builds-table.mjs";
|
|
13
|
-
import { BuildModel, ProjectModel } from "@storyshelf/core/models";
|
|
15
|
+
import { BuildModel, ProjectModel, averageInheritedRatio } from "@storyshelf/core/models";
|
|
14
16
|
import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
15
17
|
import { createUrlBuilder } from "@storyshelf/core/urls";
|
|
16
|
-
|
|
18
|
+
// src/pages/project-builds.tsx
|
|
17
19
|
const STATUSES = [
|
|
18
20
|
"reviewing",
|
|
19
21
|
"approved",
|
|
20
22
|
"rejected",
|
|
21
23
|
"failed",
|
|
24
|
+
"cancelled",
|
|
22
25
|
"pending"
|
|
23
26
|
];
|
|
24
27
|
function listHref(base, filters) {
|
|
@@ -86,6 +89,24 @@ function BuildsToolbar(props) {
|
|
|
86
89
|
})]
|
|
87
90
|
});
|
|
88
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Affected-capture efficiency: the average share of a snapshot set that recent builds inherited
|
|
94
|
+
* from the baseline instead of rendering, from the counts stored on the builds (no snapshot reads).
|
|
95
|
+
*/
|
|
96
|
+
function InheritedRatioCard(props) {
|
|
97
|
+
const average = averageInheritedRatio(props.builds);
|
|
98
|
+
if (average === null) return null;
|
|
99
|
+
return /* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsx(Stat, {
|
|
100
|
+
label: "Inherited from baseline",
|
|
101
|
+
value: `${Math.round(average.ratio * 100)}%`
|
|
102
|
+
}), /* @__PURE__ */ jsxs(Meta, { children: [
|
|
103
|
+
"Average over the last ",
|
|
104
|
+
average.builds,
|
|
105
|
+
" ",
|
|
106
|
+
average.builds === 1 ? "build" : "builds",
|
|
107
|
+
": the share of snapshots reused without rendering."
|
|
108
|
+
] })] });
|
|
109
|
+
}
|
|
89
110
|
/** Project builds page: filterable build history for one project. */
|
|
90
111
|
async function renderProjectBuildsPage(slug, query = {}) {
|
|
91
112
|
const project = (await new ProjectModel(getStore().db).list()).find((item) => item.slug === slug);
|
|
@@ -94,6 +115,7 @@ async function renderProjectBuildsPage(slug, query = {}) {
|
|
|
94
115
|
const builds = all.filter((build) => matches(build, query));
|
|
95
116
|
const urls = createUrlBuilder("/", getStore().config.publishedBaseDomain);
|
|
96
117
|
const filtered = Boolean(query.status ?? query.branch);
|
|
118
|
+
const health = await loadHealthPanel(project);
|
|
97
119
|
return /* @__PURE__ */ jsxs(DocumentLayout, {
|
|
98
120
|
title: project.name,
|
|
99
121
|
nav: {
|
|
@@ -118,26 +140,33 @@ async function renderProjectBuildsPage(slug, query = {}) {
|
|
|
118
140
|
href: urls.settings(project.slug),
|
|
119
141
|
children: "Settings"
|
|
120
142
|
})
|
|
121
|
-
}), /* @__PURE__ */ jsxs(VStack, { children: [
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
143
|
+
}), /* @__PURE__ */ jsxs(VStack, { children: [
|
|
144
|
+
/* @__PURE__ */ jsx(HealthSlot, {
|
|
145
|
+
project,
|
|
146
|
+
data: health
|
|
147
|
+
}),
|
|
148
|
+
testingEnabled() ? /* @__PURE__ */ jsx(InheritedRatioCard, { builds: all }) : null,
|
|
149
|
+
/* @__PURE__ */ jsx(BuildsToolbar, {
|
|
150
|
+
project,
|
|
151
|
+
all,
|
|
152
|
+
filters: query,
|
|
153
|
+
base: urls.buildsList(project.slug)
|
|
154
|
+
}),
|
|
155
|
+
builds.length === 0 ? /* @__PURE__ */ jsx(EmptyState, {
|
|
156
|
+
title: "No builds",
|
|
157
|
+
description: filtered ? "No builds match the current filter." : "Upload your first build with the CLI. Builds appear here once uploaded.",
|
|
158
|
+
action: filtered ? /* @__PURE__ */ jsx(Button, {
|
|
159
|
+
variant: "secondary",
|
|
160
|
+
href: urls.buildsList(project.slug),
|
|
161
|
+
children: "Clear filters"
|
|
162
|
+
}) : null
|
|
163
|
+
}) : /* @__PURE__ */ jsx(BuildsTable, {
|
|
164
|
+
project,
|
|
165
|
+
builds
|
|
166
|
+
})
|
|
167
|
+
] })]
|
|
138
168
|
});
|
|
139
169
|
}
|
|
140
|
-
//#endregion
|
|
141
170
|
export { renderProjectBuildsPage };
|
|
142
171
|
|
|
143
172
|
//# sourceMappingURL=project-builds.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project-builds.mjs","names":[],"sources":["../../src/pages/project-builds.tsx"],"sourcesContent":["import { BuildModel, ProjectModel } from \"@storyshelf/core/models\";\nimport type { Build, Project } from \"@storyshelf/core/schema\";\nimport { createUrlBuilder } from \"@storyshelf/core/urls\";\nimport { getStore } from \"../store.ts\";\nimport {\n Badge,\n Button,\n EmptyState,\n HStack,\n PageHeader,\n Segmented,\n SelectField,\n VStack,\n} from \"../ui/components.tsx\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\nimport { BuildsTable } from \"./project-builds-table.tsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\nconst STATUSES = [\"reviewing\", \"approved\", \"rejected\", \"failed\", \"pending\"] as const;\n\n/** Current list filters (from the query string). */\ninterface BuildFilters {\n status?: string;\n branch?: string;\n}\n\nfunction listHref(base: string, filters: BuildFilters): string {\n const params = new URLSearchParams();\n if (filters.status) {\n params.set(\"status\", filters.status);\n }\n if (filters.branch) {\n params.set(\"branch\", filters.branch);\n }\n const query = params.toString();\n return query ? `${base}?${query}` : base;\n}\n\nfunction matches(build: Build, filters: BuildFilters): boolean {\n return (\n (!filters.status || build.status === filters.status) &&\n (!filters.branch || build.gitBranch === filters.branch)\n );\n}\n\n/** Status chips (with counts) plus a branch select that applies instantly. */\nfunction BuildsToolbar(props: {\n project: Project;\n all: Build[];\n filters: BuildFilters;\n base: string;\n}): ReturnType<typeof HStack> {\n const { all, filters, base } = props;\n const branches = [...new Set(all.map((build) => build.gitBranch))].toSorted();\n const inBranch = all.filter((build) => !filters.branch || build.gitBranch === filters.branch);\n const count = (status: string): number =>\n inBranch.filter((build) => build.status === status).length;\n const items = [\n { label: `All ${inBranch.length}`, value: \"\", status: undefined },\n ...STATUSES.filter((status) => count(status) > 0 || filters.status === status).map(\n (status) => ({ label: `${status} ${count(status)}`, value: status, status }),\n ),\n ];\n return (\n <HStack justify=\"between\">\n <Segmented\n label=\"Filter by status\"\n items={items.map((item) => ({\n label: item.label,\n value: item.value,\n href: listHref(base, { ...filters, status: item.status }),\n active: (filters.status ?? \"\") === item.value,\n }))}\n />\n <form\n method=\"get\"\n action={base}\n hx-get={base}\n hx-trigger=\"change\"\n hx-target=\"body\"\n hx-push-url=\"true\"\n >\n {filters.status ? <input type=\"hidden\" name=\"status\" value={filters.status} /> : null}\n <SelectField\n label=\"Branch\"\n name=\"branch\"\n layout=\"inline\"\n value={filters.branch ?? \"\"}\n options={[\n { value: \"\", label: \"All branches\" },\n ...branches.map((branch) => ({ value: branch, label: branch })),\n ]}\n />\n </form>\n </HStack>\n );\n}\n\n/** Project builds page: filterable build history for one project. */\nexport async function renderProjectBuildsPage(\n slug: string,\n query: BuildFilters = {},\n): Promise<RenderedContent | null> {\n const projects = await new ProjectModel(getStore().db).list();\n const project = projects.find((item) => item.slug === slug);\n if (!project) {\n return null;\n }\n const all = await new BuildModel(getStore().db).list(project.id);\n const builds = all.filter((build) => matches(build, query));\n const urls = createUrlBuilder(\"/\", getStore().config.publishedBaseDomain);\n const filtered = Boolean(query.status ?? query.branch);\n\n return (\n <DocumentLayout\n title={project.name}\n nav={{ active: \"builds\", projectSlug: project.slug, projectName: project.name }}\n >\n <PageHeader\n title=\"Builds\"\n description={\n <>\n {project.gitRepository ?? project.slug} · default branch{\" \"}\n <Badge tone=\"neutral\">{project.gitDefaultBranch}</Badge>\n </>\n }\n actions={\n <Button variant=\"secondary\" icon=\"settings\" href={urls.settings(project.slug)}>\n Settings\n </Button>\n }\n />\n <VStack>\n <BuildsToolbar\n project={project}\n all={all}\n filters={query}\n base={urls.buildsList(project.slug)}\n />\n {builds.length === 0 ? (\n <EmptyState\n title=\"No builds\"\n description={\n filtered\n ? \"No builds match the current filter.\"\n : \"Upload your first build with the CLI. Builds appear here once uploaded.\"\n }\n action={\n filtered ? (\n <Button variant=\"secondary\" href={urls.buildsList(project.slug)}>\n Clear filters\n </Button>\n ) : null\n }\n />\n ) : (\n <BuildsTable project={project} builds={builds} />\n )}\n </VStack>\n </DocumentLayout>\n );\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"project-builds.mjs","names":[],"sources":["../../src/pages/project-builds.tsx"],"sourcesContent":["import { BuildModel, ProjectModel, averageInheritedRatio } from \"@storyshelf/core/models\";\nimport type { Build, Project } from \"@storyshelf/core/schema\";\nimport { createUrlBuilder } from \"@storyshelf/core/urls\";\nimport { loadHealthPanel } from \"../insights/health-panel.ts\";\nimport { getStore, testingEnabled } from \"../store.ts\";\nimport {\n Badge,\n Button,\n Card,\n EmptyState,\n HStack,\n Meta,\n PageHeader,\n Segmented,\n SelectField,\n Stat,\n VStack,\n} from \"../ui/components.tsx\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\nimport { HealthSlot } from \"./health-panel.tsx\";\nimport { BuildsTable } from \"./project-builds-table.tsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\nconst STATUSES = [\"reviewing\", \"approved\", \"rejected\", \"failed\", \"cancelled\", \"pending\"] as const;\n\n/** Current list filters (from the query string). */\ninterface BuildFilters {\n status?: string;\n branch?: string;\n}\n\nfunction listHref(base: string, filters: BuildFilters): string {\n const params = new URLSearchParams();\n if (filters.status) {\n params.set(\"status\", filters.status);\n }\n if (filters.branch) {\n params.set(\"branch\", filters.branch);\n }\n const query = params.toString();\n return query ? `${base}?${query}` : base;\n}\n\nfunction matches(build: Build, filters: BuildFilters): boolean {\n return (\n (!filters.status || build.status === filters.status) &&\n (!filters.branch || build.gitBranch === filters.branch)\n );\n}\n\n/** Status chips (with counts) plus a branch select that applies instantly. */\nfunction BuildsToolbar(props: {\n project: Project;\n all: Build[];\n filters: BuildFilters;\n base: string;\n}): ReturnType<typeof HStack> {\n const { all, filters, base } = props;\n const branches = [...new Set(all.map((build) => build.gitBranch))].toSorted();\n const inBranch = all.filter((build) => !filters.branch || build.gitBranch === filters.branch);\n const count = (status: string): number =>\n inBranch.filter((build) => build.status === status).length;\n const items = [\n { label: `All ${inBranch.length}`, value: \"\", status: undefined },\n ...STATUSES.filter((status) => count(status) > 0 || filters.status === status).map(\n (status) => ({ label: `${status} ${count(status)}`, value: status, status }),\n ),\n ];\n return (\n <HStack justify=\"between\">\n <Segmented\n label=\"Filter by status\"\n items={items.map((item) => ({\n label: item.label,\n value: item.value,\n href: listHref(base, { ...filters, status: item.status }),\n active: (filters.status ?? \"\") === item.value,\n }))}\n />\n <form\n method=\"get\"\n action={base}\n hx-get={base}\n hx-trigger=\"change\"\n hx-target=\"body\"\n hx-push-url=\"true\"\n >\n {filters.status ? <input type=\"hidden\" name=\"status\" value={filters.status} /> : null}\n <SelectField\n label=\"Branch\"\n name=\"branch\"\n layout=\"inline\"\n value={filters.branch ?? \"\"}\n options={[\n { value: \"\", label: \"All branches\" },\n ...branches.map((branch) => ({ value: branch, label: branch })),\n ]}\n />\n </form>\n </HStack>\n );\n}\n\n/**\n * Affected-capture efficiency: the average share of a snapshot set that recent builds inherited\n * from the baseline instead of rendering, from the counts stored on the builds (no snapshot reads).\n */\nfunction InheritedRatioCard(props: { builds: Build[] }): ReturnType<typeof Card> | null {\n const average = averageInheritedRatio(props.builds);\n if (average === null) {\n return null;\n }\n return (\n <Card>\n <Stat label=\"Inherited from baseline\" value={`${Math.round(average.ratio * 100)}%`} />\n <Meta>\n Average over the last {average.builds} {average.builds === 1 ? \"build\" : \"builds\"}: the\n share of snapshots reused without rendering.\n </Meta>\n </Card>\n );\n}\n\n/** Project builds page: filterable build history for one project. */\nexport async function renderProjectBuildsPage(\n slug: string,\n query: BuildFilters = {},\n): Promise<RenderedContent | null> {\n const projects = await new ProjectModel(getStore().db).list();\n const project = projects.find((item) => item.slug === slug);\n if (!project) {\n return null;\n }\n const all = await new BuildModel(getStore().db).list(project.id);\n const builds = all.filter((build) => matches(build, query));\n const urls = createUrlBuilder(\"/\", getStore().config.publishedBaseDomain);\n const filtered = Boolean(query.status ?? query.branch);\n const health = await loadHealthPanel(project);\n\n return (\n <DocumentLayout\n title={project.name}\n nav={{ active: \"builds\", projectSlug: project.slug, projectName: project.name }}\n >\n <PageHeader\n title=\"Builds\"\n description={\n <>\n {project.gitRepository ?? project.slug} · default branch{\" \"}\n <Badge tone=\"neutral\">{project.gitDefaultBranch}</Badge>\n </>\n }\n actions={\n <Button variant=\"secondary\" icon=\"settings\" href={urls.settings(project.slug)}>\n Settings\n </Button>\n }\n />\n <VStack>\n <HealthSlot project={project} data={health} />\n {testingEnabled() ? <InheritedRatioCard builds={all} /> : null}\n <BuildsToolbar\n project={project}\n all={all}\n filters={query}\n base={urls.buildsList(project.slug)}\n />\n {builds.length === 0 ? (\n <EmptyState\n title=\"No builds\"\n description={\n filtered\n ? \"No builds match the current filter.\"\n : \"Upload your first build with the CLI. Builds appear here once uploaded.\"\n }\n action={\n filtered ? (\n <Button variant=\"secondary\" href={urls.buildsList(project.slug)}>\n Clear filters\n </Button>\n ) : null\n }\n />\n ) : (\n <BuildsTable project={project} builds={builds} />\n )}\n </VStack>\n </DocumentLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -8,7 +8,7 @@ import { Card, PageHeader } from "../ui/layout.mjs";
|
|
|
8
8
|
import { HStack } from "../ui/stacks.mjs";
|
|
9
9
|
import { DocumentLayout } from "../ui/document.mjs";
|
|
10
10
|
import { jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
11
|
-
|
|
11
|
+
// src/pages/project-create.tsx
|
|
12
12
|
/** New-project form page (one project corresponds to one Storybook). */
|
|
13
13
|
function renderProjectCreatePage(state = {}) {
|
|
14
14
|
const values = state.values ?? {};
|
|
@@ -84,7 +84,6 @@ function renderProjectCreatePage(state = {}) {
|
|
|
84
84
|
]
|
|
85
85
|
});
|
|
86
86
|
}
|
|
87
|
-
//#endregion
|
|
88
87
|
export { renderProjectCreatePage };
|
|
89
88
|
|
|
90
89
|
//# sourceMappingURL=project-create.mjs.map
|