@storyshelf/app 0.9.0-next-d2cc082 → 0.9.0-next-588f070

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/dist/index.mjs CHANGED
@@ -380,7 +380,7 @@ function registerDocs(app) {
380
380
  * from source (dev server, tests).
381
381
  */
382
382
  function packageVersion() {
383
- return "0.9.0-next-d2cc082";
383
+ return "0.9.0-next-588f070";
384
384
  }
385
385
  export { createAuthSystemHook, createShelfApp, createShelfLogger };
386
386
 
@@ -0,0 +1,333 @@
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 { Alert, Badge, EmptyState, Meta, Stat } from "../ui/feedback.mjs";
8
+ import { Card, PageHeader, SectionTitle } from "../ui/layout.mjs";
9
+ import { HStack, VStack } from "../ui/stacks.mjs";
10
+ import { Table } from "../ui/table.mjs";
11
+ import { RelativeTime } from "../ui/time.mjs";
12
+ import { DocumentLayout } from "../ui/document.mjs";
13
+ import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
14
+ import { createUrlBuilder } from "@storyshelf/core/urls";
15
+ // src/pages/design-coverage.tsx
16
+ const wideOnly = css`
17
+ /* coverage-wide-only */
18
+ @media (max-width: 640px) {
19
+ & {
20
+ display: none;
21
+ }
22
+ }
23
+ `;
24
+ const narrowOnly = css`
25
+ /* coverage-narrow-only */
26
+ display: none;
27
+ @media (max-width: 640px) {
28
+ & {
29
+ display: block;
30
+ }
31
+ }
32
+ `;
33
+ const statLink = css`
34
+ /* coverage-stat-link */
35
+ display: block;
36
+ color: inherit;
37
+ text-decoration: none;
38
+ &:hover,
39
+ &:focus-visible {
40
+ text-decoration: none;
41
+ }
42
+ `;
43
+ const widePath = css`
44
+ /* coverage-wide-path */
45
+ ${wideOnly}
46
+ overflow-wrap: anywhere;
47
+ `;
48
+ /** The design coverage page: what lacks a design, what drifted, and how much is in sync. */
49
+ function renderDesignCoveragePage(data) {
50
+ const { project, build } = data;
51
+ const urls = createUrlBuilder("/", getStore().config.publishedBaseDomain);
52
+ return /* @__PURE__ */ jsxs(DocumentLayout, {
53
+ title: "Design coverage",
54
+ nav: {
55
+ active: "library",
56
+ projectSlug: project.slug,
57
+ projectName: project.name,
58
+ trail: [{
59
+ label: "Library",
60
+ href: urls.library(project.slug)
61
+ }, { label: "Design coverage" }]
62
+ },
63
+ children: [/* @__PURE__ */ jsx(PageHeader, {
64
+ title: "Design coverage",
65
+ description: "Which components have a design file, and which have drifted from it.",
66
+ meta: build ? `Latest accepted build: ${build.gitBranch} · ${build.gitSha.slice(0, 7)}` : void 0
67
+ }), build ? /* @__PURE__ */ jsx(CoverageBody, { data }) : /* @__PURE__ */ jsx(EmptyState, {
68
+ title: "No build to check yet",
69
+ description: "Design coverage is read from the latest accepted build. Upload a Storybook and accept a build first.",
70
+ action: /* @__PURE__ */ jsx(Button, {
71
+ variant: "secondary",
72
+ size: "sm",
73
+ href: urls.buildsList(project.slug),
74
+ children: "Go to builds"
75
+ })
76
+ })]
77
+ });
78
+ }
79
+ /** Drift cannot be told for the linked designs: some provider has no token and nothing is known to have drifted. */
80
+ function driftUnavailable(data) {
81
+ return data.withoutToken.length > 0 && data.drifted.length === 0;
82
+ }
83
+ function CoverageBody(props) {
84
+ const { data } = props;
85
+ const unavailable = driftUnavailable(data);
86
+ return /* @__PURE__ */ jsxs(VStack, { children: [
87
+ data.storyLinksRead ? null : /* @__PURE__ */ jsx(Alert, {
88
+ tone: "info",
89
+ title: "Only mapped design links are counted",
90
+ children: "Links declared in stories are read from captured builds, so they are not counted without testing. Components linked under Settings → Design are."
91
+ }),
92
+ /* @__PURE__ */ jsxs("div", {
93
+ class: "grid grid--3",
94
+ children: [
95
+ /* @__PURE__ */ jsx("a", {
96
+ href: "#no-link",
97
+ class: statLink,
98
+ children: /* @__PURE__ */ jsx(Stat, {
99
+ label: "No design link",
100
+ value: data.unlinked.length,
101
+ tone: data.unlinked.length > 0 ? "warning" : "success"
102
+ })
103
+ }),
104
+ /* @__PURE__ */ jsx("a", {
105
+ href: "#drift",
106
+ class: statLink,
107
+ children: /* @__PURE__ */ jsx(Stat, {
108
+ label: unavailable ? "Needs attention · Unavailable" : "Needs attention",
109
+ value: unavailable ? "–" : data.drifted.length,
110
+ tone: unavailable ? "neutral" : data.drifted.length > 0 ? "warning" : "success"
111
+ })
112
+ }),
113
+ /* @__PURE__ */ jsx("a", {
114
+ href: "#current",
115
+ class: statLink,
116
+ children: /* @__PURE__ */ jsx(Stat, {
117
+ label: "Up to date",
118
+ value: data.current,
119
+ tone: data.current > 0 ? "success" : "neutral"
120
+ })
121
+ })
122
+ ]
123
+ }),
124
+ /* @__PURE__ */ jsx(UnlinkedSection, { data }),
125
+ /* @__PURE__ */ jsx(DriftSection, { data }),
126
+ /* @__PURE__ */ jsx(CurrentSection, { data })
127
+ ] });
128
+ }
129
+ /** Where a component's design links are managed: its Design tab with testing, else the project's design settings. */
130
+ function linkTarget(data, component) {
131
+ const urls = createUrlBuilder("/", getStore().config.publishedBaseDomain);
132
+ return data.storyLinksRead ? urls.component(data.project.slug, component.importPath, "design") : `${urls.settings(data.project.slug)}/design`;
133
+ }
134
+ function ComponentLink(props) {
135
+ const { data, component } = props;
136
+ const urls = createUrlBuilder("/", getStore().config.publishedBaseDomain);
137
+ const href = data.storyLinksRead ? urls.component(data.project.slug, component.importPath, "design") : urls.component(data.project.slug, component.importPath);
138
+ return /* @__PURE__ */ jsx("a", {
139
+ href,
140
+ children: component.title
141
+ });
142
+ }
143
+ function UnlinkedRow(props) {
144
+ const { data, component } = props;
145
+ return /* @__PURE__ */ jsxs("tr", { children: [
146
+ /* @__PURE__ */ jsxs("td", { children: [/* @__PURE__ */ jsx(ComponentLink, {
147
+ data,
148
+ component
149
+ }), /* @__PURE__ */ jsx("div", {
150
+ class: narrowOnly,
151
+ children: /* @__PURE__ */ jsx(Meta, {
152
+ as: "span",
153
+ mono: true,
154
+ children: component.importPath
155
+ })
156
+ })] }),
157
+ /* @__PURE__ */ jsx("td", {
158
+ class: widePath,
159
+ children: /* @__PURE__ */ jsx("span", {
160
+ class: "mono",
161
+ children: component.importPath
162
+ })
163
+ }),
164
+ /* @__PURE__ */ jsx("td", {
165
+ class: wideOnly,
166
+ children: component.stories
167
+ }),
168
+ /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(Button, {
169
+ variant: "ghost",
170
+ size: "sm",
171
+ href: linkTarget(data, component),
172
+ "aria-label": `Link a design to ${component.title}`,
173
+ children: "Link a design"
174
+ }) })
175
+ ] });
176
+ }
177
+ function UnlinkedSection(props) {
178
+ const { data } = props;
179
+ const { unlinked } = data;
180
+ return /* @__PURE__ */ jsx(Card, {
181
+ id: "no-link",
182
+ children: /* @__PURE__ */ jsxs(VStack, { children: [/* @__PURE__ */ jsxs(SectionTitle, { children: [
183
+ "No design link (",
184
+ unlinked.length,
185
+ ")"
186
+ ] }), unlinked.length === 0 ? /* @__PURE__ */ jsx(EmptyState, { title: "Every component has a design link" }) : /* @__PURE__ */ jsx(Table, { children: /* @__PURE__ */ jsxs("table", { children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
187
+ /* @__PURE__ */ jsx("th", { children: "Component" }),
188
+ /* @__PURE__ */ jsx("th", {
189
+ class: wideOnly,
190
+ children: "Path"
191
+ }),
192
+ /* @__PURE__ */ jsx("th", {
193
+ class: wideOnly,
194
+ children: "Stories"
195
+ }),
196
+ /* @__PURE__ */ jsx("th", { children: /* @__PURE__ */ jsx("span", {
197
+ class: "visually-hidden",
198
+ children: "Action"
199
+ }) })
200
+ ] }) }), /* @__PURE__ */ jsx("tbody", { children: unlinked.map((component) => /* @__PURE__ */ jsx(UnlinkedRow, {
201
+ data,
202
+ component
203
+ }, component.importPath)) })] }) })] })
204
+ });
205
+ }
206
+ const REASONS = {
207
+ stale: {
208
+ badge: "Design changed",
209
+ title: "Design changed after approval"
210
+ },
211
+ deleted: {
212
+ badge: "Deleted",
213
+ title: "Design file deleted"
214
+ }
215
+ };
216
+ function DriftBadge(props) {
217
+ const reason = props.drift === "deleted" ? REASONS.deleted : REASONS.stale;
218
+ return /* @__PURE__ */ jsx("span", {
219
+ title: reason.title,
220
+ children: /* @__PURE__ */ jsx(Badge, {
221
+ tone: props.drift === "deleted" ? "danger" : "warning",
222
+ icon: true,
223
+ children: reason.badge
224
+ })
225
+ });
226
+ }
227
+ function ChangedAt(props) {
228
+ return props.value ? /* @__PURE__ */ jsx(RelativeTime, { value: props.value }) : /* @__PURE__ */ jsx(Meta, {
229
+ as: "span",
230
+ children: "unknown"
231
+ });
232
+ }
233
+ /** One row per drifted design link of a component. */
234
+ function DriftRows(props) {
235
+ const { data, component } = props;
236
+ return /* @__PURE__ */ jsx(Fragment, { children: component.links.filter((link) => link.drift === "stale" || link.drift === "deleted").map((link) => /* @__PURE__ */ jsxs("tr", { children: [
237
+ /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsxs(VStack, { children: [
238
+ /* @__PURE__ */ jsx(ComponentLink, {
239
+ data,
240
+ component
241
+ }),
242
+ /* @__PURE__ */ jsx(HStack, {
243
+ wrap: false,
244
+ children: /* @__PURE__ */ jsxs(Button, {
245
+ variant: "ghost",
246
+ size: "sm",
247
+ icon: "external-link",
248
+ href: link.url,
249
+ target: "_blank",
250
+ rel: "noopener noreferrer",
251
+ children: ["Open in ", link.providerName]
252
+ })
253
+ }),
254
+ link.name ? /* @__PURE__ */ jsx(Meta, {
255
+ as: "span",
256
+ children: link.name
257
+ }) : null
258
+ ] }) }),
259
+ /* @__PURE__ */ jsxs("td", { children: [/* @__PURE__ */ jsx(DriftBadge, { drift: link.drift }), /* @__PURE__ */ jsx("div", {
260
+ class: narrowOnly,
261
+ children: /* @__PURE__ */ jsx(Meta, {
262
+ as: "span",
263
+ children: /* @__PURE__ */ jsx(ChangedAt, { value: link.changedAt })
264
+ })
265
+ })] }),
266
+ /* @__PURE__ */ jsx("td", {
267
+ class: wideOnly,
268
+ children: /* @__PURE__ */ jsx(ChangedAt, { value: link.changedAt })
269
+ })
270
+ ] }, `${component.importPath}|${link.url}`)) });
271
+ }
272
+ /** Says which providers cannot be checked and where to add a token. */
273
+ function DriftAlert(props) {
274
+ const { project, withoutToken } = props.data;
275
+ const urls = createUrlBuilder("/", getStore().config.publishedBaseDomain);
276
+ return /* @__PURE__ */ jsxs(Alert, {
277
+ tone: "info",
278
+ title: "Drift can't be checked",
279
+ children: [
280
+ withoutToken.join(", "),
281
+ " has no access token for this project, so StoryShelf cannot see which designs changed. Add one in Settings → Design.",
282
+ /* @__PURE__ */ jsx("div", {
283
+ class: "mt-1",
284
+ children: /* @__PURE__ */ jsx(Button, {
285
+ variant: "secondary",
286
+ size: "sm",
287
+ href: `${urls.settings(project.slug)}/design`,
288
+ children: "Open design settings"
289
+ })
290
+ })
291
+ ]
292
+ });
293
+ }
294
+ function DriftTable(props) {
295
+ const { data } = props;
296
+ return /* @__PURE__ */ jsx(Table, { children: /* @__PURE__ */ jsxs("table", { children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
297
+ /* @__PURE__ */ jsx("th", { children: "Component" }),
298
+ /* @__PURE__ */ jsx("th", { children: "Reason" }),
299
+ /* @__PURE__ */ jsx("th", {
300
+ class: wideOnly,
301
+ children: "Design changed"
302
+ })
303
+ ] }) }), /* @__PURE__ */ jsx("tbody", { children: data.drifted.map((component) => /* @__PURE__ */ jsx(DriftRows, {
304
+ data,
305
+ component
306
+ }, component.importPath)) })] }) });
307
+ }
308
+ function DriftSection(props) {
309
+ const { data } = props;
310
+ const { drifted, withoutToken, lastChecked } = data;
311
+ const unavailable = driftUnavailable(data);
312
+ return /* @__PURE__ */ jsx(Card, {
313
+ id: "drift",
314
+ children: /* @__PURE__ */ jsxs(VStack, { children: [
315
+ /* @__PURE__ */ jsx(SectionTitle, { children: unavailable ? "Needs attention" : `Needs attention (${drifted.length})` }),
316
+ unavailable ? null : /* @__PURE__ */ jsx(Meta, { children: lastChecked ? /* @__PURE__ */ jsxs(Fragment, { children: ["Last checked ", /* @__PURE__ */ jsx(RelativeTime, { value: lastChecked })] }) : "Not checked yet" }),
317
+ withoutToken.length > 0 ? /* @__PURE__ */ jsx(DriftAlert, { data }) : null,
318
+ drifted.length > 0 ? /* @__PURE__ */ jsx(DriftTable, { data }) : null,
319
+ drifted.length === 0 && !unavailable ? /* @__PURE__ */ jsx(EmptyState, { title: "No drift found" }) : null
320
+ ] })
321
+ });
322
+ }
323
+ function CurrentSection(props) {
324
+ const { current, unknown } = props.data;
325
+ const unknownText = `${unknown} more ${unknown === 1 ? "has" : "have"} a link but nothing stored says whether the design changed.`;
326
+ return /* @__PURE__ */ jsx(Card, {
327
+ id: "current",
328
+ children: /* @__PURE__ */ jsxs(VStack, { children: [/* @__PURE__ */ jsx(SectionTitle, { children: "Up to date" }), /* @__PURE__ */ jsxs(Meta, { children: [current > 0 ? `${current} ${current === 1 ? "component is" : "components are"} linked and ${current === 1 ? "matches" : "match"} their approved design.` : unknown === 0 ? "No linked components yet." : "", unknown > 0 ? ` ${unknownText}` : ""] })] })
329
+ });
330
+ }
331
+ export { renderDesignCoveragePage };
332
+
333
+ //# sourceMappingURL=design-coverage.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"design-coverage.mjs","names":[],"sources":["../../src/pages/design-coverage.tsx"],"sourcesContent":["// oxlint-disable typescript/promise-function-async -- Hono JSX may return Promise<HtmlEscapedString>\nimport type { DesignDrift } from \"@storyshelf/core/models\";\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 Alert,\n Badge,\n Button,\n Card,\n EmptyState,\n HStack,\n Meta,\n PageHeader,\n RelativeTime,\n SectionTitle,\n Stat,\n Table,\n VStack,\n} from \"../ui/components.tsx\";\nimport { css } from \"../ui/css.ts\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\n\ntype Rendered = HtmlEscapedString | Promise<HtmlEscapedString>;\n\n/** One design link of a component, with what was stored about it. */\nexport interface CoverageLink {\n url: string;\n providerName: string;\n /** Design node name from the cache, when one is stored. */\n name: string | null;\n drift: DesignDrift;\n /** When the design last changed, as the provider reported it; null when unknown. */\n changedAt: string | null;\n}\n\n/** A component (story file) and the design links it resolves to. */\nexport interface CoverageComponent {\n title: string;\n importPath: string;\n /** How many stories the component has. */\n stories: number;\n links: CoverageLink[];\n}\n\n/** Everything the coverage page shows; built from stored data only (no provider calls). */\nexport interface DesignCoverageData {\n project: Project;\n /** The accepted build whose catalog was read; null when the project has none. */\n build: Build | null;\n /** Components with no design link. */\n unlinked: CoverageComponent[];\n /** Linked components whose design changed after the baseline was approved, or was deleted. */\n drifted: CoverageComponent[];\n /** Linked and up to date. */\n current: number;\n /** Linked, but nothing stored says whether the design changed (no token, or not fetched yet). */\n unknown: number;\n /** Providers whose links cannot be checked because the project has no token for them. */\n withoutToken: string[];\n /** When the linked designs' metadata was last fetched; null when it never was. */\n lastChecked: string | null;\n /** Whether links declared in stories were read (they need captured snapshots, so `testing`). */\n storyLinksRead: boolean;\n}\n\n/* Secondary columns drop out on phones; the date moves under the reason (see the spec on #458). */\nconst wideOnly = css`\n /* coverage-wide-only */\n @media (max-width: 640px) {\n & {\n display: none;\n }\n }\n`;\n\nconst narrowOnly = css`\n /* coverage-narrow-only */\n display: none;\n @media (max-width: 640px) {\n & {\n display: block;\n }\n }\n`;\n\nconst statLink = css`\n /* coverage-stat-link */\n display: block;\n color: inherit;\n text-decoration: none;\n &:hover,\n &:focus-visible {\n text-decoration: none;\n }\n`;\n\nconst widePath = css`\n /* coverage-wide-path */\n ${wideOnly}\n overflow-wrap: anywhere;\n`;\n\n/** The design coverage page: what lacks a design, what drifted, and how much is in sync. */\nexport function renderDesignCoveragePage(data: DesignCoverageData): RenderedContent {\n const { project, build } = data;\n const urls = createUrlBuilder(\"/\", getStore().config.publishedBaseDomain);\n return (\n <DocumentLayout\n title=\"Design coverage\"\n nav={{\n active: \"library\",\n projectSlug: project.slug,\n projectName: project.name,\n trail: [\n { label: \"Library\", href: urls.library(project.slug) },\n { label: \"Design coverage\" },\n ],\n }}\n >\n <PageHeader\n title=\"Design coverage\"\n description=\"Which components have a design file, and which have drifted from it.\"\n meta={\n build\n ? `Latest accepted build: ${build.gitBranch} · ${build.gitSha.slice(0, 7)}`\n : undefined\n }\n />\n {build ? (\n <CoverageBody data={data} />\n ) : (\n <EmptyState\n title=\"No build to check yet\"\n description=\"Design coverage is read from the latest accepted build. Upload a Storybook and accept a build first.\"\n action={\n <Button variant=\"secondary\" size=\"sm\" href={urls.buildsList(project.slug)}>\n Go to builds\n </Button>\n }\n />\n )}\n </DocumentLayout>\n );\n}\n\n/** Drift cannot be told for the linked designs: some provider has no token and nothing is known to have drifted. */\nfunction driftUnavailable(data: DesignCoverageData): boolean {\n return data.withoutToken.length > 0 && data.drifted.length === 0;\n}\n\nfunction CoverageBody(props: { data: DesignCoverageData }): Rendered {\n const { data } = props;\n const unavailable = driftUnavailable(data);\n return (\n <VStack>\n {data.storyLinksRead ? null : (\n <Alert tone=\"info\" title=\"Only mapped design links are counted\">\n Links declared in stories are read from captured builds, so they are not counted without\n testing. Components linked under Settings → Design are.\n </Alert>\n )}\n <div class=\"grid grid--3\">\n <a href=\"#no-link\" class={statLink}>\n <Stat\n label=\"No design link\"\n value={data.unlinked.length}\n tone={data.unlinked.length > 0 ? \"warning\" : \"success\"}\n />\n </a>\n <a href=\"#drift\" class={statLink}>\n <Stat\n label={unavailable ? \"Needs attention · Unavailable\" : \"Needs attention\"}\n value={unavailable ? \"–\" : data.drifted.length}\n tone={unavailable ? \"neutral\" : data.drifted.length > 0 ? \"warning\" : \"success\"}\n />\n </a>\n <a href=\"#current\" class={statLink}>\n <Stat\n label=\"Up to date\"\n value={data.current}\n tone={data.current > 0 ? \"success\" : \"neutral\"}\n />\n </a>\n </div>\n <UnlinkedSection data={data} />\n <DriftSection data={data} />\n <CurrentSection data={data} />\n </VStack>\n );\n}\n\n/** Where a component's design links are managed: its Design tab with testing, else the project's design settings. */\nfunction linkTarget(data: DesignCoverageData, component: CoverageComponent): string {\n const urls = createUrlBuilder(\"/\", getStore().config.publishedBaseDomain);\n return data.storyLinksRead\n ? urls.component(data.project.slug, component.importPath, \"design\")\n : `${urls.settings(data.project.slug)}/design`;\n}\n\nfunction ComponentLink(props: {\n data: DesignCoverageData;\n component: CoverageComponent;\n}): Rendered {\n const { data, component } = props;\n const urls = createUrlBuilder(\"/\", getStore().config.publishedBaseDomain);\n const href = data.storyLinksRead\n ? urls.component(data.project.slug, component.importPath, \"design\")\n : urls.component(data.project.slug, component.importPath);\n return <a href={href}>{component.title}</a>;\n}\n\nfunction UnlinkedRow(props: { data: DesignCoverageData; component: CoverageComponent }): Rendered {\n const { data, component } = props;\n return (\n <tr>\n <td>\n <ComponentLink data={data} component={component} />\n <div class={narrowOnly}>\n <Meta as=\"span\" mono>\n {component.importPath}\n </Meta>\n </div>\n </td>\n <td class={widePath}>\n <span class=\"mono\">{component.importPath}</span>\n </td>\n <td class={wideOnly}>{component.stories}</td>\n <td>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n href={linkTarget(data, component)}\n aria-label={`Link a design to ${component.title}`}\n >\n Link a design\n </Button>\n </td>\n </tr>\n );\n}\n\nfunction UnlinkedSection(props: { data: DesignCoverageData }): Rendered {\n const { data } = props;\n const { unlinked } = data;\n return (\n <Card id=\"no-link\">\n <VStack>\n <SectionTitle>No design link ({unlinked.length})</SectionTitle>\n {unlinked.length === 0 ? (\n <EmptyState title=\"Every component has a design link\" />\n ) : (\n <Table>\n <table>\n <thead>\n <tr>\n <th>Component</th>\n <th class={wideOnly}>Path</th>\n <th class={wideOnly}>Stories</th>\n <th>\n <span class=\"visually-hidden\">Action</span>\n </th>\n </tr>\n </thead>\n <tbody>\n {unlinked.map((component) => (\n <UnlinkedRow key={component.importPath} data={data} component={component} />\n ))}\n </tbody>\n </table>\n </Table>\n )}\n </VStack>\n </Card>\n );\n}\n\nconst REASONS: Record<\"stale\" | \"deleted\", { badge: string; title: string }> = {\n stale: { badge: \"Design changed\", title: \"Design changed after approval\" },\n deleted: { badge: \"Deleted\", title: \"Design file deleted\" },\n};\n\nfunction DriftBadge(props: { drift: DesignDrift }): Rendered {\n const reason = props.drift === \"deleted\" ? REASONS.deleted : REASONS.stale;\n return (\n <span title={reason.title}>\n <Badge tone={props.drift === \"deleted\" ? \"danger\" : \"warning\"} icon>\n {reason.badge}\n </Badge>\n </span>\n );\n}\n\nfunction ChangedAt(props: { value: string | null }): Rendered {\n return props.value ? <RelativeTime value={props.value} /> : <Meta as=\"span\">unknown</Meta>;\n}\n\n/** One row per drifted design link of a component. */\nfunction DriftRows(props: { data: DesignCoverageData; component: CoverageComponent }): Rendered {\n const { data, component } = props;\n return (\n <>\n {component.links\n .filter((link) => link.drift === \"stale\" || link.drift === \"deleted\")\n .map((link) => (\n <tr key={`${component.importPath}|${link.url}`}>\n <td>\n <VStack>\n <ComponentLink data={data} component={component} />\n <HStack wrap={false}>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n icon=\"external-link\"\n href={link.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n Open in {link.providerName}\n </Button>\n </HStack>\n {link.name ? <Meta as=\"span\">{link.name}</Meta> : null}\n </VStack>\n </td>\n <td>\n <DriftBadge drift={link.drift} />\n <div class={narrowOnly}>\n <Meta as=\"span\">\n <ChangedAt value={link.changedAt} />\n </Meta>\n </div>\n </td>\n <td class={wideOnly}>\n <ChangedAt value={link.changedAt} />\n </td>\n </tr>\n ))}\n </>\n );\n}\n\n/** Says which providers cannot be checked and where to add a token. */\nfunction DriftAlert(props: { data: DesignCoverageData }): Rendered {\n const { project, withoutToken } = props.data;\n const urls = createUrlBuilder(\"/\", getStore().config.publishedBaseDomain);\n return (\n <Alert tone=\"info\" title=\"Drift can't be checked\">\n {withoutToken.join(\", \")} has no access token for this project, so StoryShelf cannot see which\n designs changed. Add one in Settings → Design.\n <div class=\"mt-1\">\n <Button variant=\"secondary\" size=\"sm\" href={`${urls.settings(project.slug)}/design`}>\n Open design settings\n </Button>\n </div>\n </Alert>\n );\n}\n\nfunction DriftTable(props: { data: DesignCoverageData }): Rendered {\n const { data } = props;\n return (\n <Table>\n <table>\n <thead>\n <tr>\n <th>Component</th>\n <th>Reason</th>\n <th class={wideOnly}>Design changed</th>\n </tr>\n </thead>\n <tbody>\n {data.drifted.map((component) => (\n <DriftRows key={component.importPath} data={data} component={component} />\n ))}\n </tbody>\n </table>\n </Table>\n );\n}\n\nfunction DriftSection(props: { data: DesignCoverageData }): Rendered {\n const { data } = props;\n const { drifted, withoutToken, lastChecked } = data;\n const unavailable = driftUnavailable(data);\n return (\n <Card id=\"drift\">\n <VStack>\n <SectionTitle>\n {unavailable ? \"Needs attention\" : `Needs attention (${drifted.length})`}\n </SectionTitle>\n {unavailable ? null : (\n <Meta>\n {lastChecked ? (\n <>\n Last checked <RelativeTime value={lastChecked} />\n </>\n ) : (\n \"Not checked yet\"\n )}\n </Meta>\n )}\n {withoutToken.length > 0 ? <DriftAlert data={data} /> : null}\n {drifted.length > 0 ? <DriftTable data={data} /> : null}\n {drifted.length === 0 && !unavailable ? <EmptyState title=\"No drift found\" /> : null}\n </VStack>\n </Card>\n );\n}\n\nfunction CurrentSection(props: { data: DesignCoverageData }): Rendered {\n const { current, unknown } = props.data;\n const unknownText = `${unknown} more ${unknown === 1 ? \"has\" : \"have\"} a link but nothing stored says whether the design changed.`;\n return (\n <Card id=\"current\">\n <VStack>\n <SectionTitle>Up to date</SectionTitle>\n <Meta>\n {current > 0\n ? `${current} ${current === 1 ? \"component is\" : \"components are\"} linked and ${current === 1 ? \"matches\" : \"match\"} their approved design.`\n : unknown === 0\n ? \"No linked components yet.\"\n : \"\"}\n {unknown > 0 ? ` ${unknownText}` : \"\"}\n </Meta>\n </VStack>\n </Card>\n );\n}\n"],"mappings":"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"}
@@ -203,6 +203,12 @@ function LibraryHeader(props) {
203
203
  href: urls.short(build.id),
204
204
  children: "View Storybook"
205
205
  }),
