@storyshelf/app 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (268) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +18 -0
  3. package/dist/app-types.d.mts +54 -0
  4. package/dist/asset-manifest.mjs +41 -0
  5. package/dist/asset-manifest.mjs.map +1 -0
  6. package/dist/asset-paths.mjs +23 -0
  7. package/dist/asset-paths.mjs.map +1 -0
  8. package/dist/assets/htmx.min.js +3304 -0
  9. package/dist/assets/mark.svg +1 -0
  10. package/dist/capture-setup.mjs +139 -0
  11. package/dist/capture-setup.mjs.map +1 -0
  12. package/dist/credential-keys.mjs +20 -0
  13. package/dist/credential-keys.mjs.map +1 -0
  14. package/dist/credentials.mjs +56 -0
  15. package/dist/credentials.mjs.map +1 -0
  16. package/dist/index.d.mts +18 -0
  17. package/dist/index.mjs +310 -0
  18. package/dist/index.mjs.map +1 -0
  19. package/dist/lifecycle.mjs +132 -0
  20. package/dist/lifecycle.mjs.map +1 -0
  21. package/dist/middleware/auth-gate.mjs +36 -0
  22. package/dist/middleware/auth-gate.mjs.map +1 -0
  23. package/dist/middleware/csrf.mjs +75 -0
  24. package/dist/middleware/csrf.mjs.map +1 -0
  25. package/dist/middleware/rate-limit.mjs +68 -0
  26. package/dist/middleware/rate-limit.mjs.map +1 -0
  27. package/dist/middleware/request-log.mjs +30 -0
  28. package/dist/middleware/request-log.mjs.map +1 -0
  29. package/dist/middleware/server-timing.mjs +39 -0
  30. package/dist/middleware/server-timing.mjs.map +1 -0
  31. package/dist/middleware/setup-gate.mjs +37 -0
  32. package/dist/middleware/setup-gate.mjs.map +1 -0
  33. package/dist/middleware/store-scope.mjs +33 -0
  34. package/dist/middleware/store-scope.mjs.map +1 -0
  35. package/dist/notify.d.mts +17 -0
  36. package/dist/notify.mjs +141 -0
  37. package/dist/notify.mjs.map +1 -0
  38. package/dist/pages/admin-system.mjs +243 -0
  39. package/dist/pages/admin-system.mjs.map +1 -0
  40. package/dist/pages/build-detail.mjs +392 -0
  41. package/dist/pages/build-detail.mjs.map +1 -0
  42. package/dist/pages/build-diff-actions.mjs +162 -0
  43. package/dist/pages/build-diff-actions.mjs.map +1 -0
  44. package/dist/pages/build-diff-comments.mjs +104 -0
  45. package/dist/pages/build-diff-comments.mjs.map +1 -0
  46. package/dist/pages/build-diff-header.mjs +117 -0
  47. package/dist/pages/build-diff-header.mjs.map +1 -0
  48. package/dist/pages/build-diff-model.mjs +48 -0
  49. package/dist/pages/build-diff-model.mjs.map +1 -0
  50. package/dist/pages/build-diff-nav.mjs +122 -0
  51. package/dist/pages/build-diff-nav.mjs.map +1 -0
  52. package/dist/pages/build-diff-shortcuts.mjs +78 -0
  53. package/dist/pages/build-diff-shortcuts.mjs.map +1 -0
  54. package/dist/pages/build-diff-stale.mjs +70 -0
  55. package/dist/pages/build-diff-stale.mjs.map +1 -0
  56. package/dist/pages/build-diff-viewer.mjs +70 -0
  57. package/dist/pages/build-diff-viewer.mjs.map +1 -0
  58. package/dist/pages/build-diff.mjs +110 -0
  59. package/dist/pages/build-diff.mjs.map +1 -0
  60. package/dist/pages/compute-jobs.mjs +165 -0
  61. package/dist/pages/compute-jobs.mjs.map +1 -0
  62. package/dist/pages/invite.mjs +91 -0
  63. package/dist/pages/invite.mjs.map +1 -0
  64. package/dist/pages/label-detail.mjs +154 -0
  65. package/dist/pages/label-detail.mjs.map +1 -0
  66. package/dist/pages/library.mjs +343 -0
  67. package/dist/pages/library.mjs.map +1 -0
  68. package/dist/pages/login.mjs +117 -0
  69. package/dist/pages/login.mjs.map +1 -0
  70. package/dist/pages/passkey-ceremony.mjs +97 -0
  71. package/dist/pages/passkey-ceremony.mjs.map +1 -0
  72. package/dist/pages/profile.mjs +364 -0
  73. package/dist/pages/profile.mjs.map +1 -0
  74. package/dist/pages/project-builds-table.mjs +151 -0
  75. package/dist/pages/project-builds-table.mjs.map +1 -0
  76. package/dist/pages/project-builds.mjs +143 -0
  77. package/dist/pages/project-builds.mjs.map +1 -0
  78. package/dist/pages/project-create.mjs +90 -0
  79. package/dist/pages/project-create.mjs.map +1 -0
  80. package/dist/pages/project-settings.mjs +95 -0
  81. package/dist/pages/project-settings.mjs.map +1 -0
  82. package/dist/pages/projects-card.mjs +194 -0
  83. package/dist/pages/projects-card.mjs.map +1 -0
  84. package/dist/pages/projects.mjs +129 -0
  85. package/dist/pages/projects.mjs.map +1 -0
  86. package/dist/pages/root.mjs +98 -0
  87. package/dist/pages/root.mjs.map +1 -0
  88. package/dist/pages/settings-general.mjs +138 -0
  89. package/dist/pages/settings-general.mjs.map +1 -0
  90. package/dist/pages/settings-labels.mjs +107 -0
  91. package/dist/pages/settings-labels.mjs.map +1 -0
  92. package/dist/pages/settings-members.mjs +192 -0
  93. package/dist/pages/settings-members.mjs.map +1 -0
  94. package/dist/pages/settings-notifications.mjs +240 -0
  95. package/dist/pages/settings-notifications.mjs.map +1 -0
  96. package/dist/pages/settings-status.mjs +137 -0
  97. package/dist/pages/settings-status.mjs.map +1 -0
  98. package/dist/pages/settings-tests.mjs +111 -0
  99. package/dist/pages/settings-tests.mjs.map +1 -0
  100. package/dist/pages/settings-tokens.mjs +90 -0
  101. package/dist/pages/settings-tokens.mjs.map +1 -0
  102. package/dist/pages/settings-webhooks.mjs +104 -0
  103. package/dist/pages/settings-webhooks.mjs.map +1 -0
  104. package/dist/pages/storybook.mjs +116 -0
  105. package/dist/pages/storybook.mjs.map +1 -0
  106. package/dist/retention-timer.mjs +65 -0
  107. package/dist/retention-timer.mjs.map +1 -0
  108. package/dist/routers/admin-pages.mjs +48 -0
  109. package/dist/routers/admin-pages.mjs.map +1 -0
  110. package/dist/routers/admin.mjs +113 -0
  111. package/dist/routers/admin.mjs.map +1 -0
  112. package/dist/routers/assets.mjs +64 -0
  113. package/dist/routers/assets.mjs.map +1 -0
  114. package/dist/routers/attempts.mjs +96 -0
  115. package/dist/routers/attempts.mjs.map +1 -0
  116. package/dist/routers/auth-sync.mjs +64 -0
  117. package/dist/routers/auth-sync.mjs.map +1 -0
  118. package/dist/routers/auth.mjs +236 -0
  119. package/dist/routers/auth.mjs.map +1 -0
  120. package/dist/routers/baseline-guard.mjs +83 -0
  121. package/dist/routers/baseline-guard.mjs.map +1 -0
  122. package/dist/routers/builds.handlers.mjs +217 -0
  123. package/dist/routers/builds.handlers.mjs.map +1 -0
  124. package/dist/routers/builds.mjs +381 -0
  125. package/dist/routers/builds.mjs.map +1 -0
  126. package/dist/routers/comments.mjs +109 -0
  127. package/dist/routers/comments.mjs.map +1 -0
  128. package/dist/routers/flash.mjs +35 -0
  129. package/dist/routers/flash.mjs.map +1 -0
  130. package/dist/routers/health-report.mjs +159 -0
  131. package/dist/routers/health-report.mjs.map +1 -0
  132. package/dist/routers/health.mjs +46 -0
  133. package/dist/routers/health.mjs.map +1 -0
  134. package/dist/routers/helpers.mjs +110 -0
  135. package/dist/routers/helpers.mjs.map +1 -0
  136. package/dist/routers/htmx.mjs +24 -0
  137. package/dist/routers/htmx.mjs.map +1 -0
  138. package/dist/routers/labels.mjs +118 -0
  139. package/dist/routers/labels.mjs.map +1 -0
  140. package/dist/routers/media.mjs +53 -0
  141. package/dist/routers/media.mjs.map +1 -0
  142. package/dist/routers/members.mjs +177 -0
  143. package/dist/routers/members.mjs.map +1 -0
  144. package/dist/routers/notification-channels.mjs +190 -0
  145. package/dist/routers/notification-channels.mjs.map +1 -0
  146. package/dist/routers/notification-subscriptions.mjs +120 -0
  147. package/dist/routers/notification-subscriptions.mjs.map +1 -0
  148. package/dist/routers/profile.mjs +276 -0
  149. package/dist/routers/profile.mjs.map +1 -0
  150. package/dist/routers/projects.mjs +133 -0
  151. package/dist/routers/projects.mjs.map +1 -0
  152. package/dist/routers/review-next.mjs +47 -0
  153. package/dist/routers/review-next.mjs.map +1 -0
  154. package/dist/routers/schemas.mjs +337 -0
  155. package/dist/routers/schemas.mjs.map +1 -0
  156. package/dist/routers/settings-general.mjs +104 -0
  157. package/dist/routers/settings-general.mjs.map +1 -0
  158. package/dist/routers/settings-labels.mjs +52 -0
  159. package/dist/routers/settings-labels.mjs.map +1 -0
  160. package/dist/routers/settings-members.mjs +58 -0
  161. package/dist/routers/settings-members.mjs.map +1 -0
  162. package/dist/routers/settings-notifications.mjs +108 -0
  163. package/dist/routers/settings-notifications.mjs.map +1 -0
  164. package/dist/routers/settings-status.mjs +77 -0
  165. package/dist/routers/settings-status.mjs.map +1 -0
  166. package/dist/routers/settings-tokens.mjs +40 -0
  167. package/dist/routers/settings-tokens.mjs.map +1 -0
  168. package/dist/routers/settings-webhooks.mjs +67 -0
  169. package/dist/routers/settings-webhooks.mjs.map +1 -0
  170. package/dist/routers/settings.handlers.mjs +149 -0
  171. package/dist/routers/settings.handlers.mjs.map +1 -0
  172. package/dist/routers/settings.mjs +25 -0
  173. package/dist/routers/settings.mjs.map +1 -0
  174. package/dist/routers/snapshots.mjs +240 -0
  175. package/dist/routers/snapshots.mjs.map +1 -0
  176. package/dist/routers/status-configs.mjs +108 -0
  177. package/dist/routers/status-configs.mjs.map +1 -0
  178. package/dist/routers/storybook.mjs +171 -0
  179. package/dist/routers/storybook.mjs.map +1 -0
  180. package/dist/routers/tokens.mjs +91 -0
  181. package/dist/routers/tokens.mjs.map +1 -0
  182. package/dist/routers/ui.mjs +166 -0
  183. package/dist/routers/ui.mjs.map +1 -0
  184. package/dist/routers/webhooks.mjs +97 -0
  185. package/dist/routers/webhooks.mjs.map +1 -0
  186. package/dist/routers/well-known.mjs +43 -0
  187. package/dist/routers/well-known.mjs.map +1 -0
  188. package/dist/runtime.mjs +52 -0
  189. package/dist/runtime.mjs.map +1 -0
  190. package/dist/store.mjs +20 -0
  191. package/dist/store.mjs.map +1 -0
  192. package/dist/ui/avatar.mjs +76 -0
  193. package/dist/ui/avatar.mjs.map +1 -0
  194. package/dist/ui/buttons.mjs +205 -0
  195. package/dist/ui/buttons.mjs.map +1 -0
  196. package/dist/ui/code-block.mjs +47 -0
  197. package/dist/ui/code-block.mjs.map +1 -0
  198. package/dist/ui/compare-stage.mjs +344 -0
  199. package/dist/ui/compare-stage.mjs.map +1 -0
  200. package/dist/ui/csrf-field.mjs +20 -0
  201. package/dist/ui/csrf-field.mjs.map +1 -0
  202. package/dist/ui/css.mjs +36 -0
  203. package/dist/ui/css.mjs.map +1 -0
  204. package/dist/ui/document.mjs +130 -0
  205. package/dist/ui/document.mjs.map +1 -0
  206. package/dist/ui/dropdown.mjs +181 -0
  207. package/dist/ui/dropdown.mjs.map +1 -0
  208. package/dist/ui/feedback.mjs +341 -0
  209. package/dist/ui/feedback.mjs.map +1 -0
  210. package/dist/ui/filter-input.mjs +58 -0
  211. package/dist/ui/filter-input.mjs.map +1 -0
  212. package/dist/ui/forms.mjs +291 -0
  213. package/dist/ui/forms.mjs.map +1 -0
  214. package/dist/ui/icons/icon.mjs +53 -0
  215. package/dist/ui/icons/icon.mjs.map +1 -0
  216. package/dist/ui/icons/paths.mjs +61 -0
  217. package/dist/ui/icons/paths.mjs.map +1 -0
  218. package/dist/ui/icons/sprite.mjs +26 -0
  219. package/dist/ui/icons/sprite.mjs.map +1 -0
  220. package/dist/ui/kbd.mjs +33 -0
  221. package/dist/ui/kbd.mjs.map +1 -0
  222. package/dist/ui/layout.mjs +173 -0
  223. package/dist/ui/layout.mjs.map +1 -0
  224. package/dist/ui/progress.mjs +44 -0
  225. package/dist/ui/progress.mjs.map +1 -0
  226. package/dist/ui/segmented.mjs +77 -0
  227. package/dist/ui/segmented.mjs.map +1 -0
  228. package/dist/ui/shell/account.mjs +161 -0
  229. package/dist/ui/shell/account.mjs.map +1 -0
  230. package/dist/ui/shell/bare.mjs +69 -0
  231. package/dist/ui/shell/bare.mjs.map +1 -0
  232. package/dist/ui/shell/boot-script.mjs +46 -0
  233. package/dist/ui/shell/boot-script.mjs.map +1 -0
  234. package/dist/ui/shell/client-script.mjs +259 -0
  235. package/dist/ui/shell/client-script.mjs.map +1 -0
  236. package/dist/ui/shell/nav.mjs +111 -0
  237. package/dist/ui/shell/nav.mjs.map +1 -0
  238. package/dist/ui/shell/review-script.mjs +195 -0
  239. package/dist/ui/shell/review-script.mjs.map +1 -0
  240. package/dist/ui/shell/sidebar.mjs +323 -0
  241. package/dist/ui/shell/sidebar.mjs.map +1 -0
  242. package/dist/ui/shell/topbar.mjs +268 -0
  243. package/dist/ui/shell/topbar.mjs.map +1 -0
  244. package/dist/ui/stacks.mjs +82 -0
  245. package/dist/ui/stacks.mjs.map +1 -0
  246. package/dist/ui/styles/base.mjs +41 -0
  247. package/dist/ui/styles/base.mjs.map +1 -0
  248. package/dist/ui/styles/behavior.mjs +43 -0
  249. package/dist/ui/styles/behavior.mjs.map +1 -0
  250. package/dist/ui/styles/review-layout.mjs +347 -0
  251. package/dist/ui/styles/review-layout.mjs.map +1 -0
  252. package/dist/ui/styles/review.mjs +86 -0
  253. package/dist/ui/styles/review.mjs.map +1 -0
  254. package/dist/ui/styles/tokens.mjs +132 -0
  255. package/dist/ui/styles/tokens.mjs.map +1 -0
  256. package/dist/ui/styles.mjs +19 -0
  257. package/dist/ui/styles.mjs.map +1 -0
  258. package/dist/ui/subnav.mjs +91 -0
  259. package/dist/ui/subnav.mjs.map +1 -0
  260. package/dist/ui/table.mjs +73 -0
  261. package/dist/ui/table.mjs.map +1 -0
  262. package/dist/ui/theme.mjs +64 -0
  263. package/dist/ui/theme.mjs.map +1 -0
  264. package/dist/ui/thumbnail.mjs +51 -0
  265. package/dist/ui/thumbnail.mjs.map +1 -0
  266. package/dist/ui/time.mjs +50 -0
  267. package/dist/ui/time.mjs.map +1 -0
  268. package/package.json +92 -0
