@aphexcms/cms-core 9.4.0 → 9.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (403) hide show
  1. package/dist/admin/block-previews.svelte.d.ts +46 -0
  2. package/dist/admin/block-previews.svelte.d.ts.map +1 -0
  3. package/dist/admin/block-previews.svelte.js +26 -0
  4. package/dist/admin/field-components.svelte.d.ts +16 -0
  5. package/dist/admin/field-components.svelte.d.ts.map +1 -0
  6. package/dist/admin/field-components.svelte.js +18 -0
  7. package/dist/admin/nav.svelte.d.ts +34 -0
  8. package/dist/admin/nav.svelte.d.ts.map +1 -0
  9. package/dist/admin/nav.svelte.js +78 -0
  10. package/dist/admin/slots.svelte.d.ts +50 -0
  11. package/dist/admin/slots.svelte.d.ts.map +1 -0
  12. package/dist/admin/slots.svelte.js +67 -0
  13. package/dist/admin/types.d.ts +7 -0
  14. package/dist/admin/types.d.ts.map +1 -0
  15. package/dist/admin/types.js +1 -0
  16. package/dist/api/schemas/api-keys.d.ts +4 -37
  17. package/dist/api/schemas/api-keys.d.ts.map +1 -1
  18. package/dist/api/schemas/api-keys.js +5 -2
  19. package/dist/api/schemas/assets.d.ts +4 -0
  20. package/dist/api/schemas/assets.d.ts.map +1 -1
  21. package/dist/api/schemas/assets.js +3 -0
  22. package/dist/api/schemas/plugin-settings.d.ts +6 -0
  23. package/dist/api/schemas/plugin-settings.d.ts.map +1 -0
  24. package/dist/api/schemas/plugin-settings.js +8 -0
  25. package/dist/api/schemas/roles.d.ts +7 -56
  26. package/dist/api/schemas/roles.d.ts.map +1 -1
  27. package/dist/api/schemas/roles.js +11 -6
  28. package/dist/api/schemas/user.d.ts +2 -1
  29. package/dist/api/schemas/user.d.ts.map +1 -1
  30. package/dist/api/schemas/user.js +7 -2
  31. package/dist/app-augment.d.ts +11 -0
  32. package/dist/app-augment.d.ts.map +1 -1
  33. package/dist/app.d.ts +2 -0
  34. package/dist/auth/provider.d.ts +3 -0
  35. package/dist/auth/provider.d.ts.map +1 -1
  36. package/dist/cli/generate-types.d.ts +1 -4
  37. package/dist/cli/generate-types.d.ts.map +1 -1
  38. package/dist/cli/generate-types.js +99 -65
  39. package/dist/cli/generate-types.js.map +1 -1
  40. package/dist/cli/index.js +3 -3
  41. package/dist/cli/index.js.map +1 -1
  42. package/dist/client/index.d.ts +6 -0
  43. package/dist/client/index.d.ts.map +1 -1
  44. package/dist/client/index.js +9 -0
  45. package/dist/components/AdminApp.svelte +259 -113
  46. package/dist/components/AdminApp.svelte.d.ts +18 -2
  47. package/dist/components/AdminApp.svelte.d.ts.map +1 -1
  48. package/dist/components/admin/AdminSlot.svelte +18 -0
  49. package/dist/components/admin/AdminSlot.svelte.d.ts +12 -0
  50. package/dist/components/admin/AdminSlot.svelte.d.ts.map +1 -0
  51. package/dist/components/admin/DocumentEditor.svelte +646 -385
  52. package/dist/components/admin/DocumentEditor.svelte.d.ts +13 -0
  53. package/dist/components/admin/DocumentEditor.svelte.d.ts.map +1 -1
  54. package/dist/components/admin/MediaBrowser.svelte +13 -3
  55. package/dist/components/admin/MediaBrowser.svelte.d.ts.map +1 -1
  56. package/dist/components/admin/PluginSettingsPanel.svelte +231 -0
  57. package/dist/components/admin/PluginSettingsPanel.svelte.d.ts +8 -0
  58. package/dist/components/admin/PluginSettingsPanel.svelte.d.ts.map +1 -0
  59. package/dist/components/admin/SchemaField.svelte +76 -95
  60. package/dist/components/admin/SchemaField.svelte.d.ts +7 -0
  61. package/dist/components/admin/SchemaField.svelte.d.ts.map +1 -1
  62. package/dist/components/admin/fields/ArrayField.svelte +24 -2
  63. package/dist/components/admin/fields/ArrayField.svelte.d.ts.map +1 -1
  64. package/dist/components/admin/fields/FieldInput.svelte +109 -0
  65. package/dist/components/admin/fields/FieldInput.svelte.d.ts +17 -0
  66. package/dist/components/admin/fields/FieldInput.svelte.d.ts.map +1 -0
  67. package/dist/components/admin/fields/richtext/PortableTextImageView.svelte +5 -12
  68. package/dist/components/admin/fields/richtext/PortableTextImageView.svelte.d.ts +2 -9
  69. package/dist/components/admin/fields/richtext/PortableTextImageView.svelte.d.ts.map +1 -1
  70. package/dist/components/admin/fields/richtext/PortableTextObjectView.svelte +18 -17
  71. package/dist/components/admin/fields/richtext/PortableTextObjectView.svelte.d.ts +2 -9
  72. package/dist/components/admin/fields/richtext/PortableTextObjectView.svelte.d.ts.map +1 -1
  73. package/dist/components/admin/fields/richtext/RichtextField.svelte +55 -2
  74. package/dist/components/admin/fields/richtext/RichtextField.svelte.d.ts.map +1 -1
  75. package/dist/components/admin/fields/richtext/portable-text-object-node.d.ts +7 -0
  76. package/dist/components/admin/fields/richtext/portable-text-object-node.d.ts.map +1 -1
  77. package/dist/components/admin/fields/richtext/portable-text-object-node.js +4 -2
  78. package/dist/components/admin/fields/richtext/svelte-node-view.d.ts +16 -1
  79. package/dist/components/admin/fields/richtext/svelte-node-view.d.ts.map +1 -1
  80. package/dist/components/admin/fields/richtext/svelte-node-view.js +20 -1
  81. package/dist/components/layout/OrganizationSwitcher.svelte +31 -10
  82. package/dist/components/layout/OrganizationSwitcher.svelte.d.ts.map +1 -1
  83. package/dist/components/layout/Sidebar.svelte +49 -11
  84. package/dist/components/layout/Sidebar.svelte.d.ts +5 -1
  85. package/dist/components/layout/Sidebar.svelte.d.ts.map +1 -1
  86. package/dist/components/layout/sidebar/AppSidebar.svelte +66 -3
  87. package/dist/components/layout/sidebar/AppSidebar.svelte.d.ts +5 -0
  88. package/dist/components/layout/sidebar/AppSidebar.svelte.d.ts.map +1 -1
  89. package/dist/components/layout/sidebar/NavMain.svelte +3 -2
  90. package/dist/components/layout/sidebar/NavMain.svelte.d.ts +4 -3
  91. package/dist/components/layout/sidebar/NavMain.svelte.d.ts.map +1 -1
  92. package/dist/components/layout/sidebar/NavUser.svelte +5 -1
  93. package/dist/config.d.ts.map +1 -1
  94. package/dist/config.js +11 -0
  95. package/dist/db/interfaces/asset.d.ts +1 -0
  96. package/dist/db/interfaces/asset.d.ts.map +1 -1
  97. package/dist/db/interfaces/index.d.ts +3 -1
  98. package/dist/db/interfaces/index.d.ts.map +1 -1
  99. package/dist/db/interfaces/plugin-settings.d.ts +23 -0
  100. package/dist/db/interfaces/plugin-settings.d.ts.map +1 -0
  101. package/dist/db/interfaces/plugin-settings.js +13 -0
  102. package/dist/db/interfaces/role.d.ts +14 -5
  103. package/dist/db/interfaces/role.d.ts.map +1 -1
  104. package/dist/engine.d.ts +28 -0
  105. package/dist/engine.d.ts.map +1 -1
  106. package/dist/engine.js +44 -0
  107. package/dist/hooks.d.ts +6 -0
  108. package/dist/hooks.d.ts.map +1 -1
  109. package/dist/hooks.js +59 -2
  110. package/dist/index.d.ts +2 -0
  111. package/dist/index.d.ts.map +1 -1
  112. package/dist/index.js +5 -0
  113. package/dist/lib/admin/block-previews.svelte.d.ts +46 -0
  114. package/dist/lib/admin/block-previews.svelte.d.ts.map +1 -0
  115. package/dist/lib/admin/block-previews.svelte.js +27 -0
  116. package/dist/lib/admin/block-previews.svelte.js.map +1 -0
  117. package/dist/lib/admin/field-components.svelte.d.ts +16 -0
  118. package/dist/lib/admin/field-components.svelte.d.ts.map +1 -0
  119. package/dist/lib/admin/field-components.svelte.js +19 -0
  120. package/dist/lib/admin/field-components.svelte.js.map +1 -0
  121. package/dist/lib/admin/nav.svelte.d.ts +34 -0
  122. package/dist/lib/admin/nav.svelte.d.ts.map +1 -0
  123. package/dist/lib/admin/nav.svelte.js +79 -0
  124. package/dist/lib/admin/nav.svelte.js.map +1 -0
  125. package/dist/lib/admin/slots.svelte.d.ts +50 -0
  126. package/dist/lib/admin/slots.svelte.d.ts.map +1 -0
  127. package/dist/lib/admin/slots.svelte.js +68 -0
  128. package/dist/lib/admin/slots.svelte.js.map +1 -0
  129. package/dist/lib/admin/types.d.ts +7 -0
  130. package/dist/lib/admin/types.d.ts.map +1 -0
  131. package/dist/lib/admin/types.js +2 -0
  132. package/dist/lib/admin/types.js.map +1 -0
  133. package/dist/lib/api/schemas/api-keys.d.ts +4 -37
  134. package/dist/lib/api/schemas/api-keys.d.ts.map +1 -1
  135. package/dist/lib/api/schemas/api-keys.js +5 -2
  136. package/dist/lib/api/schemas/api-keys.js.map +1 -1
  137. package/dist/lib/api/schemas/assets.d.ts +4 -0
  138. package/dist/lib/api/schemas/assets.d.ts.map +1 -1
  139. package/dist/lib/api/schemas/assets.js +3 -0
  140. package/dist/lib/api/schemas/assets.js.map +1 -1
  141. package/dist/lib/api/schemas/plugin-settings.d.ts +6 -0
  142. package/dist/lib/api/schemas/plugin-settings.d.ts.map +1 -0
  143. package/dist/lib/api/schemas/plugin-settings.js +9 -0
  144. package/dist/lib/api/schemas/plugin-settings.js.map +1 -0
  145. package/dist/lib/api/schemas/roles.d.ts +7 -56
  146. package/dist/lib/api/schemas/roles.d.ts.map +1 -1
  147. package/dist/lib/api/schemas/roles.js +11 -6
  148. package/dist/lib/api/schemas/roles.js.map +1 -1
  149. package/dist/lib/api/schemas/user.d.ts +2 -1
  150. package/dist/lib/api/schemas/user.d.ts.map +1 -1
  151. package/dist/lib/api/schemas/user.js +7 -2
  152. package/dist/lib/api/schemas/user.js.map +1 -1
  153. package/dist/lib/app-augment.d.ts +11 -0
  154. package/dist/lib/app-augment.d.ts.map +1 -1
  155. package/dist/lib/auth/provider.d.ts +3 -0
  156. package/dist/lib/auth/provider.d.ts.map +1 -1
  157. package/dist/lib/client/index.d.ts +6 -0
  158. package/dist/lib/client/index.d.ts.map +1 -1
  159. package/dist/lib/client/index.js +9 -0
  160. package/dist/lib/client/index.js.map +1 -1
  161. package/dist/lib/components/admin/fields/richtext/portable-text-object-node.d.ts +7 -0
  162. package/dist/lib/components/admin/fields/richtext/portable-text-object-node.d.ts.map +1 -1
  163. package/dist/lib/components/admin/fields/richtext/portable-text-object-node.js +4 -2
  164. package/dist/lib/components/admin/fields/richtext/portable-text-object-node.js.map +1 -1
  165. package/dist/lib/components/admin/fields/richtext/svelte-node-view.d.ts +16 -1
  166. package/dist/lib/components/admin/fields/richtext/svelte-node-view.d.ts.map +1 -1
  167. package/dist/lib/components/admin/fields/richtext/svelte-node-view.js +20 -1
  168. package/dist/lib/components/admin/fields/richtext/svelte-node-view.js.map +1 -1
  169. package/dist/lib/config.d.ts.map +1 -1
  170. package/dist/lib/config.js +11 -0
  171. package/dist/lib/config.js.map +1 -1
  172. package/dist/lib/db/interfaces/asset.d.ts +1 -0
  173. package/dist/lib/db/interfaces/asset.d.ts.map +1 -1
  174. package/dist/lib/db/interfaces/index.d.ts +3 -1
  175. package/dist/lib/db/interfaces/index.d.ts.map +1 -1
  176. package/dist/lib/db/interfaces/plugin-settings.d.ts +23 -0
  177. package/dist/lib/db/interfaces/plugin-settings.d.ts.map +1 -0
  178. package/dist/lib/db/interfaces/plugin-settings.js +14 -0
  179. package/dist/lib/db/interfaces/plugin-settings.js.map +1 -0
  180. package/dist/lib/db/interfaces/role.d.ts +14 -5
  181. package/dist/lib/db/interfaces/role.d.ts.map +1 -1
  182. package/dist/lib/engine.d.ts +28 -0
  183. package/dist/lib/engine.d.ts.map +1 -1
  184. package/dist/lib/engine.js +44 -0
  185. package/dist/lib/engine.js.map +1 -1
  186. package/dist/lib/hooks.d.ts +6 -0
  187. package/dist/lib/hooks.d.ts.map +1 -1
  188. package/dist/lib/hooks.js +59 -2
  189. package/dist/lib/hooks.js.map +1 -1
  190. package/dist/lib/index.d.ts +2 -0
  191. package/dist/lib/index.d.ts.map +1 -1
  192. package/dist/lib/index.js +5 -0
  193. package/dist/lib/index.js.map +1 -1
  194. package/dist/lib/local-api/collection-api.d.ts.map +1 -1
  195. package/dist/lib/local-api/collection-api.js +4 -2
  196. package/dist/lib/local-api/collection-api.js.map +1 -1
  197. package/dist/lib/local-api/types.d.ts +8 -0
  198. package/dist/lib/local-api/types.d.ts.map +1 -1
  199. package/dist/lib/mcp/tools.d.ts.map +1 -1
  200. package/dist/lib/mcp/tools.js +68 -0
  201. package/dist/lib/mcp/tools.js.map +1 -1
  202. package/dist/lib/permissions-context.svelte.d.ts +7 -4
  203. package/dist/lib/permissions-context.svelte.d.ts.map +1 -1
  204. package/dist/lib/permissions-context.svelte.js.map +1 -1
  205. package/dist/lib/plugins/index.d.ts +3 -0
  206. package/dist/lib/plugins/index.d.ts.map +1 -0
  207. package/dist/lib/plugins/index.js +3 -0
  208. package/dist/lib/plugins/index.js.map +1 -0
  209. package/dist/lib/plugins/resolver.d.ts +49 -0
  210. package/dist/lib/plugins/resolver.d.ts.map +1 -0
  211. package/dist/lib/plugins/resolver.js +66 -0
  212. package/dist/lib/plugins/resolver.js.map +1 -0
  213. package/dist/lib/plugins/types.d.ts +233 -0
  214. package/dist/lib/plugins/types.d.ts.map +1 -0
  215. package/dist/lib/plugins/types.js +9 -0
  216. package/dist/lib/plugins/types.js.map +1 -0
  217. package/dist/lib/schema-utils/desugar.d.ts +27 -0
  218. package/dist/lib/schema-utils/desugar.d.ts.map +1 -0
  219. package/dist/lib/schema-utils/desugar.js +102 -0
  220. package/dist/lib/schema-utils/desugar.js.map +1 -0
  221. package/dist/lib/schema-utils/index.d.ts +2 -0
  222. package/dist/lib/schema-utils/index.d.ts.map +1 -1
  223. package/dist/lib/schema-utils/index.js +2 -0
  224. package/dist/lib/schema-utils/index.js.map +1 -1
  225. package/dist/lib/schema-utils/settings.d.ts +17 -0
  226. package/dist/lib/schema-utils/settings.d.ts.map +1 -0
  227. package/dist/lib/schema-utils/settings.js +23 -0
  228. package/dist/lib/schema-utils/settings.js.map +1 -0
  229. package/dist/lib/schema-utils/validator.d.ts.map +1 -1
  230. package/dist/lib/schema-utils/validator.js +11 -0
  231. package/dist/lib/schema-utils/validator.js.map +1 -1
  232. package/dist/lib/security/secret-crypto.d.ts +18 -0
  233. package/dist/lib/security/secret-crypto.d.ts.map +1 -0
  234. package/dist/lib/security/secret-crypto.js +71 -0
  235. package/dist/lib/security/secret-crypto.js.map +1 -0
  236. package/dist/lib/server/api/index.d.ts.map +1 -1
  237. package/dist/lib/server/api/index.js +3 -0
  238. package/dist/lib/server/api/index.js.map +1 -1
  239. package/dist/lib/server/api/routes/assets.d.ts.map +1 -1
  240. package/dist/lib/server/api/routes/assets.js +8 -2
  241. package/dist/lib/server/api/routes/assets.js.map +1 -1
  242. package/dist/lib/server/api/routes/organizations.d.ts.map +1 -1
  243. package/dist/lib/server/api/routes/organizations.js +4 -1
  244. package/dist/lib/server/api/routes/organizations.js.map +1 -1
  245. package/dist/lib/server/api/routes/plugin-settings.d.ts +4 -0
  246. package/dist/lib/server/api/routes/plugin-settings.d.ts.map +1 -0
  247. package/dist/lib/server/api/routes/plugin-settings.js +109 -0
  248. package/dist/lib/server/api/routes/plugin-settings.js.map +1 -0
  249. package/dist/lib/server/api/routes/roles.d.ts.map +1 -1
  250. package/dist/lib/server/api/routes/roles.js +37 -0
  251. package/dist/lib/server/api/routes/roles.js.map +1 -1
  252. package/dist/lib/server/api/routes/user.d.ts.map +1 -1
  253. package/dist/lib/server/api/routes/user.js +14 -3
  254. package/dist/lib/server/api/routes/user.js.map +1 -1
  255. package/dist/lib/services/asset-service.d.ts +1 -0
  256. package/dist/lib/services/asset-service.d.ts.map +1 -1
  257. package/dist/lib/services/asset-service.js.map +1 -1
  258. package/dist/lib/services/index.d.ts +2 -0
  259. package/dist/lib/services/index.d.ts.map +1 -1
  260. package/dist/lib/services/index.js +1 -0
  261. package/dist/lib/services/index.js.map +1 -1
  262. package/dist/lib/services/plugin-settings-service.d.ts +76 -0
  263. package/dist/lib/services/plugin-settings-service.d.ts.map +1 -0
  264. package/dist/lib/services/plugin-settings-service.js +236 -0
  265. package/dist/lib/services/plugin-settings-service.js.map +1 -0
  266. package/dist/lib/services/roles-service.d.ts +2 -2
  267. package/dist/lib/services/roles-service.d.ts.map +1 -1
  268. package/dist/lib/services/roles-service.js.map +1 -1
  269. package/dist/lib/theme/css.d.ts +30 -0
  270. package/dist/lib/theme/css.d.ts.map +1 -0
  271. package/dist/lib/theme/css.js +54 -0
  272. package/dist/lib/theme/css.js.map +1 -0
  273. package/dist/lib/theme/derive.d.ts +27 -0
  274. package/dist/lib/theme/derive.d.ts.map +1 -0
  275. package/dist/lib/theme/derive.js +108 -0
  276. package/dist/lib/theme/derive.js.map +1 -0
  277. package/dist/lib/theme/index.d.ts +5 -0
  278. package/dist/lib/theme/index.d.ts.map +1 -0
  279. package/dist/lib/theme/index.js +5 -0
  280. package/dist/lib/theme/index.js.map +1 -0
  281. package/dist/lib/theme/schemes.d.ts +60 -0
  282. package/dist/lib/theme/schemes.d.ts.map +1 -0
  283. package/dist/lib/theme/schemes.js +150 -0
  284. package/dist/lib/theme/schemes.js.map +1 -0
  285. package/dist/lib/theme/tokens.d.ts +57 -0
  286. package/dist/lib/theme/tokens.d.ts.map +1 -0
  287. package/dist/lib/theme/tokens.js +11 -0
  288. package/dist/lib/theme/tokens.js.map +1 -0
  289. package/dist/lib/types/auth.d.ts +10 -10
  290. package/dist/lib/types/auth.d.ts.map +1 -1
  291. package/dist/lib/types/capabilities.d.ts +50 -9
  292. package/dist/lib/types/capabilities.d.ts.map +1 -1
  293. package/dist/lib/types/capabilities.js +147 -5
  294. package/dist/lib/types/capabilities.js.map +1 -1
  295. package/dist/lib/types/config.d.ts +30 -1
  296. package/dist/lib/types/config.d.ts.map +1 -1
  297. package/dist/lib/types/schemas.d.ts +90 -2
  298. package/dist/lib/types/schemas.d.ts.map +1 -1
  299. package/dist/lib/types/schemas.js.map +1 -1
  300. package/dist/lib/types/sidebar.d.ts +3 -1
  301. package/dist/lib/types/sidebar.d.ts.map +1 -1
  302. package/dist/lib/utils/index.d.ts +1 -0
  303. package/dist/lib/utils/index.d.ts.map +1 -1
  304. package/dist/lib/utils/index.js +1 -0
  305. package/dist/lib/utils/index.js.map +1 -1
  306. package/dist/lib/utils/preview.d.ts +16 -5
  307. package/dist/lib/utils/preview.d.ts.map +1 -1
  308. package/dist/lib/utils/preview.js +7 -2
  309. package/dist/lib/utils/preview.js.map +1 -1
  310. package/dist/lib/vite/index.d.ts +7 -0
  311. package/dist/lib/vite/index.d.ts.map +1 -1
  312. package/dist/lib/vite/index.js +37 -6
  313. package/dist/lib/vite/index.js.map +1 -1
  314. package/dist/local-api/collection-api.d.ts.map +1 -1
  315. package/dist/local-api/collection-api.js +4 -2
  316. package/dist/local-api/types.d.ts +8 -0
  317. package/dist/local-api/types.d.ts.map +1 -1
  318. package/dist/mcp/tools.d.ts.map +1 -1
  319. package/dist/mcp/tools.js +68 -0
  320. package/dist/permissions-context.svelte.d.ts +7 -4
  321. package/dist/permissions-context.svelte.d.ts.map +1 -1
  322. package/dist/plugins/index.d.ts +3 -0
  323. package/dist/plugins/index.d.ts.map +1 -0
  324. package/dist/plugins/index.js +2 -0
  325. package/dist/plugins/resolver.d.ts +49 -0
  326. package/dist/plugins/resolver.d.ts.map +1 -0
  327. package/dist/plugins/resolver.js +65 -0
  328. package/dist/plugins/types.d.ts +233 -0
  329. package/dist/plugins/types.d.ts.map +1 -0
  330. package/dist/plugins/types.js +8 -0
  331. package/dist/schema-utils/desugar.d.ts +27 -0
  332. package/dist/schema-utils/desugar.d.ts.map +1 -0
  333. package/dist/schema-utils/desugar.js +101 -0
  334. package/dist/schema-utils/index.d.ts +2 -0
  335. package/dist/schema-utils/index.d.ts.map +1 -1
  336. package/dist/schema-utils/index.js +2 -0
  337. package/dist/schema-utils/settings.d.ts +17 -0
  338. package/dist/schema-utils/settings.d.ts.map +1 -0
  339. package/dist/schema-utils/settings.js +22 -0
  340. package/dist/schema-utils/validator.d.ts.map +1 -1
  341. package/dist/schema-utils/validator.js +11 -0
  342. package/dist/security/secret-crypto.d.ts +18 -0
  343. package/dist/security/secret-crypto.d.ts.map +1 -0
  344. package/dist/security/secret-crypto.js +70 -0
  345. package/dist/server/api/index.d.ts.map +1 -1
  346. package/dist/server/api/index.js +3 -0
  347. package/dist/server/api/routes/assets.d.ts.map +1 -1
  348. package/dist/server/api/routes/assets.js +8 -2
  349. package/dist/server/api/routes/organizations.d.ts.map +1 -1
  350. package/dist/server/api/routes/organizations.js +4 -1
  351. package/dist/server/api/routes/plugin-settings.d.ts +4 -0
  352. package/dist/server/api/routes/plugin-settings.d.ts.map +1 -0
  353. package/dist/server/api/routes/plugin-settings.js +108 -0
  354. package/dist/server/api/routes/roles.d.ts.map +1 -1
  355. package/dist/server/api/routes/roles.js +37 -0
  356. package/dist/server/api/routes/user.d.ts.map +1 -1
  357. package/dist/server/api/routes/user.js +14 -3
  358. package/dist/services/asset-service.d.ts +1 -0
  359. package/dist/services/asset-service.d.ts.map +1 -1
  360. package/dist/services/index.d.ts +2 -0
  361. package/dist/services/index.d.ts.map +1 -1
  362. package/dist/services/index.js +1 -0
  363. package/dist/services/plugin-settings-service.d.ts +76 -0
  364. package/dist/services/plugin-settings-service.d.ts.map +1 -0
  365. package/dist/services/plugin-settings-service.js +235 -0
  366. package/dist/services/roles-service.d.ts +2 -2
  367. package/dist/services/roles-service.d.ts.map +1 -1
  368. package/dist/theme/css.d.ts +30 -0
  369. package/dist/theme/css.d.ts.map +1 -0
  370. package/dist/theme/css.js +53 -0
  371. package/dist/theme/derive.d.ts +27 -0
  372. package/dist/theme/derive.d.ts.map +1 -0
  373. package/dist/theme/derive.js +107 -0
  374. package/dist/theme/index.d.ts +5 -0
  375. package/dist/theme/index.d.ts.map +1 -0
  376. package/dist/theme/index.js +4 -0
  377. package/dist/theme/schemes.d.ts +60 -0
  378. package/dist/theme/schemes.d.ts.map +1 -0
  379. package/dist/theme/schemes.js +149 -0
  380. package/dist/theme/tokens.d.ts +57 -0
  381. package/dist/theme/tokens.d.ts.map +1 -0
  382. package/dist/theme/tokens.js +10 -0
  383. package/dist/types/auth.d.ts +10 -10
  384. package/dist/types/auth.d.ts.map +1 -1
  385. package/dist/types/capabilities.d.ts +50 -9
  386. package/dist/types/capabilities.d.ts.map +1 -1
  387. package/dist/types/capabilities.js +147 -5
  388. package/dist/types/config.d.ts +30 -1
  389. package/dist/types/config.d.ts.map +1 -1
  390. package/dist/types/schemas.d.ts +90 -2
  391. package/dist/types/schemas.d.ts.map +1 -1
  392. package/dist/types/sidebar.d.ts +3 -1
  393. package/dist/types/sidebar.d.ts.map +1 -1
  394. package/dist/utils/index.d.ts +1 -0
  395. package/dist/utils/index.d.ts.map +1 -1
  396. package/dist/utils/index.js +1 -0
  397. package/dist/utils/preview.d.ts +16 -5
  398. package/dist/utils/preview.d.ts.map +1 -1
  399. package/dist/utils/preview.js +7 -2
  400. package/dist/vite/index.d.ts +7 -0
  401. package/dist/vite/index.d.ts.map +1 -1
  402. package/dist/vite/index.js +37 -6
  403. package/package.json +6 -4
