@aphexcms/cms-core 2.0.5 → 2.0.7

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 (106) hide show
  1. package/dist/api/documents.d.ts +18 -0
  2. package/dist/api/documents.d.ts.map +1 -1
  3. package/dist/api/documents.js +22 -1
  4. package/dist/components/AdminApp.svelte +53 -1
  5. package/dist/components/AdminApp.svelte.d.ts.map +1 -1
  6. package/dist/components/admin/DocumentEditor.svelte +440 -51
  7. package/dist/components/admin/DocumentEditor.svelte.d.ts +7 -0
  8. package/dist/components/admin/DocumentEditor.svelte.d.ts.map +1 -1
  9. package/dist/components/admin/DocumentVersionPanel.svelte +138 -0
  10. package/dist/components/admin/DocumentVersionPanel.svelte.d.ts +15 -0
  11. package/dist/components/admin/DocumentVersionPanel.svelte.d.ts.map +1 -0
  12. package/dist/components/admin/MediaBrowser.svelte +42 -13
  13. package/dist/components/admin/MediaBrowser.svelte.d.ts.map +1 -1
  14. package/dist/components/admin/fields/ArrayField.svelte +102 -2
  15. package/dist/components/admin/fields/ArrayField.svelte.d.ts.map +1 -1
  16. package/dist/components/index.d.ts +1 -0
  17. package/dist/components/index.d.ts.map +1 -1
  18. package/dist/components/index.js +1 -0
  19. package/dist/db/interfaces/document.d.ts +14 -0
  20. package/dist/db/interfaces/document.d.ts.map +1 -1
  21. package/dist/db/interfaces/index.d.ts +6 -0
  22. package/dist/db/interfaces/index.d.ts.map +1 -1
  23. package/dist/graphql/resolvers.js +1 -1
  24. package/dist/lib/api/documents.d.ts +18 -0
  25. package/dist/lib/api/documents.d.ts.map +1 -1
  26. package/dist/lib/api/documents.js +22 -1
  27. package/dist/lib/api/documents.js.map +1 -1
  28. package/dist/lib/components/index.d.ts +1 -0
  29. package/dist/lib/components/index.d.ts.map +1 -1
  30. package/dist/lib/components/index.js +1 -0
  31. package/dist/lib/components/index.js.map +1 -1
  32. package/dist/lib/db/interfaces/document.d.ts +14 -0
  33. package/dist/lib/db/interfaces/document.d.ts.map +1 -1
  34. package/dist/lib/db/interfaces/index.d.ts +6 -0
  35. package/dist/lib/db/interfaces/index.d.ts.map +1 -1
  36. package/dist/lib/graphql/resolvers.js +1 -1
  37. package/dist/lib/graphql/resolvers.js.map +1 -1
  38. package/dist/lib/local-api/collection-api.d.ts +5 -1
  39. package/dist/lib/local-api/collection-api.d.ts.map +1 -1
  40. package/dist/lib/local-api/collection-api.js +26 -6
  41. package/dist/lib/local-api/collection-api.js.map +1 -1
  42. package/dist/lib/local-api/index.d.ts +2 -0
  43. package/dist/lib/local-api/index.d.ts.map +1 -1
  44. package/dist/lib/local-api/index.js +7 -2
  45. package/dist/lib/local-api/index.js.map +1 -1
  46. package/dist/lib/routes/document-versions.d.ts +5 -0
  47. package/dist/lib/routes/document-versions.d.ts.map +1 -0
  48. package/dist/lib/routes/document-versions.js +100 -0
  49. package/dist/lib/routes/document-versions.js.map +1 -0
  50. package/dist/lib/routes-exports.d.ts +1 -0
  51. package/dist/lib/routes-exports.d.ts.map +1 -1
  52. package/dist/lib/routes-exports.js +1 -0
  53. package/dist/lib/routes-exports.js.map +1 -1
  54. package/dist/lib/services/index.d.ts +1 -0
  55. package/dist/lib/services/index.d.ts.map +1 -1
  56. package/dist/lib/services/index.js +1 -0
  57. package/dist/lib/services/index.js.map +1 -1
  58. package/dist/lib/services/version-service.d.ts +39 -0
  59. package/dist/lib/services/version-service.d.ts.map +1 -0
  60. package/dist/lib/services/version-service.js +129 -0
  61. package/dist/lib/services/version-service.js.map +1 -0
  62. package/dist/lib/types/config.d.ts +7 -0
  63. package/dist/lib/types/config.d.ts.map +1 -1
  64. package/dist/lib/types/document.d.ts +2 -2
  65. package/dist/lib/types/document.d.ts.map +1 -1
  66. package/dist/lib/types/index.d.ts +1 -0
  67. package/dist/lib/types/index.d.ts.map +1 -1
  68. package/dist/lib/types/index.js +1 -0
  69. package/dist/lib/types/index.js.map +1 -1
  70. package/dist/lib/types/schemas.d.ts +3 -0
  71. package/dist/lib/types/schemas.d.ts.map +1 -1
  72. package/dist/lib/types/version.d.ts +15 -0
  73. package/dist/lib/types/version.d.ts.map +1 -0
  74. package/dist/lib/types/version.js +2 -0
  75. package/dist/lib/types/version.js.map +1 -0
  76. package/dist/local-api/collection-api.d.ts +5 -1
  77. package/dist/local-api/collection-api.d.ts.map +1 -1
  78. package/dist/local-api/collection-api.js +26 -6
  79. package/dist/local-api/index.d.ts +2 -0
  80. package/dist/local-api/index.d.ts.map +1 -1
  81. package/dist/local-api/index.js +7 -2
  82. package/dist/routes/document-versions.d.ts +5 -0
  83. package/dist/routes/document-versions.d.ts.map +1 -0
  84. package/dist/routes/document-versions.js +99 -0
  85. package/dist/routes-exports.d.ts +1 -0
  86. package/dist/routes-exports.d.ts.map +1 -1
  87. package/dist/routes-exports.js +1 -0
  88. package/dist/services/index.d.ts +1 -0
  89. package/dist/services/index.d.ts.map +1 -1
  90. package/dist/services/index.js +1 -0
  91. package/dist/services/version-service.d.ts +39 -0
  92. package/dist/services/version-service.d.ts.map +1 -0
  93. package/dist/services/version-service.js +128 -0
  94. package/dist/types/config.d.ts +7 -0
  95. package/dist/types/config.d.ts.map +1 -1
  96. package/dist/types/document.d.ts +2 -2
  97. package/dist/types/document.d.ts.map +1 -1
  98. package/dist/types/index.d.ts +1 -0
  99. package/dist/types/index.d.ts.map +1 -1
  100. package/dist/types/index.js +1 -0
  101. package/dist/types/schemas.d.ts +3 -0
  102. package/dist/types/schemas.d.ts.map +1 -1
  103. package/dist/types/version.d.ts +15 -0
  104. package/dist/types/version.d.ts.map +1 -0
  105. package/dist/types/version.js +1 -0
  106. package/package.json +1 -1
