@growth-labs/cms 0.1.0

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 (1093) hide show
  1. package/README.md +165 -0
  2. package/dist/engine/activity-log.d.ts +17 -0
  3. package/dist/engine/activity-log.d.ts.map +1 -0
  4. package/dist/engine/activity-log.js +17 -0
  5. package/dist/engine/activity-log.js.map +1 -0
  6. package/dist/engine/ai-prompts.d.ts +57 -0
  7. package/dist/engine/ai-prompts.d.ts.map +1 -0
  8. package/dist/engine/ai-prompts.js +90 -0
  9. package/dist/engine/ai-prompts.js.map +1 -0
  10. package/dist/engine/ai-writeback.d.ts +36 -0
  11. package/dist/engine/ai-writeback.d.ts.map +1 -0
  12. package/dist/engine/ai-writeback.js +45 -0
  13. package/dist/engine/ai-writeback.js.map +1 -0
  14. package/dist/engine/api-keys.d.ts +76 -0
  15. package/dist/engine/api-keys.d.ts.map +1 -0
  16. package/dist/engine/api-keys.js +165 -0
  17. package/dist/engine/api-keys.js.map +1 -0
  18. package/dist/engine/content-insights.d.ts +36 -0
  19. package/dist/engine/content-insights.d.ts.map +1 -0
  20. package/dist/engine/content-insights.js +114 -0
  21. package/dist/engine/content-insights.js.map +1 -0
  22. package/dist/engine/contributors.d.ts +25 -0
  23. package/dist/engine/contributors.d.ts.map +1 -0
  24. package/dist/engine/contributors.js +59 -0
  25. package/dist/engine/contributors.js.map +1 -0
  26. package/dist/engine/cron.d.ts +15 -0
  27. package/dist/engine/cron.d.ts.map +1 -0
  28. package/dist/engine/cron.js +33 -0
  29. package/dist/engine/cron.js.map +1 -0
  30. package/dist/engine/d1.d.ts +16 -0
  31. package/dist/engine/d1.d.ts.map +1 -0
  32. package/dist/engine/d1.js +13 -0
  33. package/dist/engine/d1.js.map +1 -0
  34. package/dist/engine/foundry-dispatch.d.ts +52 -0
  35. package/dist/engine/foundry-dispatch.d.ts.map +1 -0
  36. package/dist/engine/foundry-dispatch.js +290 -0
  37. package/dist/engine/foundry-dispatch.js.map +1 -0
  38. package/dist/engine/import-parsers.d.ts +11 -0
  39. package/dist/engine/import-parsers.d.ts.map +1 -0
  40. package/dist/engine/import-parsers.js +373 -0
  41. package/dist/engine/import-parsers.js.map +1 -0
  42. package/dist/engine/index.d.ts +28 -0
  43. package/dist/engine/index.d.ts.map +1 -0
  44. package/dist/engine/index.js +57 -0
  45. package/dist/engine/index.js.map +1 -0
  46. package/dist/engine/invites.d.ts +78 -0
  47. package/dist/engine/invites.d.ts.map +1 -0
  48. package/dist/engine/invites.js +158 -0
  49. package/dist/engine/invites.js.map +1 -0
  50. package/dist/engine/members.d.ts +59 -0
  51. package/dist/engine/members.d.ts.map +1 -0
  52. package/dist/engine/members.js +124 -0
  53. package/dist/engine/members.js.map +1 -0
  54. package/dist/engine/membership-rules.d.ts +25 -0
  55. package/dist/engine/membership-rules.d.ts.map +1 -0
  56. package/dist/engine/membership-rules.js +44 -0
  57. package/dist/engine/membership-rules.js.map +1 -0
  58. package/dist/engine/og-render.d.ts +40 -0
  59. package/dist/engine/og-render.d.ts.map +1 -0
  60. package/dist/engine/og-render.js +26 -0
  61. package/dist/engine/og-render.js.map +1 -0
  62. package/dist/engine/publish-guard.d.ts +58 -0
  63. package/dist/engine/publish-guard.d.ts.map +1 -0
  64. package/dist/engine/publish-guard.js +80 -0
  65. package/dist/engine/publish-guard.js.map +1 -0
  66. package/dist/engine/publisher.d.ts +171 -0
  67. package/dist/engine/publisher.d.ts.map +1 -0
  68. package/dist/engine/publisher.js +597 -0
  69. package/dist/engine/publisher.js.map +1 -0
  70. package/dist/engine/revisions.d.ts +39 -0
  71. package/dist/engine/revisions.d.ts.map +1 -0
  72. package/dist/engine/revisions.js +203 -0
  73. package/dist/engine/revisions.js.map +1 -0
  74. package/dist/engine/sanitize.d.ts +52 -0
  75. package/dist/engine/sanitize.d.ts.map +1 -0
  76. package/dist/engine/sanitize.js +155 -0
  77. package/dist/engine/sanitize.js.map +1 -0
  78. package/dist/engine/seed-membership.d.ts +29 -0
  79. package/dist/engine/seed-membership.d.ts.map +1 -0
  80. package/dist/engine/seed-membership.js +65 -0
  81. package/dist/engine/seed-membership.js.map +1 -0
  82. package/dist/engine/seo.d.ts +20 -0
  83. package/dist/engine/seo.d.ts.map +1 -0
  84. package/dist/engine/seo.js +50 -0
  85. package/dist/engine/seo.js.map +1 -0
  86. package/dist/engine/slug-redirects.d.ts +8 -0
  87. package/dist/engine/slug-redirects.d.ts.map +1 -0
  88. package/dist/engine/slug-redirects.js +26 -0
  89. package/dist/engine/slug-redirects.js.map +1 -0
  90. package/dist/engine/slug.d.ts +6 -0
  91. package/dist/engine/slug.d.ts.map +1 -0
  92. package/dist/engine/slug.js +28 -0
  93. package/dist/engine/slug.js.map +1 -0
  94. package/dist/engine/soft-delete.d.ts +8 -0
  95. package/dist/engine/soft-delete.d.ts.map +1 -0
  96. package/dist/engine/soft-delete.js +28 -0
  97. package/dist/engine/soft-delete.js.map +1 -0
  98. package/dist/engine/tags.d.ts +14 -0
  99. package/dist/engine/tags.d.ts.map +1 -0
  100. package/dist/engine/tags.js +79 -0
  101. package/dist/engine/tags.js.map +1 -0
  102. package/dist/engine/topics.d.ts +10 -0
  103. package/dist/engine/topics.d.ts.map +1 -0
  104. package/dist/engine/topics.js +140 -0
  105. package/dist/engine/topics.js.map +1 -0
  106. package/dist/engine/url-guard.d.ts +12 -0
  107. package/dist/engine/url-guard.d.ts.map +1 -0
  108. package/dist/engine/url-guard.js +129 -0
  109. package/dist/engine/url-guard.js.map +1 -0
  110. package/dist/engine/validator/checks/bare-url-not-autolinked.d.ts +20 -0
  111. package/dist/engine/validator/checks/bare-url-not-autolinked.d.ts.map +1 -0
  112. package/dist/engine/validator/checks/bare-url-not-autolinked.js +54 -0
  113. package/dist/engine/validator/checks/bare-url-not-autolinked.js.map +1 -0
  114. package/dist/engine/validator/checks/broken-footnote-label.d.ts +16 -0
  115. package/dist/engine/validator/checks/broken-footnote-label.d.ts.map +1 -0
  116. package/dist/engine/validator/checks/broken-footnote-label.js +17 -0
  117. package/dist/engine/validator/checks/broken-footnote-label.js.map +1 -0
  118. package/dist/engine/validator/checks/double-encoded-entities.d.ts +18 -0
  119. package/dist/engine/validator/checks/double-encoded-entities.d.ts.map +1 -0
  120. package/dist/engine/validator/checks/double-encoded-entities.js +23 -0
  121. package/dist/engine/validator/checks/double-encoded-entities.js.map +1 -0
  122. package/dist/engine/validator/checks/empty-alt-text.d.ts +14 -0
  123. package/dist/engine/validator/checks/empty-alt-text.d.ts.map +1 -0
  124. package/dist/engine/validator/checks/empty-alt-text.js +23 -0
  125. package/dist/engine/validator/checks/empty-alt-text.js.map +1 -0
  126. package/dist/engine/validator/checks/heading-hierarchy-skip.d.ts +11 -0
  127. package/dist/engine/validator/checks/heading-hierarchy-skip.d.ts.map +1 -0
  128. package/dist/engine/validator/checks/heading-hierarchy-skip.js +20 -0
  129. package/dist/engine/validator/checks/heading-hierarchy-skip.js.map +1 -0
  130. package/dist/engine/validator/checks/html-comment-leak.d.ts +20 -0
  131. package/dist/engine/validator/checks/html-comment-leak.d.ts.map +1 -0
  132. package/dist/engine/validator/checks/html-comment-leak.js +30 -0
  133. package/dist/engine/validator/checks/html-comment-leak.js.map +1 -0
  134. package/dist/engine/validator/checks/iframe-missing-dims-and-wrapper.d.ts +12 -0
  135. package/dist/engine/validator/checks/iframe-missing-dims-and-wrapper.d.ts.map +1 -0
  136. package/dist/engine/validator/checks/iframe-missing-dims-and-wrapper.js +17 -0
  137. package/dist/engine/validator/checks/iframe-missing-dims-and-wrapper.js.map +1 -0
  138. package/dist/engine/validator/checks/invisible-control-chars.d.ts +24 -0
  139. package/dist/engine/validator/checks/invisible-control-chars.d.ts.map +1 -0
  140. package/dist/engine/validator/checks/invisible-control-chars.js +30 -0
  141. package/dist/engine/validator/checks/invisible-control-chars.js.map +1 -0
  142. package/dist/engine/validator/checks/paywall-marker-leak.d.ts +17 -0
  143. package/dist/engine/validator/checks/paywall-marker-leak.d.ts.map +1 -0
  144. package/dist/engine/validator/checks/paywall-marker-leak.js +22 -0
  145. package/dist/engine/validator/checks/paywall-marker-leak.js.map +1 -0
  146. package/dist/engine/validator/checks/raw-block-html.d.ts +28 -0
  147. package/dist/engine/validator/checks/raw-block-html.d.ts.map +1 -0
  148. package/dist/engine/validator/checks/raw-block-html.js +38 -0
  149. package/dist/engine/validator/checks/raw-block-html.js.map +1 -0
  150. package/dist/engine/validator/checks/stale-body-html.d.ts +28 -0
  151. package/dist/engine/validator/checks/stale-body-html.d.ts.map +1 -0
  152. package/dist/engine/validator/checks/stale-body-html.js +15 -0
  153. package/dist/engine/validator/checks/stale-body-html.js.map +1 -0
  154. package/dist/engine/validator/checks/unresolved-footnote-anchor.d.ts +11 -0
  155. package/dist/engine/validator/checks/unresolved-footnote-anchor.d.ts.map +1 -0
  156. package/dist/engine/validator/checks/unresolved-footnote-anchor.js +48 -0
  157. package/dist/engine/validator/checks/unresolved-footnote-anchor.js.map +1 -0
  158. package/dist/engine/validator/checks/word-gdocs-paste-artifacts.d.ts +23 -0
  159. package/dist/engine/validator/checks/word-gdocs-paste-artifacts.d.ts.map +1 -0
  160. package/dist/engine/validator/checks/word-gdocs-paste-artifacts.js +47 -0
  161. package/dist/engine/validator/checks/word-gdocs-paste-artifacts.js.map +1 -0
  162. package/dist/engine/validator/index.d.ts +75 -0
  163. package/dist/engine/validator/index.d.ts.map +1 -0
  164. package/dist/engine/validator/index.js +313 -0
  165. package/dist/engine/validator/index.js.map +1 -0
  166. package/dist/engine/validator/scan.d.ts +28 -0
  167. package/dist/engine/validator/scan.d.ts.map +1 -0
  168. package/dist/engine/validator/scan.js +97 -0
  169. package/dist/engine/validator/scan.js.map +1 -0
  170. package/dist/engine/validator/types.d.ts +50 -0
  171. package/dist/engine/validator/types.d.ts.map +1 -0
  172. package/dist/engine/validator/types.js +51 -0
  173. package/dist/engine/validator/types.js.map +1 -0
  174. package/dist/engine/webhook-signer.d.ts +39 -0
  175. package/dist/engine/webhook-signer.d.ts.map +1 -0
  176. package/dist/engine/webhook-signer.js +117 -0
  177. package/dist/engine/webhook-signer.js.map +1 -0
  178. package/dist/engine/webhooks.d.ts +75 -0
  179. package/dist/engine/webhooks.d.ts.map +1 -0
  180. package/dist/engine/webhooks.js +139 -0
  181. package/dist/engine/webhooks.js.map +1 -0
  182. package/dist/index.d.ts +5 -0
  183. package/dist/index.d.ts.map +1 -0
  184. package/dist/index.js +40 -0
  185. package/dist/index.js.map +1 -0
  186. package/dist/integration/index.d.ts +6 -0
  187. package/dist/integration/index.d.ts.map +1 -0
  188. package/dist/integration/index.js +294 -0
  189. package/dist/integration/index.js.map +1 -0
  190. package/dist/integration/options.d.ts +105 -0
  191. package/dist/integration/options.d.ts.map +1 -0
  192. package/dist/integration/options.js +25 -0
  193. package/dist/integration/options.js.map +1 -0
  194. package/dist/integration/vite-plugin.d.ts +4 -0
  195. package/dist/integration/vite-plugin.d.ts.map +1 -0
  196. package/dist/integration/vite-plugin.js +37 -0
  197. package/dist/integration/vite-plugin.js.map +1 -0
  198. package/dist/providers/index.d.ts +3 -0
  199. package/dist/providers/index.d.ts.map +1 -0
  200. package/dist/providers/index.js +3 -0
  201. package/dist/providers/index.js.map +1 -0
  202. package/dist/providers/null.d.ts +9 -0
  203. package/dist/providers/null.d.ts.map +1 -0
  204. package/dist/providers/null.js +144 -0
  205. package/dist/providers/null.js.map +1 -0
  206. package/dist/providers/types.d.ts +277 -0
  207. package/dist/providers/types.d.ts.map +1 -0
  208. package/dist/providers/types.js +2 -0
  209. package/dist/providers/types.js.map +1 -0
  210. package/dist/routes/ai.d.ts +25 -0
  211. package/dist/routes/ai.d.ts.map +1 -0
  212. package/dist/routes/ai.js +381 -0
  213. package/dist/routes/ai.js.map +1 -0
  214. package/dist/routes/analytics.d.ts +15 -0
  215. package/dist/routes/analytics.d.ts.map +1 -0
  216. package/dist/routes/analytics.js +61 -0
  217. package/dist/routes/analytics.js.map +1 -0
  218. package/dist/routes/api-keys.d.ts +13 -0
  219. package/dist/routes/api-keys.d.ts.map +1 -0
  220. package/dist/routes/api-keys.js +109 -0
  221. package/dist/routes/api-keys.js.map +1 -0
  222. package/dist/routes/authors.d.ts +19 -0
  223. package/dist/routes/authors.d.ts.map +1 -0
  224. package/dist/routes/authors.js +202 -0
  225. package/dist/routes/authors.js.map +1 -0
  226. package/dist/routes/authz-matrix.d.ts +78 -0
  227. package/dist/routes/authz-matrix.d.ts.map +1 -0
  228. package/dist/routes/authz-matrix.js +170 -0
  229. package/dist/routes/authz-matrix.js.map +1 -0
  230. package/dist/routes/calendar.d.ts +19 -0
  231. package/dist/routes/calendar.d.ts.map +1 -0
  232. package/dist/routes/calendar.js +89 -0
  233. package/dist/routes/calendar.js.map +1 -0
  234. package/dist/routes/config.d.ts +70 -0
  235. package/dist/routes/config.d.ts.map +1 -0
  236. package/dist/routes/config.js +23 -0
  237. package/dist/routes/config.js.map +1 -0
  238. package/dist/routes/content-insights.d.ts +18 -0
  239. package/dist/routes/content-insights.d.ts.map +1 -0
  240. package/dist/routes/content-insights.js +137 -0
  241. package/dist/routes/content-insights.js.map +1 -0
  242. package/dist/routes/content.d.ts +145 -0
  243. package/dist/routes/content.d.ts.map +1 -0
  244. package/dist/routes/content.js +1374 -0
  245. package/dist/routes/content.js.map +1 -0
  246. package/dist/routes/context.d.ts +104 -0
  247. package/dist/routes/context.d.ts.map +1 -0
  248. package/dist/routes/context.js +26 -0
  249. package/dist/routes/context.js.map +1 -0
  250. package/dist/routes/cron.d.ts +8 -0
  251. package/dist/routes/cron.d.ts.map +1 -0
  252. package/dist/routes/cron.js +20 -0
  253. package/dist/routes/cron.js.map +1 -0
  254. package/dist/routes/dashboard.d.ts +12 -0
  255. package/dist/routes/dashboard.d.ts.map +1 -0
  256. package/dist/routes/dashboard.js +113 -0
  257. package/dist/routes/dashboard.js.map +1 -0
  258. package/dist/routes/imports.d.ts +10 -0
  259. package/dist/routes/imports.d.ts.map +1 -0
  260. package/dist/routes/imports.js +149 -0
  261. package/dist/routes/imports.js.map +1 -0
  262. package/dist/routes/index.d.ts +75 -0
  263. package/dist/routes/index.d.ts.map +1 -0
  264. package/dist/routes/index.js +141 -0
  265. package/dist/routes/index.js.map +1 -0
  266. package/dist/routes/media-lib.d.ts +75 -0
  267. package/dist/routes/media-lib.d.ts.map +1 -0
  268. package/dist/routes/media-lib.js +305 -0
  269. package/dist/routes/media-lib.js.map +1 -0
  270. package/dist/routes/media.d.ts +32 -0
  271. package/dist/routes/media.d.ts.map +1 -0
  272. package/dist/routes/media.js +756 -0
  273. package/dist/routes/media.js.map +1 -0
  274. package/dist/routes/preview.d.ts +19 -0
  275. package/dist/routes/preview.d.ts.map +1 -0
  276. package/dist/routes/preview.js +150 -0
  277. package/dist/routes/preview.js.map +1 -0
  278. package/dist/routes/rbac-invites.d.ts +31 -0
  279. package/dist/routes/rbac-invites.d.ts.map +1 -0
  280. package/dist/routes/rbac-invites.js +174 -0
  281. package/dist/routes/rbac-invites.js.map +1 -0
  282. package/dist/routes/rbac.d.ts +12 -0
  283. package/dist/routes/rbac.d.ts.map +1 -0
  284. package/dist/routes/rbac.js +126 -0
  285. package/dist/routes/rbac.js.map +1 -0
  286. package/dist/routes/shell.d.ts +22 -0
  287. package/dist/routes/shell.d.ts.map +1 -0
  288. package/dist/routes/shell.js +123 -0
  289. package/dist/routes/shell.js.map +1 -0
  290. package/dist/routes/subscriptions.d.ts +21 -0
  291. package/dist/routes/subscriptions.d.ts.map +1 -0
  292. package/dist/routes/subscriptions.js +127 -0
  293. package/dist/routes/subscriptions.js.map +1 -0
  294. package/dist/routes/tags.d.ts +23 -0
  295. package/dist/routes/tags.d.ts.map +1 -0
  296. package/dist/routes/tags.js +68 -0
  297. package/dist/routes/tags.js.map +1 -0
  298. package/dist/routes/topics.d.ts +12 -0
  299. package/dist/routes/topics.d.ts.map +1 -0
  300. package/dist/routes/topics.js +49 -0
  301. package/dist/routes/topics.js.map +1 -0
  302. package/dist/routes/webhooks.d.ts +31 -0
  303. package/dist/routes/webhooks.d.ts.map +1 -0
  304. package/dist/routes/webhooks.js +173 -0
  305. package/dist/routes/webhooks.js.map +1 -0
  306. package/dist/schema/index.d.ts +4 -0
  307. package/dist/schema/index.d.ts.map +1 -0
  308. package/dist/schema/index.js +6 -0
  309. package/dist/schema/index.js.map +1 -0
  310. package/dist/schema/insights-ingest.d.ts +959 -0
  311. package/dist/schema/insights-ingest.d.ts.map +1 -0
  312. package/dist/schema/insights-ingest.js +112 -0
  313. package/dist/schema/insights-ingest.js.map +1 -0
  314. package/dist/schema/migrations.d.ts +63 -0
  315. package/dist/schema/migrations.d.ts.map +1 -0
  316. package/dist/schema/migrations.js +589 -0
  317. package/dist/schema/migrations.js.map +1 -0
  318. package/dist/schema/tables.d.ts +11 -0
  319. package/dist/schema/tables.d.ts.map +1 -0
  320. package/dist/schema/tables.js +56 -0
  321. package/dist/schema/tables.js.map +1 -0
  322. package/dist/schema/types.d.ts +476 -0
  323. package/dist/schema/types.d.ts.map +1 -0
  324. package/dist/schema/types.js +37 -0
  325. package/dist/schema/types.js.map +1 -0
  326. package/dist/ui/api/_authz.d.ts +6 -0
  327. package/dist/ui/api/_authz.d.ts.map +1 -0
  328. package/dist/ui/api/_authz.js +74 -0
  329. package/dist/ui/api/_authz.js.map +1 -0
  330. package/dist/ui/api/_content-config.d.ts +22 -0
  331. package/dist/ui/api/_content-config.d.ts.map +1 -0
  332. package/dist/ui/api/_content-config.js +50 -0
  333. package/dist/ui/api/_content-config.js.map +1 -0
  334. package/dist/ui/api/activity.d.ts +3 -0
  335. package/dist/ui/api/activity.d.ts.map +1 -0
  336. package/dist/ui/api/activity.js +28 -0
  337. package/dist/ui/api/activity.js.map +1 -0
  338. package/dist/ui/api/analytics.d.ts +3 -0
  339. package/dist/ui/api/analytics.d.ts.map +1 -0
  340. package/dist/ui/api/analytics.js +36 -0
  341. package/dist/ui/api/analytics.js.map +1 -0
  342. package/dist/ui/api/authors/[id].d.ts +4 -0
  343. package/dist/ui/api/authors/[id].d.ts.map +1 -0
  344. package/dist/ui/api/authors/[id].js +17 -0
  345. package/dist/ui/api/authors/[id].js.map +1 -0
  346. package/dist/ui/api/authors.d.ts +4 -0
  347. package/dist/ui/api/authors.d.ts.map +1 -0
  348. package/dist/ui/api/authors.js +12 -0
  349. package/dist/ui/api/authors.js.map +1 -0
  350. package/dist/ui/api/calendar.d.ts +3 -0
  351. package/dist/ui/api/calendar.d.ts.map +1 -0
  352. package/dist/ui/api/calendar.js +16 -0
  353. package/dist/ui/api/calendar.js.map +1 -0
  354. package/dist/ui/api/content/[id]/ai/headlines.d.ts +3 -0
  355. package/dist/ui/api/content/[id]/ai/headlines.d.ts.map +1 -0
  356. package/dist/ui/api/content/[id]/ai/headlines.js +7 -0
  357. package/dist/ui/api/content/[id]/ai/headlines.js.map +1 -0
  358. package/dist/ui/api/content/[id]/ai/meta-description.d.ts +3 -0
  359. package/dist/ui/api/content/[id]/ai/meta-description.d.ts.map +1 -0
  360. package/dist/ui/api/content/[id]/ai/meta-description.js +6 -0
  361. package/dist/ui/api/content/[id]/ai/meta-description.js.map +1 -0
  362. package/dist/ui/api/content/[id]/ai/og-image.d.ts +3 -0
  363. package/dist/ui/api/content/[id]/ai/og-image.d.ts.map +1 -0
  364. package/dist/ui/api/content/[id]/ai/og-image.js +7 -0
  365. package/dist/ui/api/content/[id]/ai/og-image.js.map +1 -0
  366. package/dist/ui/api/content/[id]/ai/proofread.d.ts +3 -0
  367. package/dist/ui/api/content/[id]/ai/proofread.d.ts.map +1 -0
  368. package/dist/ui/api/content/[id]/ai/proofread.js +7 -0
  369. package/dist/ui/api/content/[id]/ai/proofread.js.map +1 -0
  370. package/dist/ui/api/content/[id]/ai/takeaways.d.ts +3 -0
  371. package/dist/ui/api/content/[id]/ai/takeaways.d.ts.map +1 -0
  372. package/dist/ui/api/content/[id]/ai/takeaways.js +6 -0
  373. package/dist/ui/api/content/[id]/ai/takeaways.js.map +1 -0
  374. package/dist/ui/api/content/[id]/contributors.d.ts +4 -0
  375. package/dist/ui/api/content/[id]/contributors.d.ts.map +1 -0
  376. package/dist/ui/api/content/[id]/contributors.js +8 -0
  377. package/dist/ui/api/content/[id]/contributors.js.map +1 -0
  378. package/dist/ui/api/content/[id]/preview-token.d.ts +3 -0
  379. package/dist/ui/api/content/[id]/preview-token.d.ts.map +1 -0
  380. package/dist/ui/api/content/[id]/preview-token.js +16 -0
  381. package/dist/ui/api/content/[id]/preview-token.js.map +1 -0
  382. package/dist/ui/api/content/[id]/revisions/[rev]/restore.d.ts +3 -0
  383. package/dist/ui/api/content/[id]/revisions/[rev]/restore.d.ts.map +1 -0
  384. package/dist/ui/api/content/[id]/revisions/[rev]/restore.js +7 -0
  385. package/dist/ui/api/content/[id]/revisions/[rev]/restore.js.map +1 -0
  386. package/dist/ui/api/content/[id]/revisions/[rev].d.ts +3 -0
  387. package/dist/ui/api/content/[id]/revisions/[rev].d.ts.map +1 -0
  388. package/dist/ui/api/content/[id]/revisions/[rev].js +6 -0
  389. package/dist/ui/api/content/[id]/revisions/[rev].js.map +1 -0
  390. package/dist/ui/api/content/[id]/revisions.d.ts +3 -0
  391. package/dist/ui/api/content/[id]/revisions.d.ts.map +1 -0
  392. package/dist/ui/api/content/[id]/revisions.js +6 -0
  393. package/dist/ui/api/content/[id]/revisions.js.map +1 -0
  394. package/dist/ui/api/content/[id]/seo-score.d.ts +3 -0
  395. package/dist/ui/api/content/[id]/seo-score.d.ts.map +1 -0
  396. package/dist/ui/api/content/[id]/seo-score.js +7 -0
  397. package/dist/ui/api/content/[id]/seo-score.js.map +1 -0
  398. package/dist/ui/api/content/[id].d.ts +5 -0
  399. package/dist/ui/api/content/[id].d.ts.map +1 -0
  400. package/dist/ui/api/content/[id].js +17 -0
  401. package/dist/ui/api/content/[id].js.map +1 -0
  402. package/dist/ui/api/content/bulk.d.ts +3 -0
  403. package/dist/ui/api/content/bulk.d.ts.map +1 -0
  404. package/dist/ui/api/content/bulk.js +7 -0
  405. package/dist/ui/api/content/bulk.js.map +1 -0
  406. package/dist/ui/api/content/counts.d.ts +3 -0
  407. package/dist/ui/api/content/counts.d.ts.map +1 -0
  408. package/dist/ui/api/content/counts.js +8 -0
  409. package/dist/ui/api/content/counts.js.map +1 -0
  410. package/dist/ui/api/content/foundry-callback.d.ts +3 -0
  411. package/dist/ui/api/content/foundry-callback.d.ts.map +1 -0
  412. package/dist/ui/api/content/foundry-callback.js +8 -0
  413. package/dist/ui/api/content/foundry-callback.js.map +1 -0
  414. package/dist/ui/api/content/import/confirm.d.ts +3 -0
  415. package/dist/ui/api/content/import/confirm.d.ts.map +1 -0
  416. package/dist/ui/api/content/import/confirm.js +11 -0
  417. package/dist/ui/api/content/import/confirm.js.map +1 -0
  418. package/dist/ui/api/content/import/parse.d.ts +3 -0
  419. package/dist/ui/api/content/import/parse.d.ts.map +1 -0
  420. package/dist/ui/api/content/import/parse.js +11 -0
  421. package/dist/ui/api/content/import/parse.js.map +1 -0
  422. package/dist/ui/api/content/insights-ingest.d.ts +3 -0
  423. package/dist/ui/api/content/insights-ingest.d.ts.map +1 -0
  424. package/dist/ui/api/content/insights-ingest.js +15 -0
  425. package/dist/ui/api/content/insights-ingest.js.map +1 -0
  426. package/dist/ui/api/content-insights/dismiss.d.ts +3 -0
  427. package/dist/ui/api/content-insights/dismiss.d.ts.map +1 -0
  428. package/dist/ui/api/content-insights/dismiss.js +16 -0
  429. package/dist/ui/api/content-insights/dismiss.js.map +1 -0
  430. package/dist/ui/api/content-insights/index.d.ts +3 -0
  431. package/dist/ui/api/content-insights/index.d.ts.map +1 -0
  432. package/dist/ui/api/content-insights/index.js +12 -0
  433. package/dist/ui/api/content-insights/index.js.map +1 -0
  434. package/dist/ui/api/content-insights/undismiss.d.ts +3 -0
  435. package/dist/ui/api/content-insights/undismiss.d.ts.map +1 -0
  436. package/dist/ui/api/content-insights/undismiss.js +16 -0
  437. package/dist/ui/api/content-insights/undismiss.js.map +1 -0
  438. package/dist/ui/api/content.d.ts +4 -0
  439. package/dist/ui/api/content.d.ts.map +1 -0
  440. package/dist/ui/api/content.js +16 -0
  441. package/dist/ui/api/content.js.map +1 -0
  442. package/dist/ui/api/dashboard.d.ts +3 -0
  443. package/dist/ui/api/dashboard.d.ts.map +1 -0
  444. package/dist/ui/api/dashboard.js +22 -0
  445. package/dist/ui/api/dashboard.js.map +1 -0
  446. package/dist/ui/api/me.d.ts +3 -0
  447. package/dist/ui/api/me.d.ts.map +1 -0
  448. package/dist/ui/api/me.js +18 -0
  449. package/dist/ui/api/me.js.map +1 -0
  450. package/dist/ui/api/media/[id].d.ts +3 -0
  451. package/dist/ui/api/media/[id].d.ts.map +1 -0
  452. package/dist/ui/api/media/[id].js +17 -0
  453. package/dist/ui/api/media/[id].js.map +1 -0
  454. package/dist/ui/api/media/images.d.ts +3 -0
  455. package/dist/ui/api/media/images.d.ts.map +1 -0
  456. package/dist/ui/api/media/images.js +6 -0
  457. package/dist/ui/api/media/images.js.map +1 -0
  458. package/dist/ui/api/media/library/[id].d.ts +3 -0
  459. package/dist/ui/api/media/library/[id].d.ts.map +1 -0
  460. package/dist/ui/api/media/library/[id].js +6 -0
  461. package/dist/ui/api/media/library/[id].js.map +1 -0
  462. package/dist/ui/api/media/library.d.ts +3 -0
  463. package/dist/ui/api/media/library.d.ts.map +1 -0
  464. package/dist/ui/api/media/library.js +17 -0
  465. package/dist/ui/api/media/library.js.map +1 -0
  466. package/dist/ui/api/media/podcast/abort.d.ts +3 -0
  467. package/dist/ui/api/media/podcast/abort.d.ts.map +1 -0
  468. package/dist/ui/api/media/podcast/abort.js +4 -0
  469. package/dist/ui/api/media/podcast/abort.js.map +1 -0
  470. package/dist/ui/api/media/podcast/complete.d.ts +3 -0
  471. package/dist/ui/api/media/podcast/complete.d.ts.map +1 -0
  472. package/dist/ui/api/media/podcast/complete.js +4 -0
  473. package/dist/ui/api/media/podcast/complete.js.map +1 -0
  474. package/dist/ui/api/media/podcast/init.d.ts +3 -0
  475. package/dist/ui/api/media/podcast/init.d.ts.map +1 -0
  476. package/dist/ui/api/media/podcast/init.js +4 -0
  477. package/dist/ui/api/media/podcast/init.js.map +1 -0
  478. package/dist/ui/api/media/podcast/part.d.ts +3 -0
  479. package/dist/ui/api/media/podcast/part.d.ts.map +1 -0
  480. package/dist/ui/api/media/podcast/part.js +4 -0
  481. package/dist/ui/api/media/podcast/part.js.map +1 -0
  482. package/dist/ui/api/media/podcast.d.ts +3 -0
  483. package/dist/ui/api/media/podcast.d.ts.map +1 -0
  484. package/dist/ui/api/media/podcast.js +6 -0
  485. package/dist/ui/api/media/podcast.js.map +1 -0
  486. package/dist/ui/api/media/videos/abort.d.ts +3 -0
  487. package/dist/ui/api/media/videos/abort.d.ts.map +1 -0
  488. package/dist/ui/api/media/videos/abort.js +6 -0
  489. package/dist/ui/api/media/videos/abort.js.map +1 -0
  490. package/dist/ui/api/media/videos/complete.d.ts +3 -0
  491. package/dist/ui/api/media/videos/complete.d.ts.map +1 -0
  492. package/dist/ui/api/media/videos/complete.js +6 -0
  493. package/dist/ui/api/media/videos/complete.js.map +1 -0
  494. package/dist/ui/api/media/videos/init.d.ts +3 -0
  495. package/dist/ui/api/media/videos/init.d.ts.map +1 -0
  496. package/dist/ui/api/media/videos/init.js +6 -0
  497. package/dist/ui/api/media/videos/init.js.map +1 -0
  498. package/dist/ui/api/media/videos/part.d.ts +3 -0
  499. package/dist/ui/api/media/videos/part.d.ts.map +1 -0
  500. package/dist/ui/api/media/videos/part.js +6 -0
  501. package/dist/ui/api/media/videos/part.js.map +1 -0
  502. package/dist/ui/api/notifications.d.ts +4 -0
  503. package/dist/ui/api/notifications.d.ts.map +1 -0
  504. package/dist/ui/api/notifications.js +20 -0
  505. package/dist/ui/api/notifications.js.map +1 -0
  506. package/dist/ui/api/search.d.ts +3 -0
  507. package/dist/ui/api/search.d.ts.map +1 -0
  508. package/dist/ui/api/search.js +18 -0
  509. package/dist/ui/api/search.js.map +1 -0
  510. package/dist/ui/api/settings/api-keys/[id].d.ts +3 -0
  511. package/dist/ui/api/settings/api-keys/[id].d.ts.map +1 -0
  512. package/dist/ui/api/settings/api-keys/[id].js +22 -0
  513. package/dist/ui/api/settings/api-keys/[id].js.map +1 -0
  514. package/dist/ui/api/settings/api-keys.d.ts +4 -0
  515. package/dist/ui/api/settings/api-keys.d.ts.map +1 -0
  516. package/dist/ui/api/settings/api-keys.js +19 -0
  517. package/dist/ui/api/settings/api-keys.js.map +1 -0
  518. package/dist/ui/api/settings/domains.d.ts +3 -0
  519. package/dist/ui/api/settings/domains.d.ts.map +1 -0
  520. package/dist/ui/api/settings/domains.js +32 -0
  521. package/dist/ui/api/settings/domains.js.map +1 -0
  522. package/dist/ui/api/settings/integrations.d.ts +3 -0
  523. package/dist/ui/api/settings/integrations.d.ts.map +1 -0
  524. package/dist/ui/api/settings/integrations.js +32 -0
  525. package/dist/ui/api/settings/integrations.js.map +1 -0
  526. package/dist/ui/api/settings/members/[userId].d.ts +4 -0
  527. package/dist/ui/api/settings/members/[userId].d.ts.map +1 -0
  528. package/dist/ui/api/settings/members/[userId].js +26 -0
  529. package/dist/ui/api/settings/members/[userId].js.map +1 -0
  530. package/dist/ui/api/settings/members/invite.d.ts +3 -0
  531. package/dist/ui/api/settings/members/invite.d.ts.map +1 -0
  532. package/dist/ui/api/settings/members/invite.js +21 -0
  533. package/dist/ui/api/settings/members/invite.js.map +1 -0
  534. package/dist/ui/api/settings/members.d.ts +3 -0
  535. package/dist/ui/api/settings/members.d.ts.map +1 -0
  536. package/dist/ui/api/settings/members.js +17 -0
  537. package/dist/ui/api/settings/members.js.map +1 -0
  538. package/dist/ui/api/settings/webhooks/[id]/test.d.ts +3 -0
  539. package/dist/ui/api/settings/webhooks/[id]/test.d.ts.map +1 -0
  540. package/dist/ui/api/settings/webhooks/[id]/test.js +24 -0
  541. package/dist/ui/api/settings/webhooks/[id]/test.js.map +1 -0
  542. package/dist/ui/api/settings/webhooks/[id].d.ts +3 -0
  543. package/dist/ui/api/settings/webhooks/[id].d.ts.map +1 -0
  544. package/dist/ui/api/settings/webhooks/[id].js +23 -0
  545. package/dist/ui/api/settings/webhooks/[id].js.map +1 -0
  546. package/dist/ui/api/settings/webhooks.d.ts +4 -0
  547. package/dist/ui/api/settings/webhooks.d.ts.map +1 -0
  548. package/dist/ui/api/settings/webhooks.js +21 -0
  549. package/dist/ui/api/settings/webhooks.js.map +1 -0
  550. package/dist/ui/api/subscriptions.d.ts +4 -0
  551. package/dist/ui/api/subscriptions.d.ts.map +1 -0
  552. package/dist/ui/api/subscriptions.js +47 -0
  553. package/dist/ui/api/subscriptions.js.map +1 -0
  554. package/dist/ui/api/tags/[id].d.ts +4 -0
  555. package/dist/ui/api/tags/[id].d.ts.map +1 -0
  556. package/dist/ui/api/tags/[id].js +18 -0
  557. package/dist/ui/api/tags/[id].js.map +1 -0
  558. package/dist/ui/api/tags/index.d.ts +4 -0
  559. package/dist/ui/api/tags/index.d.ts.map +1 -0
  560. package/dist/ui/api/tags/index.js +13 -0
  561. package/dist/ui/api/tags/index.js.map +1 -0
  562. package/dist/ui/api/topics/[id].d.ts +3 -0
  563. package/dist/ui/api/topics/[id].d.ts.map +1 -0
  564. package/dist/ui/api/topics/[id].js +15 -0
  565. package/dist/ui/api/topics/[id].js.map +1 -0
  566. package/dist/ui/api/topics/index.d.ts +4 -0
  567. package/dist/ui/api/topics/index.d.ts.map +1 -0
  568. package/dist/ui/api/topics/index.js +12 -0
  569. package/dist/ui/api/topics/index.js.map +1 -0
  570. package/dist/ui/api/workspace-settings.d.ts +4 -0
  571. package/dist/ui/api/workspace-settings.d.ts.map +1 -0
  572. package/dist/ui/api/workspace-settings.js +20 -0
  573. package/dist/ui/api/workspace-settings.js.map +1 -0
  574. package/dist/ui/client/boot-state.d.ts +15 -0
  575. package/dist/ui/client/boot-state.d.ts.map +1 -0
  576. package/dist/ui/client/boot-state.js +36 -0
  577. package/dist/ui/client/boot-state.js.map +1 -0
  578. package/dist/ui/client/mount.d.ts +3 -0
  579. package/dist/ui/client/mount.d.ts.map +1 -0
  580. package/dist/ui/client/mount.js +37 -0
  581. package/dist/ui/client/mount.js.map +1 -0
  582. package/dist/ui/commands.d.ts +23 -0
  583. package/dist/ui/commands.d.ts.map +1 -0
  584. package/dist/ui/commands.js +48 -0
  585. package/dist/ui/commands.js.map +1 -0
  586. package/dist/ui/components/CmsApp.d.ts +16 -0
  587. package/dist/ui/components/CmsApp.d.ts.map +1 -0
  588. package/dist/ui/components/CmsApp.js +74 -0
  589. package/dist/ui/components/CmsApp.js.map +1 -0
  590. package/dist/ui/components/CommandPalette.d.ts +7 -0
  591. package/dist/ui/components/CommandPalette.d.ts.map +1 -0
  592. package/dist/ui/components/CommandPalette.js +61 -0
  593. package/dist/ui/components/CommandPalette.js.map +1 -0
  594. package/dist/ui/components/NoAccessScreen.d.ts +2 -0
  595. package/dist/ui/components/NoAccessScreen.d.ts.map +1 -0
  596. package/dist/ui/components/NoAccessScreen.js +42 -0
  597. package/dist/ui/components/NoAccessScreen.js.map +1 -0
  598. package/dist/ui/components/ShareModal.d.ts +27 -0
  599. package/dist/ui/components/ShareModal.d.ts.map +1 -0
  600. package/dist/ui/components/ShareModal.js +208 -0
  601. package/dist/ui/components/ShareModal.js.map +1 -0
  602. package/dist/ui/components/SharePickers.d.ts +39 -0
  603. package/dist/ui/components/SharePickers.d.ts.map +1 -0
  604. package/dist/ui/components/SharePickers.js +352 -0
  605. package/dist/ui/components/SharePickers.js.map +1 -0
  606. package/dist/ui/components/ShareStatsPanel.d.ts +22 -0
  607. package/dist/ui/components/ShareStatsPanel.d.ts.map +1 -0
  608. package/dist/ui/components/ShareStatsPanel.js +317 -0
  609. package/dist/ui/components/ShareStatsPanel.js.map +1 -0
  610. package/dist/ui/components/Sidebar.d.ts +7 -0
  611. package/dist/ui/components/Sidebar.d.ts.map +1 -0
  612. package/dist/ui/components/Sidebar.js +20 -0
  613. package/dist/ui/components/Sidebar.js.map +1 -0
  614. package/dist/ui/components/SiteSwitcher.d.ts +4 -0
  615. package/dist/ui/components/SiteSwitcher.d.ts.map +1 -0
  616. package/dist/ui/components/SiteSwitcher.js +35 -0
  617. package/dist/ui/components/SiteSwitcher.js.map +1 -0
  618. package/dist/ui/components/Topbar.d.ts +9 -0
  619. package/dist/ui/components/Topbar.d.ts.map +1 -0
  620. package/dist/ui/components/Topbar.js +20 -0
  621. package/dist/ui/components/Topbar.js.map +1 -0
  622. package/dist/ui/editor/AiAssistPanel.d.ts +8 -0
  623. package/dist/ui/editor/AiAssistPanel.d.ts.map +1 -0
  624. package/dist/ui/editor/AiAssistPanel.js +221 -0
  625. package/dist/ui/editor/AiAssistPanel.js.map +1 -0
  626. package/dist/ui/editor/ContentForm.d.ts +46 -0
  627. package/dist/ui/editor/ContentForm.d.ts.map +1 -0
  628. package/dist/ui/editor/ContentForm.js +821 -0
  629. package/dist/ui/editor/ContentForm.js.map +1 -0
  630. package/dist/ui/editor/Rte.d.ts +16 -0
  631. package/dist/ui/editor/Rte.d.ts.map +1 -0
  632. package/dist/ui/editor/Rte.js +272 -0
  633. package/dist/ui/editor/Rte.js.map +1 -0
  634. package/dist/ui/editor/ai-assist.d.ts +43 -0
  635. package/dist/ui/editor/ai-assist.d.ts.map +1 -0
  636. package/dist/ui/editor/ai-assist.js +114 -0
  637. package/dist/ui/editor/ai-assist.js.map +1 -0
  638. package/dist/ui/editor/autosave.d.ts +18 -0
  639. package/dist/ui/editor/autosave.d.ts.map +1 -0
  640. package/dist/ui/editor/autosave.js +23 -0
  641. package/dist/ui/editor/autosave.js.map +1 -0
  642. package/dist/ui/editor/content-payload.d.ts +19 -0
  643. package/dist/ui/editor/content-payload.d.ts.map +1 -0
  644. package/dist/ui/editor/content-payload.js +97 -0
  645. package/dist/ui/editor/content-payload.js.map +1 -0
  646. package/dist/ui/editor/editor-media-upload.d.ts +6 -0
  647. package/dist/ui/editor/editor-media-upload.d.ts.map +1 -0
  648. package/dist/ui/editor/editor-media-upload.js +20 -0
  649. package/dist/ui/editor/editor-media-upload.js.map +1 -0
  650. package/dist/ui/editor/serialize.d.ts +6 -0
  651. package/dist/ui/editor/serialize.d.ts.map +1 -0
  652. package/dist/ui/editor/serialize.js +479 -0
  653. package/dist/ui/editor/serialize.js.map +1 -0
  654. package/dist/ui/editor/tweet-embed.d.ts +4 -0
  655. package/dist/ui/editor/tweet-embed.d.ts.map +1 -0
  656. package/dist/ui/editor/tweet-embed.js +49 -0
  657. package/dist/ui/editor/tweet-embed.js.map +1 -0
  658. package/dist/ui/hash-router.d.ts +5 -0
  659. package/dist/ui/hash-router.d.ts.map +1 -0
  660. package/dist/ui/hash-router.js +25 -0
  661. package/dist/ui/hash-router.js.map +1 -0
  662. package/dist/ui/icons.d.ts +32 -0
  663. package/dist/ui/icons.d.ts.map +1 -0
  664. package/dist/ui/icons.js +86 -0
  665. package/dist/ui/icons.js.map +1 -0
  666. package/dist/ui/inspector/Field.d.ts +12 -0
  667. package/dist/ui/inspector/Field.d.ts.map +1 -0
  668. package/dist/ui/inspector/Field.js +8 -0
  669. package/dist/ui/inspector/Field.js.map +1 -0
  670. package/dist/ui/inspector/FoundryTab.d.ts +9 -0
  671. package/dist/ui/inspector/FoundryTab.d.ts.map +1 -0
  672. package/dist/ui/inspector/FoundryTab.js +362 -0
  673. package/dist/ui/inspector/FoundryTab.js.map +1 -0
  674. package/dist/ui/inspector/HistoryTab.d.ts +7 -0
  675. package/dist/ui/inspector/HistoryTab.d.ts.map +1 -0
  676. package/dist/ui/inspector/HistoryTab.js +289 -0
  677. package/dist/ui/inspector/HistoryTab.js.map +1 -0
  678. package/dist/ui/inspector/Inspector.d.ts +13 -0
  679. package/dist/ui/inspector/Inspector.d.ts.map +1 -0
  680. package/dist/ui/inspector/Inspector.js +163 -0
  681. package/dist/ui/inspector/Inspector.js.map +1 -0
  682. package/dist/ui/inspector/OrganizeTab.d.ts +15 -0
  683. package/dist/ui/inspector/OrganizeTab.d.ts.map +1 -0
  684. package/dist/ui/inspector/OrganizeTab.js +319 -0
  685. package/dist/ui/inspector/OrganizeTab.js.map +1 -0
  686. package/dist/ui/inspector/PublishTab.d.ts +18 -0
  687. package/dist/ui/inspector/PublishTab.d.ts.map +1 -0
  688. package/dist/ui/inspector/PublishTab.js +339 -0
  689. package/dist/ui/inspector/PublishTab.js.map +1 -0
  690. package/dist/ui/inspector/Section.d.ts +10 -0
  691. package/dist/ui/inspector/Section.d.ts.map +1 -0
  692. package/dist/ui/inspector/Section.js +40 -0
  693. package/dist/ui/inspector/Section.js.map +1 -0
  694. package/dist/ui/inspector/SeoTab.d.ts +19 -0
  695. package/dist/ui/inspector/SeoTab.d.ts.map +1 -0
  696. package/dist/ui/inspector/SeoTab.js +328 -0
  697. package/dist/ui/inspector/SeoTab.js.map +1 -0
  698. package/dist/ui/inspector/foundry-stages.d.ts +36 -0
  699. package/dist/ui/inspector/foundry-stages.d.ts.map +1 -0
  700. package/dist/ui/inspector/foundry-stages.js +101 -0
  701. package/dist/ui/inspector/foundry-stages.js.map +1 -0
  702. package/dist/ui/inspector/inspector-data.d.ts +80 -0
  703. package/dist/ui/inspector/inspector-data.d.ts.map +1 -0
  704. package/dist/ui/inspector/inspector-data.js +172 -0
  705. package/dist/ui/inspector/inspector-data.js.map +1 -0
  706. package/dist/ui/inspector/organize-data.d.ts +23 -0
  707. package/dist/ui/inspector/organize-data.d.ts.map +1 -0
  708. package/dist/ui/inspector/organize-data.js +28 -0
  709. package/dist/ui/inspector/organize-data.js.map +1 -0
  710. package/dist/ui/inspector/revision-diff.d.ts +49 -0
  711. package/dist/ui/inspector/revision-diff.d.ts.map +1 -0
  712. package/dist/ui/inspector/revision-diff.js +166 -0
  713. package/dist/ui/inspector/revision-diff.js.map +1 -0
  714. package/dist/ui/inspector/seo-helpers.d.ts +37 -0
  715. package/dist/ui/inspector/seo-helpers.d.ts.map +1 -0
  716. package/dist/ui/inspector/seo-helpers.js +37 -0
  717. package/dist/ui/inspector/seo-helpers.js.map +1 -0
  718. package/dist/ui/inspector/tab-visibility.d.ts +14 -0
  719. package/dist/ui/inspector/tab-visibility.d.ts.map +1 -0
  720. package/dist/ui/inspector/tab-visibility.js +28 -0
  721. package/dist/ui/inspector/tab-visibility.js.map +1 -0
  722. package/dist/ui/nav.d.ts +16 -0
  723. package/dist/ui/nav.d.ts.map +1 -0
  724. package/dist/ui/nav.js +33 -0
  725. package/dist/ui/nav.js.map +1 -0
  726. package/dist/ui/pages/admin.astro +32 -0
  727. package/dist/ui/preview/draft-page.d.ts +37 -0
  728. package/dist/ui/preview/draft-page.d.ts.map +1 -0
  729. package/dist/ui/preview/draft-page.js +212 -0
  730. package/dist/ui/preview/draft-page.js.map +1 -0
  731. package/dist/ui/preview/preview-layout.d.ts +23 -0
  732. package/dist/ui/preview/preview-layout.d.ts.map +1 -0
  733. package/dist/ui/preview/preview-layout.js +30 -0
  734. package/dist/ui/preview/preview-layout.js.map +1 -0
  735. package/dist/ui/screens/AnalyticsScreen.d.ts +2 -0
  736. package/dist/ui/screens/AnalyticsScreen.d.ts.map +1 -0
  737. package/dist/ui/screens/AnalyticsScreen.js +408 -0
  738. package/dist/ui/screens/AnalyticsScreen.js.map +1 -0
  739. package/dist/ui/screens/AuthorsScreen.d.ts +2 -0
  740. package/dist/ui/screens/AuthorsScreen.d.ts.map +1 -0
  741. package/dist/ui/screens/AuthorsScreen.js +225 -0
  742. package/dist/ui/screens/AuthorsScreen.js.map +1 -0
  743. package/dist/ui/screens/CalendarScreen.d.ts +6 -0
  744. package/dist/ui/screens/CalendarScreen.d.ts.map +1 -0
  745. package/dist/ui/screens/CalendarScreen.js +327 -0
  746. package/dist/ui/screens/CalendarScreen.js.map +1 -0
  747. package/dist/ui/screens/ContentInsightsScreen.d.ts +2 -0
  748. package/dist/ui/screens/ContentInsightsScreen.d.ts.map +1 -0
  749. package/dist/ui/screens/ContentInsightsScreen.js +129 -0
  750. package/dist/ui/screens/ContentInsightsScreen.js.map +1 -0
  751. package/dist/ui/screens/ContentRoute.d.ts +2 -0
  752. package/dist/ui/screens/ContentRoute.d.ts.map +1 -0
  753. package/dist/ui/screens/ContentRoute.js +32 -0
  754. package/dist/ui/screens/ContentRoute.js.map +1 -0
  755. package/dist/ui/screens/DashboardScreen.d.ts +6 -0
  756. package/dist/ui/screens/DashboardScreen.d.ts.map +1 -0
  757. package/dist/ui/screens/DashboardScreen.js +273 -0
  758. package/dist/ui/screens/DashboardScreen.js.map +1 -0
  759. package/dist/ui/screens/EditorScreen.d.ts +7 -0
  760. package/dist/ui/screens/EditorScreen.d.ts.map +1 -0
  761. package/dist/ui/screens/EditorScreen.js +426 -0
  762. package/dist/ui/screens/EditorScreen.js.map +1 -0
  763. package/dist/ui/screens/LibraryScreen.d.ts +6 -0
  764. package/dist/ui/screens/LibraryScreen.d.ts.map +1 -0
  765. package/dist/ui/screens/LibraryScreen.js +580 -0
  766. package/dist/ui/screens/LibraryScreen.js.map +1 -0
  767. package/dist/ui/screens/MediaScreen.d.ts +2 -0
  768. package/dist/ui/screens/MediaScreen.d.ts.map +1 -0
  769. package/dist/ui/screens/MediaScreen.js +173 -0
  770. package/dist/ui/screens/MediaScreen.js.map +1 -0
  771. package/dist/ui/screens/SettingsScreen.d.ts +4 -0
  772. package/dist/ui/screens/SettingsScreen.d.ts.map +1 -0
  773. package/dist/ui/screens/SettingsScreen.js +751 -0
  774. package/dist/ui/screens/SettingsScreen.js.map +1 -0
  775. package/dist/ui/screens/SocialShareScreen.d.ts +2 -0
  776. package/dist/ui/screens/SocialShareScreen.d.ts.map +1 -0
  777. package/dist/ui/screens/SocialShareScreen.js +224 -0
  778. package/dist/ui/screens/SocialShareScreen.js.map +1 -0
  779. package/dist/ui/screens/SubscriptionsScreen.d.ts +2 -0
  780. package/dist/ui/screens/SubscriptionsScreen.d.ts.map +1 -0
  781. package/dist/ui/screens/SubscriptionsScreen.js +441 -0
  782. package/dist/ui/screens/SubscriptionsScreen.js.map +1 -0
  783. package/dist/ui/screens/TopicsScreen.d.ts +2 -0
  784. package/dist/ui/screens/TopicsScreen.d.ts.map +1 -0
  785. package/dist/ui/screens/TopicsScreen.js +360 -0
  786. package/dist/ui/screens/TopicsScreen.js.map +1 -0
  787. package/dist/ui/screens/analytics-data.d.ts +19 -0
  788. package/dist/ui/screens/analytics-data.d.ts.map +1 -0
  789. package/dist/ui/screens/analytics-data.js +42 -0
  790. package/dist/ui/screens/analytics-data.js.map +1 -0
  791. package/dist/ui/screens/calendar-data.d.ts +45 -0
  792. package/dist/ui/screens/calendar-data.d.ts.map +1 -0
  793. package/dist/ui/screens/calendar-data.js +70 -0
  794. package/dist/ui/screens/calendar-data.js.map +1 -0
  795. package/dist/ui/screens/content-insights-data.d.ts +54 -0
  796. package/dist/ui/screens/content-insights-data.d.ts.map +1 -0
  797. package/dist/ui/screens/content-insights-data.js +82 -0
  798. package/dist/ui/screens/content-insights-data.js.map +1 -0
  799. package/dist/ui/screens/content-view.d.ts +21 -0
  800. package/dist/ui/screens/content-view.d.ts.map +1 -0
  801. package/dist/ui/screens/content-view.js +17 -0
  802. package/dist/ui/screens/content-view.js.map +1 -0
  803. package/dist/ui/screens/library-data.d.ts +76 -0
  804. package/dist/ui/screens/library-data.d.ts.map +1 -0
  805. package/dist/ui/screens/library-data.js +116 -0
  806. package/dist/ui/screens/library-data.js.map +1 -0
  807. package/dist/ui/screens/media-upload.d.ts +19 -0
  808. package/dist/ui/screens/media-upload.d.ts.map +1 -0
  809. package/dist/ui/screens/media-upload.js +187 -0
  810. package/dist/ui/screens/media-upload.js.map +1 -0
  811. package/dist/ui/screens/public-url.d.ts +24 -0
  812. package/dist/ui/screens/public-url.d.ts.map +1 -0
  813. package/dist/ui/screens/public-url.js +74 -0
  814. package/dist/ui/screens/public-url.js.map +1 -0
  815. package/dist/ui/screens/registry.d.ts +3 -0
  816. package/dist/ui/screens/registry.d.ts.map +1 -0
  817. package/dist/ui/screens/registry.js +38 -0
  818. package/dist/ui/screens/registry.js.map +1 -0
  819. package/dist/ui/screens/render-state.d.ts +105 -0
  820. package/dist/ui/screens/render-state.d.ts.map +1 -0
  821. package/dist/ui/screens/render-state.js +127 -0
  822. package/dist/ui/screens/render-state.js.map +1 -0
  823. package/dist/ui/screens/settings-data.d.ts +55 -0
  824. package/dist/ui/screens/settings-data.d.ts.map +1 -0
  825. package/dist/ui/screens/settings-data.js +89 -0
  826. package/dist/ui/screens/settings-data.js.map +1 -0
  827. package/dist/ui/screens/settings-panels-data.d.ts +58 -0
  828. package/dist/ui/screens/settings-panels-data.d.ts.map +1 -0
  829. package/dist/ui/screens/settings-panels-data.js +88 -0
  830. package/dist/ui/screens/settings-panels-data.js.map +1 -0
  831. package/dist/ui/screens/social-share-data.d.ts +307 -0
  832. package/dist/ui/screens/social-share-data.d.ts.map +1 -0
  833. package/dist/ui/screens/social-share-data.js +447 -0
  834. package/dist/ui/screens/social-share-data.js.map +1 -0
  835. package/dist/ui/screens/topics-data.d.ts +38 -0
  836. package/dist/ui/screens/topics-data.d.ts.map +1 -0
  837. package/dist/ui/screens/topics-data.js +50 -0
  838. package/dist/ui/screens/topics-data.js.map +1 -0
  839. package/dist/ui/styles/broadsheet.css +394 -0
  840. package/dist/ui/theme.d.ts +23 -0
  841. package/dist/ui/theme.d.ts.map +1 -0
  842. package/dist/ui/theme.js +87 -0
  843. package/dist/ui/theme.js.map +1 -0
  844. package/dist/ui/tweaks.d.ts +7 -0
  845. package/dist/ui/tweaks.d.ts.map +1 -0
  846. package/dist/ui/tweaks.js +31 -0
  847. package/dist/ui/tweaks.js.map +1 -0
  848. package/dist/ui/use-hash-router.d.ts +6 -0
  849. package/dist/ui/use-hash-router.d.ts.map +1 -0
  850. package/dist/ui/use-hash-router.js +15 -0
  851. package/dist/ui/use-hash-router.js.map +1 -0
  852. package/dist/ui/use-tweaks.d.ts +6 -0
  853. package/dist/ui/use-tweaks.d.ts.map +1 -0
  854. package/dist/ui/use-tweaks.js +24 -0
  855. package/dist/ui/use-tweaks.js.map +1 -0
  856. package/dist/ui/workspace-context.d.ts +34 -0
  857. package/dist/ui/workspace-context.d.ts.map +1 -0
  858. package/dist/ui/workspace-context.js +32 -0
  859. package/dist/ui/workspace-context.js.map +1 -0
  860. package/migrations/0001_create_cms_tables.sql +200 -0
  861. package/migrations/0002_review_softdelete_board.sql +112 -0
  862. package/migrations/0003_content_contributors.sql +16 -0
  863. package/migrations/0004_seo_faq_columns.sql +6 -0
  864. package/migrations/0005_revision_delta_columns.sql +5 -0
  865. package/migrations/0006_processing_trigger_token.sql +4 -0
  866. package/migrations/0007_content_slug_redirects.sql +9 -0
  867. package/migrations/0008_workspace_settings.sql +17 -0
  868. package/migrations/0009_workspace_memberships.sql +36 -0
  869. package/migrations/0010_api_keys_webhooks.sql +21 -0
  870. package/migrations/0011_notifications_activity.sql +22 -0
  871. package/migrations/0012_content_imports.sql +10 -0
  872. package/migrations/0013_media_normalization.sql +4 -0
  873. package/migrations/0014_api_key_prefix.sql +3 -0
  874. package/migrations/0015_cms_topics.sql +7 -0
  875. package/migrations/0016_content_insights.sql +53 -0
  876. package/package.json +82 -0
  877. package/src/engine/activity-log.ts +39 -0
  878. package/src/engine/ai-prompts.ts +124 -0
  879. package/src/engine/ai-writeback.ts +62 -0
  880. package/src/engine/api-keys.ts +239 -0
  881. package/src/engine/content-insights.ts +198 -0
  882. package/src/engine/contributors.ts +95 -0
  883. package/src/engine/cron.ts +62 -0
  884. package/src/engine/d1.ts +29 -0
  885. package/src/engine/foundry-dispatch.ts +417 -0
  886. package/src/engine/import-parsers.ts +478 -0
  887. package/src/engine/index.ts +230 -0
  888. package/src/engine/invites.ts +271 -0
  889. package/src/engine/members.ts +216 -0
  890. package/src/engine/membership-rules.ts +63 -0
  891. package/src/engine/og-render.ts +59 -0
  892. package/src/engine/publish-guard.ts +123 -0
  893. package/src/engine/publisher.ts +1032 -0
  894. package/src/engine/revisions.ts +292 -0
  895. package/src/engine/sanitize.ts +183 -0
  896. package/src/engine/seed-membership.ts +92 -0
  897. package/src/engine/seo.ts +72 -0
  898. package/src/engine/slug-redirects.ts +34 -0
  899. package/src/engine/slug.ts +33 -0
  900. package/src/engine/soft-delete.ts +42 -0
  901. package/src/engine/tags.ts +95 -0
  902. package/src/engine/topics.ts +158 -0
  903. package/src/engine/url-guard.ts +136 -0
  904. package/src/engine/validator/checks/bare-url-not-autolinked.ts +78 -0
  905. package/src/engine/validator/checks/broken-footnote-label.ts +33 -0
  906. package/src/engine/validator/checks/double-encoded-entities.ts +46 -0
  907. package/src/engine/validator/checks/empty-alt-text.ts +35 -0
  908. package/src/engine/validator/checks/heading-hierarchy-skip.ts +33 -0
  909. package/src/engine/validator/checks/html-comment-leak.ts +58 -0
  910. package/src/engine/validator/checks/iframe-missing-dims-and-wrapper.ts +34 -0
  911. package/src/engine/validator/checks/invisible-control-chars.ts +58 -0
  912. package/src/engine/validator/checks/paywall-marker-leak.ts +43 -0
  913. package/src/engine/validator/checks/raw-block-html.ts +65 -0
  914. package/src/engine/validator/checks/stale-body-html.ts +39 -0
  915. package/src/engine/validator/checks/unresolved-footnote-anchor.ts +61 -0
  916. package/src/engine/validator/checks/word-gdocs-paste-artifacts.ts +72 -0
  917. package/src/engine/validator/index.ts +385 -0
  918. package/src/engine/validator/scan.ts +103 -0
  919. package/src/engine/validator/types.ts +114 -0
  920. package/src/engine/webhook-signer.ts +139 -0
  921. package/src/engine/webhooks.ts +224 -0
  922. package/src/index.ts +79 -0
  923. package/src/integration/index.ts +298 -0
  924. package/src/integration/options.ts +30 -0
  925. package/src/integration/vite-plugin.ts +37 -0
  926. package/src/providers/index.ts +2 -0
  927. package/src/providers/null.ts +160 -0
  928. package/src/providers/types.ts +284 -0
  929. package/src/routes/ai.ts +461 -0
  930. package/src/routes/analytics.ts +78 -0
  931. package/src/routes/api-keys.ts +133 -0
  932. package/src/routes/authors.ts +282 -0
  933. package/src/routes/authz-matrix.ts +239 -0
  934. package/src/routes/calendar.ts +127 -0
  935. package/src/routes/config.ts +99 -0
  936. package/src/routes/content-insights.ts +159 -0
  937. package/src/routes/content.ts +1753 -0
  938. package/src/routes/context.ts +146 -0
  939. package/src/routes/cron.ts +27 -0
  940. package/src/routes/dashboard.ts +174 -0
  941. package/src/routes/imports.ts +190 -0
  942. package/src/routes/index.ts +295 -0
  943. package/src/routes/media-lib.ts +405 -0
  944. package/src/routes/media.ts +944 -0
  945. package/src/routes/preview.ts +182 -0
  946. package/src/routes/rbac-invites.ts +220 -0
  947. package/src/routes/rbac.ts +155 -0
  948. package/src/routes/shell.ts +163 -0
  949. package/src/routes/subscriptions.ts +167 -0
  950. package/src/routes/tags.ts +93 -0
  951. package/src/routes/topics.ts +58 -0
  952. package/src/routes/webhooks.ts +233 -0
  953. package/src/schema/index.ts +45 -0
  954. package/src/schema/insights-ingest.ts +126 -0
  955. package/src/schema/migrations.ts +599 -0
  956. package/src/schema/tables.ts +59 -0
  957. package/src/schema/types.ts +576 -0
  958. package/src/ui/api/_authz.ts +100 -0
  959. package/src/ui/api/_content-config.ts +75 -0
  960. package/src/ui/api/activity.ts +33 -0
  961. package/src/ui/api/analytics.ts +42 -0
  962. package/src/ui/api/authors/[id].ts +23 -0
  963. package/src/ui/api/authors.ts +19 -0
  964. package/src/ui/api/calendar.ts +21 -0
  965. package/src/ui/api/content/[id]/ai/headlines.ts +10 -0
  966. package/src/ui/api/content/[id]/ai/meta-description.ts +11 -0
  967. package/src/ui/api/content/[id]/ai/og-image.ts +10 -0
  968. package/src/ui/api/content/[id]/ai/proofread.ts +10 -0
  969. package/src/ui/api/content/[id]/ai/takeaways.ts +11 -0
  970. package/src/ui/api/content/[id]/contributors.ts +13 -0
  971. package/src/ui/api/content/[id]/preview-token.ts +21 -0
  972. package/src/ui/api/content/[id]/revisions/[rev]/restore.ts +12 -0
  973. package/src/ui/api/content/[id]/revisions/[rev].ts +11 -0
  974. package/src/ui/api/content/[id]/revisions.ts +9 -0
  975. package/src/ui/api/content/[id]/seo-score.ts +10 -0
  976. package/src/ui/api/content/[id].ts +23 -0
  977. package/src/ui/api/content/bulk.ts +10 -0
  978. package/src/ui/api/content/counts.ts +11 -0
  979. package/src/ui/api/content/foundry-callback.ts +11 -0
  980. package/src/ui/api/content/import/confirm.ts +16 -0
  981. package/src/ui/api/content/import/parse.ts +16 -0
  982. package/src/ui/api/content/insights-ingest.ts +24 -0
  983. package/src/ui/api/content-insights/dismiss.ts +23 -0
  984. package/src/ui/api/content-insights/index.ts +21 -0
  985. package/src/ui/api/content-insights/undismiss.ts +23 -0
  986. package/src/ui/api/content.ts +21 -0
  987. package/src/ui/api/dashboard.ts +28 -0
  988. package/src/ui/api/me.ts +23 -0
  989. package/src/ui/api/media/[id].ts +22 -0
  990. package/src/ui/api/media/images.ts +9 -0
  991. package/src/ui/api/media/library/[id].ts +9 -0
  992. package/src/ui/api/media/library.ts +22 -0
  993. package/src/ui/api/media/podcast/abort.ts +6 -0
  994. package/src/ui/api/media/podcast/complete.ts +6 -0
  995. package/src/ui/api/media/podcast/init.ts +6 -0
  996. package/src/ui/api/media/podcast/part.ts +6 -0
  997. package/src/ui/api/media/podcast.ts +9 -0
  998. package/src/ui/api/media/videos/abort.ts +9 -0
  999. package/src/ui/api/media/videos/complete.ts +9 -0
  1000. package/src/ui/api/media/videos/init.ts +9 -0
  1001. package/src/ui/api/media/videos/part.ts +9 -0
  1002. package/src/ui/api/notifications.ts +26 -0
  1003. package/src/ui/api/search.ts +23 -0
  1004. package/src/ui/api/settings/api-keys/[id].ts +28 -0
  1005. package/src/ui/api/settings/api-keys.ts +25 -0
  1006. package/src/ui/api/settings/domains.ts +37 -0
  1007. package/src/ui/api/settings/integrations.ts +40 -0
  1008. package/src/ui/api/settings/members/[userId].ts +33 -0
  1009. package/src/ui/api/settings/members/invite.ts +27 -0
  1010. package/src/ui/api/settings/members.ts +23 -0
  1011. package/src/ui/api/settings/webhooks/[id]/test.ts +30 -0
  1012. package/src/ui/api/settings/webhooks/[id].ts +29 -0
  1013. package/src/ui/api/settings/webhooks.ts +27 -0
  1014. package/src/ui/api/subscriptions.ts +56 -0
  1015. package/src/ui/api/tags/[id].ts +24 -0
  1016. package/src/ui/api/tags/index.ts +18 -0
  1017. package/src/ui/api/topics/[id].ts +20 -0
  1018. package/src/ui/api/topics/index.ts +17 -0
  1019. package/src/ui/api/workspace-settings.ts +26 -0
  1020. package/src/ui/client/boot-state.ts +42 -0
  1021. package/src/ui/client/mount.tsx +41 -0
  1022. package/src/ui/commands.ts +62 -0
  1023. package/src/ui/components/CmsApp.tsx +149 -0
  1024. package/src/ui/components/CommandPalette.tsx +118 -0
  1025. package/src/ui/components/NoAccessScreen.tsx +79 -0
  1026. package/src/ui/components/ShareModal.tsx +650 -0
  1027. package/src/ui/components/SharePickers.tsx +790 -0
  1028. package/src/ui/components/ShareStatsPanel.tsx +721 -0
  1029. package/src/ui/components/Sidebar.tsx +86 -0
  1030. package/src/ui/components/SiteSwitcher.tsx +100 -0
  1031. package/src/ui/components/Topbar.tsx +93 -0
  1032. package/src/ui/editor/AiAssistPanel.tsx +407 -0
  1033. package/src/ui/editor/ContentForm.tsx +1462 -0
  1034. package/src/ui/editor/Rte.tsx +382 -0
  1035. package/src/ui/editor/ai-assist.ts +139 -0
  1036. package/src/ui/editor/autosave.ts +36 -0
  1037. package/src/ui/editor/content-payload.ts +125 -0
  1038. package/src/ui/editor/editor-media-upload.ts +26 -0
  1039. package/src/ui/editor/serialize.ts +522 -0
  1040. package/src/ui/editor/tweet-embed.ts +60 -0
  1041. package/src/ui/hash-router.ts +30 -0
  1042. package/src/ui/icons.tsx +208 -0
  1043. package/src/ui/inspector/Field.tsx +30 -0
  1044. package/src/ui/inspector/FoundryTab.tsx +613 -0
  1045. package/src/ui/inspector/HistoryTab.tsx +482 -0
  1046. package/src/ui/inspector/Inspector.tsx +328 -0
  1047. package/src/ui/inspector/OrganizeTab.tsx +534 -0
  1048. package/src/ui/inspector/PublishTab.tsx +626 -0
  1049. package/src/ui/inspector/Section.tsx +81 -0
  1050. package/src/ui/inspector/SeoTab.tsx +573 -0
  1051. package/src/ui/inspector/foundry-stages.ts +140 -0
  1052. package/src/ui/inspector/inspector-data.ts +232 -0
  1053. package/src/ui/inspector/organize-data.ts +51 -0
  1054. package/src/ui/inspector/revision-diff.ts +213 -0
  1055. package/src/ui/inspector/seo-helpers.ts +71 -0
  1056. package/src/ui/inspector/tab-visibility.ts +37 -0
  1057. package/src/ui/nav.ts +48 -0
  1058. package/src/ui/pages/admin.astro +32 -0
  1059. package/src/ui/preview/draft-page.tsx +395 -0
  1060. package/src/ui/preview/preview-layout.ts +49 -0
  1061. package/src/ui/screens/AnalyticsScreen.tsx +938 -0
  1062. package/src/ui/screens/AuthorsScreen.tsx +524 -0
  1063. package/src/ui/screens/CalendarScreen.tsx +694 -0
  1064. package/src/ui/screens/ContentInsightsScreen.tsx +417 -0
  1065. package/src/ui/screens/ContentRoute.tsx +35 -0
  1066. package/src/ui/screens/DashboardScreen.tsx +654 -0
  1067. package/src/ui/screens/EditorScreen.tsx +673 -0
  1068. package/src/ui/screens/LibraryScreen.tsx +1350 -0
  1069. package/src/ui/screens/MediaScreen.tsx +357 -0
  1070. package/src/ui/screens/SettingsScreen.tsx +1841 -0
  1071. package/src/ui/screens/SocialShareScreen.tsx +670 -0
  1072. package/src/ui/screens/SubscriptionsScreen.tsx +1240 -0
  1073. package/src/ui/screens/TopicsScreen.tsx +912 -0
  1074. package/src/ui/screens/analytics-data.ts +68 -0
  1075. package/src/ui/screens/calendar-data.ts +126 -0
  1076. package/src/ui/screens/content-insights-data.ts +127 -0
  1077. package/src/ui/screens/content-view.ts +30 -0
  1078. package/src/ui/screens/library-data.ts +177 -0
  1079. package/src/ui/screens/media-upload.ts +283 -0
  1080. package/src/ui/screens/public-url.ts +81 -0
  1081. package/src/ui/screens/registry.tsx +53 -0
  1082. package/src/ui/screens/render-state.ts +228 -0
  1083. package/src/ui/screens/settings-data.ts +140 -0
  1084. package/src/ui/screens/settings-panels-data.ts +142 -0
  1085. package/src/ui/screens/social-share-data.ts +753 -0
  1086. package/src/ui/screens/topics-data.ts +75 -0
  1087. package/src/ui/styles/broadsheet.css +394 -0
  1088. package/src/ui/theme.ts +104 -0
  1089. package/src/ui/tweaks.ts +37 -0
  1090. package/src/ui/use-hash-router.ts +17 -0
  1091. package/src/ui/use-tweaks.ts +31 -0
  1092. package/src/ui/workspace-context.tsx +62 -0
  1093. package/src/virtual.d.ts +4 -0
