@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,129 @@
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 { EmptyState, Meta } from "../ui/feedback.mjs";
8
+ import { Card, PageHeader } from "../ui/layout.mjs";
9
+ import { HStack, VStack } from "../ui/stacks.mjs";
10
+ import { CodeBlock } from "../ui/code-block.mjs";
11
+ import { FilterInput } from "../ui/filter-input.mjs";
12
+ import { DocumentLayout } from "../ui/document.mjs";
13
+ import { ProjectCard } from "./projects-card.mjs";
14
+ import { BuildModel, ProjectModel, SnapshotModel } from "@storyshelf/core/models";
15
+ import { jsx, jsxs } from "hono/jsx/jsx-runtime";
16
+ import { createUrlBuilder } from "@storyshelf/core/urls";
17
+ //#region src/pages/projects.tsx
18
+ const projectGrid = css`
19
+ /* project-grid */
20
+ display: grid;
21
+ grid-template-columns: repeat(auto-fill, minmax(min(100%, 320px), 1fr));
22
+ gap: var(--space-4);
23
+ `;
24
+ const setupSummary = css`
25
+ /* setup-summary */
26
+ cursor: pointer;
27
+ font-weight: 650;
28
+ font-size: var(--text-lg);
29
+ letter-spacing: -0.01em;
30
+ `;
31
+ /** Latest build, a few previews, and the review backlog for one project. */
32
+ async function summarize(project) {
33
+ const { db } = getStore();
34
+ const [latest] = await new BuildModel(db).list(project.id);
35
+ if (!latest) return {
36
+ project,
37
+ latest: null,
38
+ previews: [],
39
+ pending: 0
40
+ };
41
+ const snapshots = await new SnapshotModel(db).listByBuild(latest.id);
42
+ const pending = snapshots.filter((snap) => snap.status === "new" || snap.status === "changed");
43
+ return {
44
+ project,
45
+ latest,
46
+ previews: (pending.length > 0 ? pending : snapshots).slice(0, 3),
47
+ pending: pending.length
48
+ };
49
+ }
50
+ /** Copy-ready steps to get a first build uploaded. */
51
+ function SetupGuide({ open }) {
52
+ return /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs("details", {
53
+ open,
54
+ children: [/* @__PURE__ */ jsx("summary", {
55
+ class: setupSummary,
56
+ children: "Set up your first upload"
57
+ }), /* @__PURE__ */ jsxs(VStack, { children: [
58
+ /* @__PURE__ */ jsx(Meta, { children: "1. Create a project (writes .storybook/storyshelf.json for later runs)." }),
59
+ /* @__PURE__ */ jsx(CodeBlock, { code: "npx storyshelf create --url http://localhost:3000 --name \"My Storybook\" --token $STORYSHELF_ADMIN_TOKEN" }),
60
+ /* @__PURE__ */ jsxs(Meta, { children: [
61
+ "2. Generate a token in ",
62
+ /* @__PURE__ */ jsx("strong", { children: "Settings → Tokens" }),
63
+ " and set it in CI."
64
+ ] }),
65
+ /* @__PURE__ */ jsx(CodeBlock, { code: "export STORYSHELF_TOKEN=<your token>" }),
66
+ /* @__PURE__ */ jsx(Meta, { children: "3. Upload your built Storybook." }),
67
+ /* @__PURE__ */ jsx(CodeBlock, { code: "npx storyshelf upload" })
68
+ ] })]
69
+ }) });
70
+ }
71
+ /** Projects overview: searchable project cards with review state, plus setup help. */
72
+ async function renderProjectsPage() {
73
+ const { db, user, config } = getStore();
74
+ const urls = createUrlBuilder("/", config.publishedBaseDomain);
75
+ const projects = await new ProjectModel(db).list();
76
+ const summaries = await Promise.all(projects.map((project) => summarize(project)));
77
+ const newProject = !user || user.role === "admin" || user.role === "member" ? /* @__PURE__ */ jsx(Button, {
78
+ variant: "primary",
79
+ icon: "plus",
80
+ href: urls.projectsNew(),
81
+ children: "New project"
82
+ }) : null;
83
+ return /* @__PURE__ */ jsxs(DocumentLayout, {
84
+ title: "Projects",
85
+ nav: { active: "projects" },
86
+ children: [/* @__PURE__ */ jsx(PageHeader, {
87
+ title: "Projects",
88
+ description: "Each project is one Storybook. Create a project, then upload builds from CI.",
89
+ actions: newProject
90
+ }), /* @__PURE__ */ jsxs(VStack, {
91
+ gap: "lg",
92
+ children: [
93
+ projects.length > 1 ? /* @__PURE__ */ jsx(HStack, { children: /* @__PURE__ */ jsx(FilterInput, {
94
+ label: "Filter projects",
95
+ placeholder: "Filter projects…"
96
+ }) }) : null,
97
+ projects.length === 0 ? /* @__PURE__ */ jsx(EmptyState, {
98
+ title: "No projects yet",
99
+ description: "Create your first project to start visual testing. Projects are free and unlimited.",
100
+ action: newProject
101
+ }) : /* @__PURE__ */ jsxs("div", {
102
+ class: projectGrid,
103
+ "data-filter-scope": true,
104
+ children: [summaries.map((summary) => /* @__PURE__ */ jsx(ProjectCard, {
105
+ summary,
106
+ urls: {
107
+ library: urls.library(summary.project.slug),
108
+ settings: urls.settings(summary.project.slug),
109
+ storybook: urls.short(summary.project.slug),
110
+ review: summary.latest ? urls.buildDiff(summary.project.slug, summary.latest.id) : urls.library(summary.project.slug)
111
+ }
112
+ }, summary.project.id)), /* @__PURE__ */ jsx("div", {
113
+ "data-filter-empty": true,
114
+ hidden: true,
115
+ children: /* @__PURE__ */ jsx(Meta, {
116
+ center: true,
117
+ children: "No projects match your filter."
118
+ })
119
+ })]
120
+ }),
121
+ /* @__PURE__ */ jsx(SetupGuide, { open: projects.length === 0 })
122
+ ]
123
+ })]
124
+ });
125
+ }
126
+ //#endregion
127
+ export { renderProjectsPage };
128
+
129
+ //# sourceMappingURL=projects.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"projects.mjs","names":[],"sources":["../../src/pages/projects.tsx"],"sourcesContent":["import { BuildModel, ProjectModel, SnapshotModel } from \"@storyshelf/core/models\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport { createUrlBuilder } from \"@storyshelf/core/urls\";\nimport { getStore } from \"../store.ts\";\nimport {\n Button,\n Card,\n CodeBlock,\n EmptyState,\n FilterInput,\n HStack,\n Meta,\n PageHeader,\n VStack,\n} from \"../ui/components.tsx\";\nimport { css } from \"../ui/css.ts\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\nimport { ProjectCard, type ProjectSummary } from \"./projects-card.tsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\nconst projectGrid = css`\n /* project-grid */\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(min(100%, 320px), 1fr));\n gap: var(--space-4);\n`;\n\nconst setupSummary = css`\n /* setup-summary */\n cursor: pointer;\n font-weight: 650;\n font-size: var(--text-lg);\n letter-spacing: -0.01em;\n`;\n\n/** Latest build, a few previews, and the review backlog for one project. */\nasync function summarize(project: Project): Promise<ProjectSummary> {\n const { db } = getStore();\n const [latest] = await new BuildModel(db).list(project.id);\n if (!latest) {\n return { project, latest: null, previews: [], pending: 0 };\n }\n const snapshots = await new SnapshotModel(db).listByBuild(latest.id);\n const pending = snapshots.filter((snap) => snap.status === \"new\" || snap.status === \"changed\");\n const preferred = pending.length > 0 ? pending : snapshots;\n return { project, latest, previews: preferred.slice(0, 3), pending: pending.length };\n}\n\n/** Copy-ready steps to get a first build uploaded. */\nfunction SetupGuide({ open }: { open: boolean }): ReturnType<typeof Card> {\n return (\n <Card>\n <details open={open}>\n <summary class={setupSummary}>Set up your first upload</summary>\n <VStack>\n <Meta>1. Create a project (writes .storybook/storyshelf.json for later runs).</Meta>\n <CodeBlock code='npx storyshelf create --url http://localhost:3000 --name \"My Storybook\" --token $STORYSHELF_ADMIN_TOKEN' />\n <Meta>\n 2. Generate a token in <strong>Settings → Tokens</strong> and set it in CI.\n </Meta>\n <CodeBlock code=\"export STORYSHELF_TOKEN=<your token>\" />\n <Meta>3. Upload your built Storybook.</Meta>\n <CodeBlock code=\"npx storyshelf upload\" />\n </VStack>\n </details>\n </Card>\n );\n}\n\n/** Projects overview: searchable project cards with review state, plus setup help. */\nexport async function renderProjectsPage(): Promise<RenderedContent> {\n const { db, user, config } = getStore();\n const urls = createUrlBuilder(\"/\", config.publishedBaseDomain);\n const projects = await new ProjectModel(db).list();\n const summaries = await Promise.all(projects.map((project) => summarize(project)));\n const canCreate = !user || user.role === \"admin\" || user.role === \"member\";\n const newProject = canCreate ? (\n <Button variant=\"primary\" icon=\"plus\" href={urls.projectsNew()}>\n New project\n </Button>\n ) : null;\n\n return (\n <DocumentLayout title=\"Projects\" nav={{ active: \"projects\" }}>\n <PageHeader\n title=\"Projects\"\n description=\"Each project is one Storybook. Create a project, then upload builds from CI.\"\n actions={newProject}\n />\n <VStack gap=\"lg\">\n {projects.length > 1 ? (\n <HStack>\n <FilterInput label=\"Filter projects\" placeholder=\"Filter projects…\" />\n </HStack>\n ) : null}\n {projects.length === 0 ? (\n <EmptyState\n title=\"No projects yet\"\n description=\"Create your first project to start visual testing. Projects are free and unlimited.\"\n action={newProject}\n />\n ) : (\n <div class={projectGrid} data-filter-scope>\n {summaries.map((summary) => (\n <ProjectCard\n key={summary.project.id}\n summary={summary}\n urls={{\n library: urls.library(summary.project.slug),\n settings: urls.settings(summary.project.slug),\n storybook: urls.short(summary.project.slug),\n review: summary.latest\n ? urls.buildDiff(summary.project.slug, summary.latest.id)\n : urls.library(summary.project.slug),\n }}\n />\n ))}\n <div data-filter-empty hidden>\n <Meta center>No projects match your filter.</Meta>\n </div>\n </div>\n )}\n <SetupGuide open={projects.length === 0} />\n </VStack>\n </DocumentLayout>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,98 @@
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 { Meta } from "../ui/feedback.mjs";
7
+ import { Card, SectionTitle } from "../ui/layout.mjs";
8
+ import { HStack, VStack } from "../ui/stacks.mjs";
9
+ import { DocumentLayout } from "../ui/document.mjs";
10
+ import { jsx, jsxs } from "hono/jsx/jsx-runtime";
11
+ //#region src/pages/root.tsx
12
+ /** Landing hero card: card surface with centered marketing padding. */
13
+ const heroCard = css`
14
+ /* hero-card */
15
+ background: var(--surface-card);
16
+ border: 1px solid var(--border);
17
+ border-radius: var(--radius);
18
+ box-shadow: var(--shadow);
19
+ padding: 2rem 1.5rem;
20
+ text-align: center;
21
+ `;
22
+ /** Landing hero title. Centering comes from the card. */
23
+ const heroTitle = css`
24
+ /* hero-title */
25
+ margin: 0 0 0.4rem;
26
+ font-size: 1.9rem;
27
+ letter-spacing: -0.02em;
28
+ `;
29
+ /** Landing hero description. Centering comes from the card. */
30
+ const heroDesc = css`
31
+ /* hero-desc */
32
+ color: var(--text-secondary);
33
+ max-width: 60ch;
34
+ margin: 0 auto 1rem;
35
+ `;
36
+ /** Landing page introducing capture, diff, and review. */
37
+ function renderRootPage() {
38
+ return /* @__PURE__ */ jsxs(DocumentLayout, {
39
+ title: "Welcome",
40
+ nav: { active: "projects" },
41
+ children: [/* @__PURE__ */ jsxs("div", {
42
+ class: heroCard,
43
+ children: [
44
+ /* @__PURE__ */ jsx("h1", {
45
+ class: heroTitle,
46
+ children: "Welcome to StoryShelf"
47
+ }),
48
+ /* @__PURE__ */ jsx("p", {
49
+ class: heroDesc,
50
+ children: "Self-hosted visual testing for Storybook. Capture every story, diff against baselines, and review changes before they ship."
51
+ }),
52
+ /* @__PURE__ */ jsxs(VStack, { children: [/* @__PURE__ */ jsxs(HStack, {
53
+ justify: "center",
54
+ children: [/* @__PURE__ */ jsx(Button, {
55
+ variant: "primary",
56
+ href: "/projects",
57
+ children: "View projects"
58
+ }), /* @__PURE__ */ jsx(Button, {
59
+ variant: "secondary",
60
+ href: "/projects/new",
61
+ children: "Create project"
62
+ })]
63
+ }), /* @__PURE__ */ jsxs(Meta, { children: [
64
+ "Tip: use",
65
+ " ",
66
+ /* @__PURE__ */ jsx("code", { children: "npx storyshelf create --url <url> --name <name> --token $STORYSHELF_ADMIN_TOKEN" }),
67
+ " ",
68
+ "to create a project and token for CI, or ",
69
+ /* @__PURE__ */ jsx("code", { children: "npx storyshelf init" }),
70
+ " to write",
71
+ " ",
72
+ /* @__PURE__ */ jsx("code", { children: ".storybook/storyshelf.json" }),
73
+ "."
74
+ ] })] })
75
+ ]
76
+ }), /* @__PURE__ */ jsxs("div", {
77
+ class: "grid grid--3 mt-1",
78
+ children: [
79
+ /* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsx(SectionTitle, {
80
+ level: 3,
81
+ children: "Capture"
82
+ }), /* @__PURE__ */ jsx(Meta, { children: "Upload your Storybook build. Server renders stories with Playwright — deterministic, no repo cloning." })] }),
83
+ /* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsx(SectionTitle, {
84
+ level: 3,
85
+ children: "Diff"
86
+ }), /* @__PURE__ */ jsx(Meta, { children: "Pixel-perfect diff with pixelmatch. Configurable thresholds, overlay images stored on disk." })] }),
87
+ /* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsx(SectionTitle, {
88
+ level: 3,
89
+ children: "Review"
90
+ }), /* @__PURE__ */ jsx(Meta, { children: "Per-branch baselines with fallback to default. Approve changes per-story or bulk." })] })
91
+ ]
92
+ })]
93
+ });
94
+ }
95
+ //#endregion
96
+ export { renderRootPage };
97
+
98
+ //# sourceMappingURL=root.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"root.mjs","names":[],"sources":["../../src/pages/root.tsx"],"sourcesContent":["import { Button, Card, HStack, Meta, SectionTitle, VStack } from \"../ui/components.tsx\";\nimport { css } from \"../ui/css.ts\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\n\n/** Landing hero card: card surface with centered marketing padding. */\nconst heroCard = css`\n /* hero-card */\n background: var(--surface-card);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n box-shadow: var(--shadow);\n padding: 2rem 1.5rem;\n text-align: center;\n`;\n\n/** Landing hero title. Centering comes from the card. */\nconst heroTitle = css`\n /* hero-title */\n margin: 0 0 0.4rem;\n font-size: 1.9rem;\n letter-spacing: -0.02em;\n`;\n\n/** Landing hero description. Centering comes from the card. */\nconst heroDesc = css`\n /* hero-desc */\n color: var(--text-secondary);\n max-width: 60ch;\n margin: 0 auto 1rem;\n`;\n\n/** Landing page introducing capture, diff, and review. */\nexport function renderRootPage(): RenderedContent {\n return (\n <DocumentLayout title=\"Welcome\" nav={{ active: \"projects\" }}>\n <div class={heroCard}>\n <h1 class={heroTitle}>Welcome to StoryShelf</h1>\n <p class={heroDesc}>\n Self-hosted visual testing for Storybook. Capture every story, diff against baselines, and\n review changes before they ship.\n </p>\n <VStack>\n <HStack justify=\"center\">\n <Button variant=\"primary\" href=\"/projects\">\n View projects\n </Button>\n <Button variant=\"secondary\" href=\"/projects/new\">\n Create project\n </Button>\n </HStack>\n <Meta>\n Tip: use{\" \"}\n <code>\n npx storyshelf create --url &lt;url&gt; --name &lt;name&gt; --token\n $STORYSHELF_ADMIN_TOKEN\n </code>{\" \"}\n to create a project and token for CI, or <code>npx storyshelf init</code> to write{\" \"}\n <code>.storybook/storyshelf.json</code>.\n </Meta>\n </VStack>\n </div>\n\n <div class=\"grid grid--3 mt-1\">\n <Card>\n <SectionTitle level={3}>Capture</SectionTitle>\n <Meta>\n Upload your Storybook build. Server renders stories with Playwright — deterministic, no\n repo cloning.\n </Meta>\n </Card>\n <Card>\n <SectionTitle level={3}>Diff</SectionTitle>\n <Meta>\n Pixel-perfect diff with pixelmatch. Configurable thresholds, overlay images stored on\n disk.\n </Meta>\n </Card>\n <Card>\n <SectionTitle level={3}>Review</SectionTitle>\n <Meta>\n Per-branch baselines with fallback to default. Approve changes per-story or bulk.\n </Meta>\n </Card>\n </div>\n </DocumentLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;AAKA,MAAM,WAAW,GAAG;;;;;;;;;;AAWpB,MAAM,YAAY,GAAG;;;;;;;AAQrB,MAAM,WAAW,GAAG;;;;;;;AAQpB,SAAgB,iBAAkC;CAChD,OACE,qBAAC,gBAAD;EAAgB,OAAM;EAAU,KAAK,EAAE,QAAQ,WAAW;EAA1D,UAAA,CACE,qBAAC,OAAD;GAAK,OAAO;GAAZ,UAAA;IACE,oBAAC,MAAD;KAAI,OAAO;KAAW,UAAA;IAAyB,CAAA;IAC/C,oBAAC,KAAD;KAAG,OAAO;KAAU,UAAA;IAGjB,CAAA;IACH,qBAAC,QAAD,EAAA,UAAA,CACE,qBAAC,QAAD;KAAQ,SAAQ;KAAhB,UAAA,CACE,oBAAC,QAAD;MAAQ,SAAQ;MAAU,MAAK;MAAY,UAAA;KAEnC,CAAA,GACR,oBAAC,QAAD;MAAQ,SAAQ;MAAY,MAAK;MAAgB,UAAA;KAEzC,CAAA,CACF;IACR,CAAA,GAAA,qBAAC,MAAD,EAAA,UAAA;KAAM;KACK;KACT,oBAAC,QAAD,EAAA,UAAM,kFAGA,CAAA;KAAE;KAAI;KAC6B,oBAAC,QAAD,EAAA,UAAM,sBAAyB,CAAA;KAAC;KAAU;KACnF,oBAAC,QAAD,EAAA,UAAM,6BAAgC,CAAA;KAAC;IACnC,EAAA,CAAA,CACA,EAAA,CAAA;GACL;EAEL,CAAA,GAAA,qBAAC,OAAD;GAAK,OAAM;GAAX,UAAA;IACE,qBAAC,MAAD,EAAA,UAAA,CACE,oBAAC,cAAD;KAAc,OAAO;KAAG,UAAA;IAAqB,CAAA,GAC7C,oBAAC,MAAD,EAAA,UAAM,wGAGA,CAAA,CACF,EAAA,CAAA;IACN,qBAAC,MAAD,EAAA,UAAA,CACE,oBAAC,cAAD;KAAc,OAAO;KAAG,UAAA;IAAkB,CAAA,GAC1C,oBAAC,MAAD,EAAA,UAAM,8FAGA,CAAA,CACF,EAAA,CAAA;IACN,qBAAC,MAAD,EAAA,UAAA,CACE,oBAAC,cAAD;KAAc,OAAO;KAAG,UAAA;IAAoB,CAAA,GAC5C,oBAAC,MAAD,EAAA,UAAM,oFAEA,CAAA,CACF,EAAA,CAAA;GACH;EACS,CAAA,CAAA;;AAEpB"}
@@ -0,0 +1,138 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { Button } from "../ui/buttons.mjs";
5
+ import { Alert, Meta } from "../ui/feedback.mjs";
6
+ import { Field, FormActions, SelectField } from "../ui/forms.mjs";
7
+ import { Card, SectionTitle } from "../ui/layout.mjs";
8
+ import { csrfField } from "../ui/csrf-field.mjs";
9
+ import { jsx, jsxs } from "hono/jsx/jsx-runtime";
10
+ //#region src/pages/settings-general.tsx
11
+ /** General settings tab: project metadata, diff thresholds, and danger zone. */
12
+ function renderSettingsGeneral(project, formState, isAdmin) {
13
+ const errors = formState?.errors ?? {};
14
+ const browser = project.browser ?? "chromium";
15
+ return /* @__PURE__ */ jsxs("div", {
16
+ class: "grid max-w-form",
17
+ children: [
18
+ formState?.globalError ? /* @__PURE__ */ jsx(Alert, {
19
+ tone: "danger",
20
+ children: formState.globalError
21
+ }) : null,
22
+ /* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsx(SectionTitle, { children: "General" }), /* @__PURE__ */ jsxs("form", {
23
+ method: "post",
24
+ action: `/projects/${project.slug}/settings`,
25
+ "hx-post": `/projects/${project.slug}/settings`,
26
+ "hx-target": "body",
27
+ "hx-swap": "outerHTML",
28
+ children: [
29
+ csrfField(),
30
+ /* @__PURE__ */ jsx(Field, {
31
+ label: "Name",
32
+ name: "name",
33
+ value: project.name,
34
+ error: errors["name"]
35
+ }),
36
+ /* @__PURE__ */ jsx(Field, {
37
+ label: "Git repository",
38
+ name: "gitRepository",
39
+ value: project.gitRepository ?? "",
40
+ placeholder: "owner/repo"
41
+ }),
42
+ /* @__PURE__ */ jsx(Field, {
43
+ label: "Default branch",
44
+ name: "gitDefaultBranch",
45
+ value: project.gitDefaultBranch
46
+ }),
47
+ /* @__PURE__ */ jsxs("div", {
48
+ class: "grid grid--2",
49
+ children: [/* @__PURE__ */ jsx(Field, {
50
+ label: "Pixel threshold",
51
+ name: "pixelThreshold",
52
+ type: "number",
53
+ step: "0.01",
54
+ min: "0",
55
+ max: "1",
56
+ value: String(project.pixelThreshold),
57
+ hint: "Per-pixel color distance 0–1"
58
+ }), /* @__PURE__ */ jsx(Field, {
59
+ label: "Max diff ratio",
60
+ name: "maxDiffRatio",
61
+ type: "number",
62
+ step: "0.001",
63
+ min: "0",
64
+ max: "1",
65
+ value: String(project.maxDiffRatio),
66
+ hint: "Allowed diff ratio 0–1"
67
+ })]
68
+ }),
69
+ /* @__PURE__ */ jsx(SelectField, {
70
+ label: "Capture browser",
71
+ name: "browser",
72
+ value: browser,
73
+ disabled: !isAdmin,
74
+ options: [
75
+ {
76
+ value: "chromium",
77
+ label: "Chromium (default)"
78
+ },
79
+ {
80
+ value: "firefox",
81
+ label: "Firefox"
82
+ },
83
+ {
84
+ value: "webkit",
85
+ label: "WebKit (Safari engine)"
86
+ },
87
+ {
88
+ value: "chrome",
89
+ label: "Chrome (Chromium engine)"
90
+ }
91
+ ],
92
+ hint: "Rendering engine for captures. Baselines track the browser, so switching starts a fresh baseline. Firefox and WebKit need Playwright system dependencies installed on the server."
93
+ }),
94
+ /* @__PURE__ */ jsx(Field, {
95
+ label: "Public branch regex",
96
+ name: "publicBranchRegex",
97
+ value: project.publicBranchRegex ?? "",
98
+ placeholder: "^main$",
99
+ hint: "Branches matching this regex are publicly viewable."
100
+ }),
101
+ isAdmin ? /* @__PURE__ */ jsx(FormActions, { children: /* @__PURE__ */ jsx(Button, {
102
+ variant: "primary",
103
+ type: "submit",
104
+ icon: "check",
105
+ children: "Save changes"
106
+ }) }) : /* @__PURE__ */ jsx(Meta, { children: "You need admin access to edit settings." })
107
+ ]
108
+ })] }),
109
+ isAdmin ? /* @__PURE__ */ jsxs(Card, {
110
+ tone: "danger",
111
+ children: [
112
+ /* @__PURE__ */ jsx(SectionTitle, {
113
+ level: 3,
114
+ tone: "danger",
115
+ children: "Danger zone"
116
+ }),
117
+ /* @__PURE__ */ jsx(Meta, { children: "Deleting a project removes all builds, snapshots, baselines and tokens. This cannot be undone." }),
118
+ /* @__PURE__ */ jsx("form", {
119
+ method: "post",
120
+ action: `/projects/${project.slug}/delete`,
121
+ "hx-post": `/projects/${project.slug}/delete`,
122
+ "hx-target": "body",
123
+ "hx-confirm": "Delete this project? This cannot be undone.",
124
+ children: /* @__PURE__ */ jsx(Button, {
125
+ variant: "danger",
126
+ type: "submit",
127
+ children: "Delete project"
128
+ })
129
+ })
130
+ ]
131
+ }) : null
132
+ ]
133
+ });
134
+ }
135
+ //#endregion
136
+ export { renderSettingsGeneral };
137
+
138
+ //# sourceMappingURL=settings-general.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings-general.mjs","names":[],"sources":["../../src/pages/settings-general.tsx"],"sourcesContent":["import type { Project } from \"@storyshelf/core/schema\";\nimport {\n Alert,\n Button,\n Card,\n Field,\n FormActions,\n Meta,\n SectionTitle,\n SelectField,\n} from \"../ui/components.tsx\";\nimport { csrfField } from \"../ui/csrf-field.tsx\";\n\n/** Form state for the settings tabs (field errors and global error). */\nexport interface SettingsFormState {\n errors?: Record<string, string>;\n globalError?: string;\n}\n\n/** General settings tab: project metadata, diff thresholds, and danger zone. */\nexport function renderSettingsGeneral(\n project: Project,\n formState: SettingsFormState | undefined,\n isAdmin: boolean,\n): unknown {\n const errors = formState?.errors ?? {};\n const browser = project.browser ?? \"chromium\";\n return (\n <div class=\"grid max-w-form\">\n {formState?.globalError ? <Alert tone=\"danger\">{formState.globalError}</Alert> : null}\n <Card>\n <SectionTitle>General</SectionTitle>\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings`}\n hx-post={`/projects/${project.slug}/settings`}\n hx-target=\"body\"\n hx-swap=\"outerHTML\"\n >\n {csrfField()}\n <Field label=\"Name\" name=\"name\" value={project.name} error={errors[\"name\"]} />\n <Field\n label=\"Git repository\"\n name=\"gitRepository\"\n value={project.gitRepository ?? \"\"}\n placeholder=\"owner/repo\"\n />\n <Field label=\"Default branch\" name=\"gitDefaultBranch\" value={project.gitDefaultBranch} />\n <div class=\"grid grid--2\">\n <Field\n label=\"Pixel threshold\"\n name=\"pixelThreshold\"\n type=\"number\"\n step=\"0.01\"\n min=\"0\"\n max=\"1\"\n value={String(project.pixelThreshold)}\n hint=\"Per-pixel color distance 0–1\"\n />\n <Field\n label=\"Max diff ratio\"\n name=\"maxDiffRatio\"\n type=\"number\"\n step=\"0.001\"\n min=\"0\"\n max=\"1\"\n value={String(project.maxDiffRatio)}\n hint=\"Allowed diff ratio 0–1\"\n />\n </div>\n <SelectField\n label=\"Capture browser\"\n name=\"browser\"\n value={browser}\n disabled={!isAdmin}\n options={[\n { value: \"chromium\", label: \"Chromium (default)\" },\n { value: \"firefox\", label: \"Firefox\" },\n { value: \"webkit\", label: \"WebKit (Safari engine)\" },\n { value: \"chrome\", label: \"Chrome (Chromium engine)\" },\n ]}\n hint=\"Rendering engine for captures. Baselines track the browser, so switching starts a fresh baseline. Firefox and WebKit need Playwright system dependencies installed on the server.\"\n />\n <Field\n label=\"Public branch regex\"\n name=\"publicBranchRegex\"\n value={project.publicBranchRegex ?? \"\"}\n placeholder=\"^main$\"\n hint=\"Branches matching this regex are publicly viewable.\"\n />\n {isAdmin ? (\n <FormActions>\n <Button variant=\"primary\" type=\"submit\" icon=\"check\">\n Save changes\n </Button>\n </FormActions>\n ) : (\n <Meta>You need admin access to edit settings.</Meta>\n )}\n </form>\n </Card>\n\n {isAdmin ? (\n <Card tone=\"danger\">\n <SectionTitle level={3} tone=\"danger\">\n Danger zone\n </SectionTitle>\n <Meta>\n Deleting a project removes all builds, snapshots, baselines and tokens. This cannot be\n undone.\n </Meta>\n <form\n method=\"post\"\n action={`/projects/${project.slug}/delete`}\n hx-post={`/projects/${project.slug}/delete`}\n hx-target=\"body\"\n hx-confirm=\"Delete this project? This cannot be undone.\"\n >\n <Button variant=\"danger\" type=\"submit\">\n Delete project\n </Button>\n </form>\n </Card>\n ) : null}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;AAoBA,SAAgB,sBACd,SACA,WACA,SACS;CACT,MAAM,SAAS,WAAW,UAAU,CAAC;CACrC,MAAM,UAAU,QAAQ,WAAW;CACnC,OACE,qBAAC,OAAD;EAAK,OAAM;EAAX,UAAA;GACG,WAAW,cAAc,oBAAC,OAAD;IAAO,MAAK;IAAU,UAAA,UAAU;GAAmB,CAAA,IAAI;GACjF,qBAAC,MAAD,EAAA,UAAA,CACE,oBAAC,cAAD,EAAA,UAAc,UAAqB,CAAA,GACnC,qBAAC,QAAD;IACE,QAAO;IACP,QAAQ,aAAa,QAAQ,KAAK;IAClC,WAAS,aAAa,QAAQ,KAAK;IACnC,aAAU;IACV,WAAQ;IALV,UAAA;KAOG,UAAU;KACX,oBAAC,OAAD;MAAO,OAAM;MAAO,MAAK;MAAO,OAAO,QAAQ;MAAM,OAAO,OAAO;KAAU,CAAA;KAC7E,oBAAC,OAAD;MACE,OAAM;MACN,MAAK;MACL,OAAO,QAAQ,iBAAiB;MAChC,aAAY;KACb,CAAA;KACD,oBAAC,OAAD;MAAO,OAAM;MAAiB,MAAK;MAAmB,OAAO,QAAQ;KAAmB,CAAA;KACxF,qBAAC,OAAD;MAAK,OAAM;MAAX,UAAA,CACE,oBAAC,OAAD;OACE,OAAM;OACN,MAAK;OACL,MAAK;OACL,MAAK;OACL,KAAI;OACJ,KAAI;OACJ,OAAO,OAAO,QAAQ,cAAc;OACpC,MAAK;MACN,CAAA,GACD,oBAAC,OAAD;OACE,OAAM;OACN,MAAK;OACL,MAAK;OACL,MAAK;OACL,KAAI;OACJ,KAAI;OACJ,OAAO,OAAO,QAAQ,YAAY;OAClC,MAAK;MACN,CAAA,CACE;;KACL,oBAAC,aAAD;MACE,OAAM;MACN,MAAK;MACL,OAAO;MACP,UAAU,CAAC;MACX,SAAS;OACP;QAAE,OAAO;QAAY,OAAO;OAAqB;OACjD;QAAE,OAAO;QAAW,OAAO;OAAU;OACrC;QAAE,OAAO;QAAU,OAAO;OAAyB;OACnD;QAAE,OAAO;QAAU,OAAO;OAA2B;MACvD;MACA,MAAK;KACN,CAAA;KACD,oBAAC,OAAD;MACE,OAAM;MACN,MAAK;MACL,OAAO,QAAQ,qBAAqB;MACpC,aAAY;MACZ,MAAK;KACN,CAAA;KACA,UACC,oBAAC,aAAD,EAAA,UACE,oBAAC,QAAD;MAAQ,SAAQ;MAAU,MAAK;MAAS,MAAK;MAAQ,UAAA;KAE7C,CAAA,EACG,CAAA,IAEb,oBAAC,MAAD,EAAA,UAAM,0CAA6C,CAAA;IAEjD;GACF,CAAA,CAAA,EAAA,CAAA;GAEL,UACC,qBAAC,MAAD;IAAM,MAAK;IAAX,UAAA;KACE,oBAAC,cAAD;MAAc,OAAO;MAAG,MAAK;MAAS,UAAA;KAExB,CAAA;KACd,oBAAC,MAAD,EAAA,UAAM,iGAGA,CAAA;KACN,oBAAC,QAAD;MACE,QAAO;MACP,QAAQ,aAAa,QAAQ,KAAK;MAClC,WAAS,aAAa,QAAQ,KAAK;MACnC,aAAU;MACV,cAAW;MAEX,UAAA,oBAAC,QAAD;OAAQ,SAAQ;OAAS,MAAK;OAAS,UAAA;MAE/B,CAAA;KACJ,CAAA;IACF;GACJ,CAAA,IAAA;EACD;;AAET"}
@@ -0,0 +1,107 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { Button } from "../ui/buttons.mjs";
5
+ import { Alert, Badge, Meta } from "../ui/feedback.mjs";
6
+ import { Field, FormActions } from "../ui/forms.mjs";
7
+ import { Card, SectionTitle } from "../ui/layout.mjs";
8
+ import { Table } from "../ui/table.mjs";
9
+ import { csrfField } from "../ui/csrf-field.mjs";
10
+ import { jsx, jsxs } from "hono/jsx/jsx-runtime";
11
+ //#region src/pages/settings-labels.tsx
12
+ /** Labels settings tab: label-type table plus the create-type form. */
13
+ function renderSettingsLabels(project, labelTypes, isAdmin, formState) {
14
+ return /* @__PURE__ */ jsxs("div", {
15
+ class: "grid max-w-form",
16
+ children: [
17
+ formState?.globalError ? /* @__PURE__ */ jsx(Alert, {
18
+ tone: "danger",
19
+ children: formState.globalError
20
+ }) : null,
21
+ /* @__PURE__ */ jsxs(Card, { children: [
22
+ /* @__PURE__ */ jsx(SectionTitle, { children: "Label types" }),
23
+ /* @__PURE__ */ jsx(Meta, { children: "Labels attach typed values to builds (e.g. pr=123, jira=ABC-123). Values link out via the template." }),
24
+ /* @__PURE__ */ jsx(Table, { children: /* @__PURE__ */ jsxs("table", { children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
25
+ /* @__PURE__ */ jsx("th", { children: "Key" }),
26
+ /* @__PURE__ */ jsx("th", { children: "Name" }),
27
+ /* @__PURE__ */ jsx("th", { children: "Template" }),
28
+ /* @__PURE__ */ jsx("th", { children: /* @__PURE__ */ jsx("span", {
29
+ class: "visually-hidden",
30
+ children: "Actions"
31
+ }) })
32
+ ] }) }), /* @__PURE__ */ jsx("tbody", { children: labelTypes.map((labelType) => /* @__PURE__ */ jsxs("tr", { children: [
33
+ /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(Badge, {
34
+ tone: "neutral",
35
+ children: labelType.key
36
+ }) }),
37
+ /* @__PURE__ */ jsx("td", { children: labelType.name }),
38
+ /* @__PURE__ */ jsx("td", {
39
+ class: "truncate max-w-cell",
40
+ children: labelType.linkTemplate ?? "—"
41
+ }),
42
+ /* @__PURE__ */ jsx("td", { children: isAdmin && labelType.key !== "persistent" && labelType.key !== "branch" ? /* @__PURE__ */ jsxs("form", {
43
+ method: "post",
44
+ action: `/projects/${project.slug}/settings/labels/${labelType.key}/delete`,
45
+ "hx-post": `/projects/${project.slug}/settings/labels/${labelType.key}/delete`,
46
+ "hx-target": "body",
47
+ "hx-confirm": "Delete this label type?",
48
+ children: [csrfField(), /* @__PURE__ */ jsx(Button, {
49
+ variant: "ghost",
50
+ type: "submit",
51
+ "aria-label": `Delete ${labelType.key}`,
52
+ children: "Delete"
53
+ })]
54
+ }) : /* @__PURE__ */ jsx(Meta, {
55
+ as: "span",
56
+ children: "built-in"
57
+ }) })
58
+ ] }, labelType.id)) })] }) }),
59
+ labelTypes.length === 0 ? /* @__PURE__ */ jsx(Meta, { children: "No label types configured." }) : null
60
+ ] }),
61
+ isAdmin ? /* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsx(SectionTitle, {
62
+ level: 3,
63
+ children: "Create label type"
64
+ }), /* @__PURE__ */ jsxs("form", {
65
+ method: "post",
66
+ action: `/projects/${project.slug}/settings/labels`,
67
+ "hx-post": `/projects/${project.slug}/settings/labels`,
68
+ "hx-target": "body",
69
+ children: [
70
+ csrfField(),
71
+ /* @__PURE__ */ jsxs("div", {
72
+ class: "grid grid--2",
73
+ children: [/* @__PURE__ */ jsx(Field, {
74
+ label: "Key",
75
+ name: "key",
76
+ required: true,
77
+ placeholder: "jira",
78
+ pattern: "^[a-z0-9_-]+$",
79
+ hint: "Lowercase, no spaces."
80
+ }), /* @__PURE__ */ jsx(Field, {
81
+ label: "Name",
82
+ name: "labelName",
83
+ required: true,
84
+ placeholder: "Jira issue"
85
+ })]
86
+ }),
87
+ /* @__PURE__ */ jsx(Field, {
88
+ label: "Link template",
89
+ name: "linkTemplate",
90
+ placeholder: "https://jira.example.com/browse/{value}",
91
+ hint: "Use {value} placeholder. Optional."
92
+ }),
93
+ /* @__PURE__ */ jsx(FormActions, { children: /* @__PURE__ */ jsx(Button, {
94
+ variant: "primary",
95
+ type: "submit",
96
+ icon: "plus",
97
+ children: "Add label type"
98
+ }) })
99
+ ]
100
+ })] }) : null
101
+ ]
102
+ });
103
+ }
104
+ //#endregion
105
+ export { renderSettingsLabels };
106
+
107
+ //# sourceMappingURL=settings-labels.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings-labels.mjs","names":[],"sources":["../../src/pages/settings-labels.tsx"],"sourcesContent":["import type { LabelType } from \"@storyshelf/core/schema\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport {\n Alert,\n Badge,\n Button,\n Card,\n Field,\n FormActions,\n Meta,\n SectionTitle,\n Table,\n} from \"../ui/components.tsx\";\nimport { csrfField } from \"../ui/csrf-field.tsx\";\n\n/** Labels settings tab: label-type table plus the create-type form. */\nexport function renderSettingsLabels(\n project: Project,\n labelTypes: LabelType[],\n isAdmin: boolean,\n formState?: { globalError?: string },\n): unknown {\n return (\n <div class=\"grid max-w-form\">\n {formState?.globalError ? <Alert tone=\"danger\">{formState.globalError}</Alert> : null}\n <Card>\n <SectionTitle>Label types</SectionTitle>\n <Meta>\n Labels attach typed values to builds (e.g. pr=123, jira=ABC-123). Values link out via the\n template.\n </Meta>\n <Table>\n <table>\n <thead>\n <tr>\n <th>Key</th>\n <th>Name</th>\n <th>Template</th>\n <th>\n <span class=\"visually-hidden\">Actions</span>\n </th>\n </tr>\n </thead>\n <tbody>\n {labelTypes.map((labelType): HtmlEscapedString | Promise<HtmlEscapedString> => (\n <tr key={labelType.id}>\n <td>\n <Badge tone=\"neutral\">{labelType.key}</Badge>\n </td>\n <td>{labelType.name}</td>\n <td class=\"truncate max-w-cell\">{labelType.linkTemplate ?? \"—\"}</td>\n <td>\n {isAdmin && labelType.key !== \"persistent\" && labelType.key !== \"branch\" ? (\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/labels/${labelType.key}/delete`}\n hx-post={`/projects/${project.slug}/settings/labels/${labelType.key}/delete`}\n hx-target=\"body\"\n hx-confirm=\"Delete this label type?\"\n >\n {csrfField()}\n <Button\n variant=\"ghost\"\n type=\"submit\"\n aria-label={`Delete ${labelType.key}`}\n >\n Delete\n </Button>\n </form>\n ) : (\n <Meta as=\"span\">built-in</Meta>\n )}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </Table>\n {labelTypes.length === 0 ? <Meta>No label types configured.</Meta> : null}\n </Card>\n\n {isAdmin ? (\n <Card>\n <SectionTitle level={3}>Create label type</SectionTitle>\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/labels`}\n hx-post={`/projects/${project.slug}/settings/labels`}\n hx-target=\"body\"\n >\n {csrfField()}\n <div class=\"grid grid--2\">\n <Field\n label=\"Key\"\n name=\"key\"\n required\n placeholder=\"jira\"\n pattern=\"^[a-z0-9_-]+$\"\n hint=\"Lowercase, no spaces.\"\n />\n <Field label=\"Name\" name=\"labelName\" required placeholder=\"Jira issue\" />\n </div>\n <Field\n label=\"Link template\"\n name=\"linkTemplate\"\n placeholder=\"https://jira.example.com/browse/{value}\"\n hint=\"Use {value} placeholder. Optional.\"\n />\n <FormActions>\n <Button variant=\"primary\" type=\"submit\" icon=\"plus\">\n Add label type\n </Button>\n </FormActions>\n </form>\n </Card>\n ) : null}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;AAiBA,SAAgB,qBACd,SACA,YACA,SACA,WACS;CACT,OACE,qBAAC,OAAD;EAAK,OAAM;EAAX,UAAA;GACG,WAAW,cAAc,oBAAC,OAAD;IAAO,MAAK;IAAU,UAAA,UAAU;GAAmB,CAAA,IAAI;GACjF,qBAAC,MAAD,EAAA,UAAA;IACE,oBAAC,cAAD,EAAA,UAAc,cAAyB,CAAA;IACvC,oBAAC,MAAD,EAAA,UAAM,sGAGA,CAAA;IACN,oBAAC,OAAD,EAAA,UACE,qBAAC,SAAD,EAAA,UAAA,CACE,oBAAC,SAAD,EAAA,UACE,qBAAC,MAAD,EAAA,UAAA;KACE,oBAAC,MAAD,EAAA,UAAI,MAAO,CAAA;KACX,oBAAC,MAAD,EAAA,UAAI,OAAQ,CAAA;KACZ,oBAAC,MAAD,EAAA,UAAI,WAAY,CAAA;KAChB,oBAAC,MAAD,EAAA,UACE,oBAAC,QAAD;MAAM,OAAM;MAAkB,UAAA;KAAa,CAAA,EACzC,CAAA;IACF,EAAA,CAAA,EACC,CAAA,GACP,oBAAC,SAAD,EAAA,UACG,WAAW,KAAK,cACf,qBAAC,MAAD,EAAA,UAAA;KACE,oBAAC,MAAD,EAAA,UACE,oBAAC,OAAD;MAAO,MAAK;MAAW,UAAA,UAAU;KAAW,CAAA,EAC1C,CAAA;KACJ,oBAAC,MAAD,EAAA,UAAK,UAAU,KAAS,CAAA;KACxB,oBAAC,MAAD;MAAI,OAAM;MAAuB,UAAA,UAAU,gBAAgB;KAAQ,CAAA;KACnE,oBAAC,MAAD,EAAA,UACG,WAAW,UAAU,QAAQ,gBAAgB,UAAU,QAAQ,WAC9D,qBAAC,QAAD;MACE,QAAO;MACP,QAAQ,aAAa,QAAQ,KAAK,mBAAmB,UAAU,IAAI;MACnE,WAAS,aAAa,QAAQ,KAAK,mBAAmB,UAAU,IAAI;MACpE,aAAU;MACV,cAAW;MALb,UAAA,CAOG,UAAU,GACX,oBAAC,QAAD;OACE,SAAQ;OACR,MAAK;OACL,cAAY,UAAU,UAAU;OACjC,UAAA;MAEO,CAAA,CACJ;KAEN,CAAA,IAAA,oBAAC,MAAD;MAAM,IAAG;MAAO,UAAA;KAAc,CAAA,EAE9B,CAAA;IACF,EAAA,GA5BK,UAAU,EA4Bf,CACL,EACI,CAAA,CACF,EAAA,CAAA,EACF,CAAA;IACN,WAAW,WAAW,IAAI,oBAAC,MAAD,EAAA,UAAM,6BAAgC,CAAA,IAAI;GACjE,EAAA,CAAA;GAEL,UACC,qBAAC,MAAD,EAAA,UAAA,CACE,oBAAC,cAAD;IAAc,OAAO;IAAG,UAAA;GAA+B,CAAA,GACvD,qBAAC,QAAD;IACE,QAAO;IACP,QAAQ,aAAa,QAAQ,KAAK;IAClC,WAAS,aAAa,QAAQ,KAAK;IACnC,aAAU;IAJZ,UAAA;KAMG,UAAU;KACX,qBAAC,OAAD;MAAK,OAAM;MAAX,UAAA,CACE,oBAAC,OAAD;OACE,OAAM;OACN,MAAK;OACL,UAAA;OACA,aAAY;OACZ,SAAQ;OACR,MAAK;MACN,CAAA,GACD,oBAAC,OAAD;OAAO,OAAM;OAAO,MAAK;OAAY,UAAA;OAAS,aAAY;MAAc,CAAA,CACrE;;KACL,oBAAC,OAAD;MACE,OAAM;MACN,MAAK;MACL,aAAY;MACZ,MAAK;KACN,CAAA;KACD,oBAAC,aAAD,EAAA,UACE,oBAAC,QAAD;MAAQ,SAAQ;MAAU,MAAK;MAAS,MAAK;MAAO,UAAA;KAE5C,CAAA,EACG,CAAA;IACT;GACF,CAAA,CAAA,EAAA,CAAA,IACJ;EACD;;AAET"}