@storyshelf/core 0.1.2 → 0.3.1

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 (275) hide show
  1. package/README.md +50 -46
  2. package/dist/adapter/auth.d.mts +4 -4
  3. package/dist/adapter/auth.mjs.map +1 -1
  4. package/dist/{adapters → adapter}/capture-queue.d.mts +6 -4
  5. package/dist/adapter/capture-queue.mjs +4 -0
  6. package/dist/adapter/capture-runner.d.mts +7 -7
  7. package/dist/adapter/database.d.mts +20 -9
  8. package/dist/adapter/database.mjs +60 -1
  9. package/dist/adapter/database.mjs.map +1 -0
  10. package/dist/adapter/git-host/comments.d.mts +41 -0
  11. package/dist/adapter/git-host/comments.mjs +54 -0
  12. package/dist/adapter/git-host/comments.mjs.map +1 -0
  13. package/dist/adapter/git-host/helpers.d.mts +1 -0
  14. package/dist/adapter/git-host/helpers.mjs +1 -0
  15. package/dist/adapter/git-host/helpers.mjs.map +1 -1
  16. package/dist/adapter/git-host.d.mts +3 -7
  17. package/dist/adapter/init.d.mts +58 -0
  18. package/dist/adapter/init.mjs +93 -0
  19. package/dist/adapter/init.mjs.map +1 -0
  20. package/dist/adapter/metadata.d.mts +83 -0
  21. package/dist/adapter/metadata.mjs +4 -0
  22. package/dist/adapter/storage.d.mts +15 -4
  23. package/dist/adapters/webhook-events.mjs +20 -4
  24. package/dist/adapters/webhook-events.mjs.map +1 -1
  25. package/dist/capture/adapter.d.mts +7 -1
  26. package/dist/capture/adapter.mjs +3 -0
  27. package/dist/capture/adapter.mjs.map +1 -1
  28. package/dist/capture/dispatch.mjs +150 -0
  29. package/dist/capture/dispatch.mjs.map +1 -0
  30. package/dist/capture/orchestrator.d.mts +9 -0
  31. package/dist/capture/orchestrator.mjs +11 -44
  32. package/dist/capture/orchestrator.mjs.map +1 -1
  33. package/dist/capture/pipeline.d.mts +4 -1
  34. package/dist/capture/pipeline.mjs +6 -6
  35. package/dist/capture/pipeline.mjs.map +1 -1
  36. package/dist/capture/queue.d.mts +4 -1
  37. package/dist/capture/queue.mjs +3 -1
  38. package/dist/capture/queue.mjs.map +1 -1
  39. package/dist/capture/skip-checks.mjs +76 -0
  40. package/dist/capture/skip-checks.mjs.map +1 -0
  41. package/dist/capture/statics.mjs +124 -0
  42. package/dist/capture/statics.mjs.map +1 -0
  43. package/dist/capture/status-fanout.mjs +1 -0
  44. package/dist/capture/status-fanout.mjs.map +1 -1
  45. package/dist/capture/storybook.d.mts +1 -0
  46. package/dist/capture/storybook.mjs +1 -0
  47. package/dist/capture/storybook.mjs.map +1 -1
  48. package/dist/capture.d.mts +8 -0
  49. package/dist/capture.mjs +9 -0
  50. package/dist/config.d.mts +37 -22
  51. package/dist/config.mjs +28 -2
  52. package/dist/config.mjs.map +1 -1
  53. package/dist/ddl.d.mts +2 -1
  54. package/dist/ddl.mjs +2 -1
  55. package/dist/ddl.mjs.map +1 -1
  56. package/dist/diff/engine.d.mts +8 -0
  57. package/dist/diff/engine.mjs +8 -0
  58. package/dist/diff/engine.mjs.map +1 -1
  59. package/dist/diff/options.d.mts +3 -1
  60. package/dist/diff.d.mts +5 -0
  61. package/dist/diff.mjs +6 -0
  62. package/dist/index.d.mts +34 -24
  63. package/dist/index.mjs +211 -252
  64. package/dist/index.mjs.map +1 -1
  65. package/dist/logger.d.mts +3 -3
  66. package/dist/logger.mjs.map +1 -1
  67. package/dist/middleware/auth-gate.mjs +36 -0
  68. package/dist/middleware/auth-gate.mjs.map +1 -0
  69. package/dist/middleware/csrf.mjs +1 -0
  70. package/dist/middleware/csrf.mjs.map +1 -1
  71. package/dist/middleware/rate-limit.mjs +1 -0
  72. package/dist/middleware/rate-limit.mjs.map +1 -1
  73. package/dist/middleware/request-log.mjs +28 -0
  74. package/dist/middleware/request-log.mjs.map +1 -0
  75. package/dist/middleware/store-scope.mjs +29 -0
  76. package/dist/middleware/store-scope.mjs.map +1 -0
  77. package/dist/models/baseline.mjs +9 -5
  78. package/dist/models/baseline.mjs.map +1 -1
  79. package/dist/models/build.mjs +5 -1
  80. package/dist/models/build.mjs.map +1 -1
  81. package/dist/models/comment.mjs +4 -1
  82. package/dist/models/comment.mjs.map +1 -1
  83. package/dist/models/label.mjs +3 -1
  84. package/dist/models/label.mjs.map +1 -1
  85. package/dist/models/member.mjs +2 -1
  86. package/dist/models/member.mjs.map +1 -1
  87. package/dist/models/project.mjs +2 -1
  88. package/dist/models/project.mjs.map +1 -1
  89. package/dist/models/snapshot.mjs +2 -1
  90. package/dist/models/snapshot.mjs.map +1 -1
  91. package/dist/models/status-config.mjs +2 -1
  92. package/dist/models/status-config.mjs.map +1 -1
  93. package/dist/models/token.mjs +2 -1
  94. package/dist/models/token.mjs.map +1 -1
  95. package/dist/models/webhook.mjs +12 -3
  96. package/dist/models/webhook.mjs.map +1 -1
  97. package/dist/pages/build-detail.mjs +10 -4
  98. package/dist/pages/build-detail.mjs.map +1 -1
  99. package/dist/pages/build-diff-comments.mjs +113 -0
  100. package/dist/pages/build-diff-comments.mjs.map +1 -0
  101. package/dist/pages/build-diff-header.mjs +163 -0
  102. package/dist/pages/build-diff-header.mjs.map +1 -0
  103. package/dist/pages/build-diff-nav.mjs +66 -0
  104. package/dist/pages/build-diff-nav.mjs.map +1 -0
  105. package/dist/pages/build-diff-viewer.mjs +197 -0
  106. package/dist/pages/build-diff-viewer.mjs.map +1 -0
  107. package/dist/pages/build-diff.mjs +66 -413
  108. package/dist/pages/build-diff.mjs.map +1 -1
  109. package/dist/pages/compute-jobs.mjs +3 -1
  110. package/dist/pages/compute-jobs.mjs.map +1 -1
  111. package/dist/pages/label-detail.mjs +4 -3
  112. package/dist/pages/label-detail.mjs.map +1 -1
  113. package/dist/pages/login.mjs +1 -0
  114. package/dist/pages/login.mjs.map +1 -1
  115. package/dist/pages/project-builds.mjs +6 -3
  116. package/dist/pages/project-builds.mjs.map +1 -1
  117. package/dist/pages/project-create.mjs +1 -0
  118. package/dist/pages/project-create.mjs.map +1 -1
  119. package/dist/pages/project-settings.mjs +1 -0
  120. package/dist/pages/project-settings.mjs.map +1 -1
  121. package/dist/pages/projects.mjs +12 -6
  122. package/dist/pages/projects.mjs.map +1 -1
  123. package/dist/pages/root.mjs +7 -3
  124. package/dist/pages/root.mjs.map +1 -1
  125. package/dist/pages/settings-general.mjs +1 -0
  126. package/dist/pages/settings-general.mjs.map +1 -1
  127. package/dist/pages/settings-labels.mjs +2 -1
  128. package/dist/pages/settings-labels.mjs.map +1 -1
  129. package/dist/pages/settings-members.mjs +2 -1
  130. package/dist/pages/settings-members.mjs.map +1 -1
  131. package/dist/pages/settings-status.mjs +3 -1
  132. package/dist/pages/settings-status.mjs.map +1 -1
  133. package/dist/pages/settings-tests.mjs +4 -3
  134. package/dist/pages/settings-tests.mjs.map +1 -1
  135. package/dist/pages/settings-tokens.mjs +1 -0
  136. package/dist/pages/settings-tokens.mjs.map +1 -1
  137. package/dist/pages/settings-webhooks.mjs +1 -0
  138. package/dist/pages/settings-webhooks.mjs.map +1 -1
  139. package/dist/pages/storybook.mjs +38 -1
  140. package/dist/pages/storybook.mjs.map +1 -1
  141. package/dist/{utils/paths.d.mts → paths.d.mts} +5 -0
  142. package/dist/{utils/paths.mjs → paths.mjs} +5 -0
  143. package/dist/paths.mjs.map +1 -0
  144. package/dist/retention/purge.mjs +2 -1
  145. package/dist/retention/purge.mjs.map +1 -1
  146. package/dist/routers/admin.mjs +2 -1
  147. package/dist/routers/admin.mjs.map +1 -1
  148. package/dist/routers/assets.mjs +1 -0
  149. package/dist/routers/assets.mjs.map +1 -1
  150. package/dist/routers/auth.mjs +1 -0
  151. package/dist/routers/auth.mjs.map +1 -1
  152. package/dist/routers/builds.handlers.mjs +114 -19
  153. package/dist/routers/builds.handlers.mjs.map +1 -1
  154. package/dist/routers/builds.mjs +55 -34
  155. package/dist/routers/builds.mjs.map +1 -1
  156. package/dist/routers/comments.mjs +1 -0
  157. package/dist/routers/comments.mjs.map +1 -1
  158. package/dist/routers/health.mjs +137 -0
  159. package/dist/routers/health.mjs.map +1 -0
  160. package/dist/routers/helpers.mjs +8 -2
  161. package/dist/routers/helpers.mjs.map +1 -1
  162. package/dist/routers/htmx.mjs +2 -0
  163. package/dist/routers/htmx.mjs.map +1 -1
  164. package/dist/routers/labels.mjs +2 -1
  165. package/dist/routers/labels.mjs.map +1 -1
  166. package/dist/routers/media.mjs +3 -2
  167. package/dist/routers/media.mjs.map +1 -1
  168. package/dist/routers/members.mjs +1 -0
  169. package/dist/routers/members.mjs.map +1 -1
  170. package/dist/routers/projects.mjs +2 -1
  171. package/dist/routers/projects.mjs.map +1 -1
  172. package/dist/routers/schemas.mjs +25 -0
  173. package/dist/routers/schemas.mjs.map +1 -1
  174. package/dist/routers/settings-general.mjs +95 -0
  175. package/dist/routers/settings-general.mjs.map +1 -0
  176. package/dist/routers/settings-labels.mjs +49 -0
  177. package/dist/routers/settings-labels.mjs.map +1 -0
  178. package/dist/routers/settings-members.mjs +30 -0
  179. package/dist/routers/settings-members.mjs.map +1 -0
  180. package/dist/routers/settings-status.mjs +73 -0
  181. package/dist/routers/settings-status.mjs.map +1 -0
  182. package/dist/routers/settings-tokens.mjs +40 -0
  183. package/dist/routers/settings-tokens.mjs.map +1 -0
  184. package/dist/routers/settings-webhooks.mjs +63 -0
  185. package/dist/routers/settings-webhooks.mjs.map +1 -0
  186. package/dist/routers/settings.handlers.mjs +93 -0
  187. package/dist/routers/settings.handlers.mjs.map +1 -0
  188. package/dist/routers/settings.mjs +14 -246
  189. package/dist/routers/settings.mjs.map +1 -1
  190. package/dist/routers/snapshots.mjs +2 -1
  191. package/dist/routers/snapshots.mjs.map +1 -1
  192. package/dist/routers/status-configs.mjs +2 -1
  193. package/dist/routers/status-configs.mjs.map +1 -1
  194. package/dist/routers/storybook.mjs +12 -4
  195. package/dist/routers/storybook.mjs.map +1 -1
  196. package/dist/routers/tokens.mjs +2 -1
  197. package/dist/routers/tokens.mjs.map +1 -1
  198. package/dist/routers/ui.mjs +5 -4
  199. package/dist/routers/ui.mjs.map +1 -1
  200. package/dist/routers/webhooks.mjs +7 -6
  201. package/dist/routers/webhooks.mjs.map +1 -1
  202. package/dist/schema/baseline.d.mts +18 -0
  203. package/dist/schema/baseline.mjs +22 -0
  204. package/dist/schema/baseline.mjs.map +1 -0
  205. package/dist/schema/build.d.mts +26 -0
  206. package/dist/schema/build.mjs +29 -0
  207. package/dist/schema/build.mjs.map +1 -0
  208. package/dist/schema/comment.d.mts +19 -0
  209. package/dist/schema/comment.mjs +26 -0
  210. package/dist/schema/comment.mjs.map +1 -0
  211. package/dist/schema/label.d.mts +25 -0
  212. package/dist/schema/label.mjs +30 -0
  213. package/dist/schema/label.mjs.map +1 -0
  214. package/dist/schema/member.d.mts +15 -0
  215. package/dist/schema/member.mjs +19 -0
  216. package/dist/schema/member.mjs.map +1 -0
  217. package/dist/schema/project.d.mts +34 -0
  218. package/dist/schema/project.mjs +25 -0
  219. package/dist/schema/project.mjs.map +1 -0
  220. package/dist/schema/snapshot.d.mts +30 -0
  221. package/dist/schema/snapshot.mjs +34 -0
  222. package/dist/schema/snapshot.mjs.map +1 -0
  223. package/dist/schema/status-config.d.mts +16 -0
  224. package/dist/schema/status-config.mjs +20 -0
  225. package/dist/schema/status-config.mjs.map +1 -0
  226. package/dist/schema/token.d.mts +15 -0
  227. package/dist/schema/token.mjs +19 -0
  228. package/dist/schema/token.mjs.map +1 -0
  229. package/dist/schema/user.d.mts +17 -0
  230. package/dist/schema/user.mjs +19 -0
  231. package/dist/schema/user.mjs.map +1 -0
  232. package/dist/schema/webhook.d.mts +16 -0
  233. package/dist/schema/webhook.mjs +20 -0
  234. package/dist/schema/webhook.mjs.map +1 -0
  235. package/dist/schema.d.mts +52 -4433
  236. package/dist/schema.mjs +37 -139
  237. package/dist/schema.mjs.map +1 -1
  238. package/dist/store.mjs +2 -0
  239. package/dist/store.mjs.map +1 -1
  240. package/dist/types.d.mts +13 -2
  241. package/dist/types.mjs +8 -0
  242. package/dist/types.mjs.map +1 -1
  243. package/dist/ui/document.mjs +1 -0
  244. package/dist/ui/document.mjs.map +1 -1
  245. package/dist/ui/feedback.mjs +24 -0
  246. package/dist/ui/feedback.mjs.map +1 -0
  247. package/dist/ui/{components.mjs → forms.mjs} +6 -16
  248. package/dist/ui/forms.mjs.map +1 -0
  249. package/dist/ui/styles.mjs +1 -0
  250. package/dist/ui/styles.mjs.map +1 -1
  251. package/dist/ui/theme.mjs +2 -0
  252. package/dist/ui/theme.mjs.map +1 -1
  253. package/dist/urls.d.mts +8 -0
  254. package/dist/urls.mjs +7 -0
  255. package/dist/urls.mjs.map +1 -1
  256. package/dist/utils/hash.mjs +3 -0
  257. package/dist/utils/hash.mjs.map +1 -1
  258. package/dist/utils/ulid.mjs +2 -0
  259. package/dist/utils/ulid.mjs.map +1 -1
  260. package/package.json +47 -45
  261. package/dist/adapters/metadata.d.mts +0 -24
  262. package/dist/models/baseline.d.mts +0 -20
  263. package/dist/models/build.d.mts +0 -50
  264. package/dist/models/comment.d.mts +0 -20
  265. package/dist/models/label.d.mts +0 -49
  266. package/dist/models/member.d.mts +0 -27
  267. package/dist/models/project.d.mts +0 -42
  268. package/dist/models/snapshot.d.mts +0 -46
  269. package/dist/models/token.d.mts +0 -33
  270. package/dist/models/webhook.d.mts +0 -37
  271. package/dist/retention/purge.d.mts +0 -28
  272. package/dist/ui/components.mjs.map +0 -1
  273. package/dist/ui/document.d.mts +0 -8
  274. package/dist/utils/paths.mjs.map +0 -1
  275. package/dist/utils/ulid.d.mts +0 -8