206
+ getStore().design.length > 0 ? /* @__PURE__ */ jsx(Button, {
207
+ variant: "ghost",
208
+ size: "sm",
209
+ href: urls.designCoverage(project.slug),
210
+ children: "Design coverage"
211
+ }) : null,
206
212
  /* @__PURE__ */ jsx(BranchPicker, {
207
213
  project,
208
214
  build,
@@ -1 +1 @@
1
- {"version":3,"file":"library.mjs","names":[],"sources":["../../src/pages/library.tsx"],"sourcesContent":["// oxlint-disable typescript/promise-function-async -- Hono JSX may return Promise<HtmlEscapedString>\nimport { BuildModel, ProjectModel, SnapshotModel } from \"@storyshelf/core/models\";\nimport type { Build, Project } from \"@storyshelf/core/schema\";\nimport { createUrlBuilder } from \"@storyshelf/core/urls\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport { loadHealthPanel, type HealthPanelData } from \"../insights/health-panel.ts\";\nimport { catalogOf, getStore, testingEnabled } from \"../store.ts\";\nimport {\n Button,\n EmptyState,\n FilterInput,\n HStack,\n Meta,\n PageHeader,\n SelectField,\n statusTone,\n Badge,\n} from \"../ui/components.tsx\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\nimport { ProjectDesignLinks } from \"./design-links.tsx\";\nimport { HealthSlot } from \"./health-panel.tsx\";\nimport { LibraryGrid } from \"./library-grid.tsx\";\nimport { entriesFromSnapshots, groupLibrary, type LibraryGroup } from \"./library-model.ts\";\nimport { loadSnapshotLayer, type SnapshotLayer } from \"./library-snapshots.tsx\";\n\ntype Rendered = HtmlEscapedString | Promise<HtmlEscapedString>;\n\n/**\n * Library page (ADR 0006, decision 9): the components of the latest build of the selected branch,\n * whatever its status, from the build's Storybook catalog. With `testing` the cards gain\n * screenshots, statuses and design links, and the page the health panel.\n */\nexport async function renderLibraryPage(\n slug: string,\n branch?: string,\n): Promise<RenderedContent | null> {\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) return null;\n const build = await getLibraryBuild(project, branch);\n if (!build) return renderEmptyLibrary(project);\n const data = await loadLibraryData(project, build);\n if (data.groups.length === 0) return renderNoStories(project, build);\n return (\n <DocumentLayout\n title=\"Library\"\n nav={{ active: \"library\", projectSlug: project.slug, projectName: project.name }}\n >\n <LibraryHeader\n project={project}\n build={build}\n components={data.groups.length}\n stories={data.groups.reduce((total, group) => total + group.stories.length, 0)}\n branches={data.branches}\n />\n {data.layer && data.layer.design.projectLinks.length > 0 ? (\n <ProjectDesignLinks links={data.layer.design.projectLinks} />\n ) : null}\n <HealthSlot project={project} data={data.health} />\n <LibraryGrid groups={data.groups} project={project} build={build} layer={data.layer} />\n <div data-filter-empty hidden>\n <Meta center>No stories match your filter.</Meta>\n </div>\n </DocumentLayout>\n );\n}\n\n/** Everything the library renders of a build: catalog groups, branches, and the `testing` layer. */\ninterface LibraryData {\n groups: LibraryGroup[];\n branches: string[];\n layer: SnapshotLayer | null;\n health: HealthPanelData | null;\n}\n\nasync function loadLibraryData(project: Project, build: Build): Promise<LibraryData> {\n const testing = testingEnabled();\n const [catalog, snapshots, branches] = await Promise.all([\n catalogOf().load(project.id, build.id),\n testing ? new SnapshotModel(getStore().db).listByBuild(build.id) : Promise.resolve([]),\n distinctBranches(project.id),\n ]);\n const entries = catalog?.entries ?? entriesFromSnapshots(snapshots);\n return {\n groups: groupLibrary(entries),\n branches,\n layer: snapshots.length > 0 ? await loadSnapshotLayer(project, snapshots) : null,\n health: testing ? await loadHealthPanel(project) : null,\n };\n}\n\n/** Latest build of the branch (default branch when none is chosen), whatever its status. */\nasync function getLibraryBuild(project: Project, branch?: string): Promise<Build | null> {\n const buildModel = new BuildModel(getStore().db);\n const trimmed = branch?.trim();\n const target = trimmed === undefined || trimmed === \"\" ? project.gitDefaultBranch : trimmed;\n const byBranch = await buildModel.list(project.id, { branch: target, limit: 1 });\n if (byBranch[0]) return byBranch[0];\n if (target !== project.gitDefaultBranch) {\n const defaults = await buildModel.list(project.id, {\n branch: project.gitDefaultBranch,\n limit: 1,\n });\n if (defaults[0]) return defaults[0];\n }\n return (await buildModel.list(project.id, { limit: 1 }))[0] ?? null;\n}\n\nasync function distinctBranches(projectId: string): Promise<string[]> {\n const buildsList = await new BuildModel(getStore().db).list(projectId);\n return [...new Set(buildsList.map((b) => b.gitBranch))].toSorted((a, b) => a.localeCompare(b));\n}\n\nfunction renderEmptyLibrary(project: Project): RenderedContent {\n return (\n <DocumentLayout\n title=\"Library\"\n nav={{ active: \"library\", projectSlug: project.slug, projectName: project.name }}\n >\n <PageHeader\n title=\"Library\"\n description={\n <>All stories/components from the latest build on {project.gitDefaultBranch}.</>\n }\n />\n <EmptyState title=\"No builds yet\" description=\"Upload a build to populate the library.\" />\n </DocumentLayout>\n );\n}\n\n/** A build with nothing to list: still publishing, failed, or its Storybook is gone. */\nfunction renderNoStories(project: Project, build: Build): RenderedContent {\n const urls = createUrlBuilder(\"/\", getStore().config.publishedBaseDomain);\n const preparing = build.status === \"pending\";\n return (\n <DocumentLayout\n title=\"Library\"\n nav={{ active: \"library\", projectSlug: project.slug, projectName: project.name }}\n >\n <PageHeader\n title=\"Library\"\n description={\n <>\n Latest build {build.gitBranch} · {build.gitSha.slice(0, 7)} · {build.status}\n </>\n }\n />\n <EmptyState\n title={preparing ? \"Preparing the library\" : \"No stories in the latest build\"}\n description={\n preparing\n ? \"The latest build is still being published. Reload in a moment.\"\n : \"The latest build has no readable Storybook index, so there is nothing to list.\"\n }\n action={\n <Button variant=\"secondary\" href={urls.build(project.slug, build.id)}>\n View build\n </Button>\n }\n />\n </DocumentLayout>\n );\n}\n\nfunction LibraryHeader(props: {\n project: Project;\n build: Build;\n components: number;\n stories: number;\n branches: string[];\n}): Rendered {\n const { project, build } = props;\n const urls = createUrlBuilder(\"/\", getStore().config.publishedBaseDomain);\n return (\n <PageHeader\n title=\"Library\"\n description={\n <>\n {props.components} {props.components === 1 ? \"component\" : \"components\"} · {props.stories}{\" \"}\n {props.stories === 1 ? \"story\" : \"stories\"} · Latest build{\" \"}\n <a href={urls.build(project.slug, build.id)}>\n {build.gitBranch} · {build.gitSha.slice(0, 7)}\n </a>{\" \"}\n · {new Date(build.createdAt).toLocaleString()}{\" \"}\n <Badge tone={statusTone(build.status)}>{build.status}</Badge>\n </>\n }\n actions={\n <HStack>\n <FilterInput label=\"Filter stories\" placeholder=\"Filter stories…\" />\n <Button variant=\"ghost\" size=\"sm\" icon=\"external-link\" href={urls.short(build.id)}>\n View Storybook\n </Button>\n <BranchPicker project={project} build={build} branches={props.branches} />\n </HStack>\n }\n />\n );\n}\n\nfunction BranchPicker(props: { project: Project; build: Build; branches: string[] }): Rendered {\n return (\n <form method=\"get\" action={`/projects/${props.project.slug}/library`}>\n <HStack>\n <SelectField\n label=\"Branch\"\n name=\"branch\"\n layout=\"inline\"\n value={props.build.gitBranch}\n options={props.branches.map((b) => ({ value: b, label: b }))}\n />\n <Button variant=\"secondary\" type=\"submit\">\n View\n </Button>\n </HStack>\n </form>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"library.mjs","names":[],"sources":["../../src/pages/library.tsx"],"sourcesContent":["// oxlint-disable typescript/promise-function-async -- Hono JSX may return Promise<HtmlEscapedString>\nimport { BuildModel, ProjectModel, SnapshotModel } from \"@storyshelf/core/models\";\nimport type { Build, Project } from \"@storyshelf/core/schema\";\nimport { createUrlBuilder } from \"@storyshelf/core/urls\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport { loadHealthPanel, type HealthPanelData } from \"../insights/health-panel.ts\";\nimport { catalogOf, getStore, testingEnabled } from \"../store.ts\";\nimport {\n Button,\n EmptyState,\n FilterInput,\n HStack,\n Meta,\n PageHeader,\n SelectField,\n statusTone,\n Badge,\n} from \"../ui/components.tsx\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\nimport { ProjectDesignLinks } from \"./design-links.tsx\";\nimport { HealthSlot } from \"./health-panel.tsx\";\nimport { LibraryGrid } from \"./library-grid.tsx\";\nimport { entriesFromSnapshots, groupLibrary, type LibraryGroup } from \"./library-model.ts\";\nimport { loadSnapshotLayer, type SnapshotLayer } from \"./library-snapshots.tsx\";\n\ntype Rendered = HtmlEscapedString | Promise<HtmlEscapedString>;\n\n/**\n * Library page (ADR 0006, decision 9): the components of the latest build of the selected branch,\n * whatever its status, from the build's Storybook catalog. With `testing` the cards gain\n * screenshots, statuses and design links, and the page the health panel.\n */\nexport async function renderLibraryPage(\n slug: string,\n branch?: string,\n): Promise<RenderedContent | null> {\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) return null;\n const build = await getLibraryBuild(project, branch);\n if (!build) return renderEmptyLibrary(project);\n const data = await loadLibraryData(project, build);\n if (data.groups.length === 0) return renderNoStories(project, build);\n return (\n <DocumentLayout\n title=\"Library\"\n nav={{ active: \"library\", projectSlug: project.slug, projectName: project.name }}\n >\n <LibraryHeader\n project={project}\n build={build}\n components={data.groups.length}\n stories={data.groups.reduce((total, group) => total + group.stories.length, 0)}\n branches={data.branches}\n />\n {data.layer && data.layer.design.projectLinks.length > 0 ? (\n <ProjectDesignLinks links={data.layer.design.projectLinks} />\n ) : null}\n <HealthSlot project={project} data={data.health} />\n <LibraryGrid groups={data.groups} project={project} build={build} layer={data.layer} />\n <div data-filter-empty hidden>\n <Meta center>No stories match your filter.</Meta>\n </div>\n </DocumentLayout>\n );\n}\n\n/** Everything the library renders of a build: catalog groups, branches, and the `testing` layer. */\ninterface LibraryData {\n groups: LibraryGroup[];\n branches: string[];\n layer: SnapshotLayer | null;\n health: HealthPanelData | null;\n}\n\nasync function loadLibraryData(project: Project, build: Build): Promise<LibraryData> {\n const testing = testingEnabled();\n const [catalog, snapshots, branches] = await Promise.all([\n catalogOf().load(project.id, build.id),\n testing ? new SnapshotModel(getStore().db).listByBuild(build.id) : Promise.resolve([]),\n distinctBranches(project.id),\n ]);\n const entries = catalog?.entries ?? entriesFromSnapshots(snapshots);\n return {\n groups: groupLibrary(entries),\n branches,\n layer: snapshots.length > 0 ? await loadSnapshotLayer(project, snapshots) : null,\n health: testing ? await loadHealthPanel(project) : null,\n };\n}\n\n/** Latest build of the branch (default branch when none is chosen), whatever its status. */\nasync function getLibraryBuild(project: Project, branch?: string): Promise<Build | null> {\n const buildModel = new BuildModel(getStore().db);\n const trimmed = branch?.trim();\n const target = trimmed === undefined || trimmed === \"\" ? project.gitDefaultBranch : trimmed;\n const byBranch = await buildModel.list(project.id, { branch: target, limit: 1 });\n if (byBranch[0]) return byBranch[0];\n if (target !== project.gitDefaultBranch) {\n const defaults = await buildModel.list(project.id, {\n branch: project.gitDefaultBranch,\n limit: 1,\n });\n if (defaults[0]) return defaults[0];\n }\n return (await buildModel.list(project.id, { limit: 1 }))[0] ?? null;\n}\n\nasync function distinctBranches(projectId: string): Promise<string[]> {\n const buildsList = await new BuildModel(getStore().db).list(projectId);\n return [...new Set(buildsList.map((b) => b.gitBranch))].toSorted((a, b) => a.localeCompare(b));\n}\n\nfunction renderEmptyLibrary(project: Project): RenderedContent {\n return (\n <DocumentLayout\n title=\"Library\"\n nav={{ active: \"library\", projectSlug: project.slug, projectName: project.name }}\n >\n <PageHeader\n title=\"Library\"\n description={\n <>All stories/components from the latest build on {project.gitDefaultBranch}.</>\n }\n />\n <EmptyState title=\"No builds yet\" description=\"Upload a build to populate the library.\" />\n </DocumentLayout>\n );\n}\n\n/** A build with nothing to list: still publishing, failed, or its Storybook is gone. */\nfunction renderNoStories(project: Project, build: Build): RenderedContent {\n const urls = createUrlBuilder(\"/\", getStore().config.publishedBaseDomain);\n const preparing = build.status === \"pending\";\n return (\n <DocumentLayout\n title=\"Library\"\n nav={{ active: \"library\", projectSlug: project.slug, projectName: project.name }}\n >\n <PageHeader\n title=\"Library\"\n description={\n <>\n Latest build {build.gitBranch} · {build.gitSha.slice(0, 7)} · {build.status}\n </>\n }\n />\n <EmptyState\n title={preparing ? \"Preparing the library\" : \"No stories in the latest build\"}\n description={\n preparing\n ? \"The latest build is still being published. Reload in a moment.\"\n : \"The latest build has no readable Storybook index, so there is nothing to list.\"\n }\n action={\n <Button variant=\"secondary\" href={urls.build(project.slug, build.id)}>\n View build\n </Button>\n }\n />\n </DocumentLayout>\n );\n}\n\nfunction LibraryHeader(props: {\n project: Project;\n build: Build;\n components: number;\n stories: number;\n branches: string[];\n}): Rendered {\n const { project, build } = props;\n const urls = createUrlBuilder(\"/\", getStore().config.publishedBaseDomain);\n return (\n <PageHeader\n title=\"Library\"\n description={\n <>\n {props.components} {props.components === 1 ? \"component\" : \"components\"} · {props.stories}{\" \"}\n {props.stories === 1 ? \"story\" : \"stories\"} · Latest build{\" \"}\n <a href={urls.build(project.slug, build.id)}>\n {build.gitBranch} · {build.gitSha.slice(0, 7)}\n </a>{\" \"}\n · {new Date(build.createdAt).toLocaleString()}{\" \"}\n <Badge tone={statusTone(build.status)}>{build.status}</Badge>\n </>\n }\n actions={\n <HStack>\n <FilterInput label=\"Filter stories\" placeholder=\"Filter stories…\" />\n <Button variant=\"ghost\" size=\"sm\" icon=\"external-link\" href={urls.short(build.id)}>\n View Storybook\n </Button>\n {getStore().design.length > 0 ? (\n <Button variant=\"ghost\" size=\"sm\" href={urls.designCoverage(project.slug)}>\n Design coverage\n </Button>\n ) : null}\n <BranchPicker project={project} build={build} branches={props.branches} />\n </HStack>\n }\n />\n );\n}\n\nfunction BranchPicker(props: { project: Project; build: Build; branches: string[] }): Rendered {\n return (\n <form method=\"get\" action={`/projects/${props.project.slug}/library`}>\n <HStack>\n <SelectField\n label=\"Branch\"\n name=\"branch\"\n layout=\"inline\"\n value={props.build.gitBranch}\n options={props.branches.map((b) => ({ value: b, label: b }))}\n />\n <Button variant=\"secondary\" type=\"submit\">\n View\n </Button>\n </HStack>\n </form>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,116 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { credentialKeys } from "../credential-keys.mjs";
5
+ import { getStore, testingEnabled } from "../store.mjs";
6
+ import { viewerIdOf } from "./design-connect.handlers.mjs";
7
+ import { nodeModel, providerNameFor } from "./design.handlers.mjs";
8
+ import { groupLibrary } from "../pages/library-model.mjs";
9
+ import { BaselineModel, DesignCredentialModel, DesignModel, designDrift, normalizeImportPath, resolveDesigns } from "@storyshelf/core/models";
10
+ import { parseDesignUrl } from "@storyshelf/core/adapter/design";
11
+ // src/routers/design-coverage.handlers.ts
12
+ /** Order of severity when a component links several designs: the worst one speaks for it. */
13
+ const SEVERITY = [
14
+ "deleted",
15
+ "stale",
16
+ "unknown",
17
+ "current"
18
+ ];
19
+ /** Coverage of a build's components: what lacks a design link, what drifted, and what is in sync. */
20
+ async function coverageOf(project, build, entries, snapshots) {
21
+ const { db, design } = getStore();
22
+ const manual = await new DesignModel(db, design).componentLinks(project.id);
23
+ const resolutions = resolveDesigns(manual, snapshots);
24
+ const components = groupLibrary(entries).filter((group) => group.importPath !== "");
25
+ const urls = [...new Set([...resolutions.values()].flatMap((resolution) => resolution.urls))];
26
+ const [cached, withToken] = await Promise.all([nodeModel().cachedFor(project.id, urls, viewerIdOf()).catch(() => /* @__PURE__ */ new Map()), providersWithToken(project, urls)]);
27
+ const baselines = await approvedVersions(project, entries);
28
+ return {
29
+ ...summarize(project, build, components.map((group) => {
30
+ const key = normalizeImportPath(group.importPath);
31
+ const baseline = baselines.get(key) ?? null;
32
+ const links = (resolutions.get(key)?.urls ?? []).map((url) => linkOf(url, cached.get(url), baseline, withToken));
33
+ return {
34
+ title: group.title,
35
+ importPath: group.importPath,
36
+ stories: group.stories.length,
37
+ links
38
+ };
39
+ }), urls, withToken),
40
+ lastChecked: newestFetch(urls.flatMap((url) => cached.get(url)?.fetchedAt ?? [])),
41
+ storyLinksRead: testingEnabled()
42
+ };
43
+ }
44
+ function linkOf(url, node, baseline, withToken) {
45
+ const kind = parseDesignUrl(getStore().design, url)?.provider.metadata.kind ?? "";
46
+ const drift = withToken.has(kind) ? designDrift(node, baseline) : "unknown";
47
+ return {
48
+ url,
49
+ providerName: providerNameFor(url),
50
+ name: node?.name ?? null,
51
+ drift,
52
+ changedAt: node?.lastModified ?? null
53
+ };
54
+ }
55
+ /**
56
+ * How far each component's approval reaches: the OLDEST `updatedAt` among the current
57
+ * default-branch baselines of its stories (every viewport and browser), by normalized story file.
58
+ * A design edited after any of those screenshots was approved has drifted from it, so the oldest
59
+ * decides. A snapshot's own `baselineVersion` is the baseline it was diffed against, which
60
+ * promotion has since replaced. Without testing there are no baselines to compare with.
61
+ */
62
+ async function approvedVersions(project, entries) {
63
+ if (!testingEnabled()) return /* @__PURE__ */ new Map();
64
+ return oldestPerFile((await new BaselineModel(getStore().db).list(project.id)).filter((baseline) => baseline.branch === project.gitDefaultBranch), entries);
65
+ }
66
+ /** The oldest `updatedAt` of the given baselines, by the normalized story file of their story. */
67
+ function oldestPerFile(baselines, entries) {
68
+ const fileOf = new Map(entries.map((entry) => [entry.id, normalizeImportPath(entry.importPath)]));
69
+ const oldest = /* @__PURE__ */ new Map();
70
+ for (const baseline of baselines) {
71
+ const key = fileOf.get(baseline.storyId);
72
+ const known = key === void 0 ? void 0 : oldest.get(key);
73
+ if (key !== void 0 && (known === void 0 || baseline.updatedAt < known)) oldest.set(key, baseline.updatedAt);
74
+ }
75
+ return oldest;
76
+ }
77
+ /** Provider kinds, among those the links use, that the project has a token for. */
78
+ async function providersWithToken(project, urls) {
79
+ const credentials = new DesignCredentialModel(getStore().db, credentialKeys(getStore().config));
80
+ const kinds = new Set(urls.flatMap((url) => parseDesignUrl(getStore().design, url)?.provider.metadata.kind ?? []));
81
+ const have = await Promise.all([...kinds].map(async (kind) => await credentials.info(project.id, kind) ? kind : null));
82
+ return new Set(have.flatMap((kind) => kind === null ? [] : [kind]));
83
+ }
84
+ /** The most recent time any of the linked designs' metadata was fetched, or null when none was. */
85
+ function newestFetch(times) {
86
+ return times.toSorted().at(-1) ?? null;
87
+ }
88
+ /** The newest `changedAt` among a component's drifted links (0 when none is known). */
89
+ function changedMs(component) {
90
+ const times = component.links.flatMap((link) => link.changedAt ? [Date.parse(link.changedAt)] : []);
91
+ return Math.max(0, ...times.filter((time) => Number.isFinite(time)));
92
+ }
93
+ const deletedFirst = (row) => worst(row.links) === "deleted" ? 0 : 1;
94
+ /** Deleted designs first, then the most recently changed. */
95
+ function byUrgency(a, b) {
96
+ return deletedFirst(a) - deletedFirst(b) || changedMs(b) - changedMs(a);
97
+ }
98
+ function worst(links) {
99
+ return SEVERITY.find((drift) => links.some((link) => link.drift === drift)) ?? "unknown";
100
+ }
101
+ function summarize(project, build, rows, urls, withToken) {
102
+ const linked = rows.filter((row) => row.links.length > 0);
103
+ const kinds = [...new Set(urls.map((url) => parseDesignUrl(getStore().design, url)?.provider))];
104
+ return {
105
+ project,
106
+ build,
107
+ unlinked: rows.filter((row) => row.links.length === 0).toSorted((a, b) => a.importPath.localeCompare(b.importPath)),
108
+ drifted: linked.filter((row) => ["deleted", "stale"].includes(worst(row.links))).toSorted(byUrgency),
109
+ current: linked.filter((row) => worst(row.links) === "current").length,
110
+ unknown: linked.filter((row) => worst(row.links) === "unknown").length,
111
+ withoutToken: kinds.flatMap((provider) => provider && !withToken.has(provider.metadata.kind) ? [provider.metadata.name] : [])
112
+ };
113
+ }
114
+ export { coverageOf };
115
+
116
+ //# sourceMappingURL=design-coverage.handlers.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"design-coverage.handlers.mjs","names":[],"sources":["../../src/routers/design-coverage.handlers.ts"],"sourcesContent":["import { parseDesignUrl } from \"@storyshelf/core/adapter/design\";\nimport type { CatalogEntry } from \"@storyshelf/core/capture\";\nimport {\n BaselineModel,\n DesignCredentialModel,\n DesignModel,\n designDrift,\n normalizeImportPath,\n resolveDesigns,\n type DesignDrift,\n} from \"@storyshelf/core/models\";\nimport type { Baseline, Build, DesignNode, Project, Snapshot } from \"@storyshelf/core/schema\";\nimport { credentialKeys } from \"../credential-keys.ts\";\nimport type {\n CoverageComponent,\n CoverageLink,\n DesignCoverageData,\n} from \"../pages/design-coverage.tsx\";\nimport { groupLibrary } from \"../pages/library-model.ts\";\nimport { getStore, testingEnabled } from \"../store.ts\";\nimport { viewerIdOf } from \"./design-connect.handlers.ts\";\nimport { nodeModel, providerNameFor } from \"./design.handlers.ts\";\n\n/** Order of severity when a component links several designs: the worst one speaks for it. */\nconst SEVERITY: readonly DesignDrift[] = [\"deleted\", \"stale\", \"unknown\", \"current\"];\n\n/** Coverage of a build's components: what lacks a design link, what drifted, and what is in sync. */\nexport async function coverageOf(\n project: Project,\n build: Build,\n entries: readonly CatalogEntry[],\n snapshots: readonly Snapshot[],\n): Promise<DesignCoverageData> {\n const { db, design } = getStore();\n const manual = await new DesignModel(db, design).componentLinks(project.id);\n const resolutions = resolveDesigns(manual, snapshots);\n const components = groupLibrary(entries).filter((group) => group.importPath !== \"\");\n const urls = [...new Set([...resolutions.values()].flatMap((resolution) => resolution.urls))];\n const [cached, withToken] = await Promise.all([\n nodeModel()\n .cachedFor(project.id, urls, viewerIdOf())\n .catch(() => new Map<string, DesignNode>()),\n providersWithToken(project, urls),\n ]);\n const baselines = await approvedVersions(project, entries);\n const rows = components.map((group) => {\n const key = normalizeImportPath(group.importPath);\n const baseline = baselines.get(key) ?? null;\n const links = (resolutions.get(key)?.urls ?? []).map((url): CoverageLink =>\n linkOf(url, cached.get(url), baseline, withToken),\n );\n return {\n title: group.title,\n importPath: group.importPath,\n stories: group.stories.length,\n links,\n };\n });\n return {\n ...summarize(project, build, rows, urls, withToken),\n lastChecked: newestFetch(urls.flatMap((url) => cached.get(url)?.fetchedAt ?? [])),\n storyLinksRead: testingEnabled(),\n };\n}\n\nfunction linkOf(\n url: string,\n node: DesignNode | undefined,\n baseline: string | null,\n withToken: ReadonlySet<string>,\n): CoverageLink {\n const kind = parseDesignUrl(getStore().design, url)?.provider.metadata.kind ?? \"\";\n // Without a project token nothing says whether the design changed, whatever a member's own\n // connection cached: report it as unknown rather than guess.\n const drift = withToken.has(kind) ? designDrift(node, baseline) : \"unknown\";\n return {\n url,\n providerName: providerNameFor(url),\n name: node?.name ?? null,\n drift,\n changedAt: node?.lastModified ?? null,\n };\n}\n\n/**\n * How far each component's approval reaches: the OLDEST `updatedAt` among the current\n * default-branch baselines of its stories (every viewport and browser), by normalized story file.\n * A design edited after any of those screenshots was approved has drifted from it, so the oldest\n * decides. A snapshot's own `baselineVersion` is the baseline it was diffed against, which\n * promotion has since replaced. Without testing there are no baselines to compare with.\n */\nasync function approvedVersions(\n project: Project,\n entries: readonly CatalogEntry[],\n): Promise<Map<string, string>> {\n if (!testingEnabled()) return new Map();\n const baselines = await new BaselineModel(getStore().db).list(project.id);\n return oldestPerFile(\n baselines.filter((baseline) => baseline.branch === project.gitDefaultBranch),\n entries,\n );\n}\n\n/** The oldest `updatedAt` of the given baselines, by the normalized story file of their story. */\nfunction oldestPerFile(\n baselines: readonly Baseline[],\n entries: readonly CatalogEntry[],\n): Map<string, string> {\n const fileOf = new Map(\n entries.map((entry) => [entry.id, normalizeImportPath(entry.importPath)] as const),\n );\n const oldest = new Map<string, string>();\n for (const baseline of baselines) {\n const key = fileOf.get(baseline.storyId);\n const known = key === undefined ? undefined : oldest.get(key);\n if (key !== undefined && (known === undefined || baseline.updatedAt < known)) {\n oldest.set(key, baseline.updatedAt);\n }\n }\n return oldest;\n}\n\n/** Provider kinds, among those the links use, that the project has a token for. */\nasync function providersWithToken(project: Project, urls: readonly string[]): Promise<Set<string>> {\n const credentials = new DesignCredentialModel(getStore().db, credentialKeys(getStore().config));\n const kinds = new Set(\n urls.flatMap((url) => parseDesignUrl(getStore().design, url)?.provider.metadata.kind ?? []),\n );\n const have = await Promise.all(\n [...kinds].map(async (kind) => ((await credentials.info(project.id, kind)) ? kind : null)),\n );\n return new Set(have.flatMap((kind) => (kind === null ? [] : [kind])));\n}\n\n/** The most recent time any of the linked designs' metadata was fetched, or null when none was. */\nfunction newestFetch(times: readonly string[]): string | null {\n return times.toSorted().at(-1) ?? null;\n}\n\n/** The newest `changedAt` among a component's drifted links (0 when none is known). */\nfunction changedMs(component: CoverageComponent): number {\n const times = component.links.flatMap((link) =>\n link.changedAt ? [Date.parse(link.changedAt)] : [],\n );\n return Math.max(0, ...times.filter((time) => Number.isFinite(time)));\n}\n\nconst deletedFirst = (row: CoverageComponent): number => (worst(row.links) === \"deleted\" ? 0 : 1);\n\n/** Deleted designs first, then the most recently changed. */\nfunction byUrgency(a: CoverageComponent, b: CoverageComponent): number {\n return deletedFirst(a) - deletedFirst(b) || changedMs(b) - changedMs(a);\n}\n\nfunction worst(links: readonly CoverageLink[]): DesignDrift {\n return SEVERITY.find((drift) => links.some((link) => link.drift === drift)) ?? \"unknown\";\n}\n\nfunction summarize(\n project: Project,\n build: Build,\n rows: readonly CoverageComponent[],\n urls: readonly string[],\n withToken: ReadonlySet<string>,\n): Omit<DesignCoverageData, \"storyLinksRead\" | \"lastChecked\"> {\n const linked = rows.filter((row) => row.links.length > 0);\n const kinds = [...new Set(urls.map((url) => parseDesignUrl(getStore().design, url)?.provider))];\n return {\n project,\n build,\n unlinked: rows\n .filter((row) => row.links.length === 0)\n .toSorted((a, b) => a.importPath.localeCompare(b.importPath)),\n drifted: linked\n .filter((row) => [\"deleted\", \"stale\"].includes(worst(row.links)))\n .toSorted(byUrgency),\n current: linked.filter((row) => worst(row.links) === \"current\").length,\n unknown: linked.filter((row) => worst(row.links) === \"unknown\").length,\n withoutToken: kinds.flatMap((provider) =>\n provider && !withToken.has(provider.metadata.kind) ? [provider.metadata.name] : [],\n ),\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,67 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { catalogOf, getStore, testingEnabled } from "../store.mjs";
5
+ import { currentProjectRole, findProjectBySlug, requestHasAdminToken } from "./helpers.mjs";
6
+ import { entriesFromSnapshots } from "../pages/library-model.mjs";
7
+ import { renderDesignCoveragePage } from "../pages/design-coverage.mjs";
8
+ import { coverageOf } from "./design-coverage.handlers.mjs";
9
+ import { BuildModel, SnapshotModel } from "@storyshelf/core/models";
10
+ // src/routers/design-coverage.ts
11
+ /** The page's data before any build was accepted: nothing to read. */
12
+ const NO_COVERAGE = {
13
+ build: null,
14
+ unlinked: [],
15
+ drifted: [],
16
+ current: 0,
17
+ unknown: 0,
18
+ withoutToken: [],
19
+ lastChecked: null,
20
+ storyLinksRead: false
21
+ };
22
+ /** The design coverage page (class `hosting`): needs a configured design provider, and a member. */
23
+ function registerDesignCoverage(app) {
24
+ app.get("/projects/:slug/design/coverage", async (c) => {
25
+ const data = await loadDesignCoverage(c, c.req.param("slug"));
26
+ return data ? c.html(renderDesignCoveragePage(data)) : c.notFound();
27
+ });
28
+ }
29
+ /**
30
+ * The page's data, or null (a 404) when no design provider is configured, the project is
31
+ * unknown, or the viewer is not a member of it. Reads only what was stored: the catalog of the
32
+ * latest accepted build, the design links, and cached design nodes. It never calls a provider.
33
+ */
34
+ async function loadDesignCoverage(c, slug) {
35
+ if (getStore().design.length === 0) return null;
36
+ const project = await findProjectBySlug(slug);
37
+ if (!project || !await mayView(c, project)) return null;
38
+ const build = await latestAcceptedBuild(project);
39
+ if (!build) return {
40
+ ...NO_COVERAGE,
41
+ project
42
+ };
43
+ const snapshots = testingEnabled() ? await new SnapshotModel(getStore().db).listByBuild(build.id) : [];
44
+ const entries = (await catalogOf().load(project.id, build.id))?.entries ?? entriesFromSnapshots(snapshots);
45
+ return await coverageOf(project, build, entries, snapshots);
46
+ }
47
+ /** With auth on, only a member (any role) or the bootstrap admin token sees the page. */
48
+ async function mayView(c, project) {
49
+ if (!getStore().authEnabled || requestHasAdminToken(c)) return true;
50
+ return await currentProjectRole(project.id) !== null;
51
+ }
52
+ /** Newest approved build on the default branch, else the newest approved on any branch. */
53
+ async function latestAcceptedBuild(project) {
54
+ const builds = new BuildModel(getStore().db);
55
+ const approved = {
56
+ status: "approved",
57
+ limit: 1
58
+ };
59
+ const [onDefault] = await builds.list(project.id, {
60
+ ...approved,
61
+ branch: project.gitDefaultBranch
62
+ });
63
+ return onDefault ?? (await builds.list(project.id, approved))[0];
64
+ }
65
+ export { loadDesignCoverage, registerDesignCoverage };
66
+
67
+ //# sourceMappingURL=design-coverage.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"design-coverage.mjs","names":[],"sources":["../../src/routers/design-coverage.ts"],"sourcesContent":["import { BuildModel, SnapshotModel } from \"@storyshelf/core/models\";\nimport type { Build, Project } from \"@storyshelf/core/schema\";\nimport type { Context } from \"hono\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { renderDesignCoveragePage, type DesignCoverageData } from \"../pages/design-coverage.tsx\";\nimport { entriesFromSnapshots } from \"../pages/library-model.ts\";\nimport { catalogOf, getStore, testingEnabled } from \"../store.ts\";\nimport { coverageOf } from \"./design-coverage.handlers.ts\";\nimport { currentProjectRole, findProjectBySlug, requestHasAdminToken } from \"./helpers.ts\";\n\n/** The page's data before any build was accepted: nothing to read. */\nconst NO_COVERAGE: Omit<DesignCoverageData, \"project\"> = {\n build: null,\n unlinked: [],\n drifted: [],\n current: 0,\n unknown: 0,\n withoutToken: [],\n lastChecked: null,\n storyLinksRead: false,\n};\n\n/** The design coverage page (class `hosting`): needs a configured design provider, and a member. */\nexport function registerDesignCoverage(app: ShelfRouter): void {\n app.get(\"/projects/:slug/design/coverage\", async (c) => {\n const data = await loadDesignCoverage(c, c.req.param(\"slug\"));\n return data ? c.html(renderDesignCoveragePage(data)) : c.notFound();\n });\n}\n\n/**\n * The page's data, or null (a 404) when no design provider is configured, the project is\n * unknown, or the viewer is not a member of it. Reads only what was stored: the catalog of the\n * latest accepted build, the design links, and cached design nodes. It never calls a provider.\n */\nexport async function loadDesignCoverage(\n c: Context,\n slug: string,\n): Promise<DesignCoverageData | null> {\n if (getStore().design.length === 0) return null;\n const project = await findProjectBySlug(slug);\n if (!project || !(await mayView(c, project))) return null;\n const build = await latestAcceptedBuild(project);\n if (!build) return { ...NO_COVERAGE, project };\n const snapshots = testingEnabled()\n ? await new SnapshotModel(getStore().db).listByBuild(build.id)\n : [];\n const catalog = await catalogOf().load(project.id, build.id);\n const entries = catalog?.entries ?? entriesFromSnapshots(snapshots);\n return await coverageOf(project, build, entries, snapshots);\n}\n\n/** With auth on, only a member (any role) or the bootstrap admin token sees the page. */\nasync function mayView(c: Context, project: Project): Promise<boolean> {\n if (!getStore().authEnabled || requestHasAdminToken(c)) return true;\n return (await currentProjectRole(project.id)) !== null;\n}\n\n/** Newest approved build on the default branch, else the newest approved on any branch. */\nasync function latestAcceptedBuild(project: Project): Promise<Build | undefined> {\n const builds = new BuildModel(getStore().db);\n const approved = { status: \"approved\", limit: 1 } as const;\n const [onDefault] = await builds.list(project.id, {\n ...approved,\n branch: project.gitDefaultBranch,\n });\n return onDefault ?? (await builds.list(project.id, approved))[0];\n}\n"],"mappings":";;;;;;;;;;;AAWA,MAAM,cAAmD;CACvD,OAAO;CACP,UAAU,CAAC;CACX,SAAS,CAAC;CACV,SAAS;CACT,SAAS;CACT,cAAc,CAAC;CACf,aAAa;CACb,gBAAgB;AAClB;;AAGA,SAAgB,uBAAuB,KAAwB;CAC7D,IAAI,IAAI,mCAAmC,OAAO,MAAM;EACtD,MAAM,OAAO,MAAM,mBAAmB,GAAG,EAAE,IAAI,MAAM,MAAM,CAAC;EAC5D,OAAO,OAAO,EAAE,KAAK,yBAAyB,IAAI,CAAC,IAAI,EAAE,SAAS;CACpE,CAAC;AACH;;;;;;AAOA,eAAsB,mBACpB,GACA,MACoC;CACpC,IAAI,SAAS,CAAC,CAAC,OAAO,WAAW,GAAG,OAAO;CAC3C,MAAM,UAAU,MAAM,kBAAkB,IAAI;CAC5C,IAAI,CAAC,WAAW,CAAE,MAAM,QAAQ,GAAG,OAAO,GAAI,OAAO;CACrD,MAAM,QAAQ,MAAM,oBAAoB,OAAO;CAC/C,IAAI,CAAC,OAAO,OAAO;EAAE,GAAG;EAAa;CAAQ;CAC7C,MAAM,YAAY,eAAe,IAC7B,MAAM,IAAI,cAAc,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,YAAY,MAAM,EAAE,IAC3D,CAAC;CAEL,MAAM,WAAU,MADM,UAAU,CAAC,CAAC,KAAK,QAAQ,IAAI,MAAM,EAAE,EAAA,EAClC,WAAW,qBAAqB,SAAS;CAClE,OAAO,MAAM,WAAW,SAAS,OAAO,SAAS,SAAS;AAC5D;;AAGA,eAAe,QAAQ,GAAY,SAAoC;CACrE,IAAI,CAAC,SAAS,CAAC,CAAC,eAAe,qBAAqB,CAAC,GAAG,OAAO;CAC/D,OAAQ,MAAM,mBAAmB,QAAQ,EAAE,MAAO;AACpD;;AAGA,eAAe,oBAAoB,SAA8C;CAC/E,MAAM,SAAS,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE;CAC3C,MAAM,WAAW;EAAE,QAAQ;EAAY,OAAO;CAAE;CAChD,MAAM,CAAC,aAAa,MAAM,OAAO,KAAK,QAAQ,IAAI;EAChD,GAAG;EACH,QAAQ,QAAQ;CAClB,CAAC;CACD,OAAO,cAAc,MAAM,OAAO,KAAK,QAAQ,IAAI,QAAQ,EAAA,CAAG;AAChE"}
@@ -88,6 +88,7 @@ async function loadDesignPreviews(project, urls) {
88
88
  renderUrl: renderUrlOf(project, node)
89
89
  }]));
90
90
  }
