@aphexcms/cms-core 2.0.9 → 2.0.11

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 (381) hide show
  1. package/LICENSE +21 -0
  2. package/dist/api/api-keys.d.ts +8 -7
  3. package/dist/api/api-keys.d.ts.map +1 -1
  4. package/dist/api/assets.d.ts +7 -22
  5. package/dist/api/assets.d.ts.map +1 -1
  6. package/dist/api/assets.js +1 -10
  7. package/dist/api/documents.d.ts +17 -16
  8. package/dist/api/documents.d.ts.map +1 -1
  9. package/dist/api/documents.js +2 -2
  10. package/dist/api/index.d.ts +1 -0
  11. package/dist/api/index.d.ts.map +1 -1
  12. package/dist/api/index.js +1 -0
  13. package/dist/api/organizations.d.ts +15 -35
  14. package/dist/api/organizations.d.ts.map +1 -1
  15. package/dist/api/roles.d.ts +22 -0
  16. package/dist/api/roles.d.ts.map +1 -0
  17. package/dist/api/roles.js +26 -0
  18. package/dist/api/schemas/api-keys.d.ts +62 -0
  19. package/dist/api/schemas/api-keys.d.ts.map +1 -0
  20. package/dist/api/schemas/api-keys.js +28 -0
  21. package/dist/api/schemas/assets.d.ts +174 -0
  22. package/dist/api/schemas/assets.d.ts.map +1 -0
  23. package/dist/api/schemas/assets.js +99 -0
  24. package/dist/api/schemas/documents.d.ts +325 -0
  25. package/dist/api/schemas/documents.d.ts.map +1 -0
  26. package/dist/api/schemas/documents.js +161 -0
  27. package/dist/api/schemas/organizations.d.ts +41 -0
  28. package/dist/api/schemas/organizations.d.ts.map +1 -0
  29. package/dist/api/schemas/organizations.js +56 -0
  30. package/dist/api/schemas/roles.d.ts +77 -0
  31. package/dist/api/schemas/roles.d.ts.map +1 -0
  32. package/dist/api/schemas/roles.js +42 -0
  33. package/dist/api/schemas/user.d.ts +10 -0
  34. package/dist/api/schemas/user.d.ts.map +1 -0
  35. package/dist/api/schemas/user.js +11 -0
  36. package/dist/api/user.d.ts +4 -6
  37. package/dist/api/user.d.ts.map +1 -1
  38. package/dist/app-augment.d.ts +32 -0
  39. package/dist/app-augment.d.ts.map +1 -0
  40. package/dist/app-augment.js +18 -0
  41. package/dist/auth/auth-hooks.d.ts +2 -1
  42. package/dist/auth/auth-hooks.d.ts.map +1 -1
  43. package/dist/auth/auth-hooks.js +22 -2
  44. package/dist/auth/provider.d.ts +5 -0
  45. package/dist/auth/provider.d.ts.map +1 -1
  46. package/dist/cli/index.js +0 -0
  47. package/dist/client/index.d.ts +4 -0
  48. package/dist/client/index.d.ts.map +1 -1
  49. package/dist/client/index.js +7 -0
  50. package/dist/components/AdminApp.svelte +306 -142
  51. package/dist/components/AdminApp.svelte.d.ts +8 -0
  52. package/dist/components/AdminApp.svelte.d.ts.map +1 -1
  53. package/dist/components/admin/DocumentEditor.svelte +653 -365
  54. package/dist/components/admin/DocumentEditor.svelte.d.ts +2 -0
  55. package/dist/components/admin/DocumentEditor.svelte.d.ts.map +1 -1
  56. package/dist/components/admin/DocumentVersionPanel.svelte +57 -44
  57. package/dist/components/admin/DocumentVersionPanel.svelte.d.ts.map +1 -1
  58. package/dist/components/admin/MediaBrowser.svelte +70 -30
  59. package/dist/components/admin/MediaBrowser.svelte.d.ts.map +1 -1
  60. package/dist/components/admin/ObjectModal.svelte +70 -95
  61. package/dist/components/admin/ObjectModal.svelte.d.ts +1 -2
  62. package/dist/components/admin/ObjectModal.svelte.d.ts.map +1 -1
  63. package/dist/components/admin/PermissionsDebug.svelte +119 -0
  64. package/dist/components/admin/PermissionsDebug.svelte.d.ts +4 -0
  65. package/dist/components/admin/PermissionsDebug.svelte.d.ts.map +1 -0
  66. package/dist/components/admin/SchemaField.svelte +3 -1
  67. package/dist/components/admin/SchemaField.svelte.d.ts.map +1 -1
  68. package/dist/components/admin/confirm-dialog/ConfirmDialogHost.svelte +35 -0
  69. package/dist/components/admin/confirm-dialog/ConfirmDialogHost.svelte.d.ts +19 -0
  70. package/dist/components/admin/confirm-dialog/ConfirmDialogHost.svelte.d.ts.map +1 -0
  71. package/dist/components/admin/confirm-dialog/confirm-dialog.svelte.d.ts +16 -0
  72. package/dist/components/admin/confirm-dialog/confirm-dialog.svelte.d.ts.map +1 -0
  73. package/dist/components/admin/confirm-dialog/confirm-dialog.svelte.js +29 -0
  74. package/dist/components/admin/fields/ArrayField.svelte +104 -108
  75. package/dist/components/admin/fields/ArrayField.svelte.d.ts.map +1 -1
  76. package/dist/components/admin/fields/FileField.svelte +22 -17
  77. package/dist/components/admin/fields/FileField.svelte.d.ts.map +1 -1
  78. package/dist/components/admin/fields/ImageField.svelte +21 -15
  79. package/dist/components/admin/fields/ImageField.svelte.d.ts.map +1 -1
  80. package/dist/components/admin/fields/ReferenceField.svelte +142 -112
  81. package/dist/components/admin/fields/ReferenceField.svelte.d.ts.map +1 -1
  82. package/dist/components/admin/fields/TextareaField.svelte +4 -3
  83. package/dist/components/admin/fields/TextareaField.svelte.d.ts +2 -2
  84. package/dist/components/admin/fields/TextareaField.svelte.d.ts.map +1 -1
  85. package/dist/components/layout/Sidebar.svelte +21 -11
  86. package/dist/components/layout/Sidebar.svelte.d.ts.map +1 -1
  87. package/dist/components/layout/sidebar/NavMain.svelte +1 -0
  88. package/dist/db/interfaces/document.d.ts.map +1 -1
  89. package/dist/db/interfaces/index.d.ts +3 -1
  90. package/dist/db/interfaces/index.d.ts.map +1 -1
  91. package/dist/db/interfaces/organization.d.ts +1 -1
  92. package/dist/db/interfaces/organization.d.ts.map +1 -1
  93. package/dist/db/interfaces/role.d.ts +25 -0
  94. package/dist/db/interfaces/role.d.ts.map +1 -0
  95. package/dist/db/interfaces/role.js +1 -0
  96. package/dist/engine.d.ts.map +1 -1
  97. package/dist/engine.js +5 -0
  98. package/dist/field-access.d.ts +30 -0
  99. package/dist/field-access.d.ts.map +1 -0
  100. package/dist/field-access.js +79 -0
  101. package/dist/hooks.d.ts +2 -0
  102. package/dist/hooks.d.ts.map +1 -1
  103. package/dist/hooks.js +23 -13
  104. package/dist/lib/api/api-keys.d.ts +8 -7
  105. package/dist/lib/api/api-keys.d.ts.map +1 -1
  106. package/dist/lib/api/api-keys.js.map +1 -1
  107. package/dist/lib/api/assets.d.ts +7 -22
  108. package/dist/lib/api/assets.d.ts.map +1 -1
  109. package/dist/lib/api/assets.js +1 -10
  110. package/dist/lib/api/assets.js.map +1 -1
  111. package/dist/lib/api/documents.d.ts +17 -16
  112. package/dist/lib/api/documents.d.ts.map +1 -1
  113. package/dist/lib/api/documents.js +2 -2
  114. package/dist/lib/api/documents.js.map +1 -1
  115. package/dist/lib/api/index.d.ts +1 -0
  116. package/dist/lib/api/index.d.ts.map +1 -1
  117. package/dist/lib/api/index.js +1 -0
  118. package/dist/lib/api/index.js.map +1 -1
  119. package/dist/lib/api/organizations.d.ts +15 -35
  120. package/dist/lib/api/organizations.d.ts.map +1 -1
  121. package/dist/lib/api/organizations.js.map +1 -1
  122. package/dist/lib/api/roles.d.ts +22 -0
  123. package/dist/lib/api/roles.d.ts.map +1 -0
  124. package/dist/lib/api/roles.js +27 -0
  125. package/dist/lib/api/roles.js.map +1 -0
  126. package/dist/lib/api/schemas/api-keys.d.ts +62 -0
  127. package/dist/lib/api/schemas/api-keys.d.ts.map +1 -0
  128. package/dist/lib/api/schemas/api-keys.js +29 -0
  129. package/dist/lib/api/schemas/api-keys.js.map +1 -0
  130. package/dist/lib/api/schemas/assets.d.ts +174 -0
  131. package/dist/lib/api/schemas/assets.d.ts.map +1 -0
  132. package/dist/lib/api/schemas/assets.js +100 -0
  133. package/dist/lib/api/schemas/assets.js.map +1 -0
  134. package/dist/lib/api/schemas/documents.d.ts +325 -0
  135. package/dist/lib/api/schemas/documents.d.ts.map +1 -0
  136. package/dist/lib/api/schemas/documents.js +162 -0
  137. package/dist/lib/api/schemas/documents.js.map +1 -0
  138. package/dist/lib/api/schemas/organizations.d.ts +41 -0
  139. package/dist/lib/api/schemas/organizations.d.ts.map +1 -0
  140. package/dist/lib/api/schemas/organizations.js +57 -0
  141. package/dist/lib/api/schemas/organizations.js.map +1 -0
  142. package/dist/lib/api/schemas/roles.d.ts +77 -0
  143. package/dist/lib/api/schemas/roles.d.ts.map +1 -0
  144. package/dist/lib/api/schemas/roles.js +43 -0
  145. package/dist/lib/api/schemas/roles.js.map +1 -0
  146. package/dist/lib/api/schemas/user.d.ts +10 -0
  147. package/dist/lib/api/schemas/user.d.ts.map +1 -0
  148. package/dist/lib/api/schemas/user.js +12 -0
  149. package/dist/lib/api/schemas/user.js.map +1 -0
  150. package/dist/lib/api/user.d.ts +4 -6
  151. package/dist/lib/api/user.d.ts.map +1 -1
  152. package/dist/lib/api/user.js.map +1 -1
  153. package/dist/lib/app-augment.d.ts +32 -0
  154. package/dist/lib/app-augment.d.ts.map +1 -0
  155. package/dist/lib/app-augment.js +19 -0
  156. package/dist/lib/app-augment.js.map +1 -0
  157. package/dist/lib/auth/auth-hooks.d.ts +2 -1
  158. package/dist/lib/auth/auth-hooks.d.ts.map +1 -1
  159. package/dist/lib/auth/auth-hooks.js +22 -2
  160. package/dist/lib/auth/auth-hooks.js.map +1 -1
  161. package/dist/lib/auth/provider.d.ts +5 -0
  162. package/dist/lib/auth/provider.d.ts.map +1 -1
  163. package/dist/lib/client/index.d.ts +4 -0
  164. package/dist/lib/client/index.d.ts.map +1 -1
  165. package/dist/lib/client/index.js +7 -0
  166. package/dist/lib/client/index.js.map +1 -1
  167. package/dist/lib/components/admin/confirm-dialog/confirm-dialog.svelte.d.ts +16 -0
  168. package/dist/lib/components/admin/confirm-dialog/confirm-dialog.svelte.d.ts.map +1 -0
  169. package/dist/lib/components/admin/confirm-dialog/confirm-dialog.svelte.js +30 -0
  170. package/dist/lib/components/admin/confirm-dialog/confirm-dialog.svelte.js.map +1 -0
  171. package/dist/lib/db/interfaces/document.d.ts.map +1 -1
  172. package/dist/lib/db/interfaces/index.d.ts +3 -1
  173. package/dist/lib/db/interfaces/index.d.ts.map +1 -1
  174. package/dist/lib/db/interfaces/organization.d.ts +1 -1
  175. package/dist/lib/db/interfaces/organization.d.ts.map +1 -1
  176. package/dist/lib/db/interfaces/role.d.ts +25 -0
  177. package/dist/lib/db/interfaces/role.d.ts.map +1 -0
  178. package/dist/lib/db/interfaces/role.js +2 -0
  179. package/dist/lib/db/interfaces/role.js.map +1 -0
  180. package/dist/lib/engine.d.ts.map +1 -1
  181. package/dist/lib/engine.js +5 -0
  182. package/dist/lib/engine.js.map +1 -1
  183. package/dist/lib/field-access.d.ts +30 -0
  184. package/dist/lib/field-access.d.ts.map +1 -0
  185. package/dist/lib/field-access.js +80 -0
  186. package/dist/lib/field-access.js.map +1 -0
  187. package/dist/lib/hooks.d.ts +2 -0
  188. package/dist/lib/hooks.d.ts.map +1 -1
  189. package/dist/lib/hooks.js +23 -13
  190. package/dist/lib/hooks.js.map +1 -1
  191. package/dist/lib/local-api/collection-api.d.ts +2 -0
  192. package/dist/lib/local-api/collection-api.d.ts.map +1 -1
  193. package/dist/lib/local-api/collection-api.js +98 -30
  194. package/dist/lib/local-api/collection-api.js.map +1 -1
  195. package/dist/lib/local-api/index.d.ts.map +1 -1
  196. package/dist/lib/local-api/index.js.map +1 -1
  197. package/dist/lib/local-api/permissions.d.ts +26 -26
  198. package/dist/lib/local-api/permissions.d.ts.map +1 -1
  199. package/dist/lib/local-api/permissions.js +122 -80
  200. package/dist/lib/local-api/permissions.js.map +1 -1
  201. package/dist/lib/permissions-context.svelte.d.ts +44 -0
  202. package/dist/lib/permissions-context.svelte.d.ts.map +1 -0
  203. package/dist/lib/permissions-context.svelte.js +80 -0
  204. package/dist/lib/permissions-context.svelte.js.map +1 -0
  205. package/dist/lib/routes/assets-bulk.d.ts.map +1 -1
  206. package/dist/lib/routes/assets-bulk.js +10 -3
  207. package/dist/lib/routes/assets-bulk.js.map +1 -1
  208. package/dist/lib/routes/assets-by-id.d.ts.map +1 -1
  209. package/dist/lib/routes/assets-by-id.js +11 -1
  210. package/dist/lib/routes/assets-by-id.js.map +1 -1
  211. package/dist/lib/routes/assets-references-counts.d.ts.map +1 -1
  212. package/dist/lib/routes/assets-references-counts.js +12 -2
  213. package/dist/lib/routes/assets-references-counts.js.map +1 -1
  214. package/dist/lib/routes/assets.d.ts.map +1 -1
  215. package/dist/lib/routes/assets.js +14 -13
  216. package/dist/lib/routes/assets.js.map +1 -1
  217. package/dist/lib/routes/document-versions.d.ts.map +1 -1
  218. package/dist/lib/routes/document-versions.js +16 -2
  219. package/dist/lib/routes/document-versions.js.map +1 -1
  220. package/dist/lib/routes/documents-by-id.d.ts.map +1 -1
  221. package/dist/lib/routes/documents-by-id.js +15 -3
  222. package/dist/lib/routes/documents-by-id.js.map +1 -1
  223. package/dist/lib/routes/documents-query.d.ts.map +1 -1
  224. package/dist/lib/routes/documents-query.js +13 -10
  225. package/dist/lib/routes/documents-query.js.map +1 -1
  226. package/dist/lib/routes/documents.d.ts.map +1 -1
  227. package/dist/lib/routes/documents.js +29 -26
  228. package/dist/lib/routes/documents.js.map +1 -1
  229. package/dist/lib/routes/organizations-by-id.d.ts.map +1 -1
  230. package/dist/lib/routes/organizations-by-id.js +17 -4
  231. package/dist/lib/routes/organizations-by-id.js.map +1 -1
  232. package/dist/lib/routes/organizations-invitations.d.ts.map +1 -1
  233. package/dist/lib/routes/organizations-invitations.js +48 -18
  234. package/dist/lib/routes/organizations-invitations.js.map +1 -1
  235. package/dist/lib/routes/organizations-members.d.ts.map +1 -1
  236. package/dist/lib/routes/organizations-members.js +24 -18
  237. package/dist/lib/routes/organizations-members.js.map +1 -1
  238. package/dist/lib/routes/organizations-switch.d.ts.map +1 -1
  239. package/dist/lib/routes/organizations-switch.js +7 -3
  240. package/dist/lib/routes/organizations-switch.js.map +1 -1
  241. package/dist/lib/routes/organizations.d.ts.map +1 -1
  242. package/dist/lib/routes/organizations.js +11 -5
  243. package/dist/lib/routes/organizations.js.map +1 -1
  244. package/dist/lib/routes/roles-by-name.d.ts +4 -0
  245. package/dist/lib/routes/roles-by-name.d.ts.map +1 -0
  246. package/dist/lib/routes/roles-by-name.js +112 -0
  247. package/dist/lib/routes/roles-by-name.js.map +1 -0
  248. package/dist/lib/routes/roles.d.ts +4 -0
  249. package/dist/lib/routes/roles.d.ts.map +1 -0
  250. package/dist/lib/routes/roles.js +91 -0
  251. package/dist/lib/routes/roles.js.map +1 -0
  252. package/dist/lib/routes/user-preferences.d.ts.map +1 -1
  253. package/dist/lib/routes/user-preferences.js +7 -13
  254. package/dist/lib/routes/user-preferences.js.map +1 -1
  255. package/dist/lib/routes-exports.d.ts +2 -0
  256. package/dist/lib/routes-exports.d.ts.map +1 -1
  257. package/dist/lib/routes-exports.js +3 -0
  258. package/dist/lib/routes-exports.js.map +1 -1
  259. package/dist/lib/save-state-context.svelte.d.ts +8 -0
  260. package/dist/lib/save-state-context.svelte.d.ts.map +1 -0
  261. package/dist/lib/save-state-context.svelte.js +9 -0
  262. package/dist/lib/save-state-context.svelte.js.map +1 -0
  263. package/dist/lib/schema-utils/validator.d.ts.map +1 -1
  264. package/dist/lib/schema-utils/validator.js +14 -0
  265. package/dist/lib/schema-utils/validator.js.map +1 -1
  266. package/dist/lib/server/index.d.ts +1 -0
  267. package/dist/lib/server/index.d.ts.map +1 -1
  268. package/dist/lib/server/index.js +1 -0
  269. package/dist/lib/server/index.js.map +1 -1
  270. package/dist/lib/services/index.d.ts +1 -0
  271. package/dist/lib/services/index.d.ts.map +1 -1
  272. package/dist/lib/services/index.js +1 -0
  273. package/dist/lib/services/index.js.map +1 -1
  274. package/dist/lib/services/roles-service.d.ts +35 -0
  275. package/dist/lib/services/roles-service.d.ts.map +1 -0
  276. package/dist/lib/services/roles-service.js +91 -0
  277. package/dist/lib/services/roles-service.js.map +1 -0
  278. package/dist/lib/services/version-service.d.ts.map +1 -1
  279. package/dist/lib/services/version-service.js.map +1 -1
  280. package/dist/lib/types/auth.d.ts +43 -9
  281. package/dist/lib/types/auth.d.ts.map +1 -1
  282. package/dist/lib/types/auth.js +31 -26
  283. package/dist/lib/types/auth.js.map +1 -1
  284. package/dist/lib/types/capabilities.d.ts +104 -0
  285. package/dist/lib/types/capabilities.d.ts.map +1 -0
  286. package/dist/lib/types/capabilities.js +217 -0
  287. package/dist/lib/types/capabilities.js.map +1 -0
  288. package/dist/lib/types/index.d.ts +1 -0
  289. package/dist/lib/types/index.d.ts.map +1 -1
  290. package/dist/lib/types/index.js +1 -0
  291. package/dist/lib/types/index.js.map +1 -1
  292. package/dist/lib/types/organization.d.ts +13 -4
  293. package/dist/lib/types/organization.d.ts.map +1 -1
  294. package/dist/lib/types/schemas.d.ts +71 -0
  295. package/dist/lib/types/schemas.d.ts.map +1 -1
  296. package/dist/lib/types/sidebar.d.ts +1 -1
  297. package/dist/lib/types/sidebar.d.ts.map +1 -1
  298. package/dist/lib/utils/mime-detect.d.ts.map +1 -1
  299. package/dist/lib/utils/mime-detect.js +28 -6
  300. package/dist/lib/utils/mime-detect.js.map +1 -1
  301. package/dist/local-api/collection-api.d.ts +2 -0
  302. package/dist/local-api/collection-api.d.ts.map +1 -1
  303. package/dist/local-api/collection-api.js +98 -30
  304. package/dist/local-api/index.d.ts.map +1 -1
  305. package/dist/local-api/permissions.d.ts +26 -26
  306. package/dist/local-api/permissions.d.ts.map +1 -1
  307. package/dist/local-api/permissions.js +122 -80
  308. package/dist/permissions-context.svelte.d.ts +44 -0
  309. package/dist/permissions-context.svelte.d.ts.map +1 -0
  310. package/dist/permissions-context.svelte.js +79 -0
  311. package/dist/plugins/README.md +20 -20
  312. package/dist/routes/assets-bulk.d.ts.map +1 -1
  313. package/dist/routes/assets-bulk.js +10 -3
  314. package/dist/routes/assets-by-id.d.ts.map +1 -1
  315. package/dist/routes/assets-by-id.js +11 -1
  316. package/dist/routes/assets-references-counts.d.ts.map +1 -1
  317. package/dist/routes/assets-references-counts.js +12 -2
  318. package/dist/routes/assets.d.ts.map +1 -1
  319. package/dist/routes/assets.js +14 -13
  320. package/dist/routes/document-versions.d.ts.map +1 -1
  321. package/dist/routes/document-versions.js +16 -2
  322. package/dist/routes/documents-by-id.d.ts.map +1 -1
  323. package/dist/routes/documents-by-id.js +15 -3
  324. package/dist/routes/documents-query.d.ts.map +1 -1
  325. package/dist/routes/documents-query.js +13 -10
  326. package/dist/routes/documents.d.ts.map +1 -1
  327. package/dist/routes/documents.js +29 -26
  328. package/dist/routes/organizations-by-id.d.ts.map +1 -1
  329. package/dist/routes/organizations-by-id.js +17 -4
  330. package/dist/routes/organizations-invitations.d.ts.map +1 -1
  331. package/dist/routes/organizations-invitations.js +48 -18
  332. package/dist/routes/organizations-members.d.ts.map +1 -1
  333. package/dist/routes/organizations-members.js +24 -18
  334. package/dist/routes/organizations-switch.d.ts.map +1 -1
  335. package/dist/routes/organizations-switch.js +7 -3
  336. package/dist/routes/organizations.d.ts.map +1 -1
  337. package/dist/routes/organizations.js +11 -5
  338. package/dist/routes/roles-by-name.d.ts +4 -0
  339. package/dist/routes/roles-by-name.d.ts.map +1 -0
  340. package/dist/routes/roles-by-name.js +111 -0
  341. package/dist/routes/roles.d.ts +4 -0
  342. package/dist/routes/roles.d.ts.map +1 -0
  343. package/dist/routes/roles.js +90 -0
  344. package/dist/routes/user-preferences.d.ts.map +1 -1
  345. package/dist/routes/user-preferences.js +7 -13
  346. package/dist/routes-exports.d.ts +2 -0
  347. package/dist/routes-exports.d.ts.map +1 -1
  348. package/dist/routes-exports.js +3 -0
  349. package/dist/save-state-context.svelte.d.ts +8 -0
  350. package/dist/save-state-context.svelte.d.ts.map +1 -0
  351. package/dist/save-state-context.svelte.js +8 -0
  352. package/dist/schema-utils/validator.d.ts.map +1 -1
  353. package/dist/schema-utils/validator.js +14 -0
  354. package/dist/server/index.d.ts +1 -0
  355. package/dist/server/index.d.ts.map +1 -1
  356. package/dist/server/index.js +1 -0
  357. package/dist/services/index.d.ts +1 -0
  358. package/dist/services/index.d.ts.map +1 -1
  359. package/dist/services/index.js +1 -0
  360. package/dist/services/roles-service.d.ts +35 -0
  361. package/dist/services/roles-service.d.ts.map +1 -0
  362. package/dist/services/roles-service.js +90 -0
  363. package/dist/services/version-service.d.ts.map +1 -1
  364. package/dist/types/auth.d.ts +43 -9
  365. package/dist/types/auth.d.ts.map +1 -1
  366. package/dist/types/auth.js +31 -26
  367. package/dist/types/capabilities.d.ts +104 -0
  368. package/dist/types/capabilities.d.ts.map +1 -0
  369. package/dist/types/capabilities.js +216 -0
  370. package/dist/types/index.d.ts +1 -0
  371. package/dist/types/index.d.ts.map +1 -1
  372. package/dist/types/index.js +1 -0
  373. package/dist/types/organization.d.ts +13 -4
  374. package/dist/types/organization.d.ts.map +1 -1
  375. package/dist/types/schemas.d.ts +71 -0
  376. package/dist/types/schemas.d.ts.map +1 -1
  377. package/dist/types/sidebar.d.ts +1 -1
  378. package/dist/types/sidebar.d.ts.map +1 -1
  379. package/dist/utils/mime-detect.d.ts.map +1 -1
  380. package/dist/utils/mime-detect.js +28 -6
  381. package/package.json +142 -138
