@aphexcms/cms-core 0.1.3 → 0.1.5

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 (280) hide show
  1. package/dist/api/assets.d.ts +48 -0
  2. package/dist/api/assets.d.ts.map +1 -0
  3. package/dist/api/assets.js +52 -0
  4. package/dist/api/client.d.ts +37 -0
  5. package/dist/api/client.d.ts.map +1 -0
  6. package/dist/api/client.js +125 -0
  7. package/dist/api/documents.d.ts +56 -0
  8. package/dist/api/documents.d.ts.map +1 -0
  9. package/dist/api/documents.js +77 -0
  10. package/dist/api/index.d.ts +7 -0
  11. package/dist/api/index.d.ts.map +1 -0
  12. package/dist/api/index.js +5 -0
  13. package/dist/api/organizations.d.ts +101 -0
  14. package/dist/api/organizations.d.ts.map +1 -0
  15. package/dist/api/organizations.js +92 -0
  16. package/dist/api/types.d.ts +23 -0
  17. package/dist/api/types.d.ts.map +1 -0
  18. package/dist/api/types.js +1 -0
  19. package/dist/app.d.ts +19 -0
  20. package/dist/auth/MULTI_TENANCY_PLAN.md +1183 -0
  21. package/dist/auth/auth-errors.d.ts +7 -0
  22. package/dist/auth/auth-errors.d.ts.map +1 -0
  23. package/dist/auth/auth-errors.js +13 -0
  24. package/dist/auth/auth-hooks.d.ts +6 -0
  25. package/dist/auth/auth-hooks.d.ts.map +1 -0
  26. package/dist/auth/auth-hooks.js +108 -0
  27. package/dist/auth/provider.d.ts +17 -0
  28. package/dist/auth/provider.d.ts.map +1 -0
  29. package/dist/auth/provider.js +1 -0
  30. package/dist/client/index.d.ts +24 -0
  31. package/dist/client/index.d.ts.map +1 -0
  32. package/dist/client/index.js +31 -0
  33. package/dist/components/AdminApp.svelte +1077 -0
  34. package/dist/components/AdminApp.svelte.d.ts +24 -0
  35. package/dist/components/AdminApp.svelte.d.ts.map +1 -0
  36. package/dist/components/admin/AdminLayout.svelte +115 -0
  37. package/dist/components/admin/AdminLayout.svelte.d.ts +15 -0
  38. package/dist/components/admin/AdminLayout.svelte.d.ts.map +1 -0
  39. package/dist/components/admin/DocumentEditor.svelte +795 -0
  40. package/dist/components/admin/DocumentEditor.svelte.d.ts +18 -0
  41. package/dist/components/admin/DocumentEditor.svelte.d.ts.map +1 -0
  42. package/dist/components/admin/DocumentTypesList.svelte +97 -0
  43. package/dist/components/admin/DocumentTypesList.svelte.d.ts +14 -0
  44. package/dist/components/admin/DocumentTypesList.svelte.d.ts.map +1 -0
  45. package/dist/components/admin/ObjectModal.svelte +135 -0
  46. package/dist/components/admin/ObjectModal.svelte.d.ts +15 -0
  47. package/dist/components/admin/ObjectModal.svelte.d.ts.map +1 -0
  48. package/dist/components/admin/SchemaField.svelte +171 -0
  49. package/dist/components/admin/SchemaField.svelte.d.ts +19 -0
  50. package/dist/components/admin/SchemaField.svelte.d.ts.map +1 -0
  51. package/dist/components/admin/fields/ArrayField.svelte +266 -0
  52. package/dist/components/admin/fields/ArrayField.svelte.d.ts +12 -0
  53. package/dist/components/admin/fields/ArrayField.svelte.d.ts.map +1 -0
  54. package/dist/components/admin/fields/BooleanField.svelte +35 -0
  55. package/dist/components/admin/fields/BooleanField.svelte.d.ts +13 -0
  56. package/dist/components/admin/fields/BooleanField.svelte.d.ts.map +1 -0
  57. package/dist/components/admin/fields/ImageField.svelte +284 -0
  58. package/dist/components/admin/fields/ImageField.svelte.d.ts +15 -0
  59. package/dist/components/admin/fields/ImageField.svelte.d.ts.map +1 -0
  60. package/dist/components/admin/fields/NumberField.svelte +82 -0
  61. package/dist/components/admin/fields/NumberField.svelte.d.ts +14 -0
  62. package/dist/components/admin/fields/NumberField.svelte.d.ts.map +1 -0
  63. package/dist/components/admin/fields/ReferenceField.svelte +260 -0
  64. package/dist/components/admin/fields/ReferenceField.svelte.d.ts +12 -0
  65. package/dist/components/admin/fields/ReferenceField.svelte.d.ts.map +1 -0
  66. package/dist/components/admin/fields/SlugField.svelte +74 -0
  67. package/dist/components/admin/fields/SlugField.svelte.d.ts +15 -0
  68. package/dist/components/admin/fields/SlugField.svelte.d.ts.map +1 -0
  69. package/dist/components/admin/fields/StringField.svelte +40 -0
  70. package/dist/components/admin/fields/StringField.svelte.d.ts +14 -0
  71. package/dist/components/admin/fields/StringField.svelte.d.ts.map +1 -0
  72. package/dist/components/admin/fields/TextareaField.svelte +40 -0
  73. package/dist/components/admin/fields/TextareaField.svelte.d.ts +14 -0
  74. package/dist/components/admin/fields/TextareaField.svelte.d.ts.map +1 -0
  75. package/dist/components/fields/index.d.ts +9 -0
  76. package/dist/components/fields/index.d.ts.map +1 -0
  77. package/dist/components/fields/index.js +9 -0
  78. package/dist/components/index.d.ts +7 -0
  79. package/dist/components/index.d.ts.map +1 -0
  80. package/dist/components/index.js +12 -0
  81. package/dist/components/layout/OrganizationSwitcher.svelte +218 -0
  82. package/dist/components/layout/OrganizationSwitcher.svelte.d.ts +11 -0
  83. package/dist/components/layout/OrganizationSwitcher.svelte.d.ts.map +1 -0
  84. package/dist/components/layout/Sidebar.svelte +88 -0
  85. package/dist/components/layout/Sidebar.svelte.d.ts +14 -0
  86. package/dist/components/layout/Sidebar.svelte.d.ts.map +1 -0
  87. package/dist/components/layout/sidebar/AppSidebar.svelte +63 -0
  88. package/dist/components/layout/sidebar/AppSidebar.svelte.d.ts +11 -0
  89. package/dist/components/layout/sidebar/AppSidebar.svelte.d.ts.map +1 -0
  90. package/dist/components/layout/sidebar/NavMain.svelte +95 -0
  91. package/dist/components/layout/sidebar/NavMain.svelte.d.ts +19 -0
  92. package/dist/components/layout/sidebar/NavMain.svelte.d.ts.map +1 -0
  93. package/dist/components/layout/sidebar/NavSecondary.svelte +69 -0
  94. package/dist/components/layout/sidebar/NavSecondary.svelte.d.ts +9 -0
  95. package/dist/components/layout/sidebar/NavSecondary.svelte.d.ts.map +1 -0
  96. package/dist/components/layout/sidebar/NavUser.svelte +85 -0
  97. package/dist/components/layout/sidebar/NavUser.svelte.d.ts +9 -0
  98. package/dist/components/layout/sidebar/NavUser.svelte.d.ts.map +1 -0
  99. package/dist/config.d.ts +3 -0
  100. package/dist/config.d.ts.map +1 -0
  101. package/dist/config.js +15 -0
  102. package/dist/db/adapters/index.d.ts +1 -0
  103. package/dist/db/adapters/index.d.ts.map +1 -0
  104. package/dist/db/adapters/index.js +4 -0
  105. package/dist/db/index.d.ts +2 -0
  106. package/dist/db/index.d.ts.map +1 -0
  107. package/dist/db/index.js +4 -0
  108. package/dist/db/interfaces/asset.d.ts +51 -0
  109. package/dist/db/interfaces/asset.d.ts.map +1 -0
  110. package/dist/db/interfaces/asset.js +1 -0
  111. package/dist/db/interfaces/document.d.ts +36 -0
  112. package/dist/db/interfaces/document.d.ts.map +1 -0
  113. package/dist/db/interfaces/document.js +1 -0
  114. package/dist/db/interfaces/index.d.ts +73 -0
  115. package/dist/db/interfaces/index.d.ts.map +1 -0
  116. package/dist/db/interfaces/index.js +1 -0
  117. package/dist/db/interfaces/organization.d.ts +27 -0
  118. package/dist/db/interfaces/organization.d.ts.map +1 -0
  119. package/dist/db/interfaces/organization.js +1 -0
  120. package/dist/db/interfaces/schema.d.ts +21 -0
  121. package/dist/db/interfaces/schema.d.ts.map +1 -0
  122. package/dist/db/interfaces/schema.js +1 -0
  123. package/dist/db/interfaces/user.d.ts +15 -0
  124. package/dist/db/interfaces/user.d.ts.map +1 -0
  125. package/dist/db/interfaces/user.js +1 -0
  126. package/dist/db/utils/reference-resolver.d.ts +18 -0
  127. package/dist/db/utils/reference-resolver.d.ts.map +1 -0
  128. package/dist/db/utils/reference-resolver.js +80 -0
  129. package/dist/define.d.ts +3 -0
  130. package/dist/define.d.ts.map +1 -0
  131. package/dist/define.js +4 -0
  132. package/dist/email/index.d.ts +2 -0
  133. package/dist/email/index.d.ts.map +1 -0
  134. package/dist/email/index.js +4 -0
  135. package/dist/email/interfaces/email.d.ts +42 -0
  136. package/dist/email/interfaces/email.d.ts.map +1 -0
  137. package/dist/email/interfaces/email.js +1 -0
  138. package/dist/engine.d.ts +26 -0
  139. package/dist/engine.d.ts.map +1 -0
  140. package/dist/engine.js +66 -0
  141. package/dist/field-validation/rule.d.ts +51 -0
  142. package/dist/field-validation/rule.d.ts.map +1 -0
  143. package/dist/field-validation/rule.js +221 -0
  144. package/dist/field-validation/utils.d.ts +21 -0
  145. package/dist/field-validation/utils.d.ts.map +1 -0
  146. package/dist/field-validation/utils.js +66 -0
  147. package/dist/hooks.d.ts +23 -0
  148. package/dist/hooks.d.ts.map +1 -0
  149. package/dist/hooks.js +96 -0
  150. package/dist/index.d.ts +2 -0
  151. package/dist/index.d.ts.map +1 -0
  152. package/dist/index.js +4 -0
  153. package/dist/plugins/README.md +154 -0
  154. package/dist/routes/assets-by-id.d.ts +5 -0
  155. package/dist/routes/assets-by-id.d.ts.map +1 -0
  156. package/dist/routes/assets-by-id.js +138 -0
  157. package/dist/routes/assets-cdn.d.ts +3 -0
  158. package/dist/routes/assets-cdn.d.ts.map +1 -0
  159. package/dist/routes/assets-cdn.js +155 -0
  160. package/dist/routes/assets.d.ts +4 -0
  161. package/dist/routes/assets.d.ts.map +1 -0
  162. package/dist/routes/assets.js +94 -0
  163. package/dist/routes/documents-by-id.d.ts +5 -0
  164. package/dist/routes/documents-by-id.d.ts.map +1 -0
  165. package/dist/routes/documents-by-id.js +142 -0
  166. package/dist/routes/documents-publish.d.ts +4 -0
  167. package/dist/routes/documents-publish.d.ts.map +1 -0
  168. package/dist/routes/documents-publish.js +151 -0
  169. package/dist/routes/documents.d.ts +4 -0
  170. package/dist/routes/documents.d.ts.map +1 -0
  171. package/dist/routes/documents.js +131 -0
  172. package/dist/routes/index.d.ts +6 -0
  173. package/dist/routes/index.d.ts.map +1 -0
  174. package/dist/routes/index.js +10 -0
  175. package/dist/routes/organizations-by-id.d.ts +5 -0
  176. package/dist/routes/organizations-by-id.d.ts.map +1 -0
  177. package/dist/routes/organizations-by-id.js +187 -0
  178. package/dist/routes/organizations-invitations.d.ts +4 -0
  179. package/dist/routes/organizations-invitations.d.ts.map +1 -0
  180. package/dist/routes/organizations-invitations.js +125 -0
  181. package/dist/routes/organizations-members.d.ts +5 -0
  182. package/dist/routes/organizations-members.d.ts.map +1 -0
  183. package/dist/routes/organizations-members.js +206 -0
  184. package/dist/routes/organizations-switch.d.ts +3 -0
  185. package/dist/routes/organizations-switch.d.ts.map +1 -0
  186. package/dist/routes/organizations-switch.js +53 -0
  187. package/dist/routes/organizations.d.ts +4 -0
  188. package/dist/routes/organizations.d.ts.map +1 -0
  189. package/dist/routes/organizations.js +108 -0
  190. package/dist/routes/schemas-by-type.d.ts +3 -0
  191. package/dist/routes/schemas-by-type.d.ts.map +1 -0
  192. package/dist/routes/schemas-by-type.js +25 -0
  193. package/dist/routes/schemas.d.ts +3 -0
  194. package/dist/routes/schemas.d.ts.map +1 -0
  195. package/dist/routes/schemas.js +11 -0
  196. package/dist/routes-exports.d.ts +14 -0
  197. package/dist/routes-exports.d.ts.map +1 -0
  198. package/dist/routes-exports.js +19 -0
  199. package/dist/schema-context.svelte.d.ts +10 -0
  200. package/dist/schema-context.svelte.d.ts.map +1 -0
  201. package/dist/schema-context.svelte.js +18 -0
  202. package/dist/schema-utils/cleanup.d.ts +21 -0
  203. package/dist/schema-utils/cleanup.d.ts.map +1 -0
  204. package/dist/schema-utils/cleanup.js +80 -0
  205. package/dist/schema-utils/index.d.ts +4 -0
  206. package/dist/schema-utils/index.d.ts.map +1 -0
  207. package/dist/schema-utils/index.js +4 -0
  208. package/dist/schema-utils/utils.d.ts +30 -0
  209. package/dist/schema-utils/utils.d.ts.map +1 -0
  210. package/dist/schema-utils/utils.js +37 -0
  211. package/dist/schema-utils/validator.d.ts +6 -0
  212. package/dist/schema-utils/validator.d.ts.map +1 -0
  213. package/dist/schema-utils/validator.js +45 -0
  214. package/dist/server/index.d.ts +16 -0
  215. package/dist/server/index.d.ts.map +1 -0
  216. package/dist/server/index.js +28 -0
  217. package/dist/services/asset-service.d.ts +86 -0
  218. package/dist/services/asset-service.d.ts.map +1 -0
  219. package/dist/services/asset-service.js +187 -0
  220. package/dist/services/index.d.ts +3 -0
  221. package/dist/services/index.d.ts.map +1 -0
  222. package/dist/services/index.js +4 -0
  223. package/dist/storage/adapters/index.d.ts +2 -0
  224. package/dist/storage/adapters/index.d.ts.map +1 -0
  225. package/dist/storage/adapters/index.js +2 -0
  226. package/dist/storage/adapters/local-storage-adapter.d.ts +54 -0
  227. package/dist/storage/adapters/local-storage-adapter.d.ts.map +1 -0
  228. package/dist/storage/adapters/local-storage-adapter.js +187 -0
  229. package/dist/storage/index.d.ts +3 -0
  230. package/dist/storage/index.d.ts.map +1 -0
  231. package/dist/storage/index.js +6 -0
  232. package/dist/storage/interfaces/index.d.ts +2 -0
  233. package/dist/storage/interfaces/index.d.ts.map +1 -0
  234. package/dist/storage/interfaces/index.js +2 -0
  235. package/dist/storage/interfaces/storage.d.ts +91 -0
  236. package/dist/storage/interfaces/storage.d.ts.map +1 -0
  237. package/dist/storage/interfaces/storage.js +1 -0
  238. package/dist/storage/providers/storage.d.ts +43 -0
  239. package/dist/storage/providers/storage.d.ts.map +1 -0
  240. package/dist/storage/providers/storage.js +64 -0
  241. package/dist/types/asset.d.ts +73 -0
  242. package/dist/types/asset.d.ts.map +1 -0
  243. package/dist/types/asset.js +2 -0
  244. package/dist/types/auth.d.ts +50 -0
  245. package/dist/types/auth.d.ts.map +1 -0
  246. package/dist/types/auth.js +41 -0
  247. package/dist/types/config.d.ts +47 -0
  248. package/dist/types/config.d.ts.map +1 -0
  249. package/dist/types/config.js +1 -0
  250. package/dist/types/document.d.ts +34 -0
  251. package/dist/types/document.d.ts.map +1 -0
  252. package/dist/types/document.js +1 -0
  253. package/dist/types/index.d.ts +9 -0
  254. package/dist/types/index.d.ts.map +1 -0
  255. package/dist/types/index.js +8 -0
  256. package/dist/types/organization.d.ts +105 -0
  257. package/dist/types/organization.d.ts.map +1 -0
  258. package/dist/types/organization.js +3 -0
  259. package/dist/types/schemas.d.ts +114 -0
  260. package/dist/types/schemas.d.ts.map +1 -0
  261. package/dist/types/schemas.js +1 -0
  262. package/dist/types/sidebar.d.ts +33 -0
  263. package/dist/types/sidebar.d.ts.map +1 -0
  264. package/dist/types/sidebar.js +1 -0
  265. package/dist/types/user.d.ts +14 -0
  266. package/dist/types/user.d.ts.map +1 -0
  267. package/dist/types/user.js +1 -0
  268. package/dist/utils/content-hash.d.ts +22 -0
  269. package/dist/utils/content-hash.d.ts.map +1 -0
  270. package/dist/utils/content-hash.js +67 -0
  271. package/dist/utils/image-url.d.ts +88 -0
  272. package/dist/utils/image-url.d.ts.map +1 -0
  273. package/dist/utils/image-url.js +165 -0
  274. package/dist/utils/index.d.ts +6 -0
  275. package/dist/utils/index.d.ts.map +1 -0
  276. package/dist/utils/index.js +9 -0
  277. package/dist/utils/slug.d.ts +13 -0
  278. package/dist/utils/slug.d.ts.map +1 -0
  279. package/dist/utils/slug.js +30 -0
  280. package/package.json +11 -41