91
+ /** The design node model wired to the store (cache reads need no provider call). */
91
92
  function nodeModel() {
92
93
  const { db, config, logger, storage, design } = getStore();
93
94
  const credentials = new DesignCredentialModel(db, credentialKeys(config));
@@ -115,6 +116,6 @@ async function refreshWithinBudget(nodes, projectId, urls, viewerId) {
115
116
  ...viewerId ? { viewerId } : {}
116
117
  }).catch(() => /* @__PURE__ */ new Map());
117
118
  }
118
- export { designUrlsFor, loadDesignPreviews, loadDesignResolutions, loadProjectDesignLinks, loadReviewDesign, providerNameFor, returnPathOf };
119
+ export { designUrlsFor, loadDesignPreviews, loadDesignResolutions, loadProjectDesignLinks, loadReviewDesign, nodeModel, providerNameFor, returnPathOf };
119
120
 
120
121
  //# sourceMappingURL=design.handlers.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"design.handlers.mjs","names":[],"sources":["../../src/routers/design.handlers.ts"],"sourcesContent":["import { parseDesignUrl } from \"@storyshelf/core/adapter/design\";\nimport {\n DesignCredentialModel,\n DesignModel,\n DesignNodeModel,\n isDesignStale,\n resolveDesigns,\n normalizeImportPath,\n type DesignResolution,\n} from \"@storyshelf/core/models\";\nimport type { DesignNode, Project, Snapshot } from \"@storyshelf/core/schema\";\nimport { connectionModel } from \"../connections.ts\";\nimport { credentialKeys } from \"../credential-keys.ts\";\nimport { announceDesignChangesWith } from \"../design-events.ts\";\nimport type {\n DesignConnect,\n DesignItem,\n DesignPreviewInfo,\n ProjectDesignLink,\n} from \"../pages/design-links.tsx\";\nimport { getStore } from \"../store.ts\";\nimport { loadConnectStates, viewerIdOf } from \"./design-connect.handlers.ts\";\n\n/** Most a page render waits for fresh design metadata; slower fetches finish into the cache for next time. */\nconst REFRESH_BUDGET_MS = 1500;\n\n/** Path + query of the current request, to return to after a connect round trip. */\nexport function returnPathOf(c: { req: { url: string } }): string {\n const url = new URL(c.req.url);\n return `${url.pathname}${url.search}`;\n}\n\n/** Component (story file) → design link(s) for a build's snapshots; files without a link are absent. */\nexport async function loadDesignResolutions(\n project: Project,\n snapshots: readonly Snapshot[],\n): Promise<Map<string, DesignResolution>> {\n const { db, design } = getStore();\n if (design.length === 0) return new Map();\n const model = new DesignModel(db, design);\n return resolveDesigns(await model.componentLinks(project.id), snapshots);\n}\n\n/** Design URLs of one snapshot's component. */\nexport function designUrlsFor(\n resolutions: Map<string, DesignResolution>,\n snapshot: Pick<Snapshot, \"storyImportPath\">,\n): string[] {\n const key = snapshot.storyImportPath ? normalizeImportPath(snapshot.storyImportPath) : \"\";\n return resolutions.get(key)?.urls ?? [];\n}\n\n/** Project-level design file links for the library header. */\nexport async function loadProjectDesignLinks(project: Project): Promise<ProjectDesignLink[]> {\n const { db, design } = getStore();\n if (design.length === 0) return [];\n const links = await new DesignModel(db, design).projectLinks(project.id);\n return links.map((link) => ({ label: link.label ?? \"Design\", url: link.url }));\n}\n\n/**\n * Design items for the review page: each link with cached name/state and a\n * stale flag against the snapshot's baseline. Refreshes expired metadata\n * within a short budget; failures degrade to plain deep links, never blocking review.\n */\nexport async function loadReviewDesign(\n project: Project,\n snapshot: Snapshot,\n urls: readonly string[],\n returnTo = \"/\",\n): Promise<DesignItem[]> {\n if (urls.length === 0) return [];\n const nodes = nodeModel();\n const viewerId = viewerIdOf();\n await refreshWithinBudget(nodes, project.id, urls, viewerId);\n const cached = await nodes\n .cachedFor(project.id, urls, viewerId)\n .catch(() => new Map<string, DesignNode>());\n await announceDesignChangesWith(\n getStore(),\n nodes,\n project,\n urls.map((url) => ({\n url,\n node: cached.get(url),\n componentPath: snapshot.storyImportPath,\n baselineVersion: snapshot.baselineVersion,\n })),\n );\n const connects = await loadConnectStates(\n project.id,\n urls.map((url) => kindOf(url)),\n returnTo,\n ).catch(() => new Map<string, DesignConnect>());\n return urls.map((url) => {\n const node = cached.get(url);\n const connect = connects.get(kindOf(url));\n return {\n ...(connect ? { connect } : {}),\n url,\n name: node?.name ?? null,\n providerName: providerNameFor(url),\n renderUrl: renderUrlOf(project, node),\n state: node?.state ?? \"unknown\",\n stale: isDesignStale(node, snapshot.baselineVersion),\n };\n });\n}\n\n/** Most distinct design links a library render refreshes (the model bounds file concurrency). */\nconst WARM_LIMIT = 10;\n\n/**\n * Cached name/render per design URL for library popovers. Missing or expired\n * entries are refreshed first, within the page-render budget and with bounded\n * concurrency, so a slow design API never holds the library up or fans out unbounded.\n */\nexport async function loadDesignPreviews(\n project: Project,\n urls: readonly string[],\n): Promise<Map<string, DesignPreviewInfo>> {\n const distinct = [...new Set(urls)];\n if (distinct.length === 0) return new Map();\n const nodes = nodeModel();\n const viewerId = viewerIdOf();\n await refreshWithinBudget(nodes, project.id, distinct.slice(0, WARM_LIMIT), viewerId);\n const cached = await nodes\n .cachedFor(project.id, distinct, viewerId)\n .catch(() => new Map<string, DesignNode>());\n return new Map(\n [...cached].map(([url, node]) => [\n url,\n { name: node.name, renderUrl: renderUrlOf(project, node) },\n ]),\n );\n}\n\nfunction nodeModel(): DesignNodeModel {\n const { db, config, logger, storage, design } = getStore();\n const credentials = new DesignCredentialModel(db, credentialKeys(config));\n return new DesignNodeModel(db, credentials, design, {\n logger,\n storage,\n connections: connectionModel(),\n });\n}\n\nfunction kindOf(url: string): string {\n return parseDesignUrl(getStore().design, url)?.provider.metadata.kind ?? \"\";\n}\n\n/** Display name of the provider that owns a design link (\"Figma\"), or a generic fallback. */\nexport function providerNameFor(url: string): string {\n return parseDesignUrl(getStore().design, url)?.provider.metadata.name ?? \"Design\";\n}\n\n/** API URL of a node's cached render (version-busted), or null when none is cached. */\nfunction renderUrlOf(project: Project, node: DesignNode | undefined): string | null {\n return node?.renderPath\n ? `/api/v1/projects/${project.slug}/design/renders/${node.id}?v=${encodeURIComponent(node.renderVersion ?? \"\")}`\n : null;\n}\n\n/** Refresh the links, aborting every provider call once the budget is spent (the cache keeps what landed). */\nasync function refreshWithinBudget(\n nodes: DesignNodeModel,\n projectId: string,\n urls: readonly string[],\n viewerId?: string,\n): Promise<void> {\n await nodes\n .refreshMany(projectId, urls, {\n signal: AbortSignal.timeout(REFRESH_BUDGET_MS),\n ...(viewerId ? { viewerId } : {}),\n })\n .catch(() => new Map());\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"design.handlers.mjs","names":[],"sources":["../../src/routers/design.handlers.ts"],"sourcesContent":["import { parseDesignUrl } from \"@storyshelf/core/adapter/design\";\nimport {\n DesignCredentialModel,\n DesignModel,\n DesignNodeModel,\n isDesignStale,\n resolveDesigns,\n normalizeImportPath,\n type DesignResolution,\n} from \"@storyshelf/core/models\";\nimport type { DesignNode, Project, Snapshot } from \"@storyshelf/core/schema\";\nimport { connectionModel } from \"../connections.ts\";\nimport { credentialKeys } from \"../credential-keys.ts\";\nimport { announceDesignChangesWith } from \"../design-events.ts\";\nimport type {\n DesignConnect,\n DesignItem,\n DesignPreviewInfo,\n ProjectDesignLink,\n} from \"../pages/design-links.tsx\";\nimport { getStore } from \"../store.ts\";\nimport { loadConnectStates, viewerIdOf } from \"./design-connect.handlers.ts\";\n\n/** Most a page render waits for fresh design metadata; slower fetches finish into the cache for next time. */\nconst REFRESH_BUDGET_MS = 1500;\n\n/** Path + query of the current request, to return to after a connect round trip. */\nexport function returnPathOf(c: { req: { url: string } }): string {\n const url = new URL(c.req.url);\n return `${url.pathname}${url.search}`;\n}\n\n/** Component (story file) → design link(s) for a build's snapshots; files without a link are absent. */\nexport async function loadDesignResolutions(\n project: Project,\n snapshots: readonly Snapshot[],\n): Promise<Map<string, DesignResolution>> {\n const { db, design } = getStore();\n if (design.length === 0) return new Map();\n const model = new DesignModel(db, design);\n return resolveDesigns(await model.componentLinks(project.id), snapshots);\n}\n\n/** Design URLs of one snapshot's component. */\nexport function designUrlsFor(\n resolutions: Map<string, DesignResolution>,\n snapshot: Pick<Snapshot, \"storyImportPath\">,\n): string[] {\n const key = snapshot.storyImportPath ? normalizeImportPath(snapshot.storyImportPath) : \"\";\n return resolutions.get(key)?.urls ?? [];\n}\n\n/** Project-level design file links for the library header. */\nexport async function loadProjectDesignLinks(project: Project): Promise<ProjectDesignLink[]> {\n const { db, design } = getStore();\n if (design.length === 0) return [];\n const links = await new DesignModel(db, design).projectLinks(project.id);\n return links.map((link) => ({ label: link.label ?? \"Design\", url: link.url }));\n}\n\n/**\n * Design items for the review page: each link with cached name/state and a\n * stale flag against the snapshot's baseline. Refreshes expired metadata\n * within a short budget; failures degrade to plain deep links, never blocking review.\n */\nexport async function loadReviewDesign(\n project: Project,\n snapshot: Snapshot,\n urls: readonly string[],\n returnTo = \"/\",\n): Promise<DesignItem[]> {\n if (urls.length === 0) return [];\n const nodes = nodeModel();\n const viewerId = viewerIdOf();\n await refreshWithinBudget(nodes, project.id, urls, viewerId);\n const cached = await nodes\n .cachedFor(project.id, urls, viewerId)\n .catch(() => new Map<string, DesignNode>());\n await announceDesignChangesWith(\n getStore(),\n nodes,\n project,\n urls.map((url) => ({\n url,\n node: cached.get(url),\n componentPath: snapshot.storyImportPath,\n baselineVersion: snapshot.baselineVersion,\n })),\n );\n const connects = await loadConnectStates(\n project.id,\n urls.map((url) => kindOf(url)),\n returnTo,\n ).catch(() => new Map<string, DesignConnect>());\n return urls.map((url) => {\n const node = cached.get(url);\n const connect = connects.get(kindOf(url));\n return {\n ...(connect ? { connect } : {}),\n url,\n name: node?.name ?? null,\n providerName: providerNameFor(url),\n renderUrl: renderUrlOf(project, node),\n state: node?.state ?? \"unknown\",\n stale: isDesignStale(node, snapshot.baselineVersion),\n };\n });\n}\n\n/** Most distinct design links a library render refreshes (the model bounds file concurrency). */\nconst WARM_LIMIT = 10;\n\n/**\n * Cached name/render per design URL for library popovers. Missing or expired\n * entries are refreshed first, within the page-render budget and with bounded\n * concurrency, so a slow design API never holds the library up or fans out unbounded.\n */\nexport async function loadDesignPreviews(\n project: Project,\n urls: readonly string[],\n): Promise<Map<string, DesignPreviewInfo>> {\n const distinct = [...new Set(urls)];\n if (distinct.length === 0) return new Map();\n const nodes = nodeModel();\n const viewerId = viewerIdOf();\n await refreshWithinBudget(nodes, project.id, distinct.slice(0, WARM_LIMIT), viewerId);\n const cached = await nodes\n .cachedFor(project.id, distinct, viewerId)\n .catch(() => new Map<string, DesignNode>());\n return new Map(\n [...cached].map(([url, node]) => [\n url,\n { name: node.name, renderUrl: renderUrlOf(project, node) },\n ]),\n );\n}\n\n/** The design node model wired to the store (cache reads need no provider call). */\nexport function nodeModel(): DesignNodeModel {\n const { db, config, logger, storage, design } = getStore();\n const credentials = new DesignCredentialModel(db, credentialKeys(config));\n return new DesignNodeModel(db, credentials, design, {\n logger,\n storage,\n connections: connectionModel(),\n });\n}\n\nfunction kindOf(url: string): string {\n return parseDesignUrl(getStore().design, url)?.provider.metadata.kind ?? \"\";\n}\n\n/** Display name of the provider that owns a design link (\"Figma\"), or a generic fallback. */\nexport function providerNameFor(url: string): string {\n return parseDesignUrl(getStore().design, url)?.provider.metadata.name ?? \"Design\";\n}\n\n/** API URL of a node's cached render (version-busted), or null when none is cached. */\nfunction renderUrlOf(project: Project, node: DesignNode | undefined): string | null {\n return node?.renderPath\n ? `/api/v1/projects/${project.slug}/design/renders/${node.id}?v=${encodeURIComponent(node.renderVersion ?? \"\")}`\n : null;\n}\n\n/** Refresh the links, aborting every provider call once the budget is spent (the cache keeps what landed). */\nasync function refreshWithinBudget(\n nodes: DesignNodeModel,\n projectId: string,\n urls: readonly string[],\n viewerId?: string,\n): Promise<void> {\n await nodes\n .refreshMany(projectId, urls, {\n signal: AbortSignal.timeout(REFRESH_BUDGET_MS),\n ...(viewerId ? { viewerId } : {}),\n })\n .catch(() => new Map());\n}\n"],"mappings":"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"}