@@ -10,11 +10,13 @@
10
10
  import { Rule } from '../../field-validation/rule';
11
11
  import { hasUnpublishedChanges } from '../../utils/content-hash';
12
12
  import { setSchemaContext } from '../../schema-context.svelte';
13
+ import { setSaveStateContext } from '../../save-state-context.svelte';
14
+ import { usePermissions } from '../../permissions-context.svelte';
13
15
  import { getDefaultValueForFieldType } from '../../utils/field-defaults';
14
- import elementEvents from '../../utils/element-events';
15
16
  import { cmsLogger } from '../../utils/logger';
16
17
  import { toast } from 'svelte-sonner';
17
- import { History, EyeOff, Trash2, Ellipsis, Search, Code } from '@lucide/svelte';
18
+ import { confirmDialog } from './confirm-dialog/confirm-dialog.svelte';
19
+ import { History, Trash2, Ellipsis, Code } from '@lucide/svelte';
18
20
 
19
21
  interface Props {
20
22
  schemas: SchemaType[];
@@ -26,9 +28,16 @@
26
28
  onAutoSaved?: (documentId: string, title: string) => void;
27
29
  onDeleted?: () => void;
28
30
  onPublished?: (documentId: string) => void;
31
+ onUnpublished?: (documentId: string) => void;
32
+ onRestored?: (documentId: string) => void;
29
33
  onOpenReference?: (documentId: string, documentType: string) => void;
30
34
  onOpenVersionHistory?: (documentId: string) => void;
31
- externalVersionPreview?: { versionNumber: number; data: Record<string, any>; eventType: string; createdAt?: string } | null;
35
+ externalVersionPreview?: {
36
+ versionNumber: number;
37
+ data: Record<string, any>;
38
+ eventType: string;
39
+ createdAt?: string;
40
+ } | null;
32
41
  isReadOnly?: boolean;
33
42
  }