@@ -14,6 +14,7 @@
14
14
  import elementEvents from '../../utils/element-events';
15
15
  import { cmsLogger } from '../../utils/logger';
16
16
  import { toast } from 'svelte-sonner';
17
+ import { History, EyeOff, Trash2, Ellipsis, Search, Code } from '@lucide/svelte';
17
18
 
18
19
  interface Props {
19
20
  schemas: SchemaType[];
@@ -26,6 +27,8 @@
26
27
  onDeleted?: () => void;
27
28
  onPublished?: (documentId: string) => void;
28
29
  onOpenReference?: (documentId: string, documentType: string) => void;
30
+ onOpenVersionHistory?: (documentId: string) => void;
31
+ externalVersionPreview?: { versionNumber: number; data: Record<string, any>; eventType: string; createdAt?: string } | null;
29
32
  isReadOnly?: boolean;
30
33
  }
31
34
 
@@ -40,6 +43,8 @@
40
43
  onDeleted,
41
44
  onPublished,
42
45
  onOpenReference,
46
+ onOpenVersionHistory,
47
+ externalVersionPreview = null,
43
48
  isReadOnly = false
44
49
  }: Props = $props();
45
50
 
@@ -59,6 +64,52 @@
59
64
  let lastSaved = $state<Date | null>(null);
60
65
  let publishSuccess = $state<Date | null>(null);
61
66
 
