@storyshelf/app 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (268) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +18 -0
  3. package/dist/app-types.d.mts +54 -0
  4. package/dist/asset-manifest.mjs +41 -0
  5. package/dist/asset-manifest.mjs.map +1 -0
  6. package/dist/asset-paths.mjs +23 -0
  7. package/dist/asset-paths.mjs.map +1 -0
  8. package/dist/assets/htmx.min.js +3304 -0
  9. package/dist/assets/mark.svg +1 -0
  10. package/dist/capture-setup.mjs +139 -0
  11. package/dist/capture-setup.mjs.map +1 -0
  12. package/dist/credential-keys.mjs +20 -0
  13. package/dist/credential-keys.mjs.map +1 -0
  14. package/dist/credentials.mjs +56 -0
  15. package/dist/credentials.mjs.map +1 -0
  16. package/dist/index.d.mts +18 -0
  17. package/dist/index.mjs +310 -0
  18. package/dist/index.mjs.map +1 -0
  19. package/dist/lifecycle.mjs +132 -0
  20. package/dist/lifecycle.mjs.map +1 -0
  21. package/dist/middleware/auth-gate.mjs +36 -0
  22. package/dist/middleware/auth-gate.mjs.map +1 -0
  23. package/dist/middleware/csrf.mjs +75 -0
  24. package/dist/middleware/csrf.mjs.map +1 -0
  25. package/dist/middleware/rate-limit.mjs +68 -0
  26. package/dist/middleware/rate-limit.mjs.map +1 -0
  27. package/dist/middleware/request-log.mjs +30 -0
  28. package/dist/middleware/request-log.mjs.map +1 -0
  29. package/dist/middleware/server-timing.mjs +39 -0
  30. package/dist/middleware/server-timing.mjs.map +1 -0
  31. package/dist/middleware/setup-gate.mjs +37 -0
  32. package/dist/middleware/setup-gate.mjs.map +1 -0
  33. package/dist/middleware/store-scope.mjs +33 -0
  34. package/dist/middleware/store-scope.mjs.map +1 -0
  35. package/dist/notify.d.mts +17 -0
  36. package/dist/notify.mjs +141 -0
  37. package/dist/notify.mjs.map +1 -0
  38. package/dist/pages/admin-system.mjs +243 -0
  39. package/dist/pages/admin-system.mjs.map +1 -0
  40. package/dist/pages/build-detail.mjs +392 -0
  41. package/dist/pages/build-detail.mjs.map +1 -0
  42. package/dist/pages/build-diff-actions.mjs +162 -0
  43. package/dist/pages/build-diff-actions.mjs.map +1 -0
  44. package/dist/pages/build-diff-comments.mjs +104 -0
  45. package/dist/pages/build-diff-comments.mjs.map +1 -0
  46. package/dist/pages/build-diff-header.mjs +117 -0
  47. package/dist/pages/build-diff-header.mjs.map +1 -0
  48. package/dist/pages/build-diff-model.mjs +48 -0
  49. package/dist/pages/build-diff-model.mjs.map +1 -0
  50. package/dist/pages/build-diff-nav.mjs +122 -0
  51. package/dist/pages/build-diff-nav.mjs.map +1 -0
  52. package/dist/pages/build-diff-shortcuts.mjs +78 -0
  53. package/dist/pages/build-diff-shortcuts.mjs.map +1 -0
  54. package/dist/pages/build-diff-stale.mjs +70 -0
  55. package/dist/pages/build-diff-stale.mjs.map +1 -0
  56. package/dist/pages/build-diff-viewer.mjs +70 -0
  57. package/dist/pages/build-diff-viewer.mjs.map +1 -0
  58. package/dist/pages/build-diff.mjs +110 -0
  59. package/dist/pages/build-diff.mjs.map +1 -0
  60. package/dist/pages/compute-jobs.mjs +165 -0
  61. package/dist/pages/compute-jobs.mjs.map +1 -0
  62. package/dist/pages/invite.mjs +91 -0
  63. package/dist/pages/invite.mjs.map +1 -0
  64. package/dist/pages/label-detail.mjs +154 -0
  65. package/dist/pages/label-detail.mjs.map +1 -0
  66. package/dist/pages/library.mjs +343 -0
  67. package/dist/pages/library.mjs.map +1 -0
  68. package/dist/pages/login.mjs +117 -0
  69. package/dist/pages/login.mjs.map +1 -0
  70. package/dist/pages/passkey-ceremony.mjs +97 -0
  71. package/dist/pages/passkey-ceremony.mjs.map +1 -0
  72. package/dist/pages/profile.mjs +364 -0
  73. package/dist/pages/profile.mjs.map +1 -0
  74. package/dist/pages/project-builds-table.mjs +151 -0
  75. package/dist/pages/project-builds-table.mjs.map +1 -0
  76. package/dist/pages/project-builds.mjs +143 -0
  77. package/dist/pages/project-builds.mjs.map +1 -0
  78. package/dist/pages/project-create.mjs +90 -0
  79. package/dist/pages/project-create.mjs.map +1 -0
  80. package/dist/pages/project-settings.mjs +95 -0
  81. package/dist/pages/project-settings.mjs.map +1 -0
  82. package/dist/pages/projects-card.mjs +194 -0
  83. package/dist/pages/projects-card.mjs.map +1 -0
  84. package/dist/pages/projects.mjs +129 -0
  85. package/dist/pages/projects.mjs.map +1 -0
  86. package/dist/pages/root.mjs +98 -0
  87. package/dist/pages/root.mjs.map +1 -0
  88. package/dist/pages/settings-general.mjs +138 -0
  89. package/dist/pages/settings-general.mjs.map +1 -0
  90. package/dist/pages/settings-labels.mjs +107 -0
  91. package/dist/pages/settings-labels.mjs.map +1 -0
  92. package/dist/pages/settings-members.mjs +192 -0
  93. package/dist/pages/settings-members.mjs.map +1 -0
  94. package/dist/pages/settings-notifications.mjs +240 -0
  95. package/dist/pages/settings-notifications.mjs.map +1 -0
  96. package/dist/pages/settings-status.mjs +137 -0
  97. package/dist/pages/settings-status.mjs.map +1 -0
  98. package/dist/pages/settings-tests.mjs +111 -0
  99. package/dist/pages/settings-tests.mjs.map +1 -0
  100. package/dist/pages/settings-tokens.mjs +90 -0
  101. package/dist/pages/settings-tokens.mjs.map +1 -0
  102. package/dist/pages/settings-webhooks.mjs +104 -0
  103. package/dist/pages/settings-webhooks.mjs.map +1 -0
  104. package/dist/pages/storybook.mjs +116 -0
  105. package/dist/pages/storybook.mjs.map +1 -0
  106. package/dist/retention-timer.mjs +65 -0
  107. package/dist/retention-timer.mjs.map +1 -0
  108. package/dist/routers/admin-pages.mjs +48 -0
  109. package/dist/routers/admin-pages.mjs.map +1 -0
  110. package/dist/routers/admin.mjs +113 -0
  111. package/dist/routers/admin.mjs.map +1 -0
  112. package/dist/routers/assets.mjs +64 -0
  113. package/dist/routers/assets.mjs.map +1 -0
  114. package/dist/routers/attempts.mjs +96 -0
  115. package/dist/routers/attempts.mjs.map +1 -0
  116. package/dist/routers/auth-sync.mjs +64 -0
  117. package/dist/routers/auth-sync.mjs.map +1 -0
  118. package/dist/routers/auth.mjs +236 -0
  119. package/dist/routers/auth.mjs.map +1 -0
  120. package/dist/routers/baseline-guard.mjs +83 -0
  121. package/dist/routers/baseline-guard.mjs.map +1 -0
  122. package/dist/routers/builds.handlers.mjs +217 -0
  123. package/dist/routers/builds.handlers.mjs.map +1 -0
  124. package/dist/routers/builds.mjs +381 -0
  125. package/dist/routers/builds.mjs.map +1 -0
  126. package/dist/routers/comments.mjs +109 -0
  127. package/dist/routers/comments.mjs.map +1 -0
  128. package/dist/routers/flash.mjs +35 -0
  129. package/dist/routers/flash.mjs.map +1 -0
  130. package/dist/routers/health-report.mjs +159 -0
  131. package/dist/routers/health-report.mjs.map +1 -0
  132. package/dist/routers/health.mjs +46 -0
  133. package/dist/routers/health.mjs.map +1 -0
  134. package/dist/routers/helpers.mjs +110 -0
  135. package/dist/routers/helpers.mjs.map +1 -0
  136. package/dist/routers/htmx.mjs +24 -0
  137. package/dist/routers/htmx.mjs.map +1 -0
  138. package/dist/routers/labels.mjs +118 -0
  139. package/dist/routers/labels.mjs.map +1 -0
  140. package/dist/routers/media.mjs +53 -0
  141. package/dist/routers/media.mjs.map +1 -0
  142. package/dist/routers/members.mjs +177 -0
  143. package/dist/routers/members.mjs.map +1 -0
  144. package/dist/routers/notification-channels.mjs +190 -0
  145. package/dist/routers/notification-channels.mjs.map +1 -0
  146. package/dist/routers/notification-subscriptions.mjs +120 -0
  147. package/dist/routers/notification-subscriptions.mjs.map +1 -0
  148. package/dist/routers/profile.mjs +276 -0
  149. package/dist/routers/profile.mjs.map +1 -0
  150. package/dist/routers/projects.mjs +133 -0
  151. package/dist/routers/projects.mjs.map +1 -0
  152. package/dist/routers/review-next.mjs +47 -0
  153. package/dist/routers/review-next.mjs.map +1 -0
  154. package/dist/routers/schemas.mjs +337 -0
  155. package/dist/routers/schemas.mjs.map +1 -0
  156. package/dist/routers/settings-general.mjs +104 -0
  157. package/dist/routers/settings-general.mjs.map +1 -0
  158. package/dist/routers/settings-labels.mjs +52 -0
  159. package/dist/routers/settings-labels.mjs.map +1 -0
  160. package/dist/routers/settings-members.mjs +58 -0
  161. package/dist/routers/settings-members.mjs.map +1 -0
  162. package/dist/routers/settings-notifications.mjs +108 -0
  163. package/dist/routers/settings-notifications.mjs.map +1 -0
  164. package/dist/routers/settings-status.mjs +77 -0
  165. package/dist/routers/settings-status.mjs.map +1 -0
  166. package/dist/routers/settings-tokens.mjs +40 -0
  167. package/dist/routers/settings-tokens.mjs.map +1 -0
  168. package/dist/routers/settings-webhooks.mjs +67 -0
  169. package/dist/routers/settings-webhooks.mjs.map +1 -0
  170. package/dist/routers/settings.handlers.mjs +149 -0
  171. package/dist/routers/settings.handlers.mjs.map +1 -0
  172. package/dist/routers/settings.mjs +25 -0
  173. package/dist/routers/settings.mjs.map +1 -0
  174. package/dist/routers/snapshots.mjs +240 -0
  175. package/dist/routers/snapshots.mjs.map +1 -0
  176. package/dist/routers/status-configs.mjs +108 -0
  177. package/dist/routers/status-configs.mjs.map +1 -0
  178. package/dist/routers/storybook.mjs +171 -0
  179. package/dist/routers/storybook.mjs.map +1 -0
  180. package/dist/routers/tokens.mjs +91 -0
  181. package/dist/routers/tokens.mjs.map +1 -0
  182. package/dist/routers/ui.mjs +166 -0
  183. package/dist/routers/ui.mjs.map +1 -0
  184. package/dist/routers/webhooks.mjs +97 -0
  185. package/dist/routers/webhooks.mjs.map +1 -0
  186. package/dist/routers/well-known.mjs +43 -0
  187. package/dist/routers/well-known.mjs.map +1 -0
  188. package/dist/runtime.mjs +52 -0
  189. package/dist/runtime.mjs.map +1 -0
  190. package/dist/store.mjs +20 -0
  191. package/dist/store.mjs.map +1 -0
  192. package/dist/ui/avatar.mjs +76 -0
  193. package/dist/ui/avatar.mjs.map +1 -0
  194. package/dist/ui/buttons.mjs +205 -0
  195. package/dist/ui/buttons.mjs.map +1 -0
  196. package/dist/ui/code-block.mjs +47 -0
  197. package/dist/ui/code-block.mjs.map +1 -0
  198. package/dist/ui/compare-stage.mjs +344 -0
  199. package/dist/ui/compare-stage.mjs.map +1 -0
  200. package/dist/ui/csrf-field.mjs +20 -0
  201. package/dist/ui/csrf-field.mjs.map +1 -0
  202. package/dist/ui/css.mjs +36 -0
  203. package/dist/ui/css.mjs.map +1 -0
  204. package/dist/ui/document.mjs +130 -0
  205. package/dist/ui/document.mjs.map +1 -0
  206. package/dist/ui/dropdown.mjs +181 -0
  207. package/dist/ui/dropdown.mjs.map +1 -0
  208. package/dist/ui/feedback.mjs +341 -0
  209. package/dist/ui/feedback.mjs.map +1 -0
  210. package/dist/ui/filter-input.mjs +58 -0
  211. package/dist/ui/filter-input.mjs.map +1 -0
  212. package/dist/ui/forms.mjs +291 -0
  213. package/dist/ui/forms.mjs.map +1 -0
  214. package/dist/ui/icons/icon.mjs +53 -0
  215. package/dist/ui/icons/icon.mjs.map +1 -0
  216. package/dist/ui/icons/paths.mjs +61 -0
  217. package/dist/ui/icons/paths.mjs.map +1 -0
  218. package/dist/ui/icons/sprite.mjs +26 -0
  219. package/dist/ui/icons/sprite.mjs.map +1 -0
  220. package/dist/ui/kbd.mjs +33 -0
  221. package/dist/ui/kbd.mjs.map +1 -0
  222. package/dist/ui/layout.mjs +173 -0
  223. package/dist/ui/layout.mjs.map +1 -0
  224. package/dist/ui/progress.mjs +44 -0
  225. package/dist/ui/progress.mjs.map +1 -0
  226. package/dist/ui/segmented.mjs +77 -0
  227. package/dist/ui/segmented.mjs.map +1 -0
  228. package/dist/ui/shell/account.mjs +161 -0
  229. package/dist/ui/shell/account.mjs.map +1 -0
  230. package/dist/ui/shell/bare.mjs +69 -0
  231. package/dist/ui/shell/bare.mjs.map +1 -0
  232. package/dist/ui/shell/boot-script.mjs +46 -0
  233. package/dist/ui/shell/boot-script.mjs.map +1 -0
  234. package/dist/ui/shell/client-script.mjs +259 -0
  235. package/dist/ui/shell/client-script.mjs.map +1 -0
  236. package/dist/ui/shell/nav.mjs +111 -0
  237. package/dist/ui/shell/nav.mjs.map +1 -0
  238. package/dist/ui/shell/review-script.mjs +195 -0
  239. package/dist/ui/shell/review-script.mjs.map +1 -0
  240. package/dist/ui/shell/sidebar.mjs +323 -0
  241. package/dist/ui/shell/sidebar.mjs.map +1 -0
  242. package/dist/ui/shell/topbar.mjs +268 -0
  243. package/dist/ui/shell/topbar.mjs.map +1 -0
  244. package/dist/ui/stacks.mjs +82 -0
  245. package/dist/ui/stacks.mjs.map +1 -0
  246. package/dist/ui/styles/base.mjs +41 -0
  247. package/dist/ui/styles/base.mjs.map +1 -0
  248. package/dist/ui/styles/behavior.mjs +43 -0
  249. package/dist/ui/styles/behavior.mjs.map +1 -0
  250. package/dist/ui/styles/review-layout.mjs +347 -0
  251. package/dist/ui/styles/review-layout.mjs.map +1 -0
  252. package/dist/ui/styles/review.mjs +86 -0
  253. package/dist/ui/styles/review.mjs.map +1 -0
  254. package/dist/ui/styles/tokens.mjs +132 -0
  255. package/dist/ui/styles/tokens.mjs.map +1 -0
  256. package/dist/ui/styles.mjs +19 -0
  257. package/dist/ui/styles.mjs.map +1 -0
  258. package/dist/ui/subnav.mjs +91 -0
  259. package/dist/ui/subnav.mjs.map +1 -0
  260. package/dist/ui/table.mjs +73 -0
  261. package/dist/ui/table.mjs.map +1 -0
  262. package/dist/ui/theme.mjs +64 -0
  263. package/dist/ui/theme.mjs.map +1 -0
  264. package/dist/ui/thumbnail.mjs +51 -0
  265. package/dist/ui/thumbnail.mjs.map +1 -0
  266. package/dist/ui/time.mjs +50 -0
  267. package/dist/ui/time.mjs.map +1 -0
  268. package/package.json +92 -0