@@ -21,6 +21,7 @@ import { renderProjectsPage } from "../pages/projects.mjs";
21
21
  import { renderRootPage } from "../pages/root.mjs";
22
22
  import { loadBaselineReference, loadDefaultComparison } from "./baseline-reference.mjs";
23
23
  import { loadComponentPage } from "./component.handlers.mjs";
24
+ import { registerDesignCoverage } from "./design-coverage.mjs";
24
25
  import { registerSettingsPages } from "./settings.mjs";
25
26
  import { BuildModel, CommentModel, LabelModel, ProjectModel, SnapshotModel } from "@storyshelf/core/models";
26
27
  // src/routers/ui.ts
@@ -180,6 +181,7 @@ function registerUiPages(app) {
180
181
  if (!html) return c.notFound();
181
182
  return c.html(html);
182
183
  });
184
+ registerDesignCoverage(app);
183
185
  registerSettingsPages(app);
184
186
  }
185
187
  /** Linked-package usage for every story of a build; empty on any failure so the page still renders. */
@@ -1 +1 @@
1
- {"version":3,"file":"ui.mjs","names":[],"sources":["../../src/routers/ui.ts"],"sourcesContent":["import { BuildModel } from \"@storyshelf/core/models\";\nimport { CommentModel } from \"@storyshelf/core/models\";\nimport { LabelModel } from \"@storyshelf/core/models\";\nimport { ProjectModel } from \"@storyshelf/core/models\";\nimport { SnapshotModel } from \"@storyshelf/core/models\";\nimport type { ProjectRole } from \"@storyshelf/core/types\";\nimport type { Context } from \"hono\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { loadInsightPanel } from \"../insights/panel.ts\";\nimport { renderBuildDetailPage } from \"../pages/build-detail.tsx\";\nimport type { LinkedUsageItem } from \"../pages/build-diff-usage.tsx\";\nimport { renderBuildDiffPage, selectedSnapshot } from \"../pages/build-diff.tsx\";\nimport { renderComponentPage } from \"../pages/component-detail.tsx\";\nimport { renderComputeJobsPage, renderActiveQueue } from \"../pages/compute-jobs.tsx\";\nimport { renderLabelDetailPage, renderLabelsPage } from \"../pages/label-detail.tsx\";\nimport { renderLibraryPage } from \"../pages/library.tsx\";\nimport { renderProjectBuildsPage } from \"../pages/project-builds.tsx\";\nimport { renderProjectCreatePage } from \"../pages/project-create.tsx\";\nimport { renderProjectsPage } from \"../pages/projects.tsx\";\nimport { renderRootPage } from \"../pages/root.tsx\";\nimport { getStore } from \"../store.ts\";\nimport { loadBaselineView } from \"./baseline-guard.ts\";\nimport { loadBaselineReference, loadDefaultComparison } from \"./baseline-reference.ts\";\nimport { loadComponentPage } from \"./component.handlers.ts\";\nimport {\n designUrlsFor,\n loadDesignResolutions,\n loadReviewDesign,\n returnPathOf,\n} from \"./design.handlers.ts\";\nimport { flash } from \"./flash.ts\";\nimport { currentProjectRole } from \"./helpers.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\nimport { linkedUsageByStory, linkedUsageFor } from \"./links.handlers.ts\";\nimport { registerSettingsPages } from \"./settings.ts\";\nfunction asString(value: FormDataEntryValue | null): string | undefined {\n return typeof value === \"string\" ? value.trim() : undefined;\n}\n\nfunction decodeRest(value: string): string {\n try {\n return decodeURIComponent(value);\n } catch {\n return \"\";\n }\n}\n\ninterface QueueView {\n buildId: string;\n status: string;\n queuedAt: string;\n startedAt?: string;\n finishedAt?: string;\n error?: string;\n}\n\nasync function getQueueView(): Promise<QueueView[]> {\n const queue = getStore().captureQueue;\n if (!queue) {\n return [];\n }\n const active = await queue.active();\n return active.map((entry) => ({\n buildId: entry.buildId,\n status: entry.status,\n queuedAt: entry.queuedAt,\n startedAt: entry.startedAt,\n finishedAt: entry.finishedAt,\n error: entry.error,\n }));\n}\n\nasync function canManageJobs(projectId: string): Promise<boolean> {\n const { authEnabled } = getStore();\n if (!authEnabled) {\n return true;\n }\n const roles: readonly ProjectRole[] = [\"developer\", \"approver\", \"admin\"];\n const role = await currentProjectRole(projectId);\n return Boolean(role && roles.includes(role));\n}\n\n/** Register the server-rendered HTML pages (projects, builds, diffs, labels, settings). */\nexport function registerUiPages(app: ShelfRouter): void {\n // eslint-disable-next-line promise-function-async -- renderRootPage returns RenderedContent (string | Promise<string>)\n app.get(\"/\", async (c) => c.html(await renderRootPage()));\n app.get(\"/projects\", async (c) => c.html(await renderProjectsPage()));\n // eslint-disable-next-line promise-function-async -- renderProjectCreatePage returns RenderedContent (string | Promise<string>)\n app.get(\"/projects/new\", async (c) => c.html(await renderProjectCreatePage()));\n\n app.post(\"/projects/new\", async (c) => {\n const form = await c.req.formData();\n const name = asString(form.get(\"name\"));\n const gitRepository = asString(form.get(\"gitRepository\"));\n const gitDefaultBranch = asString(form.get(\"gitDefaultBranch\"));\n if (!name) {\n return c.html(\n renderProjectCreatePage({\n values: { name, gitRepository, gitDefaultBranch },\n errors: { name: \"Name is required\" },\n }),\n 400,\n );\n }\n try {\n const project = await new ProjectModel(getStore().db).create({\n name,\n gitRepository,\n gitDefaultBranch,\n });\n await new LabelModel(getStore().db).seedFor(project.id);\n flash(c, `Created ${project.name}`);\n return hxRedirect(c, `/projects/${project.slug}/library`);\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Failed to create project\";\n return c.html(\n renderProjectCreatePage({\n values: { name, gitRepository, gitDefaultBranch },\n globalError: message,\n }),\n 400,\n );\n }\n });\n\n app.get(\"/projects/:slug\", (c) => c.redirect(`/projects/${c.req.param(\"slug\")}/library`, 302));\n\n app.get(\"/projects/:slug/builds\", async (c) => {\n const html = await renderProjectBuildsPage(c.req.param(\"slug\"), {\n status: c.req.query(\"status\"),\n branch: c.req.query(\"branch\"),\n });\n if (!html) {\n return c.notFound();\n }\n return c.html(html);\n });\n\n app.get(\"/projects/:slug/builds/:buildId\", async (c) => {\n const buildId = c.req.param(\"buildId\");\n const html = await renderBuildDetailPage(buildId, await loadBuildUsage(c, buildId));\n if (!html) {\n return c.notFound();\n }\n return c.html(html);\n });\n\n app.get(\"/projects/:slug/jobs\", async (c) => {\n const slug = c.req.param(\"slug\");\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n return c.notFound();\n }\n const queueView = await getQueueView();\n const canRetry = await canManageJobs(project.id);\n if (c.req.query(\"partial\") === \"queue\") {\n return c.html(renderActiveQueue(slug, queueView));\n }\n const html = await renderComputeJobsPage(slug, queueView, canRetry);\n if (!html) {\n return c.notFound();\n }\n return c.html(html);\n });\n\n app.get(\"/projects/:slug/builds/:buildId/diff\", async (c) => {\n const slug = c.req.param(\"slug\");\n const buildId = c.req.param(\"buildId\");\n const snapshotId = c.req.query(\"snapshot\");\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n return c.notFound();\n }\n const build = await new BuildModel(getStore().db).get(buildId);\n if (!build || build.projectId !== project.id) {\n return c.notFound();\n }\n const snapshots = await new SnapshotModel(getStore().db).listByBuild(build.id);\n const comments = await new CommentModel(getStore().db).listByBuild(build.id);\n const { hasBaseline, drifted } = await loadBaselineView(project, build, snapshots);\n const { user, authEnabled } = getStore();\n const canReview = !authEnabled || Boolean(user);\n const selected = selectedSnapshot(snapshots, snapshotId);\n const design = selected\n ? await loadReviewDesign(\n project,\n selected,\n designUrlsFor(await loadDesignResolutions(project, snapshots), selected),\n returnPathOf(c),\n ).catch(() => [])\n : [];\n return c.html(\n renderBuildDiffPage({\n project,\n build,\n snapshots,\n comments,\n selectedId: snapshotId,\n canReview,\n hasBaseline,\n drifted,\n reference: selected ? await loadBaselineReference(project, build, selected) : undefined,\n defaultComparison: selected ? await loadDefaultComparison(project, build, selected) : null,\n insight: await loadInsightPanel(project, build),\n design,\n usage: await linkedUsageFor(\n c,\n project,\n build.id,\n selectedSnapshot(snapshots, snapshotId)?.storyImportPath ?? null,\n ).catch(() => []),\n publishedBaseDomain: getStore().config.publishedBaseDomain,\n }),\n );\n });\n\n app.get(\"/projects/:slug/labels\", async (c) => {\n const html = await renderLabelsPage(c.req.param(\"slug\"));\n if (!html) {\n return c.notFound();\n }\n return c.html(html);\n });\n\n app.get(\"/projects/:slug/labels/:key/*\", async (c) => {\n const slug = c.req.param(\"slug\");\n const key = c.req.param(\"key\");\n // Hono's bare `/*` wildcard is not exposed through `param()`, so derive the\n // label value from the raw request path and decode it ourselves.\n const base = `/projects/${slug}/labels/${key}/`;\n const value = c.req.path.startsWith(base) ? decodeRest(c.req.path.slice(base.length)) : \"\";\n const html = await renderLabelDetailPage(slug, key, value);\n if (!html) {\n return c.notFound();\n }\n return c.html(html);\n });\n\n app.get(\"/projects/:slug/components/*\", async (c) => {\n const slug = c.req.param(\"slug\");\n const base = `/projects/${slug}/components/`;\n const path = c.req.path.startsWith(base) ? decodeRest(c.req.path.slice(base.length)) : \"\";\n const data = await loadComponentPage(c, slug, path, c.req.query(\"tab\"), c.req.query(\"story\"));\n return data ? c.html(renderComponentPage(data)) : c.notFound();\n });\n\n app.get(\"/projects/:slug/library\", async (c) => {\n const html = await renderLibraryPage(c.req.param(\"slug\"), c.req.query(\"branch\"));\n if (!html) {\n return c.notFound();\n }\n return c.html(html);\n });\n\n registerSettingsPages(app);\n}\n\n/** Linked-package usage for every story of a build; empty on any failure so the page still renders. */\nasync function loadBuildUsage(\n c: Context,\n buildId: string,\n): Promise<Map<string, LinkedUsageItem[]>> {\n try {\n const build = await new BuildModel(getStore().db).get(buildId);\n const project = build ? await new ProjectModel(getStore().db).get(build.projectId) : null;\n return build && project ? await linkedUsageByStory(c, project, build.id) : new Map();\n } catch {\n return new Map();\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ui.mjs","names":[],"sources":["../../src/routers/ui.ts"],"sourcesContent":["import { BuildModel } from \"@storyshelf/core/models\";\nimport { CommentModel } from \"@storyshelf/core/models\";\nimport { LabelModel } from \"@storyshelf/core/models\";\nimport { ProjectModel } from \"@storyshelf/core/models\";\nimport { SnapshotModel } from \"@storyshelf/core/models\";\nimport type { ProjectRole } from \"@storyshelf/core/types\";\nimport type { Context } from \"hono\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { loadInsightPanel } from \"../insights/panel.ts\";\nimport { renderBuildDetailPage } from \"../pages/build-detail.tsx\";\nimport type { LinkedUsageItem } from \"../pages/build-diff-usage.tsx\";\nimport { renderBuildDiffPage, selectedSnapshot } from \"../pages/build-diff.tsx\";\nimport { renderComponentPage } from \"../pages/component-detail.tsx\";\nimport { renderComputeJobsPage, renderActiveQueue } from \"../pages/compute-jobs.tsx\";\nimport { renderLabelDetailPage, renderLabelsPage } from \"../pages/label-detail.tsx\";\nimport { renderLibraryPage } from \"../pages/library.tsx\";\nimport { renderProjectBuildsPage } from \"../pages/project-builds.tsx\";\nimport { renderProjectCreatePage } from \"../pages/project-create.tsx\";\nimport { renderProjectsPage } from \"../pages/projects.tsx\";\nimport { renderRootPage } from \"../pages/root.tsx\";\nimport { getStore } from \"../store.ts\";\nimport { loadBaselineView } from \"./baseline-guard.ts\";\nimport { loadBaselineReference, loadDefaultComparison } from \"./baseline-reference.ts\";\nimport { loadComponentPage } from \"./component.handlers.ts\";\nimport { registerDesignCoverage } from \"./design-coverage.ts\";\nimport {\n designUrlsFor,\n loadDesignResolutions,\n loadReviewDesign,\n returnPathOf,\n} from \"./design.handlers.ts\";\nimport { flash } from \"./flash.ts\";\nimport { currentProjectRole } from \"./helpers.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\nimport { linkedUsageByStory, linkedUsageFor } from \"./links.handlers.ts\";\nimport { registerSettingsPages } from \"./settings.ts\";\nfunction asString(value: FormDataEntryValue | null): string | undefined {\n return typeof value === \"string\" ? value.trim() : undefined;\n}\n\nfunction decodeRest(value: string): string {\n try {\n return decodeURIComponent(value);\n } catch {\n return \"\";\n }\n}\n\ninterface QueueView {\n buildId: string;\n status: string;\n queuedAt: string;\n startedAt?: string;\n finishedAt?: string;\n error?: string;\n}\n\nasync function getQueueView(): Promise<QueueView[]> {\n const queue = getStore().captureQueue;\n if (!queue) {\n return [];\n }\n const active = await queue.active();\n return active.map((entry) => ({\n buildId: entry.buildId,\n status: entry.status,\n queuedAt: entry.queuedAt,\n startedAt: entry.startedAt,\n finishedAt: entry.finishedAt,\n error: entry.error,\n }));\n}\n\nasync function canManageJobs(projectId: string): Promise<boolean> {\n const { authEnabled } = getStore();\n if (!authEnabled) {\n return true;\n }\n const roles: readonly ProjectRole[] = [\"developer\", \"approver\", \"admin\"];\n const role = await currentProjectRole(projectId);\n return Boolean(role && roles.includes(role));\n}\n\n/** Register the server-rendered HTML pages (projects, builds, diffs, labels, settings). */\nexport function registerUiPages(app: ShelfRouter): void {\n // eslint-disable-next-line promise-function-async -- renderRootPage returns RenderedContent (string | Promise<string>)\n app.get(\"/\", async (c) => c.html(await renderRootPage()));\n app.get(\"/projects\", async (c) => c.html(await renderProjectsPage()));\n // eslint-disable-next-line promise-function-async -- renderProjectCreatePage returns RenderedContent (string | Promise<string>)\n app.get(\"/projects/new\", async (c) => c.html(await renderProjectCreatePage()));\n\n app.post(\"/projects/new\", async (c) => {\n const form = await c.req.formData();\n const name = asString(form.get(\"name\"));\n const gitRepository = asString(form.get(\"gitRepository\"));\n const gitDefaultBranch = asString(form.get(\"gitDefaultBranch\"));\n if (!name) {\n return c.html(\n renderProjectCreatePage({\n values: { name, gitRepository, gitDefaultBranch },\n errors: { name: \"Name is required\" },\n }),\n 400,\n );\n }\n try {\n const project = await new ProjectModel(getStore().db).create({\n name,\n gitRepository,\n gitDefaultBranch,\n });\n await new LabelModel(getStore().db).seedFor(project.id);\n flash(c, `Created ${project.name}`);\n return hxRedirect(c, `/projects/${project.slug}/library`);\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Failed to create project\";\n return c.html(\n renderProjectCreatePage({\n values: { name, gitRepository, gitDefaultBranch },\n globalError: message,\n }),\n 400,\n );\n }\n });\n\n app.get(\"/projects/:slug\", (c) => c.redirect(`/projects/${c.req.param(\"slug\")}/library`, 302));\n\n app.get(\"/projects/:slug/builds\", async (c) => {\n const html = await renderProjectBuildsPage(c.req.param(\"slug\"), {\n status: c.req.query(\"status\"),\n branch: c.req.query(\"branch\"),\n });\n if (!html) {\n return c.notFound();\n }\n return c.html(html);\n });\n\n app.get(\"/projects/:slug/builds/:buildId\", async (c) => {\n const buildId = c.req.param(\"buildId\");\n const html = await renderBuildDetailPage(buildId, await loadBuildUsage(c, buildId));\n if (!html) {\n return c.notFound();\n }\n return c.html(html);\n });\n\n app.get(\"/projects/:slug/jobs\", async (c) => {\n const slug = c.req.param(\"slug\");\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n return c.notFound();\n }\n const queueView = await getQueueView();\n const canRetry = await canManageJobs(project.id);\n if (c.req.query(\"partial\") === \"queue\") {\n return c.html(renderActiveQueue(slug, queueView));\n }\n const html = await renderComputeJobsPage(slug, queueView, canRetry);\n if (!html) {\n return c.notFound();\n }\n return c.html(html);\n });\n\n app.get(\"/projects/:slug/builds/:buildId/diff\", async (c) => {\n const slug = c.req.param(\"slug\");\n const buildId = c.req.param(\"buildId\");\n const snapshotId = c.req.query(\"snapshot\");\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n return c.notFound();\n }\n const build = await new BuildModel(getStore().db).get(buildId);\n if (!build || build.projectId !== project.id) {\n return c.notFound();\n }\n const snapshots = await new SnapshotModel(getStore().db).listByBuild(build.id);\n const comments = await new CommentModel(getStore().db).listByBuild(build.id);\n const { hasBaseline, drifted } = await loadBaselineView(project, build, snapshots);\n const { user, authEnabled } = getStore();\n const canReview = !authEnabled || Boolean(user);\n const selected = selectedSnapshot(snapshots, snapshotId);\n const design = selected\n ? await loadReviewDesign(\n project,\n selected,\n designUrlsFor(await loadDesignResolutions(project, snapshots), selected),\n returnPathOf(c),\n ).catch(() => [])\n : [];\n return c.html(\n renderBuildDiffPage({\n project,\n build,\n snapshots,\n comments,\n selectedId: snapshotId,\n canReview,\n hasBaseline,\n drifted,\n reference: selected ? await loadBaselineReference(project, build, selected) : undefined,\n defaultComparison: selected ? await loadDefaultComparison(project, build, selected) : null,\n insight: await loadInsightPanel(project, build),\n design,\n usage: await linkedUsageFor(\n c,\n project,\n build.id,\n selectedSnapshot(snapshots, snapshotId)?.storyImportPath ?? null,\n ).catch(() => []),\n publishedBaseDomain: getStore().config.publishedBaseDomain,\n }),\n );\n });\n\n app.get(\"/projects/:slug/labels\", async (c) => {\n const html = await renderLabelsPage(c.req.param(\"slug\"));\n if (!html) {\n return c.notFound();\n }\n return c.html(html);\n });\n\n app.get(\"/projects/:slug/labels/:key/*\", async (c) => {\n const slug = c.req.param(\"slug\");\n const key = c.req.param(\"key\");\n // Hono's bare `/*` wildcard is not exposed through `param()`, so derive the\n // label value from the raw request path and decode it ourselves.\n const base = `/projects/${slug}/labels/${key}/`;\n const value = c.req.path.startsWith(base) ? decodeRest(c.req.path.slice(base.length)) : \"\";\n const html = await renderLabelDetailPage(slug, key, value);\n if (!html) {\n return c.notFound();\n }\n return c.html(html);\n });\n\n app.get(\"/projects/:slug/components/*\", async (c) => {\n const slug = c.req.param(\"slug\");\n const base = `/projects/${slug}/components/`;\n const path = c.req.path.startsWith(base) ? decodeRest(c.req.path.slice(base.length)) : \"\";\n const data = await loadComponentPage(c, slug, path, c.req.query(\"tab\"), c.req.query(\"story\"));\n return data ? c.html(renderComponentPage(data)) : c.notFound();\n });\n\n app.get(\"/projects/:slug/library\", async (c) => {\n const html = await renderLibraryPage(c.req.param(\"slug\"), c.req.query(\"branch\"));\n if (!html) {\n return c.notFound();\n }\n return c.html(html);\n });\n\n registerDesignCoverage(app);\n registerSettingsPages(app);\n}\n\n/** Linked-package usage for every story of a build; empty on any failure so the page still renders. */\nasync function loadBuildUsage(\n c: Context,\n buildId: string,\n): Promise<Map<string, LinkedUsageItem[]>> {\n try {\n const build = await new BuildModel(getStore().db).get(buildId);\n const project = build ? await new ProjectModel(getStore().db).get(build.projectId) : null;\n return build && project ? await linkedUsageByStory(c, project, build.id) : new Map();\n } catch {\n return new Map();\n }\n}\n"],"mappings":"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"}
@@ -21,6 +21,7 @@ const PAGE_ROUTE_CLASSES = {
21
21
  "GET /projects/:slug/settings/links": "hosting",
22
22
  "POST /projects/:slug/settings/links": "hosting",
23
23
  "POST /projects/:slug/settings/links/delete": "hosting",
24
+ "GET /projects/:slug/design/coverage": "hosting",
24
25
  "GET /projects/:slug/settings/design": "hosting",
25
26
  "POST /projects/:slug/settings/design/files": "hosting",
26
27
  "POST /projects/:slug/settings/design/files/:id/delete": "hosting",
@@ -1 +1 @@
1
- {"version":3,"file":"page-routes.mjs","names":[],"sources":["../../src/routes/page-routes.ts"],"sourcesContent":["/** Classes of the HTML pages, Storybook serving, auth flow and engine routes. */\nimport type { RouteClass } from \"./classification.ts\";\n\n/** HTML and auth routes, keyed by `METHOD /hono-pattern`. */\nexport const PAGE_ROUTE_CLASSES: Readonly<Record<string, RouteClass>> = {\n // hosting\n \"GET /\": \"hosting\",\n \"GET /projects\": \"hosting\",\n \"GET /projects/new\": \"hosting\",\n \"POST /projects/new\": \"hosting\",\n \"GET /projects/:slug/labels\": \"hosting\",\n \"GET /projects/:slug/labels/:key/*\": \"hosting\",\n \"GET /projects/:slug/settings\": \"hosting\",\n \"POST /projects/:slug/settings\": \"hosting\",\n \"POST /projects/:slug/settings/review\": \"hosting\",\n \"POST /projects/:slug/delete\": \"hosting\",\n \"GET /projects/:slug/settings/labels\": \"hosting\",\n \"POST /projects/:slug/settings/labels\": \"hosting\",\n \"POST /projects/:slug/settings/labels/:key\": \"hosting\",\n \"POST /projects/:slug/settings/labels/:key/delete\": \"hosting\",\n \"GET /projects/:slug/settings/links\": \"hosting\",\n \"POST /projects/:slug/settings/links\": \"hosting\",\n \"POST /projects/:slug/settings/links/delete\": \"hosting\",\n \"GET /projects/:slug/settings/design\": \"hosting\",\n \"POST /projects/:slug/settings/design/files\": \"hosting\",\n \"POST /projects/:slug/settings/design/files/:id/delete\": \"hosting\",\n \"POST /projects/:slug/settings/design/components\": \"hosting\",\n \"POST /projects/:slug/settings/design/components/:id/delete\": \"hosting\",\n \"POST /projects/:slug/settings/design/:provider/token\": \"hosting\",\n \"POST /projects/:slug/settings/design/:provider/token/delete\": \"hosting\",\n \"GET /projects/:slug/settings/tokens\": \"hosting\",\n \"POST /projects/:slug/settings/tokens\": \"hosting\",\n \"POST /projects/:slug/settings/tokens/:tokenId/delete\": \"hosting\",\n \"GET /projects/:slug/settings/webhooks\": \"hosting\",\n \"POST /projects/:slug/settings/webhooks\": \"hosting\",\n \"POST /projects/:slug/settings/webhooks/:webhookId/delete\": \"hosting\",\n \"GET /projects/:slug/settings/notifications\": \"hosting\",\n \"POST /projects/:slug/settings/notifications\": \"hosting\",\n \"POST /projects/:slug/settings/notifications/:channelId/delete\": \"hosting\",\n \"POST /projects/:slug/settings/notifications/me\": \"hosting\",\n \"GET /projects/:slug/settings/members\": \"hosting\",\n \"POST /projects/:slug/settings/members\": \"hosting\",\n \"POST /projects/:slug/settings/members/:userId/remove\": \"hosting\",\n \"POST /projects/:slug/settings/members/groups\": \"hosting\",\n \"POST /projects/:slug/settings/members/groups/:mappingId/remove\": \"hosting\",\n \"GET /admin\": \"hosting\",\n \"POST /admin/credentials/reencrypt\": \"hosting\",\n \"POST /admin/public-origin\": \"hosting\",\n \"POST /admin/public-origin/report\": \"hosting\",\n \"POST /admin/public-origin/clear\": \"hosting\",\n \"GET /_\": \"hosting\",\n \"GET /_/\": \"hosting\",\n \"GET /_/:id\": \"hosting\",\n \"GET /_/:id/*\": \"hosting\",\n \"GET /projects/:slug/storybook\": \"hosting\",\n \"GET /projects/:slug/storybook/:key/*\": \"hosting\",\n \"GET /projects/:slug/storybook/build/:buildId/*\": \"hosting\",\n \"GET /_s/:token/*\": \"testing\",\n \"GET /auth/login\": \"hosting\",\n \"GET /auth/engine/login\": \"hosting\",\n \"POST /auth/engine/login\": \"hosting\",\n \"GET /auth/engine/:providerId\": \"hosting\",\n \"GET /auth/invites/:inviteId\": \"hosting\",\n \"POST /auth/invites/:inviteId\": \"hosting\",\n \"POST /auth/logout\": \"hosting\",\n \"GET /.well-known/openid-configuration\": \"hosting\",\n \"GET /.well-known/change-password\": \"hosting\",\n \"GET /profile\": \"hosting\",\n \"POST /profile\": \"hosting\",\n \"POST /profile/password\": \"hosting\",\n \"POST /profile/sessions/revoke\": \"hosting\",\n \"POST /profile/sessions/revoke-others\": \"hosting\",\n \"POST /profile/passkeys/:passkeyId/delete\": \"hosting\",\n \"POST /profile/notifications\": \"hosting\",\n \"GET /connections/:service/start\": \"hosting\",\n \"GET /connections/:service/callback\": \"hosting\",\n \"POST /connections/:service/disconnect\": \"hosting\",\n \"ALL /api/auth/*\": \"hosting\",\n // testing\n \"GET /projects/:slug/jobs\": \"testing\",\n \"GET /projects/:slug/builds/:buildId/diff\": \"testing\",\n \"GET /projects/:slug/settings/tests\": \"testing\",\n \"POST /projects/:slug/settings/tests\": \"testing\",\n \"GET /projects/:slug/settings/capture\": \"testing\",\n \"POST /projects/:slug/settings/capture\": \"testing\",\n \"GET /projects/:slug/settings/ai\": \"testing\",\n \"POST /projects/:slug/settings/ai\": \"testing\",\n \"GET /admin/ai\": \"testing\",\n \"GET /projects/:slug/insights/health/panel\": \"testing\",\n \"POST /projects/:slug/insights/health/generate\": \"testing\",\n \"GET /projects/:slug/builds/:buildId/insights/panel\": \"testing\",\n \"POST /projects/:slug/builds/:buildId/insights/generate\": \"testing\",\n // review\n \"POST /projects/:slug/builds/:buildId/comments\": \"review\",\n \"POST /projects/:slug/builds/:buildId/comments/:commentId/resolve\": \"review\",\n \"POST /projects/:slug/builds/:buildId/reviews\": \"review\",\n // both\n \"GET /projects/:slug\": \"both\",\n \"GET /projects/:slug/builds\": \"both\",\n \"GET /projects/:slug/builds/:buildId\": \"both\",\n \"GET /projects/:slug/library\": \"both\",\n \"GET /projects/:slug/components/*\": \"both\",\n};\n"],"mappings":";;;;;AAIA,MAAa,qBAA2D;CAEtE,SAAS;CACT,iBAAiB;CACjB,qBAAqB;CACrB,sBAAsB;CACtB,8BAA8B;CAC9B,qCAAqC;CACrC,gCAAgC;CAChC,iCAAiC;CACjC,wCAAwC;CACxC,+BAA+B;CAC/B,uCAAuC;CACvC,wCAAwC;CACxC,6CAA6C;CAC7C,oDAAoD;CACpD,sCAAsC;CACtC,uCAAuC;CACvC,8CAA8C;CAC9C,uCAAuC;CACvC,8CAA8C;CAC9C,yDAAyD;CACzD,mDAAmD;CACnD,8DAA8D;CAC9D,wDAAwD;CACxD,+DAA+D;CAC/D,uCAAuC;CACvC,wCAAwC;CACxC,wDAAwD;CACxD,yCAAyC;CACzC,0CAA0C;CAC1C,4DAA4D;CAC5D,8CAA8C;CAC9C,+CAA+C;CAC/C,iEAAiE;CACjE,kDAAkD;CAClD,wCAAwC;CACxC,yCAAyC;CACzC,wDAAwD;CACxD,gDAAgD;CAChD,kEAAkE;CAClE,cAAc;CACd,qCAAqC;CACrC,6BAA6B;CAC7B,oCAAoC;CACpC,mCAAmC;CACnC,UAAU;CACV,WAAW;CACX,cAAc;CACd,gBAAgB;CAChB,iCAAiC;CACjC,wCAAwC;CACxC,kDAAkD;CAClD,oBAAoB;CACpB,mBAAmB;CACnB,0BAA0B;CAC1B,2BAA2B;CAC3B,gCAAgC;CAChC,+BAA+B;CAC/B,gCAAgC;CAChC,qBAAqB;CACrB,yCAAyC;CACzC,oCAAoC;CACpC,gBAAgB;CAChB,iBAAiB;CACjB,0BAA0B;CAC1B,iCAAiC;CACjC,wCAAwC;CACxC,4CAA4C;CAC5C,+BAA+B;CAC/B,mCAAmC;CACnC,sCAAsC;CACtC,yCAAyC;CACzC,mBAAmB;CAEnB,4BAA4B;CAC5B,4CAA4C;CAC5C,sCAAsC;CACtC,uCAAuC;CACvC,wCAAwC;CACxC,yCAAyC;CACzC,mCAAmC;CACnC,oCAAoC;CACpC,iBAAiB;CACjB,6CAA6C;CAC7C,iDAAiD;CACjD,sDAAsD;CACtD,0DAA0D;CAE1D,iDAAiD;CACjD,oEAAoE;CACpE,gDAAgD;CAEhD,uBAAuB;CACvB,8BAA8B;CAC9B,uCAAuC;CACvC,+BAA+B;CAC/B,oCAAoC;AACtC"}
1
+ {"version":3,"file":"page-routes.mjs","names":[],"sources":["../../src/routes/page-routes.ts"],"sourcesContent":["/** Classes of the HTML pages, Storybook serving, auth flow and engine routes. */\nimport type { RouteClass } from \"./classification.ts\";\n\n/** HTML and auth routes, keyed by `METHOD /hono-pattern`. */\nexport const PAGE_ROUTE_CLASSES: Readonly<Record<string, RouteClass>> = {\n // hosting\n \"GET /\": \"hosting\",\n \"GET /projects\": \"hosting\",\n \"GET /projects/new\": \"hosting\",\n \"POST /projects/new\": \"hosting\",\n \"GET /projects/:slug/labels\": \"hosting\",\n \"GET /projects/:slug/labels/:key/*\": \"hosting\",\n \"GET /projects/:slug/settings\": \"hosting\",\n \"POST /projects/:slug/settings\": \"hosting\",\n \"POST /projects/:slug/settings/review\": \"hosting\",\n \"POST /projects/:slug/delete\": \"hosting\",\n \"GET /projects/:slug/settings/labels\": \"hosting\",\n \"POST /projects/:slug/settings/labels\": \"hosting\",\n \"POST /projects/:slug/settings/labels/:key\": \"hosting\",\n \"POST /projects/:slug/settings/labels/:key/delete\": \"hosting\",\n \"GET /projects/:slug/settings/links\": \"hosting\",\n \"POST /projects/:slug/settings/links\": \"hosting\",\n \"POST /projects/:slug/settings/links/delete\": \"hosting\",\n \"GET /projects/:slug/design/coverage\": \"hosting\",\n \"GET /projects/:slug/settings/design\": \"hosting\",\n \"POST /projects/:slug/settings/design/files\": \"hosting\",\n \"POST /projects/:slug/settings/design/files/:id/delete\": \"hosting\",\n \"POST /projects/:slug/settings/design/components\": \"hosting\",\n \"POST /projects/:slug/settings/design/components/:id/delete\": \"hosting\",\n \"POST /projects/:slug/settings/design/:provider/token\": \"hosting\",\n \"POST /projects/:slug/settings/design/:provider/token/delete\": \"hosting\",\n \"GET /projects/:slug/settings/tokens\": \"hosting\",\n \"POST /projects/:slug/settings/tokens\": \"hosting\",\n \"POST /projects/:slug/settings/tokens/:tokenId/delete\": \"hosting\",\n \"GET /projects/:slug/settings/webhooks\": \"hosting\",\n \"POST /projects/:slug/settings/webhooks\": \"hosting\",\n \"POST /projects/:slug/settings/webhooks/:webhookId/delete\": \"hosting\",\n \"GET /projects/:slug/settings/notifications\": \"hosting\",\n \"POST /projects/:slug/settings/notifications\": \"hosting\",\n \"POST /projects/:slug/settings/notifications/:channelId/delete\": \"hosting\",\n \"POST /projects/:slug/settings/notifications/me\": \"hosting\",\n \"GET /projects/:slug/settings/members\": \"hosting\",\n \"POST /projects/:slug/settings/members\": \"hosting\",\n \"POST /projects/:slug/settings/members/:userId/remove\": \"hosting\",\n \"POST /projects/:slug/settings/members/groups\": \"hosting\",\n \"POST /projects/:slug/settings/members/groups/:mappingId/remove\": \"hosting\",\n \"GET /admin\": \"hosting\",\n \"POST /admin/credentials/reencrypt\": \"hosting\",\n \"POST /admin/public-origin\": \"hosting\",\n \"POST /admin/public-origin/report\": \"hosting\",\n \"POST /admin/public-origin/clear\": \"hosting\",\n \"GET /_\": \"hosting\",\n \"GET /_/\": \"hosting\",\n \"GET /_/:id\": \"hosting\",\n \"GET /_/:id/*\": \"hosting\",\n \"GET /projects/:slug/storybook\": \"hosting\",\n \"GET /projects/:slug/storybook/:key/*\": \"hosting\",\n \"GET /projects/:slug/storybook/build/:buildId/*\": \"hosting\",\n \"GET /_s/:token/*\": \"testing\",\n \"GET /auth/login\": \"hosting\",\n \"GET /auth/engine/login\": \"hosting\",\n \"POST /auth/engine/login\": \"hosting\",\n \"GET /auth/engine/:providerId\": \"hosting\",\n \"GET /auth/invites/:inviteId\": \"hosting\",\n \"POST /auth/invites/:inviteId\": \"hosting\",\n \"POST /auth/logout\": \"hosting\",\n \"GET /.well-known/openid-configuration\": \"hosting\",\n \"GET /.well-known/change-password\": \"hosting\",\n \"GET /profile\": \"hosting\",\n \"POST /profile\": \"hosting\",\n \"POST /profile/password\": \"hosting\",\n \"POST /profile/sessions/revoke\": \"hosting\",\n \"POST /profile/sessions/revoke-others\": \"hosting\",\n \"POST /profile/passkeys/:passkeyId/delete\": \"hosting\",\n \"POST /profile/notifications\": \"hosting\",\n \"GET /connections/:service/start\": \"hosting\",\n \"GET /connections/:service/callback\": \"hosting\",\n \"POST /connections/:service/disconnect\": \"hosting\",\n \"ALL /api/auth/*\": \"hosting\",\n // testing\n \"GET /projects/:slug/jobs\": \"testing\",\n \"GET /projects/:slug/builds/:buildId/diff\": \"testing\",\n \"GET /projects/:slug/settings/tests\": \"testing\",\n \"POST /projects/:slug/settings/tests\": \"testing\",\n \"GET /projects/:slug/settings/capture\": \"testing\",\n \"POST /projects/:slug/settings/capture\": \"testing\",\n \"GET /projects/:slug/settings/ai\": \"testing\",\n \"POST /projects/:slug/settings/ai\": \"testing\",\n \"GET /admin/ai\": \"testing\",\n \"GET /projects/:slug/insights/health/panel\": \"testing\",\n \"POST /projects/:slug/insights/health/generate\": \"testing\",\n \"GET /projects/:slug/builds/:buildId/insights/panel\": \"testing\",\n \"POST /projects/:slug/builds/:buildId/insights/generate\": \"testing\",\n // review\n \"POST /projects/:slug/builds/:buildId/comments\": \"review\",\n \"POST /projects/:slug/builds/:buildId/comments/:commentId/resolve\": \"review\",\n \"POST /projects/:slug/builds/:buildId/reviews\": \"review\",\n // both\n \"GET /projects/:slug\": \"both\",\n \"GET /projects/:slug/builds\": \"both\",\n \"GET /projects/:slug/builds/:buildId\": \"both\",\n \"GET /projects/:slug/library\": \"both\",\n \"GET /projects/:slug/components/*\": \"both\",\n};\n"],"mappings":";;;;;AAIA,MAAa,qBAA2D;CAEtE,SAAS;CACT,iBAAiB;CACjB,qBAAqB;CACrB,sBAAsB;CACtB,8BAA8B;CAC9B,qCAAqC;CACrC,gCAAgC;CAChC,iCAAiC;CACjC,wCAAwC;CACxC,+BAA+B;CAC/B,uCAAuC;CACvC,wCAAwC;CACxC,6CAA6C;CAC7C,oDAAoD;CACpD,sCAAsC;CACtC,uCAAuC;CACvC,8CAA8C;CAC9C,uCAAuC;CACvC,uCAAuC;CACvC,8CAA8C;CAC9C,yDAAyD;CACzD,mDAAmD;CACnD,8DAA8D;CAC9D,wDAAwD;CACxD,+DAA+D;CAC/D,uCAAuC;CACvC,wCAAwC;CACxC,wDAAwD;CACxD,yCAAyC;CACzC,0CAA0C;CAC1C,4DAA4D;CAC5D,8CAA8C;CAC9C,+CAA+C;CAC/C,iEAAiE;CACjE,kDAAkD;CAClD,wCAAwC;CACxC,yCAAyC;CACzC,wDAAwD;CACxD,gDAAgD;CAChD,kEAAkE;CAClE,cAAc;CACd,qCAAqC;CACrC,6BAA6B;CAC7B,oCAAoC;CACpC,mCAAmC;CACnC,UAAU;CACV,WAAW;CACX,cAAc;CACd,gBAAgB;CAChB,iCAAiC;CACjC,wCAAwC;CACxC,kDAAkD;CAClD,oBAAoB;CACpB,mBAAmB;CACnB,0BAA0B;CAC1B,2BAA2B;CAC3B,gCAAgC;CAChC,+BAA+B;CAC/B,gCAAgC;CAChC,qBAAqB;CACrB,yCAAyC;CACzC,oCAAoC;CACpC,gBAAgB;CAChB,iBAAiB;CACjB,0BAA0B;CAC1B,iCAAiC;CACjC,wCAAwC;CACxC,4CAA4C;CAC5C,+BAA+B;CAC/B,mCAAmC;CACnC,sCAAsC;CACtC,yCAAyC;CACzC,mBAAmB;CAEnB,4BAA4B;CAC5B,4CAA4C;CAC5C,sCAAsC;CACtC,uCAAuC;CACvC,wCAAwC;CACxC,yCAAyC;CACzC,mCAAmC;CACnC,oCAAoC;CACpC,iBAAiB;CACjB,6CAA6C;CAC7C,iDAAiD;CACjD,sDAAsD;CACtD,0DAA0D;CAE1D,iDAAiD;CACjD,oEAAoE;CACpE,gDAAgD;CAEhD,uBAAuB;CACvB,8BAA8B;CAC9B,uCAAuC;CACvC,+BAA+B;CAC/B,oCAAoC;AACtC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@storyshelf/app",
3
- "version": "0.9.0-next-d2cc082",
3
+ "version": "0.9.0-next-588f070",
4
4
  "description": "StoryShelf app: Hono app, API routes, and server-rendered UI over @storyshelf/core.",
5
5
  "keywords": [
6
6
  "chromatic-alternative",
@@ -58,8 +58,8 @@
58
58
  "dependencies": {
59
59
  "@hono/swagger-ui": "^0.6.1",
60
60
  "@hono/zod-openapi": "^1.1.5",
61
- "@storyshelf/core": "0.9.0-next-d2cc082",
62
- "@storyshelf/observability": "0.9.0-next-d2cc082",
61
+ "@storyshelf/core": "0.9.0-next-588f070",
62
+ "@storyshelf/observability": "0.9.0-next-588f070",
63
63
  "hono": "^4.11.0",
64
64
  "zod": "^4.6.7"
65
65
  },
@@ -68,9 +68,9 @@
68
68
  "@opentelemetry/context-async-hooks": "^2.11.0",
69
69
  "@opentelemetry/core": "^2.11.0",
70
70
  "@opentelemetry/sdk-trace-base": "^2.11.0",
71
- "@storyshelf/auth": "0.9.0-next-d2cc082",
72
- "@storyshelf/db-sqlite": "0.9.0-next-d2cc082",
73
- "@storyshelf/design-figma": "0.9.0-next-d2cc082",
71
+ "@storyshelf/auth": "0.9.0-next-588f070",
72
+ "@storyshelf/db-sqlite": "0.9.0-next-588f070",
73
+ "@storyshelf/design-figma": "0.9.0-next-588f070",
74
74
  "@types/node": "^26.0.0",
75
75
  "@vitest/coverage-v8": "^4.0.0",
76
76
  "adm-zip": "0.6.1",