@@ -3,10 +3,10 @@ import "node:url";
3
3
  import.meta.url;
4
4
  import { BuildModel } from "../models/build.mjs";
5
5
  import { ProjectModel } from "../models/project.mjs";
6
- import { LabelModel } from "../models/label.mjs";
7
6
  import { getStore } from "../store.mjs";
7
+ import { LabelModel } from "../models/label.mjs";
8
8
  import { DocumentLayout } from "../ui/document.mjs";
9
- import { Badge, statusTone } from "../ui/components.mjs";
9
+ import { Badge, statusTone } from "../ui/feedback.mjs";
10
10
  import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
11
11
  //#region src/pages/label-detail.tsx
12
12
  /** Resolve a label type's `link_template` against a build, or return null. */
@@ -69,7 +69,8 @@ async function renderLabelDetailPage(slug, key, value) {
69
69
  tone: "neutral",
70
70
  children: key
71
71
  }),
72
- " Builds bearing this ",
72
+ " Builds bearing this",
73
+ " ",
73
74
  labelType ? labelType.name.toLowerCase() : "label",
74
75
  ", latest first."
75
76
  ]
@@ -1 +1 @@
1
- {"version":3,"file":"label-detail.mjs","names":[],"sources":["../../src/pages/label-detail.tsx"],"sourcesContent":["import type { HtmlEscapedString } from \"hono/utils/html\";\n\nimport { BuildModel } from \"../models/build.ts\";\nimport { LabelModel } from \"../models/label.ts\";\nimport { ProjectModel } from \"../models/project.ts\";\nimport { getStore } from \"../store.ts\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\nimport { Badge, statusTone } from \"../ui/components.tsx\";\n\n/** Resolve a label type's `link_template` against a build, or return null. */\nexport function resolveLabelLink(template: string | null, project: { gitRepository: string | null }, build: { gitBranch: string }, value: string): string | null {\n if (!template) {\n return null;\n }\n const repo = project.gitRepository ?? \"\";\n return template.replaceAll(\"{value}\", value).replaceAll(\"{repo}\", repo).replaceAll(\"{branch}\", build.gitBranch);\n}\n\n/** Label page: every build bearing `key=value`, latest first (ADR 0013). */\nexport async function renderLabelDetailPage(slug: string, key: string, value: string): 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 [builds, labelType] = await Promise.all([\n new BuildModel(getStore().db).list(project.id, { labelKey: key, labelValue: value }),\n new LabelModel(getStore().db).getType(project.id, key),\n ]);\n\n return (\n <DocumentLayout title={`${key}: ${value}`} nav={{ active: \"labels\", projectSlug: project.slug, projectName: project.name }}>\n <div class=\"page-header\">\n <nav class=\"breadcrumbs\" aria-label=\"Breadcrumb\">\n <ol>\n <li>\n <a href=\"/projects\">Projects</a>\n </li>\n <li>\n <a href={`/projects/${project.slug}/builds`}>{project.name}</a>\n </li>\n <li>\n <span aria-current=\"page\">\n {labelType ? labelType.name : key}: {value}\n </span>\n </li>\n </ol>\n </nav>\n <div class=\"page-header__row\">\n <div>\n <h1 class=\"page-header__title\">\n {labelType ? labelType.name : key}: {value}\n </h1>\n <p class=\"page-header__desc\">\n <Badge tone=\"neutral\">{key}</Badge> Builds bearing this {labelType ? labelType.name.toLowerCase() : \"label\"}, latest first.\n </p>\n </div>\n <div class=\"page-header__actions\">\n <a class=\"btn btn--ghost\" href={`/projects/${project.slug}/labels`}>\n All labels\n </a>\n </div>\n </div>\n </div>\n\n {builds.length === 0 ? (\n <div class=\"empty\">\n <h2 class=\"empty__title\">No builds</h2>\n <p class=\"empty__desc\">No build currently bears the label {key}: {value}.</p>\n </div>\n ) : (\n <div class=\"table-wrap\">\n <table>\n <thead>\n <tr>\n <th>Build</th>\n <th>Status</th>\n <th>Author</th>\n <th>Created</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n {builds.map(\n (build): HtmlEscapedString | Promise<HtmlEscapedString> => {\n const link = labelType?.linkTemplate ? resolveLabelLink(labelType.linkTemplate, project, build, value) : null;\n return (\n <tr key={build.id}>\n <td>\n <div style=\"font-weight:600;\">{build.gitBranch}</div>\n <div class=\"field__hint\" style=\"font-family: ui-monospace, monospace;\">\n {build.gitSha.slice(0, 7)} {build.message ? `· ${build.message.slice(0, 60)}` : \"\"}\n </div>\n </td>\n <td>\n <Badge tone={statusTone(build.status)}>{build.status}</Badge>\n </td>\n <td>\n <div>{build.authorName ?? \"—\"}</div>\n <div class=\"field__hint\">{build.authorEmail ?? \"\"}</div>\n </td>\n <td class=\"field__hint\">{new Date(build.createdAt).toLocaleString()}</td>\n <td style=\"white-space:nowrap;\">\n <a class=\"btn btn--secondary\" href={`/projects/${project.slug}/builds/${build.id}`}>\n View\n </a>\n {link ? (\n <>\n <span style=\"margin-left:.35rem;\" />\n <a class=\"btn btn--ghost\" href={link} target=\"_blank\" rel=\"noreferrer\">\n Open link\n </a>\n </>\n ) : null}\n </td>\n </tr>\n );\n },\n )}\n </tbody>\n </table>\n </div>\n )}\n </DocumentLayout>\n );\n}\n\n/** Label types overview page (architecture.md `GET /projects/:slug/labels`). */\nexport async function renderLabelsPage(slug: string): 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 labelTypes = await new LabelModel(getStore().db).listTypes(project.id);\n\n return (\n <DocumentLayout title={`Labels · ${project.name}`} nav={{ active: \"labels\", projectSlug: project.slug, projectName: project.name }}>\n <div class=\"page-header\">\n <nav class=\"breadcrumbs\" aria-label=\"Breadcrumb\">\n <ol>\n <li>\n <a href=\"/projects\">Projects</a>\n </li>\n <li>\n <a href={`/projects/${project.slug}/builds`}>{project.name}</a>\n </li>\n <li>\n <span aria-current=\"page\">Labels</span>\n </li>\n </ol>\n </nav>\n <div class=\"page-header__row\">\n <div>\n <h1 class=\"page-header__title\">Labels</h1>\n <p class=\"page-header__desc\">Typed labels attach values to builds and link out to external systems.</p>\n </div>\n </div>\n </div>\n\n {labelTypes.length === 0 ? (\n <div class=\"empty\">\n <h2 class=\"empty__title\">No label types</h2>\n <p class=\"empty__desc\">No label types are configured for this project.</p>\n </div>\n ) : (\n <div class=\"table-wrap\">\n <table>\n <thead>\n <tr>\n <th>Key</th>\n <th>Name</th>\n <th>Template</th>\n </tr>\n </thead>\n <tbody>\n {labelTypes.map(\n (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 style=\"max-width:32ch; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;\">{labelType.linkTemplate ?? \"—\"}</td>\n </tr>\n ),\n )}\n </tbody>\n </table>\n </div>\n )}\n </DocumentLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"label-detail.mjs","names":[],"sources":["../../src/pages/label-detail.tsx"],"sourcesContent":["import type { HtmlEscapedString } from \"hono/utils/html\";\nimport { BuildModel } from \"../models/build.ts\";\nimport { LabelModel } from \"../models/label.ts\";\nimport { ProjectModel } from \"../models/project.ts\";\nimport { getStore } from \"../store.ts\";\nimport { Badge, statusTone } from \"../ui/components.tsx\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\n\n/** Resolve a label type's `link_template` against a build, or return null. */\nexport function resolveLabelLink(\n template: string | null,\n project: { gitRepository: string | null },\n build: { gitBranch: string },\n value: string,\n): string | null {\n if (!template) {\n return null;\n }\n const repo = project.gitRepository ?? \"\";\n return template\n .replaceAll(\"{value}\", value)\n .replaceAll(\"{repo}\", repo)\n .replaceAll(\"{branch}\", build.gitBranch);\n}\n\n/** Label page: every build bearing `key=value`, latest first (ADR 0013). */\nexport async function renderLabelDetailPage(\n slug: string,\n key: string,\n value: string,\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 [builds, labelType] = await Promise.all([\n new BuildModel(getStore().db).list(project.id, { labelKey: key, labelValue: value }),\n new LabelModel(getStore().db).getType(project.id, key),\n ]);\n\n return (\n <DocumentLayout\n title={`${key}: ${value}`}\n nav={{ active: \"labels\", projectSlug: project.slug, projectName: project.name }}\n >\n <div class=\"page-header\">\n <nav class=\"breadcrumbs\" aria-label=\"Breadcrumb\">\n <ol>\n <li>\n <a href=\"/projects\">Projects</a>\n </li>\n <li>\n <a href={`/projects/${project.slug}/builds`}>{project.name}</a>\n </li>\n <li>\n <span aria-current=\"page\">\n {labelType ? labelType.name : key}: {value}\n </span>\n </li>\n </ol>\n </nav>\n <div class=\"page-header__row\">\n <div>\n <h1 class=\"page-header__title\">\n {labelType ? labelType.name : key}: {value}\n </h1>\n <p class=\"page-header__desc\">\n <Badge tone=\"neutral\">{key}</Badge> Builds bearing this{\" \"}\n {labelType ? labelType.name.toLowerCase() : \"label\"}, latest first.\n </p>\n </div>\n <div class=\"page-header__actions\">\n <a class=\"btn btn--ghost\" href={`/projects/${project.slug}/labels`}>\n All labels\n </a>\n </div>\n </div>\n </div>\n\n {builds.length === 0 ? (\n <div class=\"empty\">\n <h2 class=\"empty__title\">No builds</h2>\n <p class=\"empty__desc\">\n No build currently bears the label {key}: {value}.\n </p>\n </div>\n ) : (\n <div class=\"table-wrap\">\n <table>\n <thead>\n <tr>\n <th>Build</th>\n <th>Status</th>\n <th>Author</th>\n <th>Created</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n {builds.map((build): HtmlEscapedString | Promise<HtmlEscapedString> => {\n const link = labelType?.linkTemplate\n ? resolveLabelLink(labelType.linkTemplate, project, build, value)\n : null;\n return (\n <tr key={build.id}>\n <td>\n <div style=\"font-weight:600;\">{build.gitBranch}</div>\n <div class=\"field__hint\" style=\"font-family: ui-monospace, monospace;\">\n {build.gitSha.slice(0, 7)}{\" \"}\n {build.message ? `· ${build.message.slice(0, 60)}` : \"\"}\n </div>\n </td>\n <td>\n <Badge tone={statusTone(build.status)}>{build.status}</Badge>\n </td>\n <td>\n <div>{build.authorName ?? \"—\"}</div>\n <div class=\"field__hint\">{build.authorEmail ?? \"\"}</div>\n </td>\n <td class=\"field__hint\">{new Date(build.createdAt).toLocaleString()}</td>\n <td style=\"white-space:nowrap;\">\n <a\n class=\"btn btn--secondary\"\n href={`/projects/${project.slug}/builds/${build.id}`}\n >\n View\n </a>\n {link ? (\n <>\n <span style=\"margin-left:.35rem;\" />\n <a class=\"btn btn--ghost\" href={link} target=\"_blank\" rel=\"noreferrer\">\n Open link\n </a>\n </>\n ) : null}\n </td>\n </tr>\n );\n })}\n </tbody>\n </table>\n </div>\n )}\n </DocumentLayout>\n );\n}\n\n/** Label types overview page (architecture.md `GET /projects/:slug/labels`). */\nexport async function renderLabelsPage(slug: string): 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 labelTypes = await new LabelModel(getStore().db).listTypes(project.id);\n\n return (\n <DocumentLayout\n title={`Labels · ${project.name}`}\n nav={{ active: \"labels\", projectSlug: project.slug, projectName: project.name }}\n >\n <div class=\"page-header\">\n <nav class=\"breadcrumbs\" aria-label=\"Breadcrumb\">\n <ol>\n <li>\n <a href=\"/projects\">Projects</a>\n </li>\n <li>\n <a href={`/projects/${project.slug}/builds`}>{project.name}</a>\n </li>\n <li>\n <span aria-current=\"page\">Labels</span>\n </li>\n </ol>\n </nav>\n <div class=\"page-header__row\">\n <div>\n <h1 class=\"page-header__title\">Labels</h1>\n <p class=\"page-header__desc\">\n Typed labels attach values to builds and link out to external systems.\n </p>\n </div>\n </div>\n </div>\n\n {labelTypes.length === 0 ? (\n <div class=\"empty\">\n <h2 class=\"empty__title\">No label types</h2>\n <p class=\"empty__desc\">No label types are configured for this project.</p>\n </div>\n ) : (\n <div class=\"table-wrap\">\n <table>\n <thead>\n <tr>\n <th>Key</th>\n <th>Name</th>\n <th>Template</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 style=\"max-width:32ch; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;\">\n {labelType.linkTemplate ?? \"—\"}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n )}\n </DocumentLayout>\n );\n}\n"],"mappings":"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"}
@@ -4,6 +4,7 @@ import.meta.url;
4
4
  import { DocumentLayout } from "../ui/document.mjs";