@@ -13,10 +13,14 @@
13
13
  import { setSchemaContext } from '../../schema-context.svelte';
14
14
  import { setSaveStateContext } from '../../save-state-context.svelte';
15
15
  import { usePermissions } from '../../permissions-context.svelte';
16
+ import AdminSlot from './AdminSlot.svelte';
17
+ import { createPartResolver } from '../../plugins/resolver';
18
+ import type { CMSPlugin, DocumentActionProps } from '../../plugins/types';
16
19
  import { getDefaultValueForFieldType } from '../../utils/field-defaults';
17
20
  import { notifyDocumentChanged } from '../../document-refresh.svelte';
18
21
  import { collectReferenceIds } from '../../utils/reference-walk';
19
22
  import { cmsLogger } from '../../utils/logger';
23
+ import { resolvePreviewTitle } from '../../utils/preview';
20
24
  import { toast } from 'svelte-sonner';
21
25
  import { confirmDialog } from './confirm-dialog/confirm-dialog.svelte';
22
26
  import {
@@ -28,7 +32,12 @@
28
32
  Minimize2,
29
33
  Monitor,
30
34
  RefreshCw,
31
- ExternalLink
35
+ ExternalLink,
36
+ Tablet,
37
+ Smartphone,
38
+ ZoomIn,
39
+ ZoomOut,
40
+ ArrowLeft
32
41
  } from '@lucide/svelte';