@@ -0,0 +1,143 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { getStore } from "../store.mjs";
5
+ import { Button } from "../ui/buttons.mjs";
6
+ import { Badge, EmptyState } from "../ui/feedback.mjs";
7
+ import { SelectField } from "../ui/forms.mjs";
8
+ import { PageHeader } from "../ui/layout.mjs";
9
+ import { HStack, VStack } from "../ui/stacks.mjs";
10
+ import { Segmented } from "../ui/segmented.mjs";
11
+ import { DocumentLayout } from "../ui/document.mjs";
12
+ import { BuildsTable } from "./project-builds-table.mjs";
13
+ import { BuildModel, ProjectModel } from "@storyshelf/core/models";
14
+ import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
15
+ import { createUrlBuilder } from "@storyshelf/core/urls";
16
+ //#region src/pages/project-builds.tsx
17
+ const STATUSES = [
18
+ "reviewing",
19
+ "approved",
20
+ "rejected",
21
+ "failed",
22
+ "pending"
23
+ ];
24
+ function listHref(base, filters) {
25
+ const params = new URLSearchParams();
26
+ if (filters.status) params.set("status", filters.status);
27
+ if (filters.branch) params.set("branch", filters.branch);
28
+ const query = params.toString();
29
+ return query ? `${base}?${query}` : base;
30
+ }
31
+ function matches(build, filters) {
32
+ return (!filters.status || build.status === filters.status) && (!filters.branch || build.gitBranch === filters.branch);
33
+ }
34
+ /** Status chips (with counts) plus a branch select that applies instantly. */
35
+ function BuildsToolbar(props) {
36
+ const { all, filters, base } = props;
37
+ const branches = [...new Set(all.map((build) => build.gitBranch))].toSorted();
38
+ const inBranch = all.filter((build) => !filters.branch || build.gitBranch === filters.branch);
39
+ const count = (status) => inBranch.filter((build) => build.status === status).length;
40
+ const items = [{
41
+ label: `All ${inBranch.length}`,
42
+ value: "",
43
+ status: void 0
44
+ }, ...STATUSES.filter((status) => count(status) > 0 || filters.status === status).map((status) => ({
45
+ label: `${status} ${count(status)}`,
46
+ value: status,
47
+ status
48
+ }))];
49
+ return /* @__PURE__ */ jsxs(HStack, {
50
+ justify: "between",
51
+ children: [/* @__PURE__ */ jsx(Segmented, {
52
+ label: "Filter by status",
53
+ items: items.map((item) => ({
54
+ label: item.label,
55
+ value: item.value,
56
+ href: listHref(base, {
57
+ ...filters,
58
+ status: item.status
59
+ }),
60
+ active: (filters.status ?? "") === item.value
61
+ }))
62
+ }), /* @__PURE__ */ jsxs("form", {
63
+ method: "get",
64
+ action: base,
65
+ "hx-get": base,
66
+ "hx-trigger": "change",
67
+ "hx-target": "body",
68
+ "hx-push-url": "true",
69
+ children: [filters.status ? /* @__PURE__ */ jsx("input", {
70
+ type: "hidden",
71
+ name: "status",
72
+ value: filters.status
73
+ }) : null, /* @__PURE__ */ jsx(SelectField, {
74
+ label: "Branch",
75
+ name: "branch",
76
+ layout: "inline",
77
+ value: filters.branch ?? "",
78
+ options: [{
79
+ value: "",
80
+ label: "All branches"
81
+ }, ...branches.map((branch) => ({
82
+ value: branch,
83
+ label: branch
84
+ }))]
85
+ })]
86
+ })]
87
+ });
88
+ }
89
+ /** Project builds page: filterable build history for one project. */
90
+ async function renderProjectBuildsPage(slug, query = {}) {
91
+ const project = (await new ProjectModel(getStore().db).list()).find((item) => item.slug === slug);
92
+ if (!project) return null;
93
+ const all = await new BuildModel(getStore().db).list(project.id);
94
+ const builds = all.filter((build) => matches(build, query));
95
+ const urls = createUrlBuilder("/", getStore().config.publishedBaseDomain);
96
+ const filtered = Boolean(query.status ?? query.branch);
97
+ return /* @__PURE__ */ jsxs(DocumentLayout, {
98
+ title: project.name,
99
+ nav: {
100
+ active: "builds",
101
+ projectSlug: project.slug,
102
+ projectName: project.name
103
+ },
104
+ children: [/* @__PURE__ */ jsx(PageHeader, {
105
+ title: "Builds",
106
+ description: /* @__PURE__ */ jsxs(Fragment, { children: [
107
+ project.gitRepository ?? project.slug,
108
+ " · default branch",
109
+ " ",
110
+ /* @__PURE__ */ jsx(Badge, {
111
+ tone: "neutral",
112
+ children: project.gitDefaultBranch
113
+ })
114
+ ] }),
115
+ actions: /* @__PURE__ */ jsx(Button, {
116
+ variant: "secondary",
117
+ icon: "settings",
118
+ href: urls.settings(project.slug),
119
+ children: "Settings"
120
+ })
121
+ }), /* @__PURE__ */ jsxs(VStack, { children: [/* @__PURE__ */ jsx(BuildsToolbar, {
122
+ project,
123
+ all,
124
+ filters: query,
125
+ base: urls.buildsList(project.slug)
126
+ }), builds.length === 0 ? /* @__PURE__ */ jsx(EmptyState, {
127
+ title: "No builds",
128
+ description: filtered ? "No builds match the current filter." : "Upload your first build with the CLI. Builds appear here once uploaded.",
129
+ action: filtered ? /* @__PURE__ */ jsx(Button, {
130
+ variant: "secondary",
131
+ href: urls.buildsList(project.slug),
132
+ children: "Clear filters"
133
+ }) : null
134
+ }) : /* @__PURE__ */ jsx(BuildsTable, {
135
+ project,
136
+ builds
137
+ })] })]
138
+ });
139
+ }
140
+ //#endregion
141
+ export { renderProjectBuildsPage };
142
+
143
+ //# sourceMappingURL=project-builds.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-builds.mjs","names":[],"sources":["../../src/pages/project-builds.tsx"],"sourcesContent":["import { BuildModel, ProjectModel } from \"@storyshelf/core/models\";\nimport type { Build, Project } from \"@storyshelf/core/schema\";\nimport { createUrlBuilder } from \"@storyshelf/core/urls\";\nimport { getStore } from \"../store.ts\";\nimport {\n Badge,\n Button,\n EmptyState,\n HStack,\n PageHeader,\n Segmented,\n SelectField,\n VStack,\n} from \"../ui/components.tsx\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\nimport { BuildsTable } from \"./project-builds-table.tsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\nconst STATUSES = [\"reviewing\", \"approved\", \"rejected\", \"failed\", \"pending\"] as const;\n\n/** Current list filters (from the query string). */\ninterface BuildFilters {\n status?: string;\n branch?: string;\n}\n\nfunction listHref(base: string, filters: BuildFilters): string {\n const params = new URLSearchParams();\n if (filters.status) {\n params.set(\"status\", filters.status);\n }\n if (filters.branch) {\n params.set(\"branch\", filters.branch);\n }\n const query = params.toString();\n return query ? `${base}?${query}` : base;\n}\n\nfunction matches(build: Build, filters: BuildFilters): boolean {\n return (\n (!filters.status || build.status === filters.status) &&\n (!filters.branch || build.gitBranch === filters.branch)\n );\n}\n\n/** Status chips (with counts) plus a branch select that applies instantly. */\nfunction BuildsToolbar(props: {\n project: Project;\n all: Build[];\n filters: BuildFilters;\n base: string;\n}): ReturnType<typeof HStack> {\n const { all, filters, base } = props;\n const branches = [...new Set(all.map((build) => build.gitBranch))].toSorted();\n const inBranch = all.filter((build) => !filters.branch || build.gitBranch === filters.branch);\n const count = (status: string): number =>\n inBranch.filter((build) => build.status === status).length;\n const items = [\n { label: `All ${inBranch.length}`, value: \"\", status: undefined },\n ...STATUSES.filter((status) => count(status) > 0 || filters.status === status).map(\n (status) => ({ label: `${status} ${count(status)}`, value: status, status }),\n ),\n ];\n return (\n <HStack justify=\"between\">\n <Segmented\n label=\"Filter by status\"\n items={items.map((item) => ({\n label: item.label,\n value: item.value,\n href: listHref(base, { ...filters, status: item.status }),\n active: (filters.status ?? \"\") === item.value,\n }))}\n />\n <form\n method=\"get\"\n action={base}\n hx-get={base}\n hx-trigger=\"change\"\n hx-target=\"body\"\n hx-push-url=\"true\"\n >\n {filters.status ? <input type=\"hidden\" name=\"status\" value={filters.status} /> : null}\n <SelectField\n label=\"Branch\"\n name=\"branch\"\n layout=\"inline\"\n value={filters.branch ?? \"\"}\n options={[\n { value: \"\", label: \"All branches\" },\n ...branches.map((branch) => ({ value: branch, label: branch })),\n ]}\n />\n </form>\n </HStack>\n );\n}\n\n/** Project builds page: filterable build history for one project. */\nexport async function renderProjectBuildsPage(\n slug: string,\n query: BuildFilters = {},\n): Promise<RenderedContent | null> {\n const projects = await new ProjectModel(getStore().db).list();\n const project = projects.find((item) => item.slug === slug);\n if (!project) {\n return null;\n }\n const all = await new BuildModel(getStore().db).list(project.id);\n const builds = all.filter((build) => matches(build, query));\n const urls = createUrlBuilder(\"/\", getStore().config.publishedBaseDomain);\n const filtered = Boolean(query.status ?? query.branch);\n\n return (\n <DocumentLayout\n title={project.name}\n nav={{ active: \"builds\", projectSlug: project.slug, projectName: project.name }}\n >\n <PageHeader\n title=\"Builds\"\n description={\n <>\n {project.gitRepository ?? project.slug} · default branch{\" \"}\n <Badge tone=\"neutral\">{project.gitDefaultBranch}</Badge>\n </>\n }\n actions={\n <Button variant=\"secondary\" icon=\"settings\" href={urls.settings(project.slug)}>\n Settings\n </Button>\n }\n />\n <VStack>\n <BuildsToolbar\n project={project}\n all={all}\n filters={query}\n base={urls.buildsList(project.slug)}\n />\n {builds.length === 0 ? (\n <EmptyState\n title=\"No builds\"\n description={\n filtered\n ? \"No builds match the current filter.\"\n : \"Upload your first build with the CLI. Builds appear here once uploaded.\"\n }\n action={\n filtered ? (\n <Button variant=\"secondary\" href={urls.buildsList(project.slug)}>\n Clear filters\n </Button>\n ) : null\n }\n />\n ) : (\n <BuildsTable project={project} builds={builds} />\n )}\n </VStack>\n </DocumentLayout>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,90 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { Button } from "../ui/buttons.mjs";
5
+ import { Alert } from "../ui/feedback.mjs";
6
+ import { Field } from "../ui/forms.mjs";
7
+ import { Card, PageHeader } from "../ui/layout.mjs";
8
+ import { HStack } from "../ui/stacks.mjs";
9
+ import { DocumentLayout } from "../ui/document.mjs";
10
+ import { jsx, jsxs } from "hono/jsx/jsx-runtime";
11
+ //#region src/pages/project-create.tsx
12
+ /** New-project form page (one project corresponds to one Storybook). */
13
+ function renderProjectCreatePage(state = {}) {
14
+ const values = state.values ?? {};
15
+ const errors = state.errors ?? {};
16
+ return /* @__PURE__ */ jsxs(DocumentLayout, {
17
+ title: "New project",
18
+ nav: {
19
+ active: "projects",
20
+ trail: [{ label: "New project" }]
21
+ },
22
+ children: [
23
+ /* @__PURE__ */ jsx(PageHeader, {
24
+ title: "Create project",
25
+ description: "A project corresponds to one Storybook. You can have multiple projects per repository."
26
+ }),
27
+ state.globalError ? /* @__PURE__ */ jsx(Alert, {
28
+ tone: "danger",
29
+ title: "Could not create project",
30
+ children: state.globalError
31
+ }) : null,
32
+ /* @__PURE__ */ jsx("div", {
33
+ class: "max-w-prose",
34
+ children: /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs("form", {
35
+ method: "post",
36
+ action: "/projects/new",
37
+ "hx-post": "/projects/new",
38
+ "hx-target": "body",
39
+ "hx-swap": "outerHTML",
40
+ novalidate: true,
41
+ children: [
42
+ /* @__PURE__ */ jsx(Field, {
43
+ label: "Name",
44
+ name: "name",
45
+ required: true,
46
+ autofocus: true,
47
+ value: values.name ?? "",
48
+ placeholder: "Design System",
49
+ error: errors["name"],
50
+ hint: "Human readable name. A URL-friendly slug is generated automatically.",
51
+ autocomplete: "off"
52
+ }),
53
+ /* @__PURE__ */ jsx(Field, {
54
+ label: "Git repository",
55
+ name: "gitRepository",
56
+ value: values.gitRepository ?? "",
57
+ placeholder: "acme/design-system",
58
+ error: errors["gitRepository"],
59
+ hint: "Optional owner/repo for status checks and links."
60
+ }),
61
+ /* @__PURE__ */ jsx(Field, {
62
+ label: "Default branch",
63
+ name: "gitDefaultBranch",
64
+ value: values.gitDefaultBranch ?? "main",
65
+ placeholder: "main",
66
+ hint: "Baselines fall back to this branch when no branch-specific baseline exists."
67
+ }),
68
+ /* @__PURE__ */ jsx("div", {
69
+ class: "mt-1",
70
+ children: /* @__PURE__ */ jsxs(HStack, { children: [/* @__PURE__ */ jsx(Button, {
71
+ variant: "primary",
72
+ type: "submit",
73
+ icon: "plus",
74
+ children: "Create project"
75
+ }), /* @__PURE__ */ jsx(Button, {
76
+ variant: "secondary",
77
+ href: "/projects",
78
+ children: "Cancel"
79
+ })] })
80
+ })
81
+ ]
82
+ }) })
83
+ })
84
+ ]
85
+ });
86
+ }
87
+ //#endregion
88
+ export { renderProjectCreatePage };
89
+
90
+ //# sourceMappingURL=project-create.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-create.mjs","names":[],"sources":["../../src/pages/project-create.tsx"],"sourcesContent":["import { Alert, Button, Card, Field, HStack, PageHeader } from \"../ui/components.tsx\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\n\n/** Form state for the project creation page (values and validation errors). */\nexport interface ProjectCreateFormState {\n values?: { name?: string; gitRepository?: string; gitDefaultBranch?: string };\n errors?: Record<string, string>;\n globalError?: string;\n}\n\n/** New-project form page (one project corresponds to one Storybook). */\nexport function renderProjectCreatePage(state: ProjectCreateFormState = {}): RenderedContent {\n const values = state.values ?? {};\n const errors = state.errors ?? {};\n return (\n <DocumentLayout\n title=\"New project\"\n nav={{ active: \"projects\", trail: [{ label: \"New project\" }] }}\n >\n <PageHeader\n title=\"Create project\"\n description=\"A project corresponds to one Storybook. You can have multiple projects per repository.\"\n />\n\n {state.globalError ? (\n <Alert tone=\"danger\" title=\"Could not create project\">\n {state.globalError}\n </Alert>\n ) : null}\n\n <div class=\"max-w-prose\">\n <Card>\n <form\n method=\"post\"\n action=\"/projects/new\"\n hx-post=\"/projects/new\"\n hx-target=\"body\"\n hx-swap=\"outerHTML\"\n novalidate\n >\n <Field\n label=\"Name\"\n name=\"name\"\n required\n autofocus\n value={values.name ?? \"\"}\n placeholder=\"Design System\"\n error={errors[\"name\"]}\n hint=\"Human readable name. A URL-friendly slug is generated automatically.\"\n autocomplete=\"off\"\n />\n\n <Field\n label=\"Git repository\"\n name=\"gitRepository\"\n value={values.gitRepository ?? \"\"}\n placeholder=\"acme/design-system\"\n error={errors[\"gitRepository\"]}\n hint=\"Optional owner/repo for status checks and links.\"\n />\n\n <Field\n label=\"Default branch\"\n name=\"gitDefaultBranch\"\n value={values.gitDefaultBranch ?? \"main\"}\n placeholder=\"main\"\n hint=\"Baselines fall back to this branch when no branch-specific baseline exists.\"\n />\n\n <div class=\"mt-1\">\n <HStack>\n <Button variant=\"primary\" type=\"submit\" icon=\"plus\">\n Create project\n </Button>\n <Button variant=\"secondary\" href=\"/projects\">\n Cancel\n </Button>\n </HStack>\n </div>\n </form>\n </Card>\n </div>\n </DocumentLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;AAWA,SAAgB,wBAAwB,QAAgC,CAAC,GAAoB;CAC3F,MAAM,SAAS,MAAM,UAAU,CAAC;CAChC,MAAM,SAAS,MAAM,UAAU,CAAC;CAChC,OACE,qBAAC,gBAAD;EACE,OAAM;EACN,KAAK;GAAE,QAAQ;GAAY,OAAO,CAAC,EAAE,OAAO,cAAc,CAAC;EAAE;EAF/D,UAAA;GAIE,oBAAC,YAAD;IACE,OAAM;IACN,aAAY;GACb,CAAA;GAEA,MAAM,cACL,oBAAC,OAAD;IAAO,MAAK;IAAS,OAAM;IACxB,UAAA,MAAM;GACF,CAAA,IACL;GAEJ,oBAAC,OAAD;IAAK,OAAM;IACT,UAAA,oBAAC,MAAD,EAAA,UACE,qBAAC,QAAD;KACE,QAAO;KACP,QAAO;KACP,WAAQ;KACR,aAAU;KACV,WAAQ;KACR,YAAA;KANF,UAAA;MAQE,oBAAC,OAAD;OACE,OAAM;OACN,MAAK;OACL,UAAA;OACA,WAAA;OACA,OAAO,OAAO,QAAQ;OACtB,aAAY;OACZ,OAAO,OAAO;OACd,MAAK;OACL,cAAa;MACd,CAAA;MAED,oBAAC,OAAD;OACE,OAAM;OACN,MAAK;OACL,OAAO,OAAO,iBAAiB;OAC/B,aAAY;OACZ,OAAO,OAAO;OACd,MAAK;MACN,CAAA;MAED,oBAAC,OAAD;OACE,OAAM;OACN,MAAK;OACL,OAAO,OAAO,oBAAoB;OAClC,aAAY;OACZ,MAAK;MACN,CAAA;MAED,oBAAC,OAAD;OAAK,OAAM;OACT,UAAA,qBAAC,QAAD,EAAA,UAAA,CACE,oBAAC,QAAD;QAAQ,SAAQ;QAAU,MAAK;QAAS,MAAK;QAAO,UAAA;OAE5C,CAAA,GACR,oBAAC,QAAD;QAAQ,SAAQ;QAAY,MAAK;QAAY,UAAA;OAErC,CAAA,CACF,EAAA,CAAA;MACL,CAAA;KACD;IACF,CAAA,EAAA,CAAA;GACH,CAAA;EACS;;AAEpB"}
@@ -0,0 +1,95 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { PageHeader } from "../ui/layout.mjs";
5
+ import { SubNavLayout } from "../ui/subnav.mjs";
6
+ import { DocumentLayout } from "../ui/document.mjs";
7
+ import { renderSettingsGeneral } from "./settings-general.mjs";
8
+ import { renderSettingsLabels } from "./settings-labels.mjs";
9
+ import { renderSettingsMembers } from "./settings-members.mjs";
10
+ import { renderSettingsNotifications } from "./settings-notifications.mjs";
11
+ import { renderSettingsStatus } from "./settings-status.mjs";
12
+ import { renderSettingsTests } from "./settings-tests.mjs";
13
+ import { renderSettingsTokens } from "./settings-tokens.mjs";
14
+ import { renderSettingsWebhooks } from "./settings-webhooks.mjs";
15
+ import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
16
+ //#region src/pages/project-settings.tsx
17
+ function tabHref(project, tab) {
18
+ return tab === "general" ? `/projects/${project.slug}/settings` : `/projects/${project.slug}/settings/${tab}`;
19
+ }
20
+ const TAB_LABELS = {
21
+ general: "General",
22
+ tests: "Tests",
23
+ labels: "Labels",
24
+ tokens: "Tokens",
25
+ webhooks: "Webhooks",
26
+ notifications: "Notifications",
27
+ members: "Members",
28
+ status: "Git status"
29
+ };
30
+ const TAB_ICONS = {
31
+ general: "settings",
32
+ tests: "activity",
33
+ labels: "tag",
34
+ tokens: "key",
35
+ webhooks: "webhook",
36
+ notifications: "bell",
37
+ members: "users",
38
+ status: "git-branch"
39
+ };
40
+ const SETTINGS_TABS = [
41
+ "general",
42
+ "tests",
43
+ "labels",
44
+ "tokens",
45
+ "webhooks",
46
+ "notifications",
47
+ "members",
48
+ "status"
49
+ ];
50
+ function renderActiveTab(data, formState) {
51
+ const { project, activeTab } = data;
52
+ if (activeTab === "general") return renderSettingsGeneral(project, formState, data.isAdmin);
53
+ if (activeTab === "tests") return renderSettingsTests(project, data.isAdmin, formState);
54
+ if (activeTab === "labels") return renderSettingsLabels(project, data.labelTypes, data.isAdmin, formState);
55
+ if (activeTab === "tokens") return renderSettingsTokens(project, data.tokens, data.isAdmin, formState);
56
+ if (activeTab === "webhooks") return renderSettingsWebhooks(project, data.webhooks, data.isAdmin, formState);
57
+ if (activeTab === "notifications") return renderSettingsNotifications(project, data.notificationChannels, data.notifyProviders, data.mySubscription, data.isAdmin, formState);
58
+ if (activeTab === "members") return renderSettingsMembers(project, data.members, data.groupMappings, data.isAdmin, formState);
59
+ if (activeTab === "status") return renderSettingsStatus(project, data.statusConfigs, data.gitHosts, data.isAdmin, formState);
60
+ return null;
61
+ }
62
+ /** Project settings shell: tab navigation plus the active settings tab. */
63
+ function renderProjectSettingsPage(data, formState) {
64
+ const { project, activeTab } = data;
65
+ return /* @__PURE__ */ jsxs(DocumentLayout, {
66
+ title: `${project.name} · Settings`,
67
+ nav: {
68
+ active: "settings",
69
+ projectSlug: project.slug,
70
+ projectName: project.name,
71
+ trail: activeTab === "general" ? [] : [{ label: TAB_LABELS[activeTab] }]
72
+ },
73
+ children: [/* @__PURE__ */ jsx(PageHeader, {
74
+ title: "Project settings",
75
+ description: /* @__PURE__ */ jsxs(Fragment, { children: [
76
+ "Manage general settings, labels, tokens, webhooks and members for ",
77
+ project.name,
78
+ "."
79
+ ] })
80
+ }), /* @__PURE__ */ jsx(SubNavLayout, {
81
+ label: "Settings sections",
82
+ items: SETTINGS_TABS.map((tab) => ({
83
+ label: TAB_LABELS[tab],
84
+ href: tabHref(project, tab),
85
+ icon: TAB_ICONS[tab],
86
+ active: activeTab === tab
87
+ })),
88
+ children: renderActiveTab(data, formState)
89
+ })]
90
+ });
91
+ }
92
+ //#endregion
93
+ export { renderProjectSettingsPage };
94
+
95
+ //# sourceMappingURL=project-settings.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-settings.mjs","names":[],"sources":["../../src/pages/project-settings.tsx"],"sourcesContent":["import type { GitHostProvider } from \"@storyshelf/core/adapter/git-host\";\nimport type { LabelType } from \"@storyshelf/core/schema\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport type { ProjectGroupMapping } from \"@storyshelf/core/schema\";\nimport type { Token } from \"@storyshelf/core/schema\";\nimport { PageHeader, SubNavLayout } from \"../ui/components.tsx\";\nimport type { IconName } from \"../ui/components.tsx\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\nimport { renderSettingsGeneral } from \"./settings-general.tsx\";\nimport { renderSettingsLabels } from \"./settings-labels.tsx\";\nimport { renderSettingsMembers, type SettingsMember } from \"./settings-members.tsx\";\nimport {\n renderSettingsNotifications,\n type SettingsMySubscription,\n type SettingsNotificationChannel,\n} from \"./settings-notifications.tsx\";\nimport { renderSettingsStatus, type SettingsStatusConfig } from \"./settings-status.tsx\";\nimport { renderSettingsTests } from \"./settings-tests.tsx\";\nimport { renderSettingsTokens } from \"./settings-tokens.tsx\";\nimport { renderSettingsWebhooks, type SettingsWebhook } from \"./settings-webhooks.tsx\";\n\n/** Tabs available in the project settings section. */\nexport type SettingsTab =\n | \"general\"\n | \"tests\"\n | \"labels\"\n | \"tokens\"\n | \"webhooks\"\n | \"notifications\"\n | \"members\"\n | \"status\";\n\n/** Data required to render the project settings page with its active tab. */\nexport interface ProjectSettingsData {\n project: Project;\n activeTab: SettingsTab;\n labelTypes: LabelType[];\n tokens: Omit<Token, \"hash\">[];\n members: SettingsMember[];\n groupMappings: ProjectGroupMapping[];\n webhooks: SettingsWebhook[];\n notificationChannels: SettingsNotificationChannel[];\n notifyProviders: string[];\n mySubscription: SettingsMySubscription | null;\n statusConfigs: SettingsStatusConfig[];\n gitHosts: GitHostProvider[];\n isAdmin: boolean;\n}\n\n/** Form state shared by the settings tabs (field errors, global error, one-time secret). */\nexport interface SettingsFormState {\n errors?: Record<string, string>;\n globalError?: string;\n secret?: string;\n}\n\nfunction tabHref(project: Project, tab: SettingsTab): string {\n return tab === \"general\"\n ? `/projects/${project.slug}/settings`\n : `/projects/${project.slug}/settings/${tab}`;\n}\n\nconst TAB_LABELS: Record<SettingsTab, string> = {\n general: \"General\",\n tests: \"Tests\",\n labels: \"Labels\",\n tokens: \"Tokens\",\n webhooks: \"Webhooks\",\n notifications: \"Notifications\",\n members: \"Members\",\n status: \"Git status\",\n};\n\nconst TAB_ICONS: Record<SettingsTab, IconName> = {\n general: \"settings\",\n tests: \"activity\",\n labels: \"tag\",\n tokens: \"key\",\n webhooks: \"webhook\",\n notifications: \"bell\",\n members: \"users\",\n status: \"git-branch\",\n};\n\nconst SETTINGS_TABS: SettingsTab[] = [\n \"general\",\n \"tests\",\n \"labels\",\n \"tokens\",\n \"webhooks\",\n \"notifications\",\n \"members\",\n \"status\",\n];\n\nfunction renderActiveTab(data: ProjectSettingsData, formState?: SettingsFormState): unknown {\n const { project, activeTab } = data;\n if (activeTab === \"general\") return renderSettingsGeneral(project, formState, data.isAdmin);\n if (activeTab === \"tests\") return renderSettingsTests(project, data.isAdmin, formState);\n if (activeTab === \"labels\")\n return renderSettingsLabels(project, data.labelTypes, data.isAdmin, formState);\n if (activeTab === \"tokens\")\n return renderSettingsTokens(project, data.tokens, data.isAdmin, formState);\n if (activeTab === \"webhooks\")\n return renderSettingsWebhooks(project, data.webhooks, data.isAdmin, formState);\n if (activeTab === \"notifications\")\n return renderSettingsNotifications(\n project,\n data.notificationChannels,\n data.notifyProviders,\n data.mySubscription,\n data.isAdmin,\n formState,\n );\n if (activeTab === \"members\")\n return renderSettingsMembers(\n project,\n data.members,\n data.groupMappings,\n data.isAdmin,\n formState,\n );\n if (activeTab === \"status\")\n return renderSettingsStatus(\n project,\n data.statusConfigs,\n data.gitHosts,\n data.isAdmin,\n formState,\n );\n return null;\n}\n\n/** Project settings shell: tab navigation plus the active settings tab. */\nexport function renderProjectSettingsPage(\n data: ProjectSettingsData,\n formState?: SettingsFormState,\n): RenderedContent {\n const { project, activeTab } = data;\n return (\n <DocumentLayout\n title={`${project.name} · Settings`}\n nav={{\n active: \"settings\",\n projectSlug: project.slug,\n projectName: project.name,\n trail: activeTab === \"general\" ? [] : [{ label: TAB_LABELS[activeTab] }],\n }}\n >\n <PageHeader\n title=\"Project settings\"\n description={\n <>Manage general settings, labels, tokens, webhooks and members for {project.name}.</>\n }\n />\n\n <SubNavLayout\n label=\"Settings sections\"\n items={SETTINGS_TABS.map((tab) => ({\n label: TAB_LABELS[tab],\n href: tabHref(project, tab),\n icon: TAB_ICONS[tab],\n active: activeTab === tab,\n }))}\n >\n {renderActiveTab(data, formState)}\n </SubNavLayout>\n </DocumentLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAwDA,SAAS,QAAQ,SAAkB,KAA0B;CAC3D,OAAO,QAAQ,YACX,aAAa,QAAQ,KAAK,aAC1B,aAAa,QAAQ,KAAK,YAAY;AAC5C;AAEA,MAAM,aAA0C;CAC9C,SAAS;CACT,OAAO;CACP,QAAQ;CACR,QAAQ;CACR,UAAU;CACV,eAAe;CACf,SAAS;CACT,QAAQ;AACV;AAEA,MAAM,YAA2C;CAC/C,SAAS;CACT,OAAO;CACP,QAAQ;CACR,QAAQ;CACR,UAAU;CACV,eAAe;CACf,SAAS;CACT,QAAQ;AACV;AAEA,MAAM,gBAA+B;CACnC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAEA,SAAS,gBAAgB,MAA2B,WAAwC;CAC1F,MAAM,EAAE,SAAS,cAAc;CAC/B,IAAI,cAAc,WAAW,OAAO,sBAAsB,SAAS,WAAW,KAAK,OAAO;CAC1F,IAAI,cAAc,SAAS,OAAO,oBAAoB,SAAS,KAAK,SAAS,SAAS;CACtF,IAAI,cAAc,UAChB,OAAO,qBAAqB,SAAS,KAAK,YAAY,KAAK,SAAS,SAAS;CAC/E,IAAI,cAAc,UAChB,OAAO,qBAAqB,SAAS,KAAK,QAAQ,KAAK,SAAS,SAAS;CAC3E,IAAI,cAAc,YAChB,OAAO,uBAAuB,SAAS,KAAK,UAAU,KAAK,SAAS,SAAS;CAC/E,IAAI,cAAc,iBAChB,OAAO,4BACL,SACA,KAAK,sBACL,KAAK,iBACL,KAAK,gBACL,KAAK,SACL,SACF;CACF,IAAI,cAAc,WAChB,OAAO,sBACL,SACA,KAAK,SACL,KAAK,eACL,KAAK,SACL,SACF;CACF,IAAI,cAAc,UAChB,OAAO,qBACL,SACA,KAAK,eACL,KAAK,UACL,KAAK,SACL,SACF;CACF,OAAO;AACT;;AAGA,SAAgB,0BACd,MACA,WACiB;CACjB,MAAM,EAAE,SAAS,cAAc;CAC/B,OACE,qBAAC,gBAAD;EACE,OAAO,GAAG,QAAQ,KAAK;EACvB,KAAK;GACH,QAAQ;GACR,aAAa,QAAQ;GACrB,aAAa,QAAQ;GACrB,OAAO,cAAc,YAAY,CAAC,IAAI,CAAC,EAAE,OAAO,WAAW,WAAW,CAAC;EACzE;EAPF,UAAA,CASE,oBAAC,YAAD;GACE,OAAM;GACN,aACE,qBAAA,UAAA,EAAA,UAAA;IAAE;IAAmE,QAAQ;IAAK;GAAG,EAAA,CAAA;EAExF,CAAA,GAED,oBAAC,cAAD;GACE,OAAM;GACN,OAAO,cAAc,KAAK,SAAS;IACjC,OAAO,WAAW;IAClB,MAAM,QAAQ,SAAS,GAAG;IAC1B,MAAM,UAAU;IAChB,QAAQ,cAAc;GACxB,EAAE;GAED,UAAA,gBAAgB,MAAM,SAAS;EACpB,CAAA,CACA;;AAEpB"}
@@ -0,0 +1,194 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { css } from "../ui/css.mjs";
5
+ import { Button } from "../ui/buttons.mjs";
6
+ import { Badge, Meta, statusTone } from "../ui/feedback.mjs";
7
+ import { Card } from "../ui/layout.mjs";
8
+ import { Thumbnail } from "../ui/thumbnail.mjs";
9
+ import { RelativeTime } from "../ui/time.mjs";
10
+ import { jsx, jsxs } from "hono/jsx/jsx-runtime";
11
+ //#region src/pages/projects-card.tsx
12
+ const thumbStrip = css`
13
+ /* project-thumbs */
14
+ display: grid;
15
+ grid-template-columns: repeat(3, minmax(0, 1fr));
16
+ gap: var(--space-2);
17
+ padding: var(--space-3);
18
+ background: var(--accent-wash);
19
+ border-bottom: 1px solid var(--border-subtle);
20
+ `;
21
+ const cardBody = css`
22
+ /* project-body */
23
+ flex: 1;
24
+ display: grid;
25
+ align-content: start;
26
+ gap: var(--space-2);
27
+ padding: var(--space-4);
28
+ `;
29
+ const cardTitleRow = css`
30
+ /* project-title-row */
31
+ display: flex;
32
+ gap: var(--space-2);
33
+ align-items: flex-start;
34
+ justify-content: space-between;
35
+ `;
36
+ const cardTitle = css`
37
+ /* project-title */
38
+ margin: 0;
39
+ font-size: var(--text-lg);
40
+ font-weight: 650;
41
+ letter-spacing: -0.01em;
42
+ & a {
43
+ color: var(--text-primary);
44
+ }
45
+ & a:hover {
46
+ color: var(--accent-fg);
47
+ }
48
+ `;
49
+ const latestRow = css`
50
+ /* project-latest */
51
+ display: flex;
52
+ flex-wrap: wrap;
53
+ gap: var(--space-2);
54
+ align-items: center;
55
+ color: var(--text-secondary);
56
+ font-size: var(--text-sm);
57
+ `;
58
+ const cardFooter = css`
59
+ /* project-footer */
60
+ display: flex;
61
+ flex-wrap: wrap;
62
+ gap: var(--space-2);
63
+ padding: var(--space-3) var(--space-4);
64
+ border-top: 1px solid var(--border-subtle);
65
+ `;
66
+ function PendingBadge({ pending, href }) {
67
+ if (pending === 0) return /* @__PURE__ */ jsx(Badge, {
68
+ tone: "success",
69
+ icon: true,
70
+ children: "All clear"
71
+ });
72
+ return /* @__PURE__ */ jsx("a", {
73
+ href,
74
+ children: /* @__PURE__ */ jsxs(Badge, {
75
+ tone: "warning",
76
+ icon: true,
77
+ children: [pending, " to review"]
78
+ })
79
+ });
80
+ }
81
+ function LatestBuild({ latest }) {
82
+ if (!latest) return /* @__PURE__ */ jsx(Meta, { children: "No builds yet — upload one from CI." });
83
+ return /* @__PURE__ */ jsxs("div", {
84
+ class: latestRow,
85
+ children: [
86
+ /* @__PURE__ */ jsxs(Meta, {
87
+ as: "span",
88
+ mono: true,
89
+ children: [
90
+ latest.gitBranch,
91
+ " · ",
92
+ latest.gitSha.slice(0, 7)
93
+ ]
94
+ }),
95
+ /* @__PURE__ */ jsx(Badge, {
96
+ tone: statusTone(latest.status),
97
+ children: latest.status
98
+ }),
99
+ /* @__PURE__ */ jsx(RelativeTime, { value: latest.createdAt })
100
+ ]
101
+ });
102
+ }
103
+ function Previews({ project, latest, previews }) {
104
+ return /* @__PURE__ */ jsx("div", {
105
+ class: thumbStrip,
106
+ children: [
107
+ 0,
108
+ 1,
109
+ 2
110
+ ].map((slot) => {
111
+ const snap = previews[slot];
112
+ return /* @__PURE__ */ jsx(Thumbnail, {
113
+ src: snap && latest ? `/api/v1/projects/${project.slug}/builds/${latest.id}/snapshots/${snap.id}/image` : null,
114
+ alt: snap ? `${snap.storyTitle} / ${snap.storyName}` : "",
115
+ placeholder: ""
116
+ }, String(slot));
117
+ })
118
+ });
119
+ }
120
+ /** One project in the overview grid: previews, latest build, review state, actions. */
121
+ function ProjectCard(props) {
122
+ const { summary, urls } = props;
123
+ const { project, latest, previews, pending } = summary;
124
+ return /* @__PURE__ */ jsxs(Card, {
125
+ fill: true,
126
+ "data-filter-item": true,
127
+ "data-filter-text": `${project.name} ${project.slug} ${project.gitRepository ?? ""}`,
128
+ children: [
129
+ previews.length > 0 ? /* @__PURE__ */ jsx(Previews, {
130
+ project,
131
+ latest,
132
+ previews
133
+ }) : null,
134
+ /* @__PURE__ */ jsxs("div", {
135
+ class: cardBody,
136
+ children: [
137
+ /* @__PURE__ */ jsxs("div", {
138
+ class: cardTitleRow,
139
+ children: [/* @__PURE__ */ jsx("h2", {
140
+ class: cardTitle,
141
+ children: /* @__PURE__ */ jsx("a", {
142
+ href: urls.library,
143
+ children: project.name
144
+ })
145
+ }), latest ? /* @__PURE__ */ jsx(PendingBadge, {
146
+ pending,
147
+ href: urls.review
148
+ }) : null]
149
+ }),
150
+ /* @__PURE__ */ jsxs(Meta, { children: [
151
+ project.gitRepository ?? project.slug,
152
+ " · default",
153
+ " ",
154
+ /* @__PURE__ */ jsx(Badge, {
155
+ tone: "neutral",
156
+ children: project.gitDefaultBranch
157
+ })
158
+ ] }),
159
+ /* @__PURE__ */ jsx(LatestBuild, { latest })
160
+ ]
161
+ }),
162
+ /* @__PURE__ */ jsxs("div", {
163
+ class: cardFooter,
164
+ children: [
165
+ /* @__PURE__ */ jsx(Button, {
166
+ variant: "secondary",
167
+ size: "sm",
168
+ icon: "image",
169
+ href: urls.library,
170
+ children: "Library"
171
+ }),
172
+ /* @__PURE__ */ jsx(Button, {
173
+ variant: "ghost",
174
+ size: "sm",
175
+ icon: "external-link",
176
+ href: urls.storybook,
177
+ children: "Storybook"
178
+ }),
179
+ /* @__PURE__ */ jsx(Button, {
180
+ variant: "ghost",
181
+ size: "sm",
182
+ icon: "settings",
183
+ href: urls.settings,
184
+ children: "Settings"
185
+ })
186
+ ]
187
+ })
188
+ ]
189
+ });
190
+ }
191
+ //#endregion
192
+ export { ProjectCard };
193
+
194
+ //# sourceMappingURL=projects-card.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"projects-card.mjs","names":[],"sources":["../../src/pages/projects-card.tsx"],"sourcesContent":["import type { Build, Project, Snapshot } from \"@storyshelf/core/schema\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport {\n Badge,\n Button,\n Card,\n Meta,\n RelativeTime,\n Thumbnail,\n statusTone,\n} from \"../ui/components.tsx\";\nimport { css } from \"../ui/css.ts\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\nconst thumbStrip = css`\n /* project-thumbs */\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: var(--space-2);\n padding: var(--space-3);\n background: var(--accent-wash);\n border-bottom: 1px solid var(--border-subtle);\n`;\n\nconst cardBody = css`\n /* project-body */\n flex: 1;\n display: grid;\n align-content: start;\n gap: var(--space-2);\n padding: var(--space-4);\n`;\n\nconst cardTitleRow = css`\n /* project-title-row */\n display: flex;\n gap: var(--space-2);\n align-items: flex-start;\n justify-content: space-between;\n`;\n\nconst cardTitle = css`\n /* project-title */\n margin: 0;\n font-size: var(--text-lg);\n font-weight: 650;\n letter-spacing: -0.01em;\n & a {\n color: var(--text-primary);\n }\n & a:hover {\n color: var(--accent-fg);\n }\n`;\n\nconst latestRow = css`\n /* project-latest */\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-2);\n align-items: center;\n color: var(--text-secondary);\n font-size: var(--text-sm);\n`;\n\nconst cardFooter = css`\n /* project-footer */\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-2);\n padding: var(--space-3) var(--space-4);\n border-top: 1px solid var(--border-subtle);\n`;\n\n/** What the overview shows about a project's most recent build. */\nexport interface ProjectSummary {\n project: Project;\n latest: Build | null;\n /** Up to three screenshots of the latest build. */\n previews: Snapshot[];\n /** Snapshots in the latest build still waiting for a decision. */\n pending: number;\n}\n\nfunction PendingBadge({\n pending,\n href,\n}: {\n pending: number;\n href: string;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n if (pending === 0) {\n return (\n <Badge tone=\"success\" icon>\n All clear\n </Badge>\n );\n }\n return (\n <a href={href}>\n <Badge tone=\"warning\" icon>\n {pending} to review\n </Badge>\n </a>\n );\n}\n\nfunction LatestBuild({\n latest,\n}: {\n latest: Build | null;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n if (!latest) {\n return <Meta>No builds yet — upload one from CI.</Meta>;\n }\n return (\n <div class={latestRow}>\n <Meta as=\"span\" mono>\n {latest.gitBranch} · {latest.gitSha.slice(0, 7)}\n </Meta>\n <Badge tone={statusTone(latest.status)}>{latest.status}</Badge>\n <RelativeTime value={latest.createdAt} />\n </div>\n );\n}\n\nfunction Previews({\n project,\n latest,\n previews,\n}: Pick<ProjectSummary, \"project\" | \"latest\" | \"previews\">):\n | HtmlEscapedString\n | Promise<HtmlEscapedString> {\n return (\n <div class={thumbStrip}>\n {[0, 1, 2].map((slot): HtmlEscapedString | Promise<HtmlEscapedString> => {\n const snap = previews[slot];\n return (\n <Thumbnail\n key={String(slot)}\n src={\n snap && latest\n ? `/api/v1/projects/${project.slug}/builds/${latest.id}/snapshots/${snap.id}/image`\n : null\n }\n alt={snap ? `${snap.storyTitle} / ${snap.storyName}` : \"\"}\n placeholder=\"\"\n />\n );\n })}\n </div>\n );\n}\n\n/** One project in the overview grid: previews, latest build, review state, actions. */\nexport function ProjectCard(props: {\n summary: ProjectSummary;\n urls: { library: string; settings: string; storybook: string; review: string };\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { summary, urls } = props;\n const { project, latest, previews, pending } = summary;\n return (\n <Card\n fill\n data-filter-item\n data-filter-text={`${project.name} ${project.slug} ${project.gitRepository ?? \"\"}`}\n >\n {previews.length > 0 ? (\n <Previews project={project} latest={latest} previews={previews} />\n ) : null}\n <div class={cardBody}>\n <div class={cardTitleRow}>\n <h2 class={cardTitle}>\n <a href={urls.library}>{project.name}</a>\n </h2>\n {latest ? <PendingBadge pending={pending} href={urls.review} /> : null}\n </div>\n <Meta>\n {project.gitRepository ?? project.slug} · default{\" \"}\n <Badge tone=\"neutral\">{project.gitDefaultBranch}</Badge>\n </Meta>\n <LatestBuild latest={latest} />\n </div>\n <div class={cardFooter}>\n <Button variant=\"secondary\" size=\"sm\" icon=\"image\" href={urls.library}>\n Library\n </Button>\n <Button variant=\"ghost\" size=\"sm\" icon=\"external-link\" href={urls.storybook}>\n Storybook\n </Button>\n <Button variant=\"ghost\" size=\"sm\" icon=\"settings\" href={urls.settings}>\n Settings\n </Button>\n </div>\n </Card>\n );\n}\n"],"mappings":"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"}