5
5
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
6
6
  //#region src/pages/login.tsx
7
+ /** Sign-in page with password form and optional SSO button. */
7
8
  function renderLoginPage(state = {}) {
8
9
  return /* @__PURE__ */ jsx(DocumentLayout, {
9
10
  title: "Sign in",
@@ -1 +1 @@
1
- {"version":3,"file":"login.mjs","names":[],"sources":["../../src/pages/login.tsx"],"sourcesContent":["import { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\n\nexport interface LoginPageState {\n ssoUrl?: string;\n error?: string;\n}\n\nexport function renderLoginPage(state: LoginPageState = {}): RenderedContent {\n return (\n <DocumentLayout title=\"Sign in\">\n <div class=\"login\" style=\"max-width: 360px; margin: 3rem auto;\">\n <div class=\"card card--padded\">\n <h1 style=\"margin-top:0;\">Sign in</h1>\n\n {state.error ? (\n <div class=\"alert alert--danger\" role=\"alert\">\n <strong class=\"alert__title\">Could not sign in</strong>\n <div class=\"alert__body\">{state.error}</div>\n </div>\n ) : null}\n\n <form method=\"post\" action=\"/auth/login\" novalidate>\n <div class=\"field\">\n <label class=\"field__label\" for=\"password\">\n Password\n </label>\n <input\n class=\"field__input\"\n id=\"password\"\n name=\"password\"\n type=\"password\"\n required\n autofocus\n autocomplete=\"current-password\"\n />\n </div>\n <button class=\"btn btn--primary\" type=\"submit\" style=\"width:100%; margin-top:1rem;\">\n Sign in\n </button>\n </form>\n\n {state.ssoUrl ? (\n <div style=\"margin-top:1rem; text-align:center;\">\n <span style=\"color: var(--color-text-secondary); font-size:.875rem;\">or</span>\n <a class=\"btn btn--secondary\" href={state.ssoUrl} style=\"display:block; margin-top:.5rem;\">\n Sign in with SSO\n </a>\n </div>\n ) : null}\n </div>\n </div>\n </DocumentLayout>\n );\n}"],"mappings":";;;;;;AAOA,SAAgB,gBAAgB,QAAwB,CAAC,GAAoB;CAC3E,OACE,oBAAC,gBAAD;EAAgB,OAAM;EACpB,UAAA,oBAAC,OAAD;GAAK,OAAM;GAAQ,OAAM;GACvB,UAAA,qBAAC,OAAD;IAAK,OAAM;IAAX,UAAA;KACE,oBAAC,MAAD;MAAI,OAAM;MAAgB,UAAA;KAAW,CAAA;KAEpC,MAAM,QACL,qBAAC,OAAD;MAAK,OAAM;MAAsB,MAAK;MAAtC,UAAA,CACE,oBAAC,UAAD;OAAQ,OAAM;OAAe,UAAA;MAAyB,CAAA,GACtD,oBAAC,OAAD;OAAK,OAAM;OAAe,UAAA,MAAM;MAAW,CAAA,CACxC;KACH,CAAA,IAAA;KAEJ,qBAAC,QAAD;MAAM,QAAO;MAAO,QAAO;MAAc,YAAA;MAAzC,UAAA,CACE,qBAAC,OAAD;OAAK,OAAM;OAAX,UAAA,CACE,oBAAC,SAAD;QAAO,OAAM;QAAe,KAAI;QAAW,UAAA;OAEpC,CAAA,GACP,oBAAC,SAAD;QACE,OAAM;QACN,IAAG;QACH,MAAK;QACL,MAAK;QACL,UAAA;QACA,WAAA;QACA,cAAa;OACd,CAAA,CACE;MACL,CAAA,GAAA,oBAAC,UAAD;OAAQ,OAAM;OAAmB,MAAK;OAAS,OAAM;OAA+B,UAAA;MAE5E,CAAA,CACJ;;KAEL,MAAM,SACL,qBAAC,OAAD;MAAK,OAAM;MAAX,UAAA,CACE,oBAAC,QAAD;OAAM,OAAM;OAAyD,UAAA;MAAQ,CAAA,GAC7E,oBAAC,KAAD;OAAG,OAAM;OAAqB,MAAM,MAAM;OAAQ,OAAM;OAAmC,UAAA;MAExF,CAAA,CACA;KACH,CAAA,IAAA;IACD;;EACF,CAAA;CACS,CAAA;AAEpB"}
1
+ {"version":3,"file":"login.mjs","names":[],"sources":["../../src/pages/login.tsx"],"sourcesContent":["import { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\n\n/** Form state for the sign-in page (SSO link and error message). */\nexport interface LoginPageState {\n ssoUrl?: string;\n error?: string;\n}\n\n/** Sign-in page with password form and optional SSO button. */\nexport function renderLoginPage(state: LoginPageState = {}): RenderedContent {\n return (\n <DocumentLayout title=\"Sign in\">\n <div class=\"login\" style=\"max-width: 360px; margin: 3rem auto;\">\n <div class=\"card card--padded\">\n <h1 style=\"margin-top:0;\">Sign in</h1>\n\n {state.error ? (\n <div class=\"alert alert--danger\" role=\"alert\">\n <strong class=\"alert__title\">Could not sign in</strong>\n <div class=\"alert__body\">{state.error}</div>\n </div>\n ) : null}\n\n <form method=\"post\" action=\"/auth/login\" novalidate>\n <div class=\"field\">\n <label class=\"field__label\" for=\"password\">\n Password\n </label>\n <input\n class=\"field__input\"\n id=\"password\"\n name=\"password\"\n type=\"password\"\n required\n autofocus\n autocomplete=\"current-password\"\n />\n </div>\n <button class=\"btn btn--primary\" type=\"submit\" style=\"width:100%; margin-top:1rem;\">\n Sign in\n </button>\n </form>\n\n {state.ssoUrl ? (\n <div style=\"margin-top:1rem; text-align:center;\">\n <span style=\"color: var(--color-text-secondary); font-size:.875rem;\">or</span>\n <a\n class=\"btn btn--secondary\"\n href={state.ssoUrl}\n style=\"display:block; margin-top:.5rem;\"\n >\n Sign in with SSO\n </a>\n </div>\n ) : null}\n </div>\n </div>\n </DocumentLayout>\n );\n}\n"],"mappings":";;;;;;;AASA,SAAgB,gBAAgB,QAAwB,CAAC,GAAoB;CAC3E,OACE,oBAAC,gBAAD;EAAgB,OAAM;EACpB,UAAA,oBAAC,OAAD;GAAK,OAAM;GAAQ,OAAM;GACvB,UAAA,qBAAC,OAAD;IAAK,OAAM;IAAX,UAAA;KACE,oBAAC,MAAD;MAAI,OAAM;MAAgB,UAAA;KAAW,CAAA;KAEpC,MAAM,QACL,qBAAC,OAAD;MAAK,OAAM;MAAsB,MAAK;MAAtC,UAAA,CACE,oBAAC,UAAD;OAAQ,OAAM;OAAe,UAAA;MAAyB,CAAA,GACtD,oBAAC,OAAD;OAAK,OAAM;OAAe,UAAA,MAAM;MAAW,CAAA,CACxC;KACH,CAAA,IAAA;KAEJ,qBAAC,QAAD;MAAM,QAAO;MAAO,QAAO;MAAc,YAAA;MAAzC,UAAA,CACE,qBAAC,OAAD;OAAK,OAAM;OAAX,UAAA,CACE,oBAAC,SAAD;QAAO,OAAM;QAAe,KAAI;QAAW,UAAA;OAEpC,CAAA,GACP,oBAAC,SAAD;QACE,OAAM;QACN,IAAG;QACH,MAAK;QACL,MAAK;QACL,UAAA;QACA,WAAA;QACA,cAAa;OACd,CAAA,CACE;MACL,CAAA,GAAA,oBAAC,UAAD;OAAQ,OAAM;OAAmB,MAAK;OAAS,OAAM;OAA+B,UAAA;MAE5E,CAAA,CACJ;;KAEL,MAAM,SACL,qBAAC,OAAD;MAAK,OAAM;MAAX,UAAA,CACE,oBAAC,QAAD;OAAM,OAAM;OAAyD,UAAA;MAAQ,CAAA,GAC7E,oBAAC,KAAD;OACE,OAAM;OACN,MAAM,MAAM;OACZ,OAAM;OACP,UAAA;MAEE,CAAA,CACA;KACH,CAAA,IAAA;IACD;;EACF,CAAA;CACS,CAAA;AAEpB"}
@@ -5,9 +5,10 @@ import { BuildModel } from "../models/build.mjs";
5
5
  import { ProjectModel } from "../models/project.mjs";
6
6
  import { getStore } from "../store.mjs";
7
7
  import { DocumentLayout } from "../ui/document.mjs";
8
- import { Badge, statusTone } from "../ui/components.mjs";
8
+ import { Badge, statusTone } from "../ui/feedback.mjs";
9
9
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
10
10
  //#region src/pages/project-builds.tsx
11
+ /** Project builds page: filterable build history for one project. */
11
12
  async function renderProjectBuildsPage(slug, query = {}) {
12
13
  const project = (await new ProjectModel(getStore().db).list()).find((item) => item.slug === slug);
13
14
  if (!project) return null;
@@ -46,7 +47,8 @@ async function renderProjectBuildsPage(slug, query = {}) {
46
47
  /* @__PURE__ */ jsx("code", { children: project.slug }),
47
48
  " ",
48
49
  project.gitRepository ? `· ${project.gitRepository}` : "",
49
- " · ",
50
+ " ·",
51
+ " ",
50
52
  /* @__PURE__ */ jsx(Badge, {
51
53
  tone: "neutral",
52
54
  children: project.gitDefaultBranch
@@ -188,7 +190,8 @@ async function renderProjectBuildsPage(slug, query = {}) {
188
190
  build.changedCount,
189
191
  " changed · ",
190
192
  build.approvedCount,
191
- " approved · ",
193
+ " approved ·",
194
+ " ",
192
195
  build.snapshotCount,
193
196
  " total"
194
197
  ]
@@ -1 +1 @@
1
- {"version":3,"file":"project-builds.mjs","names":[],"sources":["../../src/pages/project-builds.tsx"],"sourcesContent":["import type { HtmlEscapedString } from \"hono/utils/html\";\n\nimport { BuildModel } from \"../models/build.ts\";\nimport { ProjectModel } from \"../models/project.ts\";\nimport { getStore } from \"../store.ts\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\nimport { Badge, statusTone } from \"../ui/components.tsx\";\n\nexport async function renderProjectBuildsPage(slug: string, query: { status?: string; branch?: string } = {}): 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 builds = await new BuildModel(getStore().db).list(project.id, {\n status: query.status as never,\n branch: query.branch,\n });\n\n return (\n <DocumentLayout title={project.name} nav={{ active: \"builds\", projectSlug: project.slug, projectName: project.name }}>\n <div class=\"page-header\">\n <nav class=\"breadcrumbs\" aria-label=\"Breadcrumb\">\n <ol>\n <li>\n <a href=\"/projects\">Projects</a>\n </li>\n <li>\n <span aria-current=\"page\">{project.name}</span>\n </li>\n </ol>\n </nav>\n <div class=\"page-header__row\">\n <div>\n <h1 class=\"page-header__title\">{project.name}</h1>\n <p class=\"page-header__desc\">\n <code>{project.slug}</code> {project.gitRepository ? `· ${project.gitRepository}` : \"\"} · <Badge tone=\"neutral\">{project.gitDefaultBranch}</Badge>\n </p>\n </div>\n <div class=\"page-header__actions\">\n <a class=\"btn btn--secondary\" href={`/projects/${project.slug}/settings`}>\n Settings\n </a>\n <a class=\"btn btn--ghost\" href={`/projects/${project.slug}/builds`}>\n Refresh\n </a>\n </div>\n </div>\n </div>\n\n <div class=\"card card--padded\" style=\"margin-bottom:1rem;\">\n <form method=\"get\" action={`/projects/${project.slug}/builds`} style=\"display:flex; gap:.5rem; flex-wrap:wrap; align-items:end;\">\n <div class=\"field\" style=\"margin:0; min-width:160px;\">\n <label class=\"field__label\" for=\"status\">\n Status\n </label>\n <select class=\"field__input\" id=\"status\" name=\"status\">\n <option value=\"\" selected={!query.status}>\n All\n </option>\n <option value=\"pending\" selected={query.status === \"pending\"}>\n pending\n </option>\n <option value=\"reviewing\" selected={query.status === \"reviewing\"}>\n reviewing\n </option>\n <option value=\"approved\" selected={query.status === \"approved\"}>\n approved\n </option>\n <option value=\"rejected\" selected={query.status === \"rejected\"}>\n rejected\n </option>\n <option value=\"failed\" selected={query.status === \"failed\"}>\n failed\n </option>\n </select>\n </div>\n <div class=\"field\" style=\"margin:0; min-width:180px;\">\n <label class=\"field__label\" for=\"branch\">\n Branch\n </label>\n <input class=\"field__input\" id=\"branch\" name=\"branch\" value={query.branch ?? \"\"} placeholder=\"main\" />\n </div>\n <button class=\"btn btn--secondary\" type=\"submit\">\n Filter\n </button>\n {query.status || query.branch ? (\n <a class=\"btn btn--ghost\" href={`/projects/${project.slug}/builds`}>\n Clear\n </a>\n ) : null}\n </form>\n </div>\n\n {builds.length === 0 ? (\n <div class=\"empty\">\n <h2 class=\"empty__title\">No builds</h2>\n <p class=\"empty__desc\">\n {query.status || query.branch ? \"No builds match the current filter.\" : \"Upload your first build with the CLI. Builds appear here once uploaded.\"}\n </p>\n </div>\n ) : (\n <div class=\"table-wrap\">\n <table>\n <thead>\n <tr>\n <th>Branch / SHA</th>\n <th>Status</th>\n <th>Snapshots</th>\n <th>Author</th>\n <th>Created</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n {builds.map(\n (build): HtmlEscapedString | Promise<HtmlEscapedString> => (\n <tr key={build.id}>\n <td>\n <div style=\"font-weight:600;\">{build.gitBranch}</div>\n <div class=\"field__hint\" style=\"font-family: ui-monospace, monospace;\">\n {build.gitSha.slice(0, 7)} {build.message ? `· ${build.message.slice(0, 60)}` : \"\"}\n </div>\n </td>\n <td>\n <Badge tone={statusTone(build.status)}>{build.status}</Badge>\n </td>\n <td>\n <span class=\"field__hint\">\n {build.changedCount} changed · {build.approvedCount} approved · {build.snapshotCount} total\n </span>\n </td>\n <td>\n <div>{build.authorName ?? \"—\"}</div>\n <div class=\"field__hint\">{build.authorEmail ?? \"\"}</div>\n </td>\n <td class=\"field__hint\">{new Date(build.createdAt).toLocaleString()}</td>\n <td style=\"white-space:nowrap;\">\n <a class=\"btn btn--secondary\" href={`/projects/${project.slug}/builds/${build.id}`}>\n View\n </a>\n <span style=\"margin-left:.35rem;\" />\n <a class=\"btn btn--ghost\" href={`/projects/${project.slug}/builds/${build.id}/diff`}>\n Review\n </a>\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n )}\n </DocumentLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"project-builds.mjs","names":[],"sources":["../../src/pages/project-builds.tsx"],"sourcesContent":["import type { HtmlEscapedString } from \"hono/utils/html\";\nimport { BuildModel } from \"../models/build.ts\";\nimport { ProjectModel } from \"../models/project.ts\";\nimport { getStore } from \"../store.ts\";\nimport { Badge, statusTone } from \"../ui/components.tsx\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\n\n/** Project builds page: filterable build history for one project. */\nexport async function renderProjectBuildsPage(\n slug: string,\n query: { status?: string; branch?: string } = {},\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 builds = await new BuildModel(getStore().db).list(project.id, {\n status: query.status as never,\n branch: query.branch,\n });\n\n return (\n <DocumentLayout\n title={project.name}\n nav={{ active: \"builds\", projectSlug: project.slug, projectName: project.name }}\n >\n <div class=\"page-header\">\n <nav class=\"breadcrumbs\" aria-label=\"Breadcrumb\">\n <ol>\n <li>\n <a href=\"/projects\">Projects</a>\n </li>\n <li>\n <span aria-current=\"page\">{project.name}</span>\n </li>\n </ol>\n </nav>\n <div class=\"page-header__row\">\n <div>\n <h1 class=\"page-header__title\">{project.name}</h1>\n <p class=\"page-header__desc\">\n <code>{project.slug}</code>{\" \"}\n {project.gitRepository ? `· ${project.gitRepository}` : \"\"} ·{\" \"}\n <Badge tone=\"neutral\">{project.gitDefaultBranch}</Badge>\n </p>\n </div>\n <div class=\"page-header__actions\">\n <a class=\"btn btn--secondary\" href={`/projects/${project.slug}/settings`}>\n Settings\n </a>\n <a class=\"btn btn--ghost\" href={`/projects/${project.slug}/builds`}>\n Refresh\n </a>\n </div>\n </div>\n </div>\n\n <div class=\"card card--padded\" style=\"margin-bottom:1rem;\">\n <form\n method=\"get\"\n action={`/projects/${project.slug}/builds`}\n style=\"display:flex; gap:.5rem; flex-wrap:wrap; align-items:end;\"\n >\n <div class=\"field\" style=\"margin:0; min-width:160px;\">\n <label class=\"field__label\" for=\"status\">\n Status\n </label>\n <select class=\"field__input\" id=\"status\" name=\"status\">\n <option value=\"\" selected={!query.status}>\n All\n </option>\n <option value=\"pending\" selected={query.status === \"pending\"}>\n pending\n </option>\n <option value=\"reviewing\" selected={query.status === \"reviewing\"}>\n reviewing\n </option>\n <option value=\"approved\" selected={query.status === \"approved\"}>\n approved\n </option>\n <option value=\"rejected\" selected={query.status === \"rejected\"}>\n rejected\n </option>\n <option value=\"failed\" selected={query.status === \"failed\"}>\n failed\n </option>\n </select>\n </div>\n <div class=\"field\" style=\"margin:0; min-width:180px;\">\n <label class=\"field__label\" for=\"branch\">\n Branch\n </label>\n <input\n class=\"field__input\"\n id=\"branch\"\n name=\"branch\"\n value={query.branch ?? \"\"}\n placeholder=\"main\"\n />\n </div>\n <button class=\"btn btn--secondary\" type=\"submit\">\n Filter\n </button>\n {query.status || query.branch ? (\n <a class=\"btn btn--ghost\" href={`/projects/${project.slug}/builds`}>\n Clear\n </a>\n ) : null}\n </form>\n </div>\n\n {builds.length === 0 ? (\n <div class=\"empty\">\n <h2 class=\"empty__title\">No builds</h2>\n <p class=\"empty__desc\">\n {query.status || query.branch\n ? \"No builds match the current filter.\"\n : \"Upload your first build with the CLI. Builds appear here once uploaded.\"}\n </p>\n </div>\n ) : (\n <div class=\"table-wrap\">\n <table>\n <thead>\n <tr>\n <th>Branch / SHA</th>\n <th>Status</th>\n <th>Snapshots</th>\n <th>Author</th>\n <th>Created</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n {builds.map((build): HtmlEscapedString | Promise<HtmlEscapedString> => (\n <tr key={build.id}>\n <td>\n <div style=\"font-weight:600;\">{build.gitBranch}</div>\n <div class=\"field__hint\" style=\"font-family: ui-monospace, monospace;\">\n {build.gitSha.slice(0, 7)}{\" \"}\n {build.message ? `· ${build.message.slice(0, 60)}` : \"\"}\n </div>\n </td>\n <td>\n <Badge tone={statusTone(build.status)}>{build.status}</Badge>\n </td>\n <td>\n <span class=\"field__hint\">\n {build.changedCount} changed · {build.approvedCount} approved ·{\" \"}\n {build.snapshotCount} total\n </span>\n </td>\n <td>\n <div>{build.authorName ?? \"—\"}</div>\n <div class=\"field__hint\">{build.authorEmail ?? \"\"}</div>\n </td>\n <td class=\"field__hint\">{new Date(build.createdAt).toLocaleString()}</td>\n <td style=\"white-space:nowrap;\">\n <a\n class=\"btn btn--secondary\"\n href={`/projects/${project.slug}/builds/${build.id}`}\n >\n View\n </a>\n <span style=\"margin-left:.35rem;\" />\n <a\n class=\"btn btn--ghost\"\n href={`/projects/${project.slug}/builds/${build.id}/diff`}\n >\n Review\n </a>\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n )}\n </DocumentLayout>\n );\n}\n"],"mappings":"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"}
@@ -4,6 +4,7 @@ import.meta.url;
4
4
  import { DocumentLayout } from "../ui/document.mjs";
5
5
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
6
6
  //#region src/pages/project-create.tsx
7
+ /** New-project form page (one project corresponds to one Storybook). */
7
8
  function renderProjectCreatePage(state = {}) {
8
9
  const values = state.values ?? {};
9
10
  const errors = state.errors ?? {};
@@ -1 +1 @@
1
- {"version":3,"file":"project-create.mjs","names":[],"sources":["../../src/pages/project-create.tsx"],"sourcesContent":["import { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\n\nexport interface ProjectCreateFormState {\n values?: { name?: string; gitRepository?: string; gitDefaultBranch?: string };\n errors?: Record<string, string>;\n globalError?: string;\n}\n\nexport function renderProjectCreatePage(state: ProjectCreateFormState = {}): RenderedContent {\n const values = state.values ?? {};\n const errors = state.errors ?? {};\n return (\n <DocumentLayout title=\"New project\" nav={{ active: \"projects\" }}>\n <div class=\"page-header\">\n <nav class=\"breadcrumbs\" aria-label=\"Breadcrumb\">\n <ol>\n <li>\n <a href=\"/projects\">Projects</a>\n </li>\n <li>\n <span aria-current=\"page\">New project</span>\n </li>\n </ol>\n </nav>\n <div class=\"page-header__row\">\n <div>\n <h1 class=\"page-header__title\">Create project</h1>\n <p class=\"page-header__desc\">A project corresponds to one Storybook. You can have multiple projects per repository.</p>\n </div>\n </div>\n </div>\n\n {state.globalError ? (\n <div class=\"alert alert--danger\" role=\"alert\">\n <strong class=\"alert__title\">Could not create project</strong>\n <div class=\"alert__body\">{state.globalError}</div>\n </div>\n ) : null}\n\n <div class=\"card card--padded\" style=\"max-width: 640px;\">\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 <div class=\"field\">\n <label class=\"field__label\" for=\"name\">\n Name <span aria-hidden=\"true\">*</span>\n </label>\n <input\n class={`field__input ${errors[\"name\"] ? \"field__input--error\" : \"\"}`}\n id=\"name\"\n name=\"name\"\n type=\"text\"\n required\n autofocus\n value={values.name ?? \"\"}\n placeholder=\"Design System\"\n aria-invalid={errors[\"name\"] ? \"true\" : undefined}\n aria-describedby={errors[\"name\"] ? \"name-error\" : \"name-hint\"}\n autocomplete=\"off\"\n />\n {errors[\"name\"] ? (\n <p class=\"field__error\" id=\"name-error\" role=\"alert\">\n {errors[\"name\"]}\n </p>\n ) : (\n <p class=\"field__hint\" id=\"name-hint\">\n Human readable name. A URL-friendly slug is generated automatically.\n </p>\n )}\n </div>\n\n <div class=\"field\">\n <label class=\"field__label\" for=\"gitRepository\">\n Git repository\n </label>\n <input\n class={`field__input ${errors[\"gitRepository\"] ? \"field__input--error\" : \"\"}`}\n id=\"gitRepository\"\n name=\"gitRepository\"\n type=\"text\"\n value={values.gitRepository ?? \"\"}\n placeholder=\"acme/design-system\"\n aria-invalid={errors[\"gitRepository\"] ? \"true\" : undefined}\n aria-describedby={errors[\"gitRepository\"] ? \"gitRepository-error\" : \"gitRepository-hint\"}\n />\n {errors[\"gitRepository\"] ? (\n <p class=\"field__error\" id=\"gitRepository-error\" role=\"alert\">\n {errors[\"gitRepository\"]}\n </p>\n ) : (\n <p class=\"field__hint\" id=\"gitRepository-hint\">\n Optional owner/repo for status checks and links.\n </p>\n )}\n </div>\n\n <div class=\"field\">\n <label class=\"field__label\" for=\"gitDefaultBranch\">\n Default branch\n </label>\n <input\n class=\"field__input\"\n id=\"gitDefaultBranch\"\n name=\"gitDefaultBranch\"\n type=\"text\"\n value={values.gitDefaultBranch ?? \"main\"}\n placeholder=\"main\"\n />\n <p class=\"field__hint\">Baselines fall back to this branch when no branch-specific baseline exists.</p>\n </div>\n\n <div style=\"display:flex; gap:.5rem; margin-top:1rem;\">\n <button class=\"btn btn--primary\" type=\"submit\">\n Create project\n </button>\n <a class=\"btn btn--secondary\" href=\"/projects\">\n Cancel\n </a>\n </div>\n </form>\n </div>\n </DocumentLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"project-create.mjs","names":[],"sources":["../../src/pages/project-create.tsx"],"sourcesContent":["import { 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 title=\"New project\" nav={{ active: \"projects\" }}>\n <div class=\"page-header\">\n <nav class=\"breadcrumbs\" aria-label=\"Breadcrumb\">\n <ol>\n <li>\n <a href=\"/projects\">Projects</a>\n </li>\n <li>\n <span aria-current=\"page\">New project</span>\n </li>\n </ol>\n </nav>\n <div class=\"page-header__row\">\n <div>\n <h1 class=\"page-header__title\">Create project</h1>\n <p class=\"page-header__desc\">\n A project corresponds to one Storybook. You can have multiple projects per repository.\n </p>\n </div>\n </div>\n </div>\n\n {state.globalError ? (\n <div class=\"alert alert--danger\" role=\"alert\">\n <strong class=\"alert__title\">Could not create project</strong>\n <div class=\"alert__body\">{state.globalError}</div>\n </div>\n ) : null}\n\n <div class=\"card card--padded\" style=\"max-width: 640px;\">\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 <div class=\"field\">\n <label class=\"field__label\" for=\"name\">\n Name <span aria-hidden=\"true\">*</span>\n </label>\n <input\n class={`field__input ${errors[\"name\"] ? \"field__input--error\" : \"\"}`}\n id=\"name\"\n name=\"name\"\n type=\"text\"\n required\n autofocus\n value={values.name ?? \"\"}\n placeholder=\"Design System\"\n aria-invalid={errors[\"name\"] ? \"true\" : undefined}\n aria-describedby={errors[\"name\"] ? \"name-error\" : \"name-hint\"}\n autocomplete=\"off\"\n />\n {errors[\"name\"] ? (\n <p class=\"field__error\" id=\"name-error\" role=\"alert\">\n {errors[\"name\"]}\n </p>\n ) : (\n <p class=\"field__hint\" id=\"name-hint\">\n Human readable name. A URL-friendly slug is generated automatically.\n </p>\n )}\n </div>\n\n <div class=\"field\">\n <label class=\"field__label\" for=\"gitRepository\">\n Git repository\n </label>\n <input\n class={`field__input ${errors[\"gitRepository\"] ? \"field__input--error\" : \"\"}`}\n id=\"gitRepository\"\n name=\"gitRepository\"\n type=\"text\"\n value={values.gitRepository ?? \"\"}\n placeholder=\"acme/design-system\"\n aria-invalid={errors[\"gitRepository\"] ? \"true\" : undefined}\n aria-describedby={\n errors[\"gitRepository\"] ? \"gitRepository-error\" : \"gitRepository-hint\"\n }\n />\n {errors[\"gitRepository\"] ? (\n <p class=\"field__error\" id=\"gitRepository-error\" role=\"alert\">\n {errors[\"gitRepository\"]}\n </p>\n ) : (\n <p class=\"field__hint\" id=\"gitRepository-hint\">\n Optional owner/repo for status checks and links.\n </p>\n )}\n </div>\n\n <div class=\"field\">\n <label class=\"field__label\" for=\"gitDefaultBranch\">\n Default branch\n </label>\n <input\n class=\"field__input\"\n id=\"gitDefaultBranch\"\n name=\"gitDefaultBranch\"\n type=\"text\"\n value={values.gitDefaultBranch ?? \"main\"}\n placeholder=\"main\"\n />\n <p class=\"field__hint\">\n Baselines fall back to this branch when no branch-specific baseline exists.\n </p>\n </div>\n\n <div style=\"display:flex; gap:.5rem; margin-top:1rem;\">\n <button class=\"btn btn--primary\" type=\"submit\">\n Create project\n </button>\n <a class=\"btn btn--secondary\" href=\"/projects\">\n Cancel\n </a>\n </div>\n </form>\n </div>\n </DocumentLayout>\n );\n}\n"],"mappings":";;;;;;;AAUA,SAAgB,wBAAwB,QAAgC,CAAC,GAAoB;CAC3F,MAAM,SAAS,MAAM,UAAU,CAAC;CAChC,MAAM,SAAS,MAAM,UAAU,CAAC;CAChC,OACE,qBAAC,gBAAD;EAAgB,OAAM;EAAc,KAAK,EAAE,QAAQ,WAAW;EAA9D,UAAA;GACE,qBAAC,OAAD;IAAK,OAAM;IAAX,UAAA,CACE,oBAAC,OAAD;KAAK,OAAM;KAAc,cAAW;KAClC,UAAA,qBAAC,MAAD,EAAA,UAAA,CACE,oBAAC,MAAD,EAAA,UACE,oBAAC,KAAD;MAAG,MAAK;MAAY,UAAA;KAAW,CAAA,EAC7B,CAAA,GACJ,oBAAC,MAAD,EAAA,UACE,oBAAC,QAAD;MAAM,gBAAa;MAAO,UAAA;KAAiB,CAAA,EACzC,CAAA,CACF,EAAA,CAAA;IACD,CAAA,GACL,oBAAC,OAAD;KAAK,OAAM;KACT,UAAA,qBAAC,OAAD,EAAA,UAAA,CACE,oBAAC,MAAD;MAAI,OAAM;MAAqB,UAAA;KAAkB,CAAA,GACjD,oBAAC,KAAD;MAAG,OAAM;MAAoB,UAAA;KAE1B,CAAA,CACA,EAAA,CAAA;IACF,CAAA,CACF;;GAEJ,MAAM,cACL,qBAAC,OAAD;IAAK,OAAM;IAAsB,MAAK;IAAtC,UAAA,CACE,oBAAC,UAAD;KAAQ,OAAM;KAAe,UAAA;IAAgC,CAAA,GAC7D,oBAAC,OAAD;KAAK,OAAM;KAAe,UAAA,MAAM;IAAiB,CAAA,CAC9C;GACH,CAAA,IAAA;GAEJ,oBAAC,OAAD;IAAK,OAAM;IAAoB,OAAM;IACnC,UAAA,qBAAC,QAAD;KACE,QAAO;KACP,QAAO;KACP,WAAQ;KACR,aAAU;KACV,WAAQ;KACR,YAAA;KANF,UAAA;MAQE,qBAAC,OAAD;OAAK,OAAM;OAAX,UAAA;QACE,qBAAC,SAAD;SAAO,OAAM;SAAe,KAAI;SAAhC,UAAA,CAAuC,SAChC,oBAAC,QAAD;UAAM,eAAY;UAAO,UAAA;SAAO,CAAA,CAChC;;QACP,oBAAC,SAAD;SACE,OAAO,gBAAgB,OAAO,UAAU,wBAAwB;SAChE,IAAG;SACH,MAAK;SACL,MAAK;SACL,UAAA;SACA,WAAA;SACA,OAAO,OAAO,QAAQ;SACtB,aAAY;SACZ,gBAAc,OAAO,UAAU,SAAS,KAAA;SACxC,oBAAkB,OAAO,UAAU,eAAe;SAClD,cAAa;QACd,CAAA;QACA,OAAO,UACN,oBAAC,KAAD;SAAG,OAAM;SAAe,IAAG;SAAa,MAAK;SAC1C,UAAA,OAAO;QACP,CAAA,IAEH,oBAAC,KAAD;SAAG,OAAM;SAAc,IAAG;SAAY,UAAA;QAEnC,CAAA;OAEF;;MAEL,qBAAC,OAAD;OAAK,OAAM;OAAX,UAAA;QACE,oBAAC,SAAD;SAAO,OAAM;SAAe,KAAI;SAAgB,UAAA;QAEzC,CAAA;QACP,oBAAC,SAAD;SACE,OAAO,gBAAgB,OAAO,mBAAmB,wBAAwB;SACzE,IAAG;SACH,MAAK;SACL,MAAK;SACL,OAAO,OAAO,iBAAiB;SAC/B,aAAY;SACZ,gBAAc,OAAO,mBAAmB,SAAS,KAAA;SACjD,oBACE,OAAO,mBAAmB,wBAAwB;QAErD,CAAA;QACA,OAAO,mBACN,oBAAC,KAAD;SAAG,OAAM;SAAe,IAAG;SAAsB,MAAK;SACnD,UAAA,OAAO;QACP,CAAA,IAEH,oBAAC,KAAD;SAAG,OAAM;SAAc,IAAG;SAAqB,UAAA;QAE5C,CAAA;OAEF;;MAEL,qBAAC,OAAD;OAAK,OAAM;OAAX,UAAA;QACE,oBAAC,SAAD;SAAO,OAAM;SAAe,KAAI;SAAmB,UAAA;QAE5C,CAAA;QACP,oBAAC,SAAD;SACE,OAAM;SACN,IAAG;SACH,MAAK;SACL,MAAK;SACL,OAAO,OAAO,oBAAoB;SAClC,aAAY;QACb,CAAA;QACD,oBAAC,KAAD;SAAG,OAAM;SAAc,UAAA;QAEpB,CAAA;OACA;;MAEL,qBAAC,OAAD;OAAK,OAAM;OAAX,UAAA,CACE,oBAAC,UAAD;QAAQ,OAAM;QAAmB,MAAK;QAAS,UAAA;OAEvC,CAAA,GACR,oBAAC,KAAD;QAAG,OAAM;QAAqB,MAAK;QAAY,UAAA;OAE5C,CAAA,CACA;;KACD;;GACH,CAAA;EACS;;AAEpB"}
@@ -34,6 +34,7 @@ function renderActiveTab(data, formState) {
34
34
  if (activeTab === "status") return renderSettingsStatus(project, data.statusConfigs, data.gitHosts, data.isAdmin);
35
35
  return null;
36
36
  }
37
+ /** Project settings shell: tab navigation plus the active settings tab. */
37
38
  function renderProjectSettingsPage(data, formState) {
38
39
  const { project, activeTab } = data;
39
40
  return /* @__PURE__ */ jsxs(DocumentLayout, {
@@ -1 +1 @@
1
- {"version":3,"file":"project-settings.mjs","names":[],"sources":["../../src/pages/project-settings.tsx"],"sourcesContent":["import type { GitHostProvider } from \"../adapters/git-host/index.ts\";\nimport type { LabelType, Project, Token } from \"../schema.ts\";\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 { 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\nexport type SettingsTab = \"general\" | \"tests\" | \"labels\" | \"tokens\" | \"webhooks\" | \"members\" | \"status\";\n\nexport interface ProjectSettingsData {\n project: Project;\n activeTab: SettingsTab;\n labelTypes: LabelType[];\n tokens: Omit<Token, \"hash\">[];\n members: SettingsMember[];\n webhooks: SettingsWebhook[];\n statusConfigs: SettingsStatusConfig[];\n gitHosts: GitHostProvider[];\n isAdmin: boolean;\n}\n\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\" ? `/projects/${project.slug}/settings` : `/projects/${project.slug}/settings/${tab}`;\n}\n\nfunction renderTabLink(project: Project, activeTab: SettingsTab, tab: SettingsTab, label: string): unknown {\n const active = activeTab === tab;\n return (\n <a class={`tabs__link ${active ? \"tabs__link--active\" : \"\"}`} href={tabHref(project, tab)} aria-current={active ? \"page\" : undefined}>\n {label}\n </a>\n );\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\") return renderSettingsLabels(project, data.labelTypes, data.isAdmin);\n if (activeTab === \"tokens\") return renderSettingsTokens(project, data.tokens, data.isAdmin);\n if (activeTab === \"webhooks\") return renderSettingsWebhooks(project, data.webhooks, data.isAdmin, formState);\n if (activeTab === \"members\") return renderSettingsMembers(project, data.members, data.isAdmin);\n if (activeTab === \"status\") return renderSettingsStatus(project, data.statusConfigs, data.gitHosts, data.isAdmin);\n return null;\n}\n\nexport function renderProjectSettingsPage(data: ProjectSettingsData, formState?: SettingsFormState): RenderedContent {\n const { project, activeTab } = data;\n return (\n <DocumentLayout title={`${project.name} · Settings`} nav={{ active: \"settings\", projectSlug: project.slug, projectName: project.name }}>\n <div class=\"page-header\">\n <nav class=\"breadcrumbs\" aria-label=\"Breadcrumb\">\n <ol>\n <li>\n <a href=\"/projects\">Projects</a>\n </li>\n <li>\n <a href={`/projects/${project.slug}/builds`}>{project.name}</a>\n </li>\n <li>\n <span aria-current=\"page\">Settings</span>\n </li>\n </ol>\n </nav>\n <div class=\"page-header__row\">\n <div>\n <h1 class=\"page-header__title\">Project settings</h1>\n <p class=\"page-header__desc\">Manage general settings, labels, tokens, webhooks and members for {project.name}.</p>\n </div>\n </div>\n </div>\n\n <nav class=\"tabs\" aria-label=\"Settings sections\">\n {renderTabLink(project, activeTab, \"general\", \"General\")}\n {renderTabLink(project, activeTab, \"tests\", \"Tests\")}\n {renderTabLink(project, activeTab, \"labels\", \"Labels\")}\n {renderTabLink(project, activeTab, \"tokens\", \"Tokens\")}\n {renderTabLink(project, activeTab, \"webhooks\", \"Webhooks\")}\n {renderTabLink(project, activeTab, \"members\", \"Members\")}\n {renderTabLink(project, activeTab, \"status\", \"Git status\")}\n </nav>\n\n {renderActiveTab(data, formState)}\n </DocumentLayout>\n );\n}"],"mappings":";;;;;;;;;;;;;AA+BA,SAAS,QAAQ,SAAkB,KAA0B;CAC3D,OAAO,QAAQ,YAAY,aAAa,QAAQ,KAAK,aAAa,aAAa,QAAQ,KAAK,YAAY;AAC1G;AAEA,SAAS,cAAc,SAAkB,WAAwB,KAAkB,OAAwB;CACzG,MAAM,SAAS,cAAc;CAC7B,OACE,oBAAC,KAAD;EAAG,OAAO,cAAc,SAAS,uBAAuB;EAAM,MAAM,QAAQ,SAAS,GAAG;EAAG,gBAAc,SAAS,SAAS,KAAA;EACxH,UAAA;CACA,CAAA;AAEP;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,UAAU,OAAO,qBAAqB,SAAS,KAAK,YAAY,KAAK,OAAO;CAC9F,IAAI,cAAc,UAAU,OAAO,qBAAqB,SAAS,KAAK,QAAQ,KAAK,OAAO;CAC1F,IAAI,cAAc,YAAY,OAAO,uBAAuB,SAAS,KAAK,UAAU,KAAK,SAAS,SAAS;CAC3G,IAAI,cAAc,WAAW,OAAO,sBAAsB,SAAS,KAAK,SAAS,KAAK,OAAO;CAC7F,IAAI,cAAc,UAAU,OAAO,qBAAqB,SAAS,KAAK,eAAe,KAAK,UAAU,KAAK,OAAO;CAChH,OAAO;AACT;AAEA,SAAgB,0BAA0B,MAA2B,WAAgD;CACnH,MAAM,EAAE,SAAS,cAAc;CAC/B,OACE,qBAAC,gBAAD;EAAgB,OAAO,GAAG,QAAQ,KAAK;EAAc,KAAK;GAAE,QAAQ;GAAY,aAAa,QAAQ;GAAM,aAAa,QAAQ;EAAK;EAArI,UAAA;GACE,qBAAC,OAAD;IAAK,OAAM;IAAX,UAAA,CACE,oBAAC,OAAD;KAAK,OAAM;KAAc,cAAW;KAClC,UAAA,qBAAC,MAAD,EAAA,UAAA;MACE,oBAAC,MAAD,EAAA,UACE,oBAAC,KAAD;OAAG,MAAK;OAAY,UAAA;MAAW,CAAA,EAC7B,CAAA;MACJ,oBAAC,MAAD,EAAA,UACE,oBAAC,KAAD;OAAG,MAAM,aAAa,QAAQ,KAAK;OAAW,UAAA,QAAQ;MAAQ,CAAA,EAC5D,CAAA;MACJ,oBAAC,MAAD,EAAA,UACE,oBAAC,QAAD;OAAM,gBAAa;OAAO,UAAA;MAAc,CAAA,EACtC,CAAA;KACF,EAAA,CAAA;IACD,CAAA,GACL,oBAAC,OAAD;KAAK,OAAM;KACT,UAAA,qBAAC,OAAD,EAAA,UAAA,CACE,oBAAC,MAAD;MAAI,OAAM;MAAqB,UAAA;KAAoB,CAAA,GACnD,qBAAC,KAAD;MAAG,OAAM;MAAT,UAAA;OAA6B;OAAmE,QAAQ;OAAK;MAAI;KAC9G,CAAA,CAAA,EAAA,CAAA;IACF,CAAA,CACF;;GAEL,qBAAC,OAAD;IAAK,OAAM;IAAO,cAAW;IAA7B,UAAA;KACG,cAAc,SAAS,WAAW,WAAW,SAAS;KACtD,cAAc,SAAS,WAAW,SAAS,OAAO;KAClD,cAAc,SAAS,WAAW,UAAU,QAAQ;KACpD,cAAc,SAAS,WAAW,UAAU,QAAQ;KACpD,cAAc,SAAS,WAAW,YAAY,UAAU;KACxD,cAAc,SAAS,WAAW,WAAW,SAAS;KACtD,cAAc,SAAS,WAAW,UAAU,YAAY;IACtD;;GAEJ,gBAAgB,MAAM,SAAS;EAClB;;AAEpB"}
1
+ {"version":3,"file":"project-settings.mjs","names":[],"sources":["../../src/pages/project-settings.tsx"],"sourcesContent":["import type { GitHostProvider } from \"../adapters/git-host/index.ts\";\nimport type { LabelType } from \"../schema/label.ts\";\nimport type { Project } from \"../schema/project.ts\";\nimport type { Token } from \"../schema/token.ts\";\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 { 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 | \"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 webhooks: SettingsWebhook[];\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\nfunction renderTabLink(\n project: Project,\n activeTab: SettingsTab,\n tab: SettingsTab,\n label: string,\n): unknown {\n const active = activeTab === tab;\n return (\n <a\n class={`tabs__link ${active ? \"tabs__link--active\" : \"\"}`}\n href={tabHref(project, tab)}\n aria-current={active ? \"page\" : undefined}\n >\n {label}\n </a>\n );\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\") return renderSettingsLabels(project, data.labelTypes, data.isAdmin);\n if (activeTab === \"tokens\") return renderSettingsTokens(project, data.tokens, data.isAdmin);\n if (activeTab === \"webhooks\")\n return renderSettingsWebhooks(project, data.webhooks, data.isAdmin, formState);\n if (activeTab === \"members\") return renderSettingsMembers(project, data.members, data.isAdmin);\n if (activeTab === \"status\")\n return renderSettingsStatus(project, data.statusConfigs, data.gitHosts, data.isAdmin);\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={{ active: \"settings\", projectSlug: project.slug, projectName: project.name }}\n >\n <div class=\"page-header\">\n <nav class=\"breadcrumbs\" aria-label=\"Breadcrumb\">\n <ol>\n <li>\n <a href=\"/projects\">Projects</a>\n </li>\n <li>\n <a href={`/projects/${project.slug}/builds`}>{project.name}</a>\n </li>\n <li>\n <span aria-current=\"page\">Settings</span>\n </li>\n </ol>\n </nav>\n <div class=\"page-header__row\">\n <div>\n <h1 class=\"page-header__title\">Project settings</h1>\n <p class=\"page-header__desc\">\n Manage general settings, labels, tokens, webhooks and members for {project.name}.\n </p>\n </div>\n </div>\n </div>\n\n <nav class=\"tabs\" aria-label=\"Settings sections\">\n {renderTabLink(project, activeTab, \"general\", \"General\")}\n {renderTabLink(project, activeTab, \"tests\", \"Tests\")}\n {renderTabLink(project, activeTab, \"labels\", \"Labels\")}\n {renderTabLink(project, activeTab, \"tokens\", \"Tokens\")}\n {renderTabLink(project, activeTab, \"webhooks\", \"Webhooks\")}\n {renderTabLink(project, activeTab, \"members\", \"Members\")}\n {renderTabLink(project, activeTab, \"status\", \"Git status\")}\n </nav>\n\n {renderActiveTab(data, formState)}\n </DocumentLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;AA2CA,SAAS,QAAQ,SAAkB,KAA0B;CAC3D,OAAO,QAAQ,YACX,aAAa,QAAQ,KAAK,aAC1B,aAAa,QAAQ,KAAK,YAAY;AAC5C;AAEA,SAAS,cACP,SACA,WACA,KACA,OACS;CACT,MAAM,SAAS,cAAc;CAC7B,OACE,oBAAC,KAAD;EACE,OAAO,cAAc,SAAS,uBAAuB;EACrD,MAAM,QAAQ,SAAS,GAAG;EAC1B,gBAAc,SAAS,SAAS,KAAA;EAE/B,UAAA;CACA,CAAA;AAEP;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,UAAU,OAAO,qBAAqB,SAAS,KAAK,YAAY,KAAK,OAAO;CAC9F,IAAI,cAAc,UAAU,OAAO,qBAAqB,SAAS,KAAK,QAAQ,KAAK,OAAO;CAC1F,IAAI,cAAc,YAChB,OAAO,uBAAuB,SAAS,KAAK,UAAU,KAAK,SAAS,SAAS;CAC/E,IAAI,cAAc,WAAW,OAAO,sBAAsB,SAAS,KAAK,SAAS,KAAK,OAAO;CAC7F,IAAI,cAAc,UAChB,OAAO,qBAAqB,SAAS,KAAK,eAAe,KAAK,UAAU,KAAK,OAAO;CACtF,OAAO;AACT;;AAGA,SAAgB,0BACd,MACA,WACiB;CACjB,MAAM,EAAE,SAAS,cAAc;CAC/B,OACE,qBAAC,gBAAD;EACE,OAAO,GAAG,QAAQ,KAAK;EACvB,KAAK;GAAE,QAAQ;GAAY,aAAa,QAAQ;GAAM,aAAa,QAAQ;EAAK;EAFlF,UAAA;GAIE,qBAAC,OAAD;IAAK,OAAM;IAAX,UAAA,CACE,oBAAC,OAAD;KAAK,OAAM;KAAc,cAAW;KAClC,UAAA,qBAAC,MAAD,EAAA,UAAA;MACE,oBAAC,MAAD,EAAA,UACE,oBAAC,KAAD;OAAG,MAAK;OAAY,UAAA;MAAW,CAAA,EAC7B,CAAA;MACJ,oBAAC,MAAD,EAAA,UACE,oBAAC,KAAD;OAAG,MAAM,aAAa,QAAQ,KAAK;OAAW,UAAA,QAAQ;MAAQ,CAAA,EAC5D,CAAA;MACJ,oBAAC,MAAD,EAAA,UACE,oBAAC,QAAD;OAAM,gBAAa;OAAO,UAAA;MAAc,CAAA,EACtC,CAAA;KACF,EAAA,CAAA;IACD,CAAA,GACL,oBAAC,OAAD;KAAK,OAAM;KACT,UAAA,qBAAC,OAAD,EAAA,UAAA,CACE,oBAAC,MAAD;MAAI,OAAM;MAAqB,UAAA;KAAoB,CAAA,GACnD,qBAAC,KAAD;MAAG,OAAM;MAAT,UAAA;OAA6B;OACwC,QAAQ;OAAK;MAC/E;KACA,CAAA,CAAA,EAAA,CAAA;IACF,CAAA,CACF;;GAEL,qBAAC,OAAD;IAAK,OAAM;IAAO,cAAW;IAA7B,UAAA;KACG,cAAc,SAAS,WAAW,WAAW,SAAS;KACtD,cAAc,SAAS,WAAW,SAAS,OAAO;KAClD,cAAc,SAAS,WAAW,UAAU,QAAQ;KACpD,cAAc,SAAS,WAAW,UAAU,QAAQ;KACpD,cAAc,SAAS,WAAW,YAAY,UAAU;KACxD,cAAc,SAAS,WAAW,WAAW,SAAS;KACtD,cAAc,SAAS,WAAW,UAAU,YAAY;IACtD;;GAEJ,gBAAgB,MAAM,SAAS;EAClB;;AAEpB"}
@@ -5,9 +5,10 @@ import { BuildModel } from "../models/build.mjs";
5
5
  import { ProjectModel } from "../models/project.mjs";
6
6
  import { getStore } from "../store.mjs";
7
7
  import { DocumentLayout } from "../ui/document.mjs";
8
- import { Badge } from "../ui/components.mjs";
8
+ import { Badge } from "../ui/feedback.mjs";
9
9
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
10
10
  //#region src/pages/projects.tsx
11
+ /** Projects overview page: project cards with latest build plus next steps. */
11
12
  async function renderProjectsPage() {
12
13
  const { db, user } = getStore();
13
14
  const projects = await new ProjectModel(db).list();
@@ -91,7 +92,8 @@ async function renderProjectsPage() {
91
92
  /* @__PURE__ */ jsx("code", { children: project.slug }),
92
93
  " ",
93
94
  project.gitRepository ? `· ${project.gitRepository}` : "",
94
- " · default ",
95
+ " · default",
96
+ " ",
95
97
  /* @__PURE__ */ jsx(Badge, {
96
98
  tone: "neutral",
97
99
  children: project.gitDefaultBranch
@@ -106,7 +108,8 @@ async function renderProjectsPage() {
106
108
  info.latest.gitBranch,
107
109
  " · ",
108
110
  info.latest.gitSha.slice(0, 7),
109
- " · ",
111
+ " ·",
112
+ " ",
110
113
  /* @__PURE__ */ jsx(Badge, {
111
114
  tone: info.latest.status === "approved" ? "success" : info.latest.status === "reviewing" ? "warning" : "neutral",
112
115
  children: info.latest.status
@@ -144,7 +147,8 @@ async function renderProjectsPage() {
144
147
  style: "margin:.4rem 0 0; padding-left:1.2rem; color:var(--text-secondary);",
145
148
  children: [
146
149
  /* @__PURE__ */ jsxs("li", { children: [
147
- "Create a project (or run ",
150
+ "Create a project (or run",
151
+ " ",
148
152
  /* @__PURE__ */ jsx("code", { children: "npx @storyshelf/cli create --url http://localhost:3000 --name \"My Storybook\" --token $STORYSHELF_ADMIN_TOKEN" }),
149
153
  ", or ",
150
154
  /* @__PURE__ */ jsx("code", { children: "init --url --slug" }),
@@ -155,7 +159,8 @@ async function renderProjectsPage() {
155
159
  /* @__PURE__ */ jsxs("li", { children: [
156
160
  "Generate a token in ",
157
161
  /* @__PURE__ */ jsx("strong", { children: "Settings → Tokens" }),
158
- " and set ",
162
+ " and set",
163
+ " ",
159
164
  /* @__PURE__ */ jsx("code", { children: "STORYSHELF_TOKEN" }),
160
165
  " in CI."
161
166
  ] }),
@@ -164,7 +169,8 @@ async function renderProjectsPage() {
164
169
  /* @__PURE__ */ jsx("code", { children: "npx @storyshelf/cli upload" }),
165
170
  " (or ",
166
171
  /* @__PURE__ */ jsx("code", { children: "npx @storyshelf/cli" }),
167
- " defaults to upload when config exists)"
172
+ " ",
173
+ "defaults to upload when config exists)"
168
174
  ] })
169
175
  ]
170
176
  })]
@@ -1 +1 @@
1
- {"version":3,"file":"projects.mjs","names":[],"sources":["../../src/pages/projects.tsx"],"sourcesContent":["import type { HtmlEscapedString } from \"hono/utils/html\";\nimport { ProjectModel } from \"../models/project.ts\";\nimport { BuildModel } from \"../models/build.ts\";\nimport { getStore } from \"../store.ts\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\nimport { Badge } from \"../ui/components.tsx\";\n\nexport async function renderProjectsPage(): Promise<RenderedContent> {\n const { db, user } = getStore();\n const projects = await new ProjectModel(db).list();\n const canCreate = !user || user.role === \"admin\" || user.role === \"member\";\n\n const recentCounts = await Promise.all(\n projects.map(async (project) => {\n const builds = await new BuildModel(db).list(project.id);\n return { slug: project.slug, count: builds.length, latest: builds[0] ?? null };\n }),\n );\n const countsBySlug = new Map(recentCounts.map((entry) => [entry.slug, entry]));\n\n return (\n <DocumentLayout title=\"Projects\" nav={{ active: \"projects\" }}>\n <div class=\"page-header\">\n <div class=\"page-header__row\">\n <div>\n <h1 class=\"page-header__title\">Projects</h1>\n <p class=\"page-header__desc\">Each project is one Storybook. Create a project, then upload builds from CI.</p>\n </div>\n <div class=\"page-header__actions\">\n {canCreate ? (\n <a class=\"btn btn--primary\" href=\"/projects/new\">\n New project\n </a>\n ) : null}\n </div>\n </div>\n </div>\n\n {projects.length === 0 ? (\n <div class=\"empty\">\n <h2 class=\"empty__title\">No projects yet</h2>\n <p class=\"empty__desc\">Create your first project to start visual testing. Projects are free and unlimited.</p>\n {canCreate ? (\n <div class=\"empty__action\">\n <a class=\"btn btn--primary\" href=\"/projects/new\">\n Create project\n </a>\n </div>\n ) : null}\n </div>\n ) : (\n <div class=\"grid\" style=\"gap:.75rem;\">\n {projects.map(\n (project): HtmlEscapedString | Promise<HtmlEscapedString> => {\n const info = countsBySlug.get(project.slug);\n return (\n <div key={project.id} class=\"card card--padded\">\n <div style=\"display:flex; justify-content:space-between; gap:1rem; flex-wrap:wrap; align-items:flex-start;\">\n <div style=\"min-width:0;\">\n <h2 style=\"margin:0; font-size:1.15rem;\">\n <a href={`/projects/${project.slug}/builds`}>{project.name}</a>\n </h2>\n <p class=\"field__hint\" style=\"margin:.2rem 0 0;\">\n <code>{project.slug}</code> {project.gitRepository ? `· ${project.gitRepository}` : \"\"} · default <Badge tone=\"neutral\">{project.gitDefaultBranch}</Badge>\n </p>\n {info?.latest ? (\n <p class=\"field__hint\" style=\"margin:.3rem 0 0;\">\n Latest: {info.latest.gitBranch} · {info.latest.gitSha.slice(0, 7)} · <Badge tone={info.latest.status === \"approved\" ? \"success\" : (info.latest.status === \"reviewing\" ? \"warning\" : \"neutral\")}>{info.latest.status}</Badge>\n </p>\n ) : (\n <p class=\"field__hint\" style=\"margin:.3rem 0 0;\">\n No builds yet.\n </p>\n )}\n </div>\n <div style=\"display:flex; gap:.5rem; flex-wrap:wrap;\">\n <a class=\"btn btn--secondary\" href={`/projects/${project.slug}/builds`}>\n Builds {info?.count ? `(${info.count})` : \"\"}\n </a>\n <a class=\"btn btn--ghost\" href={`/projects/${project.slug}/settings`}>\n Settings\n </a>\n </div>\n </div>\n </div>\n );\n })}\n </div>\n )}\n\n <div class=\"card card--padded\" style=\"margin-top:1rem;\">\n <h3 style=\"margin:0 0 .4rem;\">Next steps</h3>\n <ol style=\"margin:.4rem 0 0; padding-left:1.2rem; color:var(--text-secondary);\">\n <li>\n Create a project (or run <code>npx @storyshelf/cli create --url http://localhost:3000 --name \"My Storybook\" --token $STORYSHELF_ADMIN_TOKEN</code>, or <code>init --url --slug</code> to write <code>.storybook/storyshelf.json</code>)\n </li>\n <li>\n Generate a token in <strong>Settings → Tokens</strong> and set <code>STORYSHELF_TOKEN</code> in CI.\n </li>\n <li>\n Upload: <code>npx @storyshelf/cli upload</code> (or <code>npx @storyshelf/cli</code> defaults to upload when config exists)\n </li>\n </ol>\n </div>\n </DocumentLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"projects.mjs","names":[],"sources":["../../src/pages/projects.tsx"],"sourcesContent":["import type { HtmlEscapedString } from \"hono/utils/html\";\nimport { BuildModel } from \"../models/build.ts\";\nimport { ProjectModel } from \"../models/project.ts\";\nimport { getStore } from \"../store.ts\";\nimport { Badge } from \"../ui/components.tsx\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\n\n/** Projects overview page: project cards with latest build plus next steps. */\nexport async function renderProjectsPage(): Promise<RenderedContent> {\n const { db, user } = getStore();\n const projects = await new ProjectModel(db).list();\n const canCreate = !user || user.role === \"admin\" || user.role === \"member\";\n\n const recentCounts = await Promise.all(\n projects.map(async (project) => {\n const builds = await new BuildModel(db).list(project.id);\n return { slug: project.slug, count: builds.length, latest: builds[0] ?? null };\n }),\n );\n const countsBySlug = new Map(recentCounts.map((entry) => [entry.slug, entry]));\n\n return (\n <DocumentLayout title=\"Projects\" nav={{ active: \"projects\" }}>\n <div class=\"page-header\">\n <div class=\"page-header__row\">\n <div>\n <h1 class=\"page-header__title\">Projects</h1>\n <p class=\"page-header__desc\">\n Each project is one Storybook. Create a project, then upload builds from CI.\n </p>\n </div>\n <div class=\"page-header__actions\">\n {canCreate ? (\n <a class=\"btn btn--primary\" href=\"/projects/new\">\n New project\n </a>\n ) : null}\n </div>\n </div>\n </div>\n\n {projects.length === 0 ? (\n <div class=\"empty\">\n <h2 class=\"empty__title\">No projects yet</h2>\n <p class=\"empty__desc\">\n Create your first project to start visual testing. Projects are free and unlimited.\n </p>\n {canCreate ? (\n <div class=\"empty__action\">\n <a class=\"btn btn--primary\" href=\"/projects/new\">\n Create project\n </a>\n </div>\n ) : null}\n </div>\n ) : (\n <div class=\"grid\" style=\"gap:.75rem;\">\n {projects.map((project): HtmlEscapedString | Promise<HtmlEscapedString> => {\n const info = countsBySlug.get(project.slug);\n return (\n <div key={project.id} class=\"card card--padded\">\n <div style=\"display:flex; justify-content:space-between; gap:1rem; flex-wrap:wrap; align-items:flex-start;\">\n <div style=\"min-width:0;\">\n <h2 style=\"margin:0; font-size:1.15rem;\">\n <a href={`/projects/${project.slug}/builds`}>{project.name}</a>\n </h2>\n <p class=\"field__hint\" style=\"margin:.2rem 0 0;\">\n <code>{project.slug}</code>{\" \"}\n {project.gitRepository ? `· ${project.gitRepository}` : \"\"} · default{\" \"}\n <Badge tone=\"neutral\">{project.gitDefaultBranch}</Badge>\n </p>\n {info?.latest ? (\n <p class=\"field__hint\" style=\"margin:.3rem 0 0;\">\n Latest: {info.latest.gitBranch} · {info.latest.gitSha.slice(0, 7)} ·{\" \"}\n <Badge\n tone={\n info.latest.status === \"approved\"\n ? \"success\"\n : info.latest.status === \"reviewing\"\n ? \"warning\"\n : \"neutral\"\n }\n >\n {info.latest.status}\n </Badge>\n </p>\n ) : (\n <p class=\"field__hint\" style=\"margin:.3rem 0 0;\">\n No builds yet.\n </p>\n )}\n </div>\n <div style=\"display:flex; gap:.5rem; flex-wrap:wrap;\">\n <a class=\"btn btn--secondary\" href={`/projects/${project.slug}/builds`}>\n Builds {info?.count ? `(${info.count})` : \"\"}\n </a>\n <a class=\"btn btn--ghost\" href={`/projects/${project.slug}/settings`}>\n Settings\n </a>\n </div>\n </div>\n </div>\n );\n })}\n </div>\n )}\n\n <div class=\"card card--padded\" style=\"margin-top:1rem;\">\n <h3 style=\"margin:0 0 .4rem;\">Next steps</h3>\n <ol style=\"margin:.4rem 0 0; padding-left:1.2rem; color:var(--text-secondary);\">\n <li>\n Create a project (or run{\" \"}\n <code>\n npx @storyshelf/cli create --url http://localhost:3000 --name \"My Storybook\" --token\n $STORYSHELF_ADMIN_TOKEN\n </code>\n , or <code>init --url --slug</code> to write <code>.storybook/storyshelf.json</code>)\n </li>\n <li>\n Generate a token in <strong>Settings → Tokens</strong> and set{\" \"}\n <code>STORYSHELF_TOKEN</code> in CI.\n </li>\n <li>\n Upload: <code>npx @storyshelf/cli upload</code> (or <code>npx @storyshelf/cli</code>{\" \"}\n defaults to upload when config exists)\n </li>\n </ol>\n </div>\n </DocumentLayout>\n );\n}\n"],"mappings":"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"}
@@ -4,6 +4,7 @@ import.meta.url;
4
4
  import { DocumentLayout } from "../ui/document.mjs";
5
5
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
6
6
  //#region src/pages/root.tsx
7
+ /** Landing page introducing capture, diff, and review. */
7
8
  function renderRootPage() {
8
9
  return /* @__PURE__ */ jsxs(DocumentLayout, {
9
10
  title: "Welcome",
@@ -36,11 +37,14 @@ function renderRootPage() {
36
37
  class: "field__hint",
37
38
  style: "margin-top:1rem;",
38
39
  children: [
39
- "Tip: use ",
40
+ "Tip: use",
41
+ " ",
40
42
  /* @__PURE__ */ jsx("code", { children: "npx @storyshelf/cli create --url <url> --name <name> --token $STORYSHELF_ADMIN_TOKEN" }),
41
- " to create a project and token for CI, or ",
43
+ " ",
44
+ "to create a project and token for CI, or ",
42
45
  /* @__PURE__ */ jsx("code", { children: "npx @storyshelf/cli init" }),
43
- " to write ",
46
+ " to write",
47
+ " ",
44
48
  /* @__PURE__ */ jsx("code", { children: ".storybook/storyshelf.json" }),
45
49
  "."
46
50
  ]
@@ -1 +1 @@
1
- {"version":3,"file":"root.mjs","names":[],"sources":["../../src/pages/root.tsx"],"sourcesContent":["import { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\n\nexport function renderRootPage(): RenderedContent {\n return (\n <DocumentLayout title=\"Welcome\" nav={{ active: \"projects\" }}>\n <div class=\"card card--padded\" style=\"text-align:center; padding:2rem 1.5rem;\">\n <h1 style=\"margin:0 0 .4rem; font-size:1.9rem; letter-spacing:-0.02em;\">Welcome to StoryShelf</h1>\n <p style=\"color:var(--text-secondary); max-width:60ch; margin:0 auto 1rem;\">\n Self-hosted visual testing for Storybook. Capture every story, diff against baselines, and review changes before they ship.\n </p>\n <div style=\"display:flex; gap:.5rem; justify-content:center; flex-wrap:wrap;\">\n <a class=\"btn btn--primary\" href=\"/projects\">\n View projects\n </a>\n <a class=\"btn btn--secondary\" href=\"/projects/new\">\n Create project\n </a>\n </div>\n <p class=\"field__hint\" style=\"margin-top:1rem;\">\n Tip: use <code>npx @storyshelf/cli create --url &lt;url&gt; --name &lt;name&gt; --token $STORYSHELF_ADMIN_TOKEN</code> to create a project and token for CI, or <code>npx @storyshelf/cli init</code> to write <code>.storybook/storyshelf.json</code>.\n </p>\n </div>\n\n <div class=\"grid grid--3\" style=\"margin-top:1rem;\">\n <div class=\"card card--padded\">\n <h3 style=\"margin:0 0 .3rem;\">Capture</h3>\n <p class=\"field__hint\">Upload your Storybook build. Server renders stories with Playwright — deterministic, no repo cloning.</p>\n </div>\n <div class=\"card card--padded\">\n <h3 style=\"margin:0 0 .3rem;\">Diff</h3>\n <p class=\"field__hint\">Pixel-perfect diff with pixelmatch. Configurable thresholds, overlay images stored on disk.</p>\n </div>\n <div class=\"card card--padded\">\n <h3 style=\"margin:0 0 .3rem;\">Review</h3>\n <p class=\"field__hint\">Per-branch baselines with fallback to default. Approve changes per-story or bulk.</p>\n </div>\n </div>\n </DocumentLayout>\n );\n}\n"],"mappings":";;;;;;AAEA,SAAgB,iBAAkC;CAChD,OACE,qBAAC,gBAAD;EAAgB,OAAM;EAAU,KAAK,EAAE,QAAQ,WAAW;EAA1D,UAAA,CACE,qBAAC,OAAD;GAAK,OAAM;GAAoB,OAAM;GAArC,UAAA;IACE,oBAAC,MAAD;KAAI,OAAM;KAA8D,UAAA;IAAyB,CAAA;IACjG,oBAAC,KAAD;KAAG,OAAM;KAAmE,UAAA;IAEzE,CAAA;IACH,qBAAC,OAAD;KAAK,OAAM;KAAX,UAAA,CACE,oBAAC,KAAD;MAAG,OAAM;MAAmB,MAAK;MAAY,UAAA;KAE1C,CAAA,GACH,oBAAC,KAAD;MAAG,OAAM;MAAqB,MAAK;MAAgB,UAAA;KAEhD,CAAA,CACA;;IACL,qBAAC,KAAD;KAAG,OAAM;KAAc,OAAM;KAA7B,UAAA;MAAgD;MACrC,oBAAC,QAAD,EAAA,UAAM,uFAAsG,CAAA;MAAC;MAA0C,oBAAC,QAAD,EAAA,UAAM,2BAA8B,CAAA;MAAC;MAAU,oBAAC,QAAD,EAAA,UAAM,6BAAgC,CAAA;MAAC;KACrP;;GACA;EAEL,CAAA,GAAA,qBAAC,OAAD;GAAK,OAAM;GAAe,OAAM;GAAhC,UAAA;IACE,qBAAC,OAAD;KAAK,OAAM;KAAX,UAAA,CACE,oBAAC,MAAD;MAAI,OAAM;MAAoB,UAAA;KAAW,CAAA,GACzC,oBAAC,KAAD;MAAG,OAAM;MAAc,UAAA;KAAwG,CAAA,CAC5H;;IACL,qBAAC,OAAD;KAAK,OAAM;KAAX,UAAA,CACE,oBAAC,MAAD;MAAI,OAAM;MAAoB,UAAA;KAAQ,CAAA,GACtC,oBAAC,KAAD;MAAG,OAAM;MAAc,UAAA;KAA8F,CAAA,CAClH;;IACL,qBAAC,OAAD;KAAK,OAAM;KAAX,UAAA,CACE,oBAAC,MAAD;MAAI,OAAM;MAAoB,UAAA;KAAU,CAAA,GACxC,oBAAC,KAAD;MAAG,OAAM;MAAc,UAAA;KAAoF,CAAA,CACxG;;GACF;EACS,CAAA,CAAA;;AAEpB"}
1
+ {"version":3,"file":"root.mjs","names":[],"sources":["../../src/pages/root.tsx"],"sourcesContent":["import { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\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=\"card card--padded\" style=\"text-align:center; padding:2rem 1.5rem;\">\n <h1 style=\"margin:0 0 .4rem; font-size:1.9rem; letter-spacing:-0.02em;\">\n Welcome to StoryShelf\n </h1>\n <p style=\"color:var(--text-secondary); max-width:60ch; margin:0 auto 1rem;\">\n Self-hosted visual testing for Storybook. Capture every story, diff against baselines, and\n review changes before they ship.\n </p>\n <div style=\"display:flex; gap:.5rem; justify-content:center; flex-wrap:wrap;\">\n <a class=\"btn btn--primary\" href=\"/projects\">\n View projects\n </a>\n <a class=\"btn btn--secondary\" href=\"/projects/new\">\n Create project\n </a>\n </div>\n <p class=\"field__hint\" style=\"margin-top:1rem;\">\n Tip: use{\" \"}\n <code>\n npx @storyshelf/cli 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/cli init</code> to write{\" \"}\n <code>.storybook/storyshelf.json</code>.\n </p>\n </div>\n\n <div class=\"grid grid--3\" style=\"margin-top:1rem;\">\n <div class=\"card card--padded\">\n <h3 style=\"margin:0 0 .3rem;\">Capture</h3>\n <p class=\"field__hint\">\n Upload your Storybook build. Server renders stories with Playwright — deterministic, no\n repo cloning.\n </p>\n </div>\n <div class=\"card card--padded\">\n <h3 style=\"margin:0 0 .3rem;\">Diff</h3>\n <p class=\"field__hint\">\n Pixel-perfect diff with pixelmatch. Configurable thresholds, overlay images stored on\n disk.\n </p>\n </div>\n <div class=\"card card--padded\">\n <h3 style=\"margin:0 0 .3rem;\">Review</h3>\n <p class=\"field__hint\">\n Per-branch baselines with fallback to default. Approve changes per-story or bulk.\n </p>\n </div>\n </div>\n </DocumentLayout>\n );\n}\n"],"mappings":";;;;;;;AAGA,SAAgB,iBAAkC;CAChD,OACE,qBAAC,gBAAD;EAAgB,OAAM;EAAU,KAAK,EAAE,QAAQ,WAAW;EAA1D,UAAA,CACE,qBAAC,OAAD;GAAK,OAAM;GAAoB,OAAM;GAArC,UAAA;IACE,oBAAC,MAAD;KAAI,OAAM;KAA8D,UAAA;IAEpE,CAAA;IACJ,oBAAC,KAAD;KAAG,OAAM;KAAmE,UAAA;IAGzE,CAAA;IACH,qBAAC,OAAD;KAAK,OAAM;KAAX,UAAA,CACE,oBAAC,KAAD;MAAG,OAAM;MAAmB,MAAK;MAAY,UAAA;KAE1C,CAAA,GACH,oBAAC,KAAD;MAAG,OAAM;MAAqB,MAAK;MAAgB,UAAA;KAEhD,CAAA,CACA;;IACL,qBAAC,KAAD;KAAG,OAAM;KAAc,OAAM;KAA7B,UAAA;MAAgD;MACrC;MACT,oBAAC,QAAD,EAAA,UAAM,uFAGA,CAAA;MAAE;MAAI;MAC6B,oBAAC,QAAD,EAAA,UAAM,2BAA8B,CAAA;MAAC;MAAU;MACxF,oBAAC,QAAD,EAAA,UAAM,6BAAgC,CAAA;MAAC;KACtC;;GACA;EAEL,CAAA,GAAA,qBAAC,OAAD;GAAK,OAAM;GAAe,OAAM;GAAhC,UAAA;IACE,qBAAC,OAAD;KAAK,OAAM;KAAX,UAAA,CACE,oBAAC,MAAD;MAAI,OAAM;MAAoB,UAAA;KAAW,CAAA,GACzC,oBAAC,KAAD;MAAG,OAAM;MAAc,UAAA;KAGpB,CAAA,CACA;;IACL,qBAAC,OAAD;KAAK,OAAM;KAAX,UAAA,CACE,oBAAC,MAAD;MAAI,OAAM;MAAoB,UAAA;KAAQ,CAAA,GACtC,oBAAC,KAAD;MAAG,OAAM;MAAc,UAAA;KAGpB,CAAA,CACA;;IACL,qBAAC,OAAD;KAAK,OAAM;KAAX,UAAA,CACE,oBAAC,MAAD;MAAI,OAAM;MAAoB,UAAA;KAAU,CAAA,GACxC,oBAAC,KAAD;MAAG,OAAM;MAAc,UAAA;KAEpB,CAAA,CACA;;GACF;EACS,CAAA,CAAA;;AAEpB"}
@@ -3,6 +3,7 @@ import "node:url";
3
3
  import.meta.url;
4
4
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
5
5
  //#region src/pages/settings-general.tsx
6
+ /** General settings tab: project metadata, diff thresholds, and danger zone. */
6
7
  function renderSettingsGeneral(project, formState, isAdmin) {
7
8
  const errors = formState?.errors ?? {};
8
9
  return /* @__PURE__ */ jsxs("div", {
@@ -1 +1 @@
1
- {"version":3,"file":"settings-general.mjs","names":[],"sources":["../../src/pages/settings-general.tsx"],"sourcesContent":["import type { Project } from \"../schema.ts\";\n\nexport interface SettingsFormState {\n errors?: Record<string, string>;\n globalError?: string;\n}\n\nexport function renderSettingsGeneral(project: Project, formState: SettingsFormState | undefined, isAdmin: boolean): unknown {\n const errors = formState?.errors ?? {};\n return (\n <div class=\"grid\" style=\"max-width: 720px;\">\n {formState?.globalError ? (\n <div class=\"alert alert--danger\" role=\"alert\">\n {formState.globalError}\n </div>\n ) : null}\n <div class=\"card card--padded\">\n <h2 style=\"margin:0 0 .5rem;\">General</h2>\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 <div class=\"field\">\n <label class=\"field__label\" for=\"name\">\n Name\n </label>\n <input class={`field__input ${errors[\"name\"] ? \"field__input--error\" : \"\"}`} id=\"name\" name=\"name\" value={project.name} />\n {errors[\"name\"] ? (\n <p class=\"field__error\" role=\"alert\">\n {errors[\"name\"]}\n </p>\n ) : null}\n </div>\n <div class=\"field\">\n <label class=\"field__label\" for=\"gitRepository\">\n Git repository\n </label>\n <input class=\"field__input\" id=\"gitRepository\" name=\"gitRepository\" value={project.gitRepository ?? \"\"} placeholder=\"owner/repo\" />\n </div>\n <div class=\"field\">\n <label class=\"field__label\" for=\"gitDefaultBranch\">\n Default branch\n </label>\n <input class=\"field__input\" id=\"gitDefaultBranch\" name=\"gitDefaultBranch\" value={project.gitDefaultBranch} />\n </div>\n <div class=\"grid grid--2\">\n <div class=\"field\">\n <label class=\"field__label\" for=\"pixelThreshold\">\n Pixel threshold\n </label>\n <input class=\"field__input\" id=\"pixelThreshold\" name=\"pixelThreshold\" type=\"number\" step=\"0.01\" min=\"0\" max=\"1\" value={String(project.pixelThreshold)} />\n <p class=\"field__hint\">Per-pixel color distance 0–1</p>\n </div>\n <div class=\"field\">\n <label class=\"field__label\" for=\"maxDiffRatio\">\n Max diff ratio\n </label>\n <input class=\"field__input\" id=\"maxDiffRatio\" name=\"maxDiffRatio\" type=\"number\" step=\"0.001\" min=\"0\" max=\"1\" value={String(project.maxDiffRatio)} />\n <p class=\"field__hint\">Allowed diff ratio 0–1</p>\n </div>\n </div>\n <div class=\"field\">\n <label class=\"field__label\" for=\"publicBranchRegex\">\n Public branch regex\n </label>\n <input class=\"field__input\" id=\"publicBranchRegex\" name=\"publicBranchRegex\" value={project.publicBranchRegex ?? \"\"} placeholder=\"^main$\" />\n <p class=\"field__hint\">Branches matching this regex are publicly viewable.</p>\n </div>\n {isAdmin ? (\n <button class=\"btn btn--primary\" type=\"submit\">\n Save changes\n </button>\n ) : (\n <p class=\"field__hint\">You need admin access to edit settings.</p>\n )}\n </form>\n </div>\n\n {isAdmin ? (\n <div class=\"card card--padded\" style=\"border-color: var(--status-rejected);\">\n <h3 style=\"margin:0 0 .4rem; color: var(--status-rejected);\">Danger zone</h3>\n <p class=\"field__hint\">Deleting a project removes all builds, snapshots, baselines and tokens. This cannot be undone.</p>\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 class=\"btn btn--danger\" type=\"submit\">\n Delete project\n </button>\n </form>\n </div>\n ) : null}\n </div>\n );\n}"],"mappings":"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"}
1
+ {"version":3,"file":"settings-general.mjs","names":[],"sources":["../../src/pages/settings-general.tsx"],"sourcesContent":["import type { Project } from \"../schema/project.ts\";\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 return (\n <div class=\"grid\" style=\"max-width: 720px;\">\n {formState?.globalError ? (\n <div class=\"alert alert--danger\" role=\"alert\">\n {formState.globalError}\n </div>\n ) : null}\n <div class=\"card card--padded\">\n <h2 style=\"margin:0 0 .5rem;\">General</h2>\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 <div class=\"field\">\n <label class=\"field__label\" for=\"name\">\n Name\n </label>\n <input\n class={`field__input ${errors[\"name\"] ? \"field__input--error\" : \"\"}`}\n id=\"name\"\n name=\"name\"\n value={project.name}\n />\n {errors[\"name\"] ? (\n <p class=\"field__error\" role=\"alert\">\n {errors[\"name\"]}\n </p>\n ) : null}\n </div>\n <div class=\"field\">\n <label class=\"field__label\" for=\"gitRepository\">\n Git repository\n </label>\n <input\n class=\"field__input\"\n id=\"gitRepository\"\n name=\"gitRepository\"\n value={project.gitRepository ?? \"\"}\n placeholder=\"owner/repo\"\n />\n </div>\n <div class=\"field\">\n <label class=\"field__label\" for=\"gitDefaultBranch\">\n Default branch\n </label>\n <input\n class=\"field__input\"\n id=\"gitDefaultBranch\"\n name=\"gitDefaultBranch\"\n value={project.gitDefaultBranch}\n />\n </div>\n <div class=\"grid grid--2\">\n <div class=\"field\">\n <label class=\"field__label\" for=\"pixelThreshold\">\n Pixel threshold\n </label>\n <input\n class=\"field__input\"\n id=\"pixelThreshold\"\n name=\"pixelThreshold\"\n type=\"number\"\n step=\"0.01\"\n min=\"0\"\n max=\"1\"\n value={String(project.pixelThreshold)}\n />\n <p class=\"field__hint\">Per-pixel color distance 0–1</p>\n </div>\n <div class=\"field\">\n <label class=\"field__label\" for=\"maxDiffRatio\">\n Max diff ratio\n </label>\n <input\n class=\"field__input\"\n id=\"maxDiffRatio\"\n name=\"maxDiffRatio\"\n type=\"number\"\n step=\"0.001\"\n min=\"0\"\n max=\"1\"\n value={String(project.maxDiffRatio)}\n />\n <p class=\"field__hint\">Allowed diff ratio 0–1</p>\n </div>\n </div>\n <div class=\"field\">\n <label class=\"field__label\" for=\"publicBranchRegex\">\n Public branch regex\n </label>\n <input\n class=\"field__input\"\n id=\"publicBranchRegex\"\n name=\"publicBranchRegex\"\n value={project.publicBranchRegex ?? \"\"}\n placeholder=\"^main$\"\n />\n <p class=\"field__hint\">Branches matching this regex are publicly viewable.</p>\n </div>\n {isAdmin ? (\n <button class=\"btn btn--primary\" type=\"submit\">\n Save changes\n </button>\n ) : (\n <p class=\"field__hint\">You need admin access to edit settings.</p>\n )}\n </form>\n </div>\n\n {isAdmin ? (\n <div class=\"card card--padded\" style=\"border-color: var(--status-rejected);\">\n <h3 style=\"margin:0 0 .4rem; color: var(--status-rejected);\">Danger zone</h3>\n <p class=\"field__hint\">\n Deleting a project removes all builds, snapshots, baselines and tokens. This cannot be\n undone.\n </p>\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 class=\"btn btn--danger\" type=\"submit\">\n Delete project\n </button>\n </form>\n </div>\n ) : null}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1,9 +1,10 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
- import { Badge } from "../ui/components.mjs";
4
+ import { Badge } from "../ui/feedback.mjs";
5
5
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
6
6
  //#region src/pages/settings-labels.tsx
7
+ /** Labels settings tab: label-type table plus the create-type form. */
7
8
  function renderSettingsLabels(project, labelTypes, isAdmin) {
8
9
  return /* @__PURE__ */ jsxs("div", {
9
10
  class: "grid",