33
42
  import { stegaEncodeDocument } from '../../preview/stega.js';
34
43
  import { collectAssetRefs, injectAssetData, type ResolvedAsset } from '../../preview/assets.js';
@@ -40,6 +49,10 @@
40
49
  documentId?: string | null;
41
50
  isCreating: boolean;
42
51
  onBack: () => void;
52
+ /** When set, the close control renders as a labelled "← {backLabel}" button
53
+ * instead of a bare close (✕) icon — used for reference editors so the action
54
+ * reads as "go back" rather than "close". */
55
+ backLabel?: string;
43
56
  onSaved?: (documentId: string) => void;
44
57
  onAutoSaved?: (documentId: string, title: string) => void;
45
58
  onDeleted?: () => void;
@@ -61,10 +74,18 @@
61
74
  onToggleFocus?: () => void;
62
75
  /** When true, split the editor with a live preview iframe on the right. */
63
76
  presentationMode?: boolean;
77
+ /**
78
+ * Bump this to ask the preview iframe to re-fetch its server-loaded data
79
+ * (`aphex:refresh` → `invalidateAll` in the overlay). Used when a *different*
80
+ * document — e.g. one this page renders in a list — was edited elsewhere.
81
+ */
82
+ refreshToken?: number;
64
83
  /** Toggle host-driven presentation mode. Omit to hide the button entirely. */
65
84
  onTogglePresentation?: () => void;
66
85
  /** Organization ID from the host context — used as fallback for new docs that haven't been saved yet. */
67
86
  organizationId?: string | null;
87
+ /** Build-time plugins; their document-action parts render in the toolbar. */
88
+ plugins?: CMSPlugin[];
68
89
  }
69
90
 
70
91
  let {
@@ -73,6 +94,7 @@
73
94
  documentId,
74
95
  isCreating,
75
96
  onBack,
97
+ backLabel,
76
98
  onSaved,
77
99
  onAutoSaved,
78
100
  onDeleted,
@@ -86,8 +108,10 @@
86
108
  focusMode = false,
87
109
  onToggleFocus,
88
110
  presentationMode = false,
111
+ refreshToken = 0,
89
112
  onTogglePresentation,
90
- organizationId = null
113
+ organizationId = null,
114
+ plugins = []
91
115
  }: Props = $props();
92
116
 
93
117
  // Set schema context for child components (ArrayField, etc.)
@@ -136,6 +160,44 @@
136
160
  let publishedData = $state<Record<string, any> | null>(null);
137
161
  const isViewingPublished = $derived(perspective === 'published');