@@ -0,0 +1,192 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { Button } from "../ui/buttons.mjs";
5
+ import { Alert, Badge, Meta } from "../ui/feedback.mjs";
6
+ import { Field, FormActions, SelectField } from "../ui/forms.mjs";
7
+ import { Card, SectionTitle } from "../ui/layout.mjs";
8
+ import { Table } from "../ui/table.mjs";
9
+ import { csrfField } from "../ui/csrf-field.mjs";
10
+ import { jsx, jsxs } from "hono/jsx/jsx-runtime";
11
+ //#region src/pages/settings-members.tsx
12
+ /** Members settings tab: member roster plus the add-member form. */
13
+ function renderSettingsMembers(project, members, groupMappings, isAdmin, formState) {
14
+ return /* @__PURE__ */ jsxs("div", {
15
+ class: "grid max-w-form",
16
+ children: [
17
+ formState?.globalError ? /* @__PURE__ */ jsx(Alert, {
18
+ tone: "danger",
19
+ children: formState.globalError
20
+ }) : null,
21
+ /* @__PURE__ */ jsxs(Card, { children: [
22
+ /* @__PURE__ */ jsx(SectionTitle, { children: "Members" }),
23
+ /* @__PURE__ */ jsx(Meta, { children: "Project members and their roles. Site admins have implicit admin access." }),
24
+ /* @__PURE__ */ jsx(Table, { children: /* @__PURE__ */ jsxs("table", { children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
25
+ /* @__PURE__ */ jsx("th", { children: "User" }),
26
+ /* @__PURE__ */ jsx("th", { children: "Role" }),
27
+ /* @__PURE__ */ jsx("th", { children: "Since" }),
28
+ /* @__PURE__ */ jsx("th", { children: /* @__PURE__ */ jsx("span", {
29
+ class: "visually-hidden",
30
+ children: "Actions"
31
+ }) })
32
+ ] }) }), /* @__PURE__ */ jsx("tbody", { children: members.map((member) => /* @__PURE__ */ jsxs("tr", { children: [
33
+ /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx("code", { children: member.userId }) }),
34
+ /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(Badge, {
35
+ tone: member.role === "admin" ? "danger" : member.role === "viewer" ? "neutral" : "info",
36
+ children: member.role
37
+ }) }),
38
+ /* @__PURE__ */ jsx("td", { children: new Date(member.createdAt).toLocaleDateString() }),
39
+ /* @__PURE__ */ jsx("td", { children: isAdmin ? /* @__PURE__ */ jsxs("form", {
40
+ method: "post",
41
+ action: `/projects/${project.slug}/settings/members/${member.userId}/remove`,
42
+ "hx-post": `/projects/${project.slug}/settings/members/${member.userId}/remove`,
43
+ "hx-target": "body",
44
+ "hx-confirm": "Remove this member from the project?",
45
+ children: [csrfField(), /* @__PURE__ */ jsx(Button, {
46
+ variant: "ghost",
47
+ type: "submit",
48
+ children: "Remove"
49
+ })]
50
+ }) : null })
51
+ ] }, member.id)) })] }) }),
52
+ members.length === 0 ? /* @__PURE__ */ jsx(Meta, { children: "No members yet." }) : null
53
+ ] }),
54
+ isAdmin ? /* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsx(SectionTitle, {
55
+ level: 3,
56
+ children: "Add member"
57
+ }), /* @__PURE__ */ jsxs("form", {
58
+ method: "post",
59
+ action: `/projects/${project.slug}/settings/members`,
60
+ "hx-post": `/projects/${project.slug}/settings/members`,
61
+ "hx-target": "body",
62
+ children: [
63
+ csrfField(),
64
+ /* @__PURE__ */ jsx(Field, {
65
+ label: "User ID",
66
+ name: "userId",
67
+ required: true,
68
+ placeholder: "user_..."
69
+ }),
70
+ /* @__PURE__ */ jsx(SelectField, {
71
+ label: "Role",
72
+ name: "role",
73
+ options: [
74
+ {
75
+ value: "viewer",
76
+ label: "viewer"
77
+ },
78
+ {
79
+ value: "developer",
80
+ label: "developer"
81
+ },
82
+ {
83
+ value: "approver",
84
+ label: "approver"
85
+ },
86
+ {
87
+ value: "admin",
88
+ label: "admin"
89
+ }
90
+ ]
91
+ }),
92
+ /* @__PURE__ */ jsx(FormActions, { children: /* @__PURE__ */ jsx(Button, {
93
+ variant: "primary",
94
+ type: "submit",
95
+ icon: "plus",
96
+ children: "Add member"
97
+ }) })
98
+ ]
99
+ })] }) : null,
100
+ /* @__PURE__ */ jsx("div", {
101
+ class: "mt-1",
102
+ children: /* @__PURE__ */ jsxs(Card, { children: [
103
+ /* @__PURE__ */ jsx(SectionTitle, {
104
+ level: 3,
105
+ children: "Identity-provider group mappings"
106
+ }),
107
+ /* @__PURE__ */ jsx(Meta, { children: "Map an IdP group (name or provider ID, exact match) to a project role. Members sync at next login; removing a mapping revokes synced grants but never manual ones. Wildcards are not expanded." }),
108
+ /* @__PURE__ */ jsx(Table, { children: /* @__PURE__ */ jsxs("table", { children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
109
+ /* @__PURE__ */ jsx("th", { children: "Group" }),
110
+ /* @__PURE__ */ jsx("th", { children: "Role" }),
111
+ /* @__PURE__ */ jsx("th", { children: /* @__PURE__ */ jsx("span", {
112
+ class: "visually-hidden",
113
+ children: "Actions"
114
+ }) })
115
+ ] }) }), /* @__PURE__ */ jsx("tbody", { children: groupMappings.map((mapping) => /* @__PURE__ */ jsxs("tr", { children: [
116
+ /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx("code", { children: mapping.groupName }) }),
117
+ /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(Badge, {
118
+ tone: mapping.role === "admin" ? "danger" : "info",
119
+ children: mapping.role
120
+ }) }),
121
+ /* @__PURE__ */ jsx("td", { children: isAdmin ? /* @__PURE__ */ jsxs("form", {
122
+ method: "post",
123
+ action: `/projects/${project.slug}/settings/members/groups/${mapping.id}/remove`,
124
+ "hx-post": `/projects/${project.slug}/settings/members/groups/${mapping.id}/remove`,
125
+ "hx-target": "body",
126
+ "hx-confirm": "Remove this group mapping?",
127
+ children: [csrfField(), /* @__PURE__ */ jsx(Button, {
128
+ variant: "ghost",
129
+ type: "submit",
130
+ children: "Remove"
131
+ })]
132
+ }) : null })
133
+ ] }, mapping.id)) })] }) }),
134
+ groupMappings.length === 0 ? /* @__PURE__ */ jsx(Meta, { children: "No group mappings yet." }) : null
135
+ ] })
136
+ }),
137
+ isAdmin ? /* @__PURE__ */ jsx("div", {
138
+ class: "mt-1",
139
+ children: /* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsx(SectionTitle, {
140
+ level: 3,
141
+ children: "Add group mapping"
142
+ }), /* @__PURE__ */ jsxs("form", {
143
+ method: "post",
144
+ action: `/projects/${project.slug}/settings/members/groups`,
145
+ "hx-post": `/projects/${project.slug}/settings/members/groups`,
146
+ "hx-target": "body",
147
+ children: [
148
+ csrfField(),
149
+ /* @__PURE__ */ jsx(Field, {
150
+ label: "Group name or ID (exact match)",
151
+ name: "groupName",
152
+ required: true,
153
+ placeholder: "team-design"
154
+ }),
155
+ /* @__PURE__ */ jsx(SelectField, {
156
+ label: "Role",
157
+ name: "role",
158
+ options: [
159
+ {
160
+ value: "viewer",
161
+ label: "viewer"
162
+ },
163
+ {
164
+ value: "developer",
165
+ label: "developer"
166
+ },
167
+ {
168
+ value: "approver",
169
+ label: "approver"
170
+ },
171
+ {
172
+ value: "admin",
173
+ label: "admin"
174
+ }
175
+ ]
176
+ }),
177
+ /* @__PURE__ */ jsx(FormActions, { children: /* @__PURE__ */ jsx(Button, {
178
+ variant: "primary",
179
+ type: "submit",
180
+ icon: "plus",
181
+ children: "Add mapping"
182
+ }) })
183
+ ]
184
+ })] })
185
+ }) : null
186
+ ]
187
+ });
188
+ }
189
+ //#endregion
190
+ export { renderSettingsMembers };
191
+
192
+ //# sourceMappingURL=settings-members.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings-members.mjs","names":[],"sources":["../../src/pages/settings-members.tsx"],"sourcesContent":["import type { Project } from \"@storyshelf/core/schema\";\nimport type { ProjectGroupMapping } from \"@storyshelf/core/schema\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport {\n Alert,\n Badge,\n Button,\n Card,\n Field,\n FormActions,\n Meta,\n SectionTitle,\n SelectField,\n Table,\n} from \"../ui/components.tsx\";\nimport { csrfField } from \"../ui/csrf-field.tsx\";\n\n/** Project member row as rendered in the members settings tab. */\nexport interface SettingsMember {\n id: string;\n projectId: string;\n userId: string;\n role: string;\n createdAt: string;\n}\n\n/** Members settings tab: member roster plus the add-member form. */\nexport function renderSettingsMembers(\n project: Project,\n members: SettingsMember[],\n groupMappings: ProjectGroupMapping[],\n isAdmin: boolean,\n formState?: { globalError?: string },\n): unknown {\n return (\n <div class=\"grid max-w-form\">\n {formState?.globalError ? <Alert tone=\"danger\">{formState.globalError}</Alert> : null}\n <Card>\n <SectionTitle>Members</SectionTitle>\n <Meta>Project members and their roles. Site admins have implicit admin access.</Meta>\n <Table>\n <table>\n <thead>\n <tr>\n <th>User</th>\n <th>Role</th>\n <th>Since</th>\n <th>\n <span class=\"visually-hidden\">Actions</span>\n </th>\n </tr>\n </thead>\n <tbody>\n {members.map((member): HtmlEscapedString | Promise<HtmlEscapedString> => (\n <tr key={member.id}>\n <td>\n <code>{member.userId}</code>\n </td>\n <td>\n <Badge\n tone={\n member.role === \"admin\"\n ? \"danger\"\n : member.role === \"viewer\"\n ? \"neutral\"\n : \"info\"\n }\n >\n {member.role}\n </Badge>\n </td>\n <td>{new Date(member.createdAt).toLocaleDateString()}</td>\n <td>\n {isAdmin ? (\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/members/${member.userId}/remove`}\n hx-post={`/projects/${project.slug}/settings/members/${member.userId}/remove`}\n hx-target=\"body\"\n hx-confirm=\"Remove this member from the project?\"\n >\n {csrfField()}\n <Button variant=\"ghost\" type=\"submit\">\n Remove\n </Button>\n </form>\n ) : null}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </Table>\n {members.length === 0 ? <Meta>No members yet.</Meta> : null}\n </Card>\n\n {isAdmin ? (\n <Card>\n <SectionTitle level={3}>Add member</SectionTitle>\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/members`}\n hx-post={`/projects/${project.slug}/settings/members`}\n hx-target=\"body\"\n >\n {csrfField()}\n <Field label=\"User ID\" name=\"userId\" required placeholder=\"user_...\" />\n <SelectField\n label=\"Role\"\n name=\"role\"\n options={[\n { value: \"viewer\", label: \"viewer\" },\n { value: \"developer\", label: \"developer\" },\n { value: \"approver\", label: \"approver\" },\n { value: \"admin\", label: \"admin\" },\n ]}\n />\n <FormActions>\n <Button variant=\"primary\" type=\"submit\" icon=\"plus\">\n Add member\n </Button>\n </FormActions>\n </form>\n </Card>\n ) : null}\n\n <div class=\"mt-1\">\n <Card>\n <SectionTitle level={3}>Identity-provider group mappings</SectionTitle>\n <Meta>\n Map an IdP group (name or provider ID, exact match) to a project role. Members sync at\n next login; removing a mapping revokes synced grants but never manual ones. Wildcards\n are not expanded.\n </Meta>\n <Table>\n <table>\n <thead>\n <tr>\n <th>Group</th>\n <th>Role</th>\n <th>\n <span class=\"visually-hidden\">Actions</span>\n </th>\n </tr>\n </thead>\n <tbody>\n {groupMappings.map((mapping): HtmlEscapedString | Promise<HtmlEscapedString> => (\n <tr key={mapping.id}>\n <td>\n <code>{mapping.groupName}</code>\n </td>\n <td>\n <Badge tone={mapping.role === \"admin\" ? \"danger\" : \"info\"}>\n {mapping.role}\n </Badge>\n </td>\n <td>\n {isAdmin ? (\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/members/groups/${mapping.id}/remove`}\n hx-post={`/projects/${project.slug}/settings/members/groups/${mapping.id}/remove`}\n hx-target=\"body\"\n hx-confirm=\"Remove this group mapping?\"\n >\n {csrfField()}\n <Button variant=\"ghost\" type=\"submit\">\n Remove\n </Button>\n </form>\n ) : null}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </Table>\n {groupMappings.length === 0 ? <Meta>No group mappings yet.</Meta> : null}\n </Card>\n </div>\n\n {isAdmin ? (\n <div class=\"mt-1\">\n <Card>\n <SectionTitle level={3}>Add group mapping</SectionTitle>\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/members/groups`}\n hx-post={`/projects/${project.slug}/settings/members/groups`}\n hx-target=\"body\"\n >\n {csrfField()}\n <Field\n label=\"Group name or ID (exact match)\"\n name=\"groupName\"\n required\n placeholder=\"team-design\"\n />\n <SelectField\n label=\"Role\"\n name=\"role\"\n options={[\n { value: \"viewer\", label: \"viewer\" },\n { value: \"developer\", label: \"developer\" },\n { value: \"approver\", label: \"approver\" },\n { value: \"admin\", label: \"admin\" },\n ]}\n />\n <FormActions>\n <Button variant=\"primary\" type=\"submit\" icon=\"plus\">\n Add mapping\n </Button>\n </FormActions>\n </form>\n </Card>\n </div>\n ) : null}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,240 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { Button } from "../ui/buttons.mjs";
5
+ import { Alert, Badge, Meta } from "../ui/feedback.mjs";
6
+ import { CheckField, Field, FormActions, SelectField } from "../ui/forms.mjs";
7
+ import { Card, SectionTitle } from "../ui/layout.mjs";
8
+ import { HStack } from "../ui/stacks.mjs";
9
+ import { Table } from "../ui/table.mjs";
10
+ import { csrfField } from "../ui/csrf-field.mjs";
11
+ import { jsx, jsxs } from "hono/jsx/jsx-runtime";
12
+ //#region src/pages/settings-notifications.tsx
13
+ /** Notification topic catalog (mirrors the webhook event names). */
14
+ const NOTIFICATION_TOPICS = [
15
+ {
16
+ event: "build:created",
17
+ label: "Build created"
18
+ },
19
+ {
20
+ event: "build:reviewing",
21
+ label: "Build needs review"
22
+ },
23
+ {
24
+ event: "build:approved",
25
+ label: "Build approved"
26
+ },
27
+ {
28
+ event: "build:rejected",
29
+ label: "Build rejected"
30
+ },
31
+ {
32
+ event: "baseline:created",
33
+ label: "Baseline created"
34
+ },
35
+ {
36
+ event: "baseline:updated",
37
+ label: "Baseline updated"
38
+ },
39
+ {
40
+ event: "comment:created",
41
+ label: "Comment posted"
42
+ }
43
+ ];
44
+ /** Whether a topic checkbox renders checked (empty allowlist means all). */
45
+ function topicChecked(events, event) {
46
+ return events.length === 0 || events.includes(event);
47
+ }
48
+ /** The viewer's own preferences card (visible to every member). */
49
+ const MyNotificationsCard = ({ project, mySubscription }) => {
50
+ const events = mySubscription?.events ?? [];
51
+ return /* @__PURE__ */ jsxs(Card, { children: [
52
+ /* @__PURE__ */ jsx(SectionTitle, { children: "My notifications for this project" }),
53
+ /* @__PURE__ */ jsx(Meta, { children: "Opt in to email alerts for this project. Leave everything off to stay silent." }),
54
+ /* @__PURE__ */ jsxs("form", {
55
+ method: "post",
56
+ action: `/projects/${project.slug}/settings/notifications/me`,
57
+ "hx-post": `/projects/${project.slug}/settings/notifications/me`,
58
+ "hx-target": "body",
59
+ children: [
60
+ csrfField(),
61
+ /* @__PURE__ */ jsx(CheckField, {
62
+ label: "Email me about this project",
63
+ name: "enabled",
64
+ checked: mySubscription?.enabled ?? false
65
+ }),
66
+ NOTIFICATION_TOPICS.map((topic) => /* @__PURE__ */ jsx(CheckField, {
67
+ label: topic.label,
68
+ name: "events",
69
+ value: topic.event,
70
+ checked: topicChecked(events, topic.event)
71
+ })),
72
+ /* @__PURE__ */ jsx("p", { children: /* @__PURE__ */ jsx(Meta, {
73
+ as: "span",
74
+ children: "No boxes ticked means all topics."
75
+ }) }),
76
+ /* @__PURE__ */ jsx(FormActions, { children: /* @__PURE__ */ jsx(Button, {
77
+ variant: "primary",
78
+ type: "submit",
79
+ icon: "check",
80
+ children: "Save my preferences"
81
+ }) })
82
+ ]
83
+ })
84
+ ] });
85
+ };
86
+ /** Human-readable channel target (email address or masked webhook host). */
87
+ function targetOf(channel) {
88
+ const to = channel.config["to"];
89
+ if (typeof to === "string" && to) return to;
90
+ return channel.hasSecret ? `${channel.provider} webhook` : "—";
91
+ }
92
+ /** One channel table row with the admin delete action. */
93
+ const ChannelRow = ({ project, channel }) => /* @__PURE__ */ jsxs("tr", { children: [
94
+ /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(Badge, {
95
+ tone: "neutral",
96
+ children: channel.provider
97
+ }) }),
98
+ /* @__PURE__ */ jsx("td", {
99
+ class: "truncate max-w-cell",
100
+ title: targetOf(channel),
101
+ children: targetOf(channel)
102
+ }),
103
+ /* @__PURE__ */ jsx("td", { children: channel.events.length === 0 ? /* @__PURE__ */ jsx(Meta, {
104
+ as: "span",
105
+ children: "all events"
106
+ }) : /* @__PURE__ */ jsx(HStack, { children: channel.events.map((event) => /* @__PURE__ */ jsx(Badge, {
107
+ tone: "neutral",
108
+ children: event
109
+ })) }) }),
110
+ /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsxs("form", {
111
+ method: "post",
112
+ action: `/projects/${project.slug}/settings/notifications/${channel.id}/delete`,
113
+ "hx-post": `/projects/${project.slug}/settings/notifications/${channel.id}/delete`,
114
+ "hx-target": "body",
115
+ "hx-confirm": "Remove this notification channel?",
116
+ children: [csrfField(), /* @__PURE__ */ jsx(Button, {
117
+ variant: "ghost",
118
+ size: "sm",
119
+ type: "submit",
120
+ children: "Delete"
121
+ })]
122
+ }) })
123
+ ] });
124
+ /** Channel list card (delete action renders for admins only). */
125
+ const ChannelsCard = ({ project, channels, isAdmin }) => /* @__PURE__ */ jsxs(Card, { children: [
126
+ /* @__PURE__ */ jsx(SectionTitle, { children: "Channels" }),
127
+ /* @__PURE__ */ jsx(Meta, { children: "Project admins route build events to Slack, Teams, or email. Secrets are stored encrypted and decrypted only in memory at send time." }),
128
+ /* @__PURE__ */ jsx(Table, { children: /* @__PURE__ */ jsxs("table", { children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
129
+ /* @__PURE__ */ jsx("th", { children: "Provider" }),
130
+ /* @__PURE__ */ jsx("th", { children: "Target" }),
131
+ /* @__PURE__ */ jsx("th", { children: "Events" }),
132
+ /* @__PURE__ */ jsx("th", { children: /* @__PURE__ */ jsx("span", {
133
+ class: "visually-hidden",
134
+ children: "Actions"
135
+ }) })
136
+ ] }) }), /* @__PURE__ */ jsx("tbody", { children: channels.map((channel) => isAdmin ? /* @__PURE__ */ jsx(ChannelRow, {
137
+ project,
138
+ channel
139
+ }) : /* @__PURE__ */ jsxs("tr", { children: [
140
+ /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(Badge, {
141
+ tone: "neutral",
142
+ children: channel.provider
143
+ }) }),
144
+ /* @__PURE__ */ jsx("td", {
145
+ class: "truncate max-w-cell",
146
+ title: targetOf(channel),
147
+ children: targetOf(channel)
148
+ }),
149
+ /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(Meta, {
150
+ as: "span",
151
+ children: channel.events.length === 0 ? "all events" : channel.events.join(", ")
152
+ }) }),
153
+ /* @__PURE__ */ jsx("td", { children: null })
154
+ ] }, channel.id)) })] }) }),
155
+ channels.length === 0 ? /* @__PURE__ */ jsx(Meta, { children: "No channels configured." }) : null
156
+ ] });
157
+ /** Channel creation card (admins only). */
158
+ const CreateChannelCard = ({ project, providers, formState }) => /* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsx(SectionTitle, {
159
+ level: 3,
160
+ children: "Create channel"
161
+ }), /* @__PURE__ */ jsxs("form", {
162
+ method: "post",
163
+ action: `/projects/${project.slug}/settings/notifications`,
164
+ "hx-post": `/projects/${project.slug}/settings/notifications`,
165
+ "hx-target": "body",
166
+ children: [
167
+ csrfField(),
168
+ /* @__PURE__ */ jsx(SelectField, {
169
+ label: "Provider",
170
+ name: "provider",
171
+ options: providers.map((provider) => ({
172
+ value: provider,
173
+ label: provider
174
+ })),
175
+ hint: formState?.errors?.["provider"]
176
+ }),
177
+ /* @__PURE__ */ jsx(Field, {
178
+ label: "Target",
179
+ name: "target",
180
+ type: "text",
181
+ required: true,
182
+ placeholder: "team@example.com or https://hooks.slack.com/…",
183
+ hint: "Email address for email channels, webhook URL otherwise.",
184
+ error: formState?.errors?.["target"]
185
+ }),
186
+ /* @__PURE__ */ jsx(Field, {
187
+ label: "Events",
188
+ name: "events",
189
+ placeholder: "build.reviewing, build.approved",
190
+ hint: "Comma-separated. Leave blank to receive all events."
191
+ }),
192
+ /* @__PURE__ */ jsx(SelectField, {
193
+ label: "Style",
194
+ name: "style",
195
+ options: [{
196
+ value: "compact",
197
+ label: "Compact"
198
+ }, {
199
+ value: "verbose",
200
+ label: "Verbose"
201
+ }]
202
+ }),
203
+ /* @__PURE__ */ jsx(FormActions, { children: /* @__PURE__ */ jsx(Button, {
204
+ variant: "primary",
205
+ type: "submit",
206
+ icon: "plus",
207
+ children: "Add channel"
208
+ }) })
209
+ ]
210
+ })] });
211
+ /** Notifications settings tab: channel list, create form, and my preferences. */
212
+ function renderSettingsNotifications(project, channels, providers, mySubscription, isAdmin, formState) {
213
+ return /* @__PURE__ */ jsxs("div", {
214
+ class: "grid max-w-form",
215
+ children: [
216
+ formState?.globalError ? /* @__PURE__ */ jsx(Alert, {
217
+ tone: "danger",
218
+ children: formState.globalError
219
+ }) : null,
220
+ /* @__PURE__ */ jsx(MyNotificationsCard, {
221
+ project,
222
+ mySubscription
223
+ }),
224
+ /* @__PURE__ */ jsx(ChannelsCard, {
225
+ project,
226
+ channels,
227
+ isAdmin
228
+ }),
229
+ isAdmin ? /* @__PURE__ */ jsx(CreateChannelCard, {
230
+ project,
231
+ providers,
232
+ formState
233
+ }) : null
234
+ ]
235
+ });
236
+ }
237
+ //#endregion
238
+ export { NOTIFICATION_TOPICS, renderSettingsNotifications };
239
+
240
+ //# sourceMappingURL=settings-notifications.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings-notifications.mjs","names":[],"sources":["../../src/pages/settings-notifications.tsx"],"sourcesContent":["import type { Project } from \"@storyshelf/core/schema\";\nimport type { FC } from \"hono/jsx\";\nimport {\n Alert,\n Badge,\n Button,\n Card,\n CheckField,\n Field,\n FormActions,\n HStack,\n Meta,\n SectionTitle,\n SelectField,\n Table,\n} from \"../ui/components.tsx\";\nimport { csrfField } from \"../ui/csrf-field.tsx\";\n\n/** Notification topic catalog (mirrors the webhook event names). */\nexport const NOTIFICATION_TOPICS: { event: string; label: string }[] = [\n { event: \"build:created\", label: \"Build created\" },\n { event: \"build:reviewing\", label: \"Build needs review\" },\n { event: \"build:approved\", label: \"Build approved\" },\n { event: \"build:rejected\", label: \"Build rejected\" },\n { event: \"baseline:created\", label: \"Baseline created\" },\n { event: \"baseline:updated\", label: \"Baseline updated\" },\n { event: \"comment:created\", label: \"Comment posted\" },\n];\n\n/** Channel row as rendered in the notifications settings tab. */\nexport interface SettingsNotificationChannel {\n id: string;\n provider: string;\n config: Record<string, unknown>;\n hasSecret: boolean;\n events: string[];\n enabled: boolean;\n}\n\n/** The viewer's own subscription for this project (null when opted out). */\nexport interface SettingsMySubscription {\n events: string[];\n via: string[];\n enabled: boolean;\n}\n\n/** Form state for the notifications tab (field errors, global error). */\nexport interface NotificationsFormState {\n errors?: Record<string, string>;\n globalError?: string;\n}\n\n/* eslint-disable promise-function-async -- JSX components return HtmlEscapedString */\n\n/** Whether a topic checkbox renders checked (empty allowlist means all). */\nfunction topicChecked(events: string[], event: string): boolean {\n return events.length === 0 || events.includes(event);\n}\n\n/** The viewer's own preferences card (visible to every member). */\nconst MyNotificationsCard: FC<{\n project: Project;\n mySubscription: SettingsMySubscription | null;\n}> = ({ project, mySubscription }) => {\n const events = mySubscription?.events ?? [];\n return (\n <Card>\n <SectionTitle>My notifications for this project</SectionTitle>\n <Meta>Opt in to email alerts for this project. Leave everything off to stay silent.</Meta>\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/notifications/me`}\n hx-post={`/projects/${project.slug}/settings/notifications/me`}\n hx-target=\"body\"\n >\n {csrfField()}\n <CheckField\n label=\"Email me about this project\"\n name=\"enabled\"\n checked={mySubscription?.enabled ?? false}\n />\n {NOTIFICATION_TOPICS.map((topic) => (\n <CheckField\n label={topic.label}\n name=\"events\"\n value={topic.event}\n checked={topicChecked(events, topic.event)}\n />\n ))}\n <p>\n <Meta as=\"span\">No boxes ticked means all topics.</Meta>\n </p>\n <FormActions>\n <Button variant=\"primary\" type=\"submit\" icon=\"check\">\n Save my preferences\n </Button>\n </FormActions>\n </form>\n </Card>\n );\n};\n\n/** Human-readable channel target (email address or masked webhook host). */\nfunction targetOf(channel: SettingsNotificationChannel): string {\n const to = channel.config[\"to\"];\n if (typeof to === \"string\" && to) {\n return to;\n }\n return channel.hasSecret ? `${channel.provider} webhook` : \"—\";\n}\n\n/** One channel table row with the admin delete action. */\nconst ChannelRow: FC<{ project: Project; channel: SettingsNotificationChannel }> = ({\n project,\n channel,\n}) => (\n <tr>\n <td>\n <Badge tone=\"neutral\">{channel.provider}</Badge>\n </td>\n <td class=\"truncate max-w-cell\" title={targetOf(channel)}>\n {targetOf(channel)}\n </td>\n <td>\n {channel.events.length === 0 ? (\n <Meta as=\"span\">all events</Meta>\n ) : (\n <HStack>\n {channel.events.map((event) => (\n <Badge tone=\"neutral\">{event}</Badge>\n ))}\n </HStack>\n )}\n </td>\n <td>\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/notifications/${channel.id}/delete`}\n hx-post={`/projects/${project.slug}/settings/notifications/${channel.id}/delete`}\n hx-target=\"body\"\n hx-confirm=\"Remove this notification channel?\"\n >\n {csrfField()}\n <Button variant=\"ghost\" size=\"sm\" type=\"submit\">\n Delete\n </Button>\n </form>\n </td>\n </tr>\n);\n\n/** Channel list card (delete action renders for admins only). */\nconst ChannelsCard: FC<{\n project: Project;\n channels: SettingsNotificationChannel[];\n isAdmin: boolean;\n}> = ({ project, channels, isAdmin }) => (\n <Card>\n <SectionTitle>Channels</SectionTitle>\n <Meta>\n Project admins route build events to Slack, Teams, or email. Secrets are stored encrypted and\n decrypted only in memory at send time.\n </Meta>\n <Table>\n <table>\n <thead>\n <tr>\n <th>Provider</th>\n <th>Target</th>\n <th>Events</th>\n <th>\n <span class=\"visually-hidden\">Actions</span>\n </th>\n </tr>\n </thead>\n <tbody>\n {channels.map((channel) =>\n isAdmin ? (\n <ChannelRow project={project} channel={channel} />\n ) : (\n <tr key={channel.id}>\n <td>\n <Badge tone=\"neutral\">{channel.provider}</Badge>\n </td>\n <td class=\"truncate max-w-cell\" title={targetOf(channel)}>\n {targetOf(channel)}\n </td>\n <td>\n <Meta as=\"span\">\n {channel.events.length === 0 ? \"all events\" : channel.events.join(\", \")}\n </Meta>\n </td>\n <td>{null}</td>\n </tr>\n ),\n )}\n </tbody>\n </table>\n </Table>\n {channels.length === 0 ? <Meta>No channels configured.</Meta> : null}\n </Card>\n);\n\n/** Channel creation card (admins only). */\nconst CreateChannelCard: FC<{\n project: Project;\n providers: string[];\n formState?: NotificationsFormState;\n}> = ({ project, providers, formState }) => (\n <Card>\n <SectionTitle level={3}>Create channel</SectionTitle>\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/notifications`}\n hx-post={`/projects/${project.slug}/settings/notifications`}\n hx-target=\"body\"\n >\n {csrfField()}\n <SelectField\n label=\"Provider\"\n name=\"provider\"\n options={providers.map((provider) => ({ value: provider, label: provider }))}\n hint={formState?.errors?.[\"provider\"]}\n />\n <Field\n label=\"Target\"\n name=\"target\"\n type=\"text\"\n required\n placeholder=\"team@example.com or https://hooks.slack.com/…\"\n hint=\"Email address for email channels, webhook URL otherwise.\"\n error={formState?.errors?.[\"target\"]}\n />\n <Field\n label=\"Events\"\n name=\"events\"\n placeholder=\"build.reviewing, build.approved\"\n hint=\"Comma-separated. Leave blank to receive all events.\"\n />\n <SelectField\n label=\"Style\"\n name=\"style\"\n options={[\n { value: \"compact\", label: \"Compact\" },\n { value: \"verbose\", label: \"Verbose\" },\n ]}\n />\n <FormActions>\n <Button variant=\"primary\" type=\"submit\" icon=\"plus\">\n Add channel\n </Button>\n </FormActions>\n </form>\n </Card>\n);\n\n/** Notifications settings tab: channel list, create form, and my preferences. */\nexport function renderSettingsNotifications(\n project: Project,\n channels: SettingsNotificationChannel[],\n providers: string[],\n mySubscription: SettingsMySubscription | null,\n isAdmin: boolean,\n formState?: NotificationsFormState,\n): unknown {\n return (\n <div class=\"grid max-w-form\">\n {formState?.globalError ? <Alert tone=\"danger\">{formState.globalError}</Alert> : null}\n <MyNotificationsCard project={project} mySubscription={mySubscription} />\n <ChannelsCard project={project} channels={channels} isAdmin={isAdmin} />\n {isAdmin ? (\n <CreateChannelCard project={project} providers={providers} formState={formState} />\n ) : null}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,137 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { Button } from "../ui/buttons.mjs";
5
+ import { Alert, Badge, Meta } from "../ui/feedback.mjs";
6
+ import { Field, FormActions, SelectField, TextareaField } from "../ui/forms.mjs";
7
+ import { Card, SectionTitle } from "../ui/layout.mjs";
8
+ import { Table } from "../ui/table.mjs";
9
+ import { csrfField } from "../ui/csrf-field.mjs";
10
+ import { jsx, jsxs } from "hono/jsx/jsx-runtime";
11
+ //#region src/pages/settings-status.tsx
12
+ function providerOptions(providers) {
13
+ return providers.map((provider) => ({
14
+ value: provider.metadata.kind,
15
+ label: provider.metadata.name
16
+ }));
17
+ }
18
+ const StatusConfigRow = ({ config, provider, project, isAdmin }) => {
19
+ const rendered = JSON.stringify(config.config);
20
+ return /* @__PURE__ */ jsxs("tr", { children: [
21
+ /* @__PURE__ */ jsx("td", { children: provider?.metadata.name ?? config.provider }),
22
+ /* @__PURE__ */ jsx("td", {
23
+ class: "truncate max-w-cell",
24
+ title: rendered,
25
+ children: /* @__PURE__ */ jsx("code", { children: rendered })
26
+ }),
27
+ /* @__PURE__ */ jsx("td", { children: config.hasToken ? /* @__PURE__ */ jsx(Badge, {
28
+ tone: "success",
29
+ children: "configured"
30
+ }) : /* @__PURE__ */ jsx(Badge, {
31
+ tone: "danger",
32
+ children: "missing"
33
+ }) }),
34
+ /* @__PURE__ */ jsx("td", { children: new Date(config.createdAt).toLocaleDateString() }),
35
+ /* @__PURE__ */ jsx("td", { children: isAdmin ? /* @__PURE__ */ jsxs("form", {
36
+ method: "post",
37
+ action: `/projects/${project.slug}/settings/status/${config.id}/delete`,
38
+ "hx-post": `/projects/${project.slug}/settings/status/${config.id}/delete`,
39
+ "hx-target": "body",
40
+ "hx-confirm": "Remove this git status config?",
41
+ children: [csrfField(), /* @__PURE__ */ jsx(Button, {
42
+ variant: "ghost",
43
+ type: "submit",
44
+ children: "Delete"
45
+ })]
46
+ }) : null })
47
+ ] }, config.id);
48
+ };
49
+ const StatusCreateCard = ({ project, providers, formState }) => {
50
+ return /* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsx(SectionTitle, {
51
+ level: 3,
52
+ children: "Add git provider"
53
+ }), /* @__PURE__ */ jsxs("form", {
54
+ method: "post",
55
+ action: `/projects/${project.slug}/settings/status`,
56
+ "hx-post": `/projects/${project.slug}/settings/status`,
57
+ "hx-target": "body",
58
+ children: [
59
+ csrfField(),
60
+ /* @__PURE__ */ jsx(SelectField, {
61
+ label: "Provider",
62
+ name: "provider",
63
+ options: providerOptions(providers),
64
+ hint: "Integration that posts commit statuses for this project."
65
+ }),
66
+ /* @__PURE__ */ jsx(Field, {
67
+ label: "Token",
68
+ name: "token",
69
+ type: "password",
70
+ required: true,
71
+ placeholder: "ghp_…",
72
+ error: formState?.errors?.["token"],
73
+ hint: "Scoped token for the provider (e.g. a GitHub PAT with repo:status)."
74
+ }),
75
+ /* @__PURE__ */ jsx(TextareaField, {
76
+ label: "Config",
77
+ name: "config",
78
+ rows: 6,
79
+ placeholder: "{\"owner\":\"my-org\",\"repo\":\"my-repo\"}",
80
+ error: formState?.errors?.["config"],
81
+ hint: "JSON configuration for the provider. See the provider documentation for its fields."
82
+ }),
83
+ /* @__PURE__ */ jsx(FormActions, { children: /* @__PURE__ */ jsx(Button, {
84
+ variant: "primary",
85
+ type: "submit",
86
+ icon: "plus",
87
+ children: "Add git provider"
88
+ }) })
89
+ ]
90
+ })] });
91
+ };
92
+ function renderCreateSection(project, providers, formState) {
93
+ if (providers.length === 0) return /* @__PURE__ */ jsx(Meta, { children: "No git providers registered on this server." });
94
+ return /* @__PURE__ */ jsx(StatusCreateCard, {
95
+ project,
96
+ providers,
97
+ formState
98
+ });
99
+ }
100
+ /** Git status settings tab: configured providers plus the add-provider form. */
101
+ function renderSettingsStatus(project, statusConfigs, providers, isAdmin, formState) {
102
+ const byProvider = new Map(providers.map((provider) => [provider.metadata.kind, provider]));
103
+ return /* @__PURE__ */ jsxs("div", {
104
+ class: "grid max-w-form",
105
+ children: [
106
+ formState?.globalError ? /* @__PURE__ */ jsx(Alert, {
107
+ tone: "danger",
108
+ children: formState.globalError
109
+ }) : null,
110
+ /* @__PURE__ */ jsxs(Card, { children: [
111
+ /* @__PURE__ */ jsx(SectionTitle, { children: "Git status" }),
112
+ /* @__PURE__ */ jsx(Meta, { children: "Post commit statuses to a git provider so visual tests show up in your PR checks. Status is reported for each configured provider: pending while capturing, success on approval, failure on rejection or capture errors." }),
113
+ /* @__PURE__ */ jsx(Table, { children: /* @__PURE__ */ jsxs("table", { children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
114
+ /* @__PURE__ */ jsx("th", { children: "Provider" }),
115
+ /* @__PURE__ */ jsx("th", { children: "Config" }),
116
+ /* @__PURE__ */ jsx("th", { children: "Token" }),
117
+ /* @__PURE__ */ jsx("th", { children: "Created" }),
118
+ /* @__PURE__ */ jsx("th", { children: /* @__PURE__ */ jsx("span", {
119
+ class: "visually-hidden",
120
+ children: "Actions"
121
+ }) })
122
+ ] }) }), /* @__PURE__ */ jsx("tbody", { children: statusConfigs.map((config) => /* @__PURE__ */ jsx(StatusConfigRow, {
123
+ config,
124
+ provider: byProvider.get(config.provider),
125
+ project,
126
+ isAdmin
127
+ }, config.id)) })] }) }),
128
+ statusConfigs.length === 0 ? /* @__PURE__ */ jsx(Meta, { children: "No git providers configured for this project." }) : null
129
+ ] }),
130
+ isAdmin ? renderCreateSection(project, providers, formState) : null
131
+ ]
132
+ });
133
+ }
134
+ //#endregion
135
+ export { renderSettingsStatus };
136
+
137
+ //# sourceMappingURL=settings-status.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings-status.mjs","names":[],"sources":["../../src/pages/settings-status.tsx"],"sourcesContent":["import type { GitHostProvider } from \"@storyshelf/core/adapter/git-host\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport type { FC } from \"hono/jsx\";\nimport {\n Alert,\n Badge,\n Button,\n Card,\n Field,\n FormActions,\n Meta,\n SectionTitle,\n SelectField,\n Table,\n TextareaField,\n} from \"../ui/components.tsx\";\nimport { csrfField } from \"../ui/csrf-field.tsx\";\n\n/** Git status configuration row as rendered in the status settings tab. */\nexport interface SettingsStatusConfig {\n id: string;\n provider: string;\n config: Record<string, unknown>;\n hasToken: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\n/** Form state for the git status settings tab. */\nexport interface StatusFormState {\n errors?: Record<string, string>;\n globalError?: string;\n}\n\n/* eslint-disable promise-function-async -- JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\nfunction providerOptions(providers: GitHostProvider[]): { value: string; label: string }[] {\n return providers.map((provider) => ({\n value: provider.metadata.kind,\n label: provider.metadata.name,\n }));\n}\n\nconst StatusConfigRow: FC<{\n config: SettingsStatusConfig;\n provider: GitHostProvider | undefined;\n project: Project;\n isAdmin: boolean;\n}> = ({ config, provider, project, isAdmin }) => {\n const rendered = JSON.stringify(config.config);\n return (\n <tr key={config.id}>\n <td>{provider?.metadata.name ?? config.provider}</td>\n <td class=\"truncate max-w-cell\" title={rendered}>\n <code>{rendered}</code>\n </td>\n <td>\n {config.hasToken ? (\n <Badge tone=\"success\">configured</Badge>\n ) : (\n <Badge tone=\"danger\">missing</Badge>\n )}\n </td>\n <td>{new Date(config.createdAt).toLocaleDateString()}</td>\n <td>\n {isAdmin ? (\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/status/${config.id}/delete`}\n hx-post={`/projects/${project.slug}/settings/status/${config.id}/delete`}\n hx-target=\"body\"\n hx-confirm=\"Remove this git status config?\"\n >\n {csrfField()}\n <Button variant=\"ghost\" type=\"submit\">\n Delete\n </Button>\n </form>\n ) : null}\n </td>\n </tr>\n );\n};\n\nconst StatusCreateCard: FC<{\n project: Project;\n providers: GitHostProvider[];\n formState?: StatusFormState;\n}> = ({ project, providers, formState }) => {\n return (\n <Card>\n <SectionTitle level={3}>Add git provider</SectionTitle>\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/status`}\n hx-post={`/projects/${project.slug}/settings/status`}\n hx-target=\"body\"\n >\n {csrfField()}\n <SelectField\n label=\"Provider\"\n name=\"provider\"\n options={providerOptions(providers)}\n hint=\"Integration that posts commit statuses for this project.\"\n />\n <Field\n label=\"Token\"\n name=\"token\"\n type=\"password\"\n required\n placeholder=\"ghp_…\"\n error={formState?.errors?.[\"token\"]}\n hint=\"Scoped token for the provider (e.g. a GitHub PAT with repo:status).\"\n />\n <TextareaField\n label=\"Config\"\n name=\"config\"\n rows={6}\n placeholder='{\"owner\":\"my-org\",\"repo\":\"my-repo\"}'\n error={formState?.errors?.[\"config\"]}\n hint=\"JSON configuration for the provider. See the provider documentation for its fields.\"\n />\n <FormActions>\n <Button variant=\"primary\" type=\"submit\" icon=\"plus\">\n Add git provider\n </Button>\n </FormActions>\n </form>\n </Card>\n );\n};\n\nfunction renderCreateSection(\n project: Project,\n providers: GitHostProvider[],\n formState?: StatusFormState,\n): unknown {\n if (providers.length === 0) {\n return <Meta>No git providers registered on this server.</Meta>;\n }\n return <StatusCreateCard project={project} providers={providers} formState={formState} />;\n}\n\n/** Git status settings tab: configured providers plus the add-provider form. */\n// eslint-disable-next-line max-params -- matches sibling settings render functions\nexport function renderSettingsStatus(\n project: Project,\n statusConfigs: SettingsStatusConfig[],\n providers: GitHostProvider[],\n isAdmin: boolean,\n formState?: StatusFormState,\n): unknown {\n const byProvider = new Map(providers.map((provider) => [provider.metadata.kind, provider]));\n return (\n <div class=\"grid max-w-form\">\n {formState?.globalError ? <Alert tone=\"danger\">{formState.globalError}</Alert> : null}\n\n <Card>\n <SectionTitle>Git status</SectionTitle>\n <Meta>\n Post commit statuses to a git provider so visual tests show up in your PR checks. Status\n is reported for each configured provider: pending while capturing, success on approval,\n failure on rejection or capture errors.\n </Meta>\n <Table>\n <table>\n <thead>\n <tr>\n <th>Provider</th>\n <th>Config</th>\n <th>Token</th>\n <th>Created</th>\n <th>\n <span class=\"visually-hidden\">Actions</span>\n </th>\n </tr>\n </thead>\n <tbody>\n {statusConfigs.map((config) => (\n <StatusConfigRow\n key={config.id}\n config={config}\n provider={byProvider.get(config.provider)}\n project={project}\n isAdmin={isAdmin}\n />\n ))}\n </tbody>\n </table>\n </Table>\n {statusConfigs.length === 0 ? (\n <Meta>No git providers configured for this project.</Meta>\n ) : null}\n </Card>\n\n {isAdmin ? renderCreateSection(project, providers, formState) : null}\n </div>\n );\n}\n"],"mappings":"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"}