@@ -0,0 +1,1077 @@
1
+ <script lang="ts">
2
+ /**
3
+ * AdminApp - Complete CMS Admin Interface
4
+ * A packaged, reusable Sanity-style admin UI
5
+ */
6
+ import { Alert, AlertDescription, AlertTitle } from '@aphexcms/ui/shadcn/alert';
7
+ import { Button } from '@aphexcms/ui/shadcn/button';
8
+ import * as Tabs from '@aphexcms/ui/shadcn/tabs';
9
+ import { page } from '$app/state';
10
+ import { goto } from '$app/navigation';
11
+ import { SvelteURLSearchParams } from 'svelte/reactivity';
12
+ import type { SchemaType } from '../types/index.js';
13
+ import DocumentEditor from './admin/DocumentEditor.svelte';
14
+ import type { DocumentType } from '../types/index.js';
15
+ import { documents } from '../api/index.js';
16
+
17
+ type InitDocumentType = Pick<DocumentType, 'name' | 'title' | 'description'>;
18
+
19
+ interface Props {
20
+ schemas: SchemaType[];
21
+ documentTypes: InitDocumentType[];
22
+ schemaError?: { message: string } | null;
23
+ title?: string;
24
+ graphqlSettings?: { endpoint: string; enableGraphiQL: boolean } | null;
25
+ isReadOnly?: boolean;
26
+ activeTab?: { value: 'structure' | 'vision' };
27
+ handleTabChange: (value: string) => void;
28
+ }
29
+
30
+ let {
31
+ schemas,
32
+ documentTypes,
33
+ schemaError = null,
34
+ title = 'Aphex CMS',
35
+ graphqlSettings = null,
36
+ isReadOnly = false,
37
+ activeTab = { value: 'structure' } as { value: 'structure' | 'vision' },
38
+ handleTabChange = () => {},
39
+ }: Props = $props();
40
+
41
+
42
+ // Set schema context for child components
43
+
44
+ const hasDocumentTypes = $derived(documentTypes.length > 0);
45
+
46
+ // Client-side routing state
47
+ let currentView = $state<'dashboard' | 'documents' | 'editor'>('dashboard');
48
+ let selectedDocumentType = $state<string | null>(null);
49
+
50
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
51
+ let documentsList = $state<any[]>([]);
52
+ let loading = $state(false);
53
+ let error = $state<string | null>(null);
54
+
55
+ // Mobile navigation state (Sanity-style)
56
+ let mobileView = $state<'types' | 'documents' | 'editor'>('types');
57
+
58
+ // Window size reactivity
59
+ let windowWidth = $state(1024); // Default to desktop size
60
+
61
+ // Document editor state (moved before layoutConfig)
62
+ let editingDocumentId = $state<string | null>(null);
63
+ let isCreatingDocument = $state(false);
64
+
65
+ // Editor stack for nested references
66
+ interface EditorStackItem {
67
+ documentId: string;
68
+ documentType: string;
69
+ isCreating: boolean;
70
+ }
71
+ let editorStack = $state<EditorStackItem[]>([]);
72
+
73
+ // Track which editor is currently active/focused (0 = primary, 1+ = stacked)
74
+ let activeEditorIndex = $state<number>(0);
75
+
76
+ // Calculate how many editors can be shown expanded based on available space
77
+ const MIN_EDITOR_WIDTH = 600; // Minimum width for ANY expanded editor
78
+ const COLLAPSED_WIDTH = 60; // Width of collapsed panels
79
+ const TYPES_EXPANDED = 350;
80
+ const DOCS_EXPANDED = 350;
81
+
82
+ let layoutConfig = $derived.by(() => {
83
+ const start = performance.now();
84
+ const totalEditors = (currentView === 'editor' ? 1 : 0) + editorStack.length;
85
+
86
+ if (totalEditors === 0) {
87
+ return {
88
+ totalEditors: 0,
89
+ expandedCount: 0,
90
+ collapsedCount: 0,
91
+ typesCollapsed: false,
92
+ docsCollapsed: false,
93
+ expandedIndices: [] as number[],
94
+ activeIndex: activeEditorIndex,
95
+ typesExpanded: true,
96
+ docsExpanded: true
97
+ };
98
+ }
99
+
100
+ // Ensure activeEditorIndex is valid (can be -1 for types, -2 for docs)
101
+ const validActiveIndex =
102
+ activeEditorIndex < 0
103
+ ? activeEditorIndex
104
+ : Math.max(0, Math.min(activeEditorIndex, totalEditors - 1));
105
+
106
+ // Check if types or docs are active (user clicked on collapsed strip)
107
+ const typesActive = activeEditorIndex === -1;
108
+ const docsActive = activeEditorIndex === -2;
109
+
110
+ // Calculate space requirements
111
+ // If user clicked types/docs, force those panels expanded
112
+ // Otherwise, prioritize editors over panels
113
+
114
+ let typesExpanded = typesActive || true; // Expand types if clicked, or by default
115
+ let docsExpanded = docsActive || true; // Expand docs if clicked, or by default
116
+ let typesWidth = typesExpanded ? TYPES_EXPANDED : COLLAPSED_WIDTH;
117
+ let docsWidth = selectedDocumentType ? (docsExpanded ? DOCS_EXPANDED : COLLAPSED_WIDTH) : 0;
118
+
119
+ // If user explicitly clicked types/docs, keep those panels expanded no matter what
120
+ if (typesActive || docsActive) {
121
+ // Force the clicked panel to stay expanded
122
+ typesExpanded = typesActive ? true : typesExpanded;
123
+ docsExpanded = docsActive ? true : docsExpanded;
124
+ typesWidth = typesActive ? TYPES_EXPANDED : COLLAPSED_WIDTH;
125
+ docsWidth = docsActive ? DOCS_EXPANDED : selectedDocumentType ? COLLAPSED_WIDTH : 0;
126
+
127
+ // Calculate how many editors fit with these panel sizes
128
+ let remainingWidth = windowWidth - typesWidth - docsWidth;
129
+ let maxExpandedEditors = Math.floor(remainingWidth / MIN_EDITOR_WIDTH);
130
+
131
+ // Expand as many editors as possible around the last active editor
132
+ if (maxExpandedEditors < 1) maxExpandedEditors = 0;
133
+
134
+ // Build expanded indices for editors
135
+ let expandedIndices: number[] = [];
136
+ if (maxExpandedEditors > 0 && totalEditors > 0) {
137
+ // Start from rightmost editor (most recently opened)
138
+ const lastEditorIndex = totalEditors - 1;
139
+ expandedIndices.push(lastEditorIndex);
140
+
141
+ // Expand editors to the left if space allows
142
+ for (
143
+ let i = lastEditorIndex - 1;
144
+ i >= 0 && expandedIndices.length < maxExpandedEditors;
145
+ i--
146
+ ) {
147
+ expandedIndices.push(i);
148
+ }
149
+ }
150
+
151
+ const expandedCount = expandedIndices.length;
152
+
153
+ return {
154
+ totalEditors,
155
+ expandedCount,
156
+ collapsedCount: totalEditors - expandedCount,
157
+ typesCollapsed: !typesExpanded,
158
+ docsCollapsed: !docsExpanded,
159
+ expandedIndices,
160
+ activeIndex: validActiveIndex,
161
+ typesExpanded,
162
+ docsExpanded
163
+ };
164
+ }
165
+
166
+ // Normal mode: prioritize editors over panels
167
+ // Calculate how many editors we can fit with current panel widths
168
+ let remainingWidth = windowWidth - typesWidth - docsWidth;
169
+ let maxExpandedEditors = Math.floor(remainingWidth / MIN_EDITOR_WIDTH);
170
+
171
+ // If we can't fit all editors, start collapsing panels
172
+ if (maxExpandedEditors < totalEditors) {
173
+ // Try collapsing docs first
174
+ docsWidth = selectedDocumentType ? COLLAPSED_WIDTH : 0;
175
+ docsExpanded = false;
176
+ remainingWidth = windowWidth - typesWidth - docsWidth;
177
+ maxExpandedEditors = Math.floor(remainingWidth / MIN_EDITOR_WIDTH);
178
+ }
179
+
180
+ // If still not enough space, collapse types too
181
+ if (maxExpandedEditors < totalEditors) {
182
+ typesWidth = COLLAPSED_WIDTH;
183
+ typesExpanded = false;
184
+ remainingWidth = windowWidth - typesWidth - docsWidth;
185
+ maxExpandedEditors = Math.floor(remainingWidth / MIN_EDITOR_WIDTH);
186
+ }
187
+
188
+ // Always expand at least the active editor
189
+ if (maxExpandedEditors < 1) {
190
+ maxExpandedEditors = 1;
191
+ }
192
+
193
+ // Expand editors around the active one symmetrically
194
+ let expandedIndices: number[] = [validActiveIndex];
195
+
196
+ if (maxExpandedEditors > 1) {
197
+ // How many editors to add on each side
198
+ const slotsToFill = Math.min(maxExpandedEditors - 1, totalEditors - 1);
199
+
200
+ // Expand symmetrically around active editor
201
+ for (let offset = 1; offset <= slotsToFill; offset++) {
202
+ const leftIndex = validActiveIndex - offset;
203
+ const rightIndex = validActiveIndex + offset;
204
+
205
+ // Alternate left and right to maintain symmetry
206
+ if (offset % 2 === 1) {
207
+ // Odd offset: try right first, then left
208
+ if (rightIndex < totalEditors && !expandedIndices.includes(rightIndex)) {
209
+ expandedIndices.push(rightIndex);
210
+ if (expandedIndices.length >= maxExpandedEditors) break;
211
+ }
212
+ if (leftIndex >= 0 && !expandedIndices.includes(leftIndex)) {
213
+ expandedIndices.push(leftIndex);
214
+ if (expandedIndices.length >= maxExpandedEditors) break;
215
+ }
216
+ } else {
217
+ // Even offset: try left first, then right
218
+ if (leftIndex >= 0 && !expandedIndices.includes(leftIndex)) {
219
+ expandedIndices.push(leftIndex);
220
+ if (expandedIndices.length >= maxExpandedEditors) break;
221
+ }
222
+ if (rightIndex < totalEditors && !expandedIndices.includes(rightIndex)) {
223
+ expandedIndices.push(rightIndex);
224
+ if (expandedIndices.length >= maxExpandedEditors) break;
225
+ }
226
+ }
227
+ }
228
+ }
229
+
230
+ const expandedCount = expandedIndices.length;
231
+
232
+ const end = performance.now();
233
+ console.log(
234
+ `[Layout Calc] ${(end - start).toFixed(3)}ms | Editors: ${totalEditors} | Expanded: ${expandedCount} | Window: ${windowWidth}px | Active: ${validActiveIndex} | ExpandedIndices: [${expandedIndices.join(', ')}]`
235
+ );
236
+
237
+ return {
238
+ totalEditors,
239
+ expandedCount,
240
+ collapsedCount: totalEditors - expandedCount,
241
+ typesCollapsed: !typesExpanded,
242
+ docsCollapsed: !docsExpanded,
243
+ expandedIndices,
244
+ activeIndex: validActiveIndex,
245
+ typesExpanded,
246
+ docsExpanded
247
+ };
248
+ });
249
+
250
+ let typesPanel = $derived.by(() => {
251
+ if (windowWidth < 620) {
252
+ return mobileView === 'types' ? 'w-full' : 'hidden';
253
+ }
254
+
255
+ return layoutConfig.typesExpanded ? 'w-[350px]' : 'w-[60px]';
256
+ });
257
+
258
+ let documentsPanelState = $derived.by(() => {
259
+ if (windowWidth < 620) {
260
+ const state = { visible: mobileView === 'documents', width: 'full' };
261
+ console.log('[Mobile Documents Panel]', { windowWidth, mobileView, state });
262
+ return state;
263
+ }
264
+ if (!selectedDocumentType) return { visible: false, width: 'none' };
265
+
266
+ const width = layoutConfig.docsExpanded ? 'normal' : 'compact';
267
+ return { visible: true, width };
268
+ });
269
+
270
+ let primaryEditorState = $derived.by(() => {
271
+ if (windowWidth < 620) {
272
+ return { visible: mobileView === 'editor', expanded: true };
273
+ }
274
+
275
+ if (currentView !== 'editor') return { visible: false, expanded: false };
276
+
277
+ const primaryIndex = 0;
278
+ const isExpanded = layoutConfig.expandedIndices.includes(primaryIndex);
279
+
280
+ return { visible: true, expanded: isExpanded };
281
+ });
282
+
283
+ // Update window width on resize
284
+ $effect(() => {
285
+ if (typeof window !== 'undefined') {
286
+ windowWidth = window.innerWidth;
287
+ const handleResize = () => {
288
+ windowWidth = window.innerWidth;
289
+ };
290
+ window.addEventListener('resize', handleResize);
291
+ return () => window.removeEventListener('resize', handleResize);
292
+ }
293
+ });
294
+
295
+ // Watch URL params for bookmarkable navigation
296
+ $effect(() => {
297
+ const url = page.url;
298
+ const docType = url.searchParams.get('docType');
299
+ const action = url.searchParams.get('action');
300
+ const docId = url.searchParams.get('docId');
301
+ const stackParam = url.searchParams.get('stack');
302
+
303
+ console.log('[URL Effect] Params:', {
304
+ docType,
305
+ action,
306
+ docId,
307
+ stackParam,
308
+ fullURL: url.toString()
309
+ });
310
+
311
+ if (action === 'create' && docType) {
312
+ console.log('[URL Effect] Branch: CREATE');
313
+ currentView = 'editor';
314
+ mobileView = 'editor';
315
+ selectedDocumentType = docType;
316
+ isCreatingDocument = true;
317
+ editingDocumentId = null;
318
+ editorStack = [];
319
+ fetchDocuments(docType);
320
+ } else if (docId) {
321
+ console.log('[URL Effect] Branch: EDIT (docId)');
322
+ currentView = 'editor';
323
+ mobileView = 'editor';
324
+ editingDocumentId = docId;
325
+ isCreatingDocument = false;
326
+
327
+ // Parse stack param to restore stacked editors
328
+ if (stackParam) {
329
+ const stackItems = stackParam.split(',').map((item) => {
330
+ const [type, id] = item.split(':');
331
+ return { documentType: type, documentId: id, isCreating: false };
332
+ });
333
+
334
+ // Only update stack and activeEditorIndex if the stack actually changed
335
+ const stackChanged =
336
+ editorStack.length !== stackItems.length ||
337
+ editorStack.some(
338
+ (item, i) =>
339
+ item.documentId !== stackItems[i]?.documentId ||
340
+ item.documentType !== stackItems[i]?.documentType
341
+ );
342
+
343
+ if (stackChanged) {
344
+ console.log('[AdminApp] Stack changed, updating editorStack and activeEditorIndex');
345
+ editorStack = stackItems;
346
+ // Set active editor to the last stacked editor
347
+ activeEditorIndex = stackItems.length; // 0 = primary, so stackItems.length is the last stacked editor
348
+ }
349
+ } else {
350
+ // Only reset if there was a stack before
351
+ if (editorStack.length > 0) {
352
+ editorStack = [];
353
+ activeEditorIndex = 0; // Primary editor is active
354
+ }
355
+ }
356
+
357
+ if (docType) {
358
+ selectedDocumentType = docType;
359
+ if (documentsList.length === 0 || selectedDocumentType !== docType) {
360
+ fetchDocuments(docType);
361
+ }
362
+ } else {
363
+ fetchDocumentForEditing(docId);
364
+ }
365
+ } else if (docType) {
366
+ console.log('[URL Effect] Branch: DOCUMENTS (docType only)');
367
+ currentView = 'documents';
368
+ mobileView = 'documents';
369
+ selectedDocumentType = docType;
370
+ editingDocumentId = null;
371
+ isCreatingDocument = false;
372
+ editorStack = [];
373
+ fetchDocuments(docType);
374
+ } else {
375
+ currentView = 'dashboard';
376
+ mobileView = 'types';
377
+ selectedDocumentType = null;
378
+ editingDocumentId = null;
379
+ isCreatingDocument = false;
380
+ editorStack = [];
381
+ }
382
+ });
383
+
384
+ // Watch orgId changes to refetch documents when switching organizations
385
+ $effect(() => {
386
+ const orgId = page.url.searchParams.get('orgId');
387
+
388
+ // When orgId changes and we have a selected document type, refetch documents
389
+ if (orgId && selectedDocumentType) {
390
+ fetchDocuments(selectedDocumentType);
391
+ }
392
+ });
393
+
394
+ async function navigateToDocumentType(docType: string) {
395
+ const params = new SvelteURLSearchParams(page.url.searchParams);
396
+ params.set('docType', docType);
397
+ params.delete('docId');
398
+ params.delete('action');
399
+ params.delete('stack');
400
+ await goto(`/admin?${params.toString()}`, { replaceState: false });
401
+ mobileView = 'documents';
402
+ }
403
+
404
+ async function navigateToCreateDocument(docType: string) {
405
+ const params = new SvelteURLSearchParams(page.url.searchParams);
406
+ params.set('docType', docType);
407
+ params.set('action', 'create');
408
+ params.delete('docId');
409
+ params.delete('stack');
410
+ await goto(`/admin?${params.toString()}`, { replaceState: false });
411
+ mobileView = 'editor';
412
+ }
413
+
414
+ async function navigateToEditDocument(docId: string, docType?: string, replace: boolean = false) {
415
+ const params = new SvelteURLSearchParams(page.url.searchParams);
416
+ params.set('docId', docId);
417
+ if (docType) params.set('docType', docType);
418
+ params.delete('action');
419
+ params.delete('fromDocId');
420
+ params.delete('fromDocType');
421
+ await goto(`/admin?${params.toString()}`, { replaceState: replace });
422
+ mobileView = 'editor';
423
+ }
424
+
425
+ async function navigateBack() {
426
+ // Check if we came from another document (mobile reference navigation)
427
+ const fromDocId = page.url.searchParams.get('fromDocId');
428
+ const fromDocType = page.url.searchParams.get('fromDocType');
429
+
430
+ if (fromDocId && fromDocType) {
431
+ // Navigate back to the document we came from
432
+ await navigateToEditDocument(fromDocId, fromDocType, false);
433
+ } else if (selectedDocumentType) {
434
+ // Navigate back to document list
435
+ const params = new SvelteURLSearchParams(page.url.searchParams);
436
+ params.set('docType', selectedDocumentType);
437
+ params.delete('docId');
438
+ params.delete('action');
439
+ params.delete('stack');
440
+ await goto(`/admin?${params.toString()}`, { replaceState: false });
441
+ mobileView = 'documents';
442
+ } else {
443
+ // Navigate back to home
444
+ const params = new SvelteURLSearchParams(page.url.searchParams);
445
+ params.delete('docType');
446
+ params.delete('docId');
447
+ params.delete('action');
448
+ params.delete('stack');
449
+ await goto(`/admin?${params.toString()}`, { replaceState: false });
450
+ mobileView = 'types';
451
+ }
452
+ }
453
+
454
+ // Handle opening reference in new editor panel
455
+ async function handleOpenReference(documentId: string, documentType: string) {
456
+ // On mobile, navigate to the referenced document directly
457
+ // Add fromDocId to track where we came from for proper back navigation
458
+ if (windowWidth < 620) {
459
+ const params = new SvelteURLSearchParams({
460
+ docId: documentId,
461
+ docType: documentType
462
+ });
463
+ // Track the document we're coming from
464
+ if (editingDocumentId) {
465
+ params.set('fromDocId', editingDocumentId);
466
+ if (selectedDocumentType) {
467
+ params.set('fromDocType', selectedDocumentType);
468
+ }
469
+ }
470
+ await goto(`/admin?${params.toString()}`, { replaceState: false });
471
+ mobileView = 'editor';
472
+ return;
473
+ }
474
+
475
+ // On desktop, add to editor stack
476
+ const newStack = [...editorStack, { documentId, documentType, isCreating: false }];
477
+
478
+ // Build stack param string: type1:id1,type2:id2,...
479
+ const stackParam = newStack.map((item) => `${item.documentType}:${item.documentId}`).join(',');
480
+
481
+ // Update URL with new stack
482
+ const params = new SvelteURLSearchParams(page.url.searchParams);
483
+ params.set('stack', stackParam);
484
+ await goto(`/admin?${params.toString()}`, { replaceState: false });
485
+
486
+ // Set the new editor as active
487
+ activeEditorIndex = newStack.length; // 0 = primary, so stack.length is the new editor
488
+ }
489
+
490
+ // Close editor from stack
491
+ async function handleCloseStackedEditor(index: number) {
492
+ const newStack = editorStack.slice(0, index);
493
+
494
+ // Update URL
495
+ const params = new SvelteURLSearchParams(page.url.searchParams);
496
+ if (newStack.length > 0) {
497
+ const stackParam = newStack
498
+ .map((item) => `${item.documentType}:${item.documentId}`)
499
+ .join(',');
500
+ params.set('stack', stackParam);
501
+ } else {
502
+ params.delete('stack');
503
+ }
504
+ await goto(`/admin?${params.toString()}`, { replaceState: false });
505
+
506
+ // Reset active editor to the last one
507
+ activeEditorIndex = Math.min(activeEditorIndex, newStack.length);
508
+ }
509
+
510
+ // Set active editor when clicking on a strip
511
+ function setActiveEditor(index: number) {
512
+ console.log('[AdminApp] setActiveEditor called:', {
513
+ previousIndex: activeEditorIndex,
514
+ newIndex: index,
515
+ editorStackLength: editorStack.length
516
+ });
517
+ activeEditorIndex = index;
518
+ }
519
+
520
+ function handleAutoSave(documentId: string, title: string) {
521
+ if (documentsList.length > 0) {
522
+ documentsList = documentsList.map((doc) =>
523
+ doc.id === documentId ? { ...doc, title: title } : doc
524
+ );
525
+ }
526
+ }
527
+
528
+ async function fetchDocumentForEditing(docId: string) {
529
+ loading = true;
530
+ error = null;
531
+
532
+ try {
533
+ const result = await documents.getById(docId);
534
+
535
+ if (result.success && result.data) {
536
+ const documentType = result.data.type;
537
+
538
+ if (documentsList.length === 0 || selectedDocumentType !== documentType) {
539
+ await fetchDocuments(documentType);
540
+ }
541
+
542
+ selectedDocumentType = documentType;
543
+ } else {
544
+ throw new Error(result.error || 'Failed to fetch document');
545
+ }
546
+ } catch (err) {
547
+ console.error('Failed to fetch document:', err);
548
+ error = err instanceof Error ? err.message : 'Failed to load document';
549
+ await goto('/admin', { replaceState: true });
550
+ } finally {
551
+ loading = false;
552
+ }
553
+ }
554
+
555
+ async function fetchDocuments(docType: string) {
556
+ loading = true;
557
+ error = null;
558
+
559
+ try {
560
+ const result = await documents.list({ docType, limit: 50 });
561
+
562
+ if (result.success && result.data) {
563
+ // Find schema for preview config
564
+ const schema = schemas.find((s) => s.name === docType);
565
+ const previewConfig = schema?.preview;
566
+
567
+ documentsList = result.data.map((doc: any) => {
568
+ const docData = doc.draftData || doc.publishedData || {};
569
+
570
+ // Use preview config if available
571
+ const title = previewConfig?.select?.title
572
+ ? docData[previewConfig.select.title] || `Untitled`
573
+ : docData.title || `Untitled`;
574
+
575
+ const subtitle = previewConfig?.select?.subtitle
576
+ ? docData[previewConfig.select.subtitle]
577
+ : undefined;
578
+
579
+ return {
580
+ id: doc.id,
581
+ title,
582
+ subtitle,
583
+ status: doc.status,
584
+ publishedAt: doc.publishedAt ? new Date(doc.publishedAt) : null,
585
+ updatedAt: doc.updatedAt ? new Date(doc.updatedAt) : null,
586
+ createdAt: doc.createdAt ? new Date(doc.createdAt) : null,
587
+ hasChanges:
588
+ doc.status === 'published' &&
589
+ doc.draftData !== null &&
590
+ JSON.stringify(doc.draftData) !== JSON.stringify(doc.publishedData)
591
+ };
592
+ });
593
+ } else {
594
+ throw new Error(result.error || 'Failed to fetch documents');
595
+ }
596
+ } catch (err) {
597
+ console.error('Failed to fetch documents:', err);
598
+ error = err instanceof Error ? err.message : 'Failed to load documents';
599
+ documentsList = [];
600
+ } finally {
601
+ loading = false;
602
+ }
603
+ }
604
+ </script>
605
+
606
+ <svelte:head>
607
+ <title>{activeTab.value === 'structure' ? 'Content' : 'Vision'} - {title}</title>
608
+ </svelte:head>
609
+
610
+ <div class="flex h-full flex-col overflow-hidden">
611
+ <!-- Mobile breadcrumb navigation (< 620px) -->
612
+ {#if windowWidth < 620}
613
+ <div class="border-border bg-background border-b">
614
+ <div class="flex h-12 items-center px-4">
615
+ {#if mobileView === 'documents' && selectedDocumentType}
616
+ <button
617
+ onclick={async () => {
618
+ mobileView = 'types';
619
+ const params = new SvelteURLSearchParams(page.url.searchParams);
620
+ params.delete('docType');
621
+ params.delete('docId');
622
+ params.delete('action');
623
+ params.delete('stack');
624
+ await goto(`/admin?${params.toString()}`, { replaceState: false });
625
+ }}
626
+ class="text-muted-foreground hover:text-foreground flex items-center gap-2 text-sm"
627
+ >
628
+ <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
629
+ <path
630
+ stroke-linecap="round"
631
+ stroke-linejoin="round"
632
+ stroke-width="2"
633
+ d="M15 19l-7-7 7-7"
634
+ />
635
+ </svg>
636
+ Content
637
+ </button>
638
+ <span class="text-muted-foreground mx-2">/</span>
639
+ <span class="text-sm font-medium">
640
+ {(documentTypes.find((t) => t.name === selectedDocumentType)?.title ||
641
+ selectedDocumentType) + 's'}
642
+ </span>
643
+ {:else if mobileView === 'editor'}
644
+ <Button
645
+ onclick={navigateBack}
646
+ variant="ghost"
647
+ class="text-muted-foreground hover:text-foreground text-sm"
648
+ >
649
+ <svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
650
+ <path
651
+ stroke-linecap="round"
652
+ stroke-linejoin="round"
653
+ stroke-width="2"
654
+ d="M15 19l-7-7 7-7"
655
+ />
656
+ </svg>
657
+ </Button>
658
+ <span class="ml-3 text-sm font-medium">
659
+ {selectedDocumentType
660
+ ? documentTypes.find((t) => t.name === selectedDocumentType)?.title ||
661
+ selectedDocumentType
662
+ : 'Document'}
663
+ </span>
664
+ {:else}
665
+ <span class="text-sm font-medium">Content</span>
666
+ {/if}
667
+ </div>
668
+ </div>
669
+ {/if}
670
+
671
+ <!-- Main Content -->
672
+ <div class="flex-1 overflow-hidden">
673
+ <Tabs.Root value={activeTab.value} onValueChange={handleTabChange} class="h-full">
674
+ <Tabs.Content value="structure" class="h-full overflow-hidden">
675
+ {#key `${currentView}-${selectedDocumentType}-${editingDocumentId}`}
676
+ <div class={windowWidth < 620 ? 'h-full w-full' : 'flex h-full w-full overflow-hidden'}>
677
+ {#if schemaError}
678
+ <div class="bg-destructive/5 flex flex-1 items-center justify-center p-8">
679
+ <div class="w-full max-w-2xl">
680
+ <Alert variant="destructive">
681
+ <svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
682
+ <path
683
+ stroke-linecap="round"
684
+ stroke-linejoin="round"
685
+ stroke-width="2"
686
+ d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.704-.833-2.464 0L4.35 16.5c-.77.833.192 2.5 1.732 2.5z"
687
+ />
688
+ </svg>
689
+ <AlertTitle>Schema Validation Error</AlertTitle>
690
+ <AlertDescription class="whitespace-pre-line">
691
+ {schemaError.message}
692
+ </AlertDescription>
693
+ </Alert>
694
+ </div>
695
+ </div>
696
+ {:else}
697
+ <!-- Types Panel -->
698
+ <div
699
+ class="border-r transition-all duration-200 {windowWidth < 620
700
+ ? typesPanel === 'hidden'
701
+ ? 'hidden'
702
+ : 'h-full w-screen'
703
+ : typesPanel} {typesPanel === 'hidden'
704
+ ? 'hidden'
705
+ : 'block'} h-full overflow-hidden"
706
+ >
707
+ {#if typesPanel === 'w-[60px]'}
708
+ <button
709
+ onclick={() => setActiveEditor(-1)}
710
+ class="hover:bg-muted/30 flex h-full w-full flex-col transition-colors"
711
+ title="Click to expand content types"
712
+ >
713
+ <div class="flex flex-1 items-start justify-center p-2 pt-8 text-left">
714
+ <div
715
+ class="text-foreground rotate-90 transform whitespace-nowrap text-sm font-medium"
716
+ >
717
+ Content
718
+ </div>
719
+ </div>
720
+ </button>
721
+ {:else}
722
+ <div class="h-full overflow-y-auto">
723
+ {#if hasDocumentTypes}
724
+ {#each documentTypes as docType, index (index)}
725
+ <button
726
+ onclick={() => navigateToDocumentType(docType.name)}
727
+ class="hover:bg-muted/50 border-border group flex w-full items-center justify-between border-b p-3 text-left transition-colors first:border-t {selectedDocumentType ===
728
+ docType.name
729
+ ? 'bg-muted/50'
730
+ : ''}"
731
+ >
732
+ <div class="flex items-center gap-3">
733
+ <div class="flex h-6 w-6 items-center justify-center">
734
+ <span class="text-muted-foreground">📄</span>
735
+ </div>
736
+ <div>
737
+ <h3 class="text-sm font-medium">{docType.title}s</h3>
738
+ {#if docType.description}
739
+ <p class="text-muted-foreground text-xs">{docType.description}</p>
740
+ {/if}
741
+ </div>
742
+ </div>
743
+ <div
744
+ class="text-muted-foreground group-hover:text-foreground transition-colors"
745
+ >
746
+ <svg
747
+ class="h-4 w-4"
748
+ fill="none"
749
+ viewBox="0 0 24 24"
750
+ stroke="currentColor"
751
+ >
752
+ <path
753
+ stroke-linecap="round"
754
+ stroke-linejoin="round"
755
+ stroke-width="2"
756
+ d="M9 5l7 7-7 7"
757
+ />
758
+ </svg>
759
+ </div>
760
+ </button>
761
+ {/each}
762
+ {:else}
763
+ <div class="p-6 text-center">
764
+ <div
765
+ class="bg-muted/50 mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full"
766
+ >
767
+ <span class="text-muted-foreground text-xl">📄</span>
768
+ </div>
769
+ <h3 class="mb-2 font-medium">No content types found</h3>
770
+ <p class="text-muted-foreground mb-4 text-sm">
771
+ Get started by defining your first schema type
772
+ </p>
773
+ <p class="text-muted-foreground text-xs">
774
+ Add schemas in <code class="bg-muted rounded px-1.5 py-0.5 text-xs"
775
+ >src/lib/schemaTypes/</code
776
+ >
777
+ </p>
778
+ </div>
779
+ {/if}
780
+ </div>
781
+ {/if}
782
+ </div>
783
+
784
+ <!-- Documents Panel -->
785
+ {#if selectedDocumentType}
786
+ <div
787
+ class="flex h-full flex-col overflow-hidden border-r transition-all duration-200
788
+ {!documentsPanelState.visible ? 'hidden' : ''}
789
+ {windowWidth < 620 ? (documentsPanelState.visible ? 'w-screen' : 'hidden') : ''}
790
+ {windowWidth >= 620 && documentsPanelState.width === 'full' ? 'w-full' : ''}
791
+ {windowWidth >= 620 && documentsPanelState.width === 'normal' ? 'w-[350px]' : ''}
792
+ {windowWidth >= 620 && documentsPanelState.width === 'compact' ? 'w-[60px]' : ''}
793
+ {windowWidth >= 620 && documentsPanelState.width === 'flex' ? 'flex-1' : ''}
794
+ "
795
+ >
796
+ {#if documentsPanelState.width === 'compact'}
797
+ <button
798
+ onclick={() => setActiveEditor(-2)}
799
+ class="hover:bg-muted/30 flex h-full w-full flex-col transition-colors"
800
+ title="Click to expand documents list"
801
+ >
802
+ <div class="flex flex-1 items-start justify-center p-2 pt-8 text-left">
803
+ <div class="text-foreground rotate-90 transform text-sm font-medium">
804
+ {(documentTypes.find((t) => t.name === selectedDocumentType)?.title ||
805
+ selectedDocumentType) + 's'}
806
+ </div>
807
+ </div>
808
+ </button>
809
+ {:else}
810
+ <div class="border-border bg-muted/20 border-b p-3">
811
+ <div class="flex items-center justify-between">
812
+ <div class="flex items-center gap-3">
813
+ {#if windowWidth > 620}
814
+ <!-- Desktop: Icon -->
815
+ <div class="flex h-6 w-6 items-center justify-center">
816
+ <span class="text-muted-foreground">📄</span>
817
+ </div>
818
+ {/if}
819
+ <div>
820
+ <h3 class="text-sm font-medium">
821
+ {(documentTypes.find((t) => t.name === selectedDocumentType)?.title ||
822
+ selectedDocumentType) + 's'}
823
+ </h3>
824
+ <p class="text-muted-foreground text-xs">
825
+ {documentsList.length} document{documentsList.length !== 1 ? 's' : ''}
826
+ </p>
827
+ </div>
828
+ </div>
829
+ {#if !isReadOnly}
830
+ <Button
831
+ size="sm"
832
+ variant="ghost"
833
+ onclick={() => navigateToCreateDocument(selectedDocumentType!)}
834
+ class="h-8 w-8 p-0"
835
+ title="Create new document"
836
+ >
837
+ <svg
838
+ class="h-4 w-4"
839
+ fill="none"
840
+ viewBox="0 0 24 24"
841
+ stroke="currentColor"
842
+ >
843
+ <path
844
+ stroke-linecap="round"
845
+ stroke-linejoin="round"
846
+ stroke-width="2"
847
+ d="M12 4v16m8-8H4"
848
+ />
849
+ </svg>
850
+ </Button>
851
+ {/if}
852
+ </div>
853
+ </div>
854
+
855
+ <div class="flex-1 overflow-y-auto">
856
+ {#if error}
857
+ <div class="p-4">
858
+ <Alert variant="destructive">
859
+ <AlertDescription>{error}</AlertDescription>
860
+ </Alert>
861
+ </div>
862
+ {:else if loading}
863
+ <div class="p-3 text-center">
864
+ <div class="text-muted-foreground text-sm">Loading...</div>
865
+ </div>
866
+ {:else if documentsList.length > 0}
867
+ {#each documentsList as doc, index (index)}
868
+ <button
869
+ onclick={() => navigateToEditDocument(doc.id, selectedDocumentType!)}
870
+ class="hover:bg-muted/50 border-border group flex w-full items-center justify-between border-b p-3 text-left transition-colors"
871
+ >
872
+ <div class="flex min-w-0 flex-1 items-center gap-3">
873
+ <div class="flex h-6 w-6 items-center justify-center">
874
+ <span class="text-muted-foreground">📄</span>
875
+ </div>
876
+ <div class="min-w-0 flex-1">
877
+ <h3 class="truncate text-sm font-medium">{doc.title}</h3>
878
+ {#if doc.subtitle}
879
+ <p class="text-muted-foreground truncate text-xs">
880
+ {doc.subtitle}
881
+ </p>
882
+ {:else if doc.slug}
883
+ <p class="text-muted-foreground text-xs">/{doc.slug}</p>
884
+ {:else if doc.status}
885
+ <p class="text-muted-foreground text-xs">{doc.status}</p>
886
+ {/if}
887
+ </div>
888
+ </div>
889
+ <div class="text-muted-foreground text-xs">
890
+ {doc.updatedAt?.toLocaleDateString() || ''}
891
+ </div>
892
+ </button>
893
+ {/each}
894
+ {:else}
895
+ <div class="p-6 text-center">
896
+ <div
897
+ class="bg-muted/50 mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full"
898
+ >
899
+ <span class="text-muted-foreground text-xl">📄</span>
900
+ </div>
901
+ <h3 class="mb-2 font-medium">No documents found</h3>
902
+ <p class="text-muted-foreground text-sm">
903
+ Create your first {selectedDocumentType} document using the + button above
904
+ </p>
905
+ </div>
906
+ {/if}
907
+ </div>
908
+ {/if}
909
+ </div>
910
+ {/if}
911
+
912
+ <!-- Primary Editor Panel -->
913
+ {#if primaryEditorState.visible}
914
+ {#if primaryEditorState.expanded}
915
+ <div
916
+ class="transition-all duration-200 {windowWidth < 620
917
+ ? 'w-screen'
918
+ : 'flex-1'} h-full overflow-y-auto"
919
+ style={windowWidth >= 620 ? 'min-width: 0;' : ''}
920
+ >
921
+ <DocumentEditor
922
+ {schemas}
923
+ documentType={selectedDocumentType!}
924
+ documentId={editingDocumentId}
925
+ isCreating={isCreatingDocument}
926
+ onBack={navigateBack}
927
+ onOpenReference={handleOpenReference}
928
+ onSaved={async (docId) => {
929
+ if (selectedDocumentType) {
930
+ await fetchDocuments(selectedDocumentType);
931
+ }
932
+ navigateToEditDocument(docId, selectedDocumentType!);
933
+ }}
934
+ onAutoSaved={handleAutoSave}
935
+ onPublished={async (docId) => {
936
+ if (selectedDocumentType) {
937
+ await fetchDocuments(selectedDocumentType);
938
+ }
939
+ }}
940
+ onDeleted={async () => {
941
+ if (selectedDocumentType) {
942
+ await fetchDocuments(selectedDocumentType);
943
+ const params = new SvelteURLSearchParams(page.url.searchParams);
944
+ params.set('docType', selectedDocumentType);
945
+ params.delete('docId');
946
+ params.delete('action');
947
+ await goto(`/admin?${params.toString()}`, { replaceState: false });
948
+ } else {
949
+ const orgId = page.url.searchParams.get('orgId');
950
+ const url = orgId ? `/admin?orgId=${orgId}` : '/admin';
951
+ await goto(url, { replaceState: false });
952
+ }
953
+ }}
954
+ {isReadOnly}
955
+ />
956
+ </div>
957
+ {:else}
958
+ <!-- Collapsed Primary Editor Strip -->
959
+ <button
960
+ onclick={() => setActiveEditor(0)}
961
+ class="hover:bg-muted/50 flex h-full w-[60px] flex-col border-l transition-colors"
962
+ title="Click to expand {selectedDocumentType}"
963
+ >
964
+ <div class="mt-7 flex flex-1 items-start justify-center p-2 pt-8 text-left">
965
+ <div class="text-foreground rotate-90 transform text-sm font-medium">
966
+ {selectedDocumentType
967
+ ? selectedDocumentType.charAt(0).toUpperCase() +
968
+ selectedDocumentType.slice(1)
969
+ : ''}
970
+ </div>
971
+ </div>
972
+ </button>
973
+ {/if}
974
+ {/if}
975
+
976
+ <!-- Stacked Reference Editors -->
977
+ {#each editorStack as stackedEditor, index (index)}
978
+ {@const editorIndex = index + 1}
979
+ {@const isExpanded = layoutConfig.expandedIndices.includes(editorIndex)}
980
+
981
+ {#if isExpanded}
982
+ <div
983
+ class="h-full flex-1 overflow-y-auto border-l transition-all duration-200"
984
+ style="min-width: 0;"
985
+ >
986
+ <DocumentEditor
987
+ {schemas}
988
+ documentType={stackedEditor.documentType}
989
+ documentId={stackedEditor.documentId}
990
+ isCreating={stackedEditor.isCreating}
991
+ onBack={() => handleCloseStackedEditor(index)}
992
+ onOpenReference={handleOpenReference}
993
+ onSaved={async (docId) => {}}
994
+ onAutoSaved={() => {}}
995
+ onPublished={async (docId) => {}}
996
+ onDeleted={async () => {
997
+ handleCloseStackedEditor(index);
998
+ }}
999
+ {isReadOnly}
1000
+ />
1001
+ </div>
1002
+ {:else}
1003
+ <!-- Collapsed Stacked Editor Strip -->
1004
+ <button
1005
+ onclick={() => setActiveEditor(editorIndex)}
1006
+ class="hover:bg-muted/50 flex h-full w-[60px] flex-col border-l transition-colors"
1007
+ title="Click to expand {stackedEditor.documentType}"
1008
+ >
1009
+ <div
1010
+ class="-mt-2 flex h-full flex-1 items-start justify-center p-2 pt-8 text-left"
1011
+ >
1012
+ <div
1013
+ class="text-foreground rotate-90 transform whitespace-nowrap text-sm font-medium"
1014
+ >
1015
+ {stackedEditor.documentType.charAt(0).toUpperCase() +
1016
+ stackedEditor.documentType.slice(1)}
1017
+ </div>
1018
+ </div>
1019
+ </button>
1020
+ {/if}
1021
+ {/each}
1022
+ {/if}
1023
+ </div>
1024
+ {/key}
1025
+ </Tabs.Content>
1026
+
1027
+ {#if graphqlSettings?.enableGraphiQL}
1028
+ <Tabs.Content value="vision" class="m-0 h-full p-0">
1029
+ <div class="bg-muted/10 flex h-full items-center justify-center">
1030
+ <div class="space-y-4 text-center">
1031
+ <div
1032
+ class="bg-primary/10 mx-auto flex h-16 w-16 items-center justify-center rounded-full"
1033
+ >
1034
+ <svg
1035
+ class="text-primary h-8 w-8"
1036
+ fill="none"
1037
+ viewBox="0 0 24 24"
1038
+ stroke="currentColor"
1039
+ >
1040
+ <path
1041
+ stroke-linecap="round"
1042
+ stroke-linejoin="round"
1043
+ stroke-width="2"
1044
+ d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"
1045
+ />
1046
+ </svg>
1047
+ </div>
1048
+
1049
+ <div>
1050
+ <h3 class="mb-2 text-lg font-semibold">GraphQL Playground</h3>
1051
+
1052
+ <p class="text-muted-foreground mb-4">Query your CMS data with the GraphQL API</p>
1053
+
1054
+ <a
1055
+ href={graphqlSettings.endpoint}
1056
+ target="_blank"
1057
+ class="bg-primary text-primary-foreground hover:bg-primary/90 inline-flex items-center gap-2 rounded-md px-4 py-2 transition-colors"
1058
+ >
1059
+ Open Playground
1060
+
1061
+ <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1062
+ <path
1063
+ stroke-linecap="round"
1064
+ stroke-linejoin="round"
1065
+ stroke-width="2"
1066
+ d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
1067
+ />
1068
+ </svg>
1069
+ </a>
1070
+ </div>
1071
+ </div>
1072
+ </div>
1073
+ </Tabs.Content>
1074
+ {/if}
1075
+ </Tabs.Root>
1076
+ </div>
1077
+ </div>