@storyshelf/app 0.5.3
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/LICENSE +21 -0
- package/README.md +18 -0
- package/dist/app-types.d.mts +54 -0
- package/dist/asset-manifest.mjs +41 -0
- package/dist/asset-manifest.mjs.map +1 -0
- package/dist/asset-paths.mjs +23 -0
- package/dist/asset-paths.mjs.map +1 -0
- package/dist/assets/htmx.min.js +3304 -0
- package/dist/assets/mark.svg +1 -0
- package/dist/capture-setup.mjs +139 -0
- package/dist/capture-setup.mjs.map +1 -0
- package/dist/credential-keys.mjs +20 -0
- package/dist/credential-keys.mjs.map +1 -0
- package/dist/credentials.mjs +56 -0
- package/dist/credentials.mjs.map +1 -0
- package/dist/index.d.mts +18 -0
- package/dist/index.mjs +310 -0
- package/dist/index.mjs.map +1 -0
- package/dist/lifecycle.mjs +132 -0
- package/dist/lifecycle.mjs.map +1 -0
- package/dist/middleware/auth-gate.mjs +36 -0
- package/dist/middleware/auth-gate.mjs.map +1 -0
- package/dist/middleware/csrf.mjs +75 -0
- package/dist/middleware/csrf.mjs.map +1 -0
- package/dist/middleware/rate-limit.mjs +68 -0
- package/dist/middleware/rate-limit.mjs.map +1 -0
- package/dist/middleware/request-log.mjs +30 -0
- package/dist/middleware/request-log.mjs.map +1 -0
- package/dist/middleware/server-timing.mjs +39 -0
- package/dist/middleware/server-timing.mjs.map +1 -0
- package/dist/middleware/setup-gate.mjs +37 -0
- package/dist/middleware/setup-gate.mjs.map +1 -0
- package/dist/middleware/store-scope.mjs +33 -0
- package/dist/middleware/store-scope.mjs.map +1 -0
- package/dist/notify.d.mts +17 -0
- package/dist/notify.mjs +141 -0
- package/dist/notify.mjs.map +1 -0
- package/dist/pages/admin-system.mjs +243 -0
- package/dist/pages/admin-system.mjs.map +1 -0
- package/dist/pages/build-detail.mjs +392 -0
- package/dist/pages/build-detail.mjs.map +1 -0
- package/dist/pages/build-diff-actions.mjs +162 -0
- package/dist/pages/build-diff-actions.mjs.map +1 -0
- package/dist/pages/build-diff-comments.mjs +104 -0
- package/dist/pages/build-diff-comments.mjs.map +1 -0
- package/dist/pages/build-diff-header.mjs +117 -0
- package/dist/pages/build-diff-header.mjs.map +1 -0
- package/dist/pages/build-diff-model.mjs +48 -0
- package/dist/pages/build-diff-model.mjs.map +1 -0
- package/dist/pages/build-diff-nav.mjs +122 -0
- package/dist/pages/build-diff-nav.mjs.map +1 -0
- package/dist/pages/build-diff-shortcuts.mjs +78 -0
- package/dist/pages/build-diff-shortcuts.mjs.map +1 -0
- package/dist/pages/build-diff-stale.mjs +70 -0
- package/dist/pages/build-diff-stale.mjs.map +1 -0
- package/dist/pages/build-diff-viewer.mjs +70 -0
- package/dist/pages/build-diff-viewer.mjs.map +1 -0
- package/dist/pages/build-diff.mjs +110 -0
- package/dist/pages/build-diff.mjs.map +1 -0
- package/dist/pages/compute-jobs.mjs +165 -0
- package/dist/pages/compute-jobs.mjs.map +1 -0
- package/dist/pages/invite.mjs +91 -0
- package/dist/pages/invite.mjs.map +1 -0
- package/dist/pages/label-detail.mjs +154 -0
- package/dist/pages/label-detail.mjs.map +1 -0
- package/dist/pages/library.mjs +343 -0
- package/dist/pages/library.mjs.map +1 -0
- package/dist/pages/login.mjs +117 -0
- package/dist/pages/login.mjs.map +1 -0
- package/dist/pages/passkey-ceremony.mjs +97 -0
- package/dist/pages/passkey-ceremony.mjs.map +1 -0
- package/dist/pages/profile.mjs +364 -0
- package/dist/pages/profile.mjs.map +1 -0
- package/dist/pages/project-builds-table.mjs +151 -0
- package/dist/pages/project-builds-table.mjs.map +1 -0
- package/dist/pages/project-builds.mjs +143 -0
- package/dist/pages/project-builds.mjs.map +1 -0
- package/dist/pages/project-create.mjs +90 -0
- package/dist/pages/project-create.mjs.map +1 -0
- package/dist/pages/project-settings.mjs +95 -0
- package/dist/pages/project-settings.mjs.map +1 -0
- package/dist/pages/projects-card.mjs +194 -0
- package/dist/pages/projects-card.mjs.map +1 -0
- package/dist/pages/projects.mjs +129 -0
- package/dist/pages/projects.mjs.map +1 -0
- package/dist/pages/root.mjs +98 -0
- package/dist/pages/root.mjs.map +1 -0
- package/dist/pages/settings-general.mjs +138 -0
- package/dist/pages/settings-general.mjs.map +1 -0
- package/dist/pages/settings-labels.mjs +107 -0
- package/dist/pages/settings-labels.mjs.map +1 -0
- package/dist/pages/settings-members.mjs +192 -0
- package/dist/pages/settings-members.mjs.map +1 -0
- package/dist/pages/settings-notifications.mjs +240 -0
- package/dist/pages/settings-notifications.mjs.map +1 -0
- package/dist/pages/settings-status.mjs +137 -0
- package/dist/pages/settings-status.mjs.map +1 -0
- package/dist/pages/settings-tests.mjs +111 -0
- package/dist/pages/settings-tests.mjs.map +1 -0
- package/dist/pages/settings-tokens.mjs +90 -0
- package/dist/pages/settings-tokens.mjs.map +1 -0
- package/dist/pages/settings-webhooks.mjs +104 -0
- package/dist/pages/settings-webhooks.mjs.map +1 -0
- package/dist/pages/storybook.mjs +116 -0
- package/dist/pages/storybook.mjs.map +1 -0
- package/dist/retention-timer.mjs +65 -0
- package/dist/retention-timer.mjs.map +1 -0
- package/dist/routers/admin-pages.mjs +48 -0
- package/dist/routers/admin-pages.mjs.map +1 -0
- package/dist/routers/admin.mjs +113 -0
- package/dist/routers/admin.mjs.map +1 -0
- package/dist/routers/assets.mjs +64 -0
- package/dist/routers/assets.mjs.map +1 -0
- package/dist/routers/attempts.mjs +96 -0
- package/dist/routers/attempts.mjs.map +1 -0
- package/dist/routers/auth-sync.mjs +64 -0
- package/dist/routers/auth-sync.mjs.map +1 -0
- package/dist/routers/auth.mjs +236 -0
- package/dist/routers/auth.mjs.map +1 -0
- package/dist/routers/baseline-guard.mjs +83 -0
- package/dist/routers/baseline-guard.mjs.map +1 -0
- package/dist/routers/builds.handlers.mjs +217 -0
- package/dist/routers/builds.handlers.mjs.map +1 -0
- package/dist/routers/builds.mjs +381 -0
- package/dist/routers/builds.mjs.map +1 -0
- package/dist/routers/comments.mjs +109 -0
- package/dist/routers/comments.mjs.map +1 -0
- package/dist/routers/flash.mjs +35 -0
- package/dist/routers/flash.mjs.map +1 -0
- package/dist/routers/health-report.mjs +159 -0
- package/dist/routers/health-report.mjs.map +1 -0
- package/dist/routers/health.mjs +46 -0
- package/dist/routers/health.mjs.map +1 -0
- package/dist/routers/helpers.mjs +110 -0
- package/dist/routers/helpers.mjs.map +1 -0
- package/dist/routers/htmx.mjs +24 -0
- package/dist/routers/htmx.mjs.map +1 -0
- package/dist/routers/labels.mjs +118 -0
- package/dist/routers/labels.mjs.map +1 -0
- package/dist/routers/media.mjs +53 -0
- package/dist/routers/media.mjs.map +1 -0
- package/dist/routers/members.mjs +177 -0
- package/dist/routers/members.mjs.map +1 -0
- package/dist/routers/notification-channels.mjs +190 -0
- package/dist/routers/notification-channels.mjs.map +1 -0
- package/dist/routers/notification-subscriptions.mjs +120 -0
- package/dist/routers/notification-subscriptions.mjs.map +1 -0
- package/dist/routers/profile.mjs +276 -0
- package/dist/routers/profile.mjs.map +1 -0
- package/dist/routers/projects.mjs +133 -0
- package/dist/routers/projects.mjs.map +1 -0
- package/dist/routers/review-next.mjs +47 -0
- package/dist/routers/review-next.mjs.map +1 -0
- package/dist/routers/schemas.mjs +337 -0
- package/dist/routers/schemas.mjs.map +1 -0
- package/dist/routers/settings-general.mjs +104 -0
- package/dist/routers/settings-general.mjs.map +1 -0
- package/dist/routers/settings-labels.mjs +52 -0
- package/dist/routers/settings-labels.mjs.map +1 -0
- package/dist/routers/settings-members.mjs +58 -0
- package/dist/routers/settings-members.mjs.map +1 -0
- package/dist/routers/settings-notifications.mjs +108 -0
- package/dist/routers/settings-notifications.mjs.map +1 -0
- package/dist/routers/settings-status.mjs +77 -0
- package/dist/routers/settings-status.mjs.map +1 -0
- package/dist/routers/settings-tokens.mjs +40 -0
- package/dist/routers/settings-tokens.mjs.map +1 -0
- package/dist/routers/settings-webhooks.mjs +67 -0
- package/dist/routers/settings-webhooks.mjs.map +1 -0
- package/dist/routers/settings.handlers.mjs +149 -0
- package/dist/routers/settings.handlers.mjs.map +1 -0
- package/dist/routers/settings.mjs +25 -0
- package/dist/routers/settings.mjs.map +1 -0
- package/dist/routers/snapshots.mjs +240 -0
- package/dist/routers/snapshots.mjs.map +1 -0
- package/dist/routers/status-configs.mjs +108 -0
- package/dist/routers/status-configs.mjs.map +1 -0
- package/dist/routers/storybook.mjs +171 -0
- package/dist/routers/storybook.mjs.map +1 -0
- package/dist/routers/tokens.mjs +91 -0
- package/dist/routers/tokens.mjs.map +1 -0
- package/dist/routers/ui.mjs +166 -0
- package/dist/routers/ui.mjs.map +1 -0
- package/dist/routers/webhooks.mjs +97 -0
- package/dist/routers/webhooks.mjs.map +1 -0
- package/dist/routers/well-known.mjs +43 -0
- package/dist/routers/well-known.mjs.map +1 -0
- package/dist/runtime.mjs +52 -0
- package/dist/runtime.mjs.map +1 -0
- package/dist/store.mjs +20 -0
- package/dist/store.mjs.map +1 -0
- package/dist/ui/avatar.mjs +76 -0
- package/dist/ui/avatar.mjs.map +1 -0
- package/dist/ui/buttons.mjs +205 -0
- package/dist/ui/buttons.mjs.map +1 -0
- package/dist/ui/code-block.mjs +47 -0
- package/dist/ui/code-block.mjs.map +1 -0
- package/dist/ui/compare-stage.mjs +344 -0
- package/dist/ui/compare-stage.mjs.map +1 -0
- package/dist/ui/csrf-field.mjs +20 -0
- package/dist/ui/csrf-field.mjs.map +1 -0
- package/dist/ui/css.mjs +36 -0
- package/dist/ui/css.mjs.map +1 -0
- package/dist/ui/document.mjs +130 -0
- package/dist/ui/document.mjs.map +1 -0
- package/dist/ui/dropdown.mjs +181 -0
- package/dist/ui/dropdown.mjs.map +1 -0
- package/dist/ui/feedback.mjs +341 -0
- package/dist/ui/feedback.mjs.map +1 -0
- package/dist/ui/filter-input.mjs +58 -0
- package/dist/ui/filter-input.mjs.map +1 -0
- package/dist/ui/forms.mjs +291 -0
- package/dist/ui/forms.mjs.map +1 -0
- package/dist/ui/icons/icon.mjs +53 -0
- package/dist/ui/icons/icon.mjs.map +1 -0
- package/dist/ui/icons/paths.mjs +61 -0
- package/dist/ui/icons/paths.mjs.map +1 -0
- package/dist/ui/icons/sprite.mjs +26 -0
- package/dist/ui/icons/sprite.mjs.map +1 -0
- package/dist/ui/kbd.mjs +33 -0
- package/dist/ui/kbd.mjs.map +1 -0
- package/dist/ui/layout.mjs +173 -0
- package/dist/ui/layout.mjs.map +1 -0
- package/dist/ui/progress.mjs +44 -0
- package/dist/ui/progress.mjs.map +1 -0
- package/dist/ui/segmented.mjs +77 -0
- package/dist/ui/segmented.mjs.map +1 -0
- package/dist/ui/shell/account.mjs +161 -0
- package/dist/ui/shell/account.mjs.map +1 -0
- package/dist/ui/shell/bare.mjs +69 -0
- package/dist/ui/shell/bare.mjs.map +1 -0
- package/dist/ui/shell/boot-script.mjs +46 -0
- package/dist/ui/shell/boot-script.mjs.map +1 -0
- package/dist/ui/shell/client-script.mjs +259 -0
- package/dist/ui/shell/client-script.mjs.map +1 -0
- package/dist/ui/shell/nav.mjs +111 -0
- package/dist/ui/shell/nav.mjs.map +1 -0
- package/dist/ui/shell/review-script.mjs +195 -0
- package/dist/ui/shell/review-script.mjs.map +1 -0
- package/dist/ui/shell/sidebar.mjs +323 -0
- package/dist/ui/shell/sidebar.mjs.map +1 -0
- package/dist/ui/shell/topbar.mjs +268 -0
- package/dist/ui/shell/topbar.mjs.map +1 -0
- package/dist/ui/stacks.mjs +82 -0
- package/dist/ui/stacks.mjs.map +1 -0
- package/dist/ui/styles/base.mjs +41 -0
- package/dist/ui/styles/base.mjs.map +1 -0
- package/dist/ui/styles/behavior.mjs +43 -0
- package/dist/ui/styles/behavior.mjs.map +1 -0
- package/dist/ui/styles/review-layout.mjs +347 -0
- package/dist/ui/styles/review-layout.mjs.map +1 -0
- package/dist/ui/styles/review.mjs +86 -0
- package/dist/ui/styles/review.mjs.map +1 -0
- package/dist/ui/styles/tokens.mjs +132 -0
- package/dist/ui/styles/tokens.mjs.map +1 -0
- package/dist/ui/styles.mjs +19 -0
- package/dist/ui/styles.mjs.map +1 -0
- package/dist/ui/subnav.mjs +91 -0
- package/dist/ui/subnav.mjs.map +1 -0
- package/dist/ui/table.mjs +73 -0
- package/dist/ui/table.mjs.map +1 -0
- package/dist/ui/theme.mjs +64 -0
- package/dist/ui/theme.mjs.map +1 -0
- package/dist/ui/thumbnail.mjs +51 -0
- package/dist/ui/thumbnail.mjs.map +1 -0
- package/dist/ui/time.mjs +50 -0
- package/dist/ui/time.mjs.map +1 -0
- package/package.json +92 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { Button } from "../ui/buttons.mjs";
|
|
5
|
+
import { Badge, Meta } from "../ui/feedback.mjs";
|
|
6
|
+
import { TextareaField } from "../ui/forms.mjs";
|
|
7
|
+
import { Card, SectionTitle } from "../ui/layout.mjs";
|
|
8
|
+
import { VStack } from "../ui/stacks.mjs";
|
|
9
|
+
import { reviewComment, reviewCommentActions, reviewCommentBody, reviewCommentHead } from "../ui/styles/review.mjs";
|
|
10
|
+
import { jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
11
|
+
//#region src/pages/build-diff-comments.tsx
|
|
12
|
+
/** One comment with an optional resolve action. */
|
|
13
|
+
function CommentCard(props) {
|
|
14
|
+
const { project, build, comment, canReview } = props;
|
|
15
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
16
|
+
class: reviewComment,
|
|
17
|
+
children: [
|
|
18
|
+
/* @__PURE__ */ jsxs("div", {
|
|
19
|
+
class: reviewCommentHead,
|
|
20
|
+
children: [
|
|
21
|
+
/* @__PURE__ */ jsx("strong", { children: comment.userId ?? "anonymous" }),
|
|
22
|
+
/* @__PURE__ */ jsxs("span", { children: ["· ", new Date(comment.createdAt).toLocaleString()] }),
|
|
23
|
+
comment.resolved ? /* @__PURE__ */ jsx(Badge, {
|
|
24
|
+
tone: "success",
|
|
25
|
+
children: "resolved"
|
|
26
|
+
}) : null
|
|
27
|
+
]
|
|
28
|
+
}),
|
|
29
|
+
/* @__PURE__ */ jsx("p", {
|
|
30
|
+
class: reviewCommentBody,
|
|
31
|
+
children: comment.body
|
|
32
|
+
}),
|
|
33
|
+
!comment.resolved && canReview ? /* @__PURE__ */ jsx("form", {
|
|
34
|
+
method: "post",
|
|
35
|
+
action: `/api/v1/projects/${project.slug}/builds/${build.id}/comments/${comment.id}/resolve`,
|
|
36
|
+
"hx-post": `/api/v1/projects/${project.slug}/builds/${build.id}/comments/${comment.id}/resolve`,
|
|
37
|
+
"hx-target": "body",
|
|
38
|
+
class: reviewCommentActions,
|
|
39
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
40
|
+
variant: "ghost",
|
|
41
|
+
size: "sm",
|
|
42
|
+
type: "submit",
|
|
43
|
+
children: "Mark resolved"
|
|
44
|
+
})
|
|
45
|
+
}) : null
|
|
46
|
+
]
|
|
47
|
+
}, comment.id);
|
|
48
|
+
}
|
|
49
|
+
/** Reply form scoped to the selected snapshot. */
|
|
50
|
+
function CommentForm(props) {
|
|
51
|
+
const { project, build, selectedId } = props;
|
|
52
|
+
return /* @__PURE__ */ jsx("form", {
|
|
53
|
+
method: "post",
|
|
54
|
+
action: `/api/v1/projects/${project.slug}/builds/${build.id}/comments`,
|
|
55
|
+
"hx-post": `/api/v1/projects/${project.slug}/builds/${build.id}/comments`,
|
|
56
|
+
"hx-target": "body",
|
|
57
|
+
class: "mt-1",
|
|
58
|
+
children: /* @__PURE__ */ jsxs(VStack, { children: [
|
|
59
|
+
/* @__PURE__ */ jsx("input", {
|
|
60
|
+
type: "hidden",
|
|
61
|
+
name: "snapshotId",
|
|
62
|
+
value: selectedId
|
|
63
|
+
}),
|
|
64
|
+
/* @__PURE__ */ jsx(TextareaField, {
|
|
65
|
+
label: "Add comment",
|
|
66
|
+
name: "body",
|
|
67
|
+
rows: 3,
|
|
68
|
+
required: true,
|
|
69
|
+
placeholder: "Leave feedback on this snapshot…"
|
|
70
|
+
}),
|
|
71
|
+
/* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(Button, {
|
|
72
|
+
variant: "primary",
|
|
73
|
+
type: "submit",
|
|
74
|
+
children: "Comment"
|
|
75
|
+
}) })
|
|
76
|
+
] })
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/** Threaded comments for the selected snapshot plus the reply form. */
|
|
80
|
+
function DiffComments(props) {
|
|
81
|
+
const { project, build, selectedId, comments, canReview } = props;
|
|
82
|
+
const visible = comments.filter((c) => !c.snapshotId || c.snapshotId === selectedId);
|
|
83
|
+
return /* @__PURE__ */ jsxs(Card, { children: [
|
|
84
|
+
/* @__PURE__ */ jsx(SectionTitle, {
|
|
85
|
+
level: 3,
|
|
86
|
+
children: "Comments"
|
|
87
|
+
}),
|
|
88
|
+
/* @__PURE__ */ jsxs(VStack, { children: [visible.map((comment) => /* @__PURE__ */ jsx(CommentCard, {
|
|
89
|
+
project,
|
|
90
|
+
build,
|
|
91
|
+
comment,
|
|
92
|
+
canReview
|
|
93
|
+
}, comment.id)), visible.length === 0 ? /* @__PURE__ */ jsx(Meta, { children: "No comments on this snapshot." }) : null] }),
|
|
94
|
+
/* @__PURE__ */ jsx(CommentForm, {
|
|
95
|
+
project,
|
|
96
|
+
build,
|
|
97
|
+
selectedId
|
|
98
|
+
})
|
|
99
|
+
] });
|
|
100
|
+
}
|
|
101
|
+
//#endregion
|
|
102
|
+
export { DiffComments };
|
|
103
|
+
|
|
104
|
+
//# sourceMappingURL=build-diff-comments.mjs.map
|
|
@@ -0,0 +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\ninterface CommentCardProps {\n project: Project;\n build: Build;\n comment: Comment;\n canReview: boolean;\n}\n\n/** One comment with an optional resolve action. */\nfunction CommentCard(props: CommentCardProps): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, build, comment, canReview } = 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 {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={`/api/v1/projects/${project.slug}/builds/${build.id}/comments/${comment.id}/resolve`}\n hx-post={`/api/v1/projects/${project.slug}/builds/${build.id}/comments/${comment.id}/resolve`}\n hx-target=\"body\"\n class={reviewCommentActions}\n >\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={`/api/v1/projects/${project.slug}/builds/${build.id}/comments`}\n hx-post={`/api/v1/projects/${project.slug}/builds/${build.id}/comments`}\n hx-target=\"body\"\n class=\"mt-1\"\n >\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, comments, canReview } = props;\n const visible = comments.filter((c) => !c.snapshotId || c.snapshotId === selectedId);\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":";;;;;;;;;;;;AAuCA,SAAS,YAAY,OAAyE;CAC5F,MAAM,EAAE,SAAS,OAAO,SAAS,cAAc;CAC/C,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,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,oBAAC,QAAD;IACE,QAAO;IACP,QAAQ,oBAAoB,QAAQ,KAAK,UAAU,MAAM,GAAG,YAAY,QAAQ,GAAG;IACnF,WAAS,oBAAoB,QAAQ,KAAK,UAAU,MAAM,GAAG,YAAY,QAAQ,GAAG;IACpF,aAAU;IACV,OAAO;IAEP,UAAA,oBAAC,QAAD;KAAQ,SAAQ;KAAQ,MAAK;KAAK,MAAK;KAAS,UAAA;IAExC,CAAA;GACJ,CAAA,IACJ;EACD;CApBK,GAAA,QAAQ,EAoBb;AAET;;AASA,SAAS,YAAY,OAAyE;CAC5F,MAAM,EAAE,SAAS,OAAO,eAAe;CACvC,OACE,oBAAC,QAAD;EACE,QAAO;EACP,QAAQ,oBAAoB,QAAQ,KAAK,UAAU,MAAM,GAAG;EAC5D,WAAS,oBAAoB,QAAQ,KAAK,UAAU,MAAM,GAAG;EAC7D,aAAU;EACV,OAAM;EAEN,UAAA,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,CAAA;AAEV;;AAGA,SAAgB,aACd,OACgD;CAChD,MAAM,EAAE,SAAS,OAAO,YAAY,UAAU,cAAc;CAC5D,MAAM,UAAU,SAAS,QAAQ,MAAM,CAAC,EAAE,cAAc,EAAE,eAAe,UAAU;CACnF,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,117 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { Button } from "../ui/buttons.mjs";
|
|
5
|
+
import { Badge, Meta, statusTone } from "../ui/feedback.mjs";
|
|
6
|
+
import { Avatar } from "../ui/avatar.mjs";
|
|
7
|
+
import { Progress } from "../ui/progress.mjs";
|
|
8
|
+
import { headerActions, headerMain, headerMeta, headerProgress, headerSide, headerTitle, reviewHeader } from "../ui/styles/review-layout.mjs";
|
|
9
|
+
import { reviewProgress } from "./build-diff-model.mjs";
|
|
10
|
+
import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
11
|
+
//#region src/pages/build-diff-header.tsx
|
|
12
|
+
/** Approve-all / reject-all bulk actions (each asks for confirmation). */
|
|
13
|
+
function BulkActions({ project, build, pendingCount }) {
|
|
14
|
+
const base = `/api/v1/projects/${project.slug}/builds/${build.id}`;
|
|
15
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("form", {
|
|
16
|
+
method: "post",
|
|
17
|
+
action: `${base}/approve-all`,
|
|
18
|
+
"hx-post": `${base}/approve-all`,
|
|
19
|
+
"hx-target": "body",
|
|
20
|
+
"hx-confirm": `Approve all ${pendingCount} remaining changes?`,
|
|
21
|
+
"data-toast": `Approved ${pendingCount} ${pendingCount === 1 ? "change" : "changes"}`,
|
|
22
|
+
children: /* @__PURE__ */ jsxs(Button, {
|
|
23
|
+
variant: "primary",
|
|
24
|
+
size: "sm",
|
|
25
|
+
type: "submit",
|
|
26
|
+
icon: "check",
|
|
27
|
+
children: [
|
|
28
|
+
"Approve all (",
|
|
29
|
+
pendingCount,
|
|
30
|
+
")"
|
|
31
|
+
]
|
|
32
|
+
})
|
|
33
|
+
}), /* @__PURE__ */ jsx("form", {
|
|
34
|
+
method: "post",
|
|
35
|
+
action: `${base}/reject-all`,
|
|
36
|
+
"hx-post": `${base}/reject-all`,
|
|
37
|
+
"hx-target": "body",
|
|
38
|
+
"hx-confirm": `Reject all ${pendingCount} remaining changes?`,
|
|
39
|
+
"data-toast": `Rejected ${pendingCount} ${pendingCount === 1 ? "change" : "changes"}`,
|
|
40
|
+
"data-toast-tone": "warning",
|
|
41
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
42
|
+
variant: "secondary",
|
|
43
|
+
size: "sm",
|
|
44
|
+
type: "submit",
|
|
45
|
+
children: "Reject all"
|
|
46
|
+
})
|
|
47
|
+
})] });
|
|
48
|
+
}
|
|
49
|
+
function HeaderTitle({ build }) {
|
|
50
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("h1", {
|
|
51
|
+
class: headerTitle,
|
|
52
|
+
children: [
|
|
53
|
+
/* @__PURE__ */ jsx("span", { children: build.gitBranch }),
|
|
54
|
+
/* @__PURE__ */ jsx(Meta, {
|
|
55
|
+
as: "span",
|
|
56
|
+
mono: true,
|
|
57
|
+
children: build.gitSha.slice(0, 7)
|
|
58
|
+
}),
|
|
59
|
+
/* @__PURE__ */ jsx(Badge, {
|
|
60
|
+
tone: statusTone(build.status),
|
|
61
|
+
icon: true,
|
|
62
|
+
children: build.status
|
|
63
|
+
})
|
|
64
|
+
]
|
|
65
|
+
}), /* @__PURE__ */ jsxs("p", {
|
|
66
|
+
class: headerMeta,
|
|
67
|
+
children: [
|
|
68
|
+
build.authorName ? /* @__PURE__ */ jsx(Avatar, {
|
|
69
|
+
name: build.authorName,
|
|
70
|
+
size: "sm"
|
|
71
|
+
}) : null,
|
|
72
|
+
build.authorName ? /* @__PURE__ */ jsx("span", { children: build.authorName }) : null,
|
|
73
|
+
/* @__PURE__ */ jsx("span", { children: build.message ?? "No message" })
|
|
74
|
+
]
|
|
75
|
+
})] });
|
|
76
|
+
}
|
|
77
|
+
function HeaderProgress({ snapshots }) {
|
|
78
|
+
const progress = reviewProgress(snapshots);
|
|
79
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
80
|
+
class: headerProgress,
|
|
81
|
+
children: [/* @__PURE__ */ jsx("span", { children: progress.reviewable === 0 ? "No changes to review" : `${progress.done} of ${progress.reviewable} reviewed` }), /* @__PURE__ */ jsx(Progress, {
|
|
82
|
+
value: progress.done,
|
|
83
|
+
max: progress.reviewable,
|
|
84
|
+
label: "Review progress"
|
|
85
|
+
})]
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/** Compact build summary with review progress and bulk actions. */
|
|
89
|
+
function DiffHeader(props) {
|
|
90
|
+
const { project, build, snapshots, pendingCount, canReview } = props;
|
|
91
|
+
return /* @__PURE__ */ jsxs("header", {
|
|
92
|
+
class: reviewHeader,
|
|
93
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
94
|
+
class: headerMain,
|
|
95
|
+
children: /* @__PURE__ */ jsx(HeaderTitle, { build })
|
|
96
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
97
|
+
class: headerSide,
|
|
98
|
+
children: [/* @__PURE__ */ jsx(HeaderProgress, { snapshots }), /* @__PURE__ */ jsxs("div", {
|
|
99
|
+
class: headerActions,
|
|
100
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
101
|
+
variant: "secondary",
|
|
102
|
+
size: "sm",
|
|
103
|
+
href: `/projects/${project.slug}/builds/${build.id}`,
|
|
104
|
+
children: "Build overview"
|
|
105
|
+
}), canReview && pendingCount > 0 ? /* @__PURE__ */ jsx(BulkActions, {
|
|
106
|
+
project,
|
|
107
|
+
build,
|
|
108
|
+
pendingCount
|
|
109
|
+
}) : null]
|
|
110
|
+
})]
|
|
111
|
+
})]
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
//#endregion
|
|
115
|
+
export { DiffHeader };
|
|
116
|
+
|
|
117
|
+
//# sourceMappingURL=build-diff-header.mjs.map
|
|
@@ -0,0 +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({ build }: { build: Build }): HtmlEscapedString | Promise<HtmlEscapedString> {\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 </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} />\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":";;;;;;;;;;;;AAgCA,SAAS,YAAY,EACnB,SACA,OACA,gBAC4D;CAC5D,MAAM,OAAO,oBAAoB,QAAQ,KAAK,UAAU,MAAM;CAC9D,OACE,qBAAA,UAAA,EAAA,UAAA,CACE,oBAAC,QAAD;EACE,QAAO;EACP,QAAQ,GAAG,KAAK;EAChB,WAAS,GAAG,KAAK;EACjB,aAAU;EACV,cAAY,eAAe,aAAa;EACxC,cAAY,YAAY,aAAa,GAAG,iBAAiB,IAAI,WAAW;EAExE,UAAA,qBAAC,QAAD;GAAQ,SAAQ;GAAU,MAAK;GAAK,MAAK;GAAS,MAAK;GAAvD,UAAA;IAA+D;IAC/C;IAAa;GACrB;;CACJ,CAAA,GACN,oBAAC,QAAD;EACE,QAAO;EACP,QAAQ,GAAG,KAAK;EAChB,WAAS,GAAG,KAAK;EACjB,aAAU;EACV,cAAY,cAAc,aAAa;EACvC,cAAY,YAAY,aAAa,GAAG,iBAAiB,IAAI,WAAW;EACxE,mBAAgB;EAEhB,UAAA,oBAAC,QAAD;GAAQ,SAAQ;GAAY,MAAK;GAAK,MAAK;GAAS,UAAA;EAE5C,CAAA;CACJ,CAAA,CACN,EAAA,CAAA;AAEN;AAEA,SAAS,YAAY,EAAE,SAA2E;CAChG,OACE,qBAAA,UAAA,EAAA,UAAA,CACE,qBAAC,MAAD;EAAI,OAAO;EAAX,UAAA;GACE,oBAAC,QAAD,EAAA,UAAO,MAAM,UAAgB,CAAA;GAC7B,oBAAC,MAAD;IAAM,IAAG;IAAO,MAAA;IACb,UAAA,MAAM,OAAO,MAAM,GAAG,CAAC;GACpB,CAAA;GACN,oBAAC,OAAD;IAAO,MAAM,WAAW,MAAM,MAAM;IAAG,MAAA;IACpC,UAAA,MAAM;GACF,CAAA;EACL;CACJ,CAAA,GAAA,qBAAC,KAAD;EAAG,OAAO;EAAV,UAAA;GACG,MAAM,aAAa,oBAAC,QAAD;IAAQ,MAAM,MAAM;IAAY,MAAK;GAAM,CAAA,IAAI;GAClE,MAAM,aAAa,oBAAC,QAAD,EAAA,UAAO,MAAM,WAAiB,CAAA,IAAI;GACtD,oBAAC,QAAD,EAAA,UAAO,MAAM,WAAW,aAAmB,CAAA;EAC1C;CACH,CAAA,CAAA,EAAA,CAAA;AAEN;AAEA,SAAS,eAAe,EACtB,aAGiD;CACjD,MAAM,WAAW,eAAe,SAAS;CACzC,OACE,qBAAC,OAAD;EAAK,OAAO;EAAZ,UAAA,CACE,oBAAC,QAAD,EAAA,UACG,SAAS,eAAe,IACrB,yBACA,GAAG,SAAS,KAAK,MAAM,SAAS,WAAW,WAC3C,CAAA,GACN,oBAAC,UAAD;GAAU,OAAO,SAAS;GAAM,KAAK,SAAS;GAAY,OAAM;EAAmB,CAAA,CAChF;;AAET;;AAGA,SAAgB,WAAW,OAAwE;CACjG,MAAM,EAAE,SAAS,OAAO,WAAW,cAAc,cAAc;CAC/D,OACE,qBAAC,UAAD;EAAQ,OAAO;EAAf,UAAA,CACE,oBAAC,OAAD;GAAK,OAAO;GACV,UAAA,oBAAC,aAAD,EAAoB,MAAQ,CAAA;EACzB,CAAA,GACL,qBAAC,OAAD;GAAK,OAAO;GAAZ,UAAA,CACE,oBAAC,gBAAD,EAA2B,UAAY,CAAA,GACvC,qBAAC,OAAD;IAAK,OAAO;IAAZ,UAAA,CACE,oBAAC,QAAD;KACE,SAAQ;KACR,MAAK;KACL,MAAM,aAAa,QAAQ,KAAK,UAAU,MAAM;KACjD,UAAA;IAEO,CAAA,GACP,aAAa,eAAe,IAC3B,oBAAC,aAAD;KAAsB;KAAgB;KAAqB;IAAe,CAAA,IACxE,IACD;GACF,CAAA,CAAA;EACC,CAAA,CAAA;;AAEZ"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
//#region src/pages/build-diff-model.ts
|
|
5
|
+
const OPEN = /* @__PURE__ */ new Set(["new", "changed"]);
|
|
6
|
+
const DECIDED = /* @__PURE__ */ new Set(["approved", "rejected"]);
|
|
7
|
+
/** Count review progress; unchanged snapshots need no decision and are excluded. */
|
|
8
|
+
function reviewProgress(snapshots) {
|
|
9
|
+
const pending = snapshots.filter((snap) => OPEN.has(snap.status)).length;
|
|
10
|
+
const done = snapshots.filter((snap) => DECIDED.has(snap.status)).length;
|
|
11
|
+
return {
|
|
12
|
+
reviewable: pending + done,
|
|
13
|
+
done,
|
|
14
|
+
pending
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/** Previous/next snapshot around the selected one. */
|
|
18
|
+
function neighbours(snapshots, selectedId) {
|
|
19
|
+
const index = snapshots.findIndex((snap) => snap.id === selectedId);
|
|
20
|
+
if (index === -1) return {
|
|
21
|
+
position: 0,
|
|
22
|
+
total: snapshots.length,
|
|
23
|
+
prev: null,
|
|
24
|
+
next: null
|
|
25
|
+
};
|
|
26
|
+
return {
|
|
27
|
+
position: index + 1,
|
|
28
|
+
total: snapshots.length,
|
|
29
|
+
prev: snapshots[index - 1] ?? null,
|
|
30
|
+
next: snapshots[index + 1] ?? null
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** Diff size as a percentage string ("3.1%"), or null when not computed. */
|
|
34
|
+
function diffPercent(snapshot) {
|
|
35
|
+
return snapshot.diffRatio === null || snapshot.diffRatio === void 0 ? null : `${(snapshot.diffRatio * 100).toFixed(1)}%`;
|
|
36
|
+
}
|
|
37
|
+
/** Review-page URL for one snapshot. */
|
|
38
|
+
function snapshotPageUrl(project, build, snapshotId) {
|
|
39
|
+
return `/projects/${project.slug}/builds/${build.id}/diff?snapshot=${snapshotId}`;
|
|
40
|
+
}
|
|
41
|
+
/** API URL of a snapshot image (`image`), its diff overlay, or its baseline. */
|
|
42
|
+
function snapshotImageUrl(project, build, snapshot, kind) {
|
|
43
|
+
return `/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${snapshot.id}/${kind}`;
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
export { diffPercent, neighbours, reviewProgress, snapshotImageUrl, snapshotPageUrl };
|
|
47
|
+
|
|
48
|
+
//# sourceMappingURL=build-diff-model.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-diff-model.mjs","names":[],"sources":["../../src/pages/build-diff-model.ts"],"sourcesContent":["import type { Build, Project, Snapshot } from \"@storyshelf/core/schema\";\n\n/** How far a build's review has progressed. */\nexport interface ReviewProgress {\n /** Snapshots a human has to decide on (changed/new, or already decided). */\n reviewable: number;\n /** Reviewable snapshots that are approved or rejected. */\n done: number;\n /** Snapshots still waiting for a decision. */\n pending: number;\n}\n\nconst OPEN = new Set([\"new\", \"changed\"]);\nconst DECIDED = new Set([\"approved\", \"rejected\"]);\n\n/** Count review progress; unchanged snapshots need no decision and are excluded. */\nexport function reviewProgress(snapshots: Snapshot[]): ReviewProgress {\n const pending = snapshots.filter((snap) => OPEN.has(snap.status)).length;\n const done = snapshots.filter((snap) => DECIDED.has(snap.status)).length;\n return { reviewable: pending + done, done, pending };\n}\n\n/** Position of the selected snapshot plus its neighbours in list order. */\nexport interface Neighbours {\n /** 1-based position, or 0 when nothing is selected. */\n position: number;\n total: number;\n prev: Snapshot | null;\n next: Snapshot | null;\n}\n\n/** Previous/next snapshot around the selected one. */\nexport function neighbours(snapshots: Snapshot[], selectedId?: string): Neighbours {\n const index = snapshots.findIndex((snap) => snap.id === selectedId);\n if (index === -1) {\n return { position: 0, total: snapshots.length, prev: null, next: null };\n }\n return {\n position: index + 1,\n total: snapshots.length,\n prev: snapshots[index - 1] ?? null,\n next: snapshots[index + 1] ?? null,\n };\n}\n\n/** Diff size as a percentage string (\"3.1%\"), or null when not computed. */\nexport function diffPercent(snapshot: Pick<Snapshot, \"diffRatio\">): string | null {\n return snapshot.diffRatio === null || snapshot.diffRatio === undefined\n ? null\n : `${(snapshot.diffRatio * 100).toFixed(1)}%`;\n}\n\n/** Review-page URL for one snapshot. */\nexport function snapshotPageUrl(project: Project, build: Build, snapshotId: string): string {\n return `/projects/${project.slug}/builds/${build.id}/diff?snapshot=${snapshotId}`;\n}\n\n/** API URL of a snapshot image (`image`), its diff overlay, or its baseline. */\nexport function snapshotImageUrl(\n project: Project,\n build: Build,\n snapshot: Snapshot,\n kind: \"image\" | \"diff\" | \"baseline\",\n): string {\n return `/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${snapshot.id}/${kind}`;\n}\n"],"mappings":";;;;AAYA,MAAM,uBAAO,IAAI,IAAI,CAAC,OAAO,SAAS,CAAC;AACvC,MAAM,0BAAU,IAAI,IAAI,CAAC,YAAY,UAAU,CAAC;;AAGhD,SAAgB,eAAe,WAAuC;CACpE,MAAM,UAAU,UAAU,QAAQ,SAAS,KAAK,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC;CAClE,MAAM,OAAO,UAAU,QAAQ,SAAS,QAAQ,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC;CAClE,OAAO;EAAE,YAAY,UAAU;EAAM;EAAM;CAAQ;AACrD;;AAYA,SAAgB,WAAW,WAAuB,YAAiC;CACjF,MAAM,QAAQ,UAAU,WAAW,SAAS,KAAK,OAAO,UAAU;CAClE,IAAI,UAAU,IACZ,OAAO;EAAE,UAAU;EAAG,OAAO,UAAU;EAAQ,MAAM;EAAM,MAAM;CAAK;CAExE,OAAO;EACL,UAAU,QAAQ;EAClB,OAAO,UAAU;EACjB,MAAM,UAAU,QAAQ,MAAM;EAC9B,MAAM,UAAU,QAAQ,MAAM;CAChC;AACF;;AAGA,SAAgB,YAAY,UAAsD;CAChF,OAAO,SAAS,cAAc,QAAQ,SAAS,cAAc,KAAA,IACzD,OACA,IAAI,SAAS,YAAY,IAAA,CAAK,QAAQ,CAAC,EAAE;AAC/C;;AAGA,SAAgB,gBAAgB,SAAkB,OAAc,YAA4B;CAC1F,OAAO,aAAa,QAAQ,KAAK,UAAU,MAAM,GAAG,iBAAiB;AACvE;;AAGA,SAAgB,iBACd,SACA,OACA,UACA,MACQ;CACR,OAAO,oBAAoB,QAAQ,KAAK,UAAU,MAAM,GAAG,aAAa,SAAS,GAAG,GAAG;AACzF"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { css } from "../ui/css.mjs";
|
|
5
|
+
import { statusTone } from "../ui/feedback.mjs";
|
|
6
|
+
import { Segmented } from "../ui/segmented.mjs";
|
|
7
|
+
import { Thumbnail } from "../ui/thumbnail.mjs";
|
|
8
|
+
import { filmBody, filmItem, filmItemActive, filmStatus, filmSub, filmTitle, filmstrip, filmstripHead, filmstripList, statusDots } from "../ui/styles/review-layout.mjs";
|
|
9
|
+
import { diffPercent, reviewProgress, snapshotImageUrl, snapshotPageUrl } from "./build-diff-model.mjs";
|
|
10
|
+
import { jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
11
|
+
//#region src/pages/build-diff-nav.tsx
|
|
12
|
+
/**
|
|
13
|
+
* The list filters purely in CSS off `data-filter`, so it works before the
|
|
14
|
+
* review script runs. The selected snapshot always stays visible.
|
|
15
|
+
*/
|
|
16
|
+
const filteredList = css`
|
|
17
|
+
/* filmstrip-filtered */
|
|
18
|
+
${filmstripList}
|
|
19
|
+
&[data-filter="review"]
|
|
20
|
+
[data-status]:not([data-status="new"]):not([data-status="changed"]):not([aria-current="true"]) {
|
|
21
|
+
display: none;
|
|
22
|
+
}
|
|
23
|
+
&[data-filter="done"]
|
|
24
|
+
[data-status]:not([data-status="approved"]):not([data-status="rejected"]):not(
|
|
25
|
+
[aria-current="true"]
|
|
26
|
+
) {
|
|
27
|
+
display: none;
|
|
28
|
+
}
|
|
29
|
+
`;
|
|
30
|
+
function FilmItem(props) {
|
|
31
|
+
const { project, build, snap, active } = props;
|
|
32
|
+
const url = snapshotPageUrl(project, build, snap.id);
|
|
33
|
+
const percent = diffPercent(snap);
|
|
34
|
+
return /* @__PURE__ */ jsxs("a", {
|
|
35
|
+
href: url,
|
|
36
|
+
class: active ? filmItemActive : filmItem,
|
|
37
|
+
"data-snapshot-link": true,
|
|
38
|
+
"data-snapshot-id": snap.id,
|
|
39
|
+
"data-status": snap.status,
|
|
40
|
+
"aria-current": active ? "true" : void 0,
|
|
41
|
+
"hx-get": url,
|
|
42
|
+
"hx-target": "body",
|
|
43
|
+
"hx-push-url": "true",
|
|
44
|
+
children: [/* @__PURE__ */ jsx(Thumbnail, {
|
|
45
|
+
src: snapshotImageUrl(project, build, snap, "image"),
|
|
46
|
+
alt: "",
|
|
47
|
+
placeholder: "…"
|
|
48
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
49
|
+
class: filmBody,
|
|
50
|
+
children: [
|
|
51
|
+
/* @__PURE__ */ jsx("span", {
|
|
52
|
+
class: filmTitle,
|
|
53
|
+
title: `${snap.storyTitle} / ${snap.storyName}`,
|
|
54
|
+
children: snap.storyName
|
|
55
|
+
}),
|
|
56
|
+
/* @__PURE__ */ jsx("span", {
|
|
57
|
+
class: filmSub,
|
|
58
|
+
children: snap.storyTitle
|
|
59
|
+
}),
|
|
60
|
+
/* @__PURE__ */ jsxs("span", {
|
|
61
|
+
class: filmStatus,
|
|
62
|
+
children: [
|
|
63
|
+
/* @__PURE__ */ jsx("span", {
|
|
64
|
+
class: statusDots[statusTone(snap.status)],
|
|
65
|
+
"aria-hidden": "true"
|
|
66
|
+
}),
|
|
67
|
+
snap.status,
|
|
68
|
+
percent ? ` · ${percent}` : ""
|
|
69
|
+
]
|
|
70
|
+
})
|
|
71
|
+
]
|
|
72
|
+
})]
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/** Left filmstrip: filter chips plus a thumbnail per snapshot. */
|
|
76
|
+
function DiffNav(props) {
|
|
77
|
+
const { project, build, snapshots, selectedId } = props;
|
|
78
|
+
const progress = reviewProgress(snapshots);
|
|
79
|
+
const filter = progress.pending > 0 ? "review" : "all";
|
|
80
|
+
return /* @__PURE__ */ jsxs("aside", {
|
|
81
|
+
class: filmstrip,
|
|
82
|
+
"aria-label": "Snapshots",
|
|
83
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
84
|
+
class: filmstripHead,
|
|
85
|
+
children: /* @__PURE__ */ jsx(Segmented, {
|
|
86
|
+
label: "Filter snapshots",
|
|
87
|
+
"data-filter-switch": true,
|
|
88
|
+
items: [
|
|
89
|
+
{
|
|
90
|
+
label: `Needs review ${progress.pending}`,
|
|
91
|
+
value: "review",
|
|
92
|
+
active: filter === "review"
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
label: `All ${snapshots.length}`,
|
|
96
|
+
value: "all",
|
|
97
|
+
active: filter === "all"
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
label: `Done ${progress.done}`,
|
|
101
|
+
value: "done"
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
})
|
|
105
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
106
|
+
"data-diff-nav": true,
|
|
107
|
+
"data-current": selectedId,
|
|
108
|
+
"data-filter": filter,
|
|
109
|
+
class: filteredList,
|
|
110
|
+
children: snapshots.map((snap) => /* @__PURE__ */ jsx(FilmItem, {
|
|
111
|
+
project,
|
|
112
|
+
build,
|
|
113
|
+
snap,
|
|
114
|
+
active: selectedId === snap.id
|
|
115
|
+
}, snap.id))
|
|
116
|
+
})]
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
//#endregion
|
|
120
|
+
export { DiffNav };
|
|
121
|
+
|
|
122
|
+
//# sourceMappingURL=build-diff-nav.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-diff-nav.mjs","names":[],"sources":["../../src/pages/build-diff-nav.tsx"],"sourcesContent":["import type { Build, Project, Snapshot } from \"@storyshelf/core/schema\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport { Segmented, Thumbnail, statusTone } from \"../ui/components.tsx\";\nimport { css } from \"../ui/css.ts\";\nimport {\n filmBody,\n filmItem,\n filmItemActive,\n filmStatus,\n filmSub,\n filmTitle,\n filmstrip,\n filmstripHead,\n filmstripList,\n statusDots,\n} from \"../ui/styles/review-layout.ts\";\nimport {\n diffPercent,\n reviewProgress,\n snapshotImageUrl,\n snapshotPageUrl,\n} from \"./build-diff-model.ts\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\n/**\n * The list filters purely in CSS off `data-filter`, so it works before the\n * review script runs. The selected snapshot always stays visible.\n */\nconst filteredList = css`\n /* filmstrip-filtered */\n ${filmstripList}\n &[data-filter=\"review\"]\n [data-status]:not([data-status=\"new\"]):not([data-status=\"changed\"]):not([aria-current=\"true\"]) {\n display: none;\n }\n &[data-filter=\"done\"]\n [data-status]:not([data-status=\"approved\"]):not([data-status=\"rejected\"]):not(\n [aria-current=\"true\"]\n ) {\n display: none;\n }\n`;\n\n/** Snapshot navigator props for the diff review page. */\nexport interface DiffNavProps {\n project: Project;\n build: Build;\n snapshots: Snapshot[];\n selectedId?: string;\n}\n\nfunction FilmItem(props: {\n project: Project;\n build: Build;\n snap: Snapshot;\n active: boolean;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, build, snap, active } = props;\n const url = snapshotPageUrl(project, build, snap.id);\n const percent = diffPercent(snap);\n return (\n <a\n href={url}\n class={active ? filmItemActive : filmItem}\n data-snapshot-link\n data-snapshot-id={snap.id}\n data-status={snap.status}\n aria-current={active ? \"true\" : undefined}\n hx-get={url}\n hx-target=\"body\"\n hx-push-url=\"true\"\n >\n <Thumbnail src={snapshotImageUrl(project, build, snap, \"image\")} alt=\"\" placeholder=\"…\" />\n <span class={filmBody}>\n <span class={filmTitle} title={`${snap.storyTitle} / ${snap.storyName}`}>\n {snap.storyName}\n </span>\n <span class={filmSub}>{snap.storyTitle}</span>\n <span class={filmStatus}>\n <span class={statusDots[statusTone(snap.status)]} aria-hidden=\"true\" />\n {snap.status}\n {percent ? ` · ${percent}` : \"\"}\n </span>\n </span>\n </a>\n );\n}\n\n/** Left filmstrip: filter chips plus a thumbnail per snapshot. */\nexport function DiffNav(props: DiffNavProps): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, build, snapshots, selectedId } = props;\n const progress = reviewProgress(snapshots);\n const filter = progress.pending > 0 ? \"review\" : \"all\";\n return (\n <aside class={filmstrip} aria-label=\"Snapshots\">\n <div class={filmstripHead}>\n <Segmented\n label=\"Filter snapshots\"\n data-filter-switch\n items={[\n {\n label: `Needs review ${progress.pending}`,\n value: \"review\",\n active: filter === \"review\",\n },\n { label: `All ${snapshots.length}`, value: \"all\", active: filter === \"all\" },\n { label: `Done ${progress.done}`, value: \"done\" },\n ]}\n />\n </div>\n <div data-diff-nav data-current={selectedId} data-filter={filter} class={filteredList}>\n {snapshots.map((snap): HtmlEscapedString | Promise<HtmlEscapedString> => (\n <FilmItem\n key={snap.id}\n project={project}\n build={build}\n snap={snap}\n active={selectedId === snap.id}\n />\n ))}\n </div>\n </aside>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;AA6BA,MAAM,eAAe,GAAG;;IAEpB,cAAc;;;;;;;;;;;;AAqBlB,SAAS,SAAS,OAKiC;CACjD,MAAM,EAAE,SAAS,OAAO,MAAM,WAAW;CACzC,MAAM,MAAM,gBAAgB,SAAS,OAAO,KAAK,EAAE;CACnD,MAAM,UAAU,YAAY,IAAI;CAChC,OACE,qBAAC,KAAD;EACE,MAAM;EACN,OAAO,SAAS,iBAAiB;EACjC,sBAAA;EACA,oBAAkB,KAAK;EACvB,eAAa,KAAK;EAClB,gBAAc,SAAS,SAAS,KAAA;EAChC,UAAQ;EACR,aAAU;EACV,eAAY;EATd,UAAA,CAWE,oBAAC,WAAD;GAAW,KAAK,iBAAiB,SAAS,OAAO,MAAM,OAAO;GAAG,KAAI;GAAG,aAAY;EAAK,CAAA,GACzF,qBAAC,QAAD;GAAM,OAAO;GAAb,UAAA;IACE,oBAAC,QAAD;KAAM,OAAO;KAAW,OAAO,GAAG,KAAK,WAAW,KAAK,KAAK;KACzD,UAAA,KAAK;IACF,CAAA;IACN,oBAAC,QAAD;KAAM,OAAO;KAAU,UAAA,KAAK;IAAiB,CAAA;IAC7C,qBAAC,QAAD;KAAM,OAAO;KAAb,UAAA;MACE,oBAAC,QAAD;OAAM,OAAO,WAAW,WAAW,KAAK,MAAM;OAAI,eAAY;MAAQ,CAAA;MACrE,KAAK;MACL,UAAU,MAAM,YAAY;KACzB;;GACF;EACL,CAAA,CAAA;;AAEP;;AAGA,SAAgB,QAAQ,OAAqE;CAC3F,MAAM,EAAE,SAAS,OAAO,WAAW,eAAe;CAClD,MAAM,WAAW,eAAe,SAAS;CACzC,MAAM,SAAS,SAAS,UAAU,IAAI,WAAW;CACjD,OACE,qBAAC,SAAD;EAAO,OAAO;EAAW,cAAW;EAApC,UAAA,CACE,oBAAC,OAAD;GAAK,OAAO;GACV,UAAA,oBAAC,WAAD;IACE,OAAM;IACN,sBAAA;IACA,OAAO;KACL;MACE,OAAO,gBAAgB,SAAS;MAChC,OAAO;MACP,QAAQ,WAAW;KACrB;KACA;MAAE,OAAO,OAAO,UAAU;MAAU,OAAO;MAAO,QAAQ,WAAW;KAAM;KAC3E;MAAE,OAAO,QAAQ,SAAS;MAAQ,OAAO;KAAO;IAClD;GACD,CAAA;EACE,CAAA,GACL,oBAAC,OAAD;GAAK,iBAAA;GAAc,gBAAc;GAAY,eAAa;GAAQ,OAAO;GACtE,UAAA,UAAU,KAAK,SACd,oBAAC,UAAD;IAEW;IACF;IACD;IACN,QAAQ,eAAe,KAAK;GAC7B,GALM,KAAK,EAKX,CACF;EACE,CAAA,CACA;;AAEX"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { Button } from "../ui/buttons.mjs";
|
|
5
|
+
import { Kbd } from "../ui/kbd.mjs";
|
|
6
|
+
import { shortcutsDialog } from "../ui/styles/review-layout.mjs";
|
|
7
|
+
import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
8
|
+
//#region src/pages/build-diff-shortcuts.tsx
|
|
9
|
+
const SHORTCUTS = [
|
|
10
|
+
{
|
|
11
|
+
keys: ["j", "→"],
|
|
12
|
+
action: "Next snapshot"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
keys: ["k", "←"],
|
|
16
|
+
action: "Previous snapshot"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
keys: ["a"],
|
|
20
|
+
action: "Approve"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
keys: ["r"],
|
|
24
|
+
action: "Reject"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
keys: [
|
|
28
|
+
"1",
|
|
29
|
+
"–",
|
|
30
|
+
"5"
|
|
31
|
+
],
|
|
32
|
+
action: "Side by side, swipe, onion skin, diff, flip"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
keys: ["f"],
|
|
36
|
+
action: "Cycle zoom (fit, 100%, 200%, 400%)"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
keys: ["+", "–"],
|
|
40
|
+
action: "Zoom in / out"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
keys: ["t"],
|
|
44
|
+
action: "Flip baseline / current"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
keys: ["?"],
|
|
48
|
+
action: "Show this help"
|
|
49
|
+
}
|
|
50
|
+
];
|
|
51
|
+
/** Native dialog listing the review keyboard shortcuts (opened with `?`). */
|
|
52
|
+
function ShortcutsDialog() {
|
|
53
|
+
return /* @__PURE__ */ jsxs("dialog", {
|
|
54
|
+
class: shortcutsDialog,
|
|
55
|
+
"data-shortcuts": true,
|
|
56
|
+
"aria-labelledby": "shortcuts-title",
|
|
57
|
+
children: [
|
|
58
|
+
/* @__PURE__ */ jsx("h2", {
|
|
59
|
+
id: "shortcuts-title",
|
|
60
|
+
children: "Keyboard shortcuts"
|
|
61
|
+
}),
|
|
62
|
+
/* @__PURE__ */ jsx("dl", { children: SHORTCUTS.map((item) => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("dt", { children: /* @__PURE__ */ jsx(Kbd, { keys: item.keys }) }), /* @__PURE__ */ jsx("dd", { children: item.action })] })) }),
|
|
63
|
+
/* @__PURE__ */ jsx("form", {
|
|
64
|
+
method: "dialog",
|
|
65
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
66
|
+
variant: "secondary",
|
|
67
|
+
size: "sm",
|
|
68
|
+
type: "submit",
|
|
69
|
+
children: "Close"
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
]
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
76
|
+
export { ShortcutsDialog };
|
|
77
|
+
|
|
78
|
+
//# sourceMappingURL=build-diff-shortcuts.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-diff-shortcuts.mjs","names":[],"sources":["../../src/pages/build-diff-shortcuts.tsx"],"sourcesContent":["import type { HtmlEscapedString } from \"hono/utils/html\";\nimport { Button, Kbd } from \"../ui/components.tsx\";\nimport { shortcutsDialog } from \"../ui/styles/review-layout.ts\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\nconst SHORTCUTS: { keys: string[]; action: string }[] = [\n { keys: [\"j\", \"→\"], action: \"Next snapshot\" },\n { keys: [\"k\", \"←\"], action: \"Previous snapshot\" },\n { keys: [\"a\"], action: \"Approve\" },\n { keys: [\"r\"], action: \"Reject\" },\n { keys: [\"1\", \"–\", \"5\"], action: \"Side by side, swipe, onion skin, diff, flip\" },\n { keys: [\"f\"], action: \"Cycle zoom (fit, 100%, 200%, 400%)\" },\n { keys: [\"+\", \"–\"], action: \"Zoom in / out\" },\n { keys: [\"t\"], action: \"Flip baseline / current\" },\n { keys: [\"?\"], action: \"Show this help\" },\n];\n\n/** Native dialog listing the review keyboard shortcuts (opened with `?`). */\nexport function ShortcutsDialog(): HtmlEscapedString | Promise<HtmlEscapedString> {\n return (\n <dialog class={shortcutsDialog} data-shortcuts aria-labelledby=\"shortcuts-title\">\n <h2 id=\"shortcuts-title\">Keyboard shortcuts</h2>\n <dl>\n {SHORTCUTS.map((item) => (\n <>\n <dt>\n <Kbd keys={item.keys} />\n </dt>\n <dd>{item.action}</dd>\n </>\n ))}\n </dl>\n <form method=\"dialog\">\n <Button variant=\"secondary\" size=\"sm\" type=\"submit\">\n Close\n </Button>\n </form>\n </dialog>\n );\n}\n"],"mappings":";;;;;;;;AAMA,MAAM,YAAkD;CACtD;EAAE,MAAM,CAAC,KAAK,GAAG;EAAG,QAAQ;CAAgB;CAC5C;EAAE,MAAM,CAAC,KAAK,GAAG;EAAG,QAAQ;CAAoB;CAChD;EAAE,MAAM,CAAC,GAAG;EAAG,QAAQ;CAAU;CACjC;EAAE,MAAM,CAAC,GAAG;EAAG,QAAQ;CAAS;CAChC;EAAE,MAAM;GAAC;GAAK;GAAK;EAAG;EAAG,QAAQ;CAA8C;CAC/E;EAAE,MAAM,CAAC,GAAG;EAAG,QAAQ;CAAqC;CAC5D;EAAE,MAAM,CAAC,KAAK,GAAG;EAAG,QAAQ;CAAgB;CAC5C;EAAE,MAAM,CAAC,GAAG;EAAG,QAAQ;CAA0B;CACjD;EAAE,MAAM,CAAC,GAAG;EAAG,QAAQ;CAAiB;AAC1C;;AAGA,SAAgB,kBAAkE;CAChF,OACE,qBAAC,UAAD;EAAQ,OAAO;EAAiB,kBAAA;EAAe,mBAAgB;EAA/D,UAAA;GACE,oBAAC,MAAD;IAAI,IAAG;IAAkB,UAAA;GAAsB,CAAA;GAC/C,oBAAC,MAAD,EAAA,UACG,UAAU,KAAK,SACd,qBAAA,UAAA,EAAA,UAAA,CACE,oBAAC,MAAD,EAAA,UACE,oBAAC,KAAD,EAAK,MAAM,KAAK,KAAO,CAAA,EACrB,CAAA,GACJ,oBAAC,MAAD,EAAA,UAAK,KAAK,OAAW,CAAA,CACrB,EAAA,CAAA,CACH,EACC,CAAA;GACJ,oBAAC,QAAD;IAAM,QAAO;IACX,UAAA,oBAAC,QAAD;KAAQ,SAAQ;KAAY,MAAK;KAAK,MAAK;KAAS,UAAA;IAE5C,CAAA;GACJ,CAAA;EACA;;AAEZ"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { Button } from "../ui/buttons.mjs";
|
|
5
|
+
import { Alert, Meta } from "../ui/feedback.mjs";
|
|
6
|
+
import { HStack, VStack } from "../ui/stacks.mjs";
|
|
7
|
+
import { jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
8
|
+
//#region src/pages/build-diff-stale.tsx
|
|
9
|
+
/**
|
|
10
|
+
* Warns that undecided snapshots were compared with a baseline that has since changed.
|
|
11
|
+
* Builds are never recaptured automatically, so the reviewer chooses how to refresh.
|
|
12
|
+
*/
|
|
13
|
+
function StaleBaselineNotice(props) {
|
|
14
|
+
const { project, build, staleCount, canReview } = props;
|
|
15
|
+
const base = `/api/v1/projects/${project.slug}/builds/${build.id}`;
|
|
16
|
+
return /* @__PURE__ */ jsx(Alert, {
|
|
17
|
+
tone: "warning",
|
|
18
|
+
title: "Baseline changed since this build was captured",
|
|
19
|
+
children: /* @__PURE__ */ jsxs(VStack, { children: [
|
|
20
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
21
|
+
staleCount,
|
|
22
|
+
" undecided ",
|
|
23
|
+
staleCount === 1 ? "snapshot was" : "snapshots were",
|
|
24
|
+
" compared against a baseline that has since changed, so ",
|
|
25
|
+
staleCount === 1 ? "its" : "their",
|
|
26
|
+
" diff may be out of date. Builds are not recaptured automatically."
|
|
27
|
+
] }),
|
|
28
|
+
canReview ? /* @__PURE__ */ jsxs(HStack, { children: [/* @__PURE__ */ jsx("form", {
|
|
29
|
+
method: "post",
|
|
30
|
+
action: `${base}/rediff`,
|
|
31
|
+
"hx-post": `${base}/rediff`,
|
|
32
|
+
"hx-target": "body",
|
|
33
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
34
|
+
variant: "primary",
|
|
35
|
+
size: "sm",
|
|
36
|
+
type: "submit",
|
|
37
|
+
children: "Re-diff against current baselines"
|
|
38
|
+
})
|
|
39
|
+
}), /* @__PURE__ */ jsx("form", {
|
|
40
|
+
method: "post",
|
|
41
|
+
action: `${base}/retry`,
|
|
42
|
+
"hx-post": `${base}/retry`,
|
|
43
|
+
"hx-target": "body",
|
|
44
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
45
|
+
variant: "secondary",
|
|
46
|
+
size: "sm",
|
|
47
|
+
type: "submit",
|
|
48
|
+
children: "Retry capture"
|
|
49
|
+
})
|
|
50
|
+
})] }) : null,
|
|
51
|
+
/* @__PURE__ */ jsx(Meta, { children: "Re-diff reuses the stored screenshots and is quick. Retry capture re-renders every story." })
|
|
52
|
+
] })
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/** Explains why a single snapshot's diff can no longer be trusted. */
|
|
56
|
+
function StaleSnapshotNotice(props) {
|
|
57
|
+
return /* @__PURE__ */ jsxs(Alert, {
|
|
58
|
+
tone: "warning",
|
|
59
|
+
title: "This diff is out of date",
|
|
60
|
+
children: [
|
|
61
|
+
props.status === "removed" ? "The baseline it was compared against no longer exists." : "The baseline changed after this diff was computed.",
|
|
62
|
+
" ",
|
|
63
|
+
"Re-diff the build before approving, or approve anyway to replace the current baseline with this screenshot."
|
|
64
|
+
]
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
export { StaleBaselineNotice, StaleSnapshotNotice };
|
|
69
|
+
|
|
70
|
+
//# sourceMappingURL=build-diff-stale.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-diff-stale.mjs","names":[],"sources":["../../src/pages/build-diff-stale.tsx"],"sourcesContent":["import type { Build } from \"@storyshelf/core/schema\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport { Alert, Button, HStack, Meta, VStack } from \"../ui/components.tsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\n/** Props for the build-level notice about diffs computed against an outdated baseline. */\nexport interface StaleBaselineNoticeProps {\n project: Project;\n build: Build;\n staleCount: number;\n canReview: boolean;\n}\n\n/**\n * Warns that undecided snapshots were compared with a baseline that has since changed.\n * Builds are never recaptured automatically, so the reviewer chooses how to refresh.\n */\nexport function StaleBaselineNotice(\n props: StaleBaselineNoticeProps,\n): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, build, staleCount, canReview } = props;\n const base = `/api/v1/projects/${project.slug}/builds/${build.id}`;\n return (\n <Alert tone=\"warning\" title=\"Baseline changed since this build was captured\">\n <VStack>\n <span>\n {staleCount} undecided {staleCount === 1 ? \"snapshot was\" : \"snapshots were\"} compared\n against a baseline that has since changed, so {staleCount === 1 ? \"its\" : \"their\"} diff\n may be out of date. Builds are not recaptured automatically.\n </span>\n {canReview ? (\n <HStack>\n <form\n method=\"post\"\n action={`${base}/rediff`}\n hx-post={`${base}/rediff`}\n hx-target=\"body\"\n >\n <Button variant=\"primary\" size=\"sm\" type=\"submit\">\n Re-diff against current baselines\n </Button>\n </form>\n <form method=\"post\" action={`${base}/retry`} hx-post={`${base}/retry`} hx-target=\"body\">\n <Button variant=\"secondary\" size=\"sm\" type=\"submit\">\n Retry capture\n </Button>\n </form>\n </HStack>\n ) : null}\n <Meta>\n Re-diff reuses the stored screenshots and is quick. Retry capture re-renders every story.\n </Meta>\n </VStack>\n </Alert>\n );\n}\n\n/** Props for the per-snapshot notice shown above the three-up viewer. */\nexport interface StaleSnapshotNoticeProps {\n status: \"stale\" | \"removed\";\n}\n\n/** Explains why a single snapshot's diff can no longer be trusted. */\nexport function StaleSnapshotNotice(\n props: StaleSnapshotNoticeProps,\n): HtmlEscapedString | Promise<HtmlEscapedString> {\n return (\n <Alert tone=\"warning\" title=\"This diff is out of date\">\n {props.status === \"removed\"\n ? \"The baseline it was compared against no longer exists.\"\n : \"The baseline changed after this diff was computed.\"}{\" \"}\n Re-diff the build before approving, or approve anyway to replace the current baseline with\n this screenshot.\n </Alert>\n );\n}\n"],"mappings":";;;;;;;;;;;;AAmBA,SAAgB,oBACd,OACgD;CAChD,MAAM,EAAE,SAAS,OAAO,YAAY,cAAc;CAClD,MAAM,OAAO,oBAAoB,QAAQ,KAAK,UAAU,MAAM;CAC9D,OACE,oBAAC,OAAD;EAAO,MAAK;EAAU,OAAM;EAC1B,UAAA,qBAAC,QAAD,EAAA,UAAA;GACE,qBAAC,QAAD,EAAA,UAAA;IACG;IAAW;IAAY,eAAe,IAAI,iBAAiB;IAAiB;IAC9B,eAAe,IAAI,QAAQ;IAAQ;GAE9E,EAAA,CAAA;GACL,YACC,qBAAC,QAAD,EAAA,UAAA,CACE,oBAAC,QAAD;IACE,QAAO;IACP,QAAQ,GAAG,KAAK;IAChB,WAAS,GAAG,KAAK;IACjB,aAAU;IAEV,UAAA,oBAAC,QAAD;KAAQ,SAAQ;KAAU,MAAK;KAAK,MAAK;KAAS,UAAA;IAE1C,CAAA;GACJ,CAAA,GACN,oBAAC,QAAD;IAAM,QAAO;IAAO,QAAQ,GAAG,KAAK;IAAS,WAAS,GAAG,KAAK;IAAS,aAAU;IAC/E,UAAA,oBAAC,QAAD;KAAQ,SAAQ;KAAY,MAAK;KAAK,MAAK;KAAS,UAAA;IAE5C,CAAA;GACJ,CAAA,CACA,EAAA,CAAA,IACN;GACJ,oBAAC,MAAD,EAAA,UAAM,4FAEA,CAAA;EACA,EAAA,CAAA;CACH,CAAA;AAEX;;AAQA,SAAgB,oBACd,OACgD;CAChD,OACE,qBAAC,OAAD;EAAO,MAAK;EAAU,OAAM;EAA5B,UAAA;GACG,MAAM,WAAW,YACd,2DACA;GAAsD;GAAI;EAGzD;;AAEX"}
|