34
43
 
@@ -42,6 +51,8 @@
42
51
  onAutoSaved,
43
52
  onDeleted,
44
53
  onPublished,
54
+ onUnpublished,
55
+ onRestored,
45
56
  onOpenReference,
46
57
  onOpenVersionHistory,
47
58
  externalVersionPreview = null,
@@ -51,6 +62,27 @@
51
62
  // Set schema context for child components (ArrayField, etc.)
52
63
  setSchemaContext(schemas);
53
64
 
65
+ // Read permissions from the AdminApp context for per-action gating.
66
+ // `isReadOnly` prop, when explicitly true, forces everything off — this is
67
+ // the escape hatch for hosts that embed the editor outside the RBAC shell
68
+ // (version preview, public previews, etc).
69
+ const perms = usePermissions();
70
+ const canCreate = $derived(!isReadOnly && perms.can('document.create'));
71
+ const canUpdate = $derived(!isReadOnly && perms.can('document.update'));
72
+ const canDelete = $derived(!isReadOnly && perms.can('document.delete'));
73
+ const canPublishDoc = $derived(!isReadOnly && perms.can('document.publish'));
74
+ const canUnpublishDoc = $derived(!isReadOnly && perms.can('document.unpublish'));
75
+ // Whichever capability matters for the *current* document state:
76
+ // - create mode (no id yet) → document.create
77
+ // - edit mode → document.update
78
+ // Used for field editability + auto-save gating.
79
+ const canWriteCurrentDoc = $derived(isCreating ? canCreate : canUpdate);
80
+ // "Show no mutation UI at all" — when true, render the read-only badge in
81
+ // place of the publish button cluster.
82
+ const isViewingReadOnly = $derived(
83
+ isReadOnly || (!canCreate && !canUpdate && !canDelete && !canPublishDoc && !canUnpublishDoc)
84
+ );
85
+
54
86
  // Schema and document state
55
87
  let schema = $state<SchemaType | null>(null);
56
88
  let schemaLoading = $state(false);
@@ -64,15 +96,56 @@
64
96
  let lastSaved = $state<Date | null>(null);
65
97
  let publishSuccess = $state<Date | null>(null);
66
98
 
67
-
68
99
  // Perspective toggle
69
100
  let perspective = $state<'draft' | 'published'>('draft');
70
101
  let publishedData = $state<Record<string, any> | null>(null);
71
102
  const isViewingPublished = $derived(perspective === 'published');
72
103
 
104
+ // Field-level access: mirrors the server's strip/drop logic so the form
105
+ // only shows fields the user can see and disables fields they can't write.
106
+ function fieldRoleAllowed(list: string[] | undefined): boolean {
107
+ if (!list) return true;
108
+ const role = perms.role;
109
+ return role !== null && list.includes(role);
110
+ }
111
+ const hiddenFieldNames = $derived(
112
+ new Set(
113
+ (schema?.fields ?? []).filter((f) => !fieldRoleAllowed(f.access?.read)).map((f) => f.name)
114
+ )
115
+ );
116
+ function isFieldReadonly(fieldName: string): boolean {
117
+ const field = schema?.fields.find((f) => f.name === fieldName);
118
+ // In create mode, collection-level write ability is document.create;
119
+ // in edit mode it's document.update. Per-field `access.update` still
120
+ // wins as an additional restriction when the user IS able to write.
121
+ if (!field) return !canWriteCurrentDoc;
122
+ if (!fieldRoleAllowed(field.access?.update)) return true;
123
+ return !canWriteCurrentDoc;
124
+ }
125
+
73
126
  // Inspect modal
74
127
  let showInspect = $state(false);
75
128
 