@@ -0,0 +1,1841 @@
1
+ // src/ui/screens/SettingsScreen.tsx — Settings screen (P7 Task 10).
2
+ //
3
+ // 6-tab Settings screen mounted in the 'settings' route slot (previously EmptyRoute).
4
+ // Ported from /tmp/fronts-cms-design/cms/manage.jsx SettingsPage.
5
+ //
6
+ // Tabs:
7
+ // General — publication name/tagline/language/timezone + publishing defaults
8
+ // Branding & theme — logo upload + accent color + display typeface
9
+ // Roles & team — member list + role select + invite
10
+ // Integrations — read-only status (placeholder for Task 11)
11
+ // Domains — read-only domain list (placeholder for Task 11)
12
+ // API & webhooks — API keys + webhooks (placeholder for Task 11)
13
+ //
14
+ // General/Branding/Publishing-defaults call GET|PATCH /admin/api/workspace-settings.
15
+ // Roles & Team calls GET /admin/api/settings/members + PATCH /admin/api/settings/members/:userId
16
+ // + POST /admin/api/settings/members/invite.
17
+ //
18
+ // SCOPE GUARDS:
19
+ // - owner-gate on PATCH settings (canEditSettings from settings-data.ts)
20
+ // - Integrations/Domains render read-only honest state (no mutation in Task 10)
21
+ // - API & Webhooks tabs are placeholders here (completed in Task 11)
22
+ // - NO BData, no mock values — live/empty honest rendering
23
+
24
+ import { useCallback, useEffect, useReducer, useState } from 'react'
25
+ import type { ApiKeyListEntry } from '../../engine/api-keys.js'
26
+ import { WEBHOOK_EVENTS, type WebhookRecord } from '../../engine/webhooks.js'
27
+ import { Icon } from '../icons.js'
28
+ import { useWorkspace } from '../workspace-context.js'
29
+ import {
30
+ buildSettingsPatch,
31
+ canEditSettings,
32
+ type MemberRowView,
33
+ memberRowView,
34
+ roleLabel,
35
+ roleOptions,
36
+ type SettingsFormState,
37
+ } from './settings-data.js'
38
+ import {
39
+ domainRowView,
40
+ integrationStatusView,
41
+ maskApiKeyView,
42
+ webhookEventOptions,
43
+ } from './settings-panels-data.js'
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Tab definitions
47
+ // ---------------------------------------------------------------------------
48
+
49
+ type SettingsTab = 'general' | 'branding' | 'team' | 'integrations' | 'domains' | 'api'
50
+
51
+ interface TabDef {
52
+ id: SettingsTab
53
+ label: string
54
+ icon: Parameters<typeof Icon>[0]['name']
55
+ }
56
+
57
+ const TABS: TabDef[] = [
58
+ { id: 'general', label: 'General', icon: 'settings' },
59
+ { id: 'branding', label: 'Branding & theme', icon: 'image' },
60
+ { id: 'team', label: 'Roles & team', icon: 'users' },
61
+ { id: 'integrations', label: 'Integrations', icon: 'layers' },
62
+ { id: 'domains', label: 'Domains', icon: 'globe' },
63
+ { id: 'api', label: 'API & webhooks', icon: 'command' },
64
+ ]
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // Layout primitives
68
+ // ---------------------------------------------------------------------------
69
+
70
+ function Card({
71
+ kicker,
72
+ title,
73
+ desc,
74
+ children,
75
+ foot,
76
+ noRule,
77
+ }: {
78
+ /** Optional mono-uppercase eyebrow above the serif card title. */
79
+ kicker?: string
80
+ title: string
81
+ desc?: string
82
+ children: React.ReactNode
83
+ foot?: React.ReactNode
84
+ /** Suppress the masthead hairline (used when the body is itself a .tbl). */
85
+ noRule?: boolean
86
+ }) {
87
+ return (
88
+ <div className="panel" style={{ marginBottom: 'var(--gap)', overflow: 'hidden' }}>
89
+ <div style={{ padding: '16px 18px' }}>
90
+ {kicker && <div className="kicker">{kicker}</div>}
91
+ <div className="card-title">{title}</div>
92
+ {desc && (
93
+ <div className="page-sub" style={{ marginTop: 3, fontSize: 12.5 }}>
94
+ {desc}
95
+ </div>
96
+ )}
97
+ {!noRule && <hr className="masthead-rule" style={{ margin: '12px 0 0' }} />}
98
+ <div style={{ marginTop: 16 }}>{children}</div>
99
+ </div>
100
+ {foot && (
101
+ <div
102
+ style={{
103
+ padding: '12px 18px',
104
+ borderTop: '1px solid var(--border)',
105
+ background: 'var(--panel-2)',
106
+ display: 'flex',
107
+ justifyContent: 'flex-end',
108
+ gap: 8,
109
+ }}
110
+ >
111
+ {foot}
112
+ </div>
113
+ )}
114
+ </div>
115
+ )
116
+ }
117
+
118
+ function Row({
119
+ label,
120
+ children,
121
+ help,
122
+ }: {
123
+ label: string
124
+ children: React.ReactNode
125
+ help?: string
126
+ }) {
127
+ return (
128
+ <div style={{ marginBottom: 16 }}>
129
+ {/* biome-ignore lint/a11y/noLabelWithoutControl: the control is supplied by the caller via {children} and is nested inside this label, which associates them natively; the wrapper is generic so a static htmlFor is not possible. */}
130
+ <label>
131
+ <span className="field-label">{label}</span>
132
+ {children}
133
+ </label>
134
+ {help && <div className="help">{help}</div>}
135
+ </div>
136
+ )
137
+ }
138
+
139
+ // ---------------------------------------------------------------------------
140
+ // General settings
141
+ // ---------------------------------------------------------------------------
142
+
143
+ interface WorkspaceSettingsData {
144
+ publication_name: string | null
145
+ tagline: string | null
146
+ language: string
147
+ timezone: string
148
+ require_review: number
149
+ auto_seo: number
150
+ scheduled_publish: number
151
+ premium_default: number
152
+ accent_color: string | null
153
+ display_typeface: string | null
154
+ [key: string]: unknown
155
+ }
156
+
157
+ const SETTINGS_DEFAULTS: WorkspaceSettingsData = {
158
+ publication_name: null,
159
+ tagline: null,
160
+ language: 'en',
161
+ timezone: 'Europe/Paris',
162
+ require_review: 0,
163
+ auto_seo: 1,
164
+ scheduled_publish: 1,
165
+ premium_default: 0,
166
+ accent_color: 'Ink blue',
167
+ display_typeface: 'serif',
168
+ }
169
+
170
+ function ToggleRow({
171
+ label,
172
+ desc,
173
+ value,
174
+ onChange,
175
+ disabled,
176
+ }: {
177
+ label: string
178
+ desc?: string
179
+ value: boolean
180
+ onChange: (v: boolean) => void
181
+ disabled?: boolean
182
+ }) {
183
+ return (
184
+ <div
185
+ style={{
186
+ display: 'flex',
187
+ alignItems: 'center',
188
+ gap: 14,
189
+ padding: '12px 0',
190
+ borderBottom: '1px solid var(--border-soft)',
191
+ }}
192
+ >
193
+ <div style={{ flex: 1 }}>
194
+ <div style={{ fontSize: 13.5, fontWeight: 600, color: 'var(--ink)' }}>{label}</div>
195
+ {desc && (
196
+ <div style={{ fontSize: 12, color: 'var(--ink-muted)', marginTop: 2 }}>{desc}</div>
197
+ )}
198
+ </div>
199
+ <button
200
+ type="button"
201
+ onClick={() => !disabled && onChange(!value)}
202
+ style={{
203
+ width: 40,
204
+ height: 22,
205
+ borderRadius: 12,
206
+ border: 0,
207
+ background: value ? 'var(--accent)' : 'var(--hover)',
208
+ position: 'relative',
209
+ flex: 'none',
210
+ transition: 'background .2s',
211
+ cursor: disabled ? 'default' : 'pointer',
212
+ opacity: disabled ? 0.5 : 1,
213
+ }}
214
+ >
215
+ <span
216
+ style={{
217
+ position: 'absolute',
218
+ top: 2,
219
+ left: value ? 20 : 2,
220
+ width: 18,
221
+ height: 18,
222
+ borderRadius: '50%',
223
+ background: '#fff',
224
+ boxShadow: '0 1px 2px rgba(0,0,0,.3)',
225
+ transition: 'left .2s',
226
+ }}
227
+ />
228
+ </button>
229
+ </div>
230
+ )
231
+ }
232
+
233
+ function GeneralSettings({
234
+ data,
235
+ canEdit,
236
+ onSave,
237
+ saving,
238
+ }: {
239
+ data: WorkspaceSettingsData
240
+ canEdit: boolean
241
+ onSave: (patch: SettingsFormState) => Promise<void>
242
+ saving: boolean
243
+ }) {
244
+ const [form, setForm] = useState<WorkspaceSettingsData>({ ...data })
245
+
246
+ // Keep local form in sync if parent data changes
247
+ useEffect(() => {
248
+ setForm({ ...data })
249
+ }, [data])
250
+
251
+ function setField(key: keyof WorkspaceSettingsData, value: unknown) {
252
+ if (!canEdit) return
253
+ setForm((f) => ({ ...f, [key]: value }))
254
+ }
255
+
256
+ async function handleSave() {
257
+ if (!canEdit || saving) return
258
+ const patch = buildSettingsPatch(form)
259
+ await onSave(patch)
260
+ }
261
+
262
+ function handleCancel() {
263
+ setForm({ ...data })
264
+ }
265
+
266
+ const footer = canEdit ? (
267
+ <>
268
+ <button type="button" className="btn sm" onClick={handleCancel} disabled={saving}>
269
+ Cancel
270
+ </button>
271
+ <button type="button" className="btn primary sm" onClick={handleSave} disabled={saving}>
272
+ {saving ? 'Saving…' : 'Save changes'}
273
+ </button>
274
+ </>
275
+ ) : (
276
+ <span style={{ fontSize: 12, color: 'var(--ink-muted)' }}>
277
+ Settings editing is restricted to workspace owners.
278
+ </span>
279
+ )
280
+
281
+ return (
282
+ <>
283
+ <Card
284
+ kicker="Identity"
285
+ title="Publication"
286
+ desc="Identity shown across the site and in metadata."
287
+ foot={footer}
288
+ >
289
+ <Row label="Publication name">
290
+ <input
291
+ className="input"
292
+ value={form.publication_name ?? ''}
293
+ onChange={(e) => setField('publication_name', e.target.value)}
294
+ disabled={!canEdit}
295
+ />
296
+ </Row>
297
+ <Row label="Tagline">
298
+ <input
299
+ className="input"
300
+ value={form.tagline ?? ''}
301
+ onChange={(e) => setField('tagline', e.target.value)}
302
+ disabled={!canEdit}
303
+ />
304
+ </Row>
305
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
306
+ <Row label="Primary language">
307
+ <select
308
+ className="selectbox"
309
+ value={form.language}
310
+ onChange={(e) => setField('language', e.target.value)}
311
+ disabled={!canEdit}
312
+ >
313
+ <option value="en">English</option>
314
+ <option value="en-GB">English (UK)</option>
315
+ <option value="en-US">English (US)</option>
316
+ <option value="fr">French</option>
317
+ </select>
318
+ </Row>
319
+ <Row label="Timezone">
320
+ <select
321
+ className="selectbox"
322
+ value={form.timezone}
323
+ onChange={(e) => setField('timezone', e.target.value)}
324
+ disabled={!canEdit}
325
+ >
326
+ <option value="Europe/Paris">Europe/Paris (CET)</option>
327
+ <option value="Europe/London">Europe/London (GMT)</option>
328
+ <option value="America/New_York">America/New_York (ET)</option>
329
+ <option value="America/Los_Angeles">America/Los_Angeles (PT)</option>
330
+ <option value="UTC">UTC</option>
331
+ </select>
332
+ </Row>
333
+ </div>
334
+ </Card>
335
+ <Card
336
+ kicker="Defaults"
337
+ title="Publishing defaults"
338
+ desc="Applied to new content unless overridden."
339
+ >
340
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
341
+ <ToggleRow
342
+ label="Require editorial review before publish"
343
+ desc="New drafts must pass through the review column."
344
+ value={form.require_review === 1}
345
+ onChange={(v) => setField('require_review', v ? 1 : 0)}
346
+ disabled={!canEdit}
347
+ />
348
+ <ToggleRow
349
+ label="Auto-generate SEO metadata"
350
+ desc="Draft meta title, description and OG image on save."
351
+ value={form.auto_seo === 1}
352
+ onChange={(v) => setField('auto_seo', v ? 1 : 0)}
353
+ disabled={!canEdit}
354
+ />
355
+ <ToggleRow
356
+ label="Enable scheduled publishing"
357
+ desc="Allow content to auto-publish at a set time."
358
+ value={form.scheduled_publish === 1}
359
+ onChange={(v) => setField('scheduled_publish', v ? 1 : 0)}
360
+ disabled={!canEdit}
361
+ />
362
+ <ToggleRow
363
+ label="New articles are premium by default"
364
+ value={form.premium_default === 1}
365
+ onChange={(v) => setField('premium_default', v ? 1 : 0)}
366
+ disabled={!canEdit}
367
+ />
368
+ </div>
369
+ </Card>
370
+ </>
371
+ )
372
+ }
373
+
374
+ // ---------------------------------------------------------------------------
375
+ // Branding settings
376
+ // ---------------------------------------------------------------------------
377
+
378
+ const ACCENT_PRESETS = ['#0c4a6e', '#b23a2e', '#1f6b4a', '#5b4bc4', '#374151']
379
+ const TYPEFACE_OPTIONS = ['serif', 'Source Serif 4', 'Spectral', 'Newsreader', 'Inter']
380
+
381
+ function BrandingSettings({
382
+ data,
383
+ canEdit,
384
+ onSave,
385
+ saving,
386
+ }: {
387
+ data: WorkspaceSettingsData
388
+ canEdit: boolean
389
+ onSave: (patch: SettingsFormState) => Promise<void>
390
+ saving: boolean
391
+ }) {
392
+ const [accent, setAccent] = useState(data.accent_color ?? ACCENT_PRESETS[0])
393
+ const [typeface, setTypeface] = useState(data.display_typeface ?? 'serif')
394
+
395
+ useEffect(() => {
396
+ setAccent(data.accent_color ?? ACCENT_PRESETS[0])
397
+ setTypeface(data.display_typeface ?? 'serif')
398
+ }, [data])
399
+
400
+ async function handleSaveTheme() {
401
+ if (!canEdit || saving) return
402
+ await onSave({ accent_color: accent, display_typeface: typeface })
403
+ }
404
+
405
+ const themeFooter = canEdit ? (
406
+ <button type="button" className="btn primary sm" onClick={handleSaveTheme} disabled={saving}>
407
+ {saving ? 'Saving…' : 'Save theme'}
408
+ </button>
409
+ ) : (
410
+ <span style={{ fontSize: 12, color: 'var(--ink-muted)' }}>
411
+ Branding editing is restricted to workspace owners.
412
+ </span>
413
+ )
414
+
415
+ return (
416
+ <>
417
+ <Card
418
+ kicker="Brand"
419
+ title="Logo & mark"
420
+ desc="Shown in the CMS sidebar and the published site header."
421
+ >
422
+ <div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
423
+ <div
424
+ style={{
425
+ width: 56,
426
+ height: 56,
427
+ borderRadius: 12,
428
+ background: 'var(--ink)',
429
+ display: 'grid',
430
+ placeItems: 'center',
431
+ flex: 'none',
432
+ }}
433
+ >
434
+ <Icon name="settings" size={28} style={{ color: '#fff' }} />
435
+ </div>
436
+ {canEdit ? (
437
+ <>
438
+ <button type="button" className="btn sm">
439
+ <Icon name="upload" size={14} /> Upload logo
440
+ </button>
441
+ <button type="button" className="btn ghost sm">
442
+ Reset
443
+ </button>
444
+ </>
445
+ ) : (
446
+ <span style={{ fontSize: 12, color: 'var(--ink-muted)' }}>
447
+ Logo editing is restricted to workspace owners.
448
+ </span>
449
+ )}
450
+ </div>
451
+ </Card>
452
+ <Card
453
+ kicker="Appearance"
454
+ title="Theme"
455
+ desc="Per-site accent and typography. Each site in your workspace keeps its own."
456
+ foot={themeFooter}
457
+ >
458
+ <Row label="Accent color">
459
+ <div style={{ display: 'flex', gap: 10 }}>
460
+ {ACCENT_PRESETS.map((c) => (
461
+ <button
462
+ type="button"
463
+ key={c}
464
+ onClick={() => canEdit && setAccent(c)}
465
+ style={{
466
+ width: 34,
467
+ height: 34,
468
+ borderRadius: 8,
469
+ background: c,
470
+ cursor: canEdit ? 'pointer' : 'default',
471
+ outline: accent === c ? '2px solid var(--accent)' : 'none',
472
+ outlineOffset: 2,
473
+ border: 0,
474
+ opacity: canEdit ? 1 : 0.6,
475
+ }}
476
+ />
477
+ ))}
478
+ </div>
479
+ </Row>
480
+ <Row label="Display typeface" help="The serif used for headlines on the public site.">
481
+ <select
482
+ className="selectbox"
483
+ value={typeface}
484
+ onChange={(e) => canEdit && setTypeface(e.target.value)}
485
+ disabled={!canEdit}
486
+ >
487
+ {TYPEFACE_OPTIONS.map((t) => (
488
+ <option key={t} value={t}>
489
+ {t}
490
+ </option>
491
+ ))}
492
+ </select>
493
+ </Row>
494
+ </Card>
495
+ </>
496
+ )
497
+ }
498
+
499
+ // ---------------------------------------------------------------------------
500
+ // Team settings (Roles & Team)
501
+ // ---------------------------------------------------------------------------
502
+
503
+ interface TeamState {
504
+ loading: boolean
505
+ error: string | null
506
+ members: MemberRowView[]
507
+ }
508
+
509
+ type TeamAction =
510
+ | { type: 'fetch-start' }
511
+ | { type: 'fetch-ok'; members: MemberRowView[] }
512
+ | { type: 'fetch-error'; error: string }
513
+ | { type: 'update-role'; userId: string; role: string }
514
+
515
+ function teamReducer(state: TeamState, action: TeamAction): TeamState {
516
+ switch (action.type) {
517
+ case 'fetch-start':
518
+ return { ...state, loading: true, error: null }
519
+ case 'fetch-ok':
520
+ return { loading: false, error: null, members: action.members }
521
+ case 'fetch-error':
522
+ return { loading: false, error: action.error, members: [] }
523
+ case 'update-role':
524
+ return {
525
+ ...state,
526
+ members: state.members.map((m) =>
527
+ m.userId === action.userId ? { ...m, role: action.role as MemberRowView['role'] } : m,
528
+ ),
529
+ }
530
+ default:
531
+ return state
532
+ }
533
+ }
534
+
535
+ function TeamSettings({ currentSubject }: { currentSubject: string | null }) {
536
+ const [state, dispatch] = useReducer(teamReducer, {
537
+ loading: true,
538
+ error: null,
539
+ members: [],
540
+ })
541
+ const [inviteEmail, setInviteEmail] = useState('')
542
+ const [inviteRole, setInviteRole] = useState<string>('editor')
543
+ const [inviting, setInviting] = useState(false)
544
+ const [inviteError, setInviteError] = useState<string | null>(null)
545
+ const [inviteSuccess, setInviteSuccess] = useState<string | null>(null)
546
+ const [showInviteForm, setShowInviteForm] = useState(false)
547
+ const [roleChanging, setRoleChanging] = useState<string | null>(null)
548
+
549
+ const fetchMembers = useCallback(async () => {
550
+ dispatch({ type: 'fetch-start' })
551
+ try {
552
+ const res = await fetch('/admin/api/settings/members')
553
+ if (!res.ok) {
554
+ const body = (await res.json().catch(() => ({}))) as { error?: string }
555
+ dispatch({ type: 'fetch-error', error: body.error ?? `HTTP ${res.status}` })
556
+ return
557
+ }
558
+ const body = (await res.json()) as {
559
+ members: Array<{
560
+ userId: string
561
+ role: string
562
+ email: string
563
+ name: string | null
564
+ image: string | null
565
+ acceptedAt: number | null
566
+ createdAt: number
567
+ }>
568
+ }
569
+ const views = (body.members ?? []).map((m) =>
570
+ memberRowView(
571
+ {
572
+ userId: m.userId,
573
+ role: m.role as MemberRowView['role'],
574
+ email: m.email,
575
+ name: m.name,
576
+ image: m.image,
577
+ acceptedAt: m.acceptedAt,
578
+ createdAt: m.createdAt,
579
+ },
580
+ currentSubject,
581
+ ),
582
+ )
583
+ dispatch({ type: 'fetch-ok', members: views })
584
+ } catch (err) {
585
+ dispatch({ type: 'fetch-error', error: String(err) })
586
+ }
587
+ }, [currentSubject])
588
+
589
+ useEffect(() => {
590
+ fetchMembers().catch(() => {})
591
+ }, [fetchMembers])
592
+
593
+ async function handleRoleChange(userId: string, newRole: string) {
594
+ setRoleChanging(userId)
595
+ try {
596
+ const res = await fetch(`/admin/api/settings/members/${encodeURIComponent(userId)}`, {
597
+ method: 'PATCH',
598
+ headers: { 'Content-Type': 'application/json' },
599
+ body: JSON.stringify({ role: newRole }),
600
+ })
601
+ if (!res.ok) {
602
+ const body = (await res.json().catch(() => ({}))) as { error?: string; reason?: string }
603
+ alert(body.reason ?? body.error ?? `Failed to change role (HTTP ${res.status})`)
604
+ return
605
+ }
606
+ dispatch({ type: 'update-role', userId, role: newRole })
607
+ } catch (err) {
608
+ alert(String(err))
609
+ } finally {
610
+ setRoleChanging(null)
611
+ }
612
+ }
613
+
614
+ async function handleInvite(e: React.FormEvent) {
615
+ e.preventDefault()
616
+ if (!inviteEmail.trim()) return
617
+ setInviting(true)
618
+ setInviteError(null)
619
+ setInviteSuccess(null)
620
+ try {
621
+ const res = await fetch('/admin/api/settings/members/invite', {
622
+ method: 'POST',
623
+ headers: { 'Content-Type': 'application/json' },
624
+ body: JSON.stringify({ email: inviteEmail.trim(), role: inviteRole }),
625
+ })
626
+ const body = (await res.json().catch(() => ({}))) as {
627
+ ok?: boolean
628
+ token?: string
629
+ error?: string
630
+ }
631
+ if (!res.ok) {
632
+ setInviteError(body.error ?? `HTTP ${res.status}`)
633
+ return
634
+ }
635
+ setInviteSuccess(
636
+ body.token
637
+ ? `Invite sent. Accept link: /invite/accept?token=${body.token}`
638
+ : 'Invite created. Share the accept link with the invitee.',
639
+ )
640
+ setInviteEmail('')
641
+ setShowInviteForm(false)
642
+ } catch (err) {
643
+ setInviteError(String(err))
644
+ } finally {
645
+ setInviting(false)
646
+ }
647
+ }
648
+
649
+ const roles = roleOptions()
650
+
651
+ const foot = (
652
+ <button type="button" className="btn primary sm" onClick={() => setShowInviteForm((v) => !v)}>
653
+ <Icon name="plus" size={14} /> Invite member
654
+ </button>
655
+ )
656
+
657
+ return (
658
+ <Card
659
+ kicker="Access"
660
+ title="Team"
661
+ desc="People with access to this workspace and their roles."
662
+ foot={foot}
663
+ noRule
664
+ >
665
+ {state.loading && (
666
+ <div style={{ padding: 16, color: 'var(--ink-muted)', fontSize: 13 }}>Loading members…</div>
667
+ )}
668
+ {state.error && !state.loading && (
669
+ <div style={{ padding: 16, color: 'var(--red)', fontSize: 13 }}>
670
+ Failed to load members: {state.error}
671
+ </div>
672
+ )}
673
+ {!state.loading && !state.error && state.members.length === 0 && (
674
+ <div style={{ padding: 16, color: 'var(--ink-muted)', fontSize: 13 }}>No members yet.</div>
675
+ )}
676
+ {!state.loading && !state.error && state.members.length > 0 && (
677
+ <table className="tbl">
678
+ <thead>
679
+ <tr>
680
+ <th>Member</th>
681
+ <th>Email</th>
682
+ <th style={{ width: 170 }}>Role</th>
683
+ </tr>
684
+ </thead>
685
+ <tbody>
686
+ {state.members.map((m) => (
687
+ <tr key={m.userId}>
688
+ <td>
689
+ <div style={{ display: 'flex', alignItems: 'center', gap: 11 }}>
690
+ <div
691
+ className="avatar"
692
+ style={{
693
+ background: 'var(--accent-tint)',
694
+ width: 30,
695
+ height: 30,
696
+ fontSize: 11,
697
+ color: 'var(--accent)',
698
+ }}
699
+ >
700
+ {(m.name ?? m.email)[0]?.toUpperCase() ?? '?'}
701
+ </div>
702
+ <span style={{ fontSize: 13, fontWeight: 600, color: 'var(--ink)' }}>
703
+ {m.name ?? m.email}
704
+ {m.isSelf && (
705
+ <span style={{ fontSize: 11, color: 'var(--ink-muted)', marginLeft: 6 }}>
706
+ (you)
707
+ </span>
708
+ )}
709
+ </span>
710
+ </div>
711
+ </td>
712
+ <td style={{ fontSize: 12.5, color: 'var(--ink-muted)' }}>{m.email}</td>
713
+ <td>
714
+ <select
715
+ className="selectbox"
716
+ value={m.role}
717
+ onChange={(e) => handleRoleChange(m.userId, e.target.value)}
718
+ disabled={roleChanging === m.userId || m.isSelf}
719
+ style={{ width: 150, fontSize: 12.5, padding: '7px 10px' }}
720
+ >
721
+ {roles.map((r) => (
722
+ <option key={r.value} value={r.value}>
723
+ {r.label}
724
+ </option>
725
+ ))}
726
+ </select>
727
+ </td>
728
+ </tr>
729
+ ))}
730
+ </tbody>
731
+ </table>
732
+ )}
733
+
734
+ {/* Invite form */}
735
+ {showInviteForm && (
736
+ <form onSubmit={handleInvite} className="panel panel-pad" style={{ marginTop: 16 }}>
737
+ <div className="kicker" style={{ marginBottom: 10 }}>
738
+ Invite a member
739
+ </div>
740
+ <div
741
+ style={{ display: 'grid', gridTemplateColumns: '1fr auto', gap: 8, marginBottom: 8 }}
742
+ >
743
+ <input
744
+ type="email"
745
+ className="input"
746
+ placeholder="Email address"
747
+ value={inviteEmail}
748
+ onChange={(e) => setInviteEmail(e.target.value)}
749
+ required
750
+ />
751
+ <select
752
+ className="selectbox"
753
+ value={inviteRole}
754
+ onChange={(e) => setInviteRole(e.target.value)}
755
+ style={{ width: 150 }}
756
+ >
757
+ {roles.map((r) => (
758
+ <option key={r.value} value={r.value}>
759
+ {r.label}
760
+ </option>
761
+ ))}
762
+ </select>
763
+ </div>
764
+ <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
765
+ <button
766
+ type="button"
767
+ className="btn sm"
768
+ onClick={() => {
769
+ setShowInviteForm(false)
770
+ setInviteError(null)
771
+ }}
772
+ >
773
+ Cancel
774
+ </button>
775
+ <button type="submit" className="btn primary sm" disabled={inviting}>
776
+ {inviting ? 'Inviting…' : 'Send invite'}
777
+ </button>
778
+ </div>
779
+ {inviteError && (
780
+ <div style={{ marginTop: 8, fontSize: 12, color: 'var(--red)' }}>{inviteError}</div>
781
+ )}
782
+ {inviteSuccess && (
783
+ <div className="mono" style={{ marginTop: 8, fontSize: 12, color: 'var(--green)' }}>
784
+ {inviteSuccess}
785
+ </div>
786
+ )}
787
+ </form>
788
+ )}
789
+ </Card>
790
+ )
791
+ }
792
+
793
+ // ---------------------------------------------------------------------------
794
+ // API keys settings
795
+ // ---------------------------------------------------------------------------
796
+
797
+ interface ApiKeysState {
798
+ loading: boolean
799
+ error: string | null
800
+ keys: ApiKeyListEntry[]
801
+ }
802
+
803
+ type ApiKeysAction =
804
+ | { type: 'fetch-start' }
805
+ | { type: 'fetch-ok'; keys: ApiKeyListEntry[] }
806
+ | { type: 'fetch-error'; error: string }
807
+ | { type: 'revoked'; id: string }
808
+
809
+ function apiKeysReducer(state: ApiKeysState, action: ApiKeysAction): ApiKeysState {
810
+ switch (action.type) {
811
+ case 'fetch-start':
812
+ return { ...state, loading: true, error: null }
813
+ case 'fetch-ok':
814
+ return { loading: false, error: null, keys: action.keys }
815
+ case 'fetch-error':
816
+ return { loading: false, error: action.error, keys: [] }
817
+ case 'revoked':
818
+ return { ...state, keys: state.keys.filter((k) => k.id !== action.id) }
819
+ default:
820
+ return state
821
+ }
822
+ }
823
+
824
+ /** One-time reveal modal shown after key creation. */
825
+ function KeyRevealModal({ rawKey, onDone }: { rawKey: string; onDone: () => void }) {
826
+ const [copied, setCopied] = useState(false)
827
+
828
+ function handleCopy() {
829
+ void navigator.clipboard.writeText(rawKey).then(() => {
830
+ setCopied(true)
831
+ })
832
+ }
833
+
834
+ return (
835
+ <div className="scrim" style={{ placeItems: 'center' }}>
836
+ <div
837
+ className="panel"
838
+ style={{
839
+ padding: 28,
840
+ width: 480,
841
+ maxWidth: '90vw',
842
+ boxShadow: 'var(--shadow-lg)',
843
+ }}
844
+ >
845
+ <div className="kicker" style={{ marginBottom: 6 }}>
846
+ One-time reveal
847
+ </div>
848
+ <div className="card-title" style={{ marginBottom: 8 }}>
849
+ Your new API key
850
+ </div>
851
+ <div
852
+ style={{
853
+ fontSize: 12.5,
854
+ color: 'var(--red)',
855
+ marginBottom: 14,
856
+ fontWeight: 600,
857
+ }}
858
+ >
859
+ Copy this key now — it will not be shown again.
860
+ </div>
861
+ <div
862
+ className="mono"
863
+ style={{
864
+ background: 'var(--panel-2)',
865
+ border: '1px solid var(--border)',
866
+ borderRadius: 8,
867
+ padding: '11px 14px',
868
+ fontSize: 13,
869
+ wordBreak: 'break-all',
870
+ color: 'var(--ink)',
871
+ marginBottom: 14,
872
+ }}
873
+ >
874
+ {rawKey}
875
+ </div>
876
+ <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
877
+ <button type="button" className="btn sm" onClick={handleCopy}>
878
+ <Icon name="copy" size={14} /> {copied ? 'Copied!' : 'Copy'}
879
+ </button>
880
+ <button type="button" className="btn primary sm" onClick={onDone}>
881
+ Done — I've saved this key
882
+ </button>
883
+ </div>
884
+ </div>
885
+ </div>
886
+ )
887
+ }
888
+
889
+ function ApiSettings() {
890
+ const [state, dispatch] = useReducer(apiKeysReducer, {
891
+ loading: true,
892
+ error: null,
893
+ keys: [],
894
+ })
895
+ const [showCreateForm, setShowCreateForm] = useState(false)
896
+ const [newKeyName, setNewKeyName] = useState('')
897
+ const [creating, setCreating] = useState(false)
898
+ const [createError, setCreateError] = useState<string | null>(null)
899
+ const [revealedKey, setRevealedKey] = useState<string | null>(null)
900
+ const [revoking, setRevoking] = useState<string | null>(null)
901
+
902
+ const fetchKeys = useCallback(async () => {
903
+ dispatch({ type: 'fetch-start' })
904
+ try {
905
+ const res = await fetch('/admin/api/settings/api-keys')
906
+ if (!res.ok) {
907
+ const body = (await res.json().catch(() => ({}))) as { error?: string }
908
+ dispatch({ type: 'fetch-error', error: body.error ?? `HTTP ${res.status}` })
909
+ return
910
+ }
911
+ const body = (await res.json()) as { apiKeys: ApiKeyListEntry[] }
912
+ dispatch({ type: 'fetch-ok', keys: body.apiKeys ?? [] })
913
+ } catch (err) {
914
+ dispatch({ type: 'fetch-error', error: String(err) })
915
+ }
916
+ }, [])
917
+
918
+ useEffect(() => {
919
+ fetchKeys().catch(() => {})
920
+ }, [fetchKeys])
921
+
922
+ async function handleCreate(e: React.FormEvent) {
923
+ e.preventDefault()
924
+ if (!newKeyName.trim()) return
925
+ setCreating(true)
926
+ setCreateError(null)
927
+ try {
928
+ const res = await fetch('/admin/api/settings/api-keys', {
929
+ method: 'POST',
930
+ headers: { 'Content-Type': 'application/json' },
931
+ body: JSON.stringify({ name: newKeyName.trim(), scopes: [] }),
932
+ })
933
+ const body = (await res.json().catch(() => ({}))) as {
934
+ rawKey?: string
935
+ id?: string
936
+ masked?: string
937
+ error?: string
938
+ }
939
+ if (!res.ok) {
940
+ setCreateError(body.error ?? `HTTP ${res.status}`)
941
+ return
942
+ }
943
+ // Show the one-time reveal modal
944
+ if (body.rawKey) {
945
+ setRevealedKey(body.rawKey)
946
+ }
947
+ setNewKeyName('')
948
+ setShowCreateForm(false)
949
+ await fetchKeys()
950
+ } catch (err) {
951
+ setCreateError(String(err))
952
+ } finally {
953
+ setCreating(false)
954
+ }
955
+ }
956
+
957
+ async function handleRevoke(id: string) {
958
+ if (!confirm('Revoke this key? Any code using it will immediately lose access.')) return
959
+ setRevoking(id)
960
+ try {
961
+ const res = await fetch(`/admin/api/settings/api-keys/${encodeURIComponent(id)}`, {
962
+ method: 'DELETE',
963
+ })
964
+ if (res.ok) {
965
+ dispatch({ type: 'revoked', id })
966
+ } else {
967
+ const body = (await res.json().catch(() => ({}))) as { error?: string }
968
+ alert(body.error ?? `Failed to revoke (HTTP ${res.status})`)
969
+ }
970
+ } catch (err) {
971
+ alert(String(err))
972
+ } finally {
973
+ setRevoking(null)
974
+ }
975
+ }
976
+
977
+ const foot = (
978
+ <button type="button" className="btn primary sm" onClick={() => setShowCreateForm((v) => !v)}>
979
+ <Icon name="plus" size={14} /> New key
980
+ </button>
981
+ )
982
+
983
+ return (
984
+ <>
985
+ {revealedKey && <KeyRevealModal rawKey={revealedKey} onDone={() => setRevealedKey(null)} />}
986
+ <Card
987
+ kicker="Programmatic access"
988
+ title="API keys"
989
+ desc="Programmatic access to the content API."
990
+ foot={foot}
991
+ noRule
992
+ >
993
+ {state.loading && (
994
+ <div style={{ padding: 16, color: 'var(--ink-muted)', fontSize: 13 }}>
995
+ Loading API keys…
996
+ </div>
997
+ )}
998
+ {state.error && !state.loading && (
999
+ <div style={{ padding: 16, color: 'var(--red)', fontSize: 13 }}>
1000
+ Failed to load keys: {state.error}
1001
+ </div>
1002
+ )}
1003
+ {!state.loading && !state.error && state.keys.length === 0 && !showCreateForm && (
1004
+ <div style={{ padding: '16px 0', color: 'var(--ink-muted)', fontSize: 13 }}>
1005
+ No API keys yet. Create one to allow programmatic access.
1006
+ </div>
1007
+ )}
1008
+ {!state.loading && !state.error && state.keys.length > 0 && (
1009
+ <table className="tbl">
1010
+ <thead>
1011
+ <tr>
1012
+ <th>Key</th>
1013
+ <th>Scopes</th>
1014
+ <th style={{ width: 110, textAlign: 'right' }} />
1015
+ </tr>
1016
+ </thead>
1017
+ <tbody>
1018
+ {state.keys.map((k) => (
1019
+ <tr key={k.id}>
1020
+ <td>
1021
+ <div style={{ display: 'flex', alignItems: 'center', gap: 11 }}>
1022
+ <Icon
1023
+ name="command"
1024
+ size={17}
1025
+ style={{ color: 'var(--ink-muted)', flex: 'none' }}
1026
+ />
1027
+ <div style={{ minWidth: 0 }}>
1028
+ <div style={{ fontSize: 13, fontWeight: 600, color: 'var(--ink)' }}>
1029
+ {k.name}
1030
+ </div>
1031
+ <div className="mono" style={{ fontSize: 12, color: 'var(--ink-muted)' }}>
1032
+ {maskApiKeyView(k)}
1033
+ </div>
1034
+ </div>
1035
+ </div>
1036
+ </td>
1037
+ <td>
1038
+ {k.scopes.length > 0 ? (
1039
+ <span className="tag-chip">{k.scopes.join(', ')}</span>
1040
+ ) : (
1041
+ <span className="mono" style={{ fontSize: 11.5, color: 'var(--ink-faint)' }}>
1042
+
1043
+ </span>
1044
+ )}
1045
+ </td>
1046
+ <td style={{ textAlign: 'right' }}>
1047
+ <button
1048
+ type="button"
1049
+ className="btn danger sm"
1050
+ disabled={revoking === k.id}
1051
+ onClick={() => handleRevoke(k.id)}
1052
+ >
1053
+ {revoking === k.id ? 'Revoking…' : 'Revoke'}
1054
+ </button>
1055
+ </td>
1056
+ </tr>
1057
+ ))}
1058
+ </tbody>
1059
+ </table>
1060
+ )}
1061
+
1062
+ {/* Create form */}
1063
+ {showCreateForm && (
1064
+ <form onSubmit={handleCreate} className="panel panel-pad" style={{ marginTop: 16 }}>
1065
+ <div className="kicker" style={{ marginBottom: 10 }}>
1066
+ Create API key
1067
+ </div>
1068
+ <input
1069
+ type="text"
1070
+ className="input"
1071
+ placeholder="Key name (e.g. Production build)"
1072
+ value={newKeyName}
1073
+ onChange={(e) => setNewKeyName(e.target.value)}
1074
+ required
1075
+ style={{ marginBottom: 8 }}
1076
+ />
1077
+ <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
1078
+ <button
1079
+ type="button"
1080
+ className="btn sm"
1081
+ onClick={() => {
1082
+ setShowCreateForm(false)
1083
+ setCreateError(null)
1084
+ }}
1085
+ >
1086
+ Cancel
1087
+ </button>
1088
+ <button type="submit" className="btn primary sm" disabled={creating}>
1089
+ {creating ? 'Creating…' : 'Create key'}
1090
+ </button>
1091
+ </div>
1092
+ {createError && (
1093
+ <div style={{ marginTop: 8, fontSize: 12, color: 'var(--red)' }}>{createError}</div>
1094
+ )}
1095
+ </form>
1096
+ )}
1097
+ </Card>
1098
+ </>
1099
+ )
1100
+ }
1101
+
1102
+ // ---------------------------------------------------------------------------
1103
+ // Webhooks settings
1104
+ // ---------------------------------------------------------------------------
1105
+
1106
+ interface WebhooksState {
1107
+ loading: boolean
1108
+ error: string | null
1109
+ webhooks: WebhookRecord[]
1110
+ }
1111
+
1112
+ type WebhooksAction =
1113
+ | { type: 'fetch-start' }
1114
+ | { type: 'fetch-ok'; webhooks: WebhookRecord[] }
1115
+ | { type: 'fetch-error'; error: string }
1116
+ | { type: 'deleted'; id: string }
1117
+ | { type: 'update-status'; id: string; status: string }
1118
+
1119
+ function webhooksReducer(state: WebhooksState, action: WebhooksAction): WebhooksState {
1120
+ switch (action.type) {
1121
+ case 'fetch-start':
1122
+ return { ...state, loading: true, error: null }
1123
+ case 'fetch-ok':
1124
+ return { loading: false, error: null, webhooks: action.webhooks }
1125
+ case 'fetch-error':
1126
+ return { loading: false, error: action.error, webhooks: [] }
1127
+ case 'deleted':
1128
+ return { ...state, webhooks: state.webhooks.filter((w) => w.id !== action.id) }
1129
+ case 'update-status':
1130
+ return {
1131
+ ...state,
1132
+ webhooks: state.webhooks.map((w) =>
1133
+ w.id === action.id ? { ...w, lastStatus: action.status } : w,
1134
+ ),
1135
+ }
1136
+ default:
1137
+ return state
1138
+ }
1139
+ }
1140
+
1141
+ function WebhooksSettings() {
1142
+ const [state, dispatch] = useReducer(webhooksReducer, {
1143
+ loading: true,
1144
+ error: null,
1145
+ webhooks: [],
1146
+ })
1147
+ const [showCreateForm, setShowCreateForm] = useState(false)
1148
+ const [newEventType, setNewEventType] = useState<string>(WEBHOOK_EVENTS[0])
1149
+ const [newUrl, setNewUrl] = useState('')
1150
+ const [creating, setCreating] = useState(false)
1151
+ const [createError, setCreateError] = useState<string | null>(null)
1152
+ const [testing, setTesting] = useState<string | null>(null)
1153
+ const [deleting, setDeleting] = useState<string | null>(null)
1154
+
1155
+ const fetchWebhooks = useCallback(async () => {
1156
+ dispatch({ type: 'fetch-start' })
1157
+ try {
1158
+ const res = await fetch('/admin/api/settings/webhooks')
1159
+ if (!res.ok) {
1160
+ const body = (await res.json().catch(() => ({}))) as { error?: string }
1161
+ dispatch({ type: 'fetch-error', error: body.error ?? `HTTP ${res.status}` })
1162
+ return
1163
+ }
1164
+ const body = (await res.json()) as { webhooks: WebhookRecord[] }
1165
+ dispatch({ type: 'fetch-ok', webhooks: body.webhooks ?? [] })
1166
+ } catch (err) {
1167
+ dispatch({ type: 'fetch-error', error: String(err) })
1168
+ }
1169
+ }, [])
1170
+
1171
+ useEffect(() => {
1172
+ fetchWebhooks().catch(() => {})
1173
+ }, [fetchWebhooks])
1174
+
1175
+ async function handleCreate(e: React.FormEvent) {
1176
+ e.preventDefault()
1177
+ if (!newUrl.trim()) return
1178
+ setCreating(true)
1179
+ setCreateError(null)
1180
+ try {
1181
+ const res = await fetch('/admin/api/settings/webhooks', {
1182
+ method: 'POST',
1183
+ headers: { 'Content-Type': 'application/json' },
1184
+ body: JSON.stringify({ eventType: newEventType, url: newUrl.trim() }),
1185
+ })
1186
+ const body = (await res.json().catch(() => ({}))) as { error?: string }
1187
+ if (!res.ok) {
1188
+ setCreateError(body.error ?? `HTTP ${res.status}`)
1189
+ return
1190
+ }
1191
+ setNewUrl('')
1192
+ setShowCreateForm(false)
1193
+ await fetchWebhooks()
1194
+ } catch (err) {
1195
+ setCreateError(String(err))
1196
+ } finally {
1197
+ setCreating(false)
1198
+ }
1199
+ }
1200
+
1201
+ async function handleTest(id: string) {
1202
+ setTesting(id)
1203
+ try {
1204
+ const res = await fetch(`/admin/api/settings/webhooks/${encodeURIComponent(id)}/test`, {
1205
+ method: 'POST',
1206
+ })
1207
+ const body = (await res.json().catch(() => ({}))) as { error?: string }
1208
+ if (!res.ok) {
1209
+ alert(body.error ?? `Test failed (HTTP ${res.status})`)
1210
+ return
1211
+ }
1212
+ dispatch({ type: 'update-status', id, status: 'test' })
1213
+ } catch (err) {
1214
+ alert(String(err))
1215
+ } finally {
1216
+ setTesting(null)
1217
+ }
1218
+ }
1219
+
1220
+ async function handleDelete(id: string) {
1221
+ if (!confirm('Delete this webhook? It will stop receiving events.')) return
1222
+ setDeleting(id)
1223
+ try {
1224
+ const res = await fetch(`/admin/api/settings/webhooks/${encodeURIComponent(id)}`, {
1225
+ method: 'DELETE',
1226
+ })
1227
+ if (res.ok) {
1228
+ dispatch({ type: 'deleted', id })
1229
+ } else {
1230
+ const body = (await res.json().catch(() => ({}))) as { error?: string }
1231
+ alert(body.error ?? `Failed to delete (HTTP ${res.status})`)
1232
+ }
1233
+ } catch (err) {
1234
+ alert(String(err))
1235
+ } finally {
1236
+ setDeleting(null)
1237
+ }
1238
+ }
1239
+
1240
+ const eventOptions = webhookEventOptions()
1241
+
1242
+ const foot = (
1243
+ <button type="button" className="btn primary sm" onClick={() => setShowCreateForm((v) => !v)}>
1244
+ <Icon name="plus" size={14} /> Add webhook
1245
+ </button>
1246
+ )
1247
+
1248
+ return (
1249
+ <Card
1250
+ kicker="Event hooks"
1251
+ title="Webhooks"
1252
+ desc="Fired when content changes — used by Foundry and the build pipeline."
1253
+ foot={foot}
1254
+ noRule
1255
+ >
1256
+ {state.loading && (
1257
+ <div style={{ padding: 16, color: 'var(--ink-muted)', fontSize: 13 }}>
1258
+ Loading webhooks…
1259
+ </div>
1260
+ )}
1261
+ {state.error && !state.loading && (
1262
+ <div style={{ padding: 16, color: 'var(--red)', fontSize: 13 }}>
1263
+ Failed to load webhooks: {state.error}
1264
+ </div>
1265
+ )}
1266
+ {!state.loading && !state.error && state.webhooks.length === 0 && !showCreateForm && (
1267
+ <div style={{ padding: '16px 0', color: 'var(--ink-muted)', fontSize: 13 }}>
1268
+ No webhooks registered yet.
1269
+ </div>
1270
+ )}
1271
+ {!state.loading && !state.error && state.webhooks.length > 0 && (
1272
+ <table className="tbl">
1273
+ <thead>
1274
+ <tr>
1275
+ <th style={{ width: 180 }}>Event</th>
1276
+ <th>Endpoint</th>
1277
+ <th style={{ width: 90 }}>Last</th>
1278
+ <th style={{ width: 130, textAlign: 'right' }} />
1279
+ </tr>
1280
+ </thead>
1281
+ <tbody>
1282
+ {state.webhooks.map((w) => (
1283
+ <tr key={w.id}>
1284
+ <td>
1285
+ <span
1286
+ className="pill scheduled"
1287
+ style={{ background: 'var(--blue-tint)', fontSize: 11.5 }}
1288
+ >
1289
+ {w.eventType}
1290
+ </span>
1291
+ </td>
1292
+ <td
1293
+ className="mono"
1294
+ style={{
1295
+ fontSize: 12,
1296
+ color: 'var(--ink-muted)',
1297
+ maxWidth: 0,
1298
+ whiteSpace: 'nowrap',
1299
+ overflow: 'hidden',
1300
+ textOverflow: 'ellipsis',
1301
+ }}
1302
+ >
1303
+ {w.url}
1304
+ </td>
1305
+ <td>
1306
+ {w.lastStatus ? (
1307
+ <span
1308
+ style={{
1309
+ display: 'inline-flex',
1310
+ alignItems: 'center',
1311
+ gap: 5,
1312
+ fontSize: 11.5,
1313
+ color: w.lastStatus === 'test' ? 'var(--ink-muted)' : 'var(--green)',
1314
+ }}
1315
+ >
1316
+ <Icon name="checkCircle" size={13} /> {w.lastStatus}
1317
+ </span>
1318
+ ) : (
1319
+ <span className="mono" style={{ fontSize: 11.5, color: 'var(--ink-faint)' }}>
1320
+
1321
+ </span>
1322
+ )}
1323
+ </td>
1324
+ <td style={{ textAlign: 'right' }}>
1325
+ <div
1326
+ style={{
1327
+ display: 'inline-flex',
1328
+ alignItems: 'center',
1329
+ gap: 6,
1330
+ justifyContent: 'flex-end',
1331
+ }}
1332
+ >
1333
+ <button
1334
+ type="button"
1335
+ className="btn ghost sm"
1336
+ disabled={testing === w.id}
1337
+ onClick={() => handleTest(w.id)}
1338
+ style={{ fontSize: 12 }}
1339
+ >
1340
+ {testing === w.id ? 'Testing…' : 'Test'}
1341
+ </button>
1342
+ <button
1343
+ type="button"
1344
+ className="icon-btn"
1345
+ disabled={deleting === w.id}
1346
+ onClick={() => handleDelete(w.id)}
1347
+ title="Delete webhook"
1348
+ >
1349
+ <Icon name="trash" size={15} />
1350
+ </button>
1351
+ </div>
1352
+ </td>
1353
+ </tr>
1354
+ ))}
1355
+ </tbody>
1356
+ </table>
1357
+ )}
1358
+
1359
+ {/* Create form */}
1360
+ {showCreateForm && (
1361
+ <form onSubmit={handleCreate} className="panel panel-pad" style={{ marginTop: 16 }}>
1362
+ <div className="kicker" style={{ marginBottom: 10 }}>
1363
+ Add webhook
1364
+ </div>
1365
+ <div style={{ marginBottom: 8 }}>
1366
+ <label>
1367
+ <span className="field-label">Event</span>
1368
+ <select
1369
+ className="selectbox"
1370
+ value={newEventType}
1371
+ onChange={(e) => setNewEventType(e.target.value)}
1372
+ >
1373
+ {eventOptions.map((opt) => (
1374
+ <option key={opt.value} value={opt.value}>
1375
+ {opt.label}
1376
+ </option>
1377
+ ))}
1378
+ </select>
1379
+ </label>
1380
+ </div>
1381
+ <div style={{ marginBottom: 8 }}>
1382
+ <label>
1383
+ <span className="field-label">Endpoint URL</span>
1384
+ <input
1385
+ type="url"
1386
+ className="input"
1387
+ placeholder="https://your-endpoint.example.com/hook"
1388
+ value={newUrl}
1389
+ onChange={(e) => setNewUrl(e.target.value)}
1390
+ required
1391
+ />
1392
+ </label>
1393
+ </div>
1394
+ <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
1395
+ <button
1396
+ type="button"
1397
+ className="btn sm"
1398
+ onClick={() => {
1399
+ setShowCreateForm(false)
1400
+ setCreateError(null)
1401
+ }}
1402
+ >
1403
+ Cancel
1404
+ </button>
1405
+ <button type="submit" className="btn primary sm" disabled={creating}>
1406
+ {creating ? 'Adding…' : 'Add webhook'}
1407
+ </button>
1408
+ </div>
1409
+ {createError && (
1410
+ <div style={{ marginTop: 8, fontSize: 12, color: 'var(--red)' }}>{createError}</div>
1411
+ )}
1412
+ </form>
1413
+ )}
1414
+ </Card>
1415
+ )
1416
+ }
1417
+
1418
+ // ---------------------------------------------------------------------------
1419
+ // Integrations settings (read-only — no connect/disconnect mutation in P7)
1420
+ // ---------------------------------------------------------------------------
1421
+
1422
+ interface IntegrationEntry {
1423
+ name: string
1424
+ desc: string
1425
+ icon: Parameters<typeof Icon>[0]['name']
1426
+ providerKey: string
1427
+ }
1428
+
1429
+ const INTEGRATION_DEFS: IntegrationEntry[] = [
1430
+ {
1431
+ name: 'Cloudflare R2',
1432
+ desc: 'Object storage for media originals',
1433
+ icon: 'media',
1434
+ providerKey: 'r2',
1435
+ },
1436
+ {
1437
+ name: 'Foundry',
1438
+ desc: 'Video & audio processing pipeline',
1439
+ icon: 'bolt',
1440
+ providerKey: 'foundry',
1441
+ },
1442
+ {
1443
+ name: 'LemonSqueezy',
1444
+ desc: 'Subscriptions, billing & checkout',
1445
+ icon: 'inbox',
1446
+ providerKey: 'lemonsqueezy',
1447
+ },
1448
+ {
1449
+ name: 'Customer.io',
1450
+ desc: 'Newsletter & lifecycle email',
1451
+ icon: 'mail',
1452
+ providerKey: 'customerio',
1453
+ },
1454
+ {
1455
+ name: 'Cloudflare Search',
1456
+ desc: 'Full-text content index',
1457
+ icon: 'search',
1458
+ providerKey: 'search',
1459
+ },
1460
+ {
1461
+ name: 'Web Push',
1462
+ desc: 'Browser breaking-news alerts',
1463
+ icon: 'bell',
1464
+ providerKey: 'webpush',
1465
+ },
1466
+ ]
1467
+
1468
+ interface IntegrationsData {
1469
+ integrations: Record<string, { status: string; detail?: string } | null>
1470
+ }
1471
+
1472
+ function IntegrationsSettings() {
1473
+ const [data, setData] = useState<IntegrationsData | null>(null)
1474
+ const [loading, setLoading] = useState(true)
1475
+ const [error, setError] = useState<string | null>(null)
1476
+
1477
+ useEffect(() => {
1478
+ setLoading(true)
1479
+ setError(null)
1480
+ fetch('/admin/api/settings/integrations')
1481
+ .then(async (res) => {
1482
+ if (!res.ok) {
1483
+ const body = (await res.json().catch(() => ({}))) as { error?: string }
1484
+ setError(body.error ?? `HTTP ${res.status}`)
1485
+ return
1486
+ }
1487
+ const body = (await res.json()) as IntegrationsData
1488
+ setData(body)
1489
+ })
1490
+ .catch((err: unknown) => {
1491
+ setError(String(err))
1492
+ })
1493
+ .finally(() => setLoading(false))
1494
+ }, [])
1495
+
1496
+ return (
1497
+ <Card
1498
+ kicker="Connected services"
1499
+ title="Integrations"
1500
+ desc="Services connected to this workspace."
1501
+ >
1502
+ {loading && (
1503
+ <div style={{ padding: 16, color: 'var(--ink-muted)', fontSize: 13 }}>
1504
+ Loading integrations…
1505
+ </div>
1506
+ )}
1507
+ {error && !loading && (
1508
+ <div style={{ padding: 16, color: 'var(--red)', fontSize: 13 }}>
1509
+ Failed to load integrations: {error}
1510
+ </div>
1511
+ )}
1512
+ {!loading && !error && (
1513
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
1514
+ {INTEGRATION_DEFS.map((it) => {
1515
+ const raw = data?.integrations?.[it.providerKey] ?? null
1516
+ const view = integrationStatusView(raw)
1517
+ return (
1518
+ <div
1519
+ key={it.name}
1520
+ style={{
1521
+ display: 'flex',
1522
+ alignItems: 'center',
1523
+ gap: 13,
1524
+ padding: '13px 14px',
1525
+ border: '1px solid var(--border)',
1526
+ borderRadius: 10,
1527
+ background: 'var(--panel-2)',
1528
+ }}
1529
+ >
1530
+ <div
1531
+ style={{
1532
+ width: 38,
1533
+ height: 38,
1534
+ borderRadius: 9,
1535
+ background: 'var(--hover)',
1536
+ color: 'var(--ink-soft)',
1537
+ display: 'grid',
1538
+ placeItems: 'center',
1539
+ flex: 'none',
1540
+ }}
1541
+ >
1542
+ <Icon name={it.icon} size={19} />
1543
+ </div>
1544
+ <div style={{ flex: 1, minWidth: 0 }}>
1545
+ <div style={{ fontSize: 13.5, fontWeight: 600, color: 'var(--ink)' }}>
1546
+ {it.name}
1547
+ </div>
1548
+ <div style={{ fontSize: 12, color: 'var(--ink-muted)' }}>{it.desc}</div>
1549
+ </div>
1550
+ <div style={{ textAlign: 'right' }}>
1551
+ {view.connected ? (
1552
+ <span className="pill published">
1553
+ <span className="pdot" />
1554
+ {view.label}
1555
+ </span>
1556
+ ) : (
1557
+ <span className="pill draft">{view.label}</span>
1558
+ )}
1559
+ {view.detail && (
1560
+ <div
1561
+ className="mono"
1562
+ style={{ fontSize: 10.5, color: 'var(--ink-faint)', marginTop: 4 }}
1563
+ >
1564
+ {view.detail}
1565
+ </div>
1566
+ )}
1567
+ </div>
1568
+ </div>
1569
+ )
1570
+ })}
1571
+ </div>
1572
+ )}
1573
+ </Card>
1574
+ )
1575
+ }
1576
+
1577
+ // ---------------------------------------------------------------------------
1578
+ // Domains settings (read-only — managed via Cloudflare, no mutation in P7)
1579
+ // ---------------------------------------------------------------------------
1580
+
1581
+ interface DomainEntry {
1582
+ domain: string
1583
+ role?: string
1584
+ sslActive?: boolean
1585
+ }
1586
+
1587
+ interface DomainsData {
1588
+ domains: DomainEntry[]
1589
+ }
1590
+
1591
+ function DomainsSettings() {
1592
+ const [data, setData] = useState<DomainsData | null>(null)
1593
+ const [loading, setLoading] = useState(true)
1594
+ const [error, setError] = useState<string | null>(null)
1595
+
1596
+ useEffect(() => {
1597
+ setLoading(true)
1598
+ setError(null)
1599
+ fetch('/admin/api/settings/domains')
1600
+ .then(async (res) => {
1601
+ if (!res.ok) {
1602
+ const body = (await res.json().catch(() => ({}))) as { error?: string }
1603
+ setError(body.error ?? `HTTP ${res.status}`)
1604
+ return
1605
+ }
1606
+ const body = (await res.json()) as DomainsData
1607
+ setData(body)
1608
+ })
1609
+ .catch((err: unknown) => {
1610
+ setError(String(err))
1611
+ })
1612
+ .finally(() => setLoading(false))
1613
+ }, [])
1614
+
1615
+ const domainRows = (data?.domains ?? []).map((d) => domainRowView(d))
1616
+
1617
+ return (
1618
+ <Card
1619
+ kicker="Delivery"
1620
+ title="Domains"
1621
+ desc="Where this site is served. Managed via Cloudflare."
1622
+ noRule
1623
+ >
1624
+ {loading && (
1625
+ <div style={{ padding: 16, color: 'var(--ink-muted)', fontSize: 13 }}>Loading domains…</div>
1626
+ )}
1627
+ {error && !loading && (
1628
+ <div style={{ padding: 16, color: 'var(--red)', fontSize: 13 }}>
1629
+ Failed to load domains: {error}
1630
+ </div>
1631
+ )}
1632
+ {!loading && !error && domainRows.length === 0 && (
1633
+ <div style={{ padding: '16px 0', color: 'var(--ink-muted)', fontSize: 13 }}>
1634
+ No domains configured. Domains are managed via the Cloudflare dashboard.
1635
+ </div>
1636
+ )}
1637
+ {!loading && !error && domainRows.length > 0 && (
1638
+ <table className="tbl">
1639
+ <thead>
1640
+ <tr>
1641
+ <th>Domain</th>
1642
+ <th>Role</th>
1643
+ <th style={{ width: 130, textAlign: 'right' }}>SSL</th>
1644
+ </tr>
1645
+ </thead>
1646
+ <tbody>
1647
+ {domainRows.map((d) => (
1648
+ <tr key={d.domain}>
1649
+ <td>
1650
+ <div style={{ display: 'flex', alignItems: 'center', gap: 11 }}>
1651
+ <Icon
1652
+ name="globe"
1653
+ size={17}
1654
+ style={{ color: 'var(--ink-muted)', flex: 'none' }}
1655
+ />
1656
+ <span style={{ fontSize: 13, fontWeight: 600, color: 'var(--ink)' }}>
1657
+ {d.domain}
1658
+ </span>
1659
+ </div>
1660
+ </td>
1661
+ <td style={{ fontSize: 12.5, color: 'var(--ink-muted)' }}>{d.role || '—'}</td>
1662
+ <td style={{ textAlign: 'right' }}>
1663
+ {d.sslActive ? (
1664
+ <span className="pill published">
1665
+ <span className="pdot" />
1666
+ SSL active
1667
+ </span>
1668
+ ) : (
1669
+ <span className="pill draft">No SSL</span>
1670
+ )}
1671
+ </td>
1672
+ </tr>
1673
+ ))}
1674
+ </tbody>
1675
+ </table>
1676
+ )}
1677
+ </Card>
1678
+ )
1679
+ }
1680
+
1681
+ // ---------------------------------------------------------------------------
1682
+ // SettingsScreen — top-level screen component
1683
+ // ---------------------------------------------------------------------------
1684
+
1685
+ export function SettingsScreen() {
1686
+ const { state } = useWorkspace()
1687
+ const [tab, setTab] = useState<SettingsTab>('general')
1688
+ const [settingsData, setSettingsData] = useState<WorkspaceSettingsData | null>(null)
1689
+ const [settingsLoading, setSettingsLoading] = useState(true)
1690
+ const [settingsError, setSettingsError] = useState<string | null>(null)
1691
+ const [saving, setSaving] = useState(false)
1692
+
1693
+ const currentSubject = state.user?.subject ?? null
1694
+ const currentRole = state.user?.role ?? null
1695
+ const editable = canEditSettings(currentRole)
1696
+
1697
+ // Fetch workspace settings on mount
1698
+ const fetchSettings = useCallback(async () => {
1699
+ setSettingsLoading(true)
1700
+ setSettingsError(null)
1701
+ try {
1702
+ const res = await fetch('/admin/api/workspace-settings')
1703
+ if (!res.ok) {
1704
+ setSettingsError(`Failed to load settings (HTTP ${res.status})`)
1705
+ return
1706
+ }
1707
+ const body = (await res.json()) as { status: string; data: WorkspaceSettingsData }
1708
+ setSettingsData({ ...SETTINGS_DEFAULTS, ...(body.data ?? {}) })
1709
+ } catch (err) {
1710
+ setSettingsError(String(err))
1711
+ } finally {
1712
+ setSettingsLoading(false)
1713
+ }
1714
+ }, [])
1715
+
1716
+ useEffect(() => {
1717
+ fetchSettings().catch(() => {})
1718
+ }, [fetchSettings])
1719
+
1720
+ async function handleSave(patch: SettingsFormState) {
1721
+ if (!editable) return
1722
+ setSaving(true)
1723
+ try {
1724
+ const res = await fetch('/admin/api/workspace-settings', {
1725
+ method: 'PATCH',
1726
+ headers: { 'Content-Type': 'application/json' },
1727
+ body: JSON.stringify(patch),
1728
+ })
1729
+ if (!res.ok) {
1730
+ const body = (await res.json().catch(() => ({}))) as { error?: string }
1731
+ alert(body.error ?? `Failed to save settings (HTTP ${res.status})`)
1732
+ return
1733
+ }
1734
+ // Re-fetch to ensure the UI is in sync with server state
1735
+ await fetchSettings()
1736
+ } finally {
1737
+ setSaving(false)
1738
+ }
1739
+ }
1740
+
1741
+ const data = settingsData ?? SETTINGS_DEFAULTS
1742
+
1743
+ return (
1744
+ <div className="page page-wide fade-in" style={{ maxWidth: 1180 }}>
1745
+ <div className="page-head">
1746
+ <div>
1747
+ <h1 className="page-title">Settings</h1>
1748
+ <div className="page-sub">
1749
+ Workspace configuration for {data.publication_name ?? 'this publication'}.
1750
+ </div>
1751
+ </div>
1752
+ </div>
1753
+
1754
+ {settingsError && !settingsLoading && (
1755
+ <div
1756
+ className="panel"
1757
+ style={{
1758
+ padding: '10px 14px',
1759
+ marginBottom: 'var(--gap)',
1760
+ background: 'var(--red-tint)',
1761
+ borderColor: 'var(--red)',
1762
+ fontSize: 13,
1763
+ color: 'var(--red)',
1764
+ }}
1765
+ >
1766
+ {settingsError}
1767
+ </div>
1768
+ )}
1769
+
1770
+ <div
1771
+ style={{
1772
+ display: 'grid',
1773
+ gridTemplateColumns: '208px 1fr',
1774
+ gap: 'var(--gap)',
1775
+ alignItems: 'start',
1776
+ }}
1777
+ >
1778
+ {/* Left-rail navigation */}
1779
+ <div className="panel" style={{ padding: 8, position: 'sticky', top: 0 }}>
1780
+ {TABS.map((t) => (
1781
+ <button
1782
+ key={t.id}
1783
+ type="button"
1784
+ className={`nav-item${tab === t.id ? ' active' : ''}`}
1785
+ style={{
1786
+ width: '100%',
1787
+ textAlign: 'left',
1788
+ border: 0,
1789
+ background: 'transparent',
1790
+ }}
1791
+ onClick={() => setTab(t.id)}
1792
+ >
1793
+ <Icon name={t.icon} size={17} className="ni-icon" />
1794
+ <span className="ni-label">{t.label}</span>
1795
+ </button>
1796
+ ))}
1797
+ </div>
1798
+
1799
+ {/* Tab content */}
1800
+ <div className="fade-only" key={tab}>
1801
+ {settingsLoading && (tab === 'general' || tab === 'branding') ? (
1802
+ <div style={{ padding: 32, color: 'var(--ink-muted)', fontSize: 13 }}>
1803
+ Loading settings…
1804
+ </div>
1805
+ ) : (
1806
+ <>
1807
+ {tab === 'general' && (
1808
+ <GeneralSettings
1809
+ data={data}
1810
+ canEdit={editable}
1811
+ onSave={handleSave}
1812
+ saving={saving}
1813
+ />
1814
+ )}
1815
+ {tab === 'branding' && (
1816
+ <BrandingSettings
1817
+ data={data}
1818
+ canEdit={editable}
1819
+ onSave={handleSave}
1820
+ saving={saving}
1821
+ />
1822
+ )}
1823
+ {tab === 'team' && <TeamSettings currentSubject={currentSubject} />}
1824
+ {tab === 'integrations' && <IntegrationsSettings />}
1825
+ {tab === 'domains' && <DomainsSettings />}
1826
+ {tab === 'api' && (
1827
+ <>
1828
+ <ApiSettings />
1829
+ <WebhooksSettings />
1830
+ </>
1831
+ )}
1832
+ </>
1833
+ )}
1834
+ </div>
1835
+ </div>
1836
+ </div>
1837
+ )
1838
+ }
1839
+
1840
+ // Re-export roleLabel so callers importing from SettingsScreen.tsx can use it
1841
+ export { roleLabel }