@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,1462 @@
1
+ // src/ui/editor/ContentForm.tsx — four-type content form with Tiptap RTE.
2
+ //
3
+ // Handles: title, dek (excerpt), type-specific body block:
4
+ // - article / newsletter → <Rte/> (Tiptap body)
5
+ // - video → media/video block (URL input; the actual R2 multipart upload
6
+ // is wired through the existing /media/multipart routes; here
7
+ // we store a source URL or R2 key)
8
+ // - podcast → audio block (source URL; mirrors video)
9
+ //
10
+ // Footer: live word-count + read-time via engine's countWords/estimateReadTime.
11
+ //
12
+ // Autosave:
13
+ // - debounce AUTOSAVE_DEBOUNCE_MS → PATCH /content/:id
14
+ // - shouldSnapshot cadence → POST /content/:id?action=snapshot
15
+ //
16
+ // New-doc flow: when docId is null (new content), the form calls POST /content
17
+ // on the first save and switches to update mode.
18
+ //
19
+ // Compile-gated: all Tiptap/React usage verified by `pnpm run build` (tsc).
20
+
21
+ import { useCallback, useEffect, useRef, useState } from 'react'
22
+ import { countWords, estimateReadTime } from '../../engine/publisher.js'
23
+ import type { ContentStatus, ContentType } from '../../schema/types.js'
24
+ import { Icon } from '../icons.js'
25
+ import { uploadMediaAsset } from '../screens/media-upload.js'
26
+ import { AiAssistPanel } from './AiAssistPanel.js'
27
+ import { AUTOSAVE_DEBOUNCE_MS, shouldSnapshot } from './autosave.js'
28
+ import {
29
+ buildContentCreatePayload,
30
+ buildContentUpdatePayload,
31
+ type ContentDraftFields,
32
+ canCreateContentDraft,
33
+ foundryDispatchSourceForDraft,
34
+ slugifyTitle,
35
+ } from './content-payload.js'
36
+ import { uploadEditorImage } from './editor-media-upload.js'
37
+ import { Rte } from './Rte.js'
38
+ import { docToMarkdown } from './serialize.js'
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Types
42
+ // ---------------------------------------------------------------------------
43
+
44
+ export interface ContentFormProps {
45
+ /** Existing content id (null = new unsaved doc). */
46
+ docId: string | null
47
+ /** Content type (article | video | podcast | newsletter). */
48
+ contentType: ContentType
49
+ /** Called with the live resolved id once the doc is saved for the first time. */
50
+ onDocCreated?: (id: string) => void
51
+ /** Called on every autosave with the current title (for header refresh). */
52
+ onTitleChange?: (title: string) => void
53
+ /** Called whenever the editable draft changes so the live preview stays in sync. */
54
+ onDraftChange?: (draft: ContentPreviewDraft) => void
55
+ /** Incrementing signal from the editor header to flush the current draft immediately. */
56
+ saveRequest?: number
57
+ /** Called when the loaded content status changes so editor chrome can stay in sync. */
58
+ onStatusChange?: (status: ContentStatus) => void
59
+ /** Called when save state changes so editor chrome can show a real save action. */
60
+ onSaveStateChange?: (state: ContentSaveState) => void
61
+ }
62
+
63
+ export interface ContentSaveState {
64
+ saving: boolean
65
+ saveError: string | null
66
+ lastSavedAt: number | null
67
+ savedId: string | null
68
+ }
69
+
70
+ export interface ContentPreviewDraft {
71
+ id: string
72
+ type: ContentType
73
+ status: ContentStatus
74
+ title: string
75
+ slug: string
76
+ excerpt: string | null
77
+ byline: string | null
78
+ authorId: string | null
79
+ authorName: string | null
80
+ authorSlug: string | null
81
+ publishedAt: string | null
82
+ body: string | null
83
+ premium: boolean
84
+ heroImageUrl: string | null
85
+ channel: string | null
86
+ }
87
+
88
+ interface ExistingContentItem {
89
+ id?: string
90
+ type?: ContentType
91
+ status?: string
92
+ visibility?: string
93
+ slug?: string
94
+ title?: string
95
+ excerpt?: string | null
96
+ byline?: string | null
97
+ author_id?: string | null
98
+ author_name?: string | null
99
+ author_slug?: string | null
100
+ channel?: string | null
101
+ published_at?: number | null
102
+ hero_image_id?: string | null
103
+ hero_image_url?: string | null
104
+ }
105
+
106
+ interface ExistingContentPayload {
107
+ body_markdown?: string | null
108
+ body_html?: string | null
109
+ subtitle?: string | null
110
+ script?: string | null
111
+ video_id?: string | null
112
+ thumbnail_image_id?: string | null
113
+ thumbnail_image_url?: string | null
114
+ processing_source_url?: string | null
115
+ processing_source_kind?: string | null
116
+ transcript?: string | null
117
+ audio_r2_key?: string | null
118
+ }
119
+
120
+ interface ExistingContentResponse {
121
+ item?: ExistingContentItem
122
+ content?: ExistingContentPayload | null
123
+ }
124
+
125
+ interface FormState {
126
+ status: ContentStatus
127
+ title: string
128
+ dek: string
129
+ body: string
130
+ slug: string
131
+ byline: string | null
132
+ authorId: string | null
133
+ authorName: string | null
134
+ authorSlug: string | null
135
+ publishedAt: string | null
136
+ premium: boolean
137
+ channel: string | null
138
+ heroImageId: string
139
+ heroImageUrl: string | null
140
+ heroUploadStatus: 'idle' | 'uploading' | 'staged'
141
+ heroUploadError: string | null
142
+ videoUrl: string
143
+ videoSourceKind: string | null
144
+ videoId: string | null
145
+ thumbnailImageId: string
146
+ thumbnailImageUrl: string | null
147
+ thumbnailUploadStatus: 'idle' | 'uploading' | 'staged'
148
+ thumbnailUploadError: string | null
149
+ audioUrl: string
150
+ audioUploadStatus: 'idle' | 'uploading' | 'staged'
151
+ audioUploadError: string | null
152
+ videoUploadStatus: 'idle' | 'uploading' | 'staged'
153
+ videoUploadError: string | null
154
+ videoPipelineStatus: 'idle' | 'queueing' | 'queued'
155
+ videoPipelineError: string | null
156
+ /** The persisted doc id (may start as null for new docs). */
157
+ savedId: string | null
158
+ /** True while an autosave PATCH is in flight. */
159
+ saving: boolean
160
+ /** Error from last save attempt. */
161
+ saveError: string | null
162
+ /** Timestamp of last successful save. */
163
+ lastSavedAt: number | null
164
+ /** Whether the AI assist panel is open. */
165
+ aiOpen: boolean
166
+ }
167
+
168
+ function createEmptyFormState(docId: string | null): FormState {
169
+ return {
170
+ status: 'draft',
171
+ title: '',
172
+ dek: '',
173
+ body: '',
174
+ slug: '',
175
+ byline: null,
176
+ authorId: null,
177
+ authorName: null,
178
+ authorSlug: null,
179
+ publishedAt: null,
180
+ premium: false,
181
+ channel: null,
182
+ heroImageId: '',
183
+ heroImageUrl: null,
184
+ heroUploadStatus: 'idle',
185
+ heroUploadError: null,
186
+ videoUrl: '',
187
+ videoSourceKind: null,
188
+ videoId: null,
189
+ thumbnailImageId: '',
190
+ thumbnailImageUrl: null,
191
+ thumbnailUploadStatus: 'idle',
192
+ thumbnailUploadError: null,
193
+ audioUrl: '',
194
+ audioUploadStatus: 'idle',
195
+ audioUploadError: null,
196
+ videoUploadStatus: 'idle',
197
+ videoUploadError: null,
198
+ videoPipelineStatus: 'idle',
199
+ videoPipelineError: null,
200
+ savedId: docId,
201
+ saving: false,
202
+ saveError: null,
203
+ lastSavedAt: null,
204
+ aiOpen: false,
205
+ }
206
+ }
207
+
208
+ // ---------------------------------------------------------------------------
209
+ // Component
210
+ // ---------------------------------------------------------------------------
211
+
212
+ export function ContentForm({
213
+ docId,
214
+ contentType,
215
+ onDocCreated,
216
+ onTitleChange,
217
+ onDraftChange,
218
+ saveRequest = 0,
219
+ onStatusChange,
220
+ onSaveStateChange,
221
+ }: ContentFormProps) {
222
+ const [form, setForm] = useState<FormState>(() => createEmptyFormState(docId))
223
+
224
+ // Autosave state (mutable refs, not React state — no re-render needed)
225
+ const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
226
+ const savesSinceSnapshot = useRef(0)
227
+ const lastSnapshotAt = useRef(Date.now())
228
+ const queuedVideoSources = useRef<Set<string>>(new Set())
229
+ const lastHandledSaveRequest = useRef(saveRequest)
230
+ const latestForm = useRef(form)
231
+ latestForm.current = form
232
+
233
+ const publishDraft = useCallback(
234
+ (next: FormState) => {
235
+ onTitleChange?.(next.title)
236
+ onDraftChange?.(formToPreviewDraft(next, contentType))
237
+ },
238
+ [contentType, onDraftChange, onTitleChange],
239
+ )
240
+
241
+ const loadExistingContent = useCallback(async () => {
242
+ if (!docId) {
243
+ const next = createEmptyFormState(null)
244
+ setForm(next)
245
+ publishDraft(next)
246
+ return
247
+ }
248
+ try {
249
+ const res = await fetch(`/admin/api/content/${docId}`)
250
+ if (!res.ok) {
251
+ const err = (await res.json().catch(() => ({ error: res.statusText }))) as {
252
+ error?: string
253
+ }
254
+ throw new Error(err.error ?? 'Could not load content')
255
+ }
256
+ const data = (await res.json()) as ExistingContentResponse
257
+ const next = contentResponseToFormState(data, contentType, docId)
258
+ setForm((prev) => ({
259
+ ...next,
260
+ aiOpen: prev.aiOpen,
261
+ }))
262
+ onStatusChange?.(next.status)
263
+ publishDraft(next)
264
+ } catch (err) {
265
+ setForm((prev) => ({
266
+ ...prev,
267
+ savedId: docId,
268
+ saveError: err instanceof Error ? err.message : 'Could not load content',
269
+ }))
270
+ }
271
+ }, [contentType, docId, onStatusChange, publishDraft])
272
+
273
+ // -- Word-count (derived from the body markdown) -------------------------
274
+ const wordCount = countWords(form.body)
275
+ const readTime = estimateReadTime(wordCount)
276
+
277
+ useEffect(() => {
278
+ void loadExistingContent()
279
+ }, [loadExistingContent])
280
+
281
+ // -- Autosave logic -------------------------------------------------------
282
+
283
+ const queueFoundryProcessing = useCallback(
284
+ async (id: string, draft: ContentDraftFields) => {
285
+ const source = foundryDispatchSourceForDraft(contentType, draft)
286
+ if (!source) return
287
+ const queueKey = `${contentType}:${id}:${source}`
288
+ if (queuedVideoSources.current.has(queueKey)) return
289
+
290
+ setForm((prev) => ({
291
+ ...prev,
292
+ videoPipelineStatus: 'queueing',
293
+ videoPipelineError: null,
294
+ }))
295
+ try {
296
+ const res = await fetch(`/admin/api/content/${id}?action=dispatch-to-foundry`, {
297
+ method: 'POST',
298
+ })
299
+ if (!res.ok) {
300
+ const err = (await res.json().catch(() => ({ error: res.statusText }))) as {
301
+ error?: string
302
+ }
303
+ throw new Error(err.error ?? 'Foundry dispatch failed')
304
+ }
305
+ queuedVideoSources.current.add(queueKey)
306
+ setForm((prev) => ({
307
+ ...prev,
308
+ videoPipelineStatus: 'queued',
309
+ videoPipelineError: null,
310
+ }))
311
+ } catch (err) {
312
+ setForm((prev) => ({
313
+ ...prev,
314
+ videoPipelineStatus: 'idle',
315
+ videoPipelineError: err instanceof Error ? err.message : String(err),
316
+ }))
317
+ }
318
+ },
319
+ [contentType],
320
+ )
321
+
322
+ const persist = useCallback(
323
+ async (currentForm: FormState) => {
324
+ const id = currentForm.savedId
325
+ const draft: ContentDraftFields = currentForm
326
+
327
+ if (id === null) {
328
+ if (!canCreateContentDraft(contentType, draft)) {
329
+ setForm((prev) => ({
330
+ ...prev,
331
+ saveError: 'Add the required title and content before saving.',
332
+ }))
333
+ return
334
+ }
335
+ // First save — POST /content to create
336
+ try {
337
+ setForm((prev) => ({ ...prev, saving: true, saveError: null }))
338
+ const res = await fetch('/admin/api/content', {
339
+ method: 'POST',
340
+ headers: { 'content-type': 'application/json' },
341
+ body: JSON.stringify(buildContentCreatePayload(contentType, draft)),
342
+ })
343
+ if (!res.ok) {
344
+ const err = (await res.json().catch(() => ({ error: res.statusText }))) as {
345
+ error?: string
346
+ }
347
+ setForm((prev) => ({
348
+ ...prev,
349
+ saving: false,
350
+ saveError: err.error ?? 'Save failed',
351
+ }))
352
+ return
353
+ }
354
+ const created = (await res.json()) as { id: string }
355
+ setForm((prev) => ({
356
+ ...prev,
357
+ saving: false,
358
+ savedId: created.id,
359
+ lastSavedAt: Date.now(),
360
+ saveError: null,
361
+ }))
362
+ onDocCreated?.(created.id)
363
+ void queueFoundryProcessing(created.id, draft)
364
+ } catch (err) {
365
+ setForm((prev) => ({
366
+ ...prev,
367
+ saving: false,
368
+ saveError: String(err),
369
+ }))
370
+ }
371
+ } else {
372
+ // Subsequent saves — PATCH /content/:id
373
+ try {
374
+ setForm((prev) => ({ ...prev, saving: true, saveError: null }))
375
+ const res = await fetch(`/admin/api/content/${id}`, {
376
+ method: 'PATCH',
377
+ headers: { 'content-type': 'application/json' },
378
+ body: JSON.stringify(buildContentUpdatePayload(contentType, draft)),
379
+ })
380
+ if (!res.ok) {
381
+ const err = (await res.json().catch(() => ({ error: res.statusText }))) as {
382
+ error?: string
383
+ }
384
+ setForm((prev) => ({
385
+ ...prev,
386
+ saving: false,
387
+ saveError: err.error ?? 'Save failed',
388
+ }))
389
+ return
390
+ }
391
+
392
+ // Snapshot cadence check
393
+ const now = Date.now()
394
+ savesSinceSnapshot.current += 1
395
+ const snap = shouldSnapshot({
396
+ savesSinceSnapshot: savesSinceSnapshot.current,
397
+ lastSnapshotAt: lastSnapshotAt.current,
398
+ now,
399
+ })
400
+ if (snap) {
401
+ savesSinceSnapshot.current = 0
402
+ lastSnapshotAt.current = now
403
+ // Fire-and-forget the snapshot action; errors are non-fatal
404
+ void fetch(`/admin/api/content/${id}?action=snapshot`, { method: 'POST' })
405
+ }
406
+
407
+ setForm((prev) => ({
408
+ ...prev,
409
+ saving: false,
410
+ lastSavedAt: now,
411
+ saveError: null,
412
+ }))
413
+ void queueFoundryProcessing(id, draft)
414
+ } catch (err) {
415
+ setForm((prev) => ({
416
+ ...prev,
417
+ saving: false,
418
+ saveError: String(err),
419
+ }))
420
+ }
421
+ }
422
+ },
423
+ [contentType, onDocCreated, queueFoundryProcessing],
424
+ )
425
+
426
+ useEffect(() => {
427
+ if (saveRequest === lastHandledSaveRequest.current) return
428
+ lastHandledSaveRequest.current = saveRequest
429
+ if (debounceTimer.current) {
430
+ clearTimeout(debounceTimer.current)
431
+ debounceTimer.current = null
432
+ }
433
+ void persist(latestForm.current)
434
+ }, [persist, saveRequest])
435
+
436
+ useEffect(() => {
437
+ onSaveStateChange?.({
438
+ saving: form.saving,
439
+ saveError: form.saveError,
440
+ lastSavedAt: form.lastSavedAt,
441
+ savedId: form.savedId,
442
+ })
443
+ }, [form.lastSavedAt, form.saveError, form.savedId, form.saving, onSaveStateChange])
444
+
445
+ // Debounced save — fires after AUTOSAVE_DEBOUNCE_MS of inactivity
446
+ // biome-ignore lint/correctness/useExhaustiveDependencies: recovered live behavior; deps intentionally stable (adding them would re-create the debounced callback and change deployed behavior)
447
+ const scheduleAutosave = useCallback(
448
+ (nextForm: FormState) => {
449
+ if (debounceTimer.current) clearTimeout(debounceTimer.current)
450
+ debounceTimer.current = setTimeout(() => {
451
+ void persist(nextForm)
452
+ }, AUTOSAVE_DEBOUNCE_MS)
453
+ },
454
+ [persist, publishDraft],
455
+ )
456
+
457
+ // Flush autosave on unmount
458
+ useEffect(() => {
459
+ return () => {
460
+ if (debounceTimer.current) {
461
+ clearTimeout(debounceTimer.current)
462
+ }
463
+ }
464
+ }, [])
465
+
466
+ // -- Field change handlers -----------------------------------------------
467
+
468
+ function handleTitleChange(value: string) {
469
+ const next = { ...latestForm.current, title: value }
470
+ setForm(next)
471
+ publishDraft(next)
472
+ scheduleAutosave(next)
473
+ }
474
+
475
+ function handleDekChange(value: string) {
476
+ const next = { ...latestForm.current, dek: value }
477
+ setForm(next)
478
+ publishDraft(next)
479
+ scheduleAutosave(next)
480
+ }
481
+
482
+ function handleBodyChange(md: string) {
483
+ const next = { ...latestForm.current, body: md }
484
+ setForm(next)
485
+ publishDraft(next)
486
+ scheduleAutosave(next)
487
+ }
488
+
489
+ function handleVideoUrlChange(value: string) {
490
+ const next = {
491
+ ...latestForm.current,
492
+ videoUrl: value,
493
+ videoSourceKind: null,
494
+ videoId: null,
495
+ videoPipelineStatus: 'idle' as const,
496
+ videoPipelineError: null,
497
+ }
498
+ setForm(next)
499
+ publishDraft(next)
500
+ scheduleAutosave(next)
501
+ }
502
+
503
+ function handleAudioUrlChange(value: string) {
504
+ const next = { ...latestForm.current, audioUrl: value }
505
+ setForm(next)
506
+ publishDraft(next)
507
+ scheduleAutosave(next)
508
+ }
509
+
510
+ function handleHeroImageIdChange(value: string) {
511
+ const id = value.trim()
512
+ const next = {
513
+ ...latestForm.current,
514
+ heroImageId: value,
515
+ heroImageUrl: id ? mediaUrlFromId(id) : null,
516
+ }
517
+ setForm(next)
518
+ publishDraft(next)
519
+ scheduleAutosave(next)
520
+ }
521
+
522
+ function handleThumbnailImageIdChange(value: string) {
523
+ const id = value.trim()
524
+ const next = {
525
+ ...latestForm.current,
526
+ thumbnailImageId: value,
527
+ thumbnailImageUrl: id ? mediaUrlFromId(id) : null,
528
+ }
529
+ setForm(next)
530
+ publishDraft(next)
531
+ scheduleAutosave(next)
532
+ }
533
+
534
+ const handleEditorImageUpload = useCallback(
535
+ (file: File) => uploadEditorImage(file, latestForm.current.title || 'inline-image'),
536
+ [],
537
+ )
538
+
539
+ const handleVideoFileUpload = useCallback(
540
+ async (file: File) => {
541
+ const baseSlug = slugifyTitle(latestForm.current.title || file.name || 'video')
542
+ setForm((prev) => ({
543
+ ...prev,
544
+ videoUploadStatus: 'uploading',
545
+ videoUploadError: null,
546
+ videoPipelineError: null,
547
+ }))
548
+ try {
549
+ const uploaded = await uploadMediaAsset({
550
+ kind: 'video',
551
+ file,
552
+ slug: baseSlug || undefined,
553
+ })
554
+ if (!uploaded.url) throw new Error('Video upload did not return a source URL')
555
+ const next = {
556
+ ...latestForm.current,
557
+ videoUrl: uploaded.url,
558
+ videoSourceKind: uploaded.sourceKind ?? 'http_url',
559
+ videoId: uploaded.videoId ?? baseSlug,
560
+ videoUploadStatus: 'staged' as const,
561
+ videoUploadError: null,
562
+ videoPipelineStatus: 'idle' as const,
563
+ videoPipelineError: null,
564
+ }
565
+ setForm(next)
566
+ publishDraft(next)
567
+ await persist(next)
568
+ } catch (err) {
569
+ setForm((prev) => ({
570
+ ...prev,
571
+ videoUploadStatus: 'idle',
572
+ videoUploadError: err instanceof Error ? err.message : String(err),
573
+ }))
574
+ }
575
+ },
576
+ [persist, publishDraft],
577
+ )
578
+
579
+ const handleHeroImageUpload = useCallback(
580
+ async (file: File) => {
581
+ const baseSlug = slugifyTitle(latestForm.current.title || file.name || 'hero-image')
582
+ setForm((prev) => ({
583
+ ...prev,
584
+ heroUploadStatus: 'uploading',
585
+ heroUploadError: null,
586
+ }))
587
+ try {
588
+ const uploaded = await uploadMediaAsset({
589
+ kind: 'image',
590
+ file,
591
+ slug: baseSlug || undefined,
592
+ })
593
+ const next = {
594
+ ...latestForm.current,
595
+ heroImageId: uploaded.id,
596
+ heroImageUrl: uploaded.url,
597
+ heroUploadStatus: 'staged' as const,
598
+ heroUploadError: null,
599
+ }
600
+ setForm(next)
601
+ publishDraft(next)
602
+ await persist(next)
603
+ } catch (err) {
604
+ setForm((prev) => ({
605
+ ...prev,
606
+ heroUploadStatus: 'idle',
607
+ heroUploadError: err instanceof Error ? err.message : String(err),
608
+ }))
609
+ }
610
+ },
611
+ [persist, publishDraft],
612
+ )
613
+
614
+ const handleThumbnailImageUpload = useCallback(
615
+ async (file: File) => {
616
+ const baseSlug = slugifyTitle(latestForm.current.title || file.name || 'video-poster')
617
+ setForm((prev) => ({
618
+ ...prev,
619
+ thumbnailUploadStatus: 'uploading',
620
+ thumbnailUploadError: null,
621
+ }))
622
+ try {
623
+ const uploaded = await uploadMediaAsset({
624
+ kind: 'image',
625
+ file,
626
+ slug: baseSlug || undefined,
627
+ })
628
+ const next = {
629
+ ...latestForm.current,
630
+ thumbnailImageId: uploaded.id,
631
+ thumbnailImageUrl: uploaded.url,
632
+ thumbnailUploadStatus: 'staged' as const,
633
+ thumbnailUploadError: null,
634
+ }
635
+ setForm(next)
636
+ publishDraft(next)
637
+ await persist(next)
638
+ } catch (err) {
639
+ setForm((prev) => ({
640
+ ...prev,
641
+ thumbnailUploadStatus: 'idle',
642
+ thumbnailUploadError: err instanceof Error ? err.message : String(err),
643
+ }))
644
+ }
645
+ },
646
+ [persist, publishDraft],
647
+ )
648
+
649
+ // biome-ignore lint/correctness/useExhaustiveDependencies: recovered live behavior; deps intentionally stable (adding them would re-create the callback and change deployed behavior)
650
+ const handleAudioFileUpload = useCallback(
651
+ async (file: File) => {
652
+ const baseSlug = slugifyTitle(latestForm.current.title || file.name || 'podcast')
653
+ setForm((prev) => ({
654
+ ...prev,
655
+ audioUploadStatus: 'uploading',
656
+ audioUploadError: null,
657
+ }))
658
+ try {
659
+ const uploaded = await uploadMediaAsset({
660
+ kind: 'podcast',
661
+ file,
662
+ slug: baseSlug || undefined,
663
+ })
664
+ const next = {
665
+ ...latestForm.current,
666
+ audioUrl: uploaded.id,
667
+ audioUploadStatus: 'staged' as const,
668
+ audioUploadError: null,
669
+ }
670
+ setForm(next)
671
+ publishDraft(next)
672
+ await persist(next)
673
+ } catch (err) {
674
+ setForm((prev) => ({
675
+ ...prev,
676
+ audioUploadStatus: 'idle',
677
+ audioUploadError: err instanceof Error ? err.message : String(err),
678
+ }))
679
+ }
680
+ },
681
+ [persist],
682
+ )
683
+
684
+ // -- Autosaved indicator --------------------------------------------------
685
+
686
+ const autosaveLabel = form.saving
687
+ ? 'Saving…'
688
+ : form.saveError
689
+ ? `Error: ${form.saveError}`
690
+ : form.lastSavedAt
691
+ ? `Saved ${timeSince(form.lastSavedAt)}`
692
+ : 'Unsaved'
693
+
694
+ // -------------------------------------------------------------------------
695
+ // Render
696
+ // -------------------------------------------------------------------------
697
+
698
+ function handleAiToggle() {
699
+ setForm((prev) => ({ ...prev, aiOpen: !prev.aiOpen }))
700
+ }
701
+
702
+ return (
703
+ <div style={formWrap}>
704
+ {/* Autosave indicator + AI assist toggle */}
705
+ <div style={saveIndicator}>
706
+ <span
707
+ className="mono"
708
+ style={{
709
+ color: form.saveError ? 'var(--red)' : 'var(--ink-faint)',
710
+ fontSize: 11.5,
711
+ }}
712
+ >
713
+ {autosaveLabel}
714
+ </span>
715
+ {/* AI assist toggle — disabled until the doc has a saved id */}
716
+ <button
717
+ type="button"
718
+ className="btn sm"
719
+ onClick={handleAiToggle}
720
+ disabled={form.savedId === null}
721
+ title={
722
+ form.savedId === null
723
+ ? 'Save the document first to enable AI assist'
724
+ : form.aiOpen
725
+ ? 'Close AI assist'
726
+ : 'Open AI assist'
727
+ }
728
+ style={{
729
+ borderColor: form.aiOpen ? 'var(--accent)' : 'var(--field-border)',
730
+ color: form.aiOpen ? 'var(--accent)' : 'var(--ink)',
731
+ opacity: form.savedId === null ? 0.45 : 1,
732
+ }}
733
+ aria-pressed={form.aiOpen}
734
+ aria-label="AI assist"
735
+ >
736
+ <Icon name="sparkle" size={15} />
737
+ AI assist
738
+ </button>
739
+ </div>
740
+
741
+ {/* AI assist panel — shown when toggle is on and doc is saved */}
742
+ {form.aiOpen && form.savedId !== null && <AiAssistPanel docId={form.savedId} />}
743
+
744
+ {/* Title */}
745
+ <div style={fieldGroup}>
746
+ <input
747
+ type="text"
748
+ placeholder="Title"
749
+ value={form.title}
750
+ onChange={(e) => handleTitleChange(e.currentTarget.value)}
751
+ style={titleInput}
752
+ aria-label="Content title"
753
+ />
754
+ </div>
755
+
756
+ {/* Summary / subheadline */}
757
+ <div style={fieldGroup}>
758
+ <textarea
759
+ placeholder="Add a short summary or subheadline (optional)…"
760
+ value={form.dek}
761
+ onChange={(e) => handleDekChange(e.currentTarget.value)}
762
+ rows={2}
763
+ style={dekInput}
764
+ aria-label="Short summary or subheadline"
765
+ />
766
+ </div>
767
+
768
+ {/* Signature masthead hairline between the standfirst and the body */}
769
+ <hr className="masthead-rule" style={{ margin: '6px 0 20px' }} />
770
+
771
+ {/* Type-specific body block */}
772
+ <div style={fieldGroup}>
773
+ {contentType === 'video' ? (
774
+ <div style={mediaBlockStack}>
775
+ <VideoBlock
776
+ videoUrl={form.videoUrl}
777
+ onVideoUrlChange={handleVideoUrlChange}
778
+ onVideoFileUpload={handleVideoFileUpload}
779
+ uploadStatus={form.videoUploadStatus}
780
+ uploadError={form.videoUploadError}
781
+ pipelineStatus={form.videoPipelineStatus}
782
+ pipelineError={form.videoPipelineError}
783
+ />
784
+ <ImageAssetBlock
785
+ title="Video Poster Image"
786
+ description="Shown on the video page, video index, social previews, and locked-player preview."
787
+ uploadLabel="Upload Poster Image"
788
+ idLabel="Poster Image ID"
789
+ idHelp="Use this only when the poster image already exists in R2."
790
+ emptyText="No poster image attached"
791
+ successText="Poster image uploaded."
792
+ imageId={form.thumbnailImageId}
793
+ previewUrl={form.thumbnailImageUrl}
794
+ uploadStatus={form.thumbnailUploadStatus}
795
+ uploadError={form.thumbnailUploadError}
796
+ onImageIdChange={handleThumbnailImageIdChange}
797
+ onFileUpload={handleThumbnailImageUpload}
798
+ />
799
+ </div>
800
+ ) : contentType === 'podcast' ? (
801
+ <div style={mediaBlockStack}>
802
+ <AudioBlock
803
+ audioUrl={form.audioUrl}
804
+ onAudioUrlChange={handleAudioUrlChange}
805
+ onAudioFileUpload={handleAudioFileUpload}
806
+ uploadStatus={form.audioUploadStatus}
807
+ uploadError={form.audioUploadError}
808
+ pipelineStatus={form.videoPipelineStatus}
809
+ pipelineError={form.videoPipelineError}
810
+ />
811
+ <ImageAssetBlock
812
+ title="Podcast Hero Image"
813
+ description="Upload the episode artwork or hero image here. This fills the Hero Image ID automatically."
814
+ uploadLabel="Upload Podcast Image"
815
+ idLabel="Hero Image ID"
816
+ idHelp="Use this only when the podcast image already exists in R2."
817
+ emptyText="No podcast image attached"
818
+ successText="Podcast image uploaded."
819
+ imageId={form.heroImageId}
820
+ previewUrl={form.heroImageUrl}
821
+ uploadStatus={form.heroUploadStatus}
822
+ uploadError={form.heroUploadError}
823
+ onImageIdChange={handleHeroImageIdChange}
824
+ onFileUpload={handleHeroImageUpload}
825
+ />
826
+ </div>
827
+ ) : (
828
+ /* article | newsletter */
829
+ <>
830
+ {contentType === 'article' && (
831
+ <div style={{ marginBottom: 14 }}>
832
+ <ImageAssetBlock
833
+ title="Article Hero Image"
834
+ description="Upload the downloaded hero image here. This fills the Hero Image ID automatically."
835
+ uploadLabel="Upload Hero Image"
836
+ idLabel="Hero Image ID"
837
+ idHelp="Use this only when the hero image already exists in R2."
838
+ emptyText="No hero image attached"
839
+ successText="Hero image uploaded."
840
+ imageId={form.heroImageId}
841
+ previewUrl={form.heroImageUrl}
842
+ uploadStatus={form.heroUploadStatus}
843
+ uploadError={form.heroUploadError}
844
+ onImageIdChange={handleHeroImageIdChange}
845
+ onFileUpload={handleHeroImageUpload}
846
+ />
847
+ </div>
848
+ )}
849
+ <Rte
850
+ value={form.body}
851
+ onChange={handleBodyChange}
852
+ onImageUpload={handleEditorImageUpload}
853
+ />
854
+ </>
855
+ )}
856
+ </div>
857
+
858
+ {/* Footer: word count + read time (only for article/newsletter) */}
859
+ {(contentType === 'article' || contentType === 'newsletter') && (
860
+ <div style={footer}>
861
+ <Icon name="edit" size={12} style={{ color: 'var(--ink-faint)' }} />
862
+ <span>
863
+ {wordCount.toLocaleString()} {wordCount === 1 ? 'word' : 'words'}
864
+ </span>
865
+ <span style={{ color: 'var(--ink-faint)' }}>·</span>
866
+ <span>{readTime} min read</span>
867
+ </div>
868
+ )}
869
+ </div>
870
+ )
871
+ }
872
+
873
+ // ---------------------------------------------------------------------------
874
+ // Sub-components — media blocks
875
+ // ---------------------------------------------------------------------------
876
+
877
+ function ImageAssetBlock({
878
+ title,
879
+ description,
880
+ uploadLabel,
881
+ idLabel,
882
+ idHelp,
883
+ emptyText,
884
+ successText,
885
+ imageId,
886
+ previewUrl,
887
+ uploadStatus,
888
+ uploadError,
889
+ onImageIdChange,
890
+ onFileUpload,
891
+ }: {
892
+ title: string
893
+ description: string
894
+ uploadLabel: string
895
+ idLabel: string
896
+ idHelp: string
897
+ emptyText: string
898
+ successText: string
899
+ imageId: string
900
+ previewUrl: string | null
901
+ uploadStatus: 'idle' | 'uploading' | 'staged'
902
+ uploadError: string | null
903
+ onImageIdChange: (imageId: string) => void
904
+ onFileUpload: (file: File) => void | Promise<void>
905
+ }) {
906
+ const inputId = `${title.toLowerCase().replace(/[^a-z0-9]+/g, '-')}-id`
907
+ const statusText =
908
+ uploadStatus === 'uploading'
909
+ ? 'Uploading image…'
910
+ : uploadStatus === 'staged'
911
+ ? successText
912
+ : null
913
+
914
+ return (
915
+ <div style={mediaBlock}>
916
+ <div style={mediaBlockHeader}>
917
+ <Icon name="image" size={14} style={{ color: 'var(--ink-muted)' }} />
918
+ <span style={{ fontSize: 12, fontWeight: 500, color: 'var(--ink-soft)' }}>{title}</span>
919
+ </div>
920
+
921
+ <p style={assetHelp}>{description}</p>
922
+
923
+ <div style={assetPreview}>
924
+ {previewUrl ? (
925
+ <img src={previewUrl} alt={`${title} preview`} style={assetPreviewImage} />
926
+ ) : imageId ? (
927
+ <span style={{ fontSize: 11, color: 'var(--ink-muted)', wordBreak: 'break-all' }}>
928
+ {imageId}
929
+ </span>
930
+ ) : (
931
+ <span style={{ fontSize: 12, color: 'var(--ink-faint)', fontStyle: 'italic' }}>
932
+ {emptyText}
933
+ </span>
934
+ )}
935
+ </div>
936
+
937
+ <div style={videoActions}>
938
+ <label className="btn sm" style={uploadButton}>
939
+ <Icon name="upload" size={14} />
940
+ {uploadStatus === 'uploading' ? 'Uploading…' : uploadLabel}
941
+ <input
942
+ type="file"
943
+ accept="image/*,.jpg,.jpeg,.png,.webp"
944
+ disabled={uploadStatus === 'uploading'}
945
+ onChange={(event) => {
946
+ const file = event.currentTarget.files?.[0]
947
+ event.currentTarget.value = ''
948
+ if (file) void onFileUpload(file)
949
+ }}
950
+ style={hiddenFileInput}
951
+ aria-label={uploadLabel}
952
+ />
953
+ </label>
954
+ </div>
955
+
956
+ <label htmlFor={inputId} style={assetLabel}>
957
+ {idLabel}
958
+ </label>
959
+ <input
960
+ id={inputId}
961
+ type="text"
962
+ placeholder="images/articles/example.jpg"
963
+ value={imageId}
964
+ onChange={(e) => onImageIdChange(e.currentTarget.value)}
965
+ style={urlInput}
966
+ aria-label={idLabel}
967
+ />
968
+ <div style={assetHelp}>{idHelp}</div>
969
+
970
+ {statusText && <div style={videoStatusText}>{statusText}</div>}
971
+ {uploadError && <div style={videoErrorText}>{uploadError}</div>}
972
+ </div>
973
+ )
974
+ }
975
+
976
+ function VideoBlock({
977
+ videoUrl,
978
+ onVideoUrlChange,
979
+ onVideoFileUpload,
980
+ uploadStatus,
981
+ uploadError,
982
+ pipelineStatus,
983
+ pipelineError,
984
+ }: {
985
+ videoUrl: string
986
+ onVideoUrlChange: (url: string) => void
987
+ onVideoFileUpload: (file: File) => void | Promise<void>
988
+ uploadStatus: 'idle' | 'uploading' | 'staged'
989
+ uploadError: string | null
990
+ pipelineStatus: 'idle' | 'queueing' | 'queued'
991
+ pipelineError: string | null
992
+ }) {
993
+ const statusText =
994
+ pipelineStatus === 'queued'
995
+ ? 'Queued. Most videos finish in 20-45 minutes; long files can take up to 90.'
996
+ : pipelineStatus === 'queueing'
997
+ ? 'Queueing processing…'
998
+ : uploadStatus === 'uploading'
999
+ ? 'Uploading source…'
1000
+ : uploadStatus === 'staged'
1001
+ ? 'Source uploaded. Saving queues processing.'
1002
+ : null
1003
+
1004
+ return (
1005
+ <div style={mediaBlock}>
1006
+ <div style={mediaBlockHeader}>
1007
+ <Icon name="video" size={14} style={{ color: 'var(--ink-muted)' }} />
1008
+ <span style={{ fontSize: 12, fontWeight: 500, color: 'var(--ink-soft)' }}>Video</span>
1009
+ </div>
1010
+
1011
+ <div
1012
+ style={{
1013
+ aspectRatio: '16/9',
1014
+ background: 'var(--hover)',
1015
+ borderRadius: 6,
1016
+ display: 'flex',
1017
+ alignItems: 'center',
1018
+ justifyContent: 'center',
1019
+ marginBottom: 12,
1020
+ }}
1021
+ >
1022
+ {videoUrl ? (
1023
+ <span style={{ fontSize: 11, color: 'var(--ink-muted)', wordBreak: 'break-all' }}>
1024
+ {videoUrl}
1025
+ </span>
1026
+ ) : (
1027
+ <span style={{ fontSize: 12, color: 'var(--ink-faint)', fontStyle: 'italic' }}>
1028
+ No video attached
1029
+ </span>
1030
+ )}
1031
+ </div>
1032
+
1033
+ <input
1034
+ type="url"
1035
+ placeholder="Video source URL or R2 key…"
1036
+ value={videoUrl}
1037
+ onChange={(e) => onVideoUrlChange(e.currentTarget.value)}
1038
+ style={urlInput}
1039
+ aria-label="Video source URL"
1040
+ />
1041
+
1042
+ <div style={videoActions}>
1043
+ <label className="btn sm" style={uploadButton}>
1044
+ <Icon name="upload" size={14} />
1045
+ {uploadStatus === 'uploading' ? 'Uploading…' : 'Upload source'}
1046
+ <input
1047
+ type="file"
1048
+ accept="video/*,.mp4,.mov,.m4v,.webm"
1049
+ disabled={uploadStatus === 'uploading'}
1050
+ onChange={(event) => {
1051
+ const file = event.currentTarget.files?.[0]
1052
+ event.currentTarget.value = ''
1053
+ if (file) void onVideoFileUpload(file)
1054
+ }}
1055
+ style={hiddenFileInput}
1056
+ aria-label="Upload video source"
1057
+ />
1058
+ </label>
1059
+ </div>
1060
+
1061
+ {statusText && <div style={videoStatusText}>{statusText}</div>}
1062
+ {(uploadError || pipelineError) && (
1063
+ <div style={videoErrorText}>{uploadError || pipelineError}</div>
1064
+ )}
1065
+
1066
+ <div style={{ marginTop: 6, fontSize: 11, color: 'var(--ink-faint)' }}>
1067
+ Upload a source file or paste a public source URL; saving queues the Foundry pipeline.
1068
+ </div>
1069
+ </div>
1070
+ )
1071
+ }
1072
+
1073
+ function AudioBlock({
1074
+ audioUrl,
1075
+ onAudioUrlChange,
1076
+ onAudioFileUpload,
1077
+ uploadStatus,
1078
+ uploadError,
1079
+ pipelineStatus,
1080
+ pipelineError,
1081
+ }: {
1082
+ audioUrl: string
1083
+ onAudioUrlChange: (url: string) => void
1084
+ onAudioFileUpload: (file: File) => void | Promise<void>
1085
+ uploadStatus: 'idle' | 'uploading' | 'staged'
1086
+ uploadError: string | null
1087
+ pipelineStatus: 'idle' | 'queueing' | 'queued'
1088
+ pipelineError: string | null
1089
+ }) {
1090
+ const statusText =
1091
+ pipelineStatus === 'queued'
1092
+ ? 'Queued for transcription. Most episodes finish in 10-30 minutes; long files can take up to 60.'
1093
+ : pipelineStatus === 'queueing'
1094
+ ? 'Queueing transcription...'
1095
+ : uploadStatus === 'uploading'
1096
+ ? PODCAST_UPLOAD_HELP
1097
+ : uploadStatus === 'staged'
1098
+ ? 'Audio uploaded. Save or publish to start transcription and episode prep.'
1099
+ : null
1100
+
1101
+ return (
1102
+ <div style={mediaBlock}>
1103
+ <div style={mediaBlockHeader}>
1104
+ <Icon name="mic" size={14} style={{ color: 'var(--ink-muted)' }} />
1105
+ <span style={{ fontSize: 12, fontWeight: 500, color: 'var(--ink-soft)' }}>
1106
+ Podcast Audio File
1107
+ </span>
1108
+ </div>
1109
+
1110
+ <p style={assetHelp}>
1111
+ Upload the downloaded MP3, WAV, or M4A file. The Audio R2 Key fills automatically after
1112
+ upload.
1113
+ </p>
1114
+ <p style={assetHelp}>{PODCAST_UPLOAD_HELP}</p>
1115
+ <p style={assetHelp}>
1116
+ {PODCAST_PIPELINE_HELP} {PODCAST_PIPELINE_ESTIMATE}
1117
+ </p>
1118
+
1119
+ <div
1120
+ style={{
1121
+ height: 56,
1122
+ background: 'var(--hover)',
1123
+ borderRadius: 6,
1124
+ display: 'flex',
1125
+ alignItems: 'center',
1126
+ justifyContent: 'center',
1127
+ marginBottom: 12,
1128
+ }}
1129
+ >
1130
+ {audioUrl ? (
1131
+ <span style={{ fontSize: 11, color: 'var(--ink-muted)', wordBreak: 'break-all' }}>
1132
+ {audioUrl}
1133
+ </span>
1134
+ ) : (
1135
+ <span style={{ fontSize: 12, color: 'var(--ink-faint)', fontStyle: 'italic' }}>
1136
+ No audio attached
1137
+ </span>
1138
+ )}
1139
+ </div>
1140
+
1141
+ <div style={videoActions}>
1142
+ <label className="btn sm" style={uploadButton}>
1143
+ <Icon name="upload" size={14} />
1144
+ {uploadStatus === 'uploading' ? 'Uploading…' : 'Upload Podcast Audio File'}
1145
+ <input
1146
+ type="file"
1147
+ accept="audio/*,.mp3,.m4a,.m4b,.aac,.wav,.ogg,.oga,.opus,.flac"
1148
+ disabled={uploadStatus === 'uploading'}
1149
+ onChange={(event) => {
1150
+ const file = event.currentTarget.files?.[0]
1151
+ event.currentTarget.value = ''
1152
+ if (file) void onAudioFileUpload(file)
1153
+ }}
1154
+ style={hiddenFileInput}
1155
+ aria-label="Upload podcast audio file"
1156
+ />
1157
+ </label>
1158
+ </div>
1159
+
1160
+ <label htmlFor="podcast-audio-r2-key" style={assetLabel}>
1161
+ Audio R2 Key (filled after upload)
1162
+ </label>
1163
+ <input
1164
+ id="podcast-audio-r2-key"
1165
+ type="text"
1166
+ placeholder="audio/example-episode.mp3"
1167
+ value={audioUrl}
1168
+ onChange={(e) => onAudioUrlChange(e.currentTarget.value)}
1169
+ style={urlInput}
1170
+ aria-label="Audio R2 Key"
1171
+ />
1172
+
1173
+ <div style={assetHelp}>Only paste a key manually if the audio already exists in R2.</div>
1174
+
1175
+ {statusText && <div style={videoStatusText}>{statusText}</div>}
1176
+ {(uploadError || pipelineError) && (
1177
+ <div style={videoErrorText}>{uploadError || pipelineError}</div>
1178
+ )}
1179
+ </div>
1180
+ )
1181
+ }
1182
+
1183
+ const PODCAST_UPLOAD_HELP =
1184
+ 'Large audio files can take a few minutes to upload; keep this tab open until the Audio R2 Key appears.'
1185
+
1186
+ const PODCAST_PIPELINE_HELP =
1187
+ 'After upload, save or publish sends the episode through the pipeline for transcription and episode prep.'
1188
+
1189
+ const PODCAST_PIPELINE_ESTIMATE =
1190
+ 'Most episodes finish in 10-30 minutes; long files can take up to 60.'
1191
+
1192
+ function contentResponseToFormState(
1193
+ data: ExistingContentResponse,
1194
+ contentType: ContentType,
1195
+ docId: string,
1196
+ ): FormState {
1197
+ const item = data.item ?? {}
1198
+ const content = data.content ?? {}
1199
+ const heroImageId = toStringValue(item.hero_image_id)
1200
+ const thumbnailImageId = toStringValue(content.thumbnail_image_id)
1201
+ const heroImageUrl = toStringValue(item.hero_image_url)
1202
+ const thumbnailImageUrl = toStringValue(content.thumbnail_image_url)
1203
+ const next = createEmptyFormState(docId)
1204
+
1205
+ next.status = toContentStatus(item.status)
1206
+ next.title = toStringValue(item.title)
1207
+ next.dek = toStringValue(item.excerpt) || toStringValue(content.subtitle)
1208
+ next.slug = toStringValue(item.slug)
1209
+ next.byline = toNullableString(item.byline)
1210
+ next.authorId = toNullableString(item.author_id)
1211
+ next.authorName = toNullableString(item.author_name)
1212
+ next.authorSlug = toNullableString(item.author_slug)
1213
+ next.publishedAt = unixSecondsToIso(item.published_at)
1214
+ next.premium = item.visibility === 'premium'
1215
+ next.channel = toNullableString(item.channel)
1216
+ next.heroImageId = heroImageId
1217
+ next.heroImageUrl = heroImageUrl || (heroImageId ? mediaUrlFromId(heroImageId) : null)
1218
+
1219
+ if (contentType === 'article' || contentType === 'newsletter') {
1220
+ next.body = toStringValue(content.body_markdown) || toStringValue(content.body_html)
1221
+ }
1222
+
1223
+ if (contentType === 'video') {
1224
+ next.body = toStringValue(content.script)
1225
+ next.videoUrl = toStringValue(content.processing_source_url)
1226
+ next.videoSourceKind = toNullableString(content.processing_source_kind)
1227
+ next.videoId = toNullableString(content.video_id)
1228
+ next.thumbnailImageId = thumbnailImageId
1229
+ next.thumbnailImageUrl =
1230
+ thumbnailImageUrl || (thumbnailImageId ? mediaUrlFromId(thumbnailImageId) : null)
1231
+ }
1232
+
1233
+ if (contentType === 'podcast') {
1234
+ next.body = toStringValue(content.transcript)
1235
+ next.audioUrl = toStringValue(content.audio_r2_key)
1236
+ }
1237
+
1238
+ return next
1239
+ }
1240
+
1241
+ function formToPreviewDraft(form: FormState, contentType: ContentType): ContentPreviewDraft {
1242
+ return {
1243
+ id: form.savedId ?? 'draft',
1244
+ type: contentType,
1245
+ status: form.status,
1246
+ title: form.title,
1247
+ slug: form.slug,
1248
+ excerpt: form.dek || null,
1249
+ byline: form.byline,
1250
+ authorId: form.authorId,
1251
+ authorName: form.authorName,
1252
+ authorSlug: form.authorSlug,
1253
+ publishedAt: form.publishedAt,
1254
+ body: form.body || null,
1255
+ premium: form.premium,
1256
+ heroImageUrl: form.heroImageUrl,
1257
+ channel: form.channel,
1258
+ }
1259
+ }
1260
+
1261
+ function mediaUrlFromId(id: string): string {
1262
+ return `/admin/api/media/${encodeURIComponent(id)}`
1263
+ }
1264
+
1265
+ function toStringValue(value: string | null | undefined): string {
1266
+ return typeof value === 'string' ? value : ''
1267
+ }
1268
+
1269
+ function toNullableString(value: string | null | undefined): string | null {
1270
+ const normalized = toStringValue(value).trim()
1271
+ return normalized ? normalized : null
1272
+ }
1273
+
1274
+ function toContentStatus(value: string | null | undefined): ContentStatus {
1275
+ const allowed: ContentStatus[] = ['draft', 'review', 'scheduled', 'published', 'archived']
1276
+ return allowed.includes(value as ContentStatus) ? (value as ContentStatus) : 'draft'
1277
+ }
1278
+
1279
+ function unixSecondsToIso(value: number | null | undefined): string | null {
1280
+ if (typeof value !== 'number' || !Number.isFinite(value)) return null
1281
+ return new Date(value * 1000).toISOString()
1282
+ }
1283
+
1284
+ // ---------------------------------------------------------------------------
1285
+ // Relative-time helper
1286
+ // ---------------------------------------------------------------------------
1287
+
1288
+ function timeSince(ts: number): string {
1289
+ const diff = Date.now() - ts
1290
+ if (diff < 10_000) return 'just now'
1291
+ if (diff < 60_000) return `${Math.floor(diff / 1000)}s ago`
1292
+ if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`
1293
+ return `${Math.floor(diff / 3_600_000)}h ago`
1294
+ }
1295
+
1296
+ // ---------------------------------------------------------------------------
1297
+ // Styles
1298
+ // ---------------------------------------------------------------------------
1299
+
1300
+ const formWrap: React.CSSProperties = {
1301
+ display: 'flex',
1302
+ flexDirection: 'column',
1303
+ gap: 0,
1304
+ padding: '16px 24px',
1305
+ flex: 1,
1306
+ overflowY: 'auto',
1307
+ }
1308
+
1309
+ const saveIndicator: React.CSSProperties = {
1310
+ display: 'flex',
1311
+ justifyContent: 'flex-end',
1312
+ alignItems: 'center',
1313
+ gap: 8,
1314
+ marginBottom: 4,
1315
+ minHeight: 16,
1316
+ }
1317
+
1318
+ const fieldGroup: React.CSSProperties = {
1319
+ marginBottom: 14,
1320
+ }
1321
+
1322
+ const titleInput: React.CSSProperties = {
1323
+ width: '100%',
1324
+ border: 0,
1325
+ background: 'transparent',
1326
+ fontFamily: 'var(--serif)',
1327
+ fontWeight: 600,
1328
+ fontSize: 36,
1329
+ letterSpacing: '-0.025em',
1330
+ lineHeight: 1.08,
1331
+ color: 'var(--ink)',
1332
+ padding: 0,
1333
+ outline: 'none',
1334
+ }
1335
+
1336
+ const dekInput: React.CSSProperties = {
1337
+ width: '100%',
1338
+ border: 0,
1339
+ background: 'transparent',
1340
+ fontFamily: 'var(--serif)',
1341
+ fontSize: 19,
1342
+ lineHeight: 1.5,
1343
+ color: 'var(--ink-soft)',
1344
+ padding: 0,
1345
+ outline: 'none',
1346
+ resize: 'none',
1347
+ }
1348
+
1349
+ const footer: React.CSSProperties = {
1350
+ display: 'flex',
1351
+ alignItems: 'center',
1352
+ gap: 16,
1353
+ paddingTop: 12,
1354
+ fontFamily: 'var(--mono)',
1355
+ fontSize: 11.5,
1356
+ color: 'var(--ink-muted)',
1357
+ borderTop: '1px solid var(--border-soft)',
1358
+ }
1359
+
1360
+ const mediaBlockStack: React.CSSProperties = {
1361
+ display: 'flex',
1362
+ flexDirection: 'column',
1363
+ gap: 14,
1364
+ }
1365
+
1366
+ const mediaBlock: React.CSSProperties = {
1367
+ border: '1px solid var(--field-border)',
1368
+ borderRadius: 8,
1369
+ padding: 14,
1370
+ background: 'var(--field)',
1371
+ }
1372
+
1373
+ const mediaBlockHeader: React.CSSProperties = {
1374
+ display: 'flex',
1375
+ alignItems: 'center',
1376
+ gap: 6,
1377
+ marginBottom: 10,
1378
+ }
1379
+
1380
+ const assetPreview: React.CSSProperties = {
1381
+ height: 126,
1382
+ background: 'var(--hover)',
1383
+ borderRadius: 6,
1384
+ display: 'flex',
1385
+ alignItems: 'center',
1386
+ justifyContent: 'center',
1387
+ marginBottom: 12,
1388
+ overflow: 'hidden',
1389
+ }
1390
+
1391
+ const assetPreviewImage: React.CSSProperties = {
1392
+ width: '100%',
1393
+ height: '100%',
1394
+ objectFit: 'cover',
1395
+ display: 'block',
1396
+ }
1397
+
1398
+ const assetLabel: React.CSSProperties = {
1399
+ display: 'block',
1400
+ marginTop: 12,
1401
+ marginBottom: 6,
1402
+ fontSize: 11,
1403
+ fontWeight: 600,
1404
+ color: 'var(--ink-soft)',
1405
+ }
1406
+
1407
+ const assetHelp: React.CSSProperties = {
1408
+ margin: '6px 0 10px',
1409
+ fontSize: 11,
1410
+ lineHeight: 1.45,
1411
+ color: 'var(--ink-faint)',
1412
+ }
1413
+
1414
+ const urlInput: React.CSSProperties = {
1415
+ width: '100%',
1416
+ height: 32,
1417
+ padding: '0 10px',
1418
+ border: '1px solid var(--field-border)',
1419
+ borderRadius: 6,
1420
+ background: 'var(--bg)',
1421
+ color: 'var(--ink)',
1422
+ fontSize: 12,
1423
+ outline: 'none',
1424
+ }
1425
+
1426
+ const videoActions: React.CSSProperties = {
1427
+ display: 'flex',
1428
+ alignItems: 'center',
1429
+ gap: 8,
1430
+ marginTop: 10,
1431
+ }
1432
+
1433
+ const uploadButton: React.CSSProperties = {
1434
+ position: 'relative',
1435
+ overflow: 'hidden',
1436
+ cursor: 'pointer',
1437
+ }
1438
+
1439
+ const hiddenFileInput: React.CSSProperties = {
1440
+ position: 'absolute',
1441
+ inset: 0,
1442
+ opacity: 0,
1443
+ cursor: 'pointer',
1444
+ }
1445
+
1446
+ const videoStatusText: React.CSSProperties = {
1447
+ marginTop: 8,
1448
+ fontSize: 11,
1449
+ lineHeight: 1.4,
1450
+ color: 'var(--ink-muted)',
1451
+ }
1452
+
1453
+ const videoErrorText: React.CSSProperties = {
1454
+ marginTop: 8,
1455
+ fontSize: 11,
1456
+ lineHeight: 1.4,
1457
+ color: 'var(--red)',
1458
+ }
1459
+
1460
+ // Re-export for callers that need the markdown body value from the form
1461
+ // (EditorScreen uses this to feed the live-preview pane).
1462
+ export { docToMarkdown }