129
+ // Field group tabs — only render when schema declares `groups`.
130
+ // 'all' = show every field; otherwise filter to fields whose `group`
131
+ // matches (supports string or string[]).
132
+ let activeGroup = $state<string>('all');
133
+
134
+ $effect(() => {
135
+ if (!schema?.groups?.length) {
136
+ activeGroup = 'all';
137
+ return;
138
+ }
139
+ const defaultGroup = schema.groups.find((g) => g.default && !g.hidden);
140
+ activeGroup = defaultGroup?.name ?? 'all';
141
+ });
142
+
143
+ function fieldInGroup(field: { group?: string | string[] }, groupName: string): boolean {
144
+ if (groupName === 'all') return true;
145
+ if (!field.group) return false;
146
+ return Array.isArray(field.group) ? field.group.includes(groupName) : field.group === groupName;
147
+ }
148
+
76
149
  function syntaxHighlightJson(json: string): string {
77
150
  return json.replace(
78
151
  /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,
@@ -106,7 +179,12 @@
106
179
 
107
180
  // Version history
108
181
  let showVersionHistory = $state(false);
109
- let previewingVersion = $state<{ versionNumber: number; data: Record<string, any>; eventType: string; createdAt?: string } | null>(null);
182
+ let previewingVersion = $state<{
183
+ versionNumber: number;
184
+ data: Record<string, any>;
185
+ eventType: string;
186
+ createdAt?: string;
187
+ } | null>(null);
110
188
  const activePreview = $derived(externalVersionPreview || previewingVersion);
111
189
  const isPreviewingVersion = $derived(!!activePreview);
112
190
 
@@ -136,29 +214,46 @@
136
214
 
137
215
  const savedAgoText = $derived(lastSaved ? `Saved ${timeAgo(lastSaved)}` : null);
138
216
 
139
- // Menu dropdown state
140
- let showDropdown = $state(false);
141
-
142
217
  // Auto-save functionality (every 2 seconds when there are changes)
143
218
  let hasUnsavedChanges = $state(false);
144
219
  let autoSaveTimer: ReturnType<typeof setTimeout> | null = null;
145
220
  let hasValidationErrors = $state(false);
221
+ // Snapshot of the default-initialized data for a brand-new (unsaved) document.
222
+ // Used to detect real user edits without relying on "is the value truthy" heuristics,
223
+ // so unchecking a boolean counts as a change and booleans with initialValue: true
224
+ // don't auto-create the doc on mount.
225
+ let initialDocumentData: Record<string, any> | null = null;
226
+
227
+ // Share save state with nested components (e.g. ObjectModal) via context
228
+ setSaveStateContext({
229
+ get saving() {
230
+ return saving;
231
+ },
232
+ get hasUnsavedChanges() {
233
+ return hasUnsavedChanges;
234
+ },
235
+ get savedAgoText() {
236
+ return savedAgoText;
237
+ }
238
+ });
146
239
 
147
240
  let orphanedFields = $state<OrphanedField[]>([]);
148
241
  let showOrphanedFields = $state(false);
149
242
  let schemaFields: SchemaField[] = [];
150
243
 
151
244
  // Track previous document to detect actual switches (not create→edit transitions)
152
- let previousDocumentId = $state<string | null | undefined>(undefined);
153
- let previousDocumentType = $state<string | undefined>(undefined);
245
+ // Plain lets (not $state) only read/written inside the clearing effect; no need for reactivity
246
+ let previousDocumentId: string | null | undefined = undefined;
247
+ let previousDocumentType: string | undefined = undefined;
154
248
  let justCreatedDocument = $state(false); // Flag to skip loadDocumentData after creation
155
249
 
156
250
  // Hash-based state tracking
157
251
  const hasUnpublishedContent = $derived(
158
252
  hasUnpublishedChanges(documentData, fullDocument?._meta?.publishedHash || null)
159
253
  );
254
+ const isUnpublished = $derived(fullDocument?._meta?.status === 'unpublished');
160
255
  const canPublish = $derived(
161
- hasUnpublishedContent && !saving && documentId && !hasValidationErrors
256
+ (hasUnpublishedContent || isUnpublished) && !saving && documentId && !hasValidationErrors
162
257
  );
163
258
 
164
259
  // Get preview title based on schema config
@@ -189,6 +284,7 @@
189
284
  // Clear all state immediately to prevent old data from being auto-saved
190
285
  documentData = {};
191
286
  fullDocument = null;
287
+ initialDocumentData = null;
192
288
  hasUnsavedChanges = false;
193
289
  saveError = null;
194
290
  lastSaved = null;
@@ -293,8 +389,8 @@
293
389
 
294
390
  // With LocalAPI, data is flattened at top level (not in draftData)
295
391
  // Extract all fields except id and _meta
296
- // @ts-expect-error
297
- const { id, _meta, ...data } = response.data;
392
+ const { id: _id, _meta, ...data } = response.data;
393
+ cmsLogger.debug('[Document Editor', '📄 Full response.data:', _id);
298
394
  cmsLogger.debug('[Document Editor]', '📄 Full response.data:', response.data);
299
395
  cmsLogger.debug('[Document Editor]', '📄 Extracted data (after destructuring):', data);
300
396
  cmsLogger.debug('[Document Editor]', '📄 Keys in extracted data:', Object.keys(data));
@@ -365,6 +461,7 @@
365
461
  }
366
462
 
367
463
  documentData = initialData;
464
+ initialDocumentData = structuredClone(initialData);
368
465
  fullDocument = null;
369
466
  hasUnsavedChanges = false;
370
467
  lastSaved = null;
@@ -372,29 +469,21 @@
372
469
  cmsLogger.debug('[Document Editor]', '✅ Document initialized with:', initialData);
373
470
  }
374
471
 
375
- // Check if document has meaningful content (not just empty initialized values)
376
- function hasMeaningfulContent(data: Record<string, any>): boolean {
377
- return Object.values(data).some((value) => {
378
- if (typeof value === 'string') return value.trim() !== '';
379
- if (typeof value === 'boolean') return value !== false; // Assuming false is default
380
- if (Array.isArray(value)) return value.length > 0;
381
- if (typeof value === 'object' && value !== null) return Object.keys(value).length > 0;
382
- return value !== null && value !== undefined && value !== '';
383
- });
384
- }
385
-
386
- // Check if current data differs from last saved draft
472
+ // Check if current data differs from last saved draft (or initial defaults for a new doc)
387
473
  function hasChangesFromSaved(): boolean {
388
- if (!fullDocument) return true; // No saved version = has changes
474
+ const baseline = fullDocument
475
+ ? (() => {
476
+ const { id, _meta, ...savedData } = fullDocument;
477
+ return savedData;
478
+ })()
479
+ : initialDocumentData;
389
480
 
390
- // Extract saved draft data from fullDocument (same as we do in loadDocumentData)
391
- const { id, _meta, ...savedData } = fullDocument;
481
+ if (!baseline) return true;
392
482
 
393
- // Compare current documentData with saved data using stable JSON
394
483
  const currentJson = JSON.stringify(sortObjectForComparison(documentData));
395
- const savedJson = JSON.stringify(sortObjectForComparison(savedData));
484
+ const baselineJson = JSON.stringify(sortObjectForComparison(baseline));
396
485
 
397
- return currentJson !== savedJson;
486
+ return currentJson !== baselineJson;
398
487
  }
399
488
 
400
489
  // Helper to recursively sort object keys for stable comparison
@@ -419,24 +508,18 @@
419
508
 
420
509
  // Watch for changes to trigger auto-save (debounced)
