qarpeo 0.0.0-alpha.10

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 (599) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +102 -0
  3. package/dist/chunks/auth-schema.mjs +219 -0
  4. package/dist/chunks/wrangler.mjs +783 -0
  5. package/dist/cli/index.mjs +1085 -0
  6. package/dist/dev/env.d.ts +12 -0
  7. package/dist/dev/wrangler.d.ts +18 -0
  8. package/dist/module.d.ts +16 -0
  9. package/dist/module.mjs +4436 -0
  10. package/dist/runtime/ai.d.ts +25 -0
  11. package/dist/runtime/ai.mjs +106 -0
  12. package/dist/runtime/analytics.d.ts +24 -0
  13. package/dist/runtime/analytics.mjs +38 -0
  14. package/dist/runtime/app/components/BlobImage.vue +100 -0
  15. package/dist/runtime/app/components/CMSBlocks.vue +93 -0
  16. package/dist/runtime/app/components/CMSContent.vue +23 -0
  17. package/dist/runtime/app/components/CMSImage.vue +49 -0
  18. package/dist/runtime/app/components/CMSLocaleSwitcher.vue +110 -0
  19. package/dist/runtime/app/components/CMSPreviewBanner.vue +79 -0
  20. package/dist/runtime/app/components/CMSVersionBanner.vue +49 -0
  21. package/dist/runtime/app/components/CMSVersionSwitcher.vue +126 -0
  22. package/dist/runtime/app/components/ContentRenderer.vue +80 -0
  23. package/dist/runtime/app/components/FileUpload.vue +196 -0
  24. package/dist/runtime/app/components/ImpersonationBanner.vue +57 -0
  25. package/dist/runtime/app/components/SignedIn.vue +13 -0
  26. package/dist/runtime/app/components/SignedOut.vue +13 -0
  27. package/dist/runtime/app/components/shiki.css +34 -0
  28. package/dist/runtime/app/composables/analytics.d.ts +125 -0
  29. package/dist/runtime/app/composables/analytics.mjs +81 -0
  30. package/dist/runtime/app/composables/api-tokens.d.ts +49 -0
  31. package/dist/runtime/app/composables/api-tokens.mjs +61 -0
  32. package/dist/runtime/app/composables/auth.d.ts +50 -0
  33. package/dist/runtime/app/composables/auth.mjs +101 -0
  34. package/dist/runtime/app/composables/chat.d.ts +69 -0
  35. package/dist/runtime/app/composables/chat.mjs +159 -0
  36. package/dist/runtime/app/composables/cms-preview.d.ts +33 -0
  37. package/dist/runtime/app/composables/cms-preview.mjs +83 -0
  38. package/dist/runtime/app/composables/cms.d.ts +307 -0
  39. package/dist/runtime/app/composables/cms.mjs +355 -0
  40. package/dist/runtime/app/composables/docs-version.d.ts +35 -0
  41. package/dist/runtime/app/composables/docs-version.mjs +70 -0
  42. package/dist/runtime/app/composables/impersonation.d.ts +14 -0
  43. package/dist/runtime/app/composables/impersonation.mjs +44 -0
  44. package/dist/runtime/app/composables/notifications.d.ts +35 -0
  45. package/dist/runtime/app/composables/notifications.mjs +139 -0
  46. package/dist/runtime/app/composables/organization.d.ts +127 -0
  47. package/dist/runtime/app/composables/organization.mjs +138 -0
  48. package/dist/runtime/app/composables/passkey.d.ts +6 -0
  49. package/dist/runtime/app/composables/passkey.mjs +105 -0
  50. package/dist/runtime/app/composables/passwordless.d.ts +7 -0
  51. package/dist/runtime/app/composables/passwordless.mjs +67 -0
  52. package/dist/runtime/app/composables/payments.d.ts +43 -0
  53. package/dist/runtime/app/composables/payments.mjs +74 -0
  54. package/dist/runtime/app/composables/realtime.d.ts +96 -0
  55. package/dist/runtime/app/composables/realtime.mjs +432 -0
  56. package/dist/runtime/app/composables/site.d.ts +35 -0
  57. package/dist/runtime/app/composables/site.mjs +36 -0
  58. package/dist/runtime/app/composables/storage.d.ts +60 -0
  59. package/dist/runtime/app/composables/storage.mjs +294 -0
  60. package/dist/runtime/app/composables/tenant.d.ts +82 -0
  61. package/dist/runtime/app/composables/tenant.mjs +120 -0
  62. package/dist/runtime/app/composables/turnstile.d.ts +3 -0
  63. package/dist/runtime/app/composables/turnstile.mjs +10 -0
  64. package/dist/runtime/app/composables/two-factor.d.ts +12 -0
  65. package/dist/runtime/app/composables/two-factor.mjs +65 -0
  66. package/dist/runtime/app/middleware/auth.global.d.ts +2 -0
  67. package/dist/runtime/app/middleware/auth.global.mjs +44 -0
  68. package/dist/runtime/app/pages/cms.vue +2249 -0
  69. package/dist/runtime/app/pages/devtools/agents.vue +717 -0
  70. package/dist/runtime/app/pages/devtools/analytics.vue +618 -0
  71. package/dist/runtime/app/pages/devtools/auth.vue +351 -0
  72. package/dist/runtime/app/pages/devtools/blob.vue +745 -0
  73. package/dist/runtime/app/pages/devtools/db.vue +546 -0
  74. package/dist/runtime/app/pages/devtools/domains.vue +772 -0
  75. package/dist/runtime/app/pages/devtools/kv.vue +829 -0
  76. package/dist/runtime/app/pages/devtools/logs.vue +655 -0
  77. package/dist/runtime/app/pages/devtools/mail.vue +389 -0
  78. package/dist/runtime/app/pages/devtools/overview.vue +390 -0
  79. package/dist/runtime/app/pages/devtools/settings.vue +560 -0
  80. package/dist/runtime/app/pages/devtools.vue +165 -0
  81. package/dist/runtime/app/plugins/seo.d.ts +2 -0
  82. package/dist/runtime/app/plugins/seo.mjs +105 -0
  83. package/dist/runtime/app/plugins/shortcuts.client.d.ts +6 -0
  84. package/dist/runtime/app/plugins/shortcuts.client.mjs +19 -0
  85. package/dist/runtime/app/public/sw-notifications.mjs +52 -0
  86. package/dist/runtime/auth.d.ts +25 -0
  87. package/dist/runtime/auth.mjs +11 -0
  88. package/dist/runtime/bindings.d.ts +27 -0
  89. package/dist/runtime/bindings.mjs +38 -0
  90. package/dist/runtime/blob.d.ts +26 -0
  91. package/dist/runtime/blob.mjs +334 -0
  92. package/dist/runtime/bots.d.ts +1 -0
  93. package/dist/runtime/bots.mjs +1 -0
  94. package/dist/runtime/browser.d.ts +21 -0
  95. package/dist/runtime/browser.mjs +64 -0
  96. package/dist/runtime/cache.d.ts +19 -0
  97. package/dist/runtime/cache.mjs +12 -0
  98. package/dist/runtime/cms.d.ts +2 -0
  99. package/dist/runtime/cms.mjs +0 -0
  100. package/dist/runtime/db.d.ts +21 -0
  101. package/dist/runtime/db.mjs +36 -0
  102. package/dist/runtime/drizzle.d.ts +11 -0
  103. package/dist/runtime/drizzle.mjs +18 -0
  104. package/dist/runtime/email.d.ts +51 -0
  105. package/dist/runtime/email.mjs +245 -0
  106. package/dist/runtime/hyperdrive.d.ts +21 -0
  107. package/dist/runtime/hyperdrive.mjs +49 -0
  108. package/dist/runtime/index.d.ts +23 -0
  109. package/dist/runtime/index.mjs +23 -0
  110. package/dist/runtime/kv.d.ts +16 -0
  111. package/dist/runtime/kv.mjs +126 -0
  112. package/dist/runtime/payments.d.ts +2 -0
  113. package/dist/runtime/payments.mjs +0 -0
  114. package/dist/runtime/queue.d.ts +21 -0
  115. package/dist/runtime/queue.mjs +41 -0
  116. package/dist/runtime/rag.d.ts +1 -0
  117. package/dist/runtime/rag.mjs +1 -0
  118. package/dist/runtime/ratelimit.d.ts +21 -0
  119. package/dist/runtime/ratelimit.mjs +38 -0
  120. package/dist/runtime/realtime.d.ts +6 -0
  121. package/dist/runtime/realtime.mjs +13 -0
  122. package/dist/runtime/server/db/cms-schema.d.ts +651 -0
  123. package/dist/runtime/server/db/cms-schema.mjs +74 -0
  124. package/dist/runtime/server/durable-objects/agent.d.ts +14 -0
  125. package/dist/runtime/server/durable-objects/agent.mjs +571 -0
  126. package/dist/runtime/server/durable-objects/room.d.ts +34 -0
  127. package/dist/runtime/server/durable-objects/room.mjs +514 -0
  128. package/dist/runtime/server/middleware/auth.d.ts +2 -0
  129. package/dist/runtime/server/middleware/auth.mjs +108 -0
  130. package/dist/runtime/server/middleware/devtools.d.ts +2 -0
  131. package/dist/runtime/server/middleware/devtools.mjs +61 -0
  132. package/dist/runtime/server/middleware/ratelimit.d.ts +2 -0
  133. package/dist/runtime/server/middleware/ratelimit.mjs +49 -0
  134. package/dist/runtime/server/middleware/tenancy.d.ts +6 -0
  135. package/dist/runtime/server/middleware/tenancy.mjs +15 -0
  136. package/dist/runtime/server/middleware/tracker.d.ts +7 -0
  137. package/dist/runtime/server/middleware/tracker.mjs +9 -0
  138. package/dist/runtime/server/plugins/agents.d.ts +7 -0
  139. package/dist/runtime/server/plugins/agents.mjs +3 -0
  140. package/dist/runtime/server/plugins/cms.d.ts +7 -0
  141. package/dist/runtime/server/plugins/cms.mjs +16 -0
  142. package/dist/runtime/server/plugins/dev-logs.d.ts +1 -0
  143. package/dist/runtime/server/plugins/dev-logs.mjs +58 -0
  144. package/dist/runtime/server/plugins/dev-proxy.d.ts +3 -0
  145. package/dist/runtime/server/plugins/dev-proxy.mjs +21 -0
  146. package/dist/runtime/server/plugins/env.d.ts +6 -0
  147. package/dist/runtime/server/plugins/env.mjs +17 -0
  148. package/dist/runtime/server/plugins/queue.d.ts +6 -0
  149. package/dist/runtime/server/plugins/queue.mjs +8 -0
  150. package/dist/runtime/server/routes/__qarpeo/agents/chat.post.d.ts +20 -0
  151. package/dist/runtime/server/routes/__qarpeo/agents/chat.post.mjs +110 -0
  152. package/dist/runtime/server/routes/__qarpeo/agents/clear.post.d.ts +7 -0
  153. package/dist/runtime/server/routes/__qarpeo/agents/clear.post.mjs +15 -0
  154. package/dist/runtime/server/routes/__qarpeo/agents/create.post.d.ts +17 -0
  155. package/dist/runtime/server/routes/__qarpeo/agents/create.post.mjs +47 -0
  156. package/dist/runtime/server/routes/__qarpeo/agents/history.get.d.ts +15 -0
  157. package/dist/runtime/server/routes/__qarpeo/agents/history.get.mjs +24 -0
  158. package/dist/runtime/server/routes/__qarpeo/agents/list.get.d.ts +5 -0
  159. package/dist/runtime/server/routes/__qarpeo/agents/list.get.mjs +63 -0
  160. package/dist/runtime/server/routes/__qarpeo/analytics/events.get.d.ts +64 -0
  161. package/dist/runtime/server/routes/__qarpeo/analytics/events.get.mjs +55 -0
  162. package/dist/runtime/server/routes/__qarpeo/analytics/summary.get.d.ts +30 -0
  163. package/dist/runtime/server/routes/__qarpeo/analytics/summary.get.mjs +73 -0
  164. package/dist/runtime/server/routes/__qarpeo/auth/revoke-session.post.d.ts +6 -0
  165. package/dist/runtime/server/routes/__qarpeo/auth/revoke-session.post.mjs +26 -0
  166. package/dist/runtime/server/routes/__qarpeo/auth/sessions.get.d.ts +6 -0
  167. package/dist/runtime/server/routes/__qarpeo/auth/sessions.get.mjs +46 -0
  168. package/dist/runtime/server/routes/__qarpeo/auth/users.get.d.ts +7 -0
  169. package/dist/runtime/server/routes/__qarpeo/auth/users.get.mjs +65 -0
  170. package/dist/runtime/server/routes/__qarpeo/blob/files.get.d.ts +22 -0
  171. package/dist/runtime/server/routes/__qarpeo/blob/files.get.mjs +36 -0
  172. package/dist/runtime/server/routes/__qarpeo/db/query.post.d.ts +23 -0
  173. package/dist/runtime/server/routes/__qarpeo/db/query.post.mjs +34 -0
  174. package/dist/runtime/server/routes/__qarpeo/db/tables.get.d.ts +31 -0
  175. package/dist/runtime/server/routes/__qarpeo/db/tables.get.mjs +81 -0
  176. package/dist/runtime/server/routes/__qarpeo/kv/delete.post.d.ts +11 -0
  177. package/dist/runtime/server/routes/__qarpeo/kv/delete.post.mjs +18 -0
  178. package/dist/runtime/server/routes/__qarpeo/kv/get.get.d.ts +11 -0
  179. package/dist/runtime/server/routes/__qarpeo/kv/get.get.mjs +20 -0
  180. package/dist/runtime/server/routes/__qarpeo/kv/keys.get.d.ts +8 -0
  181. package/dist/runtime/server/routes/__qarpeo/kv/keys.get.mjs +27 -0
  182. package/dist/runtime/server/routes/__qarpeo/kv/set.post.d.ts +14 -0
  183. package/dist/runtime/server/routes/__qarpeo/kv/set.post.mjs +24 -0
  184. package/dist/runtime/server/routes/__qarpeo/logs.delete.d.ts +5 -0
  185. package/dist/runtime/server/routes/__qarpeo/logs.delete.mjs +6 -0
  186. package/dist/runtime/server/routes/__qarpeo/logs.get.d.ts +5 -0
  187. package/dist/runtime/server/routes/__qarpeo/logs.get.mjs +26 -0
  188. package/dist/runtime/server/routes/__qarpeo/logs.post.d.ts +6 -0
  189. package/dist/runtime/server/routes/__qarpeo/logs.post.mjs +17 -0
  190. package/dist/runtime/server/routes/__qarpeo/mail/bounce.post.d.ts +5 -0
  191. package/dist/runtime/server/routes/__qarpeo/mail/bounce.post.mjs +37 -0
  192. package/dist/runtime/server/routes/__qarpeo/mail/clear.post.d.ts +4 -0
  193. package/dist/runtime/server/routes/__qarpeo/mail/clear.post.mjs +6 -0
  194. package/dist/runtime/server/routes/__qarpeo/mail/inbox.get.d.ts +5 -0
  195. package/dist/runtime/server/routes/__qarpeo/mail/inbox.get.mjs +7 -0
  196. package/dist/runtime/server/routes/__qarpeo/mail/send-test.post.d.ts +4 -0
  197. package/dist/runtime/server/routes/__qarpeo/mail/send-test.post.mjs +29 -0
  198. package/dist/runtime/server/routes/__qarpeo/mail/track/click/[id].get.d.ts +2 -0
  199. package/dist/runtime/server/routes/__qarpeo/mail/track/click/[id].get.mjs +23 -0
  200. package/dist/runtime/server/routes/__qarpeo/mail/track/open/[id].get.d.ts +2 -0
  201. package/dist/runtime/server/routes/__qarpeo/mail/track/open/[id].get.mjs +31 -0
  202. package/dist/runtime/server/routes/__qarpeo/overview/health.get.d.ts +8 -0
  203. package/dist/runtime/server/routes/__qarpeo/overview/health.get.mjs +46 -0
  204. package/dist/runtime/server/routes/__qarpeo/overview.get.d.ts +32 -0
  205. package/dist/runtime/server/routes/__qarpeo/overview.get.mjs +38 -0
  206. package/dist/runtime/server/routes/__qarpeo/settings/reset-dev-data.post.d.ts +11 -0
  207. package/dist/runtime/server/routes/__qarpeo/settings/reset-dev-data.post.mjs +84 -0
  208. package/dist/runtime/server/routes/__qarpeo/settings.get.d.ts +124 -0
  209. package/dist/runtime/server/routes/__qarpeo/settings.get.mjs +145 -0
  210. package/dist/runtime/server/routes/__qarpeo/tenants/create.post.d.ts +7 -0
  211. package/dist/runtime/server/routes/__qarpeo/tenants/create.post.mjs +26 -0
  212. package/dist/runtime/server/routes/__qarpeo/tenants/domain-add.post.d.ts +6 -0
  213. package/dist/runtime/server/routes/__qarpeo/tenants/domain-add.post.mjs +29 -0
  214. package/dist/runtime/server/routes/__qarpeo/tenants/domain-delete.post.d.ts +7 -0
  215. package/dist/runtime/server/routes/__qarpeo/tenants/domain-delete.post.mjs +21 -0
  216. package/dist/runtime/server/routes/__qarpeo/tenants/domain-verify.post.d.ts +9 -0
  217. package/dist/runtime/server/routes/__qarpeo/tenants/domain-verify.post.mjs +21 -0
  218. package/dist/runtime/server/routes/__qarpeo/tenants/list.get.d.ts +15 -0
  219. package/dist/runtime/server/routes/__qarpeo/tenants/list.get.mjs +22 -0
  220. package/dist/runtime/server/routes/agents/[agent]/chat.post.d.ts +2 -0
  221. package/dist/runtime/server/routes/agents/[agent]/chat.post.mjs +35 -0
  222. package/dist/runtime/server/routes/agents/[agent]/clear.post.d.ts +4 -0
  223. package/dist/runtime/server/routes/agents/[agent]/clear.post.mjs +27 -0
  224. package/dist/runtime/server/routes/agents/[agent]/history.get.d.ts +4 -0
  225. package/dist/runtime/server/routes/agents/[agent]/history.get.mjs +27 -0
  226. package/dist/runtime/server/routes/analytics/events.get.d.ts +3 -0
  227. package/dist/runtime/server/routes/analytics/events.get.mjs +17 -0
  228. package/dist/runtime/server/routes/analytics/pageviews.get.d.ts +3 -0
  229. package/dist/runtime/server/routes/analytics/pageviews.get.mjs +15 -0
  230. package/dist/runtime/server/routes/analytics/summary.get.d.ts +6 -0
  231. package/dist/runtime/server/routes/analytics/summary.get.mjs +15 -0
  232. package/dist/runtime/server/routes/atom.xml.get.d.ts +2 -0
  233. package/dist/runtime/server/routes/atom.xml.get.mjs +76 -0
  234. package/dist/runtime/server/routes/auth/2fa/disable.post.d.ts +4 -0
  235. package/dist/runtime/server/routes/auth/2fa/disable.post.mjs +44 -0
  236. package/dist/runtime/server/routes/auth/2fa/setup.post.d.ts +6 -0
  237. package/dist/runtime/server/routes/auth/2fa/setup.post.mjs +32 -0
  238. package/dist/runtime/server/routes/auth/2fa/verify.post.d.ts +6 -0
  239. package/dist/runtime/server/routes/auth/2fa/verify.post.mjs +92 -0
  240. package/dist/runtime/server/routes/auth/[provider]/callback.d.ts +1 -0
  241. package/dist/runtime/server/routes/auth/[provider]/callback.mjs +1 -0
  242. package/dist/runtime/server/routes/auth/[provider].d.ts +3 -0
  243. package/dist/runtime/server/routes/auth/[provider].mjs +95 -0
  244. package/dist/runtime/server/routes/auth/account.delete.d.ts +5 -0
  245. package/dist/runtime/server/routes/auth/account.delete.mjs +52 -0
  246. package/dist/runtime/server/routes/auth/impersonate/start.post.d.ts +5 -0
  247. package/dist/runtime/server/routes/auth/impersonate/start.post.mjs +14 -0
  248. package/dist/runtime/server/routes/auth/impersonate/stop.post.d.ts +5 -0
  249. package/dist/runtime/server/routes/auth/impersonate/stop.post.mjs +5 -0
  250. package/dist/runtime/server/routes/auth/logout-all.post.d.ts +4 -0
  251. package/dist/runtime/server/routes/auth/logout-all.post.mjs +16 -0
  252. package/dist/runtime/server/routes/auth/logout-others.post.d.ts +4 -0
  253. package/dist/runtime/server/routes/auth/logout-others.post.mjs +11 -0
  254. package/dist/runtime/server/routes/auth/logout.post.d.ts +4 -0
  255. package/dist/runtime/server/routes/auth/logout.post.mjs +17 -0
  256. package/dist/runtime/server/routes/auth/magic-link/send.post.d.ts +10 -0
  257. package/dist/runtime/server/routes/auth/magic-link/send.post.mjs +29 -0
  258. package/dist/runtime/server/routes/auth/magic-link/verify.get.d.ts +2 -0
  259. package/dist/runtime/server/routes/auth/magic-link/verify.get.mjs +81 -0
  260. package/dist/runtime/server/routes/auth/organization/create.post.d.ts +5 -0
  261. package/dist/runtime/server/routes/auth/organization/create.post.mjs +10 -0
  262. package/dist/runtime/server/routes/auth/organization/invite.post.d.ts +9 -0
  263. package/dist/runtime/server/routes/auth/organization/invite.post.mjs +27 -0
  264. package/dist/runtime/server/routes/auth/organization/join.post.d.ts +6 -0
  265. package/dist/runtime/server/routes/auth/organization/join.post.mjs +15 -0
  266. package/dist/runtime/server/routes/auth/organization/list.get.d.ts +5 -0
  267. package/dist/runtime/server/routes/auth/organization/list.get.mjs +7 -0
  268. package/dist/runtime/server/routes/auth/organization/members/[id].delete.d.ts +4 -0
  269. package/dist/runtime/server/routes/auth/organization/members/[id].delete.mjs +33 -0
  270. package/dist/runtime/server/routes/auth/organization/members/index.get.d.ts +6 -0
  271. package/dist/runtime/server/routes/auth/organization/members/index.get.mjs +18 -0
  272. package/dist/runtime/server/routes/auth/organization/mfa.post.d.ts +6 -0
  273. package/dist/runtime/server/routes/auth/organization/mfa.post.mjs +33 -0
  274. package/dist/runtime/server/routes/auth/organization/sso/[id].delete.d.ts +4 -0
  275. package/dist/runtime/server/routes/auth/organization/sso/[id].delete.mjs +28 -0
  276. package/dist/runtime/server/routes/auth/organization/sso/index.get.d.ts +2 -0
  277. package/dist/runtime/server/routes/auth/organization/sso/index.get.mjs +23 -0
  278. package/dist/runtime/server/routes/auth/organization/sso/index.post.d.ts +5 -0
  279. package/dist/runtime/server/routes/auth/organization/sso/index.post.mjs +84 -0
  280. package/dist/runtime/server/routes/auth/organization/switch.post.d.ts +6 -0
  281. package/dist/runtime/server/routes/auth/organization/switch.post.mjs +14 -0
  282. package/dist/runtime/server/routes/auth/otp/send.post.d.ts +10 -0
  283. package/dist/runtime/server/routes/auth/otp/send.post.mjs +29 -0
  284. package/dist/runtime/server/routes/auth/otp/verify.post.d.ts +6 -0
  285. package/dist/runtime/server/routes/auth/otp/verify.post.mjs +95 -0
  286. package/dist/runtime/server/routes/auth/sessions/[id].delete.d.ts +5 -0
  287. package/dist/runtime/server/routes/auth/sessions/[id].delete.mjs +24 -0
  288. package/dist/runtime/server/routes/auth/sessions.get.d.ts +2 -0
  289. package/dist/runtime/server/routes/auth/sessions.get.mjs +11 -0
  290. package/dist/runtime/server/routes/auth/sso/login.get.d.ts +2 -0
  291. package/dist/runtime/server/routes/auth/sso/login.get.mjs +94 -0
  292. package/dist/runtime/server/routes/auth/sso/oidc/callback.get.d.ts +2 -0
  293. package/dist/runtime/server/routes/auth/sso/oidc/callback.get.mjs +113 -0
  294. package/dist/runtime/server/routes/auth/sso/saml/acs.post.d.ts +2 -0
  295. package/dist/runtime/server/routes/auth/sso/saml/acs.post.mjs +95 -0
  296. package/dist/runtime/server/routes/auth/tokens/[id].delete.d.ts +4 -0
  297. package/dist/runtime/server/routes/auth/tokens/[id].delete.mjs +13 -0
  298. package/dist/runtime/server/routes/auth/tokens/create.post.d.ts +9 -0
  299. package/dist/runtime/server/routes/auth/tokens/create.post.mjs +17 -0
  300. package/dist/runtime/server/routes/auth/tokens/index.get.d.ts +9 -0
  301. package/dist/runtime/server/routes/auth/tokens/index.get.mjs +5 -0
  302. package/dist/runtime/server/routes/auth/turnstile/verify.post.d.ts +2 -0
  303. package/dist/runtime/server/routes/auth/turnstile/verify.post.mjs +15 -0
  304. package/dist/runtime/server/routes/auth/user.get.d.ts +2 -0
  305. package/dist/runtime/server/routes/auth/user.get.mjs +6 -0
  306. package/dist/runtime/server/routes/auth/webauthn/login/options.post.d.ts +2 -0
  307. package/dist/runtime/server/routes/auth/webauthn/login/options.post.mjs +17 -0
  308. package/dist/runtime/server/routes/auth/webauthn/login/verify.post.d.ts +6 -0
  309. package/dist/runtime/server/routes/auth/webauthn/login/verify.post.mjs +127 -0
  310. package/dist/runtime/server/routes/auth/webauthn/register/options.post.d.ts +2 -0
  311. package/dist/runtime/server/routes/auth/webauthn/register/options.post.mjs +47 -0
  312. package/dist/runtime/server/routes/auth/webauthn/register/verify.post.d.ts +5 -0
  313. package/dist/runtime/server/routes/auth/webauthn/register/verify.post.mjs +88 -0
  314. package/dist/runtime/server/routes/cms/[collection]/[id]/preview-token.post.d.ts +8 -0
  315. package/dist/runtime/server/routes/cms/[collection]/[id]/preview-token.post.mjs +27 -0
  316. package/dist/runtime/server/routes/cms/[collection]/[id]/publish.post.d.ts +6 -0
  317. package/dist/runtime/server/routes/cms/[collection]/[id]/publish.post.mjs +19 -0
  318. package/dist/runtime/server/routes/cms/[collection]/[id]/review.post.d.ts +6 -0
  319. package/dist/runtime/server/routes/cms/[collection]/[id]/review.post.mjs +33 -0
  320. package/dist/runtime/server/routes/cms/[collection]/[id].delete.d.ts +4 -0
  321. package/dist/runtime/server/routes/cms/[collection]/[id].delete.mjs +17 -0
  322. package/dist/runtime/server/routes/cms/[collection]/[id].put.d.ts +4 -0
  323. package/dist/runtime/server/routes/cms/[collection]/[id].put.mjs +18 -0
  324. package/dist/runtime/server/routes/cms/[collection]/[slug].get.d.ts +4 -0
  325. package/dist/runtime/server/routes/cms/[collection]/[slug].get.mjs +19 -0
  326. package/dist/runtime/server/routes/cms/[collection]/atom.xml.get.d.ts +2 -0
  327. package/dist/runtime/server/routes/cms/[collection]/atom.xml.get.mjs +69 -0
  328. package/dist/runtime/server/routes/cms/[collection]/by-path.get.d.ts +8 -0
  329. package/dist/runtime/server/routes/cms/[collection]/by-path.get.mjs +23 -0
  330. package/dist/runtime/server/routes/cms/[collection]/feed.xml.get.d.ts +2 -0
  331. package/dist/runtime/server/routes/cms/[collection]/feed.xml.get.mjs +65 -0
  332. package/dist/runtime/server/routes/cms/[collection]/index.get.d.ts +7 -0
  333. package/dist/runtime/server/routes/cms/[collection]/index.get.mjs +15 -0
  334. package/dist/runtime/server/routes/cms/[collection]/index.post.d.ts +4 -0
  335. package/dist/runtime/server/routes/cms/[collection]/index.post.mjs +23 -0
  336. package/dist/runtime/server/routes/cms/[collection]/references.get.d.ts +19 -0
  337. package/dist/runtime/server/routes/cms/[collection]/references.get.mjs +47 -0
  338. package/dist/runtime/server/routes/cms/[collection]/surroundings.get.d.ts +10 -0
  339. package/dist/runtime/server/routes/cms/[collection]/surroundings.get.mjs +12 -0
  340. package/dist/runtime/server/routes/cms/collections/[id].delete.d.ts +4 -0
  341. package/dist/runtime/server/routes/cms/collections/[id].delete.mjs +10 -0
  342. package/dist/runtime/server/routes/cms/collections/[id].put.d.ts +4 -0
  343. package/dist/runtime/server/routes/cms/collections/[id].put.mjs +11 -0
  344. package/dist/runtime/server/routes/cms/collections/index.get.d.ts +4 -0
  345. package/dist/runtime/server/routes/cms/collections/index.get.mjs +6 -0
  346. package/dist/runtime/server/routes/cms/collections/index.post.d.ts +4 -0
  347. package/dist/runtime/server/routes/cms/collections/index.post.mjs +19 -0
  348. package/dist/runtime/server/routes/cms/components/index.get.d.ts +17 -0
  349. package/dist/runtime/server/routes/cms/components/index.get.mjs +16 -0
  350. package/dist/runtime/server/routes/cms/deploy.post.d.ts +4 -0
  351. package/dist/runtime/server/routes/cms/deploy.post.mjs +46 -0
  352. package/dist/runtime/server/routes/cms/export.post.d.ts +10 -0
  353. package/dist/runtime/server/routes/cms/export.post.mjs +55 -0
  354. package/dist/runtime/server/routes/cms/navigation.get.d.ts +8 -0
  355. package/dist/runtime/server/routes/cms/navigation.get.mjs +36 -0
  356. package/dist/runtime/server/routes/cms/preview/disable.get.d.ts +9 -0
  357. package/dist/runtime/server/routes/cms/preview/disable.get.mjs +9 -0
  358. package/dist/runtime/server/routes/cms/preview/enable.get.d.ts +14 -0
  359. package/dist/runtime/server/routes/cms/preview/enable.get.mjs +42 -0
  360. package/dist/runtime/server/routes/cms/publish-scheduled.post.d.ts +11 -0
  361. package/dist/runtime/server/routes/cms/publish-scheduled.post.mjs +35 -0
  362. package/dist/runtime/server/routes/cms/search.get.d.ts +13 -0
  363. package/dist/runtime/server/routes/cms/search.get.mjs +137 -0
  364. package/dist/runtime/server/routes/cms/sync.post.d.ts +7 -0
  365. package/dist/runtime/server/routes/cms/sync.post.mjs +18 -0
  366. package/dist/runtime/server/routes/cms/webhooks/[provider].post.d.ts +150 -0
  367. package/dist/runtime/server/routes/cms/webhooks/[provider].post.mjs +213 -0
  368. package/dist/runtime/server/routes/docs/search.get.d.ts +15 -0
  369. package/dist/runtime/server/routes/docs/search.get.mjs +20 -0
  370. package/dist/runtime/server/routes/feed.xml.get.d.ts +2 -0
  371. package/dist/runtime/server/routes/feed.xml.get.mjs +73 -0
  372. package/dist/runtime/server/routes/mail/bounce.post.d.ts +14 -0
  373. package/dist/runtime/server/routes/mail/bounce.post.mjs +56 -0
  374. package/dist/runtime/server/routes/mail/inbox/[id].get.d.ts +2 -0
  375. package/dist/runtime/server/routes/mail/inbox/[id].get.mjs +9 -0
  376. package/dist/runtime/server/routes/mail/inbox.get.d.ts +4 -0
  377. package/dist/runtime/server/routes/mail/inbox.get.mjs +7 -0
  378. package/dist/runtime/server/routes/mail/suppressions/[email].delete.d.ts +11 -0
  379. package/dist/runtime/server/routes/mail/suppressions/[email].delete.mjs +18 -0
  380. package/dist/runtime/server/routes/mail/suppressions/index.get.d.ts +12 -0
  381. package/dist/runtime/server/routes/mail/suppressions/index.get.mjs +23 -0
  382. package/dist/runtime/server/routes/mail/suppressions/index.post.d.ts +10 -0
  383. package/dist/runtime/server/routes/mail/suppressions/index.post.mjs +23 -0
  384. package/dist/runtime/server/routes/mail/unsubscribe.d.ts +13 -0
  385. package/dist/runtime/server/routes/mail/unsubscribe.mjs +91 -0
  386. package/dist/runtime/server/routes/mail/webhooks.post.d.ts +12 -0
  387. package/dist/runtime/server/routes/mail/webhooks.post.mjs +71 -0
  388. package/dist/runtime/server/routes/notifications/[id]/read.post.d.ts +4 -0
  389. package/dist/runtime/server/routes/notifications/[id]/read.post.mjs +13 -0
  390. package/dist/runtime/server/routes/notifications/[id].delete.d.ts +4 -0
  391. package/dist/runtime/server/routes/notifications/[id].delete.mjs +13 -0
  392. package/dist/runtime/server/routes/notifications/[id].get.d.ts +2 -0
  393. package/dist/runtime/server/routes/notifications/[id].get.mjs +15 -0
  394. package/dist/runtime/server/routes/notifications/index.get.d.ts +6 -0
  395. package/dist/runtime/server/routes/notifications/index.get.mjs +18 -0
  396. package/dist/runtime/server/routes/notifications/push/subscribe.post.d.ts +4 -0
  397. package/dist/runtime/server/routes/notifications/push/subscribe.post.mjs +20 -0
  398. package/dist/runtime/server/routes/notifications/push/unsubscribe.post.d.ts +4 -0
  399. package/dist/runtime/server/routes/notifications/push/unsubscribe.post.mjs +15 -0
  400. package/dist/runtime/server/routes/notifications/read-all.post.d.ts +4 -0
  401. package/dist/runtime/server/routes/notifications/read-all.post.mjs +11 -0
  402. package/dist/runtime/server/routes/payments/cancel.post.d.ts +7 -0
  403. package/dist/runtime/server/routes/payments/cancel.post.mjs +9 -0
  404. package/dist/runtime/server/routes/payments/change-plan.post.d.ts +7 -0
  405. package/dist/runtime/server/routes/payments/change-plan.post.mjs +12 -0
  406. package/dist/runtime/server/routes/payments/checkout.post.d.ts +9 -0
  407. package/dist/runtime/server/routes/payments/checkout.post.mjs +16 -0
  408. package/dist/runtime/server/routes/payments/pause.post.d.ts +7 -0
  409. package/dist/runtime/server/routes/payments/pause.post.mjs +9 -0
  410. package/dist/runtime/server/routes/payments/portal.post.d.ts +5 -0
  411. package/dist/runtime/server/routes/payments/portal.post.mjs +9 -0
  412. package/dist/runtime/server/routes/payments/subscription.get.d.ts +6 -0
  413. package/dist/runtime/server/routes/payments/subscription.get.mjs +9 -0
  414. package/dist/runtime/server/routes/payments/usage.get.d.ts +12 -0
  415. package/dist/runtime/server/routes/payments/usage.get.mjs +18 -0
  416. package/dist/runtime/server/routes/payments/usage.post.d.ts +6 -0
  417. package/dist/runtime/server/routes/payments/usage.post.mjs +24 -0
  418. package/dist/runtime/server/routes/payments/webhook.post.d.ts +14 -0
  419. package/dist/runtime/server/routes/payments/webhook.post.mjs +21 -0
  420. package/dist/runtime/server/routes/realtime/broadcast.post.d.ts +4 -0
  421. package/dist/runtime/server/routes/realtime/broadcast.post.mjs +26 -0
  422. package/dist/runtime/server/routes/realtime/history.delete.d.ts +13 -0
  423. package/dist/runtime/server/routes/realtime/history.delete.mjs +12 -0
  424. package/dist/runtime/server/routes/realtime/history.get.d.ts +16 -0
  425. package/dist/runtime/server/routes/realtime/history.get.mjs +15 -0
  426. package/dist/runtime/server/routes/realtime/presence.get.d.ts +6 -0
  427. package/dist/runtime/server/routes/realtime/presence.get.mjs +27 -0
  428. package/dist/runtime/server/routes/realtime/state.get.d.ts +5 -0
  429. package/dist/runtime/server/routes/realtime/state.get.mjs +26 -0
  430. package/dist/runtime/server/routes/realtime/state.post.d.ts +6 -0
  431. package/dist/runtime/server/routes/realtime/state.post.mjs +37 -0
  432. package/dist/runtime/server/routes/realtime/ws.get.d.ts +2 -0
  433. package/dist/runtime/server/routes/realtime/ws.get.mjs +73 -0
  434. package/dist/runtime/server/routes/robots.txt.get.d.ts +2 -0
  435. package/dist/runtime/server/routes/robots.txt.get.mjs +41 -0
  436. package/dist/runtime/server/routes/rss.xml.get.d.ts +2 -0
  437. package/dist/runtime/server/routes/rss.xml.get.mjs +2 -0
  438. package/dist/runtime/server/routes/sitemap.xml.get.d.ts +2 -0
  439. package/dist/runtime/server/routes/sitemap.xml.get.mjs +84 -0
  440. package/dist/runtime/server/routes/storage/file/[...key].delete.d.ts +4 -0
  441. package/dist/runtime/server/routes/storage/file/[...key].delete.mjs +23 -0
  442. package/dist/runtime/server/routes/storage/file/[...key].get.d.ts +2 -0
  443. package/dist/runtime/server/routes/storage/file/[...key].get.mjs +23 -0
  444. package/dist/runtime/server/routes/storage/files.get.d.ts +14 -0
  445. package/dist/runtime/server/routes/storage/files.get.mjs +11 -0
  446. package/dist/runtime/server/routes/storage/presigned.post.d.ts +8 -0
  447. package/dist/runtime/server/routes/storage/presigned.post.mjs +39 -0
  448. package/dist/runtime/server/routes/storage/sign.post.d.ts +6 -0
  449. package/dist/runtime/server/routes/storage/sign.post.mjs +24 -0
  450. package/dist/runtime/server/routes/storage/upload.post.d.ts +22 -0
  451. package/dist/runtime/server/routes/storage/upload.post.mjs +96 -0
  452. package/dist/runtime/server/routes/tenants/[tenant]/domains/[domain]/index.delete.d.ts +8 -0
  453. package/dist/runtime/server/routes/tenants/[tenant]/domains/[domain]/index.delete.mjs +39 -0
  454. package/dist/runtime/server/routes/tenants/[tenant]/domains/[domain]/verify.post.d.ts +8 -0
  455. package/dist/runtime/server/routes/tenants/[tenant]/domains/[domain]/verify.post.mjs +39 -0
  456. package/dist/runtime/server/routes/tenants/[tenant]/domains/index.get.d.ts +8 -0
  457. package/dist/runtime/server/routes/tenants/[tenant]/domains/index.get.mjs +20 -0
  458. package/dist/runtime/server/routes/tenants/[tenant]/domains/index.post.d.ts +8 -0
  459. package/dist/runtime/server/routes/tenants/[tenant]/domains/index.post.mjs +44 -0
  460. package/dist/runtime/server/routes/tenants/current.get.d.ts +8 -0
  461. package/dist/runtime/server/routes/tenants/current.get.mjs +6 -0
  462. package/dist/runtime/server/tasks/cms/publish.d.ts +4 -0
  463. package/dist/runtime/server/tasks/cms/publish.mjs +18 -0
  464. package/dist/runtime/server/utils/agent-store.d.ts +24 -0
  465. package/dist/runtime/server/utils/agent-store.mjs +53 -0
  466. package/dist/runtime/server/utils/agents-registry-fallback.d.ts +1 -0
  467. package/dist/runtime/server/utils/agents-registry-fallback.mjs +1 -0
  468. package/dist/runtime/server/utils/ai.d.ts +119 -0
  469. package/dist/runtime/server/utils/ai.mjs +607 -0
  470. package/dist/runtime/server/utils/analytics-query.d.ts +109 -0
  471. package/dist/runtime/server/utils/analytics-query.mjs +234 -0
  472. package/dist/runtime/server/utils/auth.d.ts +60 -0
  473. package/dist/runtime/server/utils/auth.mjs +361 -0
  474. package/dist/runtime/server/utils/blob-store.d.ts +13 -0
  475. package/dist/runtime/server/utils/blob-store.mjs +60 -0
  476. package/dist/runtime/server/utils/bots.d.ts +21 -0
  477. package/dist/runtime/server/utils/bots.mjs +491 -0
  478. package/dist/runtime/server/utils/cache.d.ts +33 -0
  479. package/dist/runtime/server/utils/cache.mjs +128 -0
  480. package/dist/runtime/server/utils/cms-events.d.ts +17 -0
  481. package/dist/runtime/server/utils/cms-events.mjs +80 -0
  482. package/dist/runtime/server/utils/cms-filter.d.ts +3 -0
  483. package/dist/runtime/server/utils/cms-filter.mjs +34 -0
  484. package/dist/runtime/server/utils/cms.d.ts +278 -0
  485. package/dist/runtime/server/utils/cms.mjs +3268 -0
  486. package/dist/runtime/server/utils/config.d.ts +27 -0
  487. package/dist/runtime/server/utils/config.mjs +38 -0
  488. package/dist/runtime/server/utils/context.d.ts +13 -0
  489. package/dist/runtime/server/utils/context.mjs +98 -0
  490. package/dist/runtime/server/utils/crypto.d.ts +16 -0
  491. package/dist/runtime/server/utils/crypto.mjs +56 -0
  492. package/dist/runtime/server/utils/dev-logs.d.ts +21 -0
  493. package/dist/runtime/server/utils/dev-logs.mjs +41 -0
  494. package/dist/runtime/server/utils/dev-proxy.d.ts +1 -0
  495. package/dist/runtime/server/utils/dev-proxy.mjs +69 -0
  496. package/dist/runtime/server/utils/domains.d.ts +46 -0
  497. package/dist/runtime/server/utils/domains.mjs +318 -0
  498. package/dist/runtime/server/utils/drizzle.d.ts +10 -0
  499. package/dist/runtime/server/utils/drizzle.mjs +36 -0
  500. package/dist/runtime/server/utils/email-templates.d.ts +44 -0
  501. package/dist/runtime/server/utils/email-templates.mjs +373 -0
  502. package/dist/runtime/server/utils/feed.d.ts +42 -0
  503. package/dist/runtime/server/utils/feed.mjs +161 -0
  504. package/dist/runtime/server/utils/impersonation.d.ts +67 -0
  505. package/dist/runtime/server/utils/impersonation.mjs +298 -0
  506. package/dist/runtime/server/utils/jobs.d.ts +70 -0
  507. package/dist/runtime/server/utils/jobs.mjs +125 -0
  508. package/dist/runtime/server/utils/kv-store.d.ts +9 -0
  509. package/dist/runtime/server/utils/kv-store.mjs +32 -0
  510. package/dist/runtime/server/utils/mail-suppression.d.ts +62 -0
  511. package/dist/runtime/server/utils/mail-suppression.mjs +328 -0
  512. package/dist/runtime/server/utils/mail.d.ts +90 -0
  513. package/dist/runtime/server/utils/mail.mjs +366 -0
  514. package/dist/runtime/server/utils/notifications.d.ts +51 -0
  515. package/dist/runtime/server/utils/notifications.mjs +215 -0
  516. package/dist/runtime/server/utils/organization.d.ts +54 -0
  517. package/dist/runtime/server/utils/organization.mjs +354 -0
  518. package/dist/runtime/server/utils/passwordless.d.ts +48 -0
  519. package/dist/runtime/server/utils/passwordless.mjs +179 -0
  520. package/dist/runtime/server/utils/payments.d.ts +121 -0
  521. package/dist/runtime/server/utils/payments.mjs +1067 -0
  522. package/dist/runtime/server/utils/queue-fallback.d.ts +2 -0
  523. package/dist/runtime/server/utils/queue-fallback.mjs +2 -0
  524. package/dist/runtime/server/utils/queue.d.ts +4 -0
  525. package/dist/runtime/server/utils/queue.mjs +3 -0
  526. package/dist/runtime/server/utils/rag.d.ts +25 -0
  527. package/dist/runtime/server/utils/rag.mjs +333 -0
  528. package/dist/runtime/server/utils/ratelimit.d.ts +45 -0
  529. package/dist/runtime/server/utils/ratelimit.mjs +182 -0
  530. package/dist/runtime/server/utils/realtime.d.ts +110 -0
  531. package/dist/runtime/server/utils/realtime.mjs +197 -0
  532. package/dist/runtime/server/utils/schema-fallback.d.ts +3 -0
  533. package/dist/runtime/server/utils/schema-fallback.mjs +2 -0
  534. package/dist/runtime/server/utils/session.d.ts +72 -0
  535. package/dist/runtime/server/utils/session.mjs +287 -0
  536. package/dist/runtime/server/utils/sso.d.ts +49 -0
  537. package/dist/runtime/server/utils/sso.mjs +131 -0
  538. package/dist/runtime/server/utils/storage.d.ts +111 -0
  539. package/dist/runtime/server/utils/storage.mjs +654 -0
  540. package/dist/runtime/server/utils/tenant.d.ts +44 -0
  541. package/dist/runtime/server/utils/tenant.mjs +285 -0
  542. package/dist/runtime/server/utils/totp.d.ts +33 -0
  543. package/dist/runtime/server/utils/totp.mjs +112 -0
  544. package/dist/runtime/server/utils/tracker.d.ts +72 -0
  545. package/dist/runtime/server/utils/tracker.mjs +139 -0
  546. package/dist/runtime/server/utils/turnstile.d.ts +17 -0
  547. package/dist/runtime/server/utils/turnstile.mjs +68 -0
  548. package/dist/runtime/server/utils/vapid.d.ts +35 -0
  549. package/dist/runtime/server/utils/vapid.mjs +211 -0
  550. package/dist/runtime/storage.d.ts +1 -0
  551. package/dist/runtime/storage.mjs +0 -0
  552. package/dist/runtime/types/ai.d.ts +68 -0
  553. package/dist/runtime/types/ai.mjs +0 -0
  554. package/dist/runtime/types/analytics.d.ts +30 -0
  555. package/dist/runtime/types/analytics.mjs +0 -0
  556. package/dist/runtime/types/auth-globals.d.ts +14 -0
  557. package/dist/runtime/types/auth.d.ts +153 -0
  558. package/dist/runtime/types/auth.mjs +0 -0
  559. package/dist/runtime/types/browser.d.ts +29 -0
  560. package/dist/runtime/types/browser.mjs +0 -0
  561. package/dist/runtime/types/d1.d.ts +44 -0
  562. package/dist/runtime/types/d1.mjs +0 -0
  563. package/dist/runtime/types/drizzle.d.ts +13 -0
  564. package/dist/runtime/types/drizzle.mjs +0 -0
  565. package/dist/runtime/types/durable-object.d.ts +51 -0
  566. package/dist/runtime/types/durable-object.mjs +0 -0
  567. package/dist/runtime/types/email.d.ts +66 -0
  568. package/dist/runtime/types/email.mjs +0 -0
  569. package/dist/runtime/types/hyperdrive.d.ts +18 -0
  570. package/dist/runtime/types/hyperdrive.mjs +0 -0
  571. package/dist/runtime/types/imports.d.ts +58 -0
  572. package/dist/runtime/types/index.d.ts +156 -0
  573. package/dist/runtime/types/index.mjs +13 -0
  574. package/dist/runtime/types/queue.d.ts +67 -0
  575. package/dist/runtime/types/queue.mjs +0 -0
  576. package/dist/runtime/types/r2.d.ts +108 -0
  577. package/dist/runtime/types/r2.mjs +0 -0
  578. package/dist/runtime/types/ratelimit.d.ts +21 -0
  579. package/dist/runtime/types/ratelimit.mjs +0 -0
  580. package/dist/runtime/types/vectorize.d.ts +74 -0
  581. package/dist/runtime/types/vectorize.mjs +0 -0
  582. package/dist/runtime/vectorize.d.ts +21 -0
  583. package/dist/runtime/vectorize.mjs +53 -0
  584. package/dist/types.d.ts +2750 -0
  585. package/dist/types.mjs +1 -0
  586. package/dist/utils/agents.d.ts +8 -0
  587. package/dist/utils/auth-schema.d.ts +11 -0
  588. package/dist/utils/bindings.d.ts +16 -0
  589. package/dist/utils/cms-schema.d.ts +1 -0
  590. package/dist/utils/cms-sources.d.ts +62 -0
  591. package/dist/utils/drizzle.d.ts +42 -0
  592. package/dist/utils/jobs.d.ts +8 -0
  593. package/dist/utils/local-db.d.ts +28 -0
  594. package/dist/utils/notifications-schema.d.ts +15 -0
  595. package/dist/utils/payments-schema.d.ts +11 -0
  596. package/dist/utils/pkg.d.ts +15 -0
  597. package/dist/utils/queue.d.ts +8 -0
  598. package/dist/utils/tenants-schema.d.ts +11 -0
  599. package/package.json +104 -0
