@storyshelf/app 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -4
- package/dist/app-types.d.mts +1 -3
- package/dist/asset-manifest.mjs +13 -5
- package/dist/asset-manifest.mjs.map +1 -1
- package/dist/assets/{htmx.min.js → htmx.mjs} +10 -1
- package/dist/assets/htmx.mjs.map +1 -0
- package/dist/assets/mark.mjs +10 -0
- package/dist/assets/mark.mjs.map +1 -0
- package/dist/boot-tasks.mjs +80 -0
- package/dist/boot-tasks.mjs.map +1 -0
- package/dist/build-state-flags.mjs +18 -0
- package/dist/build-state-flags.mjs.map +1 -0
- package/dist/build-state-setup.mjs +75 -0
- package/dist/build-state-setup.mjs.map +1 -0
- package/dist/capture-alerts.mjs +46 -0
- package/dist/capture-alerts.mjs.map +1 -0
- package/dist/capture-process.mjs +59 -0
- package/dist/capture-process.mjs.map +1 -0
- package/dist/capture-setup.mjs +95 -117
- package/dist/capture-setup.mjs.map +1 -1
- package/dist/connections.mjs +34 -0
- package/dist/connections.mjs.map +1 -0
- package/dist/consumer-setup.mjs +58 -0
- package/dist/consumer-setup.mjs.map +1 -0
- package/dist/credentials.mjs +3 -2
- package/dist/credentials.mjs.map +1 -1
- package/dist/git-status.mjs +46 -0
- package/dist/git-status.mjs.map +1 -0
- package/dist/hidden-data.mjs +24 -0
- package/dist/hidden-data.mjs.map +1 -0
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +202 -124
- package/dist/index.mjs.map +1 -1
- package/dist/insights/budget.mjs +49 -0
- package/dist/insights/budget.mjs.map +1 -0
- package/dist/insights/deps.mjs +21 -0
- package/dist/insights/deps.mjs.map +1 -0
- package/dist/insights/errors.mjs +22 -0
- package/dist/insights/errors.mjs.map +1 -0
- package/dist/insights/health-panel.mjs +40 -0
- package/dist/insights/health-panel.mjs.map +1 -0
- package/dist/insights/job.mjs +103 -0
- package/dist/insights/job.mjs.map +1 -0
- package/dist/insights/load-health.mjs +59 -0
- package/dist/insights/load-health.mjs.map +1 -0
- package/dist/insights/load-triage.mjs +98 -0
- package/dist/insights/load-triage.mjs.map +1 -0
- package/dist/insights/panel.mjs +28 -0
- package/dist/insights/panel.mjs.map +1 -0
- package/dist/insights/request.mjs +113 -0
- package/dist/insights/request.mjs.map +1 -0
- package/dist/insights/stale.mjs +21 -0
- package/dist/insights/stale.mjs.map +1 -0
- package/dist/insights/usage-report.mjs +60 -0
- package/dist/insights/usage-report.mjs.map +1 -0
- package/dist/insights/view.mjs +48 -0
- package/dist/insights/view.mjs.map +1 -0
- package/dist/job-canceller.mjs +14 -0
- package/dist/job-canceller.mjs.map +1 -0
- package/dist/job-queue.mjs +51 -0
- package/dist/job-queue.mjs.map +1 -0
- package/dist/jobs-setup.mjs +132 -0
- package/dist/jobs-setup.mjs.map +1 -0
- package/dist/lifecycle.mjs +109 -19
- package/dist/lifecycle.mjs.map +1 -1
- package/dist/middleware/auth-gate.mjs +4 -2
- package/dist/middleware/auth-gate.mjs.map +1 -1
- package/dist/middleware/rate-limit.mjs +41 -33
- package/dist/middleware/rate-limit.mjs.map +1 -1
- package/dist/middleware/request-log.mjs +3 -2
- package/dist/middleware/request-log.mjs.map +1 -1
- package/dist/middleware/require-review.mjs +51 -0
- package/dist/middleware/require-review.mjs.map +1 -0
- package/dist/middleware/require-testing.mjs +41 -0
- package/dist/middleware/require-testing.mjs.map +1 -0
- package/dist/middleware/setup-gate.mjs.map +1 -1
- package/dist/middleware/store-scope.mjs +12 -2
- package/dist/middleware/store-scope.mjs.map +1 -1
- package/dist/notifications-pause.mjs +33 -0
- package/dist/notifications-pause.mjs.map +1 -0
- package/dist/notify.d.mts +2 -1
- package/dist/notify.mjs +78 -39
- package/dist/notify.mjs.map +1 -1
- package/dist/pages/a11y-violations.mjs +116 -0
- package/dist/pages/a11y-violations.mjs.map +1 -0
- package/dist/pages/admin-ai.mjs +78 -0
- package/dist/pages/admin-ai.mjs.map +1 -0
- package/dist/pages/admin-system-origin.mjs +123 -0
- package/dist/pages/admin-system-origin.mjs.map +1 -0
- package/dist/pages/admin-system-targets.mjs +53 -0
- package/dist/pages/admin-system-targets.mjs.map +1 -0
- package/dist/pages/admin-system.mjs +63 -13
- package/dist/pages/admin-system.mjs.map +1 -1
- package/dist/pages/build-detail.mjs +104 -76
- package/dist/pages/build-detail.mjs.map +1 -1
- package/dist/pages/build-diff-actions.mjs +87 -33
- package/dist/pages/build-diff-actions.mjs.map +1 -1
- package/dist/pages/build-diff-comments.mjs +16 -14
- package/dist/pages/build-diff-comments.mjs.map +1 -1
- package/dist/pages/build-diff-environment.mjs +82 -0
- package/dist/pages/build-diff-environment.mjs.map +1 -0
- package/dist/pages/build-diff-header.mjs +22 -4
- package/dist/pages/build-diff-header.mjs.map +1 -1
- package/dist/pages/build-diff-insight.mjs +113 -0
- package/dist/pages/build-diff-insight.mjs.map +1 -0
- package/dist/pages/build-diff-model.mjs +55 -10
- package/dist/pages/build-diff-model.mjs.map +1 -1
- package/dist/pages/build-diff-nav.mjs +28 -12
- package/dist/pages/build-diff-nav.mjs.map +1 -1
- package/dist/pages/build-diff-shortcuts.mjs +5 -1
- package/dist/pages/build-diff-shortcuts.mjs.map +1 -1
- package/dist/pages/build-diff-usage.mjs +63 -0
- package/dist/pages/build-diff-usage.mjs.map +1 -0
- package/dist/pages/build-diff-viewer.mjs +62 -5
- package/dist/pages/build-diff-viewer.mjs.map +1 -1
- package/dist/pages/build-diff.mjs +34 -13
- package/dist/pages/build-diff.mjs.map +1 -1
- package/dist/pages/build-tests.mjs +74 -0
- package/dist/pages/build-tests.mjs.map +1 -0
- package/dist/pages/component-detail.mjs +249 -0
- package/dist/pages/component-detail.mjs.map +1 -0
- package/dist/pages/component-stories.mjs +168 -0
- package/dist/pages/component-stories.mjs.map +1 -0
- package/dist/pages/compute-jobs.mjs +13 -26
- package/dist/pages/compute-jobs.mjs.map +1 -1
- package/dist/pages/design-links.mjs +182 -0
- package/dist/pages/design-links.mjs.map +1 -0
- package/dist/pages/environment-change.mjs +53 -0
- package/dist/pages/environment-change.mjs.map +1 -0
- package/dist/pages/health-panel.mjs +126 -0
- package/dist/pages/health-panel.mjs.map +1 -0
- package/dist/pages/library-grid.mjs +105 -0
- package/dist/pages/library-grid.mjs.map +1 -0
- package/dist/pages/library-model.mjs +47 -0
- package/dist/pages/library-model.mjs.map +1 -0
- package/dist/pages/library-snapshots.mjs +130 -0
- package/dist/pages/library-snapshots.mjs.map +1 -0
- package/dist/pages/library.mjs +112 -218
- package/dist/pages/library.mjs.map +1 -1
- package/dist/pages/notifications-paused.mjs +22 -0
- package/dist/pages/notifications-paused.mjs.map +1 -0
- package/dist/pages/profile.mjs +53 -48
- package/dist/pages/profile.mjs.map +1 -1
- package/dist/pages/project-builds-table.mjs +2 -2
- package/dist/pages/project-builds-table.mjs.map +1 -1
- package/dist/pages/project-builds.mjs +51 -21
- package/dist/pages/project-builds.mjs.map +1 -1
- package/dist/pages/project-settings.mjs +51 -12
- package/dist/pages/project-settings.mjs.map +1 -1
- package/dist/pages/projects-card.mjs +14 -1
- package/dist/pages/projects-card.mjs.map +1 -1
- package/dist/pages/projects.mjs +6 -2
- package/dist/pages/projects.mjs.map +1 -1
- package/dist/pages/review-changeset.mjs +116 -0
- package/dist/pages/review-changeset.mjs.map +1 -0
- package/dist/pages/review-comments.mjs +103 -0
- package/dist/pages/review-comments.mjs.map +1 -0
- package/dist/pages/review-verdict.mjs +102 -0
- package/dist/pages/review-verdict.mjs.map +1 -0
- package/dist/pages/settings-ai.mjs +69 -0
- package/dist/pages/settings-ai.mjs.map +1 -0
- package/dist/pages/settings-capture.mjs +124 -0
- package/dist/pages/settings-capture.mjs.map +1 -0
- package/dist/pages/settings-design-credential.mjs +99 -0
- package/dist/pages/settings-design-credential.mjs.map +1 -0
- package/dist/pages/settings-design-links.mjs +179 -0
- package/dist/pages/settings-design-links.mjs.map +1 -0
- package/dist/pages/settings-design.mjs +48 -0
- package/dist/pages/settings-design.mjs.map +1 -0
- package/dist/pages/settings-general.mjs +46 -21
- package/dist/pages/settings-general.mjs.map +1 -1
- package/dist/pages/settings-labels.mjs +43 -2
- package/dist/pages/settings-labels.mjs.map +1 -1
- package/dist/pages/settings-links.mjs +112 -0
- package/dist/pages/settings-links.mjs.map +1 -0
- package/dist/pages/settings-notifications-git.mjs +128 -0
- package/dist/pages/settings-notifications-git.mjs.map +1 -0
- package/dist/pages/settings-notifications.mjs +79 -42
- package/dist/pages/settings-notifications.mjs.map +1 -1
- package/dist/pages/settings-tests.mjs +33 -2
- package/dist/pages/settings-tests.mjs.map +1 -1
- package/dist/pages/target-choices.mjs +39 -0
- package/dist/pages/target-choices.mjs.map +1 -0
- package/dist/publish-setup.mjs +89 -0
- package/dist/publish-setup.mjs.map +1 -0
- package/dist/retention-timer.mjs +2 -1
- package/dist/retention-timer.mjs.map +1 -1
- package/dist/review-events.mjs +48 -0
- package/dist/review-events.mjs.map +1 -0
- package/dist/routers/admin-pages.mjs +45 -3
- package/dist/routers/admin-pages.mjs.map +1 -1
- package/dist/routers/admin-public-origin.mjs +85 -0
- package/dist/routers/admin-public-origin.mjs.map +1 -0
- package/dist/routers/assets.mjs +9 -2
- package/dist/routers/assets.mjs.map +1 -1
- package/dist/routers/auth.mjs +2 -2
- package/dist/routers/auth.mjs.map +1 -1
- package/dist/routers/baseline-guard.mjs +2 -2
- package/dist/routers/baseline-guard.mjs.map +1 -1
- package/dist/routers/baselines.handlers.mjs +64 -0
- package/dist/routers/baselines.handlers.mjs.map +1 -0
- package/dist/routers/baselines.mjs +125 -0
- package/dist/routers/baselines.mjs.map +1 -0
- package/dist/routers/build-cancel.mjs +68 -0
- package/dist/routers/build-cancel.mjs.map +1 -0
- package/dist/routers/build-changeset.mjs +70 -0
- package/dist/routers/build-changeset.mjs.map +1 -0
- package/dist/routers/build-reviews.handlers.mjs +86 -0
- package/dist/routers/build-reviews.handlers.mjs.map +1 -0
- package/dist/routers/build-reviews.mjs +74 -0
- package/dist/routers/build-reviews.mjs.map +1 -0
- package/dist/routers/builds.handlers.mjs +46 -55
- package/dist/routers/builds.handlers.mjs.map +1 -1
- package/dist/routers/builds.mjs +71 -17
- package/dist/routers/builds.mjs.map +1 -1
- package/dist/routers/capture-config.mjs +35 -0
- package/dist/routers/capture-config.mjs.map +1 -0
- package/dist/routers/comments.mjs +41 -15
- package/dist/routers/comments.mjs.map +1 -1
- package/dist/routers/component-catalog.handlers.mjs +71 -0
- package/dist/routers/component-catalog.handlers.mjs.map +1 -0
- package/dist/routers/component.handlers.mjs +121 -0
- package/dist/routers/component.handlers.mjs.map +1 -0
- package/dist/routers/connections.mjs +118 -0
- package/dist/routers/connections.mjs.map +1 -0
- package/dist/routers/design-connect.handlers.mjs +44 -0
- package/dist/routers/design-connect.handlers.mjs.map +1 -0
- package/dist/routers/design.handlers.mjs +114 -0
- package/dist/routers/design.handlers.mjs.map +1 -0
- package/dist/routers/health-report.mjs +2 -3
- package/dist/routers/health-report.mjs.map +1 -1
- package/dist/routers/health.mjs +4 -3
- package/dist/routers/health.mjs.map +1 -1
- package/dist/routers/helpers.mjs +19 -3
- package/dist/routers/helpers.mjs.map +1 -1
- package/dist/routers/insights-ui.mjs +64 -0
- package/dist/routers/insights-ui.mjs.map +1 -0
- package/dist/routers/insights.handlers.mjs +45 -0
- package/dist/routers/insights.handlers.mjs.map +1 -0
- package/dist/routers/insights.mjs +189 -0
- package/dist/routers/insights.mjs.map +1 -0
- package/dist/routers/links.handlers.mjs +80 -0
- package/dist/routers/links.handlers.mjs.map +1 -0
- package/dist/routers/links.mjs +139 -0
- package/dist/routers/links.mjs.map +1 -0
- package/dist/routers/media.mjs +18 -4
- package/dist/routers/media.mjs.map +1 -1
- package/dist/routers/members.mjs +2 -1
- package/dist/routers/members.mjs.map +1 -1
- package/dist/routers/notification-channels.mjs +17 -23
- package/dist/routers/notification-channels.mjs.map +1 -1
- package/dist/routers/oauth-state.mjs +45 -0
- package/dist/routers/oauth-state.mjs.map +1 -0
- package/dist/routers/preview-base.mjs +38 -0
- package/dist/routers/preview-base.mjs.map +1 -0
- package/dist/routers/preview-token.mjs +51 -0
- package/dist/routers/preview-token.mjs.map +1 -0
- package/dist/routers/profile.mjs +4 -2
- package/dist/routers/profile.mjs.map +1 -1
- package/dist/routers/projects.mjs +17 -1
- package/dist/routers/projects.mjs.map +1 -1
- package/dist/routers/publish-upload.mjs +78 -0
- package/dist/routers/publish-upload.mjs.map +1 -0
- package/dist/routers/published-access.mjs +52 -0
- package/dist/routers/published-access.mjs.map +1 -0
- package/dist/routers/published-files.mjs +52 -0
- package/dist/routers/published-files.mjs.map +1 -0
- package/dist/routers/review-forms.mjs +72 -0
- package/dist/routers/review-forms.mjs.map +1 -0
- package/dist/routers/review-section.handlers.mjs +85 -0
- package/dist/routers/review-section.handlers.mjs.map +1 -0
- package/dist/routers/review-switch-off.mjs +29 -0
- package/dist/routers/review-switch-off.mjs.map +1 -0
- package/dist/routers/schemas.mjs +140 -22
- package/dist/routers/schemas.mjs.map +1 -1
- package/dist/routers/scoped-models.mjs +22 -0
- package/dist/routers/scoped-models.mjs.map +1 -0
- package/dist/routers/settings-ai.mjs +35 -0
- package/dist/routers/settings-ai.mjs.map +1 -0
- package/dist/routers/settings-capture.mjs +77 -0
- package/dist/routers/settings-capture.mjs.map +1 -0
- package/dist/routers/settings-design.mjs +103 -0
- package/dist/routers/settings-design.mjs.map +1 -0
- package/dist/routers/settings-general.mjs +55 -10
- package/dist/routers/settings-general.mjs.map +1 -1
- package/dist/routers/settings-git-channel.mjs +77 -0
- package/dist/routers/settings-git-channel.mjs.map +1 -0
- package/dist/routers/settings-labels.mjs +27 -1
- package/dist/routers/settings-labels.mjs.map +1 -1
- package/dist/routers/settings-links.mjs +63 -0
- package/dist/routers/settings-links.mjs.map +1 -0
- package/dist/routers/settings-members.mjs +4 -7
- package/dist/routers/settings-members.mjs.map +1 -1
- package/dist/routers/settings-notifications.mjs +15 -10
- package/dist/routers/settings-notifications.mjs.map +1 -1
- package/dist/routers/settings-tokens.mjs +2 -5
- package/dist/routers/settings-tokens.mjs.map +1 -1
- package/dist/routers/settings-webhooks.mjs +7 -24
- package/dist/routers/settings-webhooks.mjs.map +1 -1
- package/dist/routers/settings.handlers.mjs +107 -26
- package/dist/routers/settings.handlers.mjs.map +1 -1
- package/dist/routers/settings.mjs +16 -4
- package/dist/routers/settings.mjs.map +1 -1
- package/dist/routers/signed-storybook.mjs +53 -0
- package/dist/routers/signed-storybook.mjs.map +1 -0
- package/dist/routers/snapshot-groups.mjs +63 -0
- package/dist/routers/snapshot-groups.mjs.map +1 -0
- package/dist/routers/snapshots.mjs +103 -5
- package/dist/routers/snapshots.mjs.map +1 -1
- package/dist/routers/storybook.mjs +37 -88
- package/dist/routers/storybook.mjs.map +1 -1
- package/dist/routers/tokens.mjs +2 -5
- package/dist/routers/tokens.mjs.map +1 -1
- package/dist/routers/ui.mjs +34 -5
- package/dist/routers/ui.mjs.map +1 -1
- package/dist/routers/webhooks.mjs +5 -11
- package/dist/routers/webhooks.mjs.map +1 -1
- package/dist/routers/well-known.mjs +11 -4
- package/dist/routers/well-known.mjs.map +1 -1
- package/dist/routes/api-routes.mjs +93 -0
- package/dist/routes/api-routes.mjs.map +1 -0
- package/dist/routes/classification.mjs +38 -0
- package/dist/routes/classification.mjs.map +1 -0
- package/dist/routes/openapi-classes.mjs +22 -0
- package/dist/routes/openapi-classes.mjs.map +1 -0
- package/dist/routes/page-routes.mjs +104 -0
- package/dist/routes/page-routes.mjs.map +1 -0
- package/dist/runtime.mjs +18 -13
- package/dist/runtime.mjs.map +1 -1
- package/dist/serverless.mjs +5 -0
- package/dist/store.mjs +19 -1
- package/dist/store.mjs.map +1 -1
- package/dist/targets.mjs +46 -0
- package/dist/targets.mjs.map +1 -0
- package/dist/ui/buttons.mjs +16 -3
- package/dist/ui/buttons.mjs.map +1 -1
- package/dist/ui/design-reference.mjs +97 -0
- package/dist/ui/design-reference.mjs.map +1 -0
- package/dist/ui/document.mjs +1 -2
- package/dist/ui/document.mjs.map +1 -1
- package/dist/ui/forms.mjs +6 -5
- package/dist/ui/forms.mjs.map +1 -1
- package/dist/ui/icons/paths.mjs +1 -0
- package/dist/ui/icons/paths.mjs.map +1 -1
- package/dist/ui/preview-embed.mjs +82 -0
- package/dist/ui/preview-embed.mjs.map +1 -0
- package/dist/ui/shell/client-script.mjs +85 -0
- package/dist/ui/shell/client-script.mjs.map +1 -1
- package/dist/ui/shell/nav.mjs +12 -8
- package/dist/ui/shell/nav.mjs.map +1 -1
- package/dist/ui/shell/review-script.mjs +8 -0
- package/dist/ui/shell/review-script.mjs.map +1 -1
- package/dist/ui/shell/sidebar.mjs +2 -2
- package/dist/ui/shell/sidebar.mjs.map +1 -1
- package/package.json +11 -6
- 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
|
@@ -4,24 +4,51 @@ import.meta.url;
|
|
|
4
4
|
import { Button } from "../ui/buttons.mjs";
|
|
5
5
|
import { Badge, statusTone } from "../ui/feedback.mjs";
|
|
6
6
|
import { Kbd } from "../ui/kbd.mjs";
|
|
7
|
+
import { groupOf, neighbours, snapshotPageUrl } from "./build-diff-model.mjs";
|
|
7
8
|
import { actionBar, actionGroup, actionHints, actionPosition } from "../ui/styles/review-layout.mjs";
|
|
8
|
-
import { neighbours, snapshotPageUrl } from "./build-diff-model.mjs";
|
|
9
9
|
import { jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
10
10
|
//#region src/pages/build-diff-actions.tsx
|
|
11
|
-
function
|
|
12
|
-
return `/api/v1/projects/${project.slug}/builds/${build.id}
|
|
11
|
+
function apiBase(project, build) {
|
|
12
|
+
return `/api/v1/projects/${project.slug}/builds/${build.id}`;
|
|
13
13
|
}
|
|
14
|
-
|
|
14
|
+
const isOpenStatus = (snap) => snap.status === "new" || snap.status === "changed";
|
|
15
|
+
function sharedTarget(base, group, open, name) {
|
|
16
|
+
return {
|
|
17
|
+
base: `${base}/stories/${encodeURIComponent(group.storyId)}/modes/${encodeURIComponent(group.modeKey)}`,
|
|
18
|
+
shared: true,
|
|
19
|
+
label: `${name} (${open.length} browsers)`
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function decisionTarget(props) {
|
|
23
|
+
const { project, build, snapshots, selected, drifted } = props;
|
|
24
|
+
const group = groupOf(snapshots, selected.id);
|
|
25
|
+
const open = (group?.members ?? [selected]).filter((snap) => isOpenStatus(snap));
|
|
26
|
+
const [first] = open;
|
|
27
|
+
if (!first) return null;
|
|
28
|
+
const base = apiBase(project, build);
|
|
29
|
+
const drift = { drifted: open.some((snap) => drifted[snap.id] !== void 0) };
|
|
30
|
+
if (open.length > 1 && group) return {
|
|
31
|
+
...sharedTarget(base, group, open, selected.storyName),
|
|
32
|
+
...drift
|
|
33
|
+
};
|
|
34
|
+
return {
|
|
35
|
+
base: `${base}/snapshots/${first.id}`,
|
|
36
|
+
shared: false,
|
|
37
|
+
label: first.storyName,
|
|
38
|
+
...drift
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** Approve (or "Approve anyway" when a baseline moved) as its own form. */
|
|
15
42
|
function ApproveForm(props) {
|
|
16
|
-
const {
|
|
17
|
-
const url = `${
|
|
43
|
+
const { target } = props;
|
|
44
|
+
const url = `${target.base}/approve${target.drifted ? "?force=true" : ""}`;
|
|
18
45
|
return /* @__PURE__ */ jsx("form", {
|
|
19
46
|
method: "post",
|
|
20
47
|
action: url,
|
|
21
48
|
"hx-post": url,
|
|
22
49
|
"hx-target": "body",
|
|
23
|
-
"data-toast": `Approved ${
|
|
24
|
-
children: drifted ? /* @__PURE__ */ jsx(Button, {
|
|
50
|
+
"data-toast": `Approved ${target.label}`,
|
|
51
|
+
children: target.drifted ? /* @__PURE__ */ jsx(Button, {
|
|
25
52
|
variant: "secondary",
|
|
26
53
|
size: "sm",
|
|
27
54
|
type: "submit",
|
|
@@ -34,34 +61,61 @@ function ApproveForm(props) {
|
|
|
34
61
|
type: "submit",
|
|
35
62
|
icon: "check",
|
|
36
63
|
"data-approve": true,
|
|
37
|
-
title: "Approve (a)",
|
|
38
|
-
children: "Approve"
|
|
64
|
+
title: target.shared ? "Approve every browser (a)" : "Approve (a)",
|
|
65
|
+
children: target.shared ? "Approve all browsers" : "Approve"
|
|
39
66
|
})
|
|
40
67
|
});
|
|
41
68
|
}
|
|
42
|
-
/**
|
|
43
|
-
function
|
|
69
|
+
/** Per-snapshot escape hatch: approve only the browser being viewed. */
|
|
70
|
+
function ApproveOnlyThisForm(props) {
|
|
44
71
|
const { project, build, selected } = props;
|
|
45
|
-
const
|
|
72
|
+
const url = `${apiBase(project, build)}/snapshots/${selected.id}/approve`;
|
|
73
|
+
return /* @__PURE__ */ jsx("form", {
|
|
74
|
+
method: "post",
|
|
75
|
+
action: url,
|
|
76
|
+
"hx-post": url,
|
|
77
|
+
"hx-target": "body",
|
|
78
|
+
"data-toast": `Approved ${selected.storyName} on ${selected.browser ?? "chromium"}`,
|
|
79
|
+
children: /* @__PURE__ */ jsxs(Button, {
|
|
80
|
+
variant: "ghost",
|
|
81
|
+
size: "sm",
|
|
82
|
+
type: "submit",
|
|
83
|
+
title: "Approve only this browser",
|
|
84
|
+
children: ["Only ", selected.browser ?? "chromium"]
|
|
85
|
+
})
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/** Approve/reject for a reviewable story/mode; both advance to the next open one. */
|
|
89
|
+
function DecisionButtons(props) {
|
|
90
|
+
const { project, build, selected, target } = props;
|
|
91
|
+
const selectedOpen = selected.status === "new" || selected.status === "changed";
|
|
46
92
|
return /* @__PURE__ */ jsxs("div", {
|
|
47
93
|
class: actionGroup,
|
|
48
|
-
children: [
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
"data-
|
|
61
|
-
|
|
62
|
-
children:
|
|
94
|
+
children: [
|
|
95
|
+
/* @__PURE__ */ jsx(ApproveForm, { target }),
|
|
96
|
+
target.shared && selectedOpen ? /* @__PURE__ */ jsx(ApproveOnlyThisForm, {
|
|
97
|
+
project,
|
|
98
|
+
build,
|
|
99
|
+
selected
|
|
100
|
+
}) : null,
|
|
101
|
+
/* @__PURE__ */ jsx("form", {
|
|
102
|
+
method: "post",
|
|
103
|
+
action: `${target.base}/reject`,
|
|
104
|
+
"hx-post": `${target.base}/reject`,
|
|
105
|
+
"hx-target": "body",
|
|
106
|
+
"data-toast": `Rejected ${target.label}`,
|
|
107
|
+
"data-toast-tone": "warning",
|
|
108
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
109
|
+
variant: "danger",
|
|
110
|
+
size: "sm",
|
|
111
|
+
type: "submit",
|
|
112
|
+
icon: "x",
|
|
113
|
+
"data-reject": true,
|
|
114
|
+
title: "Reject (r)",
|
|
115
|
+
children: "Reject"
|
|
116
|
+
})
|
|
63
117
|
})
|
|
64
|
-
|
|
118
|
+
]
|
|
65
119
|
});
|
|
66
120
|
}
|
|
67
121
|
function StepButton(props) {
|
|
@@ -91,9 +145,9 @@ function StepButton(props) {
|
|
|
91
145
|
}
|
|
92
146
|
/** Previous/next stepping, position, shortcut hints, and the review decision. */
|
|
93
147
|
function DiffActionBar(props) {
|
|
94
|
-
const { project, build, snapshots, selected, canReview
|
|
148
|
+
const { project, build, snapshots, selected, canReview } = props;
|
|
95
149
|
const around = neighbours(snapshots, selected.id);
|
|
96
|
-
const
|
|
150
|
+
const target = decisionTarget(props);
|
|
97
151
|
return /* @__PURE__ */ jsxs("div", {
|
|
98
152
|
class: actionBar,
|
|
99
153
|
role: "toolbar",
|
|
@@ -143,11 +197,11 @@ function DiffActionBar(props) {
|
|
|
143
197
|
})
|
|
144
198
|
]
|
|
145
199
|
}),
|
|
146
|
-
canReview &&
|
|
200
|
+
canReview && target ? /* @__PURE__ */ jsx(DecisionButtons, {
|
|
147
201
|
project,
|
|
148
202
|
build,
|
|
149
203
|
selected,
|
|
150
|
-
|
|
204
|
+
target
|
|
151
205
|
}) : /* @__PURE__ */ jsx(Badge, {
|
|
152
206
|
tone: statusTone(selected.status),
|
|
153
207
|
icon: true,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-diff-actions.mjs","names":[],"sources":["../../src/pages/build-diff-actions.tsx"],"sourcesContent":["import type { Build, Project, Snapshot } from \"@storyshelf/core/schema\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport { Badge, Button, Kbd, statusTone } from \"../ui/components.tsx\";\nimport { actionBar, actionGroup, actionHints, actionPosition } from \"../ui/styles/review-layout.ts\";\nimport { neighbours, snapshotPageUrl } from \"./build-diff-model.ts\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\n/** Sticky bottom bar props. */\nexport interface DiffActionBarProps {\n project: Project;\n build: Build;\n snapshots: Snapshot[];\n selected: Snapshot;\n canReview: boolean;\n drifted: boolean;\n}\n\nfunction snapshotBase(project: Project, build: Build, snapshot: Snapshot): string {\n return `/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${snapshot.id}`;\n}\n\ntype DecisionProps = { project: Project; build: Build; selected: Snapshot; drifted: boolean };\n\n/** Approve (or \"Approve anyway\" when the baseline moved) as its own form. */\nfunction ApproveForm(props: DecisionProps): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, build, selected, drifted } = props;\n const url = `${snapshotBase(project, build, selected)}/approve${drifted ? \"?force=true\" : \"\"}`;\n return (\n <form\n method=\"post\"\n action={url}\n hx-post={url}\n hx-target=\"body\"\n data-toast={`Approved ${selected.storyName}`}\n >\n {drifted ? (\n <Button\n variant=\"secondary\"\n size=\"sm\"\n type=\"submit\"\n icon=\"check\"\n title=\"The baseline changed after this diff; replace it with this screenshot\"\n >\n Approve anyway\n </Button>\n ) : (\n <Button\n variant=\"primary\"\n size=\"sm\"\n type=\"submit\"\n icon=\"check\"\n data-approve\n title=\"Approve (a)\"\n >\n Approve\n </Button>\n )}\n </form>\n );\n}\n\n/** Approve/reject for a reviewable snapshot; both advance to the next open one. */\nfunction DecisionButtons(props: DecisionProps): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, build, selected } = props;\n const base = snapshotBase(project, build, selected);\n return (\n <div class={actionGroup}>\n <ApproveForm {...props} />\n <form\n method=\"post\"\n action={`${base}/reject`}\n hx-post={`${base}/reject`}\n hx-target=\"body\"\n data-toast={`Rejected ${selected.storyName}`}\n data-toast-tone=\"warning\"\n >\n <Button variant=\"danger\" size=\"sm\" type=\"submit\" icon=\"x\" data-reject title=\"Reject (r)\">\n Reject\n </Button>\n </form>\n </div>\n );\n}\n\nfunction StepButton(props: {\n target: Snapshot | null;\n project: Project;\n build: Build;\n direction: \"prev\" | \"next\";\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { target, project, build, direction } = props;\n const label = direction === \"prev\" ? \"Previous snapshot (k)\" : \"Next snapshot (j)\";\n const icon = direction === \"prev\" ? \"chevron-left\" : \"chevron-right\";\n if (!target) {\n return <Button variant=\"ghost\" size=\"sm\" icon={icon} disabled aria-label={label} />;\n }\n const url = snapshotPageUrl(project, build, target.id);\n const hook = direction === \"prev\" ? { \"data-snap-prev\": true } : { \"data-snap-next\": true };\n return (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n icon={icon}\n href={url}\n aria-label={label}\n title={label}\n hx-get={url}\n hx-target=\"body\"\n hx-push-url=\"true\"\n {...hook}\n />\n );\n}\n\n/** Previous/next stepping, position, shortcut hints, and the review decision. */\nexport function DiffActionBar(\n props: DiffActionBarProps,\n): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, build, snapshots, selected, canReview, drifted } = props;\n const around = neighbours(snapshots, selected.id);\n const reviewable = selected.status === \"new\" || selected.status === \"changed\";\n return (\n <div class={actionBar} role=\"toolbar\" aria-label=\"Review actions\">\n <div class={actionGroup}>\n <StepButton target={around.prev} project={project} build={build} direction=\"prev\" />\n <span class={actionPosition} aria-live=\"polite\">\n {around.position} / {around.total}\n </span>\n <StepButton target={around.next} project={project} build={build} direction=\"next\" />\n </div>\n <div class={actionHints}>\n <Kbd keys={[\"j\", \"k\"]} /> navigate <Kbd keys=\"a\" /> approve <Kbd keys=\"r\" /> reject\n <Button variant=\"ghost\" size=\"sm\" data-shortcuts-open aria-label=\"Keyboard shortcuts (?)\">\n <Kbd keys=\"?\" />\n </Button>\n </div>\n {canReview && reviewable ? (\n <DecisionButtons project={project} build={build} selected={selected} drifted={drifted} />\n ) : (\n <Badge tone={statusTone(selected.status)} icon>\n {selected.status}\n </Badge>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"build-diff-actions.mjs","names":[],"sources":["../../src/pages/build-diff-actions.tsx"],"sourcesContent":["import type { Build, Project, Snapshot } from \"@storyshelf/core/schema\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport { Badge, Button, Kbd, statusTone } from \"../ui/components.tsx\";\nimport { actionBar, actionGroup, actionHints, actionPosition } from \"../ui/styles/review-layout.ts\";\nimport { groupOf, neighbours, snapshotPageUrl, type SnapshotGroup } from \"./build-diff-model.ts\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\n/** Sticky bottom bar props. */\nexport interface DiffActionBarProps {\n project: Project;\n build: Build;\n snapshots: Snapshot[];\n selected: Snapshot;\n canReview: boolean;\n /** Open snapshots whose baseline changed after their diff was computed. */\n drifted: Record<string, \"stale\" | \"removed\">;\n}\n\n/** What one decision covers: a whole story/mode across browsers, or a single snapshot. */\ninterface DecisionTarget {\n /** Endpoint prefix; `/approve` and `/reject` are appended. */\n base: string;\n /** True when the decision applies to every open browser of the story/mode. */\n shared: boolean;\n drifted: boolean;\n label: string;\n}\n\nfunction apiBase(project: Project, build: Build): string {\n return `/api/v1/projects/${project.slug}/builds/${build.id}`;\n}\n\nconst isOpenStatus = (snap: Snapshot): boolean =>\n snap.status === \"new\" || snap.status === \"changed\";\n\nfunction sharedTarget(\n base: string,\n group: SnapshotGroup,\n open: Snapshot[],\n name: string,\n): Pick<DecisionTarget, \"base\" | \"shared\" | \"label\"> {\n const story = encodeURIComponent(group.storyId);\n const mode = encodeURIComponent(group.modeKey);\n return {\n base: `${base}/stories/${story}/modes/${mode}`,\n shared: true,\n label: `${name} (${open.length} browsers)`,\n };\n}\n\nfunction decisionTarget(props: DiffActionBarProps): DecisionTarget | null {\n const { project, build, snapshots, selected, drifted } = props;\n const group = groupOf(snapshots, selected.id);\n const open = (group?.members ?? [selected]).filter((snap) => isOpenStatus(snap));\n const [first] = open;\n if (!first) return null;\n const base = apiBase(project, build);\n const drift = { drifted: open.some((snap) => drifted[snap.id] !== undefined) };\n if (open.length > 1 && group) {\n return { ...sharedTarget(base, group, open, selected.storyName), ...drift };\n }\n return { base: `${base}/snapshots/${first.id}`, shared: false, label: first.storyName, ...drift };\n}\n\n/** Approve (or \"Approve anyway\" when a baseline moved) as its own form. */\nfunction ApproveForm(props: {\n target: DecisionTarget;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { target } = props;\n const url = `${target.base}/approve${target.drifted ? \"?force=true\" : \"\"}`;\n return (\n <form\n method=\"post\"\n action={url}\n hx-post={url}\n hx-target=\"body\"\n data-toast={`Approved ${target.label}`}\n >\n {target.drifted ? (\n <Button\n variant=\"secondary\"\n size=\"sm\"\n type=\"submit\"\n icon=\"check\"\n title=\"The baseline changed after this diff; replace it with this screenshot\"\n >\n Approve anyway\n </Button>\n ) : (\n <Button\n variant=\"primary\"\n size=\"sm\"\n type=\"submit\"\n icon=\"check\"\n data-approve\n title={target.shared ? \"Approve every browser (a)\" : \"Approve (a)\"}\n >\n {target.shared ? \"Approve all browsers\" : \"Approve\"}\n </Button>\n )}\n </form>\n );\n}\n\n/** Per-snapshot escape hatch: approve only the browser being viewed. */\nfunction ApproveOnlyThisForm(props: {\n project: Project;\n build: Build;\n selected: Snapshot;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, build, selected } = props;\n const url = `${apiBase(project, build)}/snapshots/${selected.id}/approve`;\n return (\n <form\n method=\"post\"\n action={url}\n hx-post={url}\n hx-target=\"body\"\n data-toast={`Approved ${selected.storyName} on ${selected.browser ?? \"chromium\"}`}\n >\n <Button variant=\"ghost\" size=\"sm\" type=\"submit\" title=\"Approve only this browser\">\n Only {selected.browser ?? \"chromium\"}\n </Button>\n </form>\n );\n}\n\n/** Approve/reject for a reviewable story/mode; both advance to the next open one. */\nfunction DecisionButtons(props: {\n project: Project;\n build: Build;\n selected: Snapshot;\n target: DecisionTarget;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, build, selected, target } = props;\n const selectedOpen = selected.status === \"new\" || selected.status === \"changed\";\n return (\n <div class={actionGroup}>\n <ApproveForm target={target} />\n {target.shared && selectedOpen ? (\n <ApproveOnlyThisForm project={project} build={build} selected={selected} />\n ) : null}\n <form\n method=\"post\"\n action={`${target.base}/reject`}\n hx-post={`${target.base}/reject`}\n hx-target=\"body\"\n data-toast={`Rejected ${target.label}`}\n data-toast-tone=\"warning\"\n >\n <Button variant=\"danger\" size=\"sm\" type=\"submit\" icon=\"x\" data-reject title=\"Reject (r)\">\n Reject\n </Button>\n </form>\n </div>\n );\n}\n\nfunction StepButton(props: {\n target: Snapshot | null;\n project: Project;\n build: Build;\n direction: \"prev\" | \"next\";\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { target, project, build, direction } = props;\n const label = direction === \"prev\" ? \"Previous snapshot (k)\" : \"Next snapshot (j)\";\n const icon = direction === \"prev\" ? \"chevron-left\" : \"chevron-right\";\n if (!target) {\n return <Button variant=\"ghost\" size=\"sm\" icon={icon} disabled aria-label={label} />;\n }\n const url = snapshotPageUrl(project, build, target.id);\n const hook = direction === \"prev\" ? { \"data-snap-prev\": true } : { \"data-snap-next\": true };\n return (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n icon={icon}\n href={url}\n aria-label={label}\n title={label}\n hx-get={url}\n hx-target=\"body\"\n hx-push-url=\"true\"\n {...hook}\n />\n );\n}\n\n/** Previous/next stepping, position, shortcut hints, and the review decision. */\nexport function DiffActionBar(\n props: DiffActionBarProps,\n): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, build, snapshots, selected, canReview } = props;\n const around = neighbours(snapshots, selected.id);\n const target = decisionTarget(props);\n return (\n <div class={actionBar} role=\"toolbar\" aria-label=\"Review actions\">\n <div class={actionGroup}>\n <StepButton target={around.prev} project={project} build={build} direction=\"prev\" />\n <span class={actionPosition} aria-live=\"polite\">\n {around.position} / {around.total}\n </span>\n <StepButton target={around.next} project={project} build={build} direction=\"next\" />\n </div>\n <div class={actionHints}>\n <Kbd keys={[\"j\", \"k\"]} /> navigate <Kbd keys=\"a\" /> approve <Kbd keys=\"r\" /> reject\n <Button variant=\"ghost\" size=\"sm\" data-shortcuts-open aria-label=\"Keyboard shortcuts (?)\">\n <Kbd keys=\"?\" />\n </Button>\n </div>\n {canReview && target ? (\n <DecisionButtons project={project} build={build} selected={selected} target={target} />\n ) : (\n <Badge tone={statusTone(selected.status)} icon>\n {selected.status}\n </Badge>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -6,12 +6,13 @@ import { Badge, Meta } from "../ui/feedback.mjs";
|
|
|
6
6
|
import { TextareaField } from "../ui/forms.mjs";
|
|
7
7
|
import { Card, SectionTitle } from "../ui/layout.mjs";
|
|
8
8
|
import { VStack } from "../ui/stacks.mjs";
|
|
9
|
+
import { csrfField } from "../ui/csrf-field.mjs";
|
|
9
10
|
import { reviewComment, reviewCommentActions, reviewCommentBody, reviewCommentHead } from "../ui/styles/review.mjs";
|
|
10
11
|
import { jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
11
12
|
//#region src/pages/build-diff-comments.tsx
|
|
12
13
|
/** One comment with an optional resolve action. */
|
|
13
14
|
function CommentCard(props) {
|
|
14
|
-
const { project, build, comment, canReview } = props;
|
|
15
|
+
const { project, build, comment, canReview, anchor } = props;
|
|
15
16
|
return /* @__PURE__ */ jsxs("div", {
|
|
16
17
|
class: reviewComment,
|
|
17
18
|
children: [
|
|
@@ -20,6 +21,7 @@ function CommentCard(props) {
|
|
|
20
21
|
children: [
|
|
21
22
|
/* @__PURE__ */ jsx("strong", { children: comment.userId ?? "anonymous" }),
|
|
22
23
|
/* @__PURE__ */ jsxs("span", { children: ["· ", new Date(comment.createdAt).toLocaleString()] }),
|
|
24
|
+
anchor ?? null,
|
|
23
25
|
comment.resolved ? /* @__PURE__ */ jsx(Badge, {
|
|
24
26
|
tone: "success",
|
|
25
27
|
children: "resolved"
|
|
@@ -30,18 +32,18 @@ function CommentCard(props) {
|
|
|
30
32
|
class: reviewCommentBody,
|
|
31
33
|
children: comment.body
|
|
32
34
|
}),
|
|
33
|
-
!comment.resolved && canReview ? /* @__PURE__ */
|
|
35
|
+
!comment.resolved && canReview ? /* @__PURE__ */ jsxs("form", {
|
|
34
36
|
method: "post",
|
|
35
|
-
action: `/
|
|
36
|
-
"hx-post": `/
|
|
37
|
+
action: `/projects/${project.slug}/builds/${build.id}/comments/${comment.id}/resolve`,
|
|
38
|
+
"hx-post": `/projects/${project.slug}/builds/${build.id}/comments/${comment.id}/resolve`,
|
|
37
39
|
"hx-target": "body",
|
|
38
40
|
class: reviewCommentActions,
|
|
39
|
-
children: /* @__PURE__ */ jsx(Button, {
|
|
41
|
+
children: [csrfField(), /* @__PURE__ */ jsx(Button, {
|
|
40
42
|
variant: "ghost",
|
|
41
43
|
size: "sm",
|
|
42
44
|
type: "submit",
|
|
43
45
|
children: "Mark resolved"
|
|
44
|
-
})
|
|
46
|
+
})]
|
|
45
47
|
}) : null
|
|
46
48
|
]
|
|
47
49
|
}, comment.id);
|
|
@@ -49,13 +51,13 @@ function CommentCard(props) {
|
|
|
49
51
|
/** Reply form scoped to the selected snapshot. */
|
|
50
52
|
function CommentForm(props) {
|
|
51
53
|
const { project, build, selectedId } = props;
|
|
52
|
-
return /* @__PURE__ */
|
|
54
|
+
return /* @__PURE__ */ jsxs("form", {
|
|
53
55
|
method: "post",
|
|
54
|
-
action: `/
|
|
55
|
-
"hx-post": `/
|
|
56
|
+
action: `/projects/${project.slug}/builds/${build.id}/comments`,
|
|
57
|
+
"hx-post": `/projects/${project.slug}/builds/${build.id}/comments`,
|
|
56
58
|
"hx-target": "body",
|
|
57
59
|
class: "mt-1",
|
|
58
|
-
children: /* @__PURE__ */ jsxs(VStack, { children: [
|
|
60
|
+
children: [csrfField(), /* @__PURE__ */ jsxs(VStack, { children: [
|
|
59
61
|
/* @__PURE__ */ jsx("input", {
|
|
60
62
|
type: "hidden",
|
|
61
63
|
name: "snapshotId",
|
|
@@ -73,13 +75,13 @@ function CommentForm(props) {
|
|
|
73
75
|
type: "submit",
|
|
74
76
|
children: "Comment"
|
|
75
77
|
}) })
|
|
76
|
-
] })
|
|
78
|
+
] })]
|
|
77
79
|
});
|
|
78
80
|
}
|
|
79
81
|
/** Threaded comments for the selected snapshot plus the reply form. */
|
|
80
82
|
function DiffComments(props) {
|
|
81
|
-
const { project, build, selectedId, comments, canReview } = props;
|
|
82
|
-
const visible = comments.filter((c) =>
|
|
83
|
+
const { project, build, selectedId, selectedStoryId, comments, canReview } = props;
|
|
84
|
+
const visible = comments.filter((c) => c.snapshotId ? c.snapshotId === selectedId : !c.storyId || c.storyId === selectedStoryId);
|
|
83
85
|
return /* @__PURE__ */ jsxs(Card, { children: [
|
|
84
86
|
/* @__PURE__ */ jsx(SectionTitle, {
|
|
85
87
|
level: 3,
|
|
@@ -99,6 +101,6 @@ function DiffComments(props) {
|
|
|
99
101
|
] });
|
|
100
102
|
}
|
|
101
103
|
//#endregion
|
|
102
|
-
export { DiffComments };
|
|
104
|
+
export { CommentCard, DiffComments };
|
|
103
105
|
|
|
104
106
|
//# sourceMappingURL=build-diff-comments.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-diff-comments.mjs","names":[],"sources":["../../src/pages/build-diff-comments.tsx"],"sourcesContent":["import type { Build } from \"@storyshelf/core/schema\";\nimport type { Comment } from \"@storyshelf/core/schema\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport {\n Badge,\n Button,\n Card,\n Meta,\n SectionTitle,\n TextareaField,\n VStack,\n} from \"../ui/components.tsx\";\nimport {\n reviewComment,\n reviewCommentActions,\n reviewCommentBody,\n reviewCommentHead,\n} from \"../ui/styles/review.ts\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\n/** Snapshot comment thread props. */\nexport interface DiffCommentsProps {\n project: Project;\n build: Build;\n selectedId: string;\n comments: Comment[];\n canReview: boolean;\n}\n\
|
|
1
|
+
{"version":3,"file":"build-diff-comments.mjs","names":[],"sources":["../../src/pages/build-diff-comments.tsx"],"sourcesContent":["import type { Build } from \"@storyshelf/core/schema\";\nimport type { Comment } from \"@storyshelf/core/schema\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport {\n Badge,\n Button,\n Card,\n Meta,\n SectionTitle,\n TextareaField,\n VStack,\n} from \"../ui/components.tsx\";\nimport { csrfField } from \"../ui/csrf-field.tsx\";\nimport {\n reviewComment,\n reviewCommentActions,\n reviewCommentBody,\n reviewCommentHead,\n} from \"../ui/styles/review.ts\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\n/** Snapshot comment thread props. */\nexport interface DiffCommentsProps {\n project: Project;\n build: Build;\n selectedId: string;\n /** Story of the selected snapshot: its story comments show here too (ADR 0013, decision 2). */\n selectedStoryId?: string;\n comments: Comment[];\n canReview: boolean;\n}\n\n/** One comment's props: where it sits, and what its head shows beside the author. */\nexport interface CommentCardProps {\n project: Project;\n build: Build;\n comment: Comment;\n canReview: boolean;\n /** What the comment is anchored to (a story badge), shown in its head. */\n anchor?: unknown;\n}\n\n/** One comment with an optional resolve action. */\nexport function CommentCard(\n props: CommentCardProps,\n): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, build, comment, canReview, anchor } = props;\n return (\n <div key={comment.id} class={reviewComment}>\n <div class={reviewCommentHead}>\n <strong>{comment.userId ?? \"anonymous\"}</strong>\n <span>· {new Date(comment.createdAt).toLocaleString()}</span>\n {anchor ?? null}\n {comment.resolved ? <Badge tone=\"success\">resolved</Badge> : null}\n </div>\n <p class={reviewCommentBody}>{comment.body}</p>\n {!comment.resolved && canReview ? (\n <form\n method=\"post\"\n action={`/projects/${project.slug}/builds/${build.id}/comments/${comment.id}/resolve`}\n hx-post={`/projects/${project.slug}/builds/${build.id}/comments/${comment.id}/resolve`}\n hx-target=\"body\"\n class={reviewCommentActions}\n >\n {csrfField()}\n <Button variant=\"ghost\" size=\"sm\" type=\"submit\">\n Mark resolved\n </Button>\n </form>\n ) : null}\n </div>\n );\n}\n\ninterface CommentFormProps {\n project: Project;\n build: Build;\n selectedId: string;\n}\n\n/** Reply form scoped to the selected snapshot. */\nfunction CommentForm(props: CommentFormProps): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, build, selectedId } = props;\n return (\n <form\n method=\"post\"\n action={`/projects/${project.slug}/builds/${build.id}/comments`}\n hx-post={`/projects/${project.slug}/builds/${build.id}/comments`}\n hx-target=\"body\"\n class=\"mt-1\"\n >\n {csrfField()}\n <VStack>\n <input type=\"hidden\" name=\"snapshotId\" value={selectedId} />\n <TextareaField\n label=\"Add comment\"\n name=\"body\"\n rows={3}\n required\n placeholder=\"Leave feedback on this snapshot…\"\n />\n <div>\n <Button variant=\"primary\" type=\"submit\">\n Comment\n </Button>\n </div>\n </VStack>\n </form>\n );\n}\n\n/** Threaded comments for the selected snapshot plus the reply form. */\nexport function DiffComments(\n props: DiffCommentsProps,\n): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, build, selectedId, selectedStoryId, comments, canReview } = props;\n const visible = comments.filter((c) =>\n c.snapshotId ? c.snapshotId === selectedId : !c.storyId || c.storyId === selectedStoryId,\n );\n return (\n <Card>\n <SectionTitle level={3}>Comments</SectionTitle>\n <VStack>\n {visible.map((comment): HtmlEscapedString | Promise<HtmlEscapedString> => (\n <CommentCard\n key={comment.id}\n project={project}\n build={build}\n comment={comment}\n canReview={canReview}\n />\n ))}\n {visible.length === 0 ? <Meta>No comments on this snapshot.</Meta> : null}\n </VStack>\n\n <CommentForm project={project} build={build} selectedId={selectedId} />\n </Card>\n );\n}\n"],"mappings":";;;;;;;;;;;;;AA6CA,SAAgB,YACd,OACgD;CAChD,MAAM,EAAE,SAAS,OAAO,SAAS,WAAW,WAAW;CACvD,OACE,qBAAC,OAAD;EAAsB,OAAO;EAA7B,UAAA;GACE,qBAAC,OAAD;IAAK,OAAO;IAAZ,UAAA;KACE,oBAAC,UAAD,EAAA,UAAS,QAAQ,UAAU,YAAoB,CAAA;KAC/C,qBAAC,QAAD,EAAA,UAAA,CAAM,MAAG,IAAI,KAAK,QAAQ,SAAS,CAAC,CAAC,eAAe,CAAQ,EAAA,CAAA;KAC3D,UAAU;KACV,QAAQ,WAAW,oBAAC,OAAD;MAAO,MAAK;MAAU,UAAA;KAAe,CAAA,IAAI;IAC1D;;GACL,oBAAC,KAAD;IAAG,OAAO;IAAoB,UAAA,QAAQ;GAAQ,CAAA;GAC7C,CAAC,QAAQ,YAAY,YACpB,qBAAC,QAAD;IACE,QAAO;IACP,QAAQ,aAAa,QAAQ,KAAK,UAAU,MAAM,GAAG,YAAY,QAAQ,GAAG;IAC5E,WAAS,aAAa,QAAQ,KAAK,UAAU,MAAM,GAAG,YAAY,QAAQ,GAAG;IAC7E,aAAU;IACV,OAAO;IALT,UAAA,CAOG,UAAU,GACX,oBAAC,QAAD;KAAQ,SAAQ;KAAQ,MAAK;KAAK,MAAK;KAAS,UAAA;IAExC,CAAA,CACJ;GACJ,CAAA,IAAA;EACD;CAtBK,GAAA,QAAQ,EAsBb;AAET;;AASA,SAAS,YAAY,OAAyE;CAC5F,MAAM,EAAE,SAAS,OAAO,eAAe;CACvC,OACE,qBAAC,QAAD;EACE,QAAO;EACP,QAAQ,aAAa,QAAQ,KAAK,UAAU,MAAM,GAAG;EACrD,WAAS,aAAa,QAAQ,KAAK,UAAU,MAAM,GAAG;EACtD,aAAU;EACV,OAAM;EALR,UAAA,CAOG,UAAU,GACX,qBAAC,QAAD,EAAA,UAAA;GACE,oBAAC,SAAD;IAAO,MAAK;IAAS,MAAK;IAAa,OAAO;GAAa,CAAA;GAC3D,oBAAC,eAAD;IACE,OAAM;IACN,MAAK;IACL,MAAM;IACN,UAAA;IACA,aAAY;GACb,CAAA;GACD,oBAAC,OAAD,EAAA,UACE,oBAAC,QAAD;IAAQ,SAAQ;IAAU,MAAK;IAAS,UAAA;GAEhC,CAAA,EACL,CAAA;EACC,EAAA,CAAA,CACJ;;AAEV;;AAGA,SAAgB,aACd,OACgD;CAChD,MAAM,EAAE,SAAS,OAAO,YAAY,iBAAiB,UAAU,cAAc;CAC7E,MAAM,UAAU,SAAS,QAAQ,MAC/B,EAAE,aAAa,EAAE,eAAe,aAAa,CAAC,EAAE,WAAW,EAAE,YAAY,eAC3E;CACA,OACE,qBAAC,MAAD,EAAA,UAAA;EACE,oBAAC,cAAD;GAAc,OAAO;GAAG,UAAA;EAAsB,CAAA;EAC9C,qBAAC,QAAD,EAAA,UAAA,CACG,QAAQ,KAAK,YACZ,oBAAC,aAAD;GAEW;GACF;GACE;GACE;EACZ,GALM,QAAQ,EAKd,CACF,GACA,QAAQ,WAAW,IAAI,oBAAC,MAAD,EAAA,UAAM,gCAAmC,CAAA,IAAI,IAC/D,EAAA,CAAA;EAER,oBAAC,aAAD;GAAsB;GAAgB;GAAmB;EAAa,CAAA;CAClE,EAAA,CAAA;AAEV"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { Alert } from "../ui/feedback.mjs";
|
|
5
|
+
import { StaleBaselineNotice } from "./build-diff-stale.mjs";
|
|
6
|
+
import { environmentChangeOf, labelOf, summariseEnvironmentChanges } from "./environment-change.mjs";
|
|
7
|
+
import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
8
|
+
//#region src/pages/build-diff-environment.tsx
|
|
9
|
+
/**
|
|
10
|
+
* Build-level note: some snapshots were captured in a different environment than their baseline.
|
|
11
|
+
* The default branch migrates them without a diff; a feature branch still diffs them (ADR 0005).
|
|
12
|
+
*/
|
|
13
|
+
function EnvironmentChangeNotice(props) {
|
|
14
|
+
const summary = summariseEnvironmentChanges(props.snapshots);
|
|
15
|
+
if (!summary) return null;
|
|
16
|
+
const { count, fields, browserVersion } = summary;
|
|
17
|
+
const what = `the ${fields.join(", ")} changed${browserVersion ? ` (${browserVersion.from} → ${browserVersion.to})` : ""}`;
|
|
18
|
+
return props.migrated ? /* @__PURE__ */ jsxs(Alert, {
|
|
19
|
+
tone: "info",
|
|
20
|
+
title: "Capture environment changed",
|
|
21
|
+
children: [
|
|
22
|
+
count,
|
|
23
|
+
" ",
|
|
24
|
+
count === 1 ? "snapshot was" : "snapshots were",
|
|
25
|
+
" accepted without a pixel diff because ",
|
|
26
|
+
what,
|
|
27
|
+
". The same code renders differently here, so a diff would only show the environment. The baselines were updated to match."
|
|
28
|
+
]
|
|
29
|
+
}) : /* @__PURE__ */ jsxs(Alert, {
|
|
30
|
+
tone: "info",
|
|
31
|
+
title: "Capture environment changed",
|
|
32
|
+
children: [
|
|
33
|
+
count,
|
|
34
|
+
" ",
|
|
35
|
+
count === 1 ? "snapshot was" : "snapshots were",
|
|
36
|
+
" compared against a baseline from a different capture environment: ",
|
|
37
|
+
what,
|
|
38
|
+
". A difference may come from the environment rather than the code; once the default branch captures in the new environment, a rebuild compares like with like."
|
|
39
|
+
]
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/** Per-snapshot note shown above the viewer for a snapshot whose capture environment changed. */
|
|
43
|
+
function EnvironmentChangeSnapshotNotice(props) {
|
|
44
|
+
const change = environmentChangeOf(props.snapshot);
|
|
45
|
+
if (!change) return null;
|
|
46
|
+
const what = change.changed.map((field) => labelOf(field)).join(", ");
|
|
47
|
+
return props.migrated ? /* @__PURE__ */ jsxs(Alert, {
|
|
48
|
+
tone: "info",
|
|
49
|
+
title: "No diff: capture environment changed",
|
|
50
|
+
children: [
|
|
51
|
+
"This snapshot was not compared pixel by pixel. What changed: ",
|
|
52
|
+
what,
|
|
53
|
+
"."
|
|
54
|
+
]
|
|
55
|
+
}) : /* @__PURE__ */ jsxs(Alert, {
|
|
56
|
+
tone: "info",
|
|
57
|
+
title: "Capture environment changed",
|
|
58
|
+
children: [
|
|
59
|
+
"The baseline was captured in a different environment (",
|
|
60
|
+
what,
|
|
61
|
+
"), so a difference may come from the environment rather than the code."
|
|
62
|
+
]
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/** Build-level notices above the review grid: stale baselines and environment migrations. */
|
|
66
|
+
function ReviewNotices(props) {
|
|
67
|
+
const { project, build, snapshots, drifted, canReview } = props;
|
|
68
|
+
const staleCount = Object.keys(drifted).length;
|
|
69
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [staleCount > 0 ? /* @__PURE__ */ jsx(StaleBaselineNotice, {
|
|
70
|
+
project,
|
|
71
|
+
build,
|
|
72
|
+
staleCount,
|
|
73
|
+
canReview
|
|
74
|
+
}) : null, /* @__PURE__ */ jsx(EnvironmentChangeNotice, {
|
|
75
|
+
snapshots,
|
|
76
|
+
migrated: build.isDefault
|
|
77
|
+
})] });
|
|
78
|
+
}
|
|
79
|
+
//#endregion
|
|
80
|
+
export { EnvironmentChangeNotice, EnvironmentChangeSnapshotNotice, ReviewNotices };
|
|
81
|
+
|
|
82
|
+
//# sourceMappingURL=build-diff-environment.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-diff-environment.mjs","names":[],"sources":["../../src/pages/build-diff-environment.tsx"],"sourcesContent":["import type { Build, Project, Snapshot } from \"@storyshelf/core/schema\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport { Alert } from \"../ui/components.tsx\";\nimport { StaleBaselineNotice } from \"./build-diff-stale.tsx\";\nimport { environmentChangeOf, labelOf, summariseEnvironmentChanges } from \"./environment-change.ts\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\n/**\n * Build-level note: some snapshots were captured in a different environment than their baseline.\n * The default branch migrates them without a diff; a feature branch still diffs them (ADR 0005).\n */\nexport function EnvironmentChangeNotice(props: {\n snapshots: readonly Snapshot[];\n migrated: boolean;\n}): HtmlEscapedString | Promise<HtmlEscapedString> | null {\n const summary = summariseEnvironmentChanges(props.snapshots);\n if (!summary) {\n return null;\n }\n const { count, fields, browserVersion } = summary;\n const what = `the ${fields.join(\", \")} changed${\n browserVersion ? ` (${browserVersion.from} → ${browserVersion.to})` : \"\"\n }`;\n return props.migrated ? (\n <Alert tone=\"info\" title=\"Capture environment changed\">\n {count} {count === 1 ? \"snapshot was\" : \"snapshots were\"} accepted without a pixel diff\n because {what}. The same code renders differently here, so a diff would only show the\n environment. The baselines were updated to match.\n </Alert>\n ) : (\n <Alert tone=\"info\" title=\"Capture environment changed\">\n {count} {count === 1 ? \"snapshot was\" : \"snapshots were\"} compared against a baseline from a\n different capture environment: {what}. A difference may come from the environment rather than\n the code; once the default branch captures in the new environment, a rebuild compares like\n with like.\n </Alert>\n );\n}\n\n/** Per-snapshot note shown above the viewer for a snapshot whose capture environment changed. */\nexport function EnvironmentChangeSnapshotNotice(props: {\n snapshot: Pick<Snapshot, \"envChange\">;\n migrated: boolean;\n}): HtmlEscapedString | Promise<HtmlEscapedString> | null {\n const change = environmentChangeOf(props.snapshot);\n if (!change) {\n return null;\n }\n const what = change.changed.map((field) => labelOf(field)).join(\", \");\n return props.migrated ? (\n <Alert tone=\"info\" title=\"No diff: capture environment changed\">\n This snapshot was not compared pixel by pixel. What changed: {what}.\n </Alert>\n ) : (\n <Alert tone=\"info\" title=\"Capture environment changed\">\n The baseline was captured in a different environment ({what}), so a difference may come from\n the environment rather than the code.\n </Alert>\n );\n}\n\n/** Build-level notices above the review grid: stale baselines and environment migrations. */\nexport function ReviewNotices(props: {\n project: Project;\n build: Build;\n snapshots: readonly Snapshot[];\n drifted: Record<string, unknown>;\n canReview: boolean;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, build, snapshots, drifted, canReview } = props;\n const staleCount = Object.keys(drifted).length;\n return (\n <>\n {staleCount > 0 ? (\n <StaleBaselineNotice\n project={project}\n build={build}\n staleCount={staleCount}\n canReview={canReview}\n />\n ) : null}\n <EnvironmentChangeNotice snapshots={snapshots} migrated={build.isDefault} />\n </>\n );\n}\n"],"mappings":";;;;;;;;;;;;AAYA,SAAgB,wBAAwB,OAGkB;CACxD,MAAM,UAAU,4BAA4B,MAAM,SAAS;CAC3D,IAAI,CAAC,SACH,OAAO;CAET,MAAM,EAAE,OAAO,QAAQ,mBAAmB;CAC1C,MAAM,OAAO,OAAO,OAAO,KAAK,IAAI,EAAE,UACpC,iBAAiB,KAAK,eAAe,KAAK,KAAK,eAAe,GAAG,KAAK;CAExE,OAAO,MAAM,WACX,qBAAC,OAAD;EAAO,MAAK;EAAO,OAAM;EAAzB,UAAA;GACG;GAAM;GAAE,UAAU,IAAI,iBAAiB;GAAiB;GAChD;GAAK;EAET;CAEP,CAAA,IAAA,qBAAC,OAAD;EAAO,MAAK;EAAO,OAAM;EAAzB,UAAA;GACG;GAAM;GAAE,UAAU,IAAI,iBAAiB;GAAiB;GACzB;GAAK;EAGhC;;AAEX;;AAGA,SAAgB,gCAAgC,OAGU;CACxD,MAAM,SAAS,oBAAoB,MAAM,QAAQ;CACjD,IAAI,CAAC,QACH,OAAO;CAET,MAAM,OAAO,OAAO,QAAQ,KAAK,UAAU,QAAQ,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI;CACpE,OAAO,MAAM,WACX,qBAAC,OAAD;EAAO,MAAK;EAAO,OAAM;EAAzB,UAAA;GAAgE;GACA;GAAK;EAC9D;CAEP,CAAA,IAAA,qBAAC,OAAD;EAAO,MAAK;EAAO,OAAM;EAAzB,UAAA;GAAuD;GACE;GAAK;EAEvD;;AAEX;;AAGA,SAAgB,cAAc,OAMqB;CACjD,MAAM,EAAE,SAAS,OAAO,WAAW,SAAS,cAAc;CAC1D,MAAM,aAAa,OAAO,KAAK,OAAO,CAAC,CAAC;CACxC,OACE,qBAAA,UAAA,EAAA,UAAA,CACG,aAAa,IACZ,oBAAC,qBAAD;EACW;EACF;EACK;EACD;CACZ,CAAA,IACC,MACJ,oBAAC,yBAAD;EAAoC;EAAW,UAAU,MAAM;CAAY,CAAA,CAC3E,EAAA,CAAA;AAEN"}
|
|
@@ -5,8 +5,9 @@ import { Button } from "../ui/buttons.mjs";
|
|
|
5
5
|
import { Badge, Meta, statusTone } from "../ui/feedback.mjs";
|
|
6
6
|
import { Avatar } from "../ui/avatar.mjs";
|
|
7
7
|
import { Progress } from "../ui/progress.mjs";
|
|
8
|
-
import {
|
|
8
|
+
import { a11ySummaryOf, a11yUnavailableNote } from "./a11y-violations.mjs";
|
|
9
9
|
import { reviewProgress } from "./build-diff-model.mjs";
|
|
10
|
+
import { headerActions, headerMain, headerMeta, headerProgress, headerSide, headerTitle, reviewHeader } from "../ui/styles/review-layout.mjs";
|
|
10
11
|
import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
11
12
|
//#region src/pages/build-diff-header.tsx
|
|
12
13
|
/** Approve-all / reject-all bulk actions (each asks for confirmation). */
|
|
@@ -46,7 +47,8 @@ function BulkActions({ project, build, pendingCount }) {
|
|
|
46
47
|
})
|
|
47
48
|
})] });
|
|
48
49
|
}
|
|
49
|
-
function HeaderTitle({ build }) {
|
|
50
|
+
function HeaderTitle({ build, snapshots }) {
|
|
51
|
+
const a11y = a11ySummaryOf(snapshots);
|
|
50
52
|
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("h1", {
|
|
51
53
|
class: headerTitle,
|
|
52
54
|
children: [
|
|
@@ -70,7 +72,20 @@ function HeaderTitle({ build }) {
|
|
|
70
72
|
size: "sm"
|
|
71
73
|
}) : null,
|
|
72
74
|
build.authorName ? /* @__PURE__ */ jsx("span", { children: build.authorName }) : null,
|
|
73
|
-
/* @__PURE__ */ jsx("span", { children: build.message ?? "No message" })
|
|
75
|
+
/* @__PURE__ */ jsx("span", { children: build.message ?? "No message" }),
|
|
76
|
+
a11y.findings > 0 ? /* @__PURE__ */ jsxs(Badge, {
|
|
77
|
+
tone: "warning",
|
|
78
|
+
icon: true,
|
|
79
|
+
children: [
|
|
80
|
+
"a11y ",
|
|
81
|
+
a11y.findings,
|
|
82
|
+
" (does not block)"
|
|
83
|
+
]
|
|
84
|
+
}) : null,
|
|
85
|
+
a11y.unavailable > 0 ? /* @__PURE__ */ jsx(Badge, {
|
|
86
|
+
tone: "neutral",
|
|
87
|
+
children: a11yUnavailableNote(a11y.unavailable)
|
|
88
|
+
}) : null
|
|
74
89
|
]
|
|
75
90
|
})] });
|
|
76
91
|
}
|
|
@@ -92,7 +107,10 @@ function DiffHeader(props) {
|
|
|
92
107
|
class: reviewHeader,
|
|
93
108
|
children: [/* @__PURE__ */ jsx("div", {
|
|
94
109
|
class: headerMain,
|
|
95
|
-
children: /* @__PURE__ */ jsx(HeaderTitle, {
|
|
110
|
+
children: /* @__PURE__ */ jsx(HeaderTitle, {
|
|
111
|
+
build,
|
|
112
|
+
snapshots
|
|
113
|
+
})
|
|
96
114
|
}), /* @__PURE__ */ jsxs("div", {
|
|
97
115
|
class: headerSide,
|
|
98
116
|
children: [/* @__PURE__ */ jsx(HeaderProgress, { snapshots }), /* @__PURE__ */ jsxs("div", {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-diff-header.mjs","names":[],"sources":["../../src/pages/build-diff-header.tsx"],"sourcesContent":["import type { Build, Project, Snapshot } from \"@storyshelf/core/schema\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport { Avatar, Badge, Button, Meta, Progress, statusTone } from \"../ui/components.tsx\";\nimport {\n headerActions,\n headerMain,\n headerMeta,\n headerProgress,\n headerSide,\n headerTitle,\n reviewHeader,\n} from \"../ui/styles/review-layout.ts\";\nimport { reviewProgress } from \"./build-diff-model.ts\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\n/** Header props for the diff review page. */\nexport interface DiffHeaderProps {\n project: Project;\n build: Build;\n snapshots: Snapshot[];\n pendingCount: number;\n canReview: boolean;\n}\n\ninterface BulkProps {\n project: Project;\n build: Build;\n pendingCount: number;\n}\n\n/** Approve-all / reject-all bulk actions (each asks for confirmation). */\nfunction BulkActions({\n project,\n build,\n pendingCount,\n}: BulkProps): HtmlEscapedString | Promise<HtmlEscapedString> {\n const base = `/api/v1/projects/${project.slug}/builds/${build.id}`;\n return (\n <>\n <form\n method=\"post\"\n action={`${base}/approve-all`}\n hx-post={`${base}/approve-all`}\n hx-target=\"body\"\n hx-confirm={`Approve all ${pendingCount} remaining changes?`}\n data-toast={`Approved ${pendingCount} ${pendingCount === 1 ? \"change\" : \"changes\"}`}\n >\n <Button variant=\"primary\" size=\"sm\" type=\"submit\" icon=\"check\">\n Approve all ({pendingCount})\n </Button>\n </form>\n <form\n method=\"post\"\n action={`${base}/reject-all`}\n hx-post={`${base}/reject-all`}\n hx-target=\"body\"\n hx-confirm={`Reject all ${pendingCount} remaining changes?`}\n data-toast={`Rejected ${pendingCount} ${pendingCount === 1 ? \"change\" : \"changes\"}`}\n data-toast-tone=\"warning\"\n >\n <Button variant=\"secondary\" size=\"sm\" type=\"submit\">\n Reject all\n </Button>\n </form>\n </>\n );\n}\n\nfunction HeaderTitle({
|
|
1
|
+
{"version":3,"file":"build-diff-header.mjs","names":[],"sources":["../../src/pages/build-diff-header.tsx"],"sourcesContent":["import type { Build, Project, Snapshot } from \"@storyshelf/core/schema\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport { Avatar, Badge, Button, Meta, Progress, statusTone } from \"../ui/components.tsx\";\nimport {\n headerActions,\n headerMain,\n headerMeta,\n headerProgress,\n headerSide,\n headerTitle,\n reviewHeader,\n} from \"../ui/styles/review-layout.ts\";\nimport { a11ySummaryOf, a11yUnavailableNote } from \"./a11y-violations.tsx\";\nimport { reviewProgress } from \"./build-diff-model.ts\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\n/** Header props for the diff review page. */\nexport interface DiffHeaderProps {\n project: Project;\n build: Build;\n snapshots: Snapshot[];\n pendingCount: number;\n canReview: boolean;\n}\n\ninterface BulkProps {\n project: Project;\n build: Build;\n pendingCount: number;\n}\n\n/** Approve-all / reject-all bulk actions (each asks for confirmation). */\nfunction BulkActions({\n project,\n build,\n pendingCount,\n}: BulkProps): HtmlEscapedString | Promise<HtmlEscapedString> {\n const base = `/api/v1/projects/${project.slug}/builds/${build.id}`;\n return (\n <>\n <form\n method=\"post\"\n action={`${base}/approve-all`}\n hx-post={`${base}/approve-all`}\n hx-target=\"body\"\n hx-confirm={`Approve all ${pendingCount} remaining changes?`}\n data-toast={`Approved ${pendingCount} ${pendingCount === 1 ? \"change\" : \"changes\"}`}\n >\n <Button variant=\"primary\" size=\"sm\" type=\"submit\" icon=\"check\">\n Approve all ({pendingCount})\n </Button>\n </form>\n <form\n method=\"post\"\n action={`${base}/reject-all`}\n hx-post={`${base}/reject-all`}\n hx-target=\"body\"\n hx-confirm={`Reject all ${pendingCount} remaining changes?`}\n data-toast={`Rejected ${pendingCount} ${pendingCount === 1 ? \"change\" : \"changes\"}`}\n data-toast-tone=\"warning\"\n >\n <Button variant=\"secondary\" size=\"sm\" type=\"submit\">\n Reject all\n </Button>\n </form>\n </>\n );\n}\n\nfunction HeaderTitle({\n build,\n snapshots,\n}: {\n build: Build;\n snapshots: Snapshot[];\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const a11y = a11ySummaryOf(snapshots);\n return (\n <>\n <h1 class={headerTitle}>\n <span>{build.gitBranch}</span>\n <Meta as=\"span\" mono>\n {build.gitSha.slice(0, 7)}\n </Meta>\n <Badge tone={statusTone(build.status)} icon>\n {build.status}\n </Badge>\n </h1>\n <p class={headerMeta}>\n {build.authorName ? <Avatar name={build.authorName} size=\"sm\" /> : null}\n {build.authorName ? <span>{build.authorName}</span> : null}\n <span>{build.message ?? \"No message\"}</span>\n {a11y.findings > 0 ? (\n <Badge tone=\"warning\" icon>\n a11y {a11y.findings} (does not block)\n </Badge>\n ) : null}\n {a11y.unavailable > 0 ? (\n <Badge tone=\"neutral\">{a11yUnavailableNote(a11y.unavailable)}</Badge>\n ) : null}\n </p>\n </>\n );\n}\n\nfunction HeaderProgress({\n snapshots,\n}: {\n snapshots: Snapshot[];\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const progress = reviewProgress(snapshots);\n return (\n <div class={headerProgress}>\n <span>\n {progress.reviewable === 0\n ? \"No changes to review\"\n : `${progress.done} of ${progress.reviewable} reviewed`}\n </span>\n <Progress value={progress.done} max={progress.reviewable} label=\"Review progress\" />\n </div>\n );\n}\n\n/** Compact build summary with review progress and bulk actions. */\nexport function DiffHeader(props: DiffHeaderProps): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, build, snapshots, pendingCount, canReview } = props;\n return (\n <header class={reviewHeader}>\n <div class={headerMain}>\n <HeaderTitle build={build} snapshots={snapshots} />\n </div>\n <div class={headerSide}>\n <HeaderProgress snapshots={snapshots} />\n <div class={headerActions}>\n <Button\n variant=\"secondary\"\n size=\"sm\"\n href={`/projects/${project.slug}/builds/${build.id}`}\n >\n Build overview\n </Button>\n {canReview && pendingCount > 0 ? (\n <BulkActions project={project} build={build} pendingCount={pendingCount} />\n ) : null}\n </div>\n </div>\n </header>\n );\n}\n"],"mappings":"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"}
|