421
510
  $effect(() => {
422
- const hasContent = hasMeaningfulContent(documentData);
423
511
  const hasChanges = hasChangesFromSaved();
424
512
 
425
- // Only set hasUnsavedChanges if we actually have meaningful data AND it differs from saved
426
- if (hasContent && hasChanges) {
427
- hasUnsavedChanges = true;
428
- } else if (!hasChanges) {
429
- // If there are no changes from saved, mark as not having unsaved changes
430
- hasUnsavedChanges = false;
431
- }
513
+ hasUnsavedChanges = hasChanges;
432
514
 
433
515
  if (autoSaveTimer) {
434
516
  clearTimeout(autoSaveTimer);
435
517
  }
436
518
 
437
- // Debounced auto-save - waits for 800ms pause in typing (like Notion/modern apps)
438
- // Only auto-save if there's meaningful content, data has changed, and not in read-only mode
439
- if (hasContent && hasChanges && schema && !isReadOnly) {
519
+ // Debounced auto-save. Comparing against the initial-defaults snapshot (for new
520
+ // docs) or the saved draft (for existing docs) means any real user edit triggers
521
+ // save including unchecking a boolean or clearing a field.
522
+ if (hasChanges && schema && canWriteCurrentDoc) {
440
523
  autoSaveTimer = setTimeout(() => {
441
524
  cmsLogger.debug(
442
525
  '[Document Editor]',
@@ -507,7 +590,6 @@
507
590
  // instead of what the server returned, in case server adds extra metadata
508
591
  if (response?.success && response.data) {
509
592
  cmsLogger.debug('[Document Editor]', 'meta response data:', response.data);
510
- // @ts-expect-error
511
593
  const { id: responseId, _meta } = response.data;
512
594
  // Reconstruct fullDocument using the data we sent (documentData)
513
595
  // plus the id and _meta from the response
@@ -560,10 +642,22 @@
560
642
  if (!documentId || saving) return;
561
643
 
562
644
  // Check for validation errors before publishing (Sanity-style)
563
- await validateAllFields();
564
-
565
- if (hasValidationErrors) {
566
- saveError = 'Cannot publish: Please fix validation errors first';
645
+ const invalid = await validateAllFields();
646
+
647
+ if (invalid.length > 0) {
648
+ // Show up to three field titles in the toast so the user doesn't
649
+ // have to hunt for the red outline — longer lists are summarised.
650
+ const preview = invalid
651
+ .slice(0, 3)
652
+ .map((f) => f.title)
653
+ .join(', ');
654
+ const remainder = invalid.length > 3 ? ` and ${invalid.length - 3} more` : '';
655
+ const detail = invalid.map((f) => `${f.title}: ${f.messages.join(', ')}`).join('\n');
656
+
657
+ saveError = `Cannot publish — fix: ${preview}${remainder}`;
658
+ toast.error(`Fix ${invalid.length} field${invalid.length === 1 ? '' : 's'} to publish`, {
659
+ description: detail
660
+ });
567
661
  return;
568
662
  }
569
663
 
@@ -632,7 +726,13 @@
632
726
  async function unpublishDocument() {
633
727
  if (!documentId || saving) return;
634
728
 
635
- const confirmUnpublish = confirm('Unpublish this document? It will be removed from published queries but the data is preserved.');
729
+ const confirmUnpublish = await confirmDialog({
730
+ title: 'Unpublish this document?',
731
+ description:
732
+ 'It will be removed from published queries, but the data is preserved and you can re-publish anytime.',
733
+ confirmText: 'Unpublish',
734
+ variant: 'destructive'
735
+ });
636
736
  if (!confirmUnpublish) return;
637
737
 
638
738
  saving = true;
@@ -641,9 +741,14 @@
641
741
  try {
642
742
  const response = await documents.unpublish(documentId);
643
743
  if (response.success) {
644
- fullDocument = { ...fullDocument, _meta: { ...fullDocument?._meta, status: 'unpublished' } };
744
+ fullDocument = {
745
+ ...fullDocument,
746
+ _meta: { ...fullDocument?._meta, status: 'unpublished' }
747
+ };
645
748
  toast.success('Document unpublished — you can re-publish anytime');
646
- showDropdown = false;
749
+ if (onUnpublished && documentId) {
750
+ onUnpublished(documentId);
751
+ }
647
752
  } else {
648
753
  throw new Error(response.error || 'Failed to unpublish');
649
754
  }
@@ -655,13 +760,15 @@
655
760
  }
656
761
 
657
762
  // Validate all fields before publishing
658
- async function validateAllFields(): Promise<void> {
763
+ async function validateAllFields(): Promise<
764
+ Array<{ name: string; title: string; messages: string[] }>
765
+ > {
659
766
  if (!schema) {
660
767
  hasValidationErrors = false;
661
- return;
768
+ return [];
662
769
  }
663
770
 
664
- let errorsFound = false;
771
+ const invalid: Array<{ name: string; title: string; messages: string[] }> = [];
665
772
 
666
773
  for (const field of schema.fields) {
667
774
  if (field.validation) {
@@ -670,34 +777,47 @@
670
777
  ? field.validation
671
778
  : [field.validation];
672
779
 
780
+ const messages: string[] = [];
673
781
  for (const validationFn of validationFunctions) {
674
782
  const rule = validationFn(new Rule());
675
783
  const markers = await rule.validate(documentData[field.name], { path: [field.name] });
676
784
 
677
- if (markers.some((m: any) => m.level === 'error')) {
678
- errorsFound = true;
679
- cmsLogger.debug(
680
- `[DocumentEditor] Validation error in field '${field.name}':`,
681
- markers
682
- );
785
+ for (const m of markers) {
786
+ if (m.level === 'error') messages.push(m.message ?? 'Invalid');
683
787
  }
684
788
  }
789
+
790
+ if (messages.length > 0) {
791
+ invalid.push({ name: field.name, title: field.title ?? field.name, messages });
792
+ cmsLogger.debug(
793
+ `[DocumentEditor] Validation error in field '${field.name}':`,
794
+ messages
795
+ );
796
+ }
685
797
  } catch (error) {
686
- errorsFound = true;
687
- toast.error(`Validation failed for field "${field.name}"`);
798
+ invalid.push({
799
+ name: field.name,
800
+ title: field.title ?? field.name,
801
+ messages: ['Validation check threw an error']
802
+ });
803
+ toast.error(`Validation failed for field "${field.title ?? field.name}"`);
688
804
  }
689
805
  }
690
806
  }
691
807
 
692
- hasValidationErrors = errorsFound;
808
+ hasValidationErrors = invalid.length > 0;
809
+ return invalid;
693
810
  }
694
811
 
695
812
  async function deleteDocument() {
696
813
  if (!documentId || saving) return;
697
814
 
698
- const confirmDelete = confirm(
699
- `Are you sure you want to delete this document? This action cannot be undone.`
700
- );
815
+ const confirmDelete = await confirmDialog({
816
+ title: 'Delete this document?',
817
+ description: 'This action cannot be undone.',
818
+ confirmText: 'Delete',
819
+ variant: 'destructive'
820
+ });
701
821
  if (!confirmDelete) return;
702
822
 
703
823
  saving = true;
@@ -779,264 +899,386 @@
779
899
  }
780
900
  </script>
781
901
 
782
- <div class="relative flex h-full flex-col overflow-hidden">
783
- <!-- Header Toolbar (Sanity-style) -->
784
- <div class="border-border bg-background flex h-14 items-center justify-between border-b px-4">
785
- <!-- Left side: Document info and status -->
786
- <div class="flex items-center gap-3 overflow-hidden">
787
- <div class="min-w-0 flex-1">
788
- <h3 class="truncate text-sm font-medium">
789
- {getPreviewTitle()}
790
- </h3>
791
- <div class="flex items-center gap-2">
902
+ <div class="relative flex h-full w-full min-w-0 flex-col overflow-hidden">
903
+ <!-- Hero Header -->
904
+ <div class="bg-background w-full min-w-0 overflow-x-clip px-4 pt-4 pb-5 lg:px-6 lg:pt-5">
905
+ <div class="mx-auto w-full max-w-3xl">
906
+ <!-- Top row: breadcrumb + actions -->
907
+ <div class="mb-4 flex items-center justify-between gap-3">
908
+ <div
909
+ class="text-muted-foreground flex min-w-0 items-center gap-2 text-[11px] font-medium tracking-wider uppercase"
910
+ >
911
+ <span class="whitespace-nowrap">{schema?.title || documentType}</span>
912
+ <span aria-hidden="true">·</span>
913
+ <span class="truncate">{getPreviewTitle()}</span>
914
+ </div>
915
+
916
+ <div class="flex shrink-0 items-center gap-2">
792
917
  {#if saving}
793
- <span class="text-muted-foreground text-xs">Saving...</span>
794
- {:else if savedAgoText}
795
- <span class="text-muted-foreground text-xs">
796
- {savedAgoText}
918
+ <span
919
+ class="text-muted-foreground hidden items-center gap-1.5 text-[10px] font-medium tracking-wider whitespace-nowrap uppercase sm:inline-flex"
920
+ >
921
+ <span class="bg-muted-foreground/60 h-1.5 w-1.5 animate-pulse rounded-full"></span>
922
+ Saving
797
923
  </span>
798
924
  {:else if hasUnsavedChanges}
799
- <span class="text-muted-foreground text-xs">Unsaved changes</span>
800
- {/if}
801
-
802
- <!-- Created by -->
803
- {#if fullDocument?.createdBy}
804
- <span class="text-muted-foreground hidden text-xs sm:inline">
805
- • Created by {typeof fullDocument.createdBy === 'string'
806
- ? fullDocument.createdBy
807
- : fullDocument.createdBy.name || fullDocument.createdBy.email}
925
+ <span
926
+ class="text-muted-foreground hidden items-center gap-1.5 text-[10px] font-medium tracking-wider whitespace-nowrap uppercase sm:inline-flex"
927
+ >
928
+ <span class="bg-muted-foreground/60 h-1.5 w-1.5 rounded-full"></span>
929
+ Unsaved
930
+ </span>
931
+ {:else if savedAgoText}
932
+ <span
933
+ class="text-muted-foreground hidden items-center gap-1.5 text-[10px] font-medium tracking-wider whitespace-nowrap uppercase sm:inline-flex"
934
+ >
935
+ <span class="bg-muted-foreground/60 h-1.5 w-1.5 rounded-full"></span>
936
+ Auto-saved
808
937
  </span>
809
938
  {/if}
810
- </div>
811
- </div>
812
- </div>
813
939
 
814
- <!-- Right side: Perspective toggle, actions, close -->
815
- <div class="flex items-center gap-2">
816
- <!-- Perspective toggle -->
817
- {#if documentId && fullDocument?._meta?.publishedHash}
818
- <div class="flex rounded-md border">
819
- <button
820
- class="px-2.5 py-1 text-xs font-medium transition-colors {perspective === 'draft' ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'}"
821
- onclick={() => switchPerspective('draft')}
822
- >
823
- Draft
824
- </button>
825
- <button
826
- class="px-2.5 py-1 text-xs font-medium transition-colors {perspective === 'published' ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'}"
827
- onclick={() => switchPerspective('published')}
828
- >
829
- Published
830
- </button>
831
- </div>
832
- {/if}
833
-
834
-
835
- <!-- Options dropdown -->
836
- {#if documentId}
837
- <div class="relative">
838
- <Button
839
- variant="ghost"
840
- size="icon"
841
- onclick={() => (showHeaderMenu = !showHeaderMenu)}
842
- class="h-8 w-8"
843
- >
844
- <Ellipsis class="h-4 w-4" />
845
- </Button>
846
- {#if showHeaderMenu}
847
- <div class="bg-background border-border absolute right-0 top-full z-50 mt-1 min-w-[160px] rounded-md border py-1 shadow-lg">
940
+ {#if documentId && fullDocument?._meta?.publishedHash}
941
+ {@const isPublished =
942
+ fullDocument?._meta?.status === 'published' && fullDocument?._meta?.publishedAt}
943
+ {@const isUnpub = fullDocument?._meta?.status === 'unpublished'}
944
+ <div class="flex items-center gap-1.5">
848
945
  <button
849
- onclick={() => {
850
- showHeaderMenu = false;
851
- if (onOpenVersionHistory && documentId) {
852
- onOpenVersionHistory(documentId);
853
- } else {
854
- showVersionHistory = true;
855
- }
856
- }}
857
- class="hover:bg-muted flex w-full items-center gap-2 px-3 py-2 text-left text-sm transition-colors"
946
+ 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 ===
947
+ 'draft'
948
+ ? 'bg-primary/90 text-primary-foreground border-transparent'
949
+ : 'text-muted-foreground hover:bg-muted'}"
950
+ onclick={() => switchPerspective('draft')}
858
951
  >
859
- <History class="h-3.5 w-3.5" /> History
952
+ <span
953
+ class="bg-muted-foreground/60 h-1.5 w-1.5 rounded-full {perspective === 'draft'
954
+ ? 'bg-primary-foreground/60'
955
+ : ''}"
956
+ ></span>
957
+ Draft
860
958
  </button>
861
959
  <button
862
- onclick={() => {
863
- showHeaderMenu = false;
864
- showInspect = true;
865
- }}
866
- class="hover:bg-muted flex w-full items-center gap-2 px-3 py-2 text-left text-sm transition-colors"
960
+ 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 ===
961
+ 'published'
962
+ ? 'bg-primary text-primary-foreground border-transparent'
963
+ : 'text-muted-foreground hover:bg-muted'}"
964
+ onclick={() => switchPerspective('published')}
867
965
  >
868
- <Code class="h-3.5 w-3.5" /> Inspect
966
+ {#if isPublished}
967
+ <span
968
+ class="h-1.5 w-1.5 rounded-full {perspective === 'published'
969
+ ? 'bg-primary-foreground/60'
970
+ : 'bg-green-500'}"
971
+ ></span>
972
+ Published · {timeAgo(new Date(fullDocument._meta.publishedAt))}
973
+ {:else if isUnpub}
974
+ <span
975
+ class="h-1.5 w-1.5 rounded-full {perspective === 'published'
976
+ ? 'bg-primary-foreground/60'
977
+ : 'bg-muted-foreground/60'}"
978
+ ></span>
979
+ Unpublished
980
+ {:else}
981
+ Published
982
+ {/if}
869
983
  </button>
870
984
  </div>
871
- <div
872
- class="fixed inset-0 z-40"
873
- onclick={() => (showHeaderMenu = false)}
874
- ></div>
875
985
  {/if}
876
- </div>
877
- {/if}
878
-
879
- <!-- Close button (X) - hidden on mobile -->
880
- <Button
881
- variant="ghost"
882
- size="icon"
883
- onclick={onBack}
884
- class="hidden h-8 w-8 hover:cursor-pointer lg:flex"
885
- title="Close"
886
- >
887
- <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
888
- <path
889
- stroke-linecap="round"
890
- stroke-linejoin="round"
891
- stroke-width="2"
892
- d="M6 18L18 6M6 6l12 12"
893
- />
894
- </svg>
895
- </Button>
896
- </div>
897
- </div>
898
986
 
899
- <!-- Content Form -->
900
- <div class="flex-1 space-y-4 overflow-auto p-4 lg:space-y-6 lg:p-6">
901
- {#if saveError}
902
- <div class="bg-destructive/10 border-destructive/20 rounded-md border p-3">
903
- <p class="text-destructive text-sm">{saveError}</p>
904
- </div>
905
- {/if}
987
+ {#if documentId}
988
+ <div class="relative">
989
+ <Button
990
+ variant="ghost"
991
+ size="icon"
992
+ onclick={() => (showHeaderMenu = !showHeaderMenu)}
993
+ class="h-8 w-8 cursor-pointer"
994
+ >
995
+ <Ellipsis class="h-4 w-4" />
996
+ </Button>
997
+ {#if showHeaderMenu}
998
+ <div
999
+ class="bg-background border-rule absolute top-full right-0 z-50 mt-1 min-w-[160px] rounded-md border py-1 shadow-lg"
1000
+ >
1001
+ <button
1002
+ onclick={() => {
1003
+ showHeaderMenu = false;
1004
+ if (onOpenVersionHistory && documentId) {
1005
+ onOpenVersionHistory(documentId);
1006
+ } else {
1007
+ showVersionHistory = true;
1008
+ }
1009
+ }}
1010
+ class="hover:bg-muted flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-left text-sm transition-colors"
1011
+ >
1012
+ <History class="h-3.5 w-3.5" /> History
1013
+ </button>
1014
+ <button
1015
+ onclick={() => {
1016
+ showHeaderMenu = false;
1017
+ showInspect = true;
1018
+ }}
1019
+ class="hover:bg-muted flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-left text-sm transition-colors"
1020
+ >
1021
+ <Code class="h-3.5 w-3.5" /> Inspect
1022
+ </button>
1023
+ </div>
1024
+ <div class="fixed inset-0 z-40" onclick={() => (showHeaderMenu = false)}></div>
1025
+ {/if}
1026
+ </div>
1027
+ {/if}
906
1028
 
907
- {#if schemaError}
908
- <div class="bg-destructive/10 border-destructive/20 rounded-md border p-3">
909
- <p class="text-destructive text-sm">Schema Error: {schemaError}</p>
910
- </div>
911
- {:else if schemaLoading}
912
- <div class="p-6 text-center">
913
- <div class="text-muted-foreground text-sm">Loading schema...</div>
914
- </div>
915
- {:else if schema}
916
- <!-- Orphaned Fields Warning -->
917
- {#if showOrphanedFields && orphanedFields.length > 0}
918
- <div class="space-y-3 rounded-md border border-orange-200 bg-orange-50 p-4">
919
- <div class="flex items-center gap-2">
920
- <svg
921
- class="h-5 w-5 text-orange-600"
922
- fill="none"
923
- viewBox="0 0 24 24"
924
- stroke="currentColor"
925
- >
1029
+ <Button
1030
+ variant="ghost"
1031
+ size="icon"
1032
+ onclick={onBack}
1033
+ class="hidden h-8 w-8 hover:cursor-pointer lg:flex"
1034
+ title="Close"
1035
+ >
1036
+ <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
926
1037
  <path
927
1038
  stroke-linecap="round"
928
1039
  stroke-linejoin="round"
929
1040
  stroke-width="2"
930
- 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"
1041
+ d="M6 18L18 6M6 6l12 12"
931
1042
  />
932
1043
  </svg>
933
- <h4 class="text-sm font-medium text-orange-800">
934
- {orphanedFields.length} orphaned field{orphanedFields.length === 1 ? '' : 's'} detected
935
- </h4>
936
- </div>
1044
+ </Button>
1045
+ </div>
1046
+ </div>
937
1047
 
938
- <p class="text-sm text-orange-700">
939
- These fields exist in your document but are no longer defined in the schema:
940
- </p>
1048
+ <!-- Title -->
1049
+ <h1 class="block w-full min-w-0 truncate text-3xl font-semibold tracking-tight">
1050
+ {getPreviewTitle()}
1051
+ </h1>
1052
+
1053
+ <!-- Mobile-only status row: save state + draft pill for narrow viewports -->
1054
+ <div class="mt-3 flex items-center justify-between gap-3 sm:hidden">
1055
+ <div class="flex flex-wrap items-center gap-2">
1056
+ {#if !fullDocument?._meta?.publishedHash && documentId && fullDocument?._meta?.status !== 'unpublished'}
1057
+ <span
1058
+ 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"
1059
+ >
1060
+ <span class="bg-muted-foreground/60 h-1.5 w-1.5 rounded-full"></span>
1061
+ Draft
1062
+ </span>
1063
+ {/if}
1064
+ </div>
941
1065
 
942
- <div class="space-y-2">
943
- {#each orphanedFields as field, index (index)}
944
- <div
945
- class="flex items-center justify-between rounded border border-orange-200 bg-white p-3"
1066
+ {#if saving}
1067
+ <span
1068
+ class="text-muted-foreground inline-flex items-center gap-1.5 text-[10px] font-medium tracking-wider whitespace-nowrap uppercase"
1069
+ >
1070
+ <span class="bg-muted-foreground/60 h-1.5 w-1.5 animate-pulse rounded-full"></span>
1071
+ Saving
1072
+ </span>
1073
+ {:else if hasUnsavedChanges}
1074
+ <span
1075
+ class="text-muted-foreground inline-flex items-center gap-1.5 text-[10px] font-medium tracking-wider whitespace-nowrap uppercase"
1076
+ >
1077
+ <span class="bg-muted-foreground/60 h-1.5 w-1.5 rounded-full"></span>
1078
+ Unsaved
1079
+ </span>
1080
+ {:else if savedAgoText}
1081
+ <span
1082
+ class="text-muted-foreground inline-flex items-center gap-1.5 text-[10px] font-medium tracking-wider whitespace-nowrap uppercase"
1083
+ >
1084
+ <span class="bg-muted-foreground/60 h-1.5 w-1.5 rounded-full"></span>
1085
+ Auto-saved
1086
+ </span>
1087
+ {/if}
1088
+ </div>
1089
+ </div>
1090
+ </div>
1091
+
1092
+ <!-- Content Form -->
1093
+ <div class="flex-1 overflow-auto p-4 lg:p-6">
1094
+ <div class="mx-auto w-full max-w-3xl space-y-4 lg:space-y-6">
1095
+ {#if saveError}
1096
+ <div class="bg-destructive/10 border-destructive/20 rounded-md border p-3">
1097
+ <p class="text-destructive text-sm">{saveError}</p>
1098
+ </div>
1099
+ {/if}
1100
+
1101
+ {#if schemaError}
1102
+ <div class="bg-destructive/10 border-destructive/20 rounded-md border p-3">
1103
+ <p class="text-destructive text-sm">Schema Error: {schemaError}</p>
1104
+ </div>
1105
+ {:else if schemaLoading}
1106
+ <div class="p-6 text-center">
1107
+ <div class="text-muted-foreground text-sm">Loading schema...</div>
1108
+ </div>
1109
+ {:else if schema}
1110
+ <!-- Orphaned Fields Warning -->
1111
+ {#if showOrphanedFields && orphanedFields.length > 0}
1112
+ <div class="space-y-3 rounded-md border border-orange-200 bg-orange-50 p-4">
1113
+ <div class="flex items-center gap-2">
1114
+ <svg
1115
+ class="h-5 w-5 text-orange-600"
1116
+ fill="none"
1117
+ viewBox="0 0 24 24"
1118
+ stroke="currentColor"
946
1119
  >
947
- <div class="flex-1">
948
- <div class="font-mono text-sm font-medium text-orange-800">
949
- {field.path || field.key}
950
- </div>
951
- <div class="mt-1 text-xs text-orange-600">
952
- <code class="rounded bg-orange-100 px-1">{JSON.stringify(field.value)}</code>
1120
+ <path
1121
+ stroke-linecap="round"
1122
+ stroke-linejoin="round"
1123
+ stroke-width="2"
1124
+ 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"
1125
+ />
1126
+ </svg>
1127
+ <h4 class="text-sm font-medium text-orange-800">
1128
+ {orphanedFields.length} orphaned field{orphanedFields.length === 1 ? '' : 's'} detected
1129
+ </h4>
1130
+ </div>
1131
+
1132
+ <p class="text-sm text-orange-700">
1133
+ These fields exist in your document but are no longer defined in the schema:
1134
+ </p>
1135
+
1136
+ <div class="space-y-2">
1137
+ {#each orphanedFields as field, index (index)}
1138
+ <div
1139
+ class="flex items-center justify-between rounded border border-orange-200 bg-white p-3"
1140
+ >
1141
+ <div class="flex-1">
1142
+ <div class="font-mono text-sm font-medium text-orange-800">
1143
+ {field.path || field.key}
1144
+ </div>
1145
+ <div class="mt-1 text-xs text-orange-600">
1146
+ <code class="rounded bg-orange-100 px-1">{JSON.stringify(field.value)}</code>
1147
+ </div>
953
1148
  </div>
1149
+ <Button
1150
+ size="sm"
1151
+ variant="outline"
1152
+ onclick={() => removeOrphanedField(field)}
1153
+ class="ml-3 h-8 border-red-200 px-3 text-red-600 hover:border-red-300 hover:bg-red-50"
1154
+ >
1155
+ Remove
1156
+ </Button>
954
1157
  </div>
955
- <Button
956
- size="sm"
957
- variant="outline"
958
- onclick={() => removeOrphanedField(field)}
959
- class="ml-3 h-8 border-red-200 px-3 text-red-600 hover:border-red-300 hover:bg-red-50"
960
- >
961
- Remove
962
- </Button>
963
- </div>
964
- {/each}
965
- </div>
1158
+ {/each}
1159
+ </div>
966
1160
 
967
- <div class="flex gap-2 border-t border-orange-200 pt-2">
968
- <Button
969
- size="sm"
970
- variant="outline"
971
- onclick={cleanupAllOrphanedFields}
972
- class="border-orange-600 bg-orange-600 text-white hover:bg-orange-700"
973
- >
974
- Remove All
975
- </Button>
976
- <Button
977
- size="sm"
978
- variant="ghost"
979
- onclick={dismissOrphanedFields}
980
- class="text-orange-700 hover:text-orange-800"
981
- >
982
- Dismiss
983
- </Button>
1161
+ <div class="flex gap-2 border-t border-orange-200 pt-2">
1162
+ <Button
1163
+ size="sm"
1164
+ variant="outline"
1165
+ onclick={cleanupAllOrphanedFields}
1166
+ class="border-orange-600 bg-orange-600 text-white hover:bg-orange-700"
1167
+ >
1168
+ Remove All
1169
+ </Button>
1170
+ <Button
1171
+ size="sm"
1172
+ variant="ghost"
1173
+ onclick={dismissOrphanedFields}
1174
+ class="text-orange-700 hover:text-orange-800"
1175
+ >
1176
+ Dismiss
1177
+ </Button>
1178
+ </div>
984
1179
  </div>
985
- </div>
986
- {/if}
1180
+ {/if}
987
1181
 
988
- <!-- Dynamic Schema Fields -->
989
- <svelte:boundary
990
- onerror={(error) => cmsLogger.error('[DocumentEditor]', 'Error in editor content:', error)}
991
- >
992
- {#each schema.fields as field, index (index)}
993
- {@const viewData = isPreviewingVersion && activePreview ? activePreview.data : isViewingPublished && publishedData ? publishedData : documentData}
994
- <SchemaField
995
- {field}
996
- value={viewData[field.name]}
997
- documentData={viewData}
998
- onUpdate={(newValue) => {
999
- if (isViewingPublished) return;
1000
- documentData = { ...documentData, [field.name]: newValue };
1001
- hasUnsavedChanges = true;
1002
- }}
1003
- {onOpenReference}
1004
- schemaType={documentType}
1005
- readonly={isReadOnly || isViewingPublished || isPreviewingVersion}
1006
- organizationId={fullDocument?._meta?.organizationId}
1007
- />
1008
- {/each}
1009
-
1010
- {#snippet failed(error, reset)}
1011
- <div class="border-destructive/30 bg-destructive/5 rounded-md border p-4 text-center">
1012
- <p class="text-destructive font-medium">Document editor encountered an error</p>
1013
- <p class="text-muted-foreground mt-1 text-sm">{error instanceof Error ? error.message : 'Unknown error'}</p>
1182
+ <!-- Field Group Tabs -->
1183
+ {#if schema.groups && schema.groups.length > 0}
1184
+ {@const visibleGroups = schema.groups.filter((g) => !g.hidden)}
1185
+ <div class="mb-4 flex items-center gap-1 overflow-x-auto py-1">
1014
1186
  <button
1015
- class="bg-primary text-primary-foreground mt-3 rounded px-4 py-2 text-sm"
1016
- onclick={reset}
1187
+ type="button"
1188
+ onclick={() => (activeGroup = 'all')}
1189
+ class="cursor-pointer rounded-md px-3 py-1.5 text-sm font-medium whitespace-nowrap transition-colors {activeGroup ===
1190
+ 'all'
1191
+ ? 'bg-muted text-foreground'
1192
+ : 'text-muted-foreground hover:text-foreground'}"
1017
1193
  >
1018
- Reload editor
1194
+ All fields
1019
1195
  </button>
1196
+ {#each visibleGroups as group (group.name)}
1197
+ <button
1198
+ type="button"
1199
+ onclick={() => (activeGroup = group.name)}
1200
+ 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 ===
1201
+ group.name
1202
+ ? 'bg-muted text-foreground'
1203
+ : 'text-muted-foreground hover:text-foreground'}"
1204
+ >
1205
+ {#if group.icon}
1206
+ {@const Icon = group.icon}
1207
+ <Icon class="h-4 w-4" />
1208
+ {/if}
1209
+ {group.title}
1210
+ </button>
1211
+ {/each}
1020
1212
  </div>
1021
- {/snippet}
1022
- </svelte:boundary>
1023
- {:else}
1024
- <div class="border-muted-foreground/30 rounded-md border border-dashed p-4">
1025
- <p class="text-muted-foreground text-center text-sm">
1026
- No schema found for document type: {documentType}
1027
- </p>
1028
- </div>
1029
- {/if}
1213
+ {/if}
1214
+
1215
+ <!-- Dynamic Schema Fields -->
1216
+ <svelte:boundary
1217
+ onerror={(error) =>
1218
+ cmsLogger.error('[DocumentEditor]', 'Error in editor content:', error)}
1219
+ >
1220
+ {#each schema.fields.filter((f) => fieldInGroup(f, activeGroup) && !hiddenFieldNames.has(f.name)) as field (field.name)}
1221
+ {@const viewData =
1222
+ isPreviewingVersion && activePreview
1223
+ ? activePreview.data
1224
+ : isViewingPublished && publishedData
1225
+ ? publishedData
1226
+ : documentData}
1227
+ <SchemaField
1228
+ {field}
1229
+ value={viewData[field.name]}
1230
+ documentData={viewData}
1231
+ onUpdate={(newValue) => {
1232
+ if (isViewingPublished) return;
1233
+ documentData = { ...documentData, [field.name]: newValue };
1234
+ hasUnsavedChanges = true;
1235
+ }}
1236
+ {onOpenReference}
1237
+ schemaType={documentType}
1238
+ readonly={isReadOnly ||
1239
+ isViewingPublished ||
1240
+ isPreviewingVersion ||
1241
+ isFieldReadonly(field.name)}
1242
+ organizationId={fullDocument?._meta?.organizationId}
1243
+ />
1244
+ {/each}
1245
+
1246
+ {#snippet failed(error, reset)}
1247
+ <div class="border-destructive/30 bg-destructive/5 rounded-md border p-4 text-center">
1248
+ <p class="text-destructive font-medium">Document editor encountered an error</p>
1249
+ <p class="text-muted-foreground mt-1 text-sm">
1250
+ {error instanceof Error ? error.message : 'Unknown error'}
1251
+ </p>
1252
+ <button
1253
+ class="bg-primary text-primary-foreground mt-3 rounded px-4 py-2 text-sm"
1254
+ onclick={reset}
1255
+ >
1256
+ Reload editor
1257
+ </button>
1258
+ </div>
1259
+ {/snippet}
1260
+ </svelte:boundary>
1261
+ {:else}
1262
+ <div class="border-muted-foreground/30 rounded-md border border-dashed p-4">
1263
+ <p class="text-muted-foreground text-center text-sm">
1264
+ No schema found for document type: {documentType}
1265
+ </p>
1266
+ </div>
1267
+ {/if}
1268
+ </div>
1030
1269
  </div>
1031
1270
 
1032
1271
  <!-- Sanity-style bottom bar -->
1033
1272
  {#if documentId}
1034
- <div class="border-border bg-background border-t p-4">
1273
+ <div class="border-rule bg-background relative z-50 border-t p-4">
1035
1274
  {#if isPreviewingVersion && activePreview}
1036
1275
  <!-- Version preview footer -->
1037
1276
  <div class="flex items-center justify-between">
1038
1277
  <p class="text-muted-foreground text-sm">
1039
- Revision from {new Date(activePreview.createdAt || Date.now()).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', hour12: true })}
1278
+ Revision from {new Date(activePreview.createdAt || Date.now()).toLocaleString(
1279
+ undefined,
1280
+ { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', hour12: true }
1281
+ )}
1040
1282
  </p>
1041
1283
  <Button
1042
1284
  size="sm"
@@ -1064,6 +1306,9 @@
1064
1306
  perspective = 'draft';
1065
1307
  publishedData = null;
1066
1308
  toast.success('Revision restored');
1309
+ if (onRestored && documentId) {
1310
+ onRestored(documentId);
1311
+ }
1067
1312
  } catch {
1068
1313
  toast.error('Failed to restore revision');
1069
1314
  }
@@ -1079,78 +1324,74 @@
1079
1324
  {#if fullDocument?._meta?.status === 'unpublished'}
1080
1325
  Unpublished
1081
1326
  {:else}
1082
- Published on {fullDocument?._meta?.publishedAt ? new Date(fullDocument._meta.publishedAt).toLocaleString(undefined, { month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit', hour12: true }) : 'Unknown'}
1327
+ Published on {fullDocument?._meta?.publishedAt
1328
+ ? new Date(fullDocument._meta.publishedAt).toLocaleString(undefined, {
1329
+ month: 'short',
1330
+ day: 'numeric',
1331
+ year: 'numeric',
1332
+ hour: 'numeric',
1333
+ minute: '2-digit',
1334
+ hour12: true
1335
+ })
1336
+ : 'Unknown'}
1083
1337
  {/if}
1084
1338
  </p>
1085
1339
  {#if fullDocument?._meta?.status === 'unpublished'}
1086
- <Button
1087
- size="sm"
1088
- onclick={publishDocument}
1089
- disabled={saving}
1090
- >
1091
- Publish
1092
- </Button>
1093
- {:else}
1094
- <Button
1095
- size="sm"
1096
- variant="secondary"
1097
- onclick={unpublishDocument}
1098
- disabled={saving}
1099
- >
1340
+ {#if canPublishDoc}
1341
+ <Button size="sm" onclick={publishDocument} disabled={saving}>Publish</Button>
1342
+ {/if}
1343
+ {:else if canUnpublishDoc}
1344
+ <Button size="sm" variant="secondary" onclick={unpublishDocument} disabled={saving}>
1100
1345
  Unpublish
1101
1346
  </Button>
1102
1347
  {/if}
1103
1348
  </div>
1104
1349
  {:else}
1105
- <div class="flex items-center justify-between">
1106
- <!-- Left: Save status badges -->
1107
- <div class="flex items-center gap-2">
1108
- {#if saving}
1109
- <Badge variant="secondary">Saving...</Badge>
1110
- {:else if publishSuccess && now - publishSuccess.getTime() < 3000}
1111
- <Badge variant="default">Published!</Badge>
1112
- {:else if hasUnsavedChanges}
1113
- <Badge variant="outline">Unsaved</Badge>
1114
- {:else if savedAgoText}
1115
- <Badge variant="secondary">{savedAgoText}</Badge>
1116
- {/if}
1117
- </div>
1350
+ <div class="flex items-center justify-between">
1351
+ <!-- Left: Transient publish confirmation (save/unsaved now shown in header) -->
1352
+ <div class="flex items-center gap-2">
1353
+ {#if publishSuccess && now - publishSuccess.getTime() < 3000}
1354
+ <Badge variant="default">Published!</Badge>
1355
+ {/if}
1356
+ </div>
1118
1357
 
1119
- <!-- Right: Publish button + horizontal three dots menu -->
1120
- <div class="flex items-center gap-2">
1121
- {#if !isReadOnly && !isViewingPublished}
1122
- <Button
1123
- onclick={publishDocument}
1124
- disabled={!canPublish}
1125
- size="sm"
1126
- variant={canPublish ? 'default' : 'secondary'}
1127
- class="cursor-pointer"
1128
- >
1129
- {#if saving}
1130
- Publishing...
1131
- {:else if !hasUnpublishedContent}
1132
- Published
1133
- {:else}
1134
- Publish Changes
1135
- {/if}
1136
- </Button>
1137
- {:else if isReadOnly}
1138
- <Badge variant="secondary" class="text-xs">Read Only</Badge>
1139
- {/if}
1358
+ <!-- Right: Publish button + horizontal three dots menu -->
1359
+ <div class="flex items-center gap-2">
1360
+ {#if canPublishDoc && !isViewingPublished}
1361
+ <Button
1362
+ onclick={publishDocument}
1363
+ disabled={!canPublish}
1364
+ size="sm"
1365
+ variant={canPublish ? 'default' : 'secondary'}
1366
+ class="cursor-pointer"
1367
+ >
1368
+ {#if saving}
1369
+ Publishing...
1370
+ {:else if isUnpublished}
1371
+ Publish
1372
+ {:else if !hasUnpublishedContent}
1373
+ Published
1374
+ {:else}
1375
+ Publish Changes
1376
+ {/if}
1377
+ </Button>
1378
+ {:else if isViewingReadOnly}
1379
+ <Badge variant="secondary" class="text-xs">Read Only</Badge>
1380
+ {/if}
1140
1381
 
1141
- {#if !isReadOnly}
1142
- <Button
1143
- variant="ghost"
1144
- size="icon"
1145
- class="h-8 w-8 text-muted-foreground hover:text-destructive"
1146
- onclick={deleteDocument}
1147
- title="Delete document"
1148
- >
1149
- <Trash2 class="h-4 w-4" />
1150
- </Button>
1151
- {/if}
1382
+ {#if canDelete}
1383
+ <Button
1384
+ variant="ghost"
1385
+ size="icon"
1386
+ class="text-muted-foreground hover:text-destructive h-8 w-8"
1387
+ onclick={deleteDocument}
1388
+ title="Delete document"
1389
+ >
1390
+ <Trash2 class="h-4 w-4" />
1391
+ </Button>
1392
+ {/if}
1393
+ </div>
1152
1394
  </div>
1153
- </div>
1154
1395
  {/if}
1155
1396
  </div>
1156
1397
  {/if}
@@ -1161,18 +1402,29 @@
1161
1402
  <!-- Backdrop -->
1162
1403
  <button
1163
1404
  class="flex-1 bg-black/30"
1164
- onclick={() => { showVersionHistory = false; previewingVersion = null; }}
1405
+ onclick={() => {
1406
+ showVersionHistory = false;
1407
+ previewingVersion = null;
1408
+ }}
1165
1409
  ></button>
1166
1410
  <!-- Panel -->
1167
- <div class="bg-background border-border flex w-80 flex-col border-l shadow-lg">
1168
- <div class="border-border flex items-center justify-between border-b px-4 py-3">
1411
+ <div class="bg-background border-rule flex w-80 flex-col border-l shadow-lg">
1412
+ <div class="border-rule flex items-center justify-between border-b px-4 py-3">
1169
1413
  <h3 class="text-sm font-medium">Version History</h3>
1170
1414
  <button
1171
1415
  class="hover:bg-muted rounded p-1 transition-colors"
1172
- onclick={() => { showVersionHistory = false; previewingVersion = null; }}
1416
+ onclick={() => {
1417
+ showVersionHistory = false;
1418
+ previewingVersion = null;
1419
+ }}
1173
1420
  >
1174
1421
  <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1175
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
1422
+ <path
1423
+ stroke-linecap="round"
1424
+ stroke-linejoin="round"
1425
+ stroke-width="2"
1426
+ d="M6 18L18 6M6 6l12 12"
1427
+ />
1176
1428
  </svg>
1177
1429
  </button>
1178
1430
  </div>
@@ -1186,14 +1438,17 @@
1186
1438
  <div class="divide-y">
1187
1439
  {#each response.data as version}
1188
1440
  <button
1189
- class="w-full space-y-1 px-4 py-3 text-left transition-colors hover:bg-muted {previewingVersion?.versionNumber === version.versionNumber ? 'bg-muted border-l-primary border-l-2' : ''}"
1441
+ class="hover:bg-muted w-full space-y-1 px-4 py-3 text-left transition-colors {previewingVersion?.versionNumber ===
1442
+ version.versionNumber
1443
+ ? 'bg-muted border-l-primary border-l-2'
1444
+ : ''}"
1190
1445
  onclick={async () => {
1191
1446
  try {
1192
1447
  const res = await documents.getVersion(documentId, version.versionNumber);
1193
1448
  if (res.success && res.data) {
1194
1449
  previewingVersion = {
1195
1450
  versionNumber: version.versionNumber,
1196
- data: res.data.data,
1451
+ data: res.data.data ?? {},
1197
1452
  eventType: version.eventType
1198
1453
  };
1199
1454
  }
@@ -1204,12 +1459,19 @@
1204
1459
  >
1205
1460
  <div class="flex items-center gap-2">
1206
1461
  <span class="text-xs font-medium">v{version.versionNumber}</span>
1207
- <Badge variant={version.eventType === 'publish' ? 'default' : version.eventType === 'restore' ? 'outline' : 'secondary'} class="text-[10px]">
1462
+ <Badge
1463
+ variant={version.eventType === 'publish'
1464
+ ? 'default'
1465
+ : version.eventType === 'restore'
1466
+ ? 'outline'
1467
+ : 'secondary'}
1468
+ class="text-[10px]"
1469
+ >
1208
1470
  {version.eventType}
1209
1471
  </Badge>
1210
1472
  </div>
1211
1473
  <p class="text-muted-foreground text-[11px]">
1212
- {new Date(version.createdAt).toLocaleString()}
1474
+ {version.createdAt ? new Date(version.createdAt).toLocaleString() : ''}
1213
1475
  </p>
1214
1476
  </button>
1215
1477
  {/each}
@@ -1232,32 +1494,44 @@
1232
1494
  <!-- Inspect Modal -->
1233
1495
  {#if showInspect}
1234
1496
  <div class="absolute inset-0 z-50 flex items-center justify-center bg-black/50">
1235
- <div class="bg-background border-border mx-4 flex h-[80%] w-full max-w-3xl flex-col rounded-lg border shadow-xl">
1497
+ <div
1498
+ class="bg-background border-rule mx-4 flex h-[80%] w-full max-w-3xl flex-col rounded-lg border shadow-xl"
1499
+ >
1236
1500
  <!-- Modal header -->
1237
1501
  <div class="flex items-center justify-between border-b px-4 py-3">
1238
1502
  <div>
1239
1503
  <h3 class="text-sm font-semibold">Inspecting <em>{getPreviewTitle()}</em></h3>
1240
1504
  </div>
1241
- <button
1505
+ <Button
1506
+ variant="ghost"
1242
1507
  class="hover:bg-muted rounded p-1 transition-colors"
1243
1508
  onclick={() => (showInspect = false)}
1244
1509
  >
1245
1510
  <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1246
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
1511
+ <path
1512
+ stroke-linecap="round"
1513
+ stroke-linejoin="round"
1514
+ stroke-width="2"
1515
+ d="M6 18L18 6M6 6l12 12"
1516
+ />
1247
1517
  </svg>
1248
- </button>
1518
+ </Button>
1249
1519
  </div>
1250
1520
 
1251
1521
  <!-- Tabs -->
1252
- <div class="border-b flex">
1522
+ <div class="flex border-b">
1253
1523
  <button
1254
- class="px-4 py-2 text-sm font-medium transition-colors {inspectTab === 'parsed' ? 'border-b-2 border-primary text-foreground' : 'text-muted-foreground hover:text-foreground'}"
1524
+ class="px-4 py-2 text-sm font-medium transition-colors {inspectTab === 'parsed'
1525
+ ? 'border-primary text-foreground border-b-2'
1526
+ : 'text-muted-foreground hover:text-foreground'}"
1255
1527
  onclick={() => (inspectTab = 'parsed')}
1256
1528
  >
1257
1529
  Parsed
1258
1530
  </button>
1259
1531
  <button
1260
- class="px-4 py-2 text-sm font-medium transition-colors {inspectTab === 'raw' ? 'border-b-2 border-primary text-foreground' : 'text-muted-foreground hover:text-foreground'}"
1532
+ class="px-4 py-2 text-sm font-medium transition-colors {inspectTab === 'raw'
1533
+ ? 'border-primary text-foreground border-b-2'
1534
+ : 'text-muted-foreground hover:text-foreground'}"
1261
1535
  onclick={() => (inspectTab = 'raw')}
1262
1536
  >
1263
1537
  Raw JSON
@@ -1268,7 +1542,7 @@
1268
1542
  <div class="flex-1 overflow-auto p-4 font-mono text-sm">
1269
1543
  {#if inspectTab === 'raw'}
1270
1544
  <pre
1271
- class="whitespace-pre-wrap break-all text-xs select-text"
1545
+ class="text-xs break-all whitespace-pre-wrap select-text"
1272
1546
  tabindex="0"
1273
1547
  onkeydown={(e) => {
1274
1548
  if ((e.metaKey || e.ctrlKey) && e.key === 'a') {
@@ -1280,10 +1554,19 @@
1280
1554
  sel?.removeAllRanges();
1281
1555
  sel?.addRange(range);
1282
1556
  }
1283
- }}
1284
- >{@html syntaxHighlightJson(JSON.stringify({ id: documentId, _meta: fullDocument?._meta, ...documentData }, null, 2))}</pre>
1557
+ }}>{@html syntaxHighlightJson(
1558
+ JSON.stringify(
1559
+ { id: documentId, _meta: fullDocument?._meta, ...documentData },
1560
+ null,
1561
+ 2
1562
+ )
1563
+ )}</pre>
1285
1564
  {:else}
1286
- {@render parsedValue(null, { id: documentId, _meta: fullDocument?._meta, ...documentData }, 0)}
1565
+ {@render parsedValue(
1566
+ null,
1567
+ { id: documentId, _meta: fullDocument?._meta, ...documentData },
1568
+ 0
1569
+ )}
1287
1570
  {/if}
1288
1571
  </div>
1289
1572
  </div>
@@ -1303,12 +1586,17 @@
1303
1586
  {/if}
1304
1587
  {/if}
1305
1588
  {#if Array.isArray(val)}
1306
- <span class="text-muted-foreground">[...] {val.length} {val.length === 1 ? 'item' : 'items'}</span>
1589
+ <span class="text-muted-foreground"
1590
+ >[...] {val.length} {val.length === 1 ? 'item' : 'items'}</span
1591
+ >
1307
1592
  {:else}
1308
- <span class="text-muted-foreground">&#123;...&#125; {Object.keys(val).length} {Object.keys(val).length === 1 ? 'property' : 'properties'}</span>
1593
+ <span class="text-muted-foreground"
1594
+ >&#123;...&#125; {Object.keys(val).length}
1595
+ {Object.keys(val).length === 1 ? 'property' : 'properties'}</span
1596
+ >
1309
1597
  {/if}
1310
1598
  </summary>
1311
- <div class="ml-4 border-l border-border/50 pl-3">
1599
+ <div class="border-rule/50 ml-4 border-l pl-3">
1312
1600
  {#if Array.isArray(val)}
1313
1601
  {#each val as item, i}
1314
1602
  {@render parsedValue(String(i), item, depth + 1)}