@aphexcms/cms-core 2.0.10 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (281) hide show
  1. package/dist/api/api-keys.d.ts +4 -0
  2. package/dist/api/api-keys.d.ts.map +1 -1
  3. package/dist/api/assets.d.ts.map +1 -1
  4. package/dist/api/index.d.ts +1 -0
  5. package/dist/api/index.d.ts.map +1 -1
  6. package/dist/api/index.js +1 -0
  7. package/dist/api/roles.d.ts +22 -0
  8. package/dist/api/roles.d.ts.map +1 -0
  9. package/dist/api/roles.js +26 -0
  10. package/dist/api/schemas/api-keys.d.ts +50 -4
  11. package/dist/api/schemas/api-keys.d.ts.map +1 -1
  12. package/dist/api/schemas/api-keys.js +22 -3
  13. package/dist/api/schemas/organizations.d.ts +4 -22
  14. package/dist/api/schemas/organizations.d.ts.map +1 -1
  15. package/dist/api/schemas/organizations.js +13 -2
  16. package/dist/api/schemas/roles.d.ts +77 -0
  17. package/dist/api/schemas/roles.d.ts.map +1 -0
  18. package/dist/api/schemas/roles.js +42 -0
  19. package/dist/app-augment.d.ts +32 -0
  20. package/dist/app-augment.d.ts.map +1 -0
  21. package/dist/app-augment.js +18 -0
  22. package/dist/auth/auth-hooks.d.ts +2 -1
  23. package/dist/auth/auth-hooks.d.ts.map +1 -1
  24. package/dist/auth/auth-hooks.js +22 -2
  25. package/dist/client/index.d.ts +4 -2
  26. package/dist/client/index.d.ts.map +1 -1
  27. package/dist/client/index.js +6 -2
  28. package/dist/components/AdminApp.svelte +300 -140
  29. package/dist/components/AdminApp.svelte.d.ts +8 -0
  30. package/dist/components/AdminApp.svelte.d.ts.map +1 -1
  31. package/dist/components/admin/DocumentEditor.svelte +633 -350
  32. package/dist/components/admin/DocumentEditor.svelte.d.ts +2 -0
  33. package/dist/components/admin/DocumentEditor.svelte.d.ts.map +1 -1
  34. package/dist/components/admin/DocumentVersionPanel.svelte +46 -20
  35. package/dist/components/admin/DocumentVersionPanel.svelte.d.ts.map +1 -1
  36. package/dist/components/admin/MediaBrowser.svelte +70 -30
  37. package/dist/components/admin/MediaBrowser.svelte.d.ts.map +1 -1
  38. package/dist/components/admin/ObjectModal.svelte +70 -95
  39. package/dist/components/admin/ObjectModal.svelte.d.ts +1 -2
  40. package/dist/components/admin/ObjectModal.svelte.d.ts.map +1 -1
  41. package/dist/components/admin/PermissionsDebug.svelte +119 -0
  42. package/dist/components/admin/PermissionsDebug.svelte.d.ts +4 -0
  43. package/dist/components/admin/PermissionsDebug.svelte.d.ts.map +1 -0
  44. package/dist/components/admin/SchemaField.svelte +3 -1
  45. package/dist/components/admin/SchemaField.svelte.d.ts.map +1 -1
  46. package/dist/components/admin/confirm-dialog/ConfirmDialogHost.svelte +8 -10
  47. package/dist/components/admin/confirm-dialog/ConfirmDialogHost.svelte.d.ts.map +1 -1
  48. package/dist/components/admin/fields/ArrayField.svelte +104 -108
  49. package/dist/components/admin/fields/ArrayField.svelte.d.ts.map +1 -1
  50. package/dist/components/admin/fields/FileField.svelte +22 -17
  51. package/dist/components/admin/fields/FileField.svelte.d.ts.map +1 -1
  52. package/dist/components/admin/fields/ImageField.svelte +21 -15
  53. package/dist/components/admin/fields/ImageField.svelte.d.ts.map +1 -1
  54. package/dist/components/admin/fields/ReferenceField.svelte +142 -112
  55. package/dist/components/admin/fields/ReferenceField.svelte.d.ts.map +1 -1
  56. package/dist/components/admin/fields/TextareaField.svelte +4 -3
  57. package/dist/components/admin/fields/TextareaField.svelte.d.ts +2 -2
  58. package/dist/components/admin/fields/TextareaField.svelte.d.ts.map +1 -1
  59. package/dist/components/layout/Sidebar.svelte +21 -11
  60. package/dist/components/layout/Sidebar.svelte.d.ts.map +1 -1
  61. package/dist/components/layout/sidebar/NavMain.svelte +1 -0
  62. package/dist/db/interfaces/document.d.ts.map +1 -1
  63. package/dist/db/interfaces/index.d.ts +3 -1
  64. package/dist/db/interfaces/index.d.ts.map +1 -1
  65. package/dist/db/interfaces/organization.d.ts +1 -1
  66. package/dist/db/interfaces/organization.d.ts.map +1 -1
  67. package/dist/db/interfaces/role.d.ts +25 -0
  68. package/dist/db/interfaces/role.d.ts.map +1 -0
  69. package/dist/db/interfaces/role.js +1 -0
  70. package/dist/engine.d.ts.map +1 -1
  71. package/dist/engine.js +5 -0
  72. package/dist/field-access.d.ts +30 -0
  73. package/dist/field-access.d.ts.map +1 -0
  74. package/dist/field-access.js +79 -0
  75. package/dist/hooks.d.ts +2 -0
  76. package/dist/hooks.d.ts.map +1 -1
  77. package/dist/hooks.js +19 -13
  78. package/dist/lib/api/api-keys.d.ts +4 -0
  79. package/dist/lib/api/api-keys.d.ts.map +1 -1
  80. package/dist/lib/api/api-keys.js.map +1 -1
  81. package/dist/lib/api/assets.d.ts.map +1 -1
  82. package/dist/lib/api/assets.js.map +1 -1
  83. package/dist/lib/api/index.d.ts +1 -0
  84. package/dist/lib/api/index.d.ts.map +1 -1
  85. package/dist/lib/api/index.js +1 -0
  86. package/dist/lib/api/index.js.map +1 -1
  87. package/dist/lib/api/roles.d.ts +22 -0
  88. package/dist/lib/api/roles.d.ts.map +1 -0
  89. package/dist/lib/api/roles.js +27 -0
  90. package/dist/lib/api/roles.js.map +1 -0
  91. package/dist/lib/api/schemas/api-keys.d.ts +50 -4
  92. package/dist/lib/api/schemas/api-keys.d.ts.map +1 -1
  93. package/dist/lib/api/schemas/api-keys.js +22 -3
  94. package/dist/lib/api/schemas/api-keys.js.map +1 -1
  95. package/dist/lib/api/schemas/organizations.d.ts +4 -22
  96. package/dist/lib/api/schemas/organizations.d.ts.map +1 -1
  97. package/dist/lib/api/schemas/organizations.js +13 -2
  98. package/dist/lib/api/schemas/organizations.js.map +1 -1
  99. package/dist/lib/api/schemas/roles.d.ts +77 -0
  100. package/dist/lib/api/schemas/roles.d.ts.map +1 -0
  101. package/dist/lib/api/schemas/roles.js +43 -0
  102. package/dist/lib/api/schemas/roles.js.map +1 -0
  103. package/dist/lib/app-augment.d.ts +32 -0
  104. package/dist/lib/app-augment.d.ts.map +1 -0
  105. package/dist/lib/app-augment.js +19 -0
  106. package/dist/lib/app-augment.js.map +1 -0
  107. package/dist/lib/auth/auth-hooks.d.ts +2 -1
  108. package/dist/lib/auth/auth-hooks.d.ts.map +1 -1
  109. package/dist/lib/auth/auth-hooks.js +22 -2
  110. package/dist/lib/auth/auth-hooks.js.map +1 -1
  111. package/dist/lib/client/index.d.ts +4 -2
  112. package/dist/lib/client/index.d.ts.map +1 -1
  113. package/dist/lib/client/index.js +6 -2
  114. package/dist/lib/client/index.js.map +1 -1
  115. package/dist/lib/db/interfaces/document.d.ts.map +1 -1
  116. package/dist/lib/db/interfaces/index.d.ts +3 -1
  117. package/dist/lib/db/interfaces/index.d.ts.map +1 -1
  118. package/dist/lib/db/interfaces/organization.d.ts +1 -1
  119. package/dist/lib/db/interfaces/organization.d.ts.map +1 -1
  120. package/dist/lib/db/interfaces/role.d.ts +25 -0
  121. package/dist/lib/db/interfaces/role.d.ts.map +1 -0
  122. package/dist/lib/db/interfaces/role.js +2 -0
  123. package/dist/lib/db/interfaces/role.js.map +1 -0
  124. package/dist/lib/engine.d.ts.map +1 -1
  125. package/dist/lib/engine.js +5 -0
  126. package/dist/lib/engine.js.map +1 -1
  127. package/dist/lib/field-access.d.ts +30 -0
  128. package/dist/lib/field-access.d.ts.map +1 -0
  129. package/dist/lib/field-access.js +80 -0
  130. package/dist/lib/field-access.js.map +1 -0
  131. package/dist/lib/hooks.d.ts +2 -0
  132. package/dist/lib/hooks.d.ts.map +1 -1
  133. package/dist/lib/hooks.js +19 -13
  134. package/dist/lib/hooks.js.map +1 -1
  135. package/dist/lib/local-api/collection-api.d.ts +2 -0
  136. package/dist/lib/local-api/collection-api.d.ts.map +1 -1
  137. package/dist/lib/local-api/collection-api.js +98 -30
  138. package/dist/lib/local-api/collection-api.js.map +1 -1
  139. package/dist/lib/local-api/index.d.ts.map +1 -1
  140. package/dist/lib/local-api/index.js.map +1 -1
  141. package/dist/lib/local-api/permissions.d.ts +26 -26
  142. package/dist/lib/local-api/permissions.d.ts.map +1 -1
  143. package/dist/lib/local-api/permissions.js +122 -80
  144. package/dist/lib/local-api/permissions.js.map +1 -1
  145. package/dist/lib/permissions-context.svelte.d.ts +44 -0
  146. package/dist/lib/permissions-context.svelte.d.ts.map +1 -0
  147. package/dist/lib/permissions-context.svelte.js +80 -0
  148. package/dist/lib/permissions-context.svelte.js.map +1 -0
  149. package/dist/lib/routes/assets.d.ts.map +1 -1
  150. package/dist/lib/routes/assets.js.map +1 -1
  151. package/dist/lib/routes/document-versions.d.ts.map +1 -1
  152. package/dist/lib/routes/document-versions.js.map +1 -1
  153. package/dist/lib/routes/organizations-by-id.d.ts.map +1 -1
  154. package/dist/lib/routes/organizations-by-id.js +6 -3
  155. package/dist/lib/routes/organizations-by-id.js.map +1 -1
  156. package/dist/lib/routes/organizations-invitations.d.ts.map +1 -1
  157. package/dist/lib/routes/organizations-invitations.js +17 -7
  158. package/dist/lib/routes/organizations-invitations.js.map +1 -1
  159. package/dist/lib/routes/organizations-members.d.ts.map +1 -1
  160. package/dist/lib/routes/organizations-members.js +15 -7
  161. package/dist/lib/routes/organizations-members.js.map +1 -1
  162. package/dist/lib/routes/organizations.d.ts.map +1 -1
  163. package/dist/lib/routes/organizations.js +3 -0
  164. package/dist/lib/routes/organizations.js.map +1 -1
  165. package/dist/lib/routes/roles-by-name.d.ts +4 -0
  166. package/dist/lib/routes/roles-by-name.d.ts.map +1 -0
  167. package/dist/lib/routes/roles-by-name.js +112 -0
  168. package/dist/lib/routes/roles-by-name.js.map +1 -0
  169. package/dist/lib/routes/roles.d.ts +4 -0
  170. package/dist/lib/routes/roles.d.ts.map +1 -0
  171. package/dist/lib/routes/roles.js +91 -0
  172. package/dist/lib/routes/roles.js.map +1 -0
  173. package/dist/lib/routes-exports.d.ts +2 -0
  174. package/dist/lib/routes-exports.d.ts.map +1 -1
  175. package/dist/lib/routes-exports.js +3 -0
  176. package/dist/lib/routes-exports.js.map +1 -1
  177. package/dist/lib/save-state-context.svelte.d.ts +8 -0
  178. package/dist/lib/save-state-context.svelte.d.ts.map +1 -0
  179. package/dist/lib/save-state-context.svelte.js +9 -0
  180. package/dist/lib/save-state-context.svelte.js.map +1 -0
  181. package/dist/lib/schema-utils/validator.d.ts.map +1 -1
  182. package/dist/lib/schema-utils/validator.js +14 -0
  183. package/dist/lib/schema-utils/validator.js.map +1 -1
  184. package/dist/lib/server/index.d.ts +1 -0
  185. package/dist/lib/server/index.d.ts.map +1 -1
  186. package/dist/lib/server/index.js +1 -0
  187. package/dist/lib/server/index.js.map +1 -1
  188. package/dist/lib/services/index.d.ts +1 -0
  189. package/dist/lib/services/index.d.ts.map +1 -1
  190. package/dist/lib/services/index.js +1 -0
  191. package/dist/lib/services/index.js.map +1 -1
  192. package/dist/lib/services/roles-service.d.ts +35 -0
  193. package/dist/lib/services/roles-service.d.ts.map +1 -0
  194. package/dist/lib/services/roles-service.js +91 -0
  195. package/dist/lib/services/roles-service.js.map +1 -0
  196. package/dist/lib/services/version-service.d.ts.map +1 -1
  197. package/dist/lib/services/version-service.js.map +1 -1
  198. package/dist/lib/types/auth.d.ts +43 -9
  199. package/dist/lib/types/auth.d.ts.map +1 -1
  200. package/dist/lib/types/auth.js +31 -26
  201. package/dist/lib/types/auth.js.map +1 -1
  202. package/dist/lib/types/capabilities.d.ts +104 -0
  203. package/dist/lib/types/capabilities.d.ts.map +1 -0
  204. package/dist/lib/types/capabilities.js +217 -0
  205. package/dist/lib/types/capabilities.js.map +1 -0
  206. package/dist/lib/types/index.d.ts +1 -0
  207. package/dist/lib/types/index.d.ts.map +1 -1
  208. package/dist/lib/types/index.js +1 -0
  209. package/dist/lib/types/index.js.map +1 -1
  210. package/dist/lib/types/organization.d.ts +13 -4
  211. package/dist/lib/types/organization.d.ts.map +1 -1
  212. package/dist/lib/types/schemas.d.ts +71 -0
  213. package/dist/lib/types/schemas.d.ts.map +1 -1
  214. package/dist/lib/types/sidebar.d.ts +1 -1
  215. package/dist/lib/types/sidebar.d.ts.map +1 -1
  216. package/dist/lib/utils/mime-detect.d.ts.map +1 -1
  217. package/dist/lib/utils/mime-detect.js +28 -6
  218. package/dist/lib/utils/mime-detect.js.map +1 -1
  219. package/dist/local-api/collection-api.d.ts +2 -0
  220. package/dist/local-api/collection-api.d.ts.map +1 -1
  221. package/dist/local-api/collection-api.js +98 -30
  222. package/dist/local-api/index.d.ts.map +1 -1
  223. package/dist/local-api/permissions.d.ts +26 -26
  224. package/dist/local-api/permissions.d.ts.map +1 -1
  225. package/dist/local-api/permissions.js +122 -80
  226. package/dist/permissions-context.svelte.d.ts +44 -0
  227. package/dist/permissions-context.svelte.d.ts.map +1 -0
  228. package/dist/permissions-context.svelte.js +79 -0
  229. package/dist/plugins/README.md +20 -20
  230. package/dist/routes/assets.d.ts.map +1 -1
  231. package/dist/routes/document-versions.d.ts.map +1 -1
  232. package/dist/routes/organizations-by-id.d.ts.map +1 -1
  233. package/dist/routes/organizations-by-id.js +6 -3
  234. package/dist/routes/organizations-invitations.d.ts.map +1 -1
  235. package/dist/routes/organizations-invitations.js +17 -7
  236. package/dist/routes/organizations-members.d.ts.map +1 -1
  237. package/dist/routes/organizations-members.js +15 -7
  238. package/dist/routes/organizations.d.ts.map +1 -1
  239. package/dist/routes/organizations.js +3 -0
  240. package/dist/routes/roles-by-name.d.ts +4 -0
  241. package/dist/routes/roles-by-name.d.ts.map +1 -0
  242. package/dist/routes/roles-by-name.js +111 -0
  243. package/dist/routes/roles.d.ts +4 -0
  244. package/dist/routes/roles.d.ts.map +1 -0
  245. package/dist/routes/roles.js +90 -0
  246. package/dist/routes-exports.d.ts +2 -0
  247. package/dist/routes-exports.d.ts.map +1 -1
  248. package/dist/routes-exports.js +3 -0
  249. package/dist/save-state-context.svelte.d.ts +8 -0
  250. package/dist/save-state-context.svelte.d.ts.map +1 -0
  251. package/dist/save-state-context.svelte.js +8 -0
  252. package/dist/schema-utils/validator.d.ts.map +1 -1
  253. package/dist/schema-utils/validator.js +14 -0
  254. package/dist/server/index.d.ts +1 -0
  255. package/dist/server/index.d.ts.map +1 -1
  256. package/dist/server/index.js +1 -0
  257. package/dist/services/index.d.ts +1 -0
  258. package/dist/services/index.d.ts.map +1 -1
  259. package/dist/services/index.js +1 -0
  260. package/dist/services/roles-service.d.ts +35 -0
  261. package/dist/services/roles-service.d.ts.map +1 -0
  262. package/dist/services/roles-service.js +90 -0
  263. package/dist/services/version-service.d.ts.map +1 -1
  264. package/dist/types/auth.d.ts +43 -9
  265. package/dist/types/auth.d.ts.map +1 -1
  266. package/dist/types/auth.js +31 -26
  267. package/dist/types/capabilities.d.ts +104 -0
  268. package/dist/types/capabilities.d.ts.map +1 -0
  269. package/dist/types/capabilities.js +216 -0
  270. package/dist/types/index.d.ts +1 -0
  271. package/dist/types/index.d.ts.map +1 -1
  272. package/dist/types/index.js +1 -0
  273. package/dist/types/organization.d.ts +13 -4
  274. package/dist/types/organization.d.ts.map +1 -1
  275. package/dist/types/schemas.d.ts +71 -0
  276. package/dist/types/schemas.d.ts.map +1 -1
  277. package/dist/types/sidebar.d.ts +1 -1
  278. package/dist/types/sidebar.d.ts.map +1 -1
  279. package/dist/utils/mime-detect.d.ts.map +1 -1
  280. package/dist/utils/mime-detect.js +28 -6
  281. package/package.json +6 -2
