@aphexcms/cms-core 0.1.1 → 0.1.3

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