138
162
 
163
+ // Plugin document actions applicable to this type. Capability-gated, with
164
+ // built-in privileged roles bypassing. Cheap to rebuild from the plugin list.
165
+ const pluginDocumentActions = $derived(
166
+ createPartResolver(plugins).documentActions({
167
+ schemaName: documentType,
168
+ capabilities: [...perms.capabilities],
169
+ overrideAccess: perms.role === 'super_admin' || perms.role === 'admin'
170
+ })
171
+ );
172
+
173
+ // A minimal, stable context handed to each document-action component —
174
+ // deliberately decoupled from editor internals so plugins never touch them.
175
+ const documentActionContext = $derived<DocumentActionProps | null>(
176
+ schema
177
+ ? {
178
+ documentId: documentId ?? fullDocument?.id ?? null,
179
+ schema,
180
+ data: documentData,
181
+ status: !documentId
182
+ ? 'new'
183
+ : fullDocument?._meta?.status === 'published'
184
+ ? 'published'
185
+ : fullDocument?._meta?.status === 'unpublished'
186
+ ? 'unpublished'
187
+ : 'draft',
188
+ updateData: (patch: Record<string, unknown>) => {
189
+ documentData = { ...documentData, ...patch };
190
+ },
191
+ save: async () => {
192
+ await saveDocument();
193
+ },
194
+ publish: async () => {
195
+ await publishDocument();
196
+ }
197
+ }
198
+ : null
199
+ );
200
+
139
201
  // Field-level access: mirrors the server's strip/drop logic so the form
140
202
  // only shows fields the user can see and disables fields they can't write.