@@ -10,6 +10,8 @@
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
16
  import { cmsLogger } from '../../utils/logger';
15
17
  import { toast } from 'svelte-sonner';
@@ -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
 
@@ -140,22 +218,42 @@
140
218
  let hasUnsavedChanges = $state(false);
141
219
  let autoSaveTimer: ReturnType<typeof setTimeout> | null = null;
142
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
+ });
143
239
 
144
240
  let orphanedFields = $state<OrphanedField[]>([]);
145
241
  let showOrphanedFields = $state(false);
146
242
  let schemaFields: SchemaField[] = [];
147
243
 
148
244
  // Track previous document to detect actual switches (not create→edit transitions)
149
- let previousDocumentId = $state<string | null | undefined>(undefined);
150
- 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;
151
248
  let justCreatedDocument = $state(false); // Flag to skip loadDocumentData after creation
152
249
 
153
250
  // Hash-based state tracking
154
251
  const hasUnpublishedContent = $derived(
155
252
  hasUnpublishedChanges(documentData, fullDocument?._meta?.publishedHash || null)
156
253
  );
254
+ const isUnpublished = $derived(fullDocument?._meta?.status === 'unpublished');
157
255
  const canPublish = $derived(
158
- hasUnpublishedContent && !saving && documentId && !hasValidationErrors
256
+ (hasUnpublishedContent || isUnpublished) && !saving && documentId && !hasValidationErrors
159
257
  );
