@storyshelf/app 0.6.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -4
- package/dist/app-types.d.mts +1 -3
- package/dist/asset-manifest.mjs +13 -5
- package/dist/asset-manifest.mjs.map +1 -1
- package/dist/assets/{htmx.min.js → htmx.mjs} +10 -1
- package/dist/assets/htmx.mjs.map +1 -0
- package/dist/assets/mark.mjs +10 -0
- package/dist/assets/mark.mjs.map +1 -0
- package/dist/boot-tasks.mjs +80 -0
- package/dist/boot-tasks.mjs.map +1 -0
- package/dist/build-state-flags.mjs +18 -0
- package/dist/build-state-flags.mjs.map +1 -0
- package/dist/build-state-setup.mjs +75 -0
- package/dist/build-state-setup.mjs.map +1 -0
- package/dist/capture-alerts.mjs +46 -0
- package/dist/capture-alerts.mjs.map +1 -0
- package/dist/capture-process.mjs +59 -0
- package/dist/capture-process.mjs.map +1 -0
- package/dist/capture-setup.mjs +95 -117
- package/dist/capture-setup.mjs.map +1 -1
- package/dist/connections.mjs +34 -0
- package/dist/connections.mjs.map +1 -0
- package/dist/consumer-setup.mjs +58 -0
- package/dist/consumer-setup.mjs.map +1 -0
- package/dist/git-status.mjs +46 -0
- package/dist/git-status.mjs.map +1 -0
- package/dist/hidden-data.mjs +24 -0
- package/dist/hidden-data.mjs.map +1 -0
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +203 -125
- package/dist/index.mjs.map +1 -1
- package/dist/insights/budget.mjs +49 -0
- package/dist/insights/budget.mjs.map +1 -0
- package/dist/insights/deps.mjs +21 -0
- package/dist/insights/deps.mjs.map +1 -0
- package/dist/insights/errors.mjs +22 -0
- package/dist/insights/errors.mjs.map +1 -0
- package/dist/insights/health-panel.mjs +40 -0
- package/dist/insights/health-panel.mjs.map +1 -0
- package/dist/insights/job.mjs +103 -0
- package/dist/insights/job.mjs.map +1 -0
- package/dist/insights/load-health.mjs +59 -0
- package/dist/insights/load-health.mjs.map +1 -0
- package/dist/insights/load-triage.mjs +98 -0
- package/dist/insights/load-triage.mjs.map +1 -0
- package/dist/insights/panel.mjs +28 -0
- package/dist/insights/panel.mjs.map +1 -0
- package/dist/insights/request.mjs +113 -0
- package/dist/insights/request.mjs.map +1 -0
- package/dist/insights/stale.mjs +21 -0
- package/dist/insights/stale.mjs.map +1 -0
- package/dist/insights/usage-report.mjs +60 -0
- package/dist/insights/usage-report.mjs.map +1 -0
- package/dist/insights/view.mjs +48 -0
- package/dist/insights/view.mjs.map +1 -0
- package/dist/job-canceller.mjs +14 -0
- package/dist/job-canceller.mjs.map +1 -0
- package/dist/job-queue.mjs +51 -0
- package/dist/job-queue.mjs.map +1 -0
- package/dist/jobs-setup.mjs +132 -0
- package/dist/jobs-setup.mjs.map +1 -0
- package/dist/lifecycle.mjs +91 -19
- package/dist/lifecycle.mjs.map +1 -1
- package/dist/middleware/auth-gate.mjs +4 -2
- package/dist/middleware/auth-gate.mjs.map +1 -1
- package/dist/middleware/rate-limit.mjs +41 -33
- package/dist/middleware/rate-limit.mjs.map +1 -1
- package/dist/middleware/request-log.mjs +3 -2
- package/dist/middleware/request-log.mjs.map +1 -1
- package/dist/middleware/require-review.mjs +51 -0
- package/dist/middleware/require-review.mjs.map +1 -0
- package/dist/middleware/require-testing.mjs +41 -0
- package/dist/middleware/require-testing.mjs.map +1 -0
- package/dist/middleware/setup-gate.mjs.map +1 -1
- package/dist/middleware/store-scope.mjs +12 -2
- package/dist/middleware/store-scope.mjs.map +1 -1
- package/dist/notifications-pause.mjs +33 -0
- package/dist/notifications-pause.mjs.map +1 -0
- package/dist/notify.d.mts +2 -1
- package/dist/notify.mjs +78 -39
- package/dist/notify.mjs.map +1 -1
- package/dist/pages/a11y-violations.mjs +116 -0
- package/dist/pages/a11y-violations.mjs.map +1 -0
- package/dist/pages/admin-ai.mjs +78 -0
- package/dist/pages/admin-ai.mjs.map +1 -0
- package/dist/pages/admin-system-origin.mjs +123 -0
- package/dist/pages/admin-system-origin.mjs.map +1 -0
- package/dist/pages/admin-system-targets.mjs +53 -0
- package/dist/pages/admin-system-targets.mjs.map +1 -0
- package/dist/pages/admin-system.mjs +63 -13
- package/dist/pages/admin-system.mjs.map +1 -1
- package/dist/pages/build-detail.mjs +104 -76
- package/dist/pages/build-detail.mjs.map +1 -1
- package/dist/pages/build-diff-actions.mjs +87 -33
- package/dist/pages/build-diff-actions.mjs.map +1 -1
- package/dist/pages/build-diff-comments.mjs +16 -14
- package/dist/pages/build-diff-comments.mjs.map +1 -1
- package/dist/pages/build-diff-environment.mjs +82 -0
- package/dist/pages/build-diff-environment.mjs.map +1 -0
- package/dist/pages/build-diff-header.mjs +22 -4
- package/dist/pages/build-diff-header.mjs.map +1 -1
- package/dist/pages/build-diff-insight.mjs +113 -0
- package/dist/pages/build-diff-insight.mjs.map +1 -0
- package/dist/pages/build-diff-model.mjs +55 -10
- package/dist/pages/build-diff-model.mjs.map +1 -1
- package/dist/pages/build-diff-nav.mjs +28 -12
- package/dist/pages/build-diff-nav.mjs.map +1 -1
- package/dist/pages/build-diff-shortcuts.mjs +5 -1
- package/dist/pages/build-diff-shortcuts.mjs.map +1 -1
- package/dist/pages/build-diff-usage.mjs +63 -0
- package/dist/pages/build-diff-usage.mjs.map +1 -0
- package/dist/pages/build-diff-viewer.mjs +62 -5
- package/dist/pages/build-diff-viewer.mjs.map +1 -1
- package/dist/pages/build-diff.mjs +34 -13
- package/dist/pages/build-diff.mjs.map +1 -1
- package/dist/pages/build-tests.mjs +74 -0
- package/dist/pages/build-tests.mjs.map +1 -0
- package/dist/pages/component-detail.mjs +249 -0
- package/dist/pages/component-detail.mjs.map +1 -0
- package/dist/pages/component-stories.mjs +168 -0
- package/dist/pages/component-stories.mjs.map +1 -0
- package/dist/pages/compute-jobs.mjs +13 -26
- package/dist/pages/compute-jobs.mjs.map +1 -1
- package/dist/pages/design-links.mjs +182 -0
- package/dist/pages/design-links.mjs.map +1 -0
- package/dist/pages/environment-change.mjs +53 -0
- package/dist/pages/environment-change.mjs.map +1 -0
- package/dist/pages/health-panel.mjs +126 -0
- package/dist/pages/health-panel.mjs.map +1 -0
- package/dist/pages/library-grid.mjs +105 -0
- package/dist/pages/library-grid.mjs.map +1 -0
- package/dist/pages/library-model.mjs +47 -0
- package/dist/pages/library-model.mjs.map +1 -0
- package/dist/pages/library-snapshots.mjs +130 -0
- package/dist/pages/library-snapshots.mjs.map +1 -0
- package/dist/pages/library.mjs +112 -218
- package/dist/pages/library.mjs.map +1 -1
- package/dist/pages/notifications-paused.mjs +22 -0
- package/dist/pages/notifications-paused.mjs.map +1 -0
- package/dist/pages/profile.mjs +53 -48
- package/dist/pages/profile.mjs.map +1 -1
- package/dist/pages/project-builds-table.mjs +2 -2
- package/dist/pages/project-builds-table.mjs.map +1 -1
- package/dist/pages/project-builds.mjs +51 -21
- package/dist/pages/project-builds.mjs.map +1 -1
- package/dist/pages/project-settings.mjs +51 -12
- package/dist/pages/project-settings.mjs.map +1 -1
- package/dist/pages/projects-card.mjs +14 -1
- package/dist/pages/projects-card.mjs.map +1 -1
- package/dist/pages/projects.mjs +6 -2
- package/dist/pages/projects.mjs.map +1 -1
- package/dist/pages/review-changeset.mjs +116 -0
- package/dist/pages/review-changeset.mjs.map +1 -0
- package/dist/pages/review-comments.mjs +103 -0
- package/dist/pages/review-comments.mjs.map +1 -0
- package/dist/pages/review-verdict.mjs +102 -0
- package/dist/pages/review-verdict.mjs.map +1 -0
- package/dist/pages/settings-ai.mjs +69 -0
- package/dist/pages/settings-ai.mjs.map +1 -0
- package/dist/pages/settings-capture.mjs +124 -0
- package/dist/pages/settings-capture.mjs.map +1 -0
- package/dist/pages/settings-design-credential.mjs +99 -0
- package/dist/pages/settings-design-credential.mjs.map +1 -0
- package/dist/pages/settings-design-links.mjs +179 -0
- package/dist/pages/settings-design-links.mjs.map +1 -0
- package/dist/pages/settings-design.mjs +48 -0
- package/dist/pages/settings-design.mjs.map +1 -0
- package/dist/pages/settings-general.mjs +46 -21
- package/dist/pages/settings-general.mjs.map +1 -1
- package/dist/pages/settings-labels.mjs +43 -2
- package/dist/pages/settings-labels.mjs.map +1 -1
- package/dist/pages/settings-links.mjs +112 -0
- package/dist/pages/settings-links.mjs.map +1 -0
- package/dist/pages/settings-notifications-git.mjs +128 -0
- package/dist/pages/settings-notifications-git.mjs.map +1 -0
- package/dist/pages/settings-notifications.mjs +79 -42
- package/dist/pages/settings-notifications.mjs.map +1 -1
- package/dist/pages/settings-tests.mjs +33 -2
- package/dist/pages/settings-tests.mjs.map +1 -1
- package/dist/pages/target-choices.mjs +39 -0
- package/dist/pages/target-choices.mjs.map +1 -0
- package/dist/publish-setup.mjs +89 -0
- package/dist/publish-setup.mjs.map +1 -0
- package/dist/retention-timer.mjs +2 -1
- package/dist/retention-timer.mjs.map +1 -1
- package/dist/review-events.mjs +48 -0
- package/dist/review-events.mjs.map +1 -0
- package/dist/routers/admin-pages.mjs +45 -3
- package/dist/routers/admin-pages.mjs.map +1 -1
- package/dist/routers/admin-public-origin.mjs +85 -0
- package/dist/routers/admin-public-origin.mjs.map +1 -0
- package/dist/routers/assets.mjs +9 -2
- package/dist/routers/assets.mjs.map +1 -1
- package/dist/routers/auth.mjs +1 -1
- package/dist/routers/baseline-guard.mjs +2 -2
- package/dist/routers/baseline-guard.mjs.map +1 -1
- package/dist/routers/baselines.handlers.mjs +64 -0
- package/dist/routers/baselines.handlers.mjs.map +1 -0
- package/dist/routers/baselines.mjs +125 -0
- package/dist/routers/baselines.mjs.map +1 -0
- package/dist/routers/build-cancel.mjs +68 -0
- package/dist/routers/build-cancel.mjs.map +1 -0
- package/dist/routers/build-changeset.mjs +70 -0
- package/dist/routers/build-changeset.mjs.map +1 -0
- package/dist/routers/build-reviews.handlers.mjs +86 -0
- package/dist/routers/build-reviews.handlers.mjs.map +1 -0
- package/dist/routers/build-reviews.mjs +74 -0
- package/dist/routers/build-reviews.mjs.map +1 -0
- package/dist/routers/builds.handlers.mjs +46 -55
- package/dist/routers/builds.handlers.mjs.map +1 -1
- package/dist/routers/builds.mjs +71 -17
- package/dist/routers/builds.mjs.map +1 -1
- package/dist/routers/capture-config.mjs +35 -0
- package/dist/routers/capture-config.mjs.map +1 -0
- package/dist/routers/comments.mjs +41 -15
- package/dist/routers/comments.mjs.map +1 -1
- package/dist/routers/component-catalog.handlers.mjs +71 -0
- package/dist/routers/component-catalog.handlers.mjs.map +1 -0
- package/dist/routers/component.handlers.mjs +121 -0
- package/dist/routers/component.handlers.mjs.map +1 -0
- package/dist/routers/connections.mjs +118 -0
- package/dist/routers/connections.mjs.map +1 -0
- package/dist/routers/design-connect.handlers.mjs +44 -0
- package/dist/routers/design-connect.handlers.mjs.map +1 -0
- package/dist/routers/design.handlers.mjs +114 -0
- package/dist/routers/design.handlers.mjs.map +1 -0
- package/dist/routers/health-report.mjs +2 -3
- package/dist/routers/health-report.mjs.map +1 -1
- package/dist/routers/health.mjs +4 -3
- package/dist/routers/health.mjs.map +1 -1
- package/dist/routers/helpers.mjs +19 -3
- package/dist/routers/helpers.mjs.map +1 -1
- package/dist/routers/insights-ui.mjs +64 -0
- package/dist/routers/insights-ui.mjs.map +1 -0
- package/dist/routers/insights.handlers.mjs +45 -0
- package/dist/routers/insights.handlers.mjs.map +1 -0
- package/dist/routers/insights.mjs +189 -0
- package/dist/routers/insights.mjs.map +1 -0
- package/dist/routers/links.handlers.mjs +80 -0
- package/dist/routers/links.handlers.mjs.map +1 -0
- package/dist/routers/links.mjs +139 -0
- package/dist/routers/links.mjs.map +1 -0
- package/dist/routers/media.mjs +18 -4
- package/dist/routers/media.mjs.map +1 -1
- package/dist/routers/members.mjs +2 -1
- package/dist/routers/members.mjs.map +1 -1
- package/dist/routers/notification-channels.mjs +17 -23
- package/dist/routers/notification-channels.mjs.map +1 -1
- package/dist/routers/oauth-state.mjs +45 -0
- package/dist/routers/oauth-state.mjs.map +1 -0
- package/dist/routers/preview-base.mjs +38 -0
- package/dist/routers/preview-base.mjs.map +1 -0
- package/dist/routers/preview-token.mjs +51 -0
- package/dist/routers/preview-token.mjs.map +1 -0
- package/dist/routers/profile.mjs +3 -1
- package/dist/routers/profile.mjs.map +1 -1
- package/dist/routers/projects.mjs +17 -1
- package/dist/routers/projects.mjs.map +1 -1
- package/dist/routers/publish-upload.mjs +78 -0
- package/dist/routers/publish-upload.mjs.map +1 -0
- package/dist/routers/published-access.mjs +52 -0
- package/dist/routers/published-access.mjs.map +1 -0
- package/dist/routers/published-files.mjs +52 -0
- package/dist/routers/published-files.mjs.map +1 -0
- package/dist/routers/review-forms.mjs +72 -0
- package/dist/routers/review-forms.mjs.map +1 -0
- package/dist/routers/review-section.handlers.mjs +85 -0
- package/dist/routers/review-section.handlers.mjs.map +1 -0
- package/dist/routers/review-switch-off.mjs +29 -0
- package/dist/routers/review-switch-off.mjs.map +1 -0
- package/dist/routers/schemas.mjs +140 -22
- package/dist/routers/schemas.mjs.map +1 -1
- package/dist/routers/scoped-models.mjs +22 -0
- package/dist/routers/scoped-models.mjs.map +1 -0
- package/dist/routers/settings-ai.mjs +35 -0
- package/dist/routers/settings-ai.mjs.map +1 -0
- package/dist/routers/settings-capture.mjs +77 -0
- package/dist/routers/settings-capture.mjs.map +1 -0
- package/dist/routers/settings-design.mjs +103 -0
- package/dist/routers/settings-design.mjs.map +1 -0
- package/dist/routers/settings-general.mjs +55 -10
- package/dist/routers/settings-general.mjs.map +1 -1
- package/dist/routers/settings-git-channel.mjs +77 -0
- package/dist/routers/settings-git-channel.mjs.map +1 -0
- package/dist/routers/settings-labels.mjs +27 -1
- package/dist/routers/settings-labels.mjs.map +1 -1
- package/dist/routers/settings-links.mjs +63 -0
- package/dist/routers/settings-links.mjs.map +1 -0
- package/dist/routers/settings-members.mjs +4 -7
- package/dist/routers/settings-members.mjs.map +1 -1
- package/dist/routers/settings-notifications.mjs +15 -10
- package/dist/routers/settings-notifications.mjs.map +1 -1
- package/dist/routers/settings-tokens.mjs +2 -5
- package/dist/routers/settings-tokens.mjs.map +1 -1
- package/dist/routers/settings-webhooks.mjs +7 -24
- package/dist/routers/settings-webhooks.mjs.map +1 -1
- package/dist/routers/settings.handlers.mjs +107 -26
- package/dist/routers/settings.handlers.mjs.map +1 -1
- package/dist/routers/settings.mjs +16 -4
- package/dist/routers/settings.mjs.map +1 -1
- package/dist/routers/signed-storybook.mjs +53 -0
- package/dist/routers/signed-storybook.mjs.map +1 -0
- package/dist/routers/snapshot-groups.mjs +63 -0
- package/dist/routers/snapshot-groups.mjs.map +1 -0
- package/dist/routers/snapshots.mjs +103 -5
- package/dist/routers/snapshots.mjs.map +1 -1
- package/dist/routers/storybook.mjs +37 -88
- package/dist/routers/storybook.mjs.map +1 -1
- package/dist/routers/tokens.mjs +2 -5
- package/dist/routers/tokens.mjs.map +1 -1
- package/dist/routers/ui.mjs +34 -5
- package/dist/routers/ui.mjs.map +1 -1
- package/dist/routers/webhooks.mjs +5 -11
- package/dist/routers/webhooks.mjs.map +1 -1
- package/dist/routers/well-known.mjs +11 -4
- package/dist/routers/well-known.mjs.map +1 -1
- package/dist/routes/api-routes.mjs +93 -0
- package/dist/routes/api-routes.mjs.map +1 -0
- package/dist/routes/classification.mjs +38 -0
- package/dist/routes/classification.mjs.map +1 -0
- package/dist/routes/openapi-classes.mjs +22 -0
- package/dist/routes/openapi-classes.mjs.map +1 -0
- package/dist/routes/page-routes.mjs +104 -0
- package/dist/routes/page-routes.mjs.map +1 -0
- package/dist/runtime.mjs +18 -13
- package/dist/runtime.mjs.map +1 -1
- package/dist/serverless.mjs +5 -0
- package/dist/store.mjs +19 -1
- package/dist/store.mjs.map +1 -1
- package/dist/targets.mjs +46 -0
- package/dist/targets.mjs.map +1 -0
- package/dist/ui/buttons.mjs +16 -3
- package/dist/ui/buttons.mjs.map +1 -1
- package/dist/ui/design-reference.mjs +97 -0
- package/dist/ui/design-reference.mjs.map +1 -0
- package/dist/ui/document.mjs +1 -2
- package/dist/ui/document.mjs.map +1 -1
- package/dist/ui/forms.mjs +6 -5
- package/dist/ui/forms.mjs.map +1 -1
- package/dist/ui/icons/paths.mjs +1 -0
- package/dist/ui/icons/paths.mjs.map +1 -1
- package/dist/ui/preview-embed.mjs +82 -0
- package/dist/ui/preview-embed.mjs.map +1 -0
- package/dist/ui/shell/client-script.mjs +85 -0
- package/dist/ui/shell/client-script.mjs.map +1 -1
- package/dist/ui/shell/nav.mjs +12 -8
- package/dist/ui/shell/nav.mjs.map +1 -1
- package/dist/ui/shell/review-script.mjs +8 -0
- package/dist/ui/shell/review-script.mjs.map +1 -1
- package/dist/ui/shell/sidebar.mjs +2 -2
- package/dist/ui/shell/sidebar.mjs.map +1 -1
- package/package.json +11 -9
- package/dist/asset-paths.mjs +0 -23
- package/dist/asset-paths.mjs.map +0 -1
- package/dist/assets/mark.svg +0 -1
- package/dist/pages/settings-status.mjs +0 -137
- package/dist/pages/settings-status.mjs.map +0 -1
- package/dist/routers/settings-status.mjs +0 -77
- package/dist/routers/settings-status.mjs.map +0 -1
- package/dist/routers/status-configs.mjs +0 -108
- package/dist/routers/status-configs.mjs.map +0 -1
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { Button } from "../ui/buttons.mjs";
|
|
5
|
+
import { Alert, Badge, Meta } from "../ui/feedback.mjs";
|
|
6
|
+
import { Card, SectionTitle } from "../ui/layout.mjs";
|
|
7
|
+
import { HStack, VStack } from "../ui/stacks.mjs";
|
|
8
|
+
import { csrfField } from "../ui/csrf-field.mjs";
|
|
9
|
+
import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
10
|
+
//#region src/pages/health-panel.tsx
|
|
11
|
+
/** Badge tone for a health verdict. */
|
|
12
|
+
function healthTone(verdict) {
|
|
13
|
+
if (verdict === "healthy") return "success";
|
|
14
|
+
if (verdict === "watch") return "warning";
|
|
15
|
+
return verdict === "unhealthy" ? "danger" : "neutral";
|
|
16
|
+
}
|
|
17
|
+
function trendsOf(output) {
|
|
18
|
+
const trends = output?.trends;
|
|
19
|
+
return Array.isArray(trends) ? trends : [];
|
|
20
|
+
}
|
|
21
|
+
function scoreText(output) {
|
|
22
|
+
const score = output?.score;
|
|
23
|
+
return typeof score === "number" ? ` · score ${Math.round(score)}/100` : "";
|
|
24
|
+
}
|
|
25
|
+
function HealthBody(props) {
|
|
26
|
+
const { latest } = props;
|
|
27
|
+
if (!latest) return /* @__PURE__ */ jsxs(Meta, { children: [
|
|
28
|
+
"No health digest yet for the last ",
|
|
29
|
+
"30d",
|
|
30
|
+
"."
|
|
31
|
+
] });
|
|
32
|
+
if (latest.status === "failed") return /* @__PURE__ */ jsxs(Alert, {
|
|
33
|
+
tone: "warning",
|
|
34
|
+
title: "Health digest failed",
|
|
35
|
+
children: [
|
|
36
|
+
"The model call did not finish (",
|
|
37
|
+
latest.errorCode,
|
|
38
|
+
"). You can try again."
|
|
39
|
+
]
|
|
40
|
+
});
|
|
41
|
+
if (latest.status !== "done") return /* @__PURE__ */ jsx(Meta, { children: "Working on it…" });
|
|
42
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
43
|
+
/* @__PURE__ */ jsx("span", { children: latest.summary }),
|
|
44
|
+
/* @__PURE__ */ jsx("ul", { children: trendsOf(latest.output).map((trend) => /* @__PURE__ */ jsxs("li", { children: [
|
|
45
|
+
/* @__PURE__ */ jsx("strong", { children: trend.label }),
|
|
46
|
+
" (",
|
|
47
|
+
trend.direction,
|
|
48
|
+
"): ",
|
|
49
|
+
trend.note
|
|
50
|
+
] })) }),
|
|
51
|
+
/* @__PURE__ */ jsxs(Meta, { children: [
|
|
52
|
+
latest.profile,
|
|
53
|
+
" · ",
|
|
54
|
+
latest.model,
|
|
55
|
+
" · ",
|
|
56
|
+
latest.createdAt.slice(0, 16).replace("T", " "),
|
|
57
|
+
scoreText(latest.output)
|
|
58
|
+
] })
|
|
59
|
+
] });
|
|
60
|
+
}
|
|
61
|
+
/** Project health digest (advisory) with a generate/refresh button for approvers and admins. */
|
|
62
|
+
function HealthPanel(props) {
|
|
63
|
+
const { project, data } = props;
|
|
64
|
+
const base = `/projects/${project.slug}/insights/health`;
|
|
65
|
+
const latest = data.latest;
|
|
66
|
+
const inFlight = latest?.status === "pending" || latest?.status === "running";
|
|
67
|
+
const poll = inFlight ? {
|
|
68
|
+
"hx-get": `${base}/panel`,
|
|
69
|
+
"hx-trigger": "load delay:3s",
|
|
70
|
+
"hx-swap": "outerHTML"
|
|
71
|
+
} : {};
|
|
72
|
+
return /* @__PURE__ */ jsx("div", {
|
|
73
|
+
id: "health-panel",
|
|
74
|
+
...poll,
|
|
75
|
+
children: /* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsxs(HStack, { children: [
|
|
76
|
+
/* @__PURE__ */ jsxs(SectionTitle, {
|
|
77
|
+
level: 3,
|
|
78
|
+
children: [
|
|
79
|
+
"Project health (AI, last ",
|
|
80
|
+
"30d",
|
|
81
|
+
")"
|
|
82
|
+
]
|
|
83
|
+
}),
|
|
84
|
+
latest?.status === "done" ? /* @__PURE__ */ jsx(Badge, {
|
|
85
|
+
tone: healthTone(latest.verdict),
|
|
86
|
+
children: latest.verdict
|
|
87
|
+
}) : null,
|
|
88
|
+
inFlight ? /* @__PURE__ */ jsx(Badge, {
|
|
89
|
+
tone: "info",
|
|
90
|
+
children: "generating…"
|
|
91
|
+
}) : null
|
|
92
|
+
] }), /* @__PURE__ */ jsxs(VStack, { children: [/* @__PURE__ */ jsx(HealthBody, { latest }), data.canGenerate && !inFlight ? /* @__PURE__ */ jsxs("form", {
|
|
93
|
+
method: "post",
|
|
94
|
+
action: `${base}/generate`,
|
|
95
|
+
"hx-post": `${base}/generate`,
|
|
96
|
+
"hx-target": "#health-panel",
|
|
97
|
+
"hx-swap": "outerHTML",
|
|
98
|
+
children: [
|
|
99
|
+
csrfField(),
|
|
100
|
+
/* @__PURE__ */ jsx("input", {
|
|
101
|
+
type: "hidden",
|
|
102
|
+
name: "force",
|
|
103
|
+
value: latest ? "true" : "false"
|
|
104
|
+
}),
|
|
105
|
+
/* @__PURE__ */ jsx(Button, {
|
|
106
|
+
variant: "secondary",
|
|
107
|
+
size: "sm",
|
|
108
|
+
type: "submit",
|
|
109
|
+
icon: "sparkles",
|
|
110
|
+
children: latest ? "Refresh health" : "Generate health"
|
|
111
|
+
})
|
|
112
|
+
]
|
|
113
|
+
}) : null] })] })
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/** Renders the panel when AI health is available for the project, nothing otherwise. */
|
|
117
|
+
function HealthSlot(props) {
|
|
118
|
+
return props.data ? /* @__PURE__ */ jsx(HealthPanel, {
|
|
119
|
+
project: props.project,
|
|
120
|
+
data: props.data
|
|
121
|
+
}) : null;
|
|
122
|
+
}
|
|
123
|
+
//#endregion
|
|
124
|
+
export { HealthPanel, HealthSlot, healthTone };
|
|
125
|
+
|
|
126
|
+
//# sourceMappingURL=health-panel.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health-panel.mjs","names":[],"sources":["../../src/pages/health-panel.tsx"],"sourcesContent":["import type { Project } from \"@storyshelf/core/schema\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport { HEALTH_UI_WINDOW, type HealthPanelData } from \"../insights/health-panel.ts\";\nimport {\n Alert,\n Badge,\n Button,\n Card,\n HStack,\n Meta,\n SectionTitle,\n VStack,\n} from \"../ui/components.tsx\";\nimport { csrfField } from \"../ui/csrf-field.tsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\n/** Badge tone for a health verdict. */\nexport function healthTone(verdict: string | null): \"success\" | \"warning\" | \"danger\" | \"neutral\" {\n if (verdict === \"healthy\") {\n return \"success\";\n }\n if (verdict === \"watch\") {\n return \"warning\";\n }\n return verdict === \"unhealthy\" ? \"danger\" : \"neutral\";\n}\n\ninterface Trend {\n label: string;\n note: string;\n direction: string;\n}\n\nfunction trendsOf(output: unknown): Trend[] {\n const trends = (output as { trends?: unknown } | null)?.trends;\n return Array.isArray(trends) ? (trends as Trend[]) : [];\n}\n\nfunction scoreText(output: unknown): string {\n const score = (output as { score?: unknown } | null)?.score;\n return typeof score === \"number\" ? ` · score ${Math.round(score)}/100` : \"\";\n}\n\nfunction HealthBody(props: {\n latest: HealthPanelData[\"latest\"];\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { latest } = props;\n if (!latest) {\n return <Meta>No health digest yet for the last {HEALTH_UI_WINDOW}.</Meta>;\n }\n if (latest.status === \"failed\") {\n return (\n <Alert tone=\"warning\" title=\"Health digest failed\">\n The model call did not finish ({latest.errorCode}). You can try again.\n </Alert>\n );\n }\n if (latest.status !== \"done\") {\n return <Meta>Working on it…</Meta>;\n }\n return (\n <>\n <span>{latest.summary}</span>\n <ul>\n {trendsOf(latest.output).map((trend) => (\n <li>\n <strong>{trend.label}</strong> ({trend.direction}): {trend.note}\n </li>\n ))}\n </ul>\n <Meta>\n {latest.profile} · {latest.model} · {latest.createdAt.slice(0, 16).replace(\"T\", \" \")}\n {scoreText(latest.output)}\n </Meta>\n </>\n );\n}\n\n/** Props for the health panel. */\nexport interface HealthPanelProps {\n project: Project;\n data: HealthPanelData;\n}\n\n/** Project health digest (advisory) with a generate/refresh button for approvers and admins. */\nexport function HealthPanel(\n props: HealthPanelProps,\n): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, data } = props;\n const base = `/projects/${project.slug}/insights/health`;\n const latest = data.latest;\n const inFlight = latest?.status === \"pending\" || latest?.status === \"running\";\n const poll = inFlight\n ? { \"hx-get\": `${base}/panel`, \"hx-trigger\": \"load delay:3s\", \"hx-swap\": \"outerHTML\" }\n : {};\n return (\n <div id=\"health-panel\" {...poll}>\n <Card>\n <HStack>\n <SectionTitle level={3}>Project health (AI, last {HEALTH_UI_WINDOW})</SectionTitle>\n {latest?.status === \"done\" ? (\n <Badge tone={healthTone(latest.verdict)}>{latest.verdict}</Badge>\n ) : null}\n {inFlight ? <Badge tone=\"info\">generating…</Badge> : null}\n </HStack>\n <VStack>\n <HealthBody latest={latest} />\n {data.canGenerate && !inFlight ? (\n <form\n method=\"post\"\n action={`${base}/generate`}\n hx-post={`${base}/generate`}\n hx-target=\"#health-panel\"\n hx-swap=\"outerHTML\"\n >\n {csrfField()}\n <input type=\"hidden\" name=\"force\" value={latest ? \"true\" : \"false\"} />\n <Button variant=\"secondary\" size=\"sm\" type=\"submit\" icon=\"sparkles\">\n {latest ? \"Refresh health\" : \"Generate health\"}\n </Button>\n </form>\n ) : null}\n </VStack>\n </Card>\n </div>\n );\n}\n\n/** Renders the panel when AI health is available for the project, nothing otherwise. */\nexport function HealthSlot(props: {\n project: Project;\n data: HealthPanelData | null;\n}): HtmlEscapedString | Promise<HtmlEscapedString> | null {\n return props.data ? <HealthPanel project={props.project} data={props.data} /> : null;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { getStore } from "../store.mjs";
|
|
5
|
+
import { Button } from "../ui/buttons.mjs";
|
|
6
|
+
import { Badge, Meta } from "../ui/feedback.mjs";
|
|
7
|
+
import { Card, CardSection, SectionTitle } from "../ui/layout.mjs";
|
|
8
|
+
import { HStack, VStack } from "../ui/stacks.mjs";
|
|
9
|
+
import { SnapshotCards } from "./library-snapshots.mjs";
|
|
10
|
+
import { jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
11
|
+
import { createUrlBuilder } from "@storyshelf/core/urls";
|
|
12
|
+
//#region src/pages/library-grid.tsx
|
|
13
|
+
/** The component cards of a build; `layer` is present only with `testing` and captured snapshots. */
|
|
14
|
+
function LibraryGrid(props) {
|
|
15
|
+
return /* @__PURE__ */ jsx(VStack, { children: props.groups.map((group) => /* @__PURE__ */ jsx(LibraryCard, {
|
|
16
|
+
group,
|
|
17
|
+
project: props.project,
|
|
18
|
+
build: props.build,
|
|
19
|
+
layer: props.layer
|
|
20
|
+
}, group.title)) });
|
|
21
|
+
}
|
|
22
|
+
function LibraryCard(props) {
|
|
23
|
+
const { group, project, build, layer } = props;
|
|
24
|
+
const href = componentHref(project, group);
|
|
25
|
+
const captured = layer ? group.stories.flatMap((story) => layer.byStory.get(story.id) ?? []) : [];
|
|
26
|
+
const plain = group.stories.filter((story) => !layer?.byStory.has(story.id));
|
|
27
|
+
return /* @__PURE__ */ jsxs(Card, {
|
|
28
|
+
padded: false,
|
|
29
|
+
"data-filter-item": true,
|
|
30
|
+
"data-filter-text": `${group.title} ${group.stories.map((story) => story.name).join(" ")}`,
|
|
31
|
+
children: [/* @__PURE__ */ jsxs(CardSection, {
|
|
32
|
+
divider: true,
|
|
33
|
+
children: [/* @__PURE__ */ jsx(SectionTitle, { children: href ? /* @__PURE__ */ jsx("a", {
|
|
34
|
+
href,
|
|
35
|
+
children: group.title
|
|
36
|
+
}) : group.title }), /* @__PURE__ */ jsxs(HStack, { children: [
|
|
37
|
+
group.tags.map((tag) => /* @__PURE__ */ jsx(Badge, {
|
|
38
|
+
tone: "neutral",
|
|
39
|
+
children: tag
|
|
40
|
+
}, tag)),
|
|
41
|
+
/* @__PURE__ */ jsxs(Meta, {
|
|
42
|
+
as: "span",
|
|
43
|
+
children: [
|
|
44
|
+
group.stories.length,
|
|
45
|
+
" ",
|
|
46
|
+
group.stories.length === 1 ? "story" : "stories"
|
|
47
|
+
]
|
|
48
|
+
}),
|
|
49
|
+
/* @__PURE__ */ jsx(DocsLink, {
|
|
50
|
+
group,
|
|
51
|
+
project,
|
|
52
|
+
build
|
|
53
|
+
})
|
|
54
|
+
] })]
|
|
55
|
+
}), /* @__PURE__ */ jsx(CardSection, { children: /* @__PURE__ */ jsxs(VStack, { children: [layer && captured.length > 0 ? /* @__PURE__ */ jsx(SnapshotCards, {
|
|
56
|
+
snapshots: captured,
|
|
57
|
+
project,
|
|
58
|
+
buildId: build.id,
|
|
59
|
+
layer,
|
|
60
|
+
componentHref: href
|
|
61
|
+
}) : null, plain.length > 0 ? /* @__PURE__ */ jsx(StoryLinks, {
|
|
62
|
+
stories: plain,
|
|
63
|
+
project,
|
|
64
|
+
build
|
|
65
|
+
}) : null] }) })]
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/** Component page of a group (its story file), when the index names one. */
|
|
69
|
+
function componentHref(project, group) {
|
|
70
|
+
return group.importPath ? createUrlBuilder("/", getStore().config.publishedBaseDomain).component(project.slug, group.importPath) : void 0;
|
|
71
|
+
}
|
|
72
|
+
function DocsLink(props) {
|
|
73
|
+
const docs = props.group.docs[0];
|
|
74
|
+
if (!docs) return null;
|
|
75
|
+
return /* @__PURE__ */ jsx(Button, {
|
|
76
|
+
variant: "ghost",
|
|
77
|
+
size: "sm",
|
|
78
|
+
icon: "book",
|
|
79
|
+
href: `/projects/${props.project.slug}/storybook/build/${props.build.id}/?storyId=${encodeURIComponent(docs.id)}&viewMode=docs`,
|
|
80
|
+
target: "_blank",
|
|
81
|
+
rel: "noopener",
|
|
82
|
+
children: "Docs"
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/** Links into the published Storybook, one per story, for stories without a snapshot. */
|
|
86
|
+
function StoryLinks(props) {
|
|
87
|
+
const base = createUrlBuilder("/", getStore().config.publishedBaseDomain).storybookBuild(props.project.slug, props.build.id);
|
|
88
|
+
return /* @__PURE__ */ jsx(HStack, { children: props.stories.map((story) => /* @__PURE__ */ jsx("span", {
|
|
89
|
+
"data-filter-item": true,
|
|
90
|
+
"data-filter-text": `${story.title} ${story.name}`,
|
|
91
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
92
|
+
variant: "ghost",
|
|
93
|
+
size: "sm",
|
|
94
|
+
icon: "external-link",
|
|
95
|
+
href: `${base}/?storyId=${encodeURIComponent(story.id)}`,
|
|
96
|
+
target: "_blank",
|
|
97
|
+
rel: "noopener",
|
|
98
|
+
children: story.name
|
|
99
|
+
})
|
|
100
|
+
}, story.id)) });
|
|
101
|
+
}
|
|
102
|
+
//#endregion
|
|
103
|
+
export { LibraryGrid };
|
|
104
|
+
|
|
105
|
+
//# sourceMappingURL=library-grid.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"library-grid.mjs","names":[],"sources":["../../src/pages/library-grid.tsx"],"sourcesContent":["/** The library's component list: one card per title, from the catalog, with snapshots layered on. */\nimport type { Build, Project } from \"@storyshelf/core/schema\";\nimport { createUrlBuilder } from \"@storyshelf/core/urls\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport { getStore } from \"../store.ts\";\nimport {\n Badge,\n Button,\n Card,\n CardSection,\n HStack,\n Meta,\n SectionTitle,\n VStack,\n} from \"../ui/components.tsx\";\nimport type { LibraryGroup } from \"./library-model.ts\";\nimport { SnapshotCards, type SnapshotLayer } from \"./library-snapshots.tsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\ntype Rendered = HtmlEscapedString | Promise<HtmlEscapedString>;\n\n/** The component cards of a build; `layer` is present only with `testing` and captured snapshots. */\nexport function LibraryGrid(props: {\n groups: LibraryGroup[];\n project: Project;\n build: Build;\n layer: SnapshotLayer | null;\n}): Rendered {\n return (\n <VStack>\n {props.groups.map((group): Rendered => (\n <LibraryCard\n key={group.title}\n group={group}\n project={props.project}\n build={props.build}\n layer={props.layer}\n />\n ))}\n </VStack>\n );\n}\n\nfunction LibraryCard(props: {\n group: LibraryGroup;\n project: Project;\n build: Build;\n layer: SnapshotLayer | null;\n}): Rendered {\n const { group, project, build, layer } = props;\n const href = componentHref(project, group);\n const captured = layer ? group.stories.flatMap((story) => layer.byStory.get(story.id) ?? []) : [];\n const plain = group.stories.filter((story) => !layer?.byStory.has(story.id));\n return (\n <Card\n padded={false}\n data-filter-item\n data-filter-text={`${group.title} ${group.stories.map((story) => story.name).join(\" \")}`}\n >\n <CardSection divider>\n <SectionTitle>{href ? <a href={href}>{group.title}</a> : group.title}</SectionTitle>\n <HStack>\n {group.tags.map((tag) => (\n <Badge key={tag} tone=\"neutral\">\n {tag}\n </Badge>\n ))}\n <Meta as=\"span\">\n {group.stories.length} {group.stories.length === 1 ? \"story\" : \"stories\"}\n </Meta>\n <DocsLink group={group} project={project} build={build} />\n </HStack>\n </CardSection>\n <CardSection>\n <VStack>\n {layer && captured.length > 0 ? (\n <SnapshotCards\n snapshots={captured}\n project={project}\n buildId={build.id}\n layer={layer}\n componentHref={href}\n />\n ) : null}\n {plain.length > 0 ? <StoryLinks stories={plain} project={project} build={build} /> : null}\n </VStack>\n </CardSection>\n </Card>\n );\n}\n\n/** Component page of a group (its story file), when the index names one. */\nfunction componentHref(project: Project, group: LibraryGroup): string | undefined {\n return group.importPath\n ? createUrlBuilder(\"/\", getStore().config.publishedBaseDomain).component(\n project.slug,\n group.importPath,\n )\n : undefined;\n}\n\nfunction DocsLink(props: { group: LibraryGroup; project: Project; build: Build }): Rendered | null {\n const docs = props.group.docs[0];\n if (!docs) {\n return null;\n }\n return (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n icon=\"book\"\n href={`/projects/${props.project.slug}/storybook/build/${props.build.id}/?storyId=${encodeURIComponent(docs.id)}&viewMode=docs`}\n target=\"_blank\"\n rel=\"noopener\"\n >\n Docs\n </Button>\n );\n}\n\n/** Links into the published Storybook, one per story, for stories without a snapshot. */\nfunction StoryLinks(props: {\n stories: LibraryGroup[\"stories\"];\n project: Project;\n build: Build;\n}): Rendered {\n const base = createUrlBuilder(\"/\", getStore().config.publishedBaseDomain).storybookBuild(\n props.project.slug,\n props.build.id,\n );\n return (\n <HStack>\n {props.stories.map((story): Rendered => (\n <span key={story.id} data-filter-item data-filter-text={`${story.title} ${story.name}`}>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n icon=\"external-link\"\n href={`${base}/?storyId=${encodeURIComponent(story.id)}`}\n target=\"_blank\"\n rel=\"noopener\"\n >\n {story.name}\n </Button>\n </span>\n ))}\n </HStack>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { normalizeImportPath } from "@storyshelf/core/models";
|
|
5
|
+
//#region src/pages/library-model.ts
|
|
6
|
+
/** Tags Storybook adds on its own; they say nothing about the component. */
|
|
7
|
+
const STORYBOOK_TAGS = /* @__PURE__ */ new Set([
|
|
8
|
+
"dev",
|
|
9
|
+
"test",
|
|
10
|
+
"autodocs",
|
|
11
|
+
"play-fn",
|
|
12
|
+
"manifest",
|
|
13
|
+
"attached-mdx",
|
|
14
|
+
"unattached-mdx"
|
|
15
|
+
]);
|
|
16
|
+
/** Group catalog entries by title, ordered by title; stories keep index order. */
|
|
17
|
+
function groupLibrary(entries) {
|
|
18
|
+
return [...Map.groupBy(entries, (entry) => entry.title).entries()].map(([title, group]) => {
|
|
19
|
+
return {
|
|
20
|
+
title,
|
|
21
|
+
importPath: group.find((entry) => entry.importPath !== "")?.importPath ?? "",
|
|
22
|
+
stories: group.filter((entry) => entry.type === "story"),
|
|
23
|
+
docs: group.filter((entry) => entry.type === "docs"),
|
|
24
|
+
tags: [...new Set(group.flatMap((entry) => entry.tags))].filter((tag) => !STORYBOOK_TAGS.has(tag))
|
|
25
|
+
};
|
|
26
|
+
}).toSorted((a, b) => a.title.localeCompare(b.title));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Entries rebuilt from a build's snapshots, for a captured build whose Storybook index is gone
|
|
30
|
+
* (statics purged): the pages still list what was captured.
|
|
31
|
+
*/
|
|
32
|
+
function entriesFromSnapshots(snapshots) {
|
|
33
|
+
const seen = /* @__PURE__ */ new Map();
|
|
34
|
+
for (const snap of snapshots) if (!seen.has(snap.storyId)) seen.set(snap.storyId, {
|
|
35
|
+
id: snap.storyId,
|
|
36
|
+
title: snap.storyTitle || "Untitled",
|
|
37
|
+
name: snap.storyName,
|
|
38
|
+
importPath: snap.storyImportPath ? normalizeImportPath(snap.storyImportPath) : "",
|
|
39
|
+
type: "story",
|
|
40
|
+
tags: []
|
|
41
|
+
});
|
|
42
|
+
return [...seen.values()];
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
45
|
+
export { entriesFromSnapshots, groupLibrary };
|
|
46
|
+
|
|
47
|
+
//# sourceMappingURL=library-model.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"library-model.mjs","names":[],"sources":["../../src/pages/library-model.ts"],"sourcesContent":["import type { CatalogEntry } from \"@storyshelf/core/capture\";\nimport { normalizeImportPath } from \"@storyshelf/core/models\";\nimport type { Snapshot } from \"@storyshelf/core/schema\";\n\n/** Tags Storybook adds on its own; they say nothing about the component. */\nconst STORYBOOK_TAGS: ReadonlySet<string> = new Set([\n \"dev\",\n \"test\",\n \"autodocs\",\n \"play-fn\",\n \"manifest\",\n \"attached-mdx\",\n \"unattached-mdx\",\n]);\n\n/** The library's unit: every entry that shares a title, grouped like Storybook's sidebar. */\nexport interface LibraryGroup {\n title: string;\n /** Story file of the first entry that has one; empty when the index names none. */\n importPath: string;\n stories: CatalogEntry[];\n /** Docs entries; the first is the component's docs page. */\n docs: CatalogEntry[];\n /** Tags worth showing, in first-seen order. */\n tags: string[];\n}\n\n/** Group catalog entries by title, ordered by title; stories keep index order. */\nexport function groupLibrary(entries: readonly CatalogEntry[]): LibraryGroup[] {\n const groups = Map.groupBy(entries, (entry) => entry.title);\n return [...groups.entries()]\n .map(([title, group]): LibraryGroup => {\n return {\n title,\n importPath: group.find((entry) => entry.importPath !== \"\")?.importPath ?? \"\",\n stories: group.filter((entry) => entry.type === \"story\"),\n docs: group.filter((entry) => entry.type === \"docs\"),\n tags: [...new Set(group.flatMap((entry) => entry.tags))].filter(\n (tag) => !STORYBOOK_TAGS.has(tag),\n ),\n };\n })\n .toSorted((a, b) => a.title.localeCompare(b.title));\n}\n\n/**\n * Entries rebuilt from a build's snapshots, for a captured build whose Storybook index is gone\n * (statics purged): the pages still list what was captured.\n */\nexport function entriesFromSnapshots(snapshots: readonly Snapshot[]): CatalogEntry[] {\n const seen = new Map<string, CatalogEntry>();\n for (const snap of snapshots) {\n if (!seen.has(snap.storyId)) {\n seen.set(snap.storyId, {\n id: snap.storyId,\n title: snap.storyTitle || \"Untitled\",\n name: snap.storyName,\n importPath: snap.storyImportPath ? normalizeImportPath(snap.storyImportPath) : \"\",\n type: \"story\",\n tags: [],\n });\n }\n }\n return [...seen.values()];\n}\n"],"mappings":";;;;;;AAKA,MAAM,iCAAsC,IAAI,IAAI;CAClD;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;AAeD,SAAgB,aAAa,SAAkD;CAE7E,OAAO,CAAC,GADO,IAAI,QAAQ,UAAU,UAAU,MAAM,KACrC,CAAC,CAAC,QAAQ,CAAC,CAAC,CACzB,KAAK,CAAC,OAAO,WAAyB;EACrC,OAAO;GACL;GACA,YAAY,MAAM,MAAM,UAAU,MAAM,eAAe,EAAE,CAAC,EAAE,cAAc;GAC1E,SAAS,MAAM,QAAQ,UAAU,MAAM,SAAS,OAAO;GACvD,MAAM,MAAM,QAAQ,UAAU,MAAM,SAAS,MAAM;GACnD,MAAM,CAAC,GAAG,IAAI,IAAI,MAAM,SAAS,UAAU,MAAM,IAAI,CAAC,CAAC,CAAC,CAAC,QACtD,QAAQ,CAAC,eAAe,IAAI,GAAG,CAClC;EACF;CACF,CAAC,CAAC,CACD,UAAU,GAAG,MAAM,EAAE,MAAM,cAAc,EAAE,KAAK,CAAC;AACtD;;;;;AAMA,SAAgB,qBAAqB,WAAgD;CACnF,MAAM,uBAAO,IAAI,IAA0B;CAC3C,KAAK,MAAM,QAAQ,WACjB,IAAI,CAAC,KAAK,IAAI,KAAK,OAAO,GACxB,KAAK,IAAI,KAAK,SAAS;EACrB,IAAI,KAAK;EACT,OAAO,KAAK,cAAc;EAC1B,MAAM,KAAK;EACX,YAAY,KAAK,kBAAkB,oBAAoB,KAAK,eAAe,IAAI;EAC/E,MAAM;EACN,MAAM,CAAC;CACT,CAAC;CAGL,OAAO,CAAC,GAAG,KAAK,OAAO,CAAC;AAC1B"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { getStore } from "../store.mjs";
|
|
5
|
+
import { css } from "../ui/css.mjs";
|
|
6
|
+
import { Button } from "../ui/buttons.mjs";
|
|
7
|
+
import { Badge, statusTone } from "../ui/feedback.mjs";
|
|
8
|
+
import { SectionTitle } from "../ui/layout.mjs";
|
|
9
|
+
import { HStack } from "../ui/stacks.mjs";
|
|
10
|
+
import { designUrlsFor, loadDesignPreviews, loadDesignResolutions, loadProjectDesignLinks, providerNameFor } from "../routers/design.handlers.mjs";
|
|
11
|
+
import { diffPaneImg, snapshotCard, snapshotCardBody, snapshotCardHead, snapshotCardMeta, snapshotGrid } from "../ui/styles/review.mjs";
|
|
12
|
+
import { A11yViolationsDetails } from "./a11y-violations.mjs";
|
|
13
|
+
import { groupSnapshots } from "./build-diff-model.mjs";
|
|
14
|
+
import { DesignButtons } from "./design-links.mjs";
|
|
15
|
+
import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
16
|
+
import { createUrlBuilder } from "@storyshelf/core/urls";
|
|
17
|
+
//#region src/pages/library-snapshots.tsx
|
|
18
|
+
/** Snapshot image with library height cap, extending the shared diff pane image. */
|
|
19
|
+
const snapshotImg = css`
|
|
20
|
+
/* snapshot-img */
|
|
21
|
+
${diffPaneImg}
|
|
22
|
+
max-height: 240px;
|
|
23
|
+
object-fit: contain;
|
|
24
|
+
`;
|
|
25
|
+
const NO_DESIGN = {
|
|
26
|
+
bySnapshot: /* @__PURE__ */ new Map(),
|
|
27
|
+
previews: /* @__PURE__ */ new Map(),
|
|
28
|
+
projectLinks: []
|
|
29
|
+
};
|
|
30
|
+
/** The layer for a build's snapshots; design data that cannot be loaded leaves the library plain. */
|
|
31
|
+
async function loadSnapshotLayer(project, snapshots) {
|
|
32
|
+
const primary = groupSnapshots(snapshots).map((group) => group.primary);
|
|
33
|
+
const design = await loadLibraryDesign(project, primary).catch(() => NO_DESIGN);
|
|
34
|
+
return {
|
|
35
|
+
byStory: Map.groupBy(primary, (snap) => snap.storyId),
|
|
36
|
+
design,
|
|
37
|
+
multipleViewports: new Set(primary.map((snap) => snap.viewportName)).size > 1
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
async function loadLibraryDesign(project, snapshots) {
|
|
41
|
+
const [resolutions, projectLinks] = await Promise.all([loadDesignResolutions(project, snapshots), loadProjectDesignLinks(project)]);
|
|
42
|
+
const bySnapshot = new Map(snapshots.map((snap) => [snap.id, designUrlsFor(resolutions, snap)]));
|
|
43
|
+
return {
|
|
44
|
+
bySnapshot,
|
|
45
|
+
previews: await loadDesignPreviews(project, [...bySnapshot.values()].flat()),
|
|
46
|
+
projectLinks
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** Snapshot cards of a component's captured stories, one grid per viewport. */
|
|
50
|
+
function SnapshotCards(props) {
|
|
51
|
+
const byViewport = Map.groupBy(props.snapshots, (snap) => snap.viewportName);
|
|
52
|
+
return /* @__PURE__ */ jsx(Fragment, { children: [...byViewport.keys()].toSorted().map((name) => /* @__PURE__ */ jsxs("div", { children: [props.layer.multipleViewports ? /* @__PURE__ */ jsx(SectionTitle, {
|
|
53
|
+
level: 3,
|
|
54
|
+
children: name
|
|
55
|
+
}) : null, /* @__PURE__ */ jsx("div", {
|
|
56
|
+
class: snapshotGrid,
|
|
57
|
+
children: (byViewport.get(name) ?? []).toSorted((a, b) => a.storyName.localeCompare(b.storyName)).map((snap) => /* @__PURE__ */ jsx(SnapshotCardItem, {
|
|
58
|
+
snap,
|
|
59
|
+
...props
|
|
60
|
+
}, snap.id))
|
|
61
|
+
})] }, name)) });
|
|
62
|
+
}
|
|
63
|
+
function SnapshotCardItem(props) {
|
|
64
|
+
const { snap, project, buildId } = props;
|
|
65
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
66
|
+
class: snapshotCard,
|
|
67
|
+
"data-filter-item": true,
|
|
68
|
+
"data-filter-text": `${snap.storyTitle} ${snap.storyName}`,
|
|
69
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
70
|
+
class: snapshotCardHead,
|
|
71
|
+
children: [/* @__PURE__ */ jsx("strong", { children: snap.storyName }), /* @__PURE__ */ jsxs("span", {
|
|
72
|
+
class: snapshotCardMeta,
|
|
73
|
+
children: [
|
|
74
|
+
/* @__PURE__ */ jsx(Badge, {
|
|
75
|
+
tone: statusTone(snap.status),
|
|
76
|
+
children: snap.status
|
|
77
|
+
}),
|
|
78
|
+
" ",
|
|
79
|
+
snap.viewportWidth,
|
|
80
|
+
"×",
|
|
81
|
+
snap.viewportHeight
|
|
82
|
+
]
|
|
83
|
+
})]
|
|
84
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
85
|
+
class: snapshotCardBody,
|
|
86
|
+
children: [
|
|
87
|
+
/* @__PURE__ */ jsx("img", {
|
|
88
|
+
class: snapshotImg,
|
|
89
|
+
src: `/api/v1/projects/${project.slug}/builds/${buildId}/snapshots/${snap.id}/image`,
|
|
90
|
+
alt: `${snap.storyTitle} / ${snap.storyName}`,
|
|
91
|
+
loading: "lazy"
|
|
92
|
+
}),
|
|
93
|
+
/* @__PURE__ */ jsx(A11yViolationsDetails, { snapshot: snap }),
|
|
94
|
+
/* @__PURE__ */ jsx(SnapshotActions, { ...props })
|
|
95
|
+
]
|
|
96
|
+
})]
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
function SnapshotActions(props) {
|
|
100
|
+
const { snap, project, buildId, layer } = props;
|
|
101
|
+
const urls = createUrlBuilder("/", getStore().config.publishedBaseDomain);
|
|
102
|
+
return /* @__PURE__ */ jsxs(HStack, { children: [
|
|
103
|
+
/* @__PURE__ */ jsx(Button, {
|
|
104
|
+
variant: "secondary",
|
|
105
|
+
size: "sm",
|
|
106
|
+
icon: "columns",
|
|
107
|
+
href: `${urls.buildDiff(project.slug, buildId)}?snapshot=${snap.id}`,
|
|
108
|
+
children: "Review"
|
|
109
|
+
}),
|
|
110
|
+
/* @__PURE__ */ jsx(Button, {
|
|
111
|
+
variant: "ghost",
|
|
112
|
+
size: "sm",
|
|
113
|
+
icon: "external-link",
|
|
114
|
+
href: `${urls.storybookBuild(project.slug, buildId)}/?storyId=${encodeURIComponent(snap.storyId)}`,
|
|
115
|
+
target: "_blank",
|
|
116
|
+
rel: "noopener",
|
|
117
|
+
children: "Preview"
|
|
118
|
+
}),
|
|
119
|
+
/* @__PURE__ */ jsx(DesignButtons, {
|
|
120
|
+
urls: layer.design.bySnapshot.get(snap.id) ?? [],
|
|
121
|
+
providerName: providerNameFor,
|
|
122
|
+
previews: layer.design.previews,
|
|
123
|
+
componentHref: props.componentHref
|
|
124
|
+
})
|
|
125
|
+
] });
|
|
126
|
+
}
|
|
127
|
+
//#endregion
|
|
128
|
+
export { SnapshotCards, loadSnapshotLayer };
|
|
129
|
+
|
|
130
|
+
//# sourceMappingURL=library-snapshots.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"library-snapshots.mjs","names":[],"sources":["../../src/pages/library-snapshots.tsx"],"sourcesContent":["/** The snapshot layer of the library: what `testing` adds on top of the catalog (ADR 0006, decision 9). */\nimport type { Project, Snapshot } from \"@storyshelf/core/schema\";\nimport { createUrlBuilder } from \"@storyshelf/core/urls\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport {\n loadDesignResolutions,\n designUrlsFor,\n loadDesignPreviews,\n loadProjectDesignLinks,\n providerNameFor,\n} from \"../routers/design.handlers.ts\";\nimport { getStore } from \"../store.ts\";\nimport { Badge, Button, HStack, SectionTitle, statusTone } from \"../ui/components.tsx\";\nimport { css } from \"../ui/css.ts\";\nimport {\n diffPaneImg,\n snapshotCard,\n snapshotCardBody,\n snapshotCardHead,\n snapshotCardMeta,\n snapshotGrid,\n} from \"../ui/styles/review.ts\";\nimport { A11yViolationsDetails } from \"./a11y-violations.tsx\";\nimport { groupSnapshots } from \"./build-diff-model.ts\";\nimport { DesignButtons, type DesignPreviewInfo, type ProjectDesignLink } from \"./design-links.tsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\ntype Rendered = HtmlEscapedString | Promise<HtmlEscapedString>;\n\n/** Snapshot image with library height cap, extending the shared diff pane image. */\nconst snapshotImg = css`\n /* snapshot-img */\n ${diffPaneImg}\n max-height: 240px;\n object-fit: contain;\n`;\n\n/** Design data of the library: per-snapshot component links plus project-level file links. */\nexport interface LibraryDesign {\n bySnapshot: Map<string, string[]>;\n previews: Map<string, DesignPreviewInfo>;\n projectLinks: ProjectDesignLink[];\n}\n\n/** Snapshots and design links of the build, by story; layered onto the catalog's stories. */\nexport interface SnapshotLayer {\n /** One snapshot per story and mode (the primary engine's; other browsers are review detail). */\n byStory: Map<string, Snapshot[]>;\n design: LibraryDesign;\n /** Whether the build has more than one viewport (so the cards name theirs). */\n multipleViewports: boolean;\n}\n\nconst NO_DESIGN: LibraryDesign = { bySnapshot: new Map(), previews: new Map(), projectLinks: [] };\n\n/** The layer for a build's snapshots; design data that cannot be loaded leaves the library plain. */\nexport async function loadSnapshotLayer(\n project: Project,\n snapshots: Snapshot[],\n): Promise<SnapshotLayer> {\n const primary = groupSnapshots(snapshots).map((group) => group.primary);\n const design = await loadLibraryDesign(project, primary).catch(() => NO_DESIGN);\n return {\n byStory: Map.groupBy(primary, (snap) => snap.storyId),\n design,\n multipleViewports: new Set(primary.map((snap) => snap.viewportName)).size > 1,\n };\n}\n\nasync function loadLibraryDesign(project: Project, snapshots: Snapshot[]): Promise<LibraryDesign> {\n const [resolutions, projectLinks] = await Promise.all([\n loadDesignResolutions(project, snapshots),\n loadProjectDesignLinks(project),\n ]);\n const bySnapshot = new Map(\n snapshots.map((snap) => [snap.id, designUrlsFor(resolutions, snap)] as const),\n );\n const previews = await loadDesignPreviews(project, [...bySnapshot.values()].flat());\n return { bySnapshot, previews, projectLinks };\n}\n\n/** Snapshot cards of a component's captured stories, one grid per viewport. */\nexport function SnapshotCards(props: {\n snapshots: Snapshot[];\n project: Project;\n buildId: string;\n layer: SnapshotLayer;\n componentHref: string | undefined;\n}): Rendered {\n const byViewport = Map.groupBy(props.snapshots, (snap) => snap.viewportName);\n return (\n <>\n {[...byViewport.keys()].toSorted().map((name): Rendered => (\n <div key={name}>\n {props.layer.multipleViewports ? <SectionTitle level={3}>{name}</SectionTitle> : null}\n <div class={snapshotGrid}>\n {(byViewport.get(name) ?? [])\n .toSorted((a, b) => a.storyName.localeCompare(b.storyName))\n .map((snap) => (\n <SnapshotCardItem key={snap.id} snap={snap} {...props} />\n ))}\n </div>\n </div>\n ))}\n </>\n );\n}\n\nfunction SnapshotCardItem(props: {\n snap: Snapshot;\n project: Project;\n buildId: string;\n layer: SnapshotLayer;\n componentHref: string | undefined;\n}): Rendered {\n const { snap, project, buildId } = props;\n return (\n <div\n class={snapshotCard}\n data-filter-item\n data-filter-text={`${snap.storyTitle} ${snap.storyName}`}\n >\n <div class={snapshotCardHead}>\n <strong>{snap.storyName}</strong>\n <span class={snapshotCardMeta}>\n <Badge tone={statusTone(snap.status)}>{snap.status}</Badge> {snap.viewportWidth}×\n {snap.viewportHeight}\n </span>\n </div>\n <div class={snapshotCardBody}>\n <img\n class={snapshotImg}\n src={`/api/v1/projects/${project.slug}/builds/${buildId}/snapshots/${snap.id}/image`}\n alt={`${snap.storyTitle} / ${snap.storyName}`}\n loading=\"lazy\"\n />\n <A11yViolationsDetails snapshot={snap} />\n <SnapshotActions {...props} />\n </div>\n </div>\n );\n}\n\nfunction SnapshotActions(props: {\n snap: Snapshot;\n project: Project;\n buildId: string;\n layer: SnapshotLayer;\n componentHref: string | undefined;\n}): Rendered {\n const { snap, project, buildId, layer } = props;\n const urls = createUrlBuilder(\"/\", getStore().config.publishedBaseDomain);\n return (\n <HStack>\n <Button\n variant=\"secondary\"\n size=\"sm\"\n icon=\"columns\"\n href={`${urls.buildDiff(project.slug, buildId)}?snapshot=${snap.id}`}\n >\n Review\n </Button>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n icon=\"external-link\"\n href={`${urls.storybookBuild(project.slug, buildId)}/?storyId=${encodeURIComponent(snap.storyId)}`}\n target=\"_blank\"\n rel=\"noopener\"\n >\n Preview\n </Button>\n <DesignButtons\n urls={layer.design.bySnapshot.get(snap.id) ?? []}\n providerName={providerNameFor}\n previews={layer.design.previews}\n componentHref={props.componentHref}\n />\n </HStack>\n );\n}\n"],"mappings":"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"}
|