@@ -0,0 +1,2249 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed, watch, onMounted } from "vue"
3
+
4
+ definePageMeta({
5
+ layout: false,
6
+ })
7
+
8
+ const route = useRoute()
9
+ const router = useRouter()
10
+
11
+ const runtimeConfig = useRuntimeConfig()
12
+ const cmsConfig = (runtimeConfig.public as any)?.qarpeo?.cms || {}
13
+ const apiPath = cmsConfig.apiPath || "/api/cms"
14
+ const hasDeployTrigger = Boolean(cmsConfig.hasDeployTrigger)
15
+
16
+ interface CMSCollectionItem {
17
+ id: string
18
+ name: string
19
+ description?: string | null
20
+ schemaJson?: string
21
+ fields?: Record<string, any>
22
+ source?: string
23
+ mode?: string
24
+ }
25
+
26
+ interface CMSEntryItem {
27
+ id: string
28
+ collectionId: string
29
+ slug: string
30
+ title: string
31
+ description?: string | null
32
+ coverImage?: string | null
33
+ tags?: string | null
34
+ order?: number
35
+ contentMd: string
36
+ status: string
37
+ authorId?: string | null
38
+ publishedAt?: number | null
39
+ createdAt?: number
40
+ updatedAt?: number
41
+ metadata?: Record<string, any>
42
+ }
43
+
44
+ // State
45
+ const user = ref<any>(null)
46
+ const collections = ref<CMSCollectionItem[]>([])
47
+ const selectedCollectionId = computed(() => (route.query.collection as string) || "")
48
+ const entries = ref<CMSEntryItem[]>([])
49
+ const currentView = ref<"list" | "editor">("list")
50
+
51
+ const loading = ref(false)
52
+ const saving = ref(false)
53
+ const syncing = ref(false)
54
+ const exporting = ref(false)
55
+ const deploying = ref(false)
56
+
57
+ const searchQuery = ref("")
58
+ const statusFilter = ref("all")
59
+ const statusOptions = [
60
+ { label: "All Statuses", value: "all" },
61
+ { label: "Published", value: "published" },
62
+ { label: "Drafts", value: "draft" },
63
+ { label: "Pending Review", value: "pending_review" },
64
+ { label: "Scheduled", value: "scheduled" },
65
+ { label: "Archived", value: "archived" },
66
+ ]
67
+ const tableColumns = [
68
+ { accessorKey: "title", header: "Title" },
69
+ { accessorKey: "slug", header: "Slug" },
70
+ { accessorKey: "status", header: "Status" },
71
+ { accessorKey: "updatedAt", header: "Updated" },
72
+ { id: "actions", header: "" },
73
+ ]
74
+ const openMediaLibrary = ref(false)
75
+ const showNewCollectionModal = ref(false)
76
+ const showPreview = ref(false)
77
+ const uploadProgress = ref(0)
78
+ const mediaItems = ref<any[]>([])
79
+ const mediaLoading = ref(false)
80
+ const mediaSearchQuery = ref("")
81
+ const mediaCopiedUrl = ref<string | null>(null)
82
+ const imageTargetField = ref<string | null>(null)
83
+
84
+ // Live preview state
85
+ const previewMode = ref<"doc" | "page">("doc")
86
+ const previewDevice = ref<"desktop" | "tablet" | "mobile">("desktop")
87
+ const previewIframeUrl = ref("")
88
+ const previewLoading = ref(false)
89
+ const previewCopied = ref(false)
90
+
91
+ const editorToolbarItems = [
92
+ [
93
+ { kind: "undo", icon: "i-lucide-undo", tooltip: { text: "Undo" } },
94
+ { kind: "redo", icon: "i-lucide-redo", tooltip: { text: "Redo" } },
95
+ ],
96
+ [
97
+ { kind: "heading", level: 1, icon: "i-lucide-heading-1", tooltip: { text: "Heading 1" } },
98
+ { kind: "heading", level: 2, icon: "i-lucide-heading-2", tooltip: { text: "Heading 2" } },
99
+ { kind: "heading", level: 3, icon: "i-lucide-heading-3", tooltip: { text: "Heading 3" } },
100
+ ],
101
+ [
102
+ { kind: "mark", mark: "bold", icon: "i-lucide-bold", tooltip: { text: "Bold" } },
103
+ { kind: "mark", mark: "italic", icon: "i-lucide-italic", tooltip: { text: "Italic" } },
104
+ {
105
+ kind: "mark",
106
+ mark: "strike",
107
+ icon: "i-lucide-strikethrough",
108
+ tooltip: { text: "Strikethrough" },
109
+ },
110
+ { kind: "mark", mark: "code", icon: "i-lucide-code", tooltip: { text: "Inline Code" } },
111
+ ],
112
+ [
113
+ { kind: "bulletList", icon: "i-lucide-list", tooltip: { text: "Bullet List" } },
114
+ { kind: "orderedList", icon: "i-lucide-list-ordered", tooltip: { text: "Numbered List" } },
115
+ { kind: "blockquote", icon: "i-lucide-quote", tooltip: { text: "Blockquote" } },
116
+ { kind: "codeBlock", icon: "i-lucide-square-code", tooltip: { text: "Code Block" } },
117
+ { kind: "horizontalRule", icon: "i-lucide-minus", tooltip: { text: "Divider" } },
118
+ ],
119
+ [
120
+ { kind: "link", icon: "i-lucide-link", tooltip: { text: "Link" } },
121
+ {
122
+ icon: "i-lucide-image",
123
+ tooltip: { text: "Insert Image" },
124
+ onClick: () => {
125
+ imageTargetField.value = null
126
+ openMediaLibrary.value = true
127
+ },
128
+ },
129
+ ],
130
+ ]
131
+
132
+ const newCol = ref({ id: "", name: "", description: "" })
133
+ const scheduledPublishDate = ref("")
134
+
135
+ const activeEntry = ref<CMSEntryItem>({
136
+ id: "",
137
+ collectionId: "",
138
+ slug: "",
139
+ title: "",
140
+ description: "",
141
+ coverImage: "",
142
+ tags: "",
143
+ order: 0,
144
+ contentMd: "",
145
+ status: "draft",
146
+ metadata: {},
147
+ })
148
+
149
+ // Computeds
150
+ const currentCollection = computed(() =>
151
+ collections.value.find((c) => c.id === selectedCollectionId.value),
152
+ )
153
+
154
+ const collectionNavItems = computed(() =>
155
+ collections.value.map((col) => ({
156
+ label: col.name,
157
+ value: col.id,
158
+ badge: col.source
159
+ ? {
160
+ label: col.source,
161
+ size: "xs" as const,
162
+ variant: "subtle" as const,
163
+ color: "neutral" as const,
164
+ }
165
+ : undefined,
166
+ onSelect: () => selectCollection(col.id),
167
+ })),
168
+ )
169
+
170
+ const collectionConfig = computed(() => {
171
+ return cmsConfig.collections?.[selectedCollectionId.value] || {}
172
+ })
173
+
174
+ const collectionMode = computed<string>(() => {
175
+ return collectionConfig.value.mode || currentCollection.value?.mode || "db"
176
+ })
177
+
178
+ const isFileLocked = computed(() => collectionMode.value === "file-lock")
179
+ const isGitSync = computed(() => collectionMode.value === "git-sync")
180
+
181
+ const activeApproval = computed(() => {
182
+ return collectionConfig.value.approval
183
+ })
184
+
185
+ const hasApproval = computed(() => Boolean(activeApproval.value))
186
+
187
+ const isAuthor = computed(() => {
188
+ return (
189
+ activeEntry.value.authorId && user.value?.id && activeEntry.value.authorId === user.value.id
190
+ )
191
+ })
192
+
193
+ const canApprove = computed(() => {
194
+ if (!hasApproval.value) return true
195
+ const app = activeApproval.value
196
+ const u = user.value
197
+ if (!u) return false
198
+ const role = u.role || "user"
199
+ const isApprover = u.isAdmin || (app.roles && app.roles.includes(role))
200
+ if (!isApprover) return false
201
+ if (!app.allowSelfApproval && isAuthor.value) return false
202
+ return true
203
+ })
204
+
205
+ const currentCollectionFields = computed<Record<string, any>>(() => {
206
+ const staticFields = collectionConfig.value.fields || {}
207
+ const dbFields = currentCollection.value?.fields || {}
208
+ return { ...staticFields, ...dbFields }
209
+ })
210
+
211
+ const hasCustomFields = computed(() => Object.keys(currentCollectionFields.value).length > 0)
212
+
213
+ // Methods
214
+ async function fetchUser() {
215
+ try {
216
+ const res = await fetch("/api/auth/user")
217
+ if (res.ok) {
218
+ const data = await res.json()
219
+ user.value = data.user
220
+ }
221
+ } catch {}
222
+ }
223
+
224
+ async function fetchCollections() {
225
+ try {
226
+ const res = await fetch(`${apiPath}/collections`)
227
+ const data = await res.json()
228
+ collections.value = data.collections || []
229
+
230
+ const currentId = selectedCollectionId.value
231
+ const exists = collections.value.some((c) => c.id === currentId)
232
+ if (exists) {
233
+ fetchEntries()
234
+ }
235
+ } catch (err) {
236
+ console.error("Failed to load collections:", err)
237
+ }
238
+ }
239
+
240
+ async function fetchEntries() {
241
+ if (!selectedCollectionId.value) return
242
+ loading.value = true
243
+ try {
244
+ const params = new URLSearchParams({ limit: "50" })
245
+ params.set("status", statusFilter.value)
246
+ const res = await fetch(`${apiPath}/${selectedCollectionId.value}?${params.toString()}`)
247
+ const data = await res.json()
248
+ entries.value = data.entries || []
249
+ } finally {
250
+ loading.value = false
251
+ }
252
+ }
253
+
254
+ function selectCollection(id: string) {
255
+ const query = { ...route.query }
256
+ if (id) {
257
+ query.collection = id
258
+ } else {
259
+ delete query.collection
260
+ }
261
+ router.replace({ query })
262
+ currentView.value = "list"
263
+ if (id) {
264
+ fetchEntries()
265
+ } else {
266
+ entries.value = []
267
+ }
268
+ }
269
+
270
+ function backToList() {
271
+ currentView.value = "list"
272
+ fetchEntries()
273
+ }
274
+
275
+ watch(selectedCollectionId, (newId) => {
276
+ currentView.value = "list"
277
+ if (newId) {
278
+ fetchEntries()
279
+ } else {
280
+ entries.value = []
281
+ }
282
+ })
283
+
284
+ function createNewEntry() {
285
+ const meta: Record<string, any> = {}
286
+ for (const [k, fld] of Object.entries(currentCollectionFields.value)) {
287
+ if (fld.default !== undefined) meta[k] = fld.default
288
+ }
289
+ activeEntry.value = {
290
+ id: "",
291
+ collectionId: selectedCollectionId.value,
292
+ title: "",
293
+ slug: "",
294
+ description: "",
295
+ coverImage: "",
296
+ tags: "",
297
+ order: 0,
298
+ contentMd: "# New Entry\n\nWrite content here...",
299
+ status: "draft",
300
+ metadata: meta,
301
+ }
302
+ scheduledPublishDate.value = ""
303
+ currentView.value = "editor"
304
+ }
305
+
306
+ function editEntry(entry: CMSEntryItem) {
307
+ const meta = entry.metadata ? { ...entry.metadata } : {}
308
+ for (const [k, fld] of Object.entries(currentCollectionFields.value)) {
309
+ if (meta[k] === undefined && fld.default !== undefined) {
310
+ meta[k] = fld.default
311
+ }
312
+ }
313
+ activeEntry.value = {
314
+ ...entry,
315
+ contentMd: entry.contentMd || "",
316
+ metadata: meta,
317
+ }
318
+ if (entry.publishedAt && entry.status === "scheduled") {
319
+ const d = new Date(entry.publishedAt)
320
+ const pad = (n: number) => String(n).padStart(2, "0")
321
+ scheduledPublishDate.value = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`
322
+ } else {
323
+ scheduledPublishDate.value = ""
324
+ }
325
+ currentView.value = "editor"
326
+ }
327
+
328
+ function onTitleChange() {
329
+ if (!activeEntry.value.id) {
330
+ activeEntry.value.slug = activeEntry.value.title
331
+ .toLowerCase()
332
+ .replace(/[^a-z0-9]+/g, "-")
333
+ .replace(/(^-|-$)/g, "")
334
+ }
335
+ }
336
+
337
+ async function saveEntry(targetStatus?: string) {
338
+ saving.value = true
339
+ try {
340
+ const scheduledTimestamp = scheduledPublishDate.value
341
+ ? new Date(scheduledPublishDate.value).getTime()
342
+ : undefined
343
+
344
+ const payload = {
345
+ title: activeEntry.value.title,
346
+ slug: activeEntry.value.slug,
347
+ description: activeEntry.value.description,
348
+ coverImage: activeEntry.value.coverImage,
349
+ tags: activeEntry.value.tags,
350
+ order: Number(activeEntry.value.order) || 0,
351
+ contentMd: activeEntry.value.contentMd,
352
+ status: targetStatus || activeEntry.value.status,
353
+ publishedAt:
354
+ targetStatus === "scheduled"
355
+ ? scheduledTimestamp
356
+ : targetStatus === "published"
357
+ ? activeEntry.value.publishedAt || Date.now()
358
+ : null,
359
+ metadata: activeEntry.value.metadata || {},
360
+ }
361
+
362
+ let res: Response
363
+ if (activeEntry.value.id) {
364
+ res = await fetch(`${apiPath}/${selectedCollectionId.value}/${activeEntry.value.id}`, {
365
+ method: "PUT",
366
+ headers: { "Content-Type": "application/json" },
367
+ body: JSON.stringify(payload),
368
+ })
369
+ } else {
370
+ res = await fetch(`${apiPath}/${selectedCollectionId.value}`, {
371
+ method: "POST",
372
+ headers: { "Content-Type": "application/json" },
373
+ body: JSON.stringify(payload),
374
+ })
375
+ }
376
+
377
+ if (res.ok) {
378
+ const data = await res.json()
379
+ activeEntry.value = data.entry
380
+ alert("Saved successfully!")
381
+ fetchEntries()
382
+ } else {
383
+ const err = await res.json().catch(() => ({}))
384
+ alert(err.message || "Failed to save")
385
+ }
386
+ } finally {
387
+ saving.value = false
388
+ }
389
+ }
390
+
391
+ async function togglePublish(entry: CMSEntryItem) {
392
+ const nextStatus = entry.status === "published" ? "draft" : "published"
393
+ const res = await fetch(`${apiPath}/${selectedCollectionId.value}/${entry.id}/publish`, {
394
+ method: "POST",
395
+ headers: { "Content-Type": "application/json" },
396
+ body: JSON.stringify({ status: nextStatus }),
397
+ })
398
+ if (res.ok) fetchEntries()
399
+ }
400
+
401
+ async function deleteEntry(id: string) {
402
+ if (!confirm("Are you sure you want to delete this entry?")) return
403
+ const res = await fetch(`${apiPath}/${selectedCollectionId.value}/${id}`, {
404
+ method: "DELETE",
405
+ })
406
+ if (res.ok) fetchEntries()
407
+ }
408
+
409
+ async function submitForReview() {
410
+ if (!activeEntry.value.id) {
411
+ await saveEntry("draft")
412
+ }
413
+ if (!activeEntry.value.id) return
414
+ const note = prompt("Optional note for reviewer:")
415
+ saving.value = true
416
+ try {
417
+ const res = await fetch(
418
+ `${apiPath}/${selectedCollectionId.value}/${activeEntry.value.id}/review`,
419
+ {
420
+ method: "POST",
421
+ headers: { "Content-Type": "application/json" },
422
+ body: JSON.stringify({ action: "submit", note: note || undefined }),
423
+ },
424
+ )
425
+ if (res.ok) {
426
+ const data = await res.json()
427
+ activeEntry.value = data.entry
428
+ alert("Submitted for review!")
429
+ fetchEntries()
430
+ } else {
431
+ const err = await res.json().catch(() => ({}))
432
+ alert(err.message || "Failed to submit for review")
433
+ }
434
+ } finally {
435
+ saving.value = false
436
+ }
437
+ }
438
+
439
+ async function rejectReview() {
440
+ if (!activeEntry.value.id) return
441
+ const reason = prompt("Reason for requesting changes:")
442
+ if (reason === null) return
443
+ saving.value = true
444
+ try {
445
+ const res = await fetch(
446
+ `${apiPath}/${selectedCollectionId.value}/${activeEntry.value.id}/review`,
447
+ {
448
+ method: "POST",
449
+ headers: { "Content-Type": "application/json" },
450
+ body: JSON.stringify({ action: "reject", reason: reason || undefined }),
451
+ },
452
+ )
453
+ if (res.ok) {
454
+ const data = await res.json()
455
+ activeEntry.value = data.entry
456
+ alert("Changes requested. Entry reverted to draft.")
457
+ fetchEntries()
458
+ } else {
459
+ const err = await res.json().catch(() => ({}))
460
+ alert(err.message || "Failed to reject review")
461
+ }
462
+ } finally {
463
+ saving.value = false
464
+ }
465
+ }
466
+
467
+ async function getPreviewUrl(): Promise<string | null> {
468
+ if (!activeEntry.value.id) return null
469
+ try {
470
+ const res = await fetch(
471
+ `${apiPath}/${selectedCollectionId.value}/${activeEntry.value.id}/preview-token`,
472
+ { method: "POST" },
473
+ )
474
+ if (res.ok) {
475
+ const data = await res.json()
476
+ const slug = activeEntry.value.slug
477
+ const col = selectedCollectionId.value
478
+ return `${window.location.origin}/${col}/${slug}?preview=true&preview_token=${encodeURIComponent(data.token)}&expires=${data.expires}`
479
+ }
480
+ } catch (e) {
481
+ console.error("Failed to generate preview token:", e)
482
+ }
483
+ return null
484
+ }
485
+
486
+ async function loadPagePreview() {
487
+ if (!activeEntry.value.id) return
488
+ previewLoading.value = true
489
+ const url = await getPreviewUrl()
490
+ if (url) {
491
+ previewIframeUrl.value = url
492
+ }
493
+ previewLoading.value = false
494
+ }
495
+
496
+ async function copyPreviewLink() {
497
+ if (!activeEntry.value.id) return
498
+ const url = await getPreviewUrl()
499
+ if (url) {
500
+ await navigator.clipboard.writeText(url)
501
+ previewCopied.value = true
502
+ setTimeout(() => {
503
+ previewCopied.value = false
504
+ }, 2500)
505
+ }
506
+ }
507
+
508
+ function openPreviewInNewTab() {
509
+ if (previewIframeUrl.value) {
510
+ window.open(previewIframeUrl.value, "_blank")
511
+ } else {
512
+ getPreviewUrl().then((url) => {
513
+ if (url) window.open(url, "_blank")
514
+ })
515
+ }
516
+ }
517
+
518
+ watch([showPreview, previewMode], async ([open, mode]) => {
519
+ if (open && mode === "page" && !previewIframeUrl.value) {
520
+ await loadPagePreview()
521
+ }
522
+ })
523
+
524
+ async function triggerSync() {
525
+ syncing.value = true
526
+ try {
527
+ const res = await fetch(`${apiPath}/sync`, { method: "POST" })
528
+ if (res.ok) {
529
+ const data = await res.json()
530
+ alert(`Sync completed! Added/updated: ${data.synced || 0} entries.`)
531
+ fetchEntries()
532
+ }
533
+ } finally {
534
+ syncing.value = false
535
+ }
536
+ }
537
+
538
+ async function triggerExport() {
539
+ exporting.value = true
540
+ try {
541
+ const res = await fetch(`${apiPath}/export`, { method: "POST" })
542
+ if (res.ok) {
543
+ const data = await res.json()
544
+ alert(`Export completed! Written: ${data.exported || 0} files.`)
545
+ }
546
+ } finally {
547
+ exporting.value = false
548
+ }
549
+ }
550
+
551
+ async function triggerDeploy() {
552
+ if (!confirm("Trigger a site redeploy now?")) return
553
+ deploying.value = true
554
+ try {
555
+ const res = await fetch(`${apiPath}/deploy`, { method: "POST" })
556
+ if (res.ok) {
557
+ const data = await res.json()
558
+ alert("Deploy triggered! (" + (data.triggered || []).join(", ") + ")")
559
+ } else {
560
+ const err = await res.json().catch(() => ({}))
561
+ alert("Deploy failed: " + (err.message || res.statusText))
562
+ }
563
+ } catch (e) {
564
+ alert("Deploy error: " + e)
565
+ } finally {
566
+ deploying.value = false
567
+ }
568
+ }
569
+
570
+ async function createCollection() {
571
+ if (!newCol.value.id || !newCol.value.name) return
572
+ const createdId = newCol.value.id
573
+ const res = await fetch(`${apiPath}/collections`, {
574
+ method: "POST",
575
+ headers: { "Content-Type": "application/json" },
576
+ body: JSON.stringify(newCol.value),
577
+ })
578
+ if (res.ok) {
579
+ showNewCollectionModal.value = false
580
+ newCol.value = { id: "", name: "", description: "" }
581
+ await fetchCollections()
582
+ selectCollection(createdId)
583
+ }
584
+ }
585
+
586
+ const filteredMediaItems = computed(() => {
587
+ if (!mediaSearchQuery.value.trim()) return mediaItems.value
588
+ const q = mediaSearchQuery.value.toLowerCase()
589
+ return mediaItems.value.filter((item) =>
590
+ (item.pathname || item.key || item.url || "").toLowerCase().includes(q),
591
+ )
592
+ })
593
+
594
+ async function fetchMediaFiles() {
595
+ mediaLoading.value = true
596
+ try {
597
+ const res = await fetch("/api/storage/files")
598
+ if (res.ok) {
599
+ const data = await res.json()
600
+ mediaItems.value = data.files || []
601
+ }
602
+ } catch (e) {
603
+ console.error("Failed to load media files:", e)
604
+ } finally {
605
+ mediaLoading.value = false
606
+ }
607
+ }
608
+
609
+ watch(openMediaLibrary, (isOpen) => {
610
+ if (isOpen) {
611
+ fetchMediaFiles()
612
+ }
613
+ })
614
+
615
+ function formatBytes(bytes: number): string {
616
+ if (!bytes || bytes <= 0) return "0 B"
617
+ const k = 1024
618
+ const sizes = ["B", "KB", "MB", "GB"]
619
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
620
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`
621
+ }
622
+
623
+ function copyMediaUrl(url: string) {
624
+ navigator.clipboard.writeText(url)
625
+ mediaCopiedUrl.value = url
626
+ setTimeout(() => {
627
+ if (mediaCopiedUrl.value === url) mediaCopiedUrl.value = null
628
+ }, 2000)
629
+ }
630
+
631
+ async function deleteMediaItem(item: any) {
632
+ if (
633
+ !confirm(`Are you sure you want to delete "${item.pathname || item.key}" from Cloudflare R2?`)
634
+ )
635
+ return
636
+ try {
637
+ const res = await fetch(`/api/storage/file/${encodeURIComponent(item.key)}`, {
638
+ method: "DELETE",
639
+ })
640
+ if (res.ok) {
641
+ mediaItems.value = mediaItems.value.filter((m) => m.key !== item.key)
642
+ }
643
+ } catch (e) {
644
+ console.error("Failed to delete media file:", e)
645
+ }
646
+ }
647
+
648
+ async function handleFileUpload(e: any) {
649
+ const file = e.target.files?.[0]
650
+ if (!file) return
651
+ uploadProgress.value = 10
652
+ const formData = new FormData()
653
+ formData.append("file", file)
654
+ try {
655
+ const res = await fetch("/api/storage/upload", {
656
+ method: "POST",
657
+ body: formData,
658
+ })
659
+ uploadProgress.value = 100
660
+ if (res.ok) {
661
+ const data = await res.json()
662
+ if (data.file) {
663
+ mediaItems.value.unshift(data.file)
664
+ } else {
665
+ await fetchMediaFiles()
666
+ }
667
+ }
668
+ } finally {
669
+ setTimeout(() => {
670
+ uploadProgress.value = 0
671
+ }, 1000)
672
+ }
673
+ }
674
+
675
+ function pickImageForCover() {
676
+ imageTargetField.value = "coverImage"
677
+ openMediaLibrary.value = true
678
+ }
679
+
680
+ function pickImageForField(fieldKey: string) {
681
+ imageTargetField.value = fieldKey
682
+ openMediaLibrary.value = true
683
+ }
684
+
685
+ function selectMediaItem(url: string) {
686
+ if (imageTargetField.value === "coverImage") {
687
+ activeEntry.value.coverImage = url
688
+ } else if (imageTargetField.value) {
689
+ if (!activeEntry.value.metadata) activeEntry.value.metadata = {}
690
+ activeEntry.value.metadata[imageTargetField.value] = url
691
+ } else {
692
+ insertImageMarkdown(url)
693
+ }
694
+ openMediaLibrary.value = false
695
+ imageTargetField.value = null
696
+ }
697
+
698
+ function insertImageMarkdown(url: string) {
699
+ activeEntry.value.contentMd += `\n\n![](${url})\n`
700
+ openMediaLibrary.value = false
701
+ }
702
+
703
+ function formatJsonField(val: any) {
704
+ if (val === undefined || val === null) return ""
705
+ if (typeof val === "string") return val
706
+ try {
707
+ return JSON.stringify(val, null, 2)
708
+ } catch {
709
+ return String(val)
710
+ }
711
+ }
712
+
713
+ function updateJsonField(key: string, text: string) {
714
+ if (!activeEntry.value.metadata) activeEntry.value.metadata = {}
715
+ try {
716
+ activeEntry.value.metadata[key] = JSON.parse(text)
717
+ } catch {
718
+ activeEntry.value.metadata[key] = text
719
+ }
720
+ }
721
+
722
+ const collapsedBlocks = ref<Record<string, boolean>>({})
723
+
724
+ function getBlocksList(key: string | number): any[] {
725
+ if (!activeEntry.value.metadata) activeEntry.value.metadata = {}
726
+ if (!Array.isArray(activeEntry.value.metadata[key])) {
727
+ activeEntry.value.metadata[key] = []
728
+ }
729
+ return activeEntry.value.metadata[key]
730
+ }
731
+
732
+ function isBlockCollapsed(fieldKey: string | number, bIdx: number): boolean {
733
+ return Boolean(collapsedBlocks.value[`${fieldKey}:${bIdx}`])
734
+ }
735
+
736
+ function toggleBlockCollapse(fieldKey: string | number, bIdx: number) {
737
+ const k = `${fieldKey}:${bIdx}`
738
+ collapsedBlocks.value[k] = !collapsedBlocks.value[k]
739
+ }
740
+
741
+ function moveBlock(fieldKey: string | number, bIdx: number, direction: number) {
742
+ const list = getBlocksList(fieldKey)
743
+ const target = bIdx + direction
744
+ if (target < 0 || target >= list.length) return
745
+ const item = list.splice(bIdx, 1)[0]
746
+ list.splice(target, 0, item)
747
+ }
748
+
749
+ function duplicateBlock(fieldKey: string | number, bIdx: number) {
750
+ const list = getBlocksList(fieldKey)
751
+ const block = list[bIdx]
752
+ if (!block) return
753
+ const clone = JSON.parse(JSON.stringify(block))
754
+ clone._id = crypto.randomUUID().slice(0, 8)
755
+ list.splice(bIdx + 1, 0, clone)
756
+ }
757
+
758
+ function deleteBlock(fieldKey: string | number, bIdx: number) {
759
+ const list = getBlocksList(fieldKey)
760
+ list.splice(bIdx, 1)
761
+ }
762
+
763
+ function addBlock(fieldKey: string | number, blockType: string, blockDef?: any) {
764
+ const list = getBlocksList(fieldKey)
765
+ const newBlock: Record<string, any> = {
766
+ _id: crypto.randomUUID().slice(0, 8),
767
+ _type: blockType,
768
+ }
769
+ if (blockDef?.fields) {
770
+ for (const [k, f] of Object.entries<any>(blockDef.fields)) {
771
+ if (f.default !== undefined) {
772
+ newBlock[k] = f.default
773
+ }
774
+ }
775
+ }
776
+ list.push(newBlock)
777
+ }
778
+
779
+ function getBlockMenuItems(fieldKey: string | number, blocksDict: Record<string, any>) {
780
+ return [
781
+ Object.entries(blocksDict).map(([type, def]) => ({
782
+ label: def.label || type,
783
+ icon: def.icon || "i-lucide-box",
784
+ onSelect: () => addBlock(fieldKey, type, def),
785
+ })),
786
+ ]
787
+ }
788
+
789
+ function updateBlockJsonField(block: any, fieldKey: string, text: string) {
790
+ try {
791
+ block[fieldKey] = JSON.parse(text)
792
+ } catch {
793
+ block[fieldKey] = text
794
+ }
795
+ }
796
+
797
+ function copyToClipboard(text: string) {
798
+ navigator.clipboard.writeText(text)
799
+ alert("URL copied!")
800
+ }
801
+
802
+ function formatDate(ts?: number | null) {
803
+ if (!ts) return "—"
804
+ return new Date(ts).toLocaleDateString("en-US", {
805
+ month: "short",
806
+ day: "numeric",
807
+ })
808
+ }
809
+
810
+ function formatDateTime(ts?: number | null) {
811
+ if (!ts) return ""
812
+ return new Date(ts).toLocaleString()
813
+ }
814
+
815
+ function getStatusBadgeColor(status: string): "success" | "warning" | "info" | "error" | "neutral" {
816
+ if (status === "published") return "success"
817
+ if (status === "pending_review") return "warning"
818
+ if (status === "scheduled") return "info"
819
+ if (status === "archived") return "error"
820
+ return "neutral"
821
+ }
822
+
823
+ function formatStatus(status: string) {
824
+ if (status === "pending_review") return "Pending Review"
825
+ if (status === "scheduled") return "Scheduled"
826
+ if (status === "published") return "Published"
827
+ if (status === "archived") return "Archived"
828
+ return "Draft"
829
+ }
830
+
831
+ let searchTimeout: any
832
+ function debouncedSearch() {
833
+ clearTimeout(searchTimeout)
834
+ searchTimeout = setTimeout(async () => {
835
+ if (!searchQuery.value) {
836
+ fetchEntries()
837
+ return
838
+ }
839
+ loading.value = true
840
+ try {
841
+ const res = await fetch(
842
+ `${apiPath}/search?q=${encodeURIComponent(searchQuery.value)}&collection=${selectedCollectionId.value}`,
843
+ )
844
+ const data = await res.json()
845
+ entries.value = data.results || []
846
+ } finally {
847
+ loading.value = false
848
+ }
849
+ }, 300)
850
+ }
851
+
852
+ function logout() {
853
+ fetch("/api/auth/logout", { method: "POST" }).then(() => {
854
+ window.location.reload()
855
+ })
856
+ }
857
+
858
+ onMounted(() => {
859
+ fetchUser()
860
+ fetchCollections()
861
+ })
862
+ </script>
863
+
864
+ <template>
865
+ <UDashboardGroup>
866
+ <!-- Left Sidebar: Collections & Management / Entry Settings -->
867
+ <UDashboardSidebar resizable collapsible :ui="{ header: 'border-b border-default' }">
868
+ <template #header="{ collapsed }">
869
+ <div
870
+ v-if="currentView === 'editor'"
871
+ class="flex items-center justify-between px-2 py-1 w-full overflow-hidden"
872
+ >
873
+ <div class="flex items-center gap-2 truncate">
874
+ <UButton
875
+ size="xs"
876
+ color="neutral"
877
+ variant="ghost"
878
+ icon="i-lucide-arrow-left"
879
+ @click="backToList"
880
+ />
881
+ <span
882
+ v-if="!collapsed"
883
+ class="font-bold text-xs uppercase text-slate-300 tracking-wider truncate"
884
+ >
885
+ Entry Settings
886
+ </span>
887
+ </div>
888
+ <UBadge
889
+ v-if="!collapsed"
890
+ size="xs"
891
+ variant="subtle"
892
+ :color="getStatusBadgeColor(activeEntry.status)"
893
+ >
894
+ {{ formatStatus(activeEntry.status) }}
895
+ </UBadge>
896
+ </div>
897
+ <div v-else class="flex items-center gap-2 px-2 py-1 w-full overflow-hidden">
898
+ <span class="inline-block w-2.5 h-2.5 rounded-full bg-emerald-500 shrink-0"></span>
899
+ <span
900
+ v-if="!collapsed"
901
+ class="font-bold text-sm text-white flex items-center gap-1.5 truncate"
902
+ >
903
+ Qarpeo <UBadge size="xs" variant="subtle" color="primary">CMS</UBadge>
904
+ </span>
905
+ </div>
906
+ </template>
907
+
908
+ <!-- 1. Collections list (List Mode) -->
909
+ <div v-if="currentView === 'list'" class="space-y-1">
910
+ <UButton
911
+ label="Dashboard Overview"
912
+ size="xs"
913
+ color="neutral"
914
+ variant="ghost"
915
+ :active="!selectedCollectionId"
916
+ active-color="primary"
917
+ active-variant="subtle"
918
+ icon="i-lucide-layout-dashboard"
919
+ class="w-full justify-start mb-2"
920
+ @click="selectCollection('')"
921
+ />
922
+
923
+ <div class="flex items-center justify-between py-1 mb-1">
924
+ <span class="text-xs uppercase font-semibold text-slate-400 tracking-wider"
925
+ >Collections</span
926
+ >
927
+ <UButton
928
+ label="New"
929
+ size="xs"
930
+ color="primary"
931
+ variant="ghost"
932
+ icon="i-lucide-plus"
933
+ @click="showNewCollectionModal = true"
934
+ />
935
+ </div>
936
+
937
+ <UNavigationMenu
938
+ :items="collectionNavItems"
939
+ orientation="vertical"
940
+ :model-value="selectedCollectionId"
941
+ highlight
942
+ highlight-color="primary"
943
+ color="neutral"
944
+ />
945
+ </div>
946
+
947
+ <!-- 2. Entry Settings / Metadata Inputs (Editor Mode) -->
948
+ <div v-else-if="currentView === 'editor'" class="space-y-4 p-1 overflow-y-auto">
949
+ <UFormField label="Title" name="title">
950
+ <UInput v-model="activeEntry.title" class="w-full" @input="onTitleChange" />
951
+ </UFormField>
952
+
953
+ <UFormField label="Slug" name="slug">
954
+ <UInput v-model="activeEntry.slug" class="w-full font-mono text-xs" />
955
+ </UFormField>
956
+
957
+ <UFormField label="Description / Excerpt" name="description">
958
+ <UTextarea v-model="activeEntry.description" :rows="2" class="w-full" />
959
+ </UFormField>
960
+
961
+ <UFormField label="Cover Image" name="coverImage">
962
+ <div class="flex gap-2">
963
+ <UInput
964
+ v-model="activeEntry.coverImage"
965
+ placeholder="/api/storage/file/..."
966
+ class="flex-1"
967
+ />
968
+ <UButton size="xs" color="neutral" variant="outline" @click="pickImageForCover"
969
+ >Pick</UButton
970
+ >
971
+ </div>
972
+ <div v-if="activeEntry.coverImage" class="mt-2">
973
+ <img
974
+ :src="activeEntry.coverImage"
975
+ class="w-full h-24 object-cover rounded border border-default"
976
+ />
977
+ </div>
978
+ </UFormField>
979
+
980
+ <UFormField label="Tags (comma-separated)" name="tags">
981
+ <UInput v-model="activeEntry.tags" placeholder="news, edge, cloudflare" class="w-full" />
982
+ </UFormField>
983
+
984
+ <UFormField label="Order" name="order">
985
+ <UInput v-model.number="activeEntry.order" type="number" class="w-full" />
986
+ </UFormField>
987
+
988
+ <UFormField
989
+ label="Schedule Publish"
990
+ name="scheduledPublishDate"
991
+ :help="
992
+ activeEntry.publishedAt && activeEntry.status === 'scheduled'
993
+ ? `Scheduled for ${formatDateTime(activeEntry.publishedAt)}`
994
+ : undefined
995
+ "
996
+ >
997
+ <UInput v-model="scheduledPublishDate" type="datetime-local" class="w-full" />
998
+ </UFormField>
999
+
1000
+ <!-- Custom Dynamic Fields -->
1001
+ <div v-if="hasCustomFields" class="pt-3 border-t border-default space-y-3">
1002
+ <div class="flex items-center justify-between">
1003
+ <span class="text-xs font-semibold text-slate-300 uppercase tracking-wider"
1004
+ >Custom Fields</span
1005
+ >
1006
+ <span class="text-[10px] text-slate-500 font-mono"
1007
+ >{{ Object.keys(currentCollectionFields).length }} fields</span
1008
+ >
1009
+ </div>
1010
+
1011
+ <UFormField
1012
+ v-for="(field, key) in currentCollectionFields"
1013
+ :key="key"
1014
+ :label="field.label || String(key)"
1015
+ :description="field.description"
1016
+ :required="field.required"
1017
+ :name="String(key)"
1018
+ >
1019
+ <!-- 1. String with options -->
1020
+ <template v-if="field.type === 'string' && field.options && field.options.length">
1021
+ <USelect
1022
+ v-model="activeEntry.metadata![key]"
1023
+ :items="field.options"
1024
+ label-key="label"
1025
+ value-key="value"
1026
+ placeholder="Select option..."
1027
+ class="w-full"
1028
+ />
1029
+ </template>
1030
+
1031
+ <!-- 2. String standard -->
1032
+ <template v-else-if="field.type === 'string'">
1033
+ <UInput
1034
+ v-model="activeEntry.metadata![key]"
1035
+ :placeholder="field.default ? String(field.default) : ''"
1036
+ class="w-full"
1037
+ />
1038
+ </template>
1039
+
1040
+ <!-- 3. Text multi-line -->
1041
+ <template v-else-if="field.type === 'text'">
1042
+ <UTextarea v-model="activeEntry.metadata![key]" :rows="2" class="w-full" />
1043
+ </template>
1044
+
1045
+ <!-- 4. Number -->
1046
+ <template v-else-if="field.type === 'number'">
1047
+ <UInput v-model.number="activeEntry.metadata![key]" type="number" class="w-full" />
1048
+ </template>
1049
+
1050
+ <!-- 5. Boolean -->
1051
+ <template v-else-if="field.type === 'boolean'">
1052
+ <div class="flex items-center gap-2 pt-1">
1053
+ <USwitch v-model="activeEntry.metadata![key]" />
1054
+ <span class="text-xs text-slate-300">{{
1055
+ activeEntry.metadata![key] ? "Enabled" : "Disabled"
1056
+ }}</span>
1057
+ </div>
1058
+ </template>
1059
+
1060
+ <!-- 6. Date -->
1061
+ <template v-else-if="field.type === 'date'">
1062
+ <UInput v-model="activeEntry.metadata![key]" type="date" class="w-full" />
1063
+ </template>
1064
+
1065
+ <!-- 7. Image -->
1066
+ <template v-else-if="field.type === 'image'">
1067
+ <div class="flex gap-2">
1068
+ <UInput
1069
+ v-model="activeEntry.metadata![key]"
1070
+ placeholder="/api/storage/file/..."
1071
+ class="flex-1"
1072
+ />
1073
+ <UButton
1074
+ size="xs"
1075
+ color="neutral"
1076
+ variant="outline"
1077
+ @click="pickImageForField(String(key))"
1078
+ >Pick</UButton
1079
+ >
1080
+ </div>
1081
+ <div v-if="activeEntry.metadata![key]" class="mt-1">
1082
+ <img
1083
+ :src="activeEntry.metadata![key]"
1084
+ class="w-16 h-16 object-cover rounded border border-default bg-muted"
1085
+ />
1086
+ </div>
1087
+ </template>
1088
+
1089
+ <!-- 8. Tags -->
1090
+ <template v-else-if="field.type === 'tags'">
1091
+ <UInput
1092
+ v-model="activeEntry.metadata![key]"
1093
+ placeholder="tag1, tag2"
1094
+ class="w-full"
1095
+ />
1096
+ </template>
1097
+
1098
+ <!-- 9. JSON -->
1099
+ <template v-else-if="field.type === 'json'">
1100
+ <UTextarea
1101
+ :model-value="formatJsonField(activeEntry.metadata![key])"
1102
+ :rows="3"
1103
+ class="w-full font-mono text-[11px]"
1104
+ @update:model-value="updateJsonField(String(key), $event)"
1105
+ />
1106
+ </template>
1107
+
1108
+ <!-- 10. Blocks / Dynamic Zones (Page Builder) -->
1109
+ <template v-else-if="field.type === 'blocks'">
1110
+ <div class="space-y-3 pt-1">
1111
+ <div
1112
+ v-for="(block, bIdx) in getBlocksList(key)"
1113
+ :key="block._id || bIdx"
1114
+ class="border border-default rounded-lg bg-muted/20 overflow-hidden"
1115
+ >
1116
+ <!-- Block Header -->
1117
+ <div
1118
+ class="flex items-center justify-between px-3 py-2 bg-muted/50 border-b border-default select-none"
1119
+ >
1120
+ <div
1121
+ class="flex items-center gap-2 cursor-pointer flex-1"
1122
+ @click="toggleBlockCollapse(key, bIdx)"
1123
+ >
1124
+ <UIcon
1125
+ :name="
1126
+ isBlockCollapsed(key, bIdx)
1127
+ ? 'i-lucide-chevron-right'
1128
+ : 'i-lucide-chevron-down'
1129
+ "
1130
+ class="w-4 h-4 text-muted-foreground"
1131
+ />
1132
+ <UIcon
1133
+ :name="field.blocks?.[block._type]?.icon || 'i-lucide-box'"
1134
+ class="w-4 h-4 text-primary"
1135
+ />
1136
+ <span class="font-medium text-xs">
1137
+ {{ field.blocks?.[block._type]?.label || block._type }}
1138
+ </span>
1139
+ <UBadge size="xs" variant="subtle" color="neutral">
1140
+ #{{ bIdx + 1 }}
1141
+ </UBadge>
1142
+ </div>
1143
+
1144
+ <div class="flex items-center gap-1">
1145
+ <UButton
1146
+ size="xs"
1147
+ color="neutral"
1148
+ variant="ghost"
1149
+ icon="i-lucide-arrow-up"
1150
+ :disabled="bIdx === 0"
1151
+ @click="moveBlock(key, bIdx, -1)"
1152
+ />
1153
+ <UButton
1154
+ size="xs"
1155
+ color="neutral"
1156
+ variant="ghost"
1157
+ icon="i-lucide-arrow-down"
1158
+ :disabled="bIdx === getBlocksList(key).length - 1"
1159
+ @click="moveBlock(key, bIdx, 1)"
1160
+ />
1161
+ <UButton
1162
+ size="xs"
1163
+ color="neutral"
1164
+ variant="ghost"
1165
+ icon="i-lucide-copy"
1166
+ @click="duplicateBlock(key, bIdx)"
1167
+ />
1168
+ <UButton
1169
+ size="xs"
1170
+ color="error"
1171
+ variant="ghost"
1172
+ icon="i-lucide-trash-2"
1173
+ @click="deleteBlock(key, bIdx)"
1174
+ />
1175
+ </div>
1176
+ </div>
1177
+
1178
+ <!-- Block Fields -->
1179
+ <div v-show="!isBlockCollapsed(key, bIdx)" class="p-3 space-y-3 bg-card/30">
1180
+ <div
1181
+ v-for="(subField, subKey) in (field.blocks?.[block._type]?.fields || {})"
1182
+ :key="subKey"
1183
+ class="space-y-1"
1184
+ >
1185
+ <label class="text-[11px] font-medium text-muted-foreground">
1186
+ {{ subField.label || subKey }}
1187
+ <span v-if="subField.required" class="text-error">*</span>
1188
+ </label>
1189
+
1190
+ <template v-if="subField.type === 'string' && subField.options?.length">
1191
+ <USelect
1192
+ v-model="block[subKey]"
1193
+ :items="subField.options"
1194
+ label-key="label"
1195
+ value-key="value"
1196
+ class="w-full"
1197
+ />
1198
+ </template>
1199
+ <template v-else-if="subField.type === 'string'">
1200
+ <UInput v-model="block[subKey]" class="w-full" />
1201
+ </template>
1202
+ <template v-else-if="subField.type === 'text'">
1203
+ <UTextarea v-model="block[subKey]" :rows="2" class="w-full" />
1204
+ </template>
1205
+ <template v-else-if="subField.type === 'number'">
1206
+ <UInput v-model.number="block[subKey]" type="number" class="w-full" />
1207
+ </template>
1208
+ <template v-else-if="subField.type === 'boolean'">
1209
+ <div class="flex items-center gap-2 pt-1">
1210
+ <USwitch v-model="block[subKey]" />
1211
+ <span class="text-xs text-muted-foreground">
1212
+ {{ block[subKey] ? "Enabled" : "Disabled" }}
1213
+ </span>
1214
+ </div>
1215
+ </template>
1216
+ <template v-else-if="subField.type === 'date'">
1217
+ <UInput v-model="block[subKey]" type="date" class="w-full" />
1218
+ </template>
1219
+ <template v-else-if="subField.type === 'image'">
1220
+ <UInput
1221
+ v-model="block[subKey]"
1222
+ placeholder="/api/storage/file/..."
1223
+ class="w-full"
1224
+ />
1225
+ </template>
1226
+ <template v-else-if="subField.type === 'tags'">
1227
+ <UInput v-model="block[subKey]" placeholder="tag1, tag2" class="w-full" />
1228
+ </template>
1229
+ <template v-else-if="subField.type === 'json'">
1230
+ <UTextarea
1231
+ :model-value="formatJsonField(block[subKey])"
1232
+ :rows="2"
1233
+ class="w-full font-mono text-[11px]"
1234
+ @update:model-value="updateBlockJsonField(block, String(subKey), $event)"
1235
+ />
1236
+ </template>
1237
+ </div>
1238
+ </div>
1239
+ </div>
1240
+
1241
+ <!-- Add Block Button -->
1242
+ <div
1243
+ v-if="field.blocks && Object.keys(field.blocks).length"
1244
+ class="flex flex-wrap items-center gap-2 pt-1"
1245
+ >
1246
+ <UDropdownMenu
1247
+ :items="getBlockMenuItems(key, field.blocks)"
1248
+ :content="{ align: 'start' }"
1249
+ >
1250
+ <UButton size="xs" color="primary" variant="soft" icon="i-lucide-plus">
1251
+ Add Block
1252
+ </UButton>
1253
+ </UDropdownMenu>
1254
+ <span class="text-[11px] text-muted-foreground">
1255
+ {{ getBlocksList(key).length }} block(s) added
1256
+ </span>
1257
+ </div>
1258
+ </div>
1259
+ </template>
1260
+ </UFormField>
1261
+ </div>
1262
+ </div>
1263
+
1264
+ <template #footer="{ collapsed }">
1265
+ <div v-if="currentView === 'editor'" class="p-2 border-t border-default w-full">
1266
+ <UButton
1267
+ size="xs"
1268
+ color="neutral"
1269
+ variant="ghost"
1270
+ icon="i-lucide-arrow-left"
1271
+ block
1272
+ @click="backToList"
1273
+ >
1274
+ <span v-if="!collapsed">Back to Collections</span>
1275
+ </UButton>
1276
+ </div>
1277
+ <div
1278
+ v-else-if="user"
1279
+ class="p-2 flex items-center justify-between w-full border-t border-default"
1280
+ >
1281
+ <div v-if="!collapsed" class="flex flex-col truncate pr-2">
1282
+ <span class="text-xs font-medium text-white truncate">{{
1283
+ user.name || user.email
1284
+ }}</span>
1285
+ <span class="text-[10px] text-slate-400 truncate">{{ user.role || "user" }}</span>
1286
+ </div>
1287
+ <UButton
1288
+ size="xs"
1289
+ color="error"
1290
+ variant="ghost"
1291
+ icon="i-lucide-log-out"
1292
+ @click="logout"
1293
+ />
1294
+ </div>
1295
+ </template>
1296
+ </UDashboardSidebar>
1297
+
1298
+ <!-- Main Content Panel -->
1299
+ <UDashboardPanel>
1300
+ <template #header>
1301
+ <UDashboardNavbar>
1302
+ <template #title>
1303
+ <div class="flex items-center gap-2">
1304
+ <span class="font-semibold text-sm text-white">
1305
+ {{
1306
+ currentView === "editor"
1307
+ ? activeEntry.title || "Untitled Entry"
1308
+ : currentCollection?.name || "Dashboard Overview"
1309
+ }}
1310
+ </span>
1311
+ <UBadge
1312
+ v-if="currentView === 'editor'"
1313
+ size="sm"
1314
+ variant="subtle"
1315
+ :color="getStatusBadgeColor(activeEntry.status)"
1316
+ >
1317
+ {{ formatStatus(activeEntry.status) }}
1318
+ </UBadge>
1319
+ <UBadge
1320
+ v-if="currentView === 'editor' && isGitSync"
1321
+ label="🌿 Git-sync active"
1322
+ size="xs"
1323
+ color="success"
1324
+ variant="subtle"
1325
+ />
1326
+ <UBadge
1327
+ v-if="currentView === 'editor' && hasApproval"
1328
+ label="🛡️ Approval required"
1329
+ size="xs"
1330
+ color="primary"
1331
+ variant="subtle"
1332
+ />
1333
+ </div>
1334
+ </template>
1335
+
1336
+ <template #right>
1337
+ <div class="flex items-center space-x-2">
1338
+ <!-- When in editor view, back button & editorial actions -->
1339
+ <template v-if="currentView === 'editor'">
1340
+ <UButton
1341
+ size="xs"
1342
+ color="neutral"
1343
+ variant="ghost"
1344
+ icon="i-lucide-arrow-left"
1345
+ @click="backToList"
1346
+ >
1347
+ Back
1348
+ </UButton>
1349
+
1350
+ <UButton
1351
+ size="xs"
1352
+ color="neutral"
1353
+ variant="outline"
1354
+ icon="i-lucide-link"
1355
+ @click="copyPreviewLink"
1356
+ >
1357
+ Share Preview
1358
+ </UButton>
1359
+
1360
+ <template v-if="!isFileLocked">
1361
+ <!-- Approval Workflow -->
1362
+ <template v-if="hasApproval">
1363
+ <UButton
1364
+ size="xs"
1365
+ color="neutral"
1366
+ variant="outline"
1367
+ :loading="saving"
1368
+ @click="saveEntry('draft')"
1369
+ >
1370
+ Save Draft
1371
+ </UButton>
1372
+ <UButton
1373
+ v-if="scheduledPublishDate && canApprove"
1374
+ size="xs"
1375
+ color="secondary"
1376
+ icon="i-lucide-clock"
1377
+ :loading="saving"
1378
+ @click="saveEntry('scheduled')"
1379
+ >
1380
+ Schedule
1381
+ </UButton>
1382
+ <UButton
1383
+ v-if="activeEntry.status === 'draft'"
1384
+ size="xs"
1385
+ color="primary"
1386
+ icon="i-lucide-send"
1387
+ :loading="saving"
1388
+ @click="submitForReview"
1389
+ >
1390
+ Submit for Review
1391
+ </UButton>
1392
+ <template v-else-if="activeEntry.status === 'pending_review'">
1393
+ <UButton
1394
+ v-if="canApprove"
1395
+ size="xs"
1396
+ color="error"
1397
+ variant="outline"
1398
+ :loading="saving"
1399
+ @click="rejectReview"
1400
+ >
1401
+ Request Changes
1402
+ </UButton>
1403
+ <UButton
1404
+ v-if="canApprove"
1405
+ size="xs"
1406
+ color="success"
1407
+ icon="i-lucide-check"
1408
+ :loading="saving"
1409
+ @click="saveEntry('published')"
1410
+ >
1411
+ Approve & Publish
1412
+ </UButton>
1413
+ <UBadge v-else color="warning" variant="subtle">
1414
+ ⏳ Awaiting review
1415
+ {{
1416
+ isAuthor && !activeApproval?.allowSelfApproval
1417
+ ? "(Self-approval disabled)"
1418
+ : ""
1419
+ }}
1420
+ </UBadge>
1421
+ </template>
1422
+ <template v-else-if="activeEntry.status === 'published'">
1423
+ <UButton
1424
+ size="xs"
1425
+ color="neutral"
1426
+ variant="outline"
1427
+ :loading="saving"
1428
+ @click="saveEntry('draft')"
1429
+ >
1430
+ Unpublish
1431
+ </UButton>
1432
+ <UButton
1433
+ v-if="canApprove"
1434
+ size="xs"
1435
+ color="success"
1436
+ :loading="saving"
1437
+ @click="saveEntry('published')"
1438
+ >
1439
+ Publish Changes
1440
+ </UButton>
1441
+ </template>
1442
+ </template>
1443
+
1444
+ <!-- Standard Workflow -->
1445
+ <template v-else>
1446
+ <UButton
1447
+ size="xs"
1448
+ color="neutral"
1449
+ variant="outline"
1450
+ :loading="saving"
1451
+ @click="saveEntry('draft')"
1452
+ >
1453
+ Save Draft
1454
+ </UButton>
1455
+ <UButton
1456
+ v-if="scheduledPublishDate"
1457
+ size="xs"
1458
+ color="secondary"
1459
+ icon="i-lucide-clock"
1460
+ :loading="saving"
1461
+ @click="saveEntry('scheduled')"
1462
+ >
1463
+ Schedule
1464
+ </UButton>
1465
+ <UButton
1466
+ size="xs"
1467
+ color="success"
1468
+ icon="i-lucide-upload"
1469
+ :loading="saving"
1470
+ @click="saveEntry('published')"
1471
+ >
1472
+ Publish
1473
+ </UButton>
1474
+ </template>
1475
+ </template>
1476
+ <UBadge v-else color="warning" variant="subtle">
1477
+ 🔒 File-locked — edit source files
1478
+ </UBadge>
1479
+ </template>
1480
+
1481
+ <!-- Global actions -->
1482
+ <UButton
1483
+ size="xs"
1484
+ color="neutral"
1485
+ variant="outline"
1486
+ icon="i-lucide-image"
1487
+ @click="openMediaLibrary = true"
1488
+ >
1489
+ Media
1490
+ </UButton>
1491
+
1492
+ <UButton
1493
+ size="xs"
1494
+ color="neutral"
1495
+ variant="outline"
1496
+ icon="i-lucide-refresh-cw"
1497
+ :loading="syncing"
1498
+ @click="triggerSync"
1499
+ >
1500
+ Sync Files
1501
+ </UButton>
1502
+
1503
+ <UButton
1504
+ size="xs"
1505
+ color="neutral"
1506
+ variant="outline"
1507
+ icon="i-lucide-download"
1508
+ :loading="exporting"
1509
+ @click="triggerExport"
1510
+ >
1511
+ Export
1512
+ </UButton>
1513
+
1514
+ <UButton
1515
+ v-if="hasDeployTrigger"
1516
+ size="xs"
1517
+ color="primary"
1518
+ variant="solid"
1519
+ icon="i-lucide-rocket"
1520
+ :loading="deploying"
1521
+ @click="triggerDeploy"
1522
+ >
1523
+ Deploy
1524
+ </UButton>
1525
+ </div>
1526
+ </template>
1527
+ </UDashboardNavbar>
1528
+
1529
+ <!-- List View Toolbar inside DashboardPanel Header (When collection is selected) -->
1530
+ <UDashboardToolbar v-if="currentView === 'list' && selectedCollectionId">
1531
+ <template #left>
1532
+ <div class="flex items-center gap-3">
1533
+ <UInput
1534
+ v-model="searchQuery"
1535
+ placeholder="Search entries..."
1536
+ class="w-64"
1537
+ @input="debouncedSearch"
1538
+ />
1539
+ <USelect
1540
+ v-model="statusFilter"
1541
+ :items="statusOptions"
1542
+ class="w-40"
1543
+ @update:model-value="fetchEntries"
1544
+ />
1545
+ </div>
1546
+ </template>
1547
+
1548
+ <template #right>
1549
+ <div class="flex items-center gap-2">
1550
+ <UButton
1551
+ v-if="!isFileLocked"
1552
+ color="primary"
1553
+ icon="i-lucide-plus"
1554
+ size="sm"
1555
+ @click="createNewEntry"
1556
+ >
1557
+ New Entry
1558
+ </UButton>
1559
+ <UBadge v-else color="warning" variant="subtle" class="gap-1.5 py-1 px-2.5">
1560
+ 🔒 File-locked — read-only
1561
+ </UBadge>
1562
+ </div>
1563
+ </template>
1564
+ </UDashboardToolbar>
1565
+ </template>
1566
+
1567
+ <template #body>
1568
+ <!-- 0. DASHBOARD OVERVIEW (When no collection is selected in URL) -->
1569
+ <div v-if="currentView === 'list' && !selectedCollectionId">
1570
+ <!-- Top Row: Metric Cards (flex-1) + Quick Actions sidebar -->
1571
+ <div class="flex flex-col md:flex-row gap-6 items-start">
1572
+ <!-- Left: Metric Cards -->
1573
+ <div class="flex-1 min-w-0">
1574
+ <ProseCardGroup>
1575
+ <ProseCard
1576
+ title="Total Collections"
1577
+ :description="collections.length"
1578
+ icon="i-lucide-folder"
1579
+ color="primary"
1580
+ to="https://nuxt.lemonsqueezy.com"
1581
+ target="_blank"
1582
+ />
1583
+ <ProseCard
1584
+ title="Media Storage"
1585
+ description="Cloudflare R2"
1586
+ icon="i-lucide-image"
1587
+ color="primary"
1588
+ to="https://nuxt.lemonsqueezy.com"
1589
+ target="_blank"
1590
+ />
1591
+ <ProseCard
1592
+ title="CMS Mode"
1593
+ description="Edge + Local DB"
1594
+ icon="i-lucide-database"
1595
+ color="primary"
1596
+ to="https://nuxt.lemonsqueezy.com"
1597
+ target="_blank"
1598
+ />
1599
+ <ProseCard
1600
+ title="Deploy Trigger"
1601
+ :description="hasDeployTrigger ? 'Active' : 'Not configured'"
1602
+ icon="i-lucide-rocket"
1603
+ color="primary"
1604
+ to="https://nuxt.lemonsqueezy.com"
1605
+ target="_blank"
1606
+ />
1607
+ </ProseCardGroup>
1608
+ </div>
1609
+
1610
+ <!-- Right: Quick Actions Box List -->
1611
+ <div class="w-full md:w-64 lg:w-72 shrink-0 space-y-4">
1612
+ <UCard>
1613
+ <template #header>
1614
+ <div class="flex items-center justify-between">
1615
+ <span class="text-xs font-semibold text-white uppercase tracking-wider"
1616
+ >Quick Actions</span
1617
+ >
1618
+ <UIcon name="i-lucide-zap" class="w-3.5 h-3.5 text-primary" />
1619
+ </div>
1620
+ </template>
1621
+
1622
+ <UButton
1623
+ size="sm"
1624
+ color="neutral"
1625
+ variant="ghost"
1626
+ icon="i-lucide-image"
1627
+ block
1628
+ class="justify-start text-xs py-2"
1629
+ @click="openMediaLibrary = true"
1630
+ >
1631
+ <div class="text-left flex-1 min-w-0">
1632
+ <p class="font-medium text-white truncate">Media Library</p>
1633
+ <p class="text-[10px] text-muted truncate">Upload & browse files</p>
1634
+ </div>
1635
+ </UButton>
1636
+
1637
+ <UButton
1638
+ size="sm"
1639
+ color="neutral"
1640
+ variant="ghost"
1641
+ icon="i-lucide-refresh-cw"
1642
+ block
1643
+ class="justify-start text-xs py-2"
1644
+ :loading="syncing"
1645
+ @click="triggerSync"
1646
+ >
1647
+ <div class="text-left flex-1 min-w-0">
1648
+ <p class="font-medium text-white truncate">Sync Files</p>
1649
+ <p class="text-[10px] text-muted truncate">Sync markdown content</p>
1650
+ </div>
1651
+ </UButton>
1652
+
1653
+ <UButton
1654
+ size="sm"
1655
+ color="neutral"
1656
+ variant="ghost"
1657
+ icon="i-lucide-download"
1658
+ block
1659
+ class="justify-start text-xs py-2"
1660
+ :loading="exporting"
1661
+ @click="triggerExport"
1662
+ >
1663
+ <div class="text-left flex-1 min-w-0">
1664
+ <p class="font-medium text-white truncate">Export Content</p>
1665
+ <p class="text-[10px] text-muted truncate">Download backup archive</p>
1666
+ </div>
1667
+ </UButton>
1668
+
1669
+ <UButton
1670
+ v-if="hasDeployTrigger"
1671
+ size="sm"
1672
+ color="neutral"
1673
+ variant="ghost"
1674
+ icon="i-lucide-rocket"
1675
+ block
1676
+ class="justify-start text-xs py-2"
1677
+ :loading="deploying"
1678
+ @click="triggerDeploy"
1679
+ >
1680
+ <div class="text-left flex-1 min-w-0">
1681
+ <p class="font-medium text-white truncate">Deploy Site</p>
1682
+ <p class="text-[10px] text-muted truncate">Trigger remote build</p>
1683
+ </div>
1684
+ </UButton>
1685
+ </UCard>
1686
+ </div>
1687
+ </div>
1688
+
1689
+ <!-- Collections Section (full width below) -->
1690
+ <div class="space-y-4">
1691
+ <UDashboardToolbar>
1692
+ <div>
1693
+ <h3 class="font-semibold text-sm text-white">Content Collections</h3>
1694
+ <p class="text-xs text-muted">Choose a collection to manage or create entries</p>
1695
+ </div>
1696
+ <UButton
1697
+ label="New Collection"
1698
+ icon="i-lucide-plus"
1699
+ @click="showNewCollectionModal = true"
1700
+ />
1701
+ </UDashboardToolbar>
1702
+ <UBlogPosts class="grid grid-cols-1 sm:grid-cols-2 gap-4">
1703
+ <UBlogPost
1704
+ v-for="col in collections"
1705
+ :key="col.id"
1706
+ :description="col.description || 'No description provided'"
1707
+ :title="col.name"
1708
+ :badge="{
1709
+ label: `${col.source || 'db'} | ${col.mode === 'file-lock' ? '🔒 File-locked' : col.mode === 'git-sync' ? '🌿 Git-sync' : '💾 Database'}`,
1710
+ }"
1711
+ :to="{ query: { ...route.query, collection: col.id } }"
1712
+ />
1713
+ </UBlogPosts>
1714
+ </div>
1715
+ </div>
1716
+
1717
+ <!-- 1. ENTRIES LIST VIEW (When a collection is selected in URL) -->
1718
+ <div v-else-if="currentView === 'list' && selectedCollectionId">
1719
+ <div class="flex-1 overflow-auto p-4 min-w-0">
1720
+ <UTable :data="entries" :columns="tableColumns" :loading="loading" class="w-full">
1721
+ <template #empty>
1722
+ <div class="text-center py-12 text-slate-500 text-sm">
1723
+ No entries found. Click "+ New Entry" to create one.
1724
+ </div>
1725
+ </template>
1726
+
1727
+ <template #title-cell="{ row }">
1728
+ <span
1729
+ class="font-medium text-white cursor-pointer hover:text-indigo-400"
1730
+ @click="editEntry(row.original)"
1731
+ >
1732
+ {{ row.original.title }}
1733
+ </span>
1734
+ </template>
1735
+
1736
+ <template #slug-cell="{ row }">
1737
+ <span class="font-mono text-xs text-slate-400">
1738
+ {{ row.original.slug }}
1739
+ </span>
1740
+ </template>
1741
+
1742
+ <template #status-cell="{ row }">
1743
+ <UBadge
1744
+ size="xs"
1745
+ variant="subtle"
1746
+ :color="getStatusBadgeColor(row.original.status)"
1747
+ >
1748
+ {{ formatStatus(row.original.status) }}
1749
+ </UBadge>
1750
+ </template>
1751
+
1752
+ <template #updatedAt-cell="{ row }">
1753
+ <span class="text-xs text-slate-500">
1754
+ {{ formatDate(row.original.updatedAt) }}
1755
+ </span>
1756
+ </template>
1757
+
1758
+ <template #actions-cell="{ row }">
1759
+ <div class="flex items-center justify-end space-x-2">
1760
+ <UButton
1761
+ size="xs"
1762
+ color="neutral"
1763
+ variant="ghost"
1764
+ @click="editEntry(row.original)"
1765
+ >
1766
+ Edit
1767
+ </UButton>
1768
+ <UButton
1769
+ v-if="!isFileLocked"
1770
+ :label="row.original.status === 'published' ? 'Unpublish' : 'Publish'"
1771
+ size="xs"
1772
+ :color="row.original.status === 'published' ? 'warning' : 'success'"
1773
+ variant="ghost"
1774
+ @click="togglePublish(row.original)"
1775
+ />
1776
+ <UButton
1777
+ v-if="!isFileLocked"
1778
+ label="Delete"
1779
+ size="xs"
1780
+ color="error"
1781
+ variant="ghost"
1782
+ @click="deleteEntry(row.original.id)"
1783
+ />
1784
+ </div>
1785
+ </template>
1786
+ </UTable>
1787
+ </div>
1788
+ </div>
1789
+
1790
+ <!-- 2. EDITOR VIEW -->
1791
+ <div v-else-if="currentView === 'editor'">
1792
+ <!-- Main UEditor Column -->
1793
+ <div class="flex-1 flex flex-col overflow-hidden min-w-0">
1794
+ <UEditor
1795
+ v-model="activeEntry.contentMd"
1796
+ content-type="markdown"
1797
+ placeholder="Start writing your markdown content here..."
1798
+ class="flex-1 flex flex-col overflow-hidden w-full min-w-0"
1799
+ :ui="{
1800
+ root: 'flex-1 flex flex-col overflow-hidden',
1801
+ content: 'flex-1 overflow-y-auto p-4 sm:p-6 min-w-0',
1802
+ }"
1803
+ >
1804
+ <template #default="{ editor }">
1805
+ <div
1806
+ class="border-b border-default bg-default/80 px-3 py-1.5 flex items-center justify-between shrink-0 flex-wrap gap-2"
1807
+ >
1808
+ <UEditorToolbar :editor="editor" :items="editorToolbarItems" />
1809
+ <div class="flex items-center gap-2">
1810
+ <UButton
1811
+ size="xs"
1812
+ :color="showPreview ? 'primary' : 'neutral'"
1813
+ :variant="showPreview ? 'subtle' : 'ghost'"
1814
+ icon="i-lucide-eye"
1815
+ @click="showPreview = !showPreview"
1816
+ >
1817
+ {{ showPreview ? "Hide Preview" : "Preview" }}
1818
+ </UButton>
1819
+ </div>
1820
+ </div>
1821
+ </template>
1822
+ </UEditor>
1823
+ </div>
1824
+
1825
+ <!-- Optional Live Preview Pane (Toggleable) -->
1826
+ <div
1827
+ v-if="showPreview"
1828
+ class="w-1/2 border-l border-default flex flex-col overflow-hidden bg-muted/20 shrink-0"
1829
+ >
1830
+ <!-- Header Bar -->
1831
+ <div
1832
+ class="h-10 bg-muted/40 border-b border-default px-3 flex items-center justify-between text-xs shrink-0 gap-2"
1833
+ >
1834
+ <div class="flex items-center gap-1.5">
1835
+ <div
1836
+ class="inline-flex rounded-md p-0.5 bg-muted/60 border border-default text-[11px]"
1837
+ >
1838
+ <button
1839
+ type="button"
1840
+ class="px-2 py-0.5 rounded font-medium transition-colors"
1841
+ :class="
1842
+ previewMode === 'doc'
1843
+ ? 'bg-background text-foreground shadow-xs'
1844
+ : 'text-muted hover:text-foreground'
1845
+ "
1846
+ @click="previewMode = 'doc'"
1847
+ >
1848
+ Document
1849
+ </button>
1850
+ <button
1851
+ type="button"
1852
+ class="px-2 py-0.5 rounded font-medium transition-colors"
1853
+ :class="
1854
+ previewMode === 'page'
1855
+ ? 'bg-background text-foreground shadow-xs'
1856
+ : 'text-muted hover:text-foreground'
1857
+ "
1858
+ @click="previewMode = 'page'"
1859
+ >
1860
+ Live Page
1861
+ </button>
1862
+ </div>
1863
+
1864
+ <!-- Device switcher for iframe -->
1865
+ <div v-if="previewMode === 'page'" class="flex items-center gap-0.5 ml-2">
1866
+ <UButton
1867
+ size="xs"
1868
+ :color="previewDevice === 'desktop' ? 'primary' : 'neutral'"
1869
+ :variant="previewDevice === 'desktop' ? 'subtle' : 'ghost'"
1870
+ icon="i-lucide-monitor"
1871
+ title="Desktop (100%)"
1872
+ @click="previewDevice = 'desktop'"
1873
+ />
1874
+ <UButton
1875
+ size="xs"
1876
+ :color="previewDevice === 'tablet' ? 'primary' : 'neutral'"
1877
+ :variant="previewDevice === 'tablet' ? 'subtle' : 'ghost'"
1878
+ icon="i-lucide-tablet"
1879
+ title="Tablet (768px)"
1880
+ @click="previewDevice = 'tablet'"
1881
+ />
1882
+ <UButton
1883
+ size="xs"
1884
+ :color="previewDevice === 'mobile' ? 'primary' : 'neutral'"
1885
+ :variant="previewDevice === 'mobile' ? 'subtle' : 'ghost'"
1886
+ icon="i-lucide-smartphone"
1887
+ title="Mobile (375px)"
1888
+ @click="previewDevice = 'mobile'"
1889
+ />
1890
+ </div>
1891
+ </div>
1892
+
1893
+ <div class="flex items-center gap-1">
1894
+ <UButton
1895
+ v-if="previewMode === 'page'"
1896
+ size="xs"
1897
+ color="neutral"
1898
+ variant="ghost"
1899
+ icon="i-lucide-refresh-cw"
1900
+ :loading="previewLoading"
1901
+ title="Reload preview"
1902
+ @click="loadPagePreview"
1903
+ />
1904
+ <UButton
1905
+ size="xs"
1906
+ :color="previewCopied ? 'success' : 'neutral'"
1907
+ :variant="previewCopied ? 'subtle' : 'ghost'"
1908
+ :icon="previewCopied ? 'i-lucide-check' : 'i-lucide-copy'"
1909
+ title="Copy preview link"
1910
+ @click="copyPreviewLink"
1911
+ >
1912
+ {{ previewCopied ? "Copied!" : "Share" }}
1913
+ </UButton>
1914
+ <UButton
1915
+ size="xs"
1916
+ color="neutral"
1917
+ variant="ghost"
1918
+ icon="i-lucide-external-link"
1919
+ title="Open preview in new tab"
1920
+ @click="openPreviewInNewTab"
1921
+ />
1922
+ <UButton
1923
+ size="xs"
1924
+ color="neutral"
1925
+ variant="ghost"
1926
+ icon="i-lucide-x"
1927
+ @click="showPreview = false"
1928
+ />
1929
+ </div>
1930
+ </div>
1931
+
1932
+ <!-- Preview Body: Document Mode -->
1933
+ <div v-if="previewMode === 'doc'" class="flex-1 p-6 overflow-y-auto space-y-4">
1934
+ <!-- Cover image header if present -->
1935
+ <div
1936
+ v-if="activeEntry.coverImage"
1937
+ class="rounded-lg overflow-hidden border border-default bg-muted max-h-56"
1938
+ >
1939
+ <img
1940
+ :src="activeEntry.coverImage"
1941
+ class="w-full h-full object-cover"
1942
+ alt="Cover preview"
1943
+ />
1944
+ </div>
1945
+
1946
+ <!-- Header info -->
1947
+ <div class="space-y-1">
1948
+ <div class="flex items-center gap-2 flex-wrap">
1949
+ <UBadge
1950
+ size="xs"
1951
+ variant="subtle"
1952
+ :color="getStatusBadgeColor(activeEntry.status)"
1953
+ >
1954
+ {{ formatStatus(activeEntry.status) }}
1955
+ </UBadge>
1956
+ <span class="text-xs text-muted font-mono"
1957
+ >/{{ selectedCollectionId }}/{{ activeEntry.slug }}</span
1958
+ >
1959
+ <span v-if="activeEntry.tags" class="text-xs text-muted"
1960
+ >#{{ activeEntry.tags }}</span
1961
+ >
1962
+ </div>
1963
+ <h1 class="text-2xl font-bold tracking-tight text-foreground">
1964
+ {{ activeEntry.title || "Untitled Entry" }}
1965
+ </h1>
1966
+ <p v-if="activeEntry.description" class="text-sm text-muted">
1967
+ {{ activeEntry.description }}
1968
+ </p>
1969
+ </div>
1970
+
1971
+ <hr class="border-default" />
1972
+
1973
+ <!-- Rendered Markdown Content -->
1974
+ <ContentRenderer
1975
+ :value="activeEntry.contentMd"
1976
+ class="prose dark:prose-invert max-w-none text-sm"
1977
+ />
1978
+ </div>
1979
+
1980
+ <!-- Preview Body: Page Mode (Iframe) -->
1981
+ <div
1982
+ v-else
1983
+ class="flex-1 flex flex-col items-center justify-center overflow-hidden bg-muted/30 p-2 sm:p-4"
1984
+ >
1985
+ <div
1986
+ class="h-full flex flex-col bg-background rounded-lg border border-default shadow-md overflow-hidden transition-all duration-200"
1987
+ :class="{
1988
+ 'w-full': previewDevice === 'desktop',
1989
+ 'w-[768px] max-w-full': previewDevice === 'tablet',
1990
+ 'w-[375px] max-w-full': previewDevice === 'mobile',
1991
+ }"
1992
+ >
1993
+ <!-- Simulated browser mini bar -->
1994
+ <div
1995
+ class="h-7 bg-muted/60 border-b border-default px-2.5 flex items-center justify-between text-[11px] text-muted shrink-0"
1996
+ >
1997
+ <div class="flex items-center gap-1.5">
1998
+ <span class="w-2 h-2 rounded-full bg-red-400 inline-block"></span>
1999
+ <span class="w-2 h-2 rounded-full bg-amber-400 inline-block"></span>
2000
+ <span class="w-2 h-2 rounded-full bg-emerald-400 inline-block"></span>
2001
+ </div>
2002
+ <span class="font-mono text-[10px] text-muted truncate max-w-[260px]">{{
2003
+ previewIframeUrl || "Loading preview..."
2004
+ }}</span>
2005
+ <span class="text-[10px] text-muted capitalize">{{ previewDevice }}</span>
2006
+ </div>
2007
+
2008
+ <div
2009
+ v-if="previewLoading"
2010
+ class="flex-1 flex items-center justify-center p-6 text-xs text-muted"
2011
+ >
2012
+ <UIcon name="i-lucide-loader-2" class="w-5 h-5 animate-spin mr-2" />
2013
+ Generating preview token & loading live page...
2014
+ </div>
2015
+ <iframe
2016
+ v-else-if="previewIframeUrl"
2017
+ :src="previewIframeUrl"
2018
+ class="flex-1 w-full h-full border-0 bg-background"
2019
+ />
2020
+ <div
2021
+ v-else
2022
+ class="flex-1 flex flex-col items-center justify-center p-6 text-center text-xs text-muted space-y-2"
2023
+ >
2024
+ <UIcon name="i-lucide-file-question" class="w-8 h-8 text-muted" />
2025
+ <p>Save entry first or click reload to generate live preview.</p>
2026
+ <UButton
2027
+ size="xs"
2028
+ color="primary"
2029
+ label="Generate Preview"
2030
+ @click="loadPagePreview"
2031
+ />
2032
+ </div>
2033
+ </div>
2034
+ </div>
2035
+ </div>
2036
+ </div>
2037
+ </template>
2038
+ </UDashboardPanel>
2039
+
2040
+ <!-- Modals (outside panel, inside group) -->
2041
+ <UModal
2042
+ v-model:open="openMediaLibrary"
2043
+ title="Media Library (Cloudflare R2)"
2044
+ description="Upload, browse, and manage images stored in Cloudflare R2"
2045
+ :ui="{ content: 'sm:max-w-3xl' }"
2046
+ >
2047
+ <template #body>
2048
+ <div class="space-y-4">
2049
+ <!-- Toolbar: Search + Refresh + Upload Trigger -->
2050
+ <div class="flex items-center justify-between gap-2 flex-wrap">
2051
+ <div class="flex items-center gap-2 flex-1 min-w-[200px]">
2052
+ <UInput
2053
+ v-model="mediaSearchQuery"
2054
+ placeholder="Search images in R2..."
2055
+ icon="i-lucide-search"
2056
+ size="xs"
2057
+ class="w-full max-w-xs"
2058
+ />
2059
+ <UButton
2060
+ size="xs"
2061
+ color="neutral"
2062
+ variant="ghost"
2063
+ icon="i-lucide-refresh-cw"
2064
+ :loading="mediaLoading"
2065
+ title="Refresh media files"
2066
+ @click="fetchMediaFiles"
2067
+ />
2068
+ <span class="text-xs text-muted">
2069
+ {{ filteredMediaItems.length }} file{{ filteredMediaItems.length === 1 ? "" : "s" }}
2070
+ </span>
2071
+ </div>
2072
+
2073
+ <div class="flex items-center gap-2 shrink-0">
2074
+ <input
2075
+ type="file"
2076
+ class="hidden"
2077
+ id="cms-file-upload"
2078
+ accept="image/*"
2079
+ @change="handleFileUpload"
2080
+ />
2081
+ <label
2082
+ for="cms-file-upload"
2083
+ class="cursor-pointer inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs bg-primary hover:bg-primary/90 text-white font-medium shadow-xs transition-colors"
2084
+ >
2085
+ <UIcon name="i-lucide-upload" class="w-3.5 h-3.5" />
2086
+ Upload Image
2087
+ </label>
2088
+ </div>
2089
+ </div>
2090
+
2091
+ <!-- Upload Progress indicator -->
2092
+ <div
2093
+ v-if="uploadProgress > 0"
2094
+ class="p-2.5 rounded-md bg-primary/10 border border-primary/20 flex items-center justify-between text-xs text-primary"
2095
+ >
2096
+ <span>Uploading image to Cloudflare R2...</span>
2097
+ <span class="font-mono font-medium">{{ uploadProgress }}%</span>
2098
+ </div>
2099
+
2100
+ <!-- Image Grid -->
2101
+ <div
2102
+ v-if="mediaLoading && mediaItems.length === 0"
2103
+ class="py-12 flex flex-col items-center justify-center text-xs text-muted"
2104
+ >
2105
+ <UIcon name="i-lucide-loader-2" class="w-6 h-6 animate-spin mb-2" />
2106
+ Loading images from Cloudflare R2...
2107
+ </div>
2108
+
2109
+ <div
2110
+ v-else-if="filteredMediaItems.length === 0"
2111
+ class="py-12 text-center text-xs text-muted border border-dashed border-default rounded-lg bg-muted/10"
2112
+ >
2113
+ <UIcon name="i-lucide-image" class="w-8 h-8 text-muted mx-auto mb-2" />
2114
+ <p v-if="mediaSearchQuery">No images match your search "{{ mediaSearchQuery }}".</p>
2115
+ <p v-else>No images found in Cloudflare R2 storage. Upload an image to get started.</p>
2116
+ </div>
2117
+
2118
+ <div
2119
+ v-else
2120
+ class="max-h-[380px] overflow-y-auto grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3 p-1"
2121
+ >
2122
+ <div
2123
+ v-for="item in filteredMediaItems"
2124
+ :key="item.key || item.url"
2125
+ class="group relative flex flex-col bg-muted/20 hover:bg-muted/40 rounded-lg border border-default overflow-hidden transition-all duration-150"
2126
+ >
2127
+ <!-- Thumbnail -->
2128
+ <div
2129
+ class="aspect-square bg-muted/60 relative overflow-hidden flex items-center justify-center"
2130
+ >
2131
+ <img
2132
+ :src="item.url"
2133
+ :alt="item.pathname || item.key"
2134
+ class="w-full h-full object-cover transition-transform group-hover:scale-105 duration-200"
2135
+ loading="lazy"
2136
+ />
2137
+ <!-- Quick Action Overlay on hover -->
2138
+ <div
2139
+ class="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-1.5 p-2"
2140
+ >
2141
+ <UButton
2142
+ size="xs"
2143
+ color="primary"
2144
+ label="Select"
2145
+ @click="selectMediaItem(item.url)"
2146
+ />
2147
+ <UButton
2148
+ size="xs"
2149
+ color="neutral"
2150
+ variant="subtle"
2151
+ icon="i-lucide-trash-2"
2152
+ title="Delete from R2"
2153
+ @click="deleteMediaItem(item)"
2154
+ />
2155
+ </div>
2156
+ </div>
2157
+
2158
+ <!-- Metadata footer -->
2159
+ <div class="p-2 flex flex-col gap-1 min-w-0">
2160
+ <p
2161
+ class="text-[11px] font-medium text-foreground truncate"
2162
+ :title="item.pathname || item.key"
2163
+ >
2164
+ {{ (item.pathname || item.key || "").split("/").pop() }}
2165
+ </p>
2166
+ <div class="flex items-center justify-between text-[10px] text-muted">
2167
+ <span>{{ formatBytes(item.size) }}</span>
2168
+ <div class="flex items-center gap-1">
2169
+ <button
2170
+ type="button"
2171
+ class="hover:text-foreground text-[10px]"
2172
+ :title="mediaCopiedUrl === item.url ? 'Copied!' : 'Copy URL'"
2173
+ @click.stop="copyMediaUrl(item.url)"
2174
+ >
2175
+ {{ mediaCopiedUrl === item.url ? "✓ Copied" : "Copy" }}
2176
+ </button>
2177
+ <span>·</span>
2178
+ <button
2179
+ type="button"
2180
+ class="hover:text-foreground text-[10px]"
2181
+ title="Insert Markdown"
2182
+ @click.stop="insertImageMarkdown(item.url)"
2183
+ >
2184
+ Insert
2185
+ </button>
2186
+ </div>
2187
+ </div>
2188
+ </div>
2189
+ </div>
2190
+ </div>
2191
+ </div>
2192
+ </template>
2193
+
2194
+ <template #footer>
2195
+ <div class="flex items-center justify-between w-full">
2196
+ <span class="text-xs text-muted">
2197
+ Directly connected to Cloudflare R2 bucket binding (<span class="font-mono">BLOB</span>)
2198
+ </span>
2199
+ <UButton
2200
+ label="Close"
2201
+ size="xs"
2202
+ color="neutral"
2203
+ variant="ghost"
2204
+ @click="openMediaLibrary = false"
2205
+ />
2206
+ </div>
2207
+ </template>
2208
+ </UModal>
2209
+
2210
+ <!-- New Collection Modal -->
2211
+ <UModal
2212
+ v-model:open="showNewCollectionModal"
2213
+ title="Create New Collection"
2214
+ description="Add a new content collection to the CMS"
2215
+ >
2216
+ <template #body>
2217
+ <div class="space-y-3">
2218
+ <UFormField label="Collection ID" name="collectionId" required>
2219
+ <UInput v-model="newCol.id" placeholder="e.g. team-members" class="w-full" />
2220
+ </UFormField>
2221
+ <UFormField label="Name" name="name" required>
2222
+ <UInput v-model="newCol.name" placeholder="e.g. Team Members" class="w-full" />
2223
+ </UFormField>
2224
+ <UFormField label="Description" name="description">
2225
+ <UInput
2226
+ v-model="newCol.description"
2227
+ placeholder="Optional description"
2228
+ class="w-full"
2229
+ />
2230
+ </UFormField>
2231
+ </div>
2232
+ </template>
2233
+
2234
+ <template #footer>
2235
+ <div class="flex justify-end space-x-2 w-full">
2236
+ <UButton
2237
+ size="xs"
2238
+ color="neutral"
2239
+ variant="ghost"
2240
+ @click="showNewCollectionModal = false"
2241
+ >
2242
+ Cancel
2243
+ </UButton>
2244
+ <UButton size="xs" color="primary" @click="createCollection"> Create </UButton>
2245
+ </div>
2246
+ </template>
2247
+ </UModal>
2248
+ </UDashboardGroup>
2249
+ </template>