160
258
 
161
259
  // Get preview title based on schema config
@@ -186,6 +284,7 @@
186
284
  // Clear all state immediately to prevent old data from being auto-saved
187
285
  documentData = {};
188
286
  fullDocument = null;
287
+ initialDocumentData = null;
189
288
  hasUnsavedChanges = false;
190
289
  saveError = null;
191
290
  lastSaved = null;
@@ -291,7 +390,7 @@
291
390
  // With LocalAPI, data is flattened at top level (not in draftData)
292
391
  // Extract all fields except id and _meta
293
392
  const { id: _id, _meta, ...data } = response.data;
294
- cmsLogger.debug('[Document Editor', '📄 Full response.data:', _id)
393
+ cmsLogger.debug('[Document Editor', '📄 Full response.data:', _id);
295
394
  cmsLogger.debug('[Document Editor]', '📄 Full response.data:', response.data);
296
395
  cmsLogger.debug('[Document Editor]', '📄 Extracted data (after destructuring):', data);
297
396
  cmsLogger.debug('[Document Editor]', '📄 Keys in extracted data:', Object.keys(data));
@@ -362,6 +461,7 @@
362
461
  }
363
462
 
364
463
  documentData = initialData;
464
+ initialDocumentData = structuredClone(initialData);
365
465
  fullDocument = null;