141
203
  function fieldRoleAllowed(list: string[] | undefined): boolean {
@@ -169,6 +231,31 @@
169
231
  let fieldsWidth = $state(500);
170
232
  let isResizing = $state(false);
171
233
 
234
+ // Preview canvas — a viewport width and a zoom level so the preview reads like
235
+ // a real device canvas rather than a cramped pane.
236
+ let previewViewport = $state<'desktop' | 'tablet' | 'mobile'>('desktop');
237
+ let previewZoom = $state(1);
238
+ const VIEWPORT_WIDTH: Record<'desktop' | 'tablet' | 'mobile', number> = {
239
+ desktop: 0, // 0 = fill available width
240
+ tablet: 768,
241
+ mobile: 390
242
+ };
243
+ function zoomBy(delta: number) {
244
+ previewZoom = Math.round(Math.min(1.5, Math.max(0.5, previewZoom + delta)) * 100) / 100;
245
+ }
246
+ // Scale-compensated frame sizing: at desktop the iframe fills (up-sized then
247
+ // scaled so content stays crisp); device modes get a fixed centered width.
248
+ const frameStyle = $derived.by(() => {
249
+ const z = previewZoom;
250
+ if (previewViewport === 'desktop') {
251
+ if (z === 1) return '';
252
+ return `width:${100 / z}%;height:${100 / z}%;transform:scale(${z});transform-origin:top left;`;
253
+ }
254
+ const w = VIEWPORT_WIDTH[previewViewport];
255
+ const h = z === 1 ? '100%' : `${100 / z}%`;
256
+ return `width:${w}px;height:${h};transform:scale(${z});transform-origin:top center;`;
257
+ });
258
+
172
259
  function refreshPreview() {
173
260
  if (!iframeRef || !iframeUrl) return;
174
261
  iframeRef.src = iframeUrl;
@@ -210,6 +297,8 @@
210
297
  win.postMessage(
211
298
  {
212
299
  type: 'aphex:data',
300
+ documentType,
301
+ documentId: documentId ?? undefined,
213
302
  document: iframeStega ? stegaEncodeDocument(snapshot, schema?.fields ?? []) : snapshot
214
303
  },
215
304
  '*'
@@ -226,7 +315,7 @@
226
315
  const startX = e.clientX;
227
316
  const startWidth = fieldsWidth;
228
317
  function onMove(ev: MouseEvent) {
229
- fieldsWidth = Math.max(500, Math.min(700, startWidth + (ev.clientX - startX)));
318
+ fieldsWidth = Math.max(500, Math.min(720, startWidth + (ev.clientX - startX)));
230
319
  }
231
320
  function onUp() {
232
321
  isResizing = false;
@@ -269,6 +358,11 @@
269
358
  $effect(() => {
270
359
  if (!presentationMode) return;
271
360
  const handler = (e: MessageEvent) => {
361
+ // Only handle messages from *this* editor's own preview iframe. Presentation
362
+ // editors can be nested (a base editor + a reference modal), and both listen
363
+ // on window — without this guard a click in one preview would also fire the
364
+ // other's handler (and could open a stray reference).
365
+ if (iframeRef?.contentWindow && e.source !== iframeRef.contentWindow) return;
272
366
  if (!e.data || typeof e.data !== 'object') return;
273
367
  const msg = e.data as {
274
368
  type: string;
@@ -278,6 +372,8 @@
278
372
  arrayIndex?: number;
279
373
  objectPath?: string;
280
374
  linkHref?: string;
375
+ documentId?: string;
376
+ documentType?: string;
281
377
  };
282
378
  if (msg.type === 'aphex:ready') {
283
379
  iframeStega = (msg as any).stega !== false;
@@ -286,6 +382,14 @@
286
382
  iframeRef?.contentWindow?.postMessage({ type: 'aphex:edit-mode', enabled: false }, '*');
287
383
  }
288
384
  } else if (msg.type === 'aphex:field-click' && msg.fieldPath) {
385
+ // Cross-document click: the rendered element belongs to a *different*
386
+ // document than the one being edited (e.g. an app-level "list of posts"
387
+ // block whose cards aren't a stored reference). Open that document rather
388
+ // than trying to resolve the field in the current one.
389
+ if (msg.documentId && msg.documentType && msg.documentId !== documentId) {
390
+ onOpenReference?.(msg.documentId, msg.documentType);
391
+ return;
392
+ }
289
393
  focusFieldByName(msg.fieldPath, {
290
394
  blockIndex: msg.blockIndex,
291
395
  blockKey: msg.blockKey,
@@ -309,6 +413,14 @@
309
413
  return () => clearTimeout(timer);
310
414
  });
311
415
 
416
+ // Host bumped refreshToken (a different document changed) — ask the preview to
417
+ // re-fetch its server-loaded data. The overlay maps this to `invalidateAll()`.
418
+ $effect(() => {
419
+ if (refreshToken === 0) return; // initial value — nothing to refresh yet
420
+ if (!presentationMode || !iframeRef?.contentWindow) return;
421
+ iframeRef.contentWindow.postMessage({ type: 'aphex:refresh' }, '*');
422
+ });
423
+
312
424
  /**
313
425
  * Map a portable text array index to the absolute ProseMirror cursor position (end of content)
314
426
  * for that block in the TipTap editor. Accounts for list grouping (multiple PT list-item blocks
@@ -420,7 +532,14 @@
420
532
  await tick();
421
533
  const container = document.querySelector<HTMLElement>(`[data-field-name="${fieldName}"]`);
422
534
  if (!container) return;
423
- container.scrollIntoView({ behavior: 'smooth', block: 'center' });
535
+ // For rich-text caret navigation, scroll the wrapper instantly to 'nearest' so it
536
+ // doesn't smooth-animate the (possibly tall) field back to centre and shove the
537
+ // caret off-screen — the editor's own scrollIntoView positions the caret precisely.
538
+ const isRichtextNav = opts.blockIndex != null || !!opts.blockKey || !!opts.linkHref;
539
+ container.scrollIntoView({
540
+ behavior: isRichtextNav ? 'auto' : 'smooth',
541
+ block: isRichtextNav ? 'nearest' : 'center'
542
+ });
424
543
  setTimeout(() => {
425
544
  if (focusedFieldName === fieldName) focusedFieldName = null;
426
545
  }, 2000);
@@ -449,8 +568,9 @@
449
568
  return true;
450
569
  });
451
570
  if (linkPos !== null) {
452
- richtextEditor.commands.focus();
453
- richtextEditor.commands.setTextSelection(linkPos);
571
+ // Chain focus → select → scrollIntoView so the caret is scrolled to the
572
+ // actual selection, not left off-screen when the field scrolled to center.
573
+ richtextEditor.chain().focus().setTextSelection(linkPos).scrollIntoView().run();
454
574
  richtextHandle?.openLinkPopover();
455
575
  return;
456
576
  }
@@ -489,8 +609,9 @@
489
609
  if (Array.isArray(ptBlocks)) {
490
610
  const cursorPos = findTiptapCursorForPTBlock(ptBlocks, blockIndex!, doc);
491
611
  if (cursorPos !== null) {
492
- richtextEditor.commands.focus();
493
- richtextEditor.commands.setTextSelection(cursorPos);
612
+ // Scroll to the caret itself — the field-level center scroll above can
613
+ // leave the actual cursor position off-screen in a long block.
614
+ richtextEditor.chain().focus().setTextSelection(cursorPos).scrollIntoView().run();
494
615
  return;
495
616
  }
496
617
  }
@@ -702,10 +823,10 @@
702
823
  // fields, which is confusing.
703
824
  function getPreviewTitle(): string {
704
825
  const source = perspective === 'published' && publishedData ? publishedData : documentData;
705
- if (!schema?.preview?.select?.title) {
706
- return source.title || `Untitled`;
707
- }
708
- return source[schema.preview.select.title] || `Untitled`;
826
+ // Shared resolver: honors preview.prepare(), the literal preview.title, the
827
+ // select.title dot-path, and conventional field fallbacks — the same title an
828
+ // item shows in list/array/reference rows.
829
+ return resolvePreviewTitle(source, schema);
709
830
  }
710
831
 
711
832
  // CRITICAL: Clear state IMMEDIATELY when switching documents to prevent cross-contamination
@@ -1428,421 +1549,503 @@
1428
1549
  }
1429
1550
  </script>
1430
1551
 
1552
+ {#snippet editorActions()}
1553
+ <div class="flex shrink-0 items-center gap-2">
1554
+ {#if saving}
1555
+ <span
1556
+ class="text-muted-foreground hidden items-center gap-1.5 text-[10px] font-medium tracking-wider whitespace-nowrap uppercase sm:inline-flex"
1557
+ >
1558
+ <span class="bg-muted-foreground/60 h-1.5 w-1.5 animate-pulse rounded-full"></span>
1559
+ Saving
1560
+ </span>
1561
+ {:else if hasUnsavedChanges}
1562
+ <span
1563
+ class="text-muted-foreground hidden items-center gap-1.5 text-[10px] font-medium tracking-wider whitespace-nowrap uppercase sm:inline-flex"
1564
+ >
1565
+ <span class="bg-muted-foreground/60 h-1.5 w-1.5 rounded-full"></span>
1566
+ Unsaved
1567
+ </span>
1568
+ {:else if savedAgoText}
1569
+ <span
1570
+ class="text-muted-foreground hidden items-center gap-1.5 text-[10px] font-medium tracking-wider whitespace-nowrap uppercase sm:inline-flex"
1571
+ >
1572
+ <span class="bg-muted-foreground/60 h-1.5 w-1.5 rounded-full"></span>
1573
+ Auto-saved
1574
+ </span>
1575
+ {/if}
1576
+
1577
+ {#if documentId && fullDocument?._meta?.publishedHash}
1578
+ {@const isPublished =
1579
+ fullDocument?._meta?.status === 'published' && fullDocument?._meta?.publishedAt}
1580
+ {@const isUnpub = fullDocument?._meta?.status === 'unpublished'}
1581
+ <div class="flex items-center gap-1.5">
1582
+ <button
1583
+ class="inline-flex cursor-pointer items-center gap-1.5 rounded-full border px-2.5 py-1 text-[10px] font-medium tracking-wider uppercase transition-colors {perspective ===
1584
+ 'draft'
1585
+ ? 'bg-primary/90 text-primary-foreground border-transparent'
1586
+ : 'text-muted-foreground hover:bg-muted'}"
1587
+ onclick={() => switchPerspective('draft')}
1588
+ >
1589
+ <span
1590
+ class="bg-muted-foreground/60 h-1.5 w-1.5 rounded-full {perspective === 'draft'
1591
+ ? 'bg-primary-foreground/60'
1592
+ : ''}"
1593
+ ></span>
1594
+ Draft
1595
+ </button>
1596
+ <button
1597
+ class="inline-flex cursor-pointer items-center gap-1.5 rounded-full border px-2.5 py-1 text-[10px] font-medium tracking-wider uppercase transition-colors {perspective ===
1598
+ 'published'
1599
+ ? 'bg-primary text-primary-foreground border-transparent'
1600
+ : 'text-muted-foreground hover:bg-muted'}"
1601
+ onclick={() => switchPerspective('published')}
1602
+ >
1603
+ {#if isPublished}
1604
+ <span
1605
+ class="h-1.5 w-1.5 rounded-full {perspective === 'published'
1606
+ ? 'bg-primary-foreground/60'
1607
+ : 'bg-green-500'}"
1608
+ ></span>
1609
+ Published · {timeAgo(new Date(fullDocument._meta.publishedAt))}
1610
+ {:else if isUnpub}
1611
+ <span
1612
+ class="h-1.5 w-1.5 rounded-full {perspective === 'published'
1613
+ ? 'bg-primary-foreground/60'
1614
+ : 'bg-muted-foreground/60'}"
1615
+ ></span>
1616
+ Unpublished
1617
+ {:else}
1618
+ Published
1619
+ {/if}
1620
+ </button>
1621
+ </div>
1622
+ {/if}
1623
+
1624
+ <!-- Plugin document actions (aphex/document/action), applicable to this type -->
1625
+ {#if documentActionContext}
1626
+ {#each pluginDocumentActions as action (action.id)}
1627
+ {@const ActionComponent = action.component}
1628
+ <ActionComponent action={documentActionContext} />
1629
+ {/each}
1630
+ {/if}
1631
+
1632
+ {#if documentId}
1633
+ <div class="relative">
1634
+ <Button
1635
+ variant="ghost"
1636
+ size="icon"
1637
+ onclick={() => (showHeaderMenu = !showHeaderMenu)}
1638
+ class="h-8 w-8 cursor-pointer"
1639
+ >
1640
+ <Ellipsis class="h-4 w-4" />
1641
+ </Button>
1642
+ {#if showHeaderMenu}
1643
+ <div
1644
+ class="bg-background border-rule absolute top-full right-0 z-[60] mt-1 min-w-[160px] rounded-md border py-1 shadow-lg"
1645
+ >
1646
+ <button
1647
+ onclick={() => {
1648
+ showHeaderMenu = false;
1649
+ if (onOpenVersionHistory && documentId) {
1650
+ onOpenVersionHistory(documentId);
1651
+ } else {
1652
+ showVersionHistory = true;
1653
+ }
1654
+ }}
1655
+ class="hover:bg-muted flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-left text-sm transition-colors"
1656
+ >
1657
+ <History class="h-3.5 w-3.5" /> History
1658
+ </button>
1659
+ <button
1660
+ onclick={() => {
1661
+ showHeaderMenu = false;
1662
+ showInspect = true;
1663
+ }}
1664
+ class="hover:bg-muted flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-left text-sm transition-colors"
1665
+ >
1666
+ <Code class="h-3.5 w-3.5" /> Inspect
1667
+ </button>
1668
+ </div>
1669
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
1670
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
1671
+ <div class="fixed inset-0 z-[55]" onclick={() => (showHeaderMenu = false)}></div>
1672
+ {/if}
1673
+ </div>
1674
+ {/if}
1675
+
1676
+ <!-- Toggle presentation. In the visual editor it's a second way to switch off
1677
+ (alongside the breadcrumb back arrow); highlighted while active. -->
1678
+ {#if onTogglePresentation && schema?.previewUrl}
1679
+ <Button
1680
+ variant="ghost"
1681
+ size="icon"
1682
+ onclick={onTogglePresentation}
1683
+ class="hidden h-8 w-8 hover:cursor-pointer lg:flex {presentationMode ? 'text-primary' : ''}"
1684
+ title={presentationMode ? 'Exit visual editing' : 'Present'}
1685
+ >
1686
+ <Monitor class="h-4 w-4" />
1687
+ </Button>
1688
+ {/if}
1689
+
1690
+ <!-- Focus mode hides the side panels; presentation already does that, so it's
1691
+ redundant in the visual editor — hide it there. -->
1692
+ {#if onToggleFocus && !presentationMode}
1693
+ <Button
1694
+ variant="ghost"
1695
+ size="icon"
1696
+ onclick={onToggleFocus}
1697
+ class="hidden h-8 w-8 hover:cursor-pointer lg:flex"
1698
+ title={focusMode ? 'Exit focus mode' : 'Enter focus mode'}
1699
+ >
1700
+ {#if focusMode}
1701
+ <Minimize2 class="h-4 w-4" />
1702
+ {:else}
1703
+ <Maximize2 class="h-4 w-4" />
1704
+ {/if}
1705
+ </Button>
1706
+ {/if}
1707
+
1708
+ {#if backLabel}
1709
+ <Button
1710
+ variant="ghost"
1711
+ size="sm"
1712
+ onclick={onBack}
1713
+ class="hidden h-8 gap-1.5 px-2 hover:cursor-pointer lg:flex"
1714
+ title={backLabel}
1715
+ >
1716
+ <ArrowLeft class="h-4 w-4" />
1717
+ <span class="text-sm font-medium">{backLabel}</span>
1718
+ </Button>
1719
+ {:else if !presentationMode}
1720
+ <!-- Non-presentation only: the visual editor exits via the breadcrumb back. -->
1721
+ <Button
1722
+ variant="ghost"
1723
+ size="icon"
1724
+ onclick={onBack}
1725
+ class="hidden h-8 w-8 hover:cursor-pointer lg:flex"
1726
+ title="Close"
1727
+ >
1728
+ <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1729
+ <path
1730
+ stroke-linecap="round"
1731
+ stroke-linejoin="round"
1732
+ stroke-width="2"
1733
+ d="M6 18L18 6M6 6l12 12"
1734
+ />
1735
+ </svg>
1736
+ </Button>
1737
+ {/if}
1738
+ </div>
1739
+ {/snippet}
1740
+
1741
+ {#snippet editorBreadcrumb()}
1742
+ <div
1743
+ class="text-muted-foreground flex min-w-0 items-center gap-1.5 text-[11px] font-medium tracking-wider uppercase"
1744
+ >
1745
+ <!-- In the visual editor, a breadcrumb-style back arrow before the type · title
1746
+ exits visual editing (back to the document list), replacing the close X. -->
1747
+ {#if presentationMode && onTogglePresentation}
1748
+ <button
1749
+ type="button"
1750
+ onclick={onTogglePresentation}
1751
+ class="hover:text-foreground hover:bg-muted -ml-1 flex shrink-0 cursor-pointer items-center rounded p-1 transition-colors"
1752
+ title="Exit visual editing"
1753
+ aria-label="Exit visual editing"
1754
+ >
1755
+ <ArrowLeft class="h-4 w-4" />
1756
+ </button>
1757
+ {/if}
1758
+ <span class="shrink-0 whitespace-nowrap">{schema?.title || documentType}</span>
1759
+ <!-- The title is only shown here in presentation mode, where there's no <h1>.
1760
+ In the normal editor the <h1> already renders it, so don't duplicate it. -->
1761
+ {#if presentationMode}
1762
+ <span class="shrink-0" aria-hidden="true">·</span>
1763
+ <span class="max-w-[24rem] min-w-0 truncate">{getPreviewTitle()}</span>
1764
+ {/if}
1765
+ </div>
1766
+ {/snippet}
1767
+
1768
+ {#if presentationMode}
1769
+ <AdminSlot name="navbar-start" id="editor-breadcrumb">
1770
+ {@render editorBreadcrumb()}
1771
+ </AdminSlot>
1772
+ <AdminSlot name="navbar-end" id="editor-actions">
1773
+ {@render editorActions()}
1774
+ </AdminSlot>
1775
+ {/if}
1776
+
1431
1777
  <div class="relative flex h-full w-full min-w-0 flex-col overflow-hidden">
1432
1778
  <!-- Hero Header -->
1433
- <div class="bg-background w-full min-w-0 overflow-x-clip px-4 pt-4 pb-5 lg:px-6 lg:pt-5">
1434
- <div class="w-full">
1435
- <!-- Top row: breadcrumb + actions -->
1436
- <div class="mb-4 flex items-center justify-between gap-3">
1779
+ {#if !presentationMode}
1780
+ <div class="bg-background w-full min-w-0 overflow-x-clip px-4 pt-4 pb-5 lg:px-6 lg:pt-5">
1781
+ <div class="w-full">
1782
+ <!-- Top row: breadcrumb + actions. Wraps rather than crumpling when the
1783
+ editor is narrow (e.g. the reference panel). -->
1437
1784
  <div
1438
- class="text-muted-foreground flex min-w-0 items-center gap-2 text-[11px] font-medium tracking-wider uppercase"
1785
+ class="flex flex-wrap items-center justify-between gap-x-3 gap-y-2 {presentationMode
1786
+ ? ''
1787
+ : 'mb-4'}"
1439
1788
  >
1440
- <span class="whitespace-nowrap">{schema?.title || documentType}</span>
1441
- <span aria-hidden="true">·</span>
1442
- <span class="truncate">{getPreviewTitle()}</span>
1789
+ {@render editorBreadcrumb()}
1790
+ {@render editorActions()}
1443
1791
  </div>
1444
1792
 
1445
- <div class="flex shrink-0 items-center gap-2">
1793
+ <!-- Title (the whole hero is already hidden in presentation mode, where
1794
+ the live preview shows the title instead). -->
1795
+ <!-- Always one line (`truncate`); font shrinks where space is tight (narrow
1796
+ editor / reference panel) so more of the title fits before the ellipsis,
1797
+ and stays large on wide screens. -->
1798
+ <h1 class="block w-full min-w-0 truncate text-xl font-semibold tracking-tight lg:text-2xl">
1799
+ {getPreviewTitle()}
1800
+ </h1>
1801
+
1802
+ <!-- Mobile-only status row: save state + draft pill for narrow viewports -->
1803
+ <div class="mt-3 flex items-center justify-between gap-3 sm:hidden">
1804
+ <div class="flex flex-wrap items-center gap-2">
1805
+ {#if !fullDocument?._meta?.publishedHash && documentId && fullDocument?._meta?.status !== 'unpublished'}
1806
+ <span
1807
+ class="text-muted-foreground inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-[10px] font-medium tracking-wider uppercase"
1808
+ >
1809
+ <span class="bg-muted-foreground/60 h-1.5 w-1.5 rounded-full"></span>
1810
+ Draft
1811
+ </span>
1812
+ {/if}
1813
+ </div>
1814
+
1446
1815
  {#if saving}
1447
1816
  <span
1448
- class="text-muted-foreground hidden items-center gap-1.5 text-[10px] font-medium tracking-wider whitespace-nowrap uppercase sm:inline-flex"
1817
+ class="text-muted-foreground inline-flex items-center gap-1.5 text-[10px] font-medium tracking-wider whitespace-nowrap uppercase"
1449
1818
  >
1450
1819
  <span class="bg-muted-foreground/60 h-1.5 w-1.5 animate-pulse rounded-full"></span>
1451
1820
  Saving
1452
1821
  </span>
1453
1822
  {:else if hasUnsavedChanges}
1454
1823
  <span
1455
- class="text-muted-foreground hidden items-center gap-1.5 text-[10px] font-medium tracking-wider whitespace-nowrap uppercase sm:inline-flex"
1824
+ class="text-muted-foreground inline-flex items-center gap-1.5 text-[10px] font-medium tracking-wider whitespace-nowrap uppercase"
1456
1825
  >
1457
1826
  <span class="bg-muted-foreground/60 h-1.5 w-1.5 rounded-full"></span>
1458
1827
  Unsaved
1459
1828
  </span>
1460
1829
  {:else if savedAgoText}
1461
1830
  <span
1462
- class="text-muted-foreground hidden items-center gap-1.5 text-[10px] font-medium tracking-wider whitespace-nowrap uppercase sm:inline-flex"
1831
+ class="text-muted-foreground inline-flex items-center gap-1.5 text-[10px] font-medium tracking-wider whitespace-nowrap uppercase"
1463
1832
  >
1464
1833
  <span class="bg-muted-foreground/60 h-1.5 w-1.5 rounded-full"></span>
1465
1834
  Auto-saved
1466
1835
  </span>
1467
1836
  {/if}
1468
-
1469
- {#if documentId && fullDocument?._meta?.publishedHash}
1470
- {@const isPublished =
1471
- fullDocument?._meta?.status === 'published' && fullDocument?._meta?.publishedAt}
1472
- {@const isUnpub = fullDocument?._meta?.status === 'unpublished'}
1473
- <div class="flex items-center gap-1.5">
1474
- <button
1475
- class="inline-flex cursor-pointer items-center gap-1.5 rounded-full border px-2.5 py-1 text-[10px] font-medium tracking-wider uppercase transition-colors {perspective ===
1476
- 'draft'
1477
- ? 'bg-primary/90 text-primary-foreground border-transparent'
1478
- : 'text-muted-foreground hover:bg-muted'}"
1479
- onclick={() => switchPerspective('draft')}
1480
- >
1481
- <span
1482
- class="bg-muted-foreground/60 h-1.5 w-1.5 rounded-full {perspective === 'draft'
1483
- ? 'bg-primary-foreground/60'
1484
- : ''}"
1485
- ></span>
1486
- Draft
1487
- </button>
1488
- <button
1489
- class="inline-flex cursor-pointer items-center gap-1.5 rounded-full border px-2.5 py-1 text-[10px] font-medium tracking-wider uppercase transition-colors {perspective ===
1490
- 'published'
1491
- ? 'bg-primary text-primary-foreground border-transparent'
1492
- : 'text-muted-foreground hover:bg-muted'}"
1493
- onclick={() => switchPerspective('published')}
1494
- >
1495
- {#if isPublished}
1496
- <span
1497
- class="h-1.5 w-1.5 rounded-full {perspective === 'published'
1498
- ? 'bg-primary-foreground/60'
1499
- : 'bg-green-500'}"
1500
- ></span>
1501
- Published · {timeAgo(new Date(fullDocument._meta.publishedAt))}
1502
- {:else if isUnpub}
1503
- <span
1504
- class="h-1.5 w-1.5 rounded-full {perspective === 'published'
1505
- ? 'bg-primary-foreground/60'
1506
- : 'bg-muted-foreground/60'}"
1507
- ></span>
1508
- Unpublished
1509
- {:else}
1510
- Published
1511
- {/if}
1512
- </button>
1513
- </div>
1514
- {/if}
1515
-
1516
- {#if documentId}
1517
- <div class="relative">
1518
- <Button
1519
- variant="ghost"
1520
- size="icon"
1521
- onclick={() => (showHeaderMenu = !showHeaderMenu)}
1522
- class="h-8 w-8 cursor-pointer"
1523
- >
1524
- <Ellipsis class="h-4 w-4" />
1525
- </Button>
1526
- {#if showHeaderMenu}
1527
- <div
1528
- class="bg-background border-rule absolute top-full right-0 z-[60] mt-1 min-w-[160px] rounded-md border py-1 shadow-lg"
1529
- >
1530
- <button
1531
- onclick={() => {
1532
- showHeaderMenu = false;
1533
- if (onOpenVersionHistory && documentId) {
1534
- onOpenVersionHistory(documentId);
1535
- } else {
1536
- showVersionHistory = true;
1537
- }
1538
- }}
1539
- class="hover:bg-muted flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-left text-sm transition-colors"
1540
- >
1541
- <History class="h-3.5 w-3.5" /> History
1542
- </button>
1543
- <button
1544
- onclick={() => {
1545
- showHeaderMenu = false;
1546
- showInspect = true;
1547
- }}
1548
- class="hover:bg-muted flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-left text-sm transition-colors"
1549
- >
1550
- <Code class="h-3.5 w-3.5" /> Inspect
1551
- </button>
1552
- </div>
1553
- <!-- svelte-ignore a11y_click_events_have_key_events -->
1554
- <!-- svelte-ignore a11y_no_static_element_interactions -->
1555
- <div class="fixed inset-0 z-[55]" onclick={() => (showHeaderMenu = false)}></div>
1556
- {/if}
1557
- </div>
1558
- {/if}
1559
-
1560
- {#if onTogglePresentation && schema?.previewUrl}
1561
- <Button
1562
- variant="ghost"
1563
- size="icon"
1564
- onclick={onTogglePresentation}
1565
- class="hidden h-8 w-8 hover:cursor-pointer lg:flex {presentationMode
1566
- ? 'text-primary'
1567
- : ''}"
1568
- title={presentationMode ? 'Exit presentation mode' : 'Present'}
1569
- >
1570
- <Monitor class="h-4 w-4" />
1571
- </Button>
1572
- {/if}
1573
-
1574
- {#if onToggleFocus}
1575
- <Button
1576
- variant="ghost"
1577
- size="icon"
1578
- onclick={onToggleFocus}
1579
- class="hidden h-8 w-8 hover:cursor-pointer lg:flex"
1580
- title={focusMode ? 'Exit focus mode' : 'Enter focus mode'}
1581
- >
1582
- {#if focusMode}
1583
- <Minimize2 class="h-4 w-4" />
1584
- {:else}
1585
- <Maximize2 class="h-4 w-4" />
1586
- {/if}
1587
- </Button>
1588
- {/if}
1589
-
1590
- <Button
1591
- variant="ghost"
1592
- size="icon"
1593
- onclick={onBack}
1594
- class="hidden h-8 w-8 hover:cursor-pointer lg:flex"
1595
- title="Close"
1596
- >
1597
- <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1598
- <path
1599
- stroke-linecap="round"
1600
- stroke-linejoin="round"
1601
- stroke-width="2"
1602
- d="M6 18L18 6M6 6l12 12"
1603
- />
1604
- </svg>
1605
- </Button>
1606
- </div>
1607
- </div>
1608
-
1609
- <!-- Title -->
1610
- <h1 class="block w-full min-w-0 truncate text-3xl font-semibold tracking-tight">
1611
- {getPreviewTitle()}
1612
- </h1>
1613
-
1614
- <!-- Mobile-only status row: save state + draft pill for narrow viewports -->
1615
- <div class="mt-3 flex items-center justify-between gap-3 sm:hidden">
1616
- <div class="flex flex-wrap items-center gap-2">
1617
- {#if !fullDocument?._meta?.publishedHash && documentId && fullDocument?._meta?.status !== 'unpublished'}
1618
- <span
1619
- class="text-muted-foreground inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-[10px] font-medium tracking-wider uppercase"
1620
- >
1621
- <span class="bg-muted-foreground/60 h-1.5 w-1.5 rounded-full"></span>
1622
- Draft
1623
- </span>
1624
- {/if}
1625
1837
  </div>
1626
-
1627
- {#if saving}
1628
- <span
1629
- class="text-muted-foreground inline-flex items-center gap-1.5 text-[10px] font-medium tracking-wider whitespace-nowrap uppercase"
1630
- >
1631
- <span class="bg-muted-foreground/60 h-1.5 w-1.5 animate-pulse rounded-full"></span>
1632
- Saving
1633
- </span>
1634
- {:else if hasUnsavedChanges}
1635
- <span
1636
- class="text-muted-foreground inline-flex items-center gap-1.5 text-[10px] font-medium tracking-wider whitespace-nowrap uppercase"
1637
- >
1638
- <span class="bg-muted-foreground/60 h-1.5 w-1.5 rounded-full"></span>
1639
- Unsaved
1640
- </span>
1641
- {:else if savedAgoText}
1642
- <span
1643
- class="text-muted-foreground inline-flex items-center gap-1.5 text-[10px] font-medium tracking-wider whitespace-nowrap uppercase"
1644
- >
1645
- <span class="bg-muted-foreground/60 h-1.5 w-1.5 rounded-full"></span>
1646
- Auto-saved
1647
- </span>
1648
- {/if}
1649
1838
  </div>
1650
1839
  </div>
1651
- </div>
1840
+ {/if}
1652
1841
 
1653
1842
  <!-- Content Form -->
1654
1843
  <div
1655
1844
  data-document-editor
1656
1845
  class="relative flex min-h-0 flex-1 {presentationMode ? 'flex-row' : 'flex-col'}"
1657
1846
  >
1847
+ <!-- Non-scrolling positioning boundary: field modals (ObjectModal, asset browser)
1848
+ are `absolute inset-0`, so they bind here and stay contained to the fields
1849
+ column in presentation mode instead of covering the preview iframe. Scroll
1850
+ lives on the inner container so the overlay doesn't scroll with the content. -->
1658
1851
  <div
1659
- class="{presentationMode
1660
- ? 'shrink-0 overflow-y-auto'
1661
- : 'flex flex-1 flex-col overflow-auto'} p-4 lg:p-6"
1852
+ class="relative flex min-h-0 flex-col {presentationMode ? 'shrink-0' : 'flex-1'}"
1662
1853
  style={presentationMode ? `width: ${fieldsWidth}px; min-width: 500px` : undefined}
1663
1854
  >
1664
- <div class="flex w-full flex-1 flex-col gap-4 lg:gap-6">
1665
- {#if saveError}
1666
- <div class="bg-destructive/10 border-destructive/20 rounded-md border p-3">
1667
- <p class="text-destructive text-sm">{saveError}</p>
1668
- </div>
1669
- {/if}
1855
+ <!-- Field Group Tabs a fixed header OUTSIDE the scroll container, so they
1856
+ never scroll away or get clipped by a field (previous sticky-inside-scroll
1857
+ approach was fragile across field types). Only the fields below scroll. -->
1858
+ {#if schema && schema.groups && schema.groups.length > 0}
1859
+ {@const visibleGroups = schema.groups.filter((g) => !g.hidden)}
1860
+ <div
1861
+ class="border-rule/60 flex shrink-0 items-center gap-1 overflow-x-auto border-b px-4 py-2 [scrollbar-width:none] lg:px-6 [&::-webkit-scrollbar]:hidden"
1862
+ >
1863
+ <button
1864
+ type="button"
1865
+ onclick={() => (activeGroup = 'all')}
1866
+ class="cursor-pointer rounded-md px-3 py-1.5 text-sm font-medium whitespace-nowrap transition-colors {activeGroup ===
1867
+ 'all'
1868
+ ? 'bg-muted text-foreground'
1869
+ : 'text-muted-foreground hover:text-foreground'}"
1870
+ >
1871
+ All fields
1872
+ </button>
1873
+ {#each visibleGroups as group (group.name)}
1874
+ <button
1875
+ type="button"
1876
+ onclick={() => (activeGroup = group.name)}
1877
+ class="flex cursor-pointer items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium whitespace-nowrap transition-colors {activeGroup ===
1878
+ group.name
1879
+ ? 'bg-muted text-foreground'
1880
+ : 'text-muted-foreground hover:text-foreground'}"
1881
+ >
1882
+ {#if group.icon}
1883
+ {@const Icon = group.icon}
1884
+ <Icon class="h-4 w-4" />
1885
+ {/if}
1886
+ {group.title}
1887
+ </button>
1888
+ {/each}
1889
+ </div>
1890
+ {/if}
1891
+ <!-- gap between fields is intentionally wider than each field's internal
1892
+ spacing (space-y-2), so a label+description+input reads as one unit and
1893
+ fields don't blur together — tighter within, looser between. Uniform 2rem:
1894
+ mobile felt cramped on the smaller base gap while desktop was fine. -->
1895
+ <div class="min-h-0 w-full flex-1 overflow-y-auto">
1896
+ <div class="flex flex-col gap-8 p-4 lg:p-6">
1897
+ {#if saveError}
1898
+ <div class="bg-destructive/10 border-destructive/20 rounded-md border p-3">
1899
+ <p class="text-destructive text-sm">{saveError}</p>
1900
+ </div>
1901
+ {/if}
1670
1902
 
1671
- {#if schemaError}
1672
- <div class="bg-destructive/10 border-destructive/20 rounded-md border p-3">
1673
- <p class="text-destructive text-sm">Schema Error: {schemaError}</p>
1674
- </div>
1675
- {:else if schemaLoading}
1676
- <div class="p-6 text-center">
1677
- <div class="text-muted-foreground text-sm">Loading schema...</div>
1678
- </div>
1679
- {:else if schema}
1680
- <!-- Orphaned Fields Warning -->
1681
- {#if showOrphanedFields && orphanedFields.length > 0}
1682
- <div class="space-y-3 rounded-md border border-orange-200 bg-orange-50 p-4">
1683
- <div class="flex items-center gap-2">
1684
- <svg
1685
- class="h-5 w-5 text-orange-600"
1686
- fill="none"
1687
- viewBox="0 0 24 24"
1688
- stroke="currentColor"
1689
- >
1690
- <path
1691
- stroke-linecap="round"
1692
- stroke-linejoin="round"
1693
- stroke-width="2"
1694
- d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L5.081 16.5c-.77.833.192 2.5 1.732 2.5z"
1695
- />
1696
- </svg>
1697
- <h4 class="text-sm font-medium text-orange-800">
1698
- {orphanedFields.length} orphaned field{orphanedFields.length === 1 ? '' : 's'} detected
1699
- </h4>
1700
- </div>
1903
+ {#if schemaError}
1904
+ <div class="bg-destructive/10 border-destructive/20 rounded-md border p-3">
1905
+ <p class="text-destructive text-sm">Schema Error: {schemaError}</p>
1906
+ </div>
1907
+ {:else if schemaLoading}
1908
+ <div class="p-6 text-center">
1909
+ <div class="text-muted-foreground text-sm">Loading schema...</div>
1910
+ </div>
1911
+ {:else if schema}
1912
+ <!-- Orphaned Fields Warning -->
1913
+ {#if showOrphanedFields && orphanedFields.length > 0}
1914
+ <div class="space-y-3 rounded-md border border-orange-200 bg-orange-50 p-4">
1915
+ <div class="flex items-center gap-2">
1916
+ <svg
1917
+ class="h-5 w-5 text-orange-600"
1918
+ fill="none"
1919
+ viewBox="0 0 24 24"
1920
+ stroke="currentColor"
1921
+ >
1922
+ <path
1923
+ stroke-linecap="round"
1924
+ stroke-linejoin="round"
1925
+ stroke-width="2"
1926
+ d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L5.081 16.5c-.77.833.192 2.5 1.732 2.5z"
1927
+ />
1928
+ </svg>
1929
+ <h4 class="text-sm font-medium text-orange-800">
1930
+ {orphanedFields.length} orphaned field{orphanedFields.length === 1 ? '' : 's'} detected
1931
+ </h4>
1932
+ </div>
1701
1933
 
1702
- <p class="text-sm text-orange-700">
1703
- These fields exist in your document but are no longer defined in the schema:
1704
- </p>
1934
+ <p class="text-sm text-orange-700">
1935
+ These fields exist in your document but are no longer defined in the schema:
1936
+ </p>
1705
1937
 
1706
- <div class="space-y-2">
1707
- {#each orphanedFields as field, index (index)}
1708
- <div
1709
- class="flex items-center justify-between rounded border border-orange-200 bg-white p-3"
1710
- >
1711
- <div class="flex-1">
1712
- <div class="font-mono text-sm font-medium text-orange-800">
1713
- {field.path || field.key}
1714
- </div>
1715
- <div class="mt-1 text-xs text-orange-600">
1716
- <code class="rounded bg-orange-100 px-1">{JSON.stringify(field.value)}</code
1717
- >
1938
+ <div class="space-y-2">
1939
+ {#each orphanedFields as field, index (index)}
1940
+ <div
1941
+ class="flex items-center justify-between rounded border border-orange-200 bg-white p-3"
1942
+ >
1943
+ <div class="flex-1">
1944
+ <div class="font-mono text-sm font-medium text-orange-800">
1945
+ {field.path || field.key}
1946
+ </div>
1947
+ <div class="mt-1 text-xs text-orange-600">
1948
+ <code class="rounded bg-orange-100 px-1"
1949
+ >{JSON.stringify(field.value)}</code
1950
+ >
1951
+ </div>
1718
1952
  </div>
1953
+ <Button
1954
+ size="sm"
1955
+ variant="outline"
1956
+ onclick={() => removeOrphanedField(field)}
1957
+ class="ml-3 h-8 border-red-200 px-3 text-red-600 hover:border-red-300 hover:bg-red-50"
1958
+ >
1959
+ Remove
1960
+ </Button>
1719
1961
  </div>
1720
- <Button
1721
- size="sm"
1722
- variant="outline"
1723
- onclick={() => removeOrphanedField(field)}
1724
- class="ml-3 h-8 border-red-200 px-3 text-red-600 hover:border-red-300 hover:bg-red-50"
1725
- >
1726
- Remove
1727
- </Button>
1728
- </div>
1729
- {/each}
1730
- </div>
1962
+ {/each}
1963
+ </div>
1731
1964
 
1732
- <div class="flex gap-2 border-t border-orange-200 pt-2">
1733
- <Button
1734
- size="sm"
1735
- variant="outline"
1736
- onclick={cleanupAllOrphanedFields}
1737
- class="border-orange-600 bg-orange-600 text-white hover:bg-orange-700"
1738
- >
1739
- Remove All
1740
- </Button>
1741
- <Button
1742
- size="sm"
1743
- variant="ghost"
1744
- onclick={dismissOrphanedFields}
1745
- class="text-orange-700 hover:text-orange-800"
1746
- >
1747
- Dismiss
1748
- </Button>
1965
+ <div class="flex gap-2 border-t border-orange-200 pt-2">
1966
+ <Button
1967
+ size="sm"
1968
+ variant="outline"
1969
+ onclick={cleanupAllOrphanedFields}
1970
+ class="border-orange-600 bg-orange-600 text-white hover:bg-orange-700"
1971
+ >
1972
+ Remove All
1973
+ </Button>
1974
+ <Button
1975
+ size="sm"
1976
+ variant="ghost"
1977
+ onclick={dismissOrphanedFields}
1978
+ class="text-orange-700 hover:text-orange-800"
1979
+ >
1980
+ Dismiss
1981
+ </Button>
1982
+ </div>
1749
1983
  </div>
1750
- </div>
1751
- {/if}
1984
+ {/if}
1752
1985
 
1753
- <!-- Field Group Tabs -->
1754
- {#if schema.groups && schema.groups.length > 0}
1755
- {@const visibleGroups = schema.groups.filter((g) => !g.hidden)}
1756
- <div class="mb-4 flex items-center gap-1 overflow-x-auto py-1">
1757
- <button
1758
- type="button"
1759
- onclick={() => (activeGroup = 'all')}
1760
- class="cursor-pointer rounded-md px-3 py-1.5 text-sm font-medium whitespace-nowrap transition-colors {activeGroup ===
1761
- 'all'
1762
- ? 'bg-muted text-foreground'
1763
- : 'text-muted-foreground hover:text-foreground'}"
1764
- >
1765
- All fields
1766
- </button>
1767
- {#each visibleGroups as group (group.name)}
1768
- <button
1769
- type="button"
1770
- onclick={() => (activeGroup = group.name)}
1771
- class="flex cursor-pointer items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium whitespace-nowrap transition-colors {activeGroup ===
1772
- group.name
1773
- ? 'bg-muted text-foreground'
1774
- : 'text-muted-foreground hover:text-foreground'}"
1986
+ <!-- Dynamic Schema Fields -->
1987
+ <svelte:boundary
1988
+ onerror={(error) =>
1989
+ cmsLogger.error('[DocumentEditor]', 'Error in editor content:', error)}
1990
+ >
1991
+ {#each schema.fields.filter((f) => fieldInGroup(f, activeGroup) && !hiddenFieldNames.has(f.name)) as field (field.name)}
1992
+ {@const viewData =
1993
+ isPreviewingVersion && activePreview
1994
+ ? activePreview.data
1995
+ : isViewingPublished && publishedData
1996
+ ? publishedData
1997
+ : documentData}
1998
+ <div
1999
+ data-field-name={field.name}
2000
+ class="rounded-md transition-all duration-300 {focusedFieldName === field.name
2001
+ ? 'ring-primary/40 ring-2 ring-offset-2'
2002
+ : ''}"
1775
2003
  >
1776
- {#if group.icon}
1777
- {@const Icon = group.icon}
1778
- <Icon class="h-4 w-4" />
1779
- {/if}
1780
- {group.title}
1781
- </button>
2004
+ <SchemaField
2005
+ {field}
2006
+ value={viewData[field.name]}
2007
+ documentData={viewData}
2008
+ onUpdate={(newValue) => {
2009
+ if (isViewingPublished) return;
2010
+ documentData = { ...documentData, [field.name]: newValue };
2011
+ hasUnsavedChanges = true;
2012
+ }}
2013
+ {onOpenReference}
2014
+ schemaType={documentType}
2015
+ readonly={isReadOnly ||
2016
+ isViewingPublished ||
2017
+ isPreviewingVersion ||
2018
+ isFieldReadonly(field.name)}
2019
+ organizationId={fullDocument?._meta?.organizationId}
2020
+ />
2021
+ </div>
1782
2022
  {/each}
1783
- </div>
1784
- {/if}
1785
-
1786
- <!-- Dynamic Schema Fields -->
1787
- <svelte:boundary
1788
- onerror={(error) =>
1789
- cmsLogger.error('[DocumentEditor]', 'Error in editor content:', error)}
1790
- >
1791
- {#each schema.fields.filter((f) => fieldInGroup(f, activeGroup) && !hiddenFieldNames.has(f.name)) as field (field.name)}
1792
- {@const viewData =
1793
- isPreviewingVersion && activePreview
1794
- ? activePreview.data
1795
- : isViewingPublished && publishedData
1796
- ? publishedData
1797
- : documentData}
1798
- <div
1799
- data-field-name={field.name}
1800
- class="rounded-md transition-all duration-300 {focusedFieldName === field.name
1801
- ? 'ring-primary/40 ring-2 ring-offset-2'
1802
- : ''}"
1803
- >
1804
- <SchemaField
1805
- {field}
1806
- value={viewData[field.name]}
1807
- documentData={viewData}
1808
- onUpdate={(newValue) => {
1809
- if (isViewingPublished) return;
1810
- documentData = { ...documentData, [field.name]: newValue };
1811
- hasUnsavedChanges = true;
1812
- }}
1813
- {onOpenReference}
1814
- schemaType={documentType}
1815
- readonly={isReadOnly ||
1816
- isViewingPublished ||
1817
- isPreviewingVersion ||
1818
- isFieldReadonly(field.name)}
1819
- organizationId={fullDocument?._meta?.organizationId}
1820
- />
1821
- </div>
1822
- {/each}
1823
2023
 
1824
- {#snippet failed(error, reset)}
1825
- <div class="border-destructive/30 bg-destructive/5 rounded-md border p-4 text-center">
1826
- <p class="text-destructive font-medium">Document editor encountered an error</p>
1827
- <p class="text-muted-foreground mt-1 text-sm">
1828
- {error instanceof Error ? error.message : 'Unknown error'}
1829
- </p>
1830
- <button
1831
- class="bg-primary text-primary-foreground mt-3 rounded px-4 py-2 text-sm"
1832
- onclick={reset}
2024
+ {#snippet failed(error, reset)}
2025
+ <div
2026
+ class="border-destructive/30 bg-destructive/5 rounded-md border p-4 text-center"
1833
2027
  >
1834
- Reload editor
1835
- </button>
1836
- </div>
1837
- {/snippet}
1838
- </svelte:boundary>
1839
- {:else}
1840
- <div class="border-muted-foreground/30 rounded-md border border-dashed p-4">
1841
- <p class="text-muted-foreground text-center text-sm">
1842
- No schema found for document type: {documentType}
1843
- </p>
1844
- </div>
1845
- {/if}
2028
+ <p class="text-destructive font-medium">Document editor encountered an error</p>
2029
+ <p class="text-muted-foreground mt-1 text-sm">
2030
+ {error instanceof Error ? error.message : 'Unknown error'}
2031
+ </p>
2032
+ <button
2033
+ class="bg-primary text-primary-foreground mt-3 rounded px-4 py-2 text-sm"
2034
+ onclick={reset}
2035
+ >
2036
+ Reload editor
2037
+ </button>
2038
+ </div>
2039
+ {/snippet}
2040
+ </svelte:boundary>
2041
+ {:else}
2042
+ <div class="border-muted-foreground/30 rounded-md border border-dashed p-4">
2043
+ <p class="text-muted-foreground text-center text-sm">
2044
+ No schema found for document type: {documentType}
2045
+ </p>
2046
+ </div>
2047
+ {/if}
2048
+ </div>
1846
2049
  </div>
1847
2050
  </div>
1848
2051
 
@@ -1909,6 +2112,49 @@
1909
2112
  >
1910
2113
  </div>
1911
2114
 
2115
+ <!-- Viewport switcher -->
2116
+ <div class="bg-muted flex items-center gap-0.5 rounded p-0.5">
2117
+ {#each [{ v: 'desktop', Icon: Monitor, label: 'Desktop' }, { v: 'tablet', Icon: Tablet, label: 'Tablet' }, { v: 'mobile', Icon: Smartphone, label: 'Mobile' }] as { v, Icon, label } (v)}
2118
+ <button
2119
+ onclick={() => (previewViewport = v as typeof previewViewport)}
2120
+ class="cursor-pointer rounded p-1 transition-colors {previewViewport === v
2121
+ ? 'bg-background text-foreground shadow-sm'
2122
+ : 'text-muted-foreground hover:text-foreground'}"
2123
+ title={label}
2124
+ aria-pressed={previewViewport === v}
2125
+ >
2126
+ <Icon class="h-3.5 w-3.5" />
2127
+ </button>
2128
+ {/each}
2129
+ </div>
2130
+
2131
+ <!-- Zoom -->
2132
+ <div class="ml-1 flex items-center gap-0.5">
2133
+ <button
2134
+ onclick={() => zoomBy(-0.1)}
2135
+ disabled={previewZoom <= 0.5}
2136
+ class="hover:bg-muted text-muted-foreground hover:text-foreground cursor-pointer rounded p-1.5 transition-colors disabled:cursor-default disabled:opacity-40"
2137
+ title="Zoom out"
2138
+ >
2139
+ <ZoomOut class="h-3.5 w-3.5" />
2140
+ </button>
2141
+ <button
2142
+ onclick={() => (previewZoom = 1)}
2143
+ class="text-muted-foreground hover:text-foreground w-9 cursor-pointer text-center font-mono text-[11px] tabular-nums transition-colors"
2144
+ title="Reset zoom"
2145
+ >
2146
+ {Math.round(previewZoom * 100)}%
2147
+ </button>
2148
+ <button
2149
+ onclick={() => zoomBy(0.1)}
2150
+ disabled={previewZoom >= 1.5}
2151
+ class="hover:bg-muted text-muted-foreground hover:text-foreground cursor-pointer rounded p-1.5 transition-colors disabled:cursor-default disabled:opacity-40"
2152
+ title="Zoom in"
2153
+ >
2154
+ <ZoomIn class="h-3.5 w-3.5" />
2155
+ </button>
2156
+ </div>
2157
+
1912
2158
  <!-- Open in new tab -->
1913
2159
  <button
1914
2160
  onclick={openPreviewInNewTab}
@@ -1919,13 +2165,28 @@
1919
2165
  </button>
1920
2166
  </div>
1921
2167
 
1922
- <div class="relative min-h-0 flex-1">
1923
- <iframe
1924
- bind:this={iframeRef}
1925
- src={iframeUrl}
1926
- class="h-full w-full border-none"
1927
- title="Page preview"
1928
- ></iframe>
2168
+ <!-- `safe center`: centre the frame when it fits, but align to start when
2169
+ it's wider than the container (zoomed in) so you can scroll to the
2170
+ overflow — plain `justify-center` makes the left side unreachable. -->
2171
+ <div
2172
+ class="bg-muted/20 relative flex min-h-0 flex-1 [justify-content:safe_center] {previewViewport ===
2173
+ 'desktop'
2174
+ ? 'overflow-hidden'
2175
+ : 'overflow-auto p-4'}"
2176
+ >
2177
+ <div
2178
+ class={previewViewport === 'desktop'
2179
+ ? 'h-full w-full'
2180
+ : 'bg-background h-full shrink-0 overflow-hidden rounded-xl border shadow-sm'}
2181
+ style={frameStyle}
2182
+ >
2183
+ <iframe
2184
+ bind:this={iframeRef}
2185
+ src={iframeUrl}
2186
+ class="h-full w-full border-none"
2187
+ title="Page preview"
2188
+ ></iframe>
2189
+ </div>
1929
2190
  {#if isResizing}
1930
2191
  <div class="absolute inset-0"></div>
1931
2192
  {/if}