67
+
68
+ // Perspective toggle
69
+ let perspective = $state<'draft' | 'published'>('draft');
70
+ let publishedData = $state<Record<string, any> | null>(null);
71
+ const isViewingPublished = $derived(perspective === 'published');
72
+
73
+ // Inspect modal
74
+ let showInspect = $state(false);
75
+
76
+ function syntaxHighlightJson(json: string): string {
77
+ return json.replace(
78
+ /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,
79
+ (match) => {
80
+ let cls = 'text-green-400'; // number
81
+ if (/^"/.test(match)) {
82
+ if (/:$/.test(match)) {
83
+ // key
84
+ const key = match.slice(0, -1); // remove trailing colon
85
+ return `<span class="text-blue-400">${escapeHtml(key)}</span>:`;
86
+ } else {
87
+ cls = 'text-yellow-500'; // string
88
+ }
89
+ } else if (/true|false/.test(match)) {
90
+ cls = 'text-orange-400'; // boolean
91
+ } else if (/null/.test(match)) {
92
+ cls = 'text-red-400'; // null
93
+ }
94
+ return `<span class="${cls}">${escapeHtml(match)}</span>`;
95
+ }
96
+ );
97
+ }
98
+
99
+ function escapeHtml(str: string): string {
100
+ return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
101
+ }
102
+ let inspectTab = $state<'parsed' | 'raw'>('parsed');
103
+
104
+ // Header options dropdown
105
+ let showHeaderMenu = $state(false);
106
+
107
+ // Version history
108
+ let showVersionHistory = $state(false);
109
+ let previewingVersion = $state<{ versionNumber: number; data: Record<string, any>; eventType: string; createdAt?: string } | null>(null);
110
+ const activePreview = $derived(externalVersionPreview || previewingVersion);
111
+ const isPreviewingVersion = $derived(!!activePreview);
112
+
62
113
  // Ticker to keep relative time updating
63
114
  let now = $state(Date.now());
64
115
  let tickerInterval: ReturnType<typeof setInterval> | null = null;
@@ -142,6 +193,9 @@
142
193
  saveError = null;
143
194
  lastSaved = null;
144
195
  publishSuccess = null;
196
+ perspective = 'draft';
197
+ publishedData = null;
198
+ previewingVersion = null;
145
199
 
146
200
  // Cancel pending auto-save
147
201
  if (autoSaveTimer) {
@@ -344,6 +398,7 @@
344
398
  }
345
399
 
346
400
  // Helper to recursively sort object keys for stable comparison
401
+ // Strips _key fields since those are auto-generated and not real content changes
347
402
  function sortObjectForComparison(item: any): any {
348
403
  if (item === null || typeof item !== 'object') return item;
349
404
 
@@ -351,6 +406,9 @@
351
406
  return item.map(sortObjectForComparison);
352
407
  }
353
408
 
409
+ const { _key, ...rest } = item;
410
+ item = rest;
411
+
354
412
  const sortedKeys = Object.keys(item).sort();
355
413
  const sortedObj: any = {};
356
414
  for (const key of sortedKeys) {
@@ -548,6 +606,54 @@
548
606
  }
549
607
  }
550
608
 
609
+ async function switchPerspective(newPerspective: 'draft' | 'published') {
610
+ if (newPerspective === perspective) return;
611
+
612
+ if (newPerspective === 'published') {
613
+ if (!documentId) return;
614
+ // Fetch published version of the document
615
+ try {
616
+ const response = await documents.getById(`${documentId}?perspective=published`);
617
+ if (response.success && response.data) {
618
+ publishedData = response.data;
619
+ perspective = 'published';
620
+ } else {
621
+ toast.error('No published version available');
622
+ }
623
+ } catch {
624
+ toast.error('Failed to load published version');
625
+ }
626
+ } else {
627
+ perspective = 'draft';
628
+ publishedData = null;
629
+ }
630
+ }
631
+
632
+ async function unpublishDocument() {
633
+ if (!documentId || saving) return;
634
+
635
+ const confirmUnpublish = confirm('Unpublish this document? It will be removed from published queries but the data is preserved.');
636
+ if (!confirmUnpublish) return;
637
+
638
+ saving = true;
639
+ saveError = null;
640
+
641
+ try {
642
+ const response = await documents.unpublish(documentId);
643
+ if (response.success) {
644
+ fullDocument = { ...fullDocument, _meta: { ...fullDocument?._meta, status: 'unpublished' } };
645
+ toast.success('Document unpublished — you can re-publish anytime');
646
+ showDropdown = false;
647
+ } else {
648
+ throw new Error(response.error || 'Failed to unpublish');
649
+ }
650
+ } catch (err) {
651
+ toast.error(err instanceof ApiError ? err.message : 'Failed to unpublish document');
652
+ } finally {
653
+ saving = false;
654
+ }
655
+ }
656
+
551
657
  // Validate all fields before publishing