366
466
  hasUnsavedChanges = false;
367
467
  lastSaved = null;
@@ -369,29 +469,21 @@
369
469
  cmsLogger.debug('[Document Editor]', '✅ Document initialized with:', initialData);
370
470
  }
371
471
 
372
- // Check if document has meaningful content (not just empty initialized values)
373
- function hasMeaningfulContent(data: Record<string, any>): boolean {
374
- return Object.values(data).some((value) => {
375
- if (typeof value === 'string') return value.trim() !== '';
376
- if (typeof value === 'boolean') return value !== false; // Assuming false is default
377
- if (Array.isArray(value)) return value.length > 0;
378
- if (typeof value === 'object' && value !== null) return Object.keys(value).length > 0;
379
- return value !== null && value !== undefined && value !== '';
380
- });
381
- }
382
-
383
- // 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)
384
473
  function hasChangesFromSaved(): boolean {
385
- 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;
386
480
 
387
- // Extract saved draft data from fullDocument (same as we do in loadDocumentData)
388
- const { id, _meta, ...savedData } = fullDocument;
481
+ if (!baseline) return true;
389
482
 
390
- // Compare current documentData with saved data using stable JSON
391
483
  const currentJson = JSON.stringify(sortObjectForComparison(documentData));
392
- const savedJson = JSON.stringify(sortObjectForComparison(savedData));
484
+ const baselineJson = JSON.stringify(sortObjectForComparison(baseline));
393
485
 
394
- return currentJson !== savedJson;
486
+ return currentJson !== baselineJson;
395
487
  }
396
488
 
397
489
  // Helper to recursively sort object keys for stable comparison
@@ -416,24 +508,18 @@
416
508
 
417
509
  // Watch for changes to trigger auto-save (debounced)
418
510
  $effect(() => {
419
- const hasContent = hasMeaningfulContent(documentData);
420
511
  const hasChanges = hasChangesFromSaved();
421
512
 
422
- // Only set hasUnsavedChanges if we actually have meaningful data AND it differs from saved
423
- if (hasContent && hasChanges) {
424
- hasUnsavedChanges = true;
425
- } else if (!hasChanges) {
426
- // If there are no changes from saved, mark as not having unsaved changes
427
- hasUnsavedChanges = false;
428
- }
513
+ hasUnsavedChanges = hasChanges;
429
514
 
430
515
  if (autoSaveTimer) {
431
516
  clearTimeout(autoSaveTimer);
432
517
  }
433
518
 
434
- // Debounced auto-save - waits for 800ms pause in typing (like Notion/modern apps)
435
- // Only auto-save if there's meaningful content, data has changed, and not in read-only mode
436
- 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) {
437
523
  autoSaveTimer = setTimeout(() => {
438
524
  cmsLogger.debug(
439
525
  '[Document Editor]',
@@ -556,10 +642,22 @@
556
642
  if (!documentId || saving) return;
557
643
 
558
644
  // Check for validation errors before publishing (Sanity-style)
559
- await validateAllFields();
560
-
561
- if (hasValidationErrors) {
562
- 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
+ });
563
661
  return;
564
662
  }
565
663
 
@@ -643,8 +741,14 @@
643
741
  try {
644
742
  const response = await documents.unpublish(documentId);
645
743
  if (response.success) {
646
- fullDocument = { ...fullDocument, _meta: { ...fullDocument?._meta, status: 'unpublished' } };
744
+ fullDocument = {
745
+ ...fullDocument,
746
+ _meta: { ...fullDocument?._meta, status: 'unpublished' }
747
+ };
647
748
  toast.success('Document unpublished — you can re-publish anytime');
749
+ if (onUnpublished && documentId) {
750
+ onUnpublished(documentId);
751
+ }
648
752
  } else {
649
753
  throw new Error(response.error || 'Failed to unpublish');
650
754
  }
@@ -656,13 +760,15 @@
656
760
  }
657
761
 
658
762
  // Validate all fields before publishing
659
- async function validateAllFields(): Promise<void> {
763
+ async function validateAllFields(): Promise<
764
+ Array<{ name: string; title: string; messages: string[] }>
765
+ > {
660
766
  if (!schema) {
661
767
  hasValidationErrors = false;
662
- return;
768
+ return [];
663
769
  }
664
770
 
665
- let errorsFound = false;
771
+ const invalid: Array<{ name: string; title: string; messages: string[] }> = [];
666
772
 
667
773
  for (const field of schema.fields) {
668
774
  if (field.validation) {
@@ -671,26 +777,36 @@
671
777
  ? field.validation
672
778
  : [field.validation];
673
779
 
780
+ const messages: string[] = [];
674
781
  for (const validationFn of validationFunctions) {
675
782
  const rule = validationFn(new Rule());
676
783
  const markers = await rule.validate(documentData[field.name], { path: [field.name] });
677
784
 
678
- if (markers.some((m: any) => m.level === 'error')) {
679
- errorsFound = true;
680
- cmsLogger.debug(
681
- `[DocumentEditor] Validation error in field '${field.name}':`,
682
- markers
683
- );
785
+ for (const m of markers) {
786
+ if (m.level === 'error') messages.push(m.message ?? 'Invalid');
684
787
  }
685
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
+ }
686
797
  } catch (error) {
687
- errorsFound = true;
688
- 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}"`);
689
804
  }
690
805
  }
691
806
  }
692
807
 
693
- hasValidationErrors = errorsFound;
808
+ hasValidationErrors = invalid.length > 0;
809
+ return invalid;
694
810
  }
695
811
 
696
812
  async function deleteDocument() {
@@ -783,264 +899,386 @@
783
899
  }
784
900
  </script>
785
901
 
786
- <div class="relative flex h-full flex-col overflow-hidden">
787
- <!-- Header Toolbar (Sanity-style) -->
788
- <div class="border-border bg-background flex h-14 items-center justify-between border-b px-4">
789
- <!-- Left side: Document info and status -->
790
- <div class="flex items-center gap-3 overflow-hidden">
791
- <div class="min-w-0 flex-1">
792
- <h3 class="truncate text-sm font-medium">
793
- {getPreviewTitle()}
794
- </h3>
795
- <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">
796
917
  {#if saving}
797
- <span class="text-muted-foreground text-xs">Saving...</span>
798
- {:else if savedAgoText}
799
- <span class="text-muted-foreground text-xs">
800
- {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
801
923
  </span>
802
924
  {:else if hasUnsavedChanges}
803
- <span class="text-muted-foreground text-xs">Unsaved changes</span>
804
- {/if}
805
-
806
- <!-- Created by -->
807
- {#if fullDocument?.createdBy}
808
- <span class="text-muted-foreground hidden text-xs sm:inline">
809
- • Created by {typeof fullDocument.createdBy === 'string'
810
- ? (fullDocument.createdBy.startsWith('apikey:') ? 'API Key' : fullDocument.createdBy)
811
- : 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
812
937
  </span>
813
938
  {/if}
814
- </div>
815
- </div>
816
- </div>
817
-
818
- <!-- Right side: Perspective toggle, actions, close -->
819
- <div class="flex items-center gap-2">
820
- <!-- Perspective toggle -->
821
- {#if documentId && fullDocument?._meta?.publishedHash}
822
- <div class="flex rounded-md border">
823
- <button
824
- class="px-2.5 py-1 text-xs font-medium transition-colors {perspective === 'draft' ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'}"
825
- onclick={() => switchPerspective('draft')}
826
- >
827
- Draft
828
- </button>
829
- <button
830
- class="px-2.5 py-1 text-xs font-medium transition-colors {perspective === 'published' ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'}"
831
- onclick={() => switchPerspective('published')}
832
- >
833
- Published
834
- </button>
835
- </div>
836
- {/if}
837
-
838
939
 
839
- <!-- Options dropdown -->
840
- {#if documentId}
841
- <div class="relative">
842
- <Button
843
- variant="ghost"
844
- size="icon"
845
- onclick={() => (showHeaderMenu = !showHeaderMenu)}
846
- class="h-8 w-8"
847
- >
848
- <Ellipsis class="h-4 w-4" />
849
- </Button>
850
- {#if showHeaderMenu}
851
- <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">
852
945
  <button
853
- onclick={() => {
854
- showHeaderMenu = false;
855
- if (onOpenVersionHistory && documentId) {
856
- onOpenVersionHistory(documentId);
857
- } else {
858
- showVersionHistory = true;
859
- }
860
- }}
861
- 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')}
862
951
  >
863
- <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
864
958
  </button>
865
959
  <button
866
- onclick={() => {
867
- showHeaderMenu = false;
868
- showInspect = true;
869
- }}
870
- 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')}
871
965
  >
872
- <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}
873
983
  </button>
874
984
  </div>
875
- <div
876
- class="fixed inset-0 z-40"
877
- onclick={() => (showHeaderMenu = false)}
878
- ></div>
879
985
  {/if}
880
- </div>
881
- {/if}
882
-
883
- <!-- Close button (X) - hidden on mobile -->
884
- <Button
885
- variant="ghost"
886
- size="icon"
887
- onclick={onBack}
888
- class="hidden h-8 w-8 hover:cursor-pointer lg:flex"
889
- title="Close"
890
- >
891
- <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
892
- <path
893
- stroke-linecap="round"
894
- stroke-linejoin="round"
895
- stroke-width="2"
896
- d="M6 18L18 6M6 6l12 12"
897
- />
898
- </svg>
899
- </Button>
900
- </div>
901
- </div>
902
986
 
903
- <!-- Content Form -->
904
- <div class="flex-1 space-y-4 overflow-auto p-4 lg:space-y-6 lg:p-6">
905
- {#if saveError}
906
- <div class="bg-destructive/10 border-destructive/20 rounded-md border p-3">
907
- <p class="text-destructive text-sm">{saveError}</p>
908
- </div>
909
- {/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}
910
1028
 
911
- {#if schemaError}
912
- <div class="bg-destructive/10 border-destructive/20 rounded-md border p-3">
913
- <p class="text-destructive text-sm">Schema Error: {schemaError}</p>
914
- </div>
915
- {:else if schemaLoading}
916
- <div class="p-6 text-center">
917
- <div class="text-muted-foreground text-sm">Loading schema...</div>
918
- </div>
919
- {:else if schema}
920
- <!-- Orphaned Fields Warning -->
921
- {#if showOrphanedFields && orphanedFields.length > 0}
922
- <div class="space-y-3 rounded-md border border-orange-200 bg-orange-50 p-4">
923
- <div class="flex items-center gap-2">
924
- <svg
925
- class="h-5 w-5 text-orange-600"
926
- fill="none"
927
- viewBox="0 0 24 24"
928
- stroke="currentColor"
929
- >
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">
930
1037
  <path
931
1038
  stroke-linecap="round"
932
1039
  stroke-linejoin="round"
933
1040
  stroke-width="2"
934
- 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"
935
1042
  />
936
1043
  </svg>
937
- <h4 class="text-sm font-medium text-orange-800">
938
- {orphanedFields.length} orphaned field{orphanedFields.length === 1 ? '' : 's'} detected
939
- </h4>
940
- </div>
1044
+ </Button>
1045
+ </div>
1046
+ </div>
941
1047
 
942
- <p class="text-sm text-orange-700">
943
- These fields exist in your document but are no longer defined in the schema:
944
- </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>
945
1065
 
946
- <div class="space-y-2">
947
- {#each orphanedFields as field, index (index)}
948
- <div
949
- 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"
950
1119
  >
951
- <div class="flex-1">
952
- <div class="font-mono text-sm font-medium text-orange-800">
953
- {field.path || field.key}
954
- </div>
955
- <div class="mt-1 text-xs text-orange-600">
956
- <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>
957
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>
958
1157
  </div>
959
- <Button
960
- size="sm"
961
- variant="outline"
962
- onclick={() => removeOrphanedField(field)}
963
- class="ml-3 h-8 border-red-200 px-3 text-red-600 hover:border-red-300 hover:bg-red-50"
964
- >
965
- Remove
966
- </Button>
967
- </div>
968
- {/each}
969
- </div>
1158
+ {/each}
1159
+ </div>
970
1160
 
971
- <div class="flex gap-2 border-t border-orange-200 pt-2">
972
- <Button
973
- size="sm"
974
- variant="outline"
975
- onclick={cleanupAllOrphanedFields}
976
- class="border-orange-600 bg-orange-600 text-white hover:bg-orange-700"
977
- >
978
- Remove All
979
- </Button>
980
- <Button
981
- size="sm"
982
- variant="ghost"
983
- onclick={dismissOrphanedFields}
984
- class="text-orange-700 hover:text-orange-800"
985
- >
986
- Dismiss
987
- </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>
988
1179
  </div>
989
- </div>
990
- {/if}
1180
+ {/if}
991
1181
 
992
- <!-- Dynamic Schema Fields -->
993
- <svelte:boundary
994
- onerror={(error) => cmsLogger.error('[DocumentEditor]', 'Error in editor content:', error)}
995
- >
996
- {#each schema.fields as field, index (index)}
997
- {@const viewData = isPreviewingVersion && activePreview ? activePreview.data : isViewingPublished && publishedData ? publishedData : documentData}
998
- <SchemaField
999
- {field}
1000
- value={viewData[field.name]}
1001
- documentData={viewData}
1002
- onUpdate={(newValue) => {
1003
- if (isViewingPublished) return;
1004
- documentData = { ...documentData, [field.name]: newValue };
1005
- hasUnsavedChanges = true;
1006
- }}
1007
- {onOpenReference}
1008
- schemaType={documentType}
1009
- readonly={isReadOnly || isViewingPublished || isPreviewingVersion}
1010
- organizationId={fullDocument?._meta?.organizationId}
1011
- />
1012
- {/each}
1013
-
1014
- {#snippet failed(error, reset)}
1015
- <div class="border-destructive/30 bg-destructive/5 rounded-md border p-4 text-center">
1016
- <p class="text-destructive font-medium">Document editor encountered an error</p>
1017
- <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">
1018
1186
  <button
1019
- class="bg-primary text-primary-foreground mt-3 rounded px-4 py-2 text-sm"
1020
- 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'}"
1021
1193
  >
1022
- Reload editor
1194
+ All fields
1023
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}
1024
1212
  </div>
1025
- {/snippet}
1026
- </svelte:boundary>
1027
- {:else}
1028
- <div class="border-muted-foreground/30 rounded-md border border-dashed p-4">
1029
- <p class="text-muted-foreground text-center text-sm">
1030
- No schema found for document type: {documentType}
1031
- </p>
1032
- </div>
1033
- {/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>
1034
1269
  </div>
1035
1270
 
1036
1271
  <!-- Sanity-style bottom bar -->
1037
1272
  {#if documentId}
1038
- <div class="border-border bg-background border-t p-4">
1273
+ <div class="border-rule bg-background relative z-50 border-t p-4">
1039
1274
  {#if isPreviewingVersion && activePreview}
1040
1275
  <!-- Version preview footer -->
1041
1276
  <div class="flex items-center justify-between">
1042
1277
  <p class="text-muted-foreground text-sm">
1043
- 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
+ )}
1044
1282
  </p>
1045
1283
  <Button
1046
1284
  size="sm"
@@ -1068,6 +1306,9 @@
1068
1306
  perspective = 'draft';
1069
1307
  publishedData = null;
1070
1308
  toast.success('Revision restored');
1309
+ if (onRestored && documentId) {
1310
+ onRestored(documentId);
1311
+ }
1071
1312
  } catch {
1072
1313
  toast.error('Failed to restore revision');
1073
1314
  }
@@ -1083,78 +1324,74 @@
1083
1324
  {#if fullDocument?._meta?.status === 'unpublished'}
1084
1325
  Unpublished
1085
1326
  {:else}
1086
- 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'}
1087
1337
  {/if}
1088
1338
  </p>
1089
1339
  {#if fullDocument?._meta?.status === 'unpublished'}
1090
- <Button
1091
- size="sm"
1092
- onclick={publishDocument}
1093
- disabled={saving}
1094
- >
1095
- Publish
1096
- </Button>
1097
- {:else}
1098
- <Button
1099
- size="sm"
1100
- variant="secondary"
1101
- onclick={unpublishDocument}
1102
- disabled={saving}
1103
- >
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}>
1104
1345
  Unpublish
1105
1346
  </Button>
1106
1347
  {/if}
1107
1348
  </div>
1108
1349
  {:else}
1109
- <div class="flex items-center justify-between">
1110
- <!-- Left: Save status badges -->
1111
- <div class="flex items-center gap-2">
1112
- {#if saving}
1113
- <Badge variant="secondary">Saving...</Badge>
1114
- {:else if publishSuccess && now - publishSuccess.getTime() < 3000}
1115
- <Badge variant="default">Published!</Badge>
1116
- {:else if hasUnsavedChanges}
1117
- <Badge variant="outline">Unsaved</Badge>
1118
- {:else if savedAgoText}
1119
- <Badge variant="secondary">{savedAgoText}</Badge>
1120
- {/if}
1121
- </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>
1122
1357
 
1123
- <!-- Right: Publish button + horizontal three dots menu -->
1124
- <div class="flex items-center gap-2">
1125
- {#if !isReadOnly && !isViewingPublished}
1126
- <Button
1127
- onclick={publishDocument}
1128
- disabled={!canPublish}
1129
- size="sm"
1130
- variant={canPublish ? 'default' : 'secondary'}
1131
- class="cursor-pointer"
1132
- >
1133
- {#if saving}
1134
- Publishing...
1135
- {:else if !hasUnpublishedContent}
1136
- Published
1137
- {:else}
1138
- Publish Changes
1139
- {/if}
1140
- </Button>
1141
- {:else if isReadOnly}
1142
- <Badge variant="secondary" class="text-xs">Read Only</Badge>
1143
- {/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}
1144
1381
 
1145
- {#if !isReadOnly}
1146
- <Button
1147
- variant="ghost"
1148
- size="icon"
1149
- class="h-8 w-8 text-muted-foreground hover:text-destructive"
1150
- onclick={deleteDocument}
1151
- title="Delete document"
1152
- >
1153
- <Trash2 class="h-4 w-4" />
1154
- </Button>
1155
- {/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>
1156
1394
  </div>
1157
- </div>
1158
1395
  {/if}
1159
1396
  </div>
1160
1397
  {/if}
@@ -1165,18 +1402,29 @@
1165
1402
  <!-- Backdrop -->
1166
1403
  <button
1167
1404
  class="flex-1 bg-black/30"
1168
- onclick={() => { showVersionHistory = false; previewingVersion = null; }}
1405
+ onclick={() => {
1406
+ showVersionHistory = false;
1407
+ previewingVersion = null;
1408
+ }}
1169
1409
  ></button>
1170
1410
  <!-- Panel -->
1171
- <div class="bg-background border-border flex w-80 flex-col border-l shadow-lg">
1172
- <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">
1173
1413
  <h3 class="text-sm font-medium">Version History</h3>
1174
1414
  <button
1175
1415
  class="hover:bg-muted rounded p-1 transition-colors"
1176
- onclick={() => { showVersionHistory = false; previewingVersion = null; }}
1416
+ onclick={() => {
1417
+ showVersionHistory = false;
1418
+ previewingVersion = null;
1419
+ }}
1177
1420
  >
1178
1421
  <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1179
- <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
+ />
1180
1428
  </svg>
1181
1429
  </button>
1182
1430
  </div>
@@ -1190,7 +1438,10 @@
1190
1438
  <div class="divide-y">
1191
1439
  {#each response.data as version}
1192
1440
  <button
1193
- 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
+ : ''}"
1194
1445
  onclick={async () => {
1195
1446
  try {
1196
1447
  const res = await documents.getVersion(documentId, version.versionNumber);
@@ -1208,7 +1459,14 @@
1208
1459
  >
1209
1460
  <div class="flex items-center gap-2">
1210
1461
  <span class="text-xs font-medium">v{version.versionNumber}</span>
1211
- <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
+ >
1212
1470
  {version.eventType}
1213
1471
  </Badge>
1214
1472
  </div>
@@ -1236,33 +1494,44 @@
1236
1494
  <!-- Inspect Modal -->
1237
1495
  {#if showInspect}
1238
1496
  <div class="absolute inset-0 z-50 flex items-center justify-center bg-black/50">
1239
- <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
+ >
1240
1500
  <!-- Modal header -->
1241
1501
  <div class="flex items-center justify-between border-b px-4 py-3">
1242
1502
  <div>
1243
1503
  <h3 class="text-sm font-semibold">Inspecting <em>{getPreviewTitle()}</em></h3>
1244
1504
  </div>
1245
1505
  <Button
1246
- variant="ghost"
1506
+ variant="ghost"
1247
1507
  class="hover:bg-muted rounded p-1 transition-colors"
1248
1508
  onclick={() => (showInspect = false)}
1249
1509
  >
1250
1510
  <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1251
- <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
+ />
1252
1517
  </svg>
1253
1518
  </Button>
1254
1519
  </div>
1255
1520
 
1256
1521
  <!-- Tabs -->
1257
- <div class="border-b flex">
1522
+ <div class="flex border-b">
1258
1523
  <button
1259
- 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'}"
1260
1527
  onclick={() => (inspectTab = 'parsed')}
1261
1528
  >
1262
1529
  Parsed
1263
1530
  </button>
1264
1531
  <button
1265
- 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'}"
1266
1535
  onclick={() => (inspectTab = 'raw')}
1267
1536
  >
1268
1537
  Raw JSON
@@ -1273,7 +1542,7 @@
1273
1542
  <div class="flex-1 overflow-auto p-4 font-mono text-sm">
1274
1543
  {#if inspectTab === 'raw'}
1275
1544
  <pre
1276
- class="whitespace-pre-wrap break-all text-xs select-text"
1545
+ class="text-xs break-all whitespace-pre-wrap select-text"
1277
1546
  tabindex="0"
1278
1547
  onkeydown={(e) => {
1279
1548
  if ((e.metaKey || e.ctrlKey) && e.key === 'a') {
@@ -1285,10 +1554,19 @@
1285
1554
  sel?.removeAllRanges();
1286
1555
  sel?.addRange(range);
1287
1556
  }
1288
- }}
1289
- >{@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>
1290
1564
  {:else}
1291
- {@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
+ )}
1292
1570
  {/if}
1293
1571
  </div>
1294
1572
  </div>
@@ -1308,12 +1586,17 @@
1308
1586
  {/if}
1309
1587
  {/if}
1310
1588
  {#if Array.isArray(val)}
1311
- <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
+ >
1312
1592
  {:else}
1313
- <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
+ >
1314
1597
  {/if}
1315
1598
  </summary>
1316
- <div class="ml-4 border-l border-border/50 pl-3">
1599
+ <div class="border-rule/50 ml-4 border-l pl-3">
1317
1600
  {#if Array.isArray(val)}
1318
1601
  {#each val as item, i}
1319
1602
  {@render parsedValue(String(i), item, depth + 1)}