552
658
  async function validateAllFields(): Promise<void> {
553
659
  if (!schema) {
@@ -705,13 +811,69 @@
705
811
  </div>
706
812
  </div>
707
813
 
708
- <!-- Right side: Actions and close button -->
814
+ <!-- Right side: Perspective toggle, actions, close -->
709
815
  <div class="flex items-center gap-2">
710
- <!-- Status badges -->
711
- {#if saving}
712
- <Badge variant="secondary" class="hidden sm:flex">Saving...</Badge>
713
- {:else if publishSuccess && now - publishSuccess.getTime() < 3000}
714
- <Badge variant="default" class="hidden sm:flex">Published!</Badge>
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">
848
+ <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"
858
+ >
859
+ <History class="h-3.5 w-3.5" /> History
860
+ </button>
861
+ <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"
867
+ >
868
+ <Code class="h-3.5 w-3.5" /> Inspect
869
+ </button>
870
+ </div>
871
+ <div
872
+ class="fixed inset-0 z-40"
873
+ onclick={() => (showHeaderMenu = false)}
874
+ ></div>
875
+ {/if}
876
+ </div>
715
877
  {/if}
716
878
 
717
879
  <!-- Close button (X) - hidden on mobile -->
@@ -828,17 +990,19 @@
828
990
  onerror={(error) => cmsLogger.error('[DocumentEditor]', 'Error in editor content:', error)}
829
991
  >
830
992
  {#each schema.fields as field, index (index)}
993
+ {@const viewData = isPreviewingVersion && activePreview ? activePreview.data : isViewingPublished && publishedData ? publishedData : documentData}
831
994
  <SchemaField
832
995
  {field}
833
- value={documentData[field.name]}
834
- {documentData}
996
+ value={viewData[field.name]}
997
+ documentData={viewData}
835
998
  onUpdate={(newValue) => {
999
+ if (isViewingPublished) return;
836
1000
  documentData = { ...documentData, [field.name]: newValue };
837
1001
  hasUnsavedChanges = true;
838
1002
  }}
839
1003
  {onOpenReference}
840
1004
  schemaType={documentType}
841
- readonly={isReadOnly}
1005
+ readonly={isReadOnly || isViewingPublished || isPreviewingVersion}
842
1006
  organizationId={fullDocument?._meta?.organizationId}
843
1007
  />
844
1008
  {/each}
@@ -868,6 +1032,80 @@
868
1032
  <!-- Sanity-style bottom bar -->
869
1033
  {#if documentId}
870
1034
  <div class="border-border bg-background border-t p-4">
1035
+ {#if isPreviewingVersion && activePreview}
1036
+ <!-- Version preview footer -->
1037
+ <div class="flex items-center justify-between">
1038
+ <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 })}
1040
+ </p>
1041
+ {#if fullDocument?._meta?.publishedHash && fullDocument?._meta?.status !== 'unpublished'}
1042
+ <p class="text-muted-foreground text-xs">Unpublish first to restore</p>
1043
+ {:else}
1044
+ <Button
1045
+ size="sm"
1046
+ onclick={async () => {
1047
+ if (!documentId || !activePreview) return;
1048
+ try {
1049
+ await documents.restoreVersion(documentId, activePreview.versionNumber);
1050
+ const docRes = await documents.getById(documentId);
1051
+ if (docRes.success && docRes.data) {
1052
+ const doc = docRes.data as Record<string, any>;
1053
+ fullDocument = doc;
1054
+ const newData: Record<string, any> = {};
1055
+ if (schema) {
1056
+ for (const field of schema.fields) {
1057
+ if (doc[field.name] !== undefined) {
1058
+ newData[field.name] = doc[field.name];
1059
+ }
1060
+ }
1061
+ }
1062
+ documentData = newData;
1063
+ hasUnsavedChanges = false;
1064
+ lastSaved = new Date();
1065
+ }
1066
+ previewingVersion = null;
1067
+ perspective = 'draft';
1068
+ publishedData = null;
1069
+ toast.success('Revision restored');
1070
+ } catch {
1071
+ toast.error('Failed to restore revision');
1072
+ }
1073
+ }}
1074
+ >
1075
+ Restore
1076
+ </Button>
1077
+ {/if}
1078
+ </div>
1079
+ {:else if isViewingPublished}
1080
+ <!-- Published view footer -->
1081
+ <div class="flex items-center justify-between">
1082
+ <p class="text-muted-foreground text-sm">
1083
+ {#if fullDocument?._meta?.status === 'unpublished'}
1084
+ Unpublished
1085
+ {: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'}
1087
+ {/if}
1088
+ </p>
1089
+ {#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
+ >
1104
+ Unpublish
1105
+ </Button>
1106
+ {/if}
1107
+ </div>
1108
+ {:else}
871
1109
  <div class="flex items-center justify-between">
872
1110
  <!-- Left: Save status badges -->
873
1111
  <div class="flex items-center gap-2">
@@ -884,7 +1122,7 @@
884
1122
 
885
1123
  <!-- Right: Publish button + horizontal three dots menu -->
886
1124
  <div class="flex items-center gap-2">
887
- {#if !isReadOnly}
1125
+ {#if !isReadOnly && !isViewingPublished}
888
1126
  <Button
889
1127
  onclick={publishDocument}
890
1128
  disabled={!canPublish}
@@ -900,57 +1138,208 @@
900
1138
  Publish Changes
901
1139
  {/if}
902
1140
  </Button>
903
- {:else}
1141
+ {:else if isReadOnly}
904
1142
  <Badge variant="secondary" class="text-xs">Read Only</Badge>
905
1143
  {/if}
906
1144
 
907
- <!-- Horizontal three dots menu (only for non-read-only users) -->
908
1145
  {#if !isReadOnly}
909
- <div class="relative">
910
- <Button
911
- onclick={() => (showDropdown = !showDropdown)}
912
- variant="ghost"
913
- class="hover:bg-muted flex h-8 w-8 items-center justify-center rounded transition-colors"
914
- >
915
- <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
916
- <path
917
- stroke-linecap="round"
918
- stroke-linejoin="round"
919
- stroke-width="2"
920
- d="M5 12h.01M12 12h.01M19 12h.01"
921
- />
922
- </svg>
923
- </Button>
924
-
925
- {#if showDropdown}
926
- <!-- Dropdown menu -->
927
- <div
928
- class="bg-background border-border absolute right-0 bottom-full z-50 mb-2 min-w-[140px] rounded-md border py-1 shadow-lg"
929
- >
930
- <Button
931
- variant="ghost"
932
- onclick={() => {
933
- showDropdown = false;
934
- deleteDocument();
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}
1156
+ </div>
1157
+ </div>
1158
+ {/if}
1159
+ </div>
1160
+ {/if}
1161
+
1162
+ <!-- Version History Panel -->
1163
+ {#if showVersionHistory && documentId}
1164
+ <div class="absolute inset-0 z-50 flex">
1165
+ <!-- Backdrop -->
1166
+ <button
1167
+ class="flex-1 bg-black/30"
1168
+ onclick={() => { showVersionHistory = false; previewingVersion = null; }}
1169
+ ></button>
1170
+ <!-- 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">
1173
+ <h3 class="text-sm font-medium">Version History</h3>
1174
+ <button
1175
+ class="hover:bg-muted rounded p-1 transition-colors"
1176
+ onclick={() => { showVersionHistory = false; previewingVersion = null; }}
1177
+ >
1178
+ <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" />
1180
+ </svg>
1181
+ </button>
1182
+ </div>
1183
+ <div class="flex-1 overflow-auto">
1184
+ {#await documents.listVersions(documentId)}
1185
+ <div class="p-4 text-center">
1186
+ <span class="text-muted-foreground text-sm">Loading versions...</span>
1187
+ </div>
1188
+ {:then response}
1189
+ {#if response.success && response.data && response.data.length > 0}
1190
+ <div class="divide-y">
1191
+ {#each response.data as version}
1192
+ <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' : ''}"
1194
+ onclick={async () => {
1195
+ try {
1196
+ const res = await documents.getVersion(documentId, version.versionNumber);
1197
+ if (res.success && res.data) {
1198
+ previewingVersion = {
1199
+ versionNumber: version.versionNumber,
1200
+ data: res.data.data,
1201
+ eventType: version.eventType
1202
+ };
1203
+ }
1204
+ } catch {
1205
+ toast.error('Failed to load version');
1206
+ }
935
1207
  }}
936
- class="hover:bg-muted text-destructive flex w-full items-center gap-2 px-3 py-2 text-left text-sm transition-colors"
937
1208
  >
938
- Delete document
939
- </Button>
940
- </div>
941
-
942
- <!-- Click outside to close -->
943
- <div
944
- class="fixed inset-0 z-40"
945
- use:elementEvents={{
946
- events: [{ name: 'click', handler: () => (showDropdown = false) }]
947
- }}
948
- ></div>
949
- {/if}
1209
+ <div class="flex items-center gap-2">
1210
+ <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]">
1212
+ {version.eventType}
1213
+ </Badge>
1214
+ </div>
1215
+ <p class="text-muted-foreground text-[11px]">
1216
+ {new Date(version.createdAt).toLocaleString()}
1217
+ </p>
1218
+ </button>
1219
+ {/each}
1220
+ </div>
1221
+ {:else}
1222
+ <div class="p-4 text-center">
1223
+ <span class="text-muted-foreground text-sm">No versions yet</span>
1224
+ </div>
1225
+ {/if}
1226
+ {:catch}
1227
+ <div class="p-4 text-center">
1228
+ <span class="text-destructive text-sm">Failed to load versions</span>
950
1229
  </div>
1230
+ {/await}
1231
+ </div>
1232
+ </div>
1233
+ </div>
1234
+ {/if}
1235
+
1236
+ <!-- Inspect Modal -->
1237
+ {#if showInspect}
1238
+ <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">
1240
+ <!-- Modal header -->
1241
+ <div class="flex items-center justify-between border-b px-4 py-3">
1242
+ <div>
1243
+ <h3 class="text-sm font-semibold">Inspecting <em>{getPreviewTitle()}</em></h3>
1244
+ </div>
1245
+ <button
1246
+ class="hover:bg-muted rounded p-1 transition-colors"
1247
+ onclick={() => (showInspect = false)}
1248
+ >
1249
+ <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1250
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
1251
+ </svg>
1252
+ </button>
1253
+ </div>
1254
+
1255
+ <!-- Tabs -->
1256
+ <div class="border-b flex">
1257
+ <button
1258
+ 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'}"
1259
+ onclick={() => (inspectTab = 'parsed')}
1260
+ >
1261
+ Parsed
1262
+ </button>
1263
+ <button
1264
+ 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'}"
1265
+ onclick={() => (inspectTab = 'raw')}
1266
+ >
1267
+ Raw JSON
1268
+ </button>
1269
+ </div>
1270
+
1271
+ <!-- Content -->
1272
+ <div class="flex-1 overflow-auto p-4 font-mono text-sm">
1273
+ {#if inspectTab === 'raw'}
1274
+ <pre
1275
+ class="whitespace-pre-wrap break-all text-xs select-text"
1276
+ tabindex="0"
1277
+ onkeydown={(e) => {
1278
+ if ((e.metaKey || e.ctrlKey) && e.key === 'a') {
1279
+ e.preventDefault();
1280
+ e.stopPropagation();
1281
+ const sel = window.getSelection();
1282
+ const range = document.createRange();
1283
+ range.selectNodeContents(e.currentTarget);
1284
+ sel?.removeAllRanges();
1285
+ sel?.addRange(range);
1286
+ }
1287
+ }}
1288
+ >{@html syntaxHighlightJson(JSON.stringify({ id: documentId, _meta: fullDocument?._meta, ...documentData }, null, 2))}</pre>
1289
+ {:else}
1290
+ {@render parsedValue(null, { id: documentId, _meta: fullDocument?._meta, ...documentData }, 0)}
951
1291
  {/if}
952
1292
  </div>
953
1293
  </div>
954
1294
  </div>
955
1295
  {/if}
956
1296
  </div>
1297
+
1298
+ {#snippet parsedValue(key: string | null, val: any, depth: number)}
1299
+ {#if val && typeof val === 'object'}
1300
+ <details class="my-0.5" open={depth < 2}>
1301
+ <summary class="cursor-pointer text-xs leading-relaxed">
1302
+ {#if key !== null}
1303
+ {#if typeof key === 'number' || /^\d+$/.test(String(key))}
1304
+ <span class="text-purple-400">{key}:</span>
1305
+ {:else}
1306
+ <span class="text-blue-400">{key}:</span>
1307
+ {/if}
1308
+ {/if}
1309
+ {#if Array.isArray(val)}
1310
+ <span class="text-muted-foreground">[...] {val.length} {val.length === 1 ? 'item' : 'items'}</span>
1311
+ {:else}
1312
+ <span class="text-muted-foreground">&#123;...&#125; {Object.keys(val).length} {Object.keys(val).length === 1 ? 'property' : 'properties'}</span>
1313
+ {/if}
1314
+ </summary>
1315
+ <div class="ml-4 border-l border-border/50 pl-3">
1316
+ {#if Array.isArray(val)}
1317
+ {#each val as item, i}
1318
+ {@render parsedValue(String(i), item, depth + 1)}
1319
+ {/each}
1320
+ {:else}
1321
+ {#each Object.entries(val) as [k, v]}
1322
+ {@render parsedValue(k, v, depth + 1)}
1323
+ {/each}
1324
+ {/if}
1325
+ </div>
1326
+ </details>
1327
+ {:else}
1328
+ <div class="my-0.5 text-xs leading-relaxed">
1329
+ {#if key !== null}
1330
+ <span class="text-blue-400">{key}:</span>
1331
+ {/if}
1332
+ {#if typeof val === 'string'}
1333
+ <span class="text-yellow-500">{val}</span>
1334
+ {:else if typeof val === 'number'}
1335
+ <span class="text-green-400">{val}</span>
1336
+ {:else if typeof val === 'boolean'}
1337
+ <span class="text-orange-400">{val}</span>
1338
+ {:else if val === null || val === undefined}
1339
+ <span class="text-red-400">null</span>
1340
+ {:else}
1341
+ <span class="text-muted-foreground">{JSON.stringify(val)}</span>
1342
+ {/if}
1343
+ </div>
1344
+ {/if}
1345
+ {/snippet}
@@ -10,6 +10,13 @@ interface Props {
10
10
  onDeleted?: () => void;
11
11
  onPublished?: (documentId: string) => void;
12
12
  onOpenReference?: (documentId: string, documentType: string) => void;
13
+ onOpenVersionHistory?: (documentId: string) => void;
14
+ externalVersionPreview?: {
15
+ versionNumber: number;
16
+ data: Record<string, any>;
17
+ eventType: string;
18
+ createdAt?: string;
19
+ } | null;
13
20
  isReadOnly?: boolean;
14
21
  }
15
22
  declare const DocumentEditor: import("svelte").Component<Props, {}, "">;
@@ -1 +1 @@
1
- {"version":3,"file":"DocumentEditor.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/components/admin/DocumentEditor.svelte.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAUxD,UAAU,KAAK;IACd,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,EAAE,OAAO,CAAC;IACpB,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,OAAO,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,WAAW,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1D,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,WAAW,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,eAAe,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,IAAI,CAAC;IACrE,UAAU,CAAC,EAAE,OAAO,CAAC;CACrB;AAg2BF,QAAA,MAAM,cAAc,2CAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
1
+ {"version":3,"file":"DocumentEditor.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/components/admin/DocumentEditor.svelte.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAWxD,UAAU,KAAK;IACd,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,EAAE,OAAO,CAAC;IACpB,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,OAAO,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,WAAW,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1D,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,WAAW,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,eAAe,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,IAAI,CAAC;IACrE,oBAAoB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IACpD,sBAAsB,CAAC,EAAE;QAAE,aAAa,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC5H,UAAU,CAAC,EAAE,OAAO,CAAC;CACrB;AAsrCF,QAAA,MAAM,cAAc,2CAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}