@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,284 +0,0 @@
1
- <script lang="ts">
2
- import { Button } from '@aphexcms/ui/shadcn/button';
3
- import { Trash2, Upload, Image as ImageIcon, FileImage } from 'lucide-svelte';
4
- import type { ImageValue } from '../../../types/asset.js';
5
- import type { ImageField as ImageFieldType } from '../../../types/schemas.js';
6
- import { assets } from '../../../api/assets';
7
-
8
- interface Props {
9
- field: ImageFieldType;
10
- value: ImageValue | null;
11
- validationClasses?: string;
12
- onUpdate: (value: ImageValue | null) => void;
13
- schemaType?: string;
14
- fieldPath?: string;
15
- readonly?: boolean;
16
- }
17
-
18
- let {
19
- field,
20
- value,
21
- onUpdate,
22
- validationClasses,
23
- schemaType,
24
- fieldPath,
25
- readonly = false
26
- }: Props = $props();
27
-
28
- // Component state
29
- let isDragging = $state(false);
30
- let isUploading = $state(false);
31
- let uploadError = $state<string | null>(null);
32
- let fileInputRef: HTMLInputElement;
33
-
34
- // Upload file to server
35
- async function uploadFile(file: File): Promise<ImageValue | null> {
36
- isUploading = true;
37
- uploadError = null;
38
-
39
- try {
40
- const formData = new FormData();
41
- formData.append('file', file);
42
-
43
- // Add field metadata for privacy checking
44
- if (schemaType) formData.append('schemaType', schemaType);
45
- if (fieldPath) formData.append('fieldPath', fieldPath);
46
-
47
- const result = await assets.upload(formData);
48
-
49
- if (!result.success) {
50
- throw new Error(result.error || 'Upload failed');
51
- }
52
-
53
- // Extract asset from response
54
- const asset = result.data;
55
-
56
- // Return Sanity-style image value
57
- return {
58
- _type: 'image',
59
- asset: {
60
- _type: 'reference',
61
- _ref: asset!.id
62
- }
63
- };
64
- } catch (error) {
65
- uploadError = error instanceof Error ? error.message : 'Upload failed';
66
- return null;
67
- } finally {
68
- isUploading = false;
69
- }
70
- }
71
-
72
- // Handle file selection
73
- async function handleFileSelect(files: FileList | null) {
74
- if (readonly || !files || files.length === 0) return;
75
-
76
- const file = files[0];
77
-
78
- const imageValue = await uploadFile(file);
79
- if (imageValue) {
80
- onUpdate(imageValue);
81
- }
82
- }
83
-
84
- // Drag and drop handlers
85
- function handleDragOver(event: DragEvent) {
86
- if (readonly) return;
87
- event.preventDefault();
88
- isDragging = true;
89
- }
90
-
91
- function handleDragLeave(event: DragEvent) {
92
- if (readonly) return;
93
- event.preventDefault();
94
- isDragging = false;
95
- }
96
-
97
- function handleDrop(event: DragEvent) {
98
- if (readonly) return;
99
- event.preventDefault();
100
- isDragging = false;
101
- handleFileSelect(event.dataTransfer?.files || null);
102
- }
103
-
104
- // File input handlers
105
- function handleFileInputChange(event: Event) {
106
- if (readonly) return;
107
- const target = event.target as HTMLInputElement;
108
- handleFileSelect(target.files);
109
- }
110
-
111
- function openFileDialog() {
112
- if (readonly) return;
113
- fileInputRef?.click();
114
- }
115
-
116
- // Remove image
117
- function removeImage() {
118
- if (readonly) return;
119
- onUpdate(null);
120
- uploadError = null;
121
- }
122
-
123
- // Asset data state
124
- let assetData = $state<any>(null);
125
- let loadingAsset = $state(false);
126
-
127
- // Fetch asset details when asset reference changes
128
- $effect(() => {
129
- async function loadAsset() {
130
- if (value?.asset?._ref) {
131
- loadingAsset = true;
132
- try {
133
- const result = await assets.getById(value.asset._ref);
134
- if (result.success) {
135
- assetData = result.data;
136
- } else {
137
- console.error('Failed to fetch asset details');
138
- assetData = null;
139
- }
140
- } catch (error) {
141
- console.error('Error fetching asset:', error);
142
- assetData = null;
143
- } finally {
144
- loadingAsset = false;
145
- }
146
- } else {
147
- assetData = null;
148
- }
149
- }
150
- loadAsset();
151
- });
152
-
153
- // Get asset URL for preview
154
- const previewUrl = $derived(assetData?.url || null);
155
- </script>
156
-
157
- <!-- Hidden file input -->
158
- <input
159
- bind:this={fileInputRef}
160
- type="file"
161
- accept={field.accept || 'image/*'}
162
- style="display: none"
163
- onchange={handleFileInputChange}
164
- />
165
-
166
- {#if value && value.asset}
167
- <!-- Image preview with controls -->
168
- <div class="border-border overflow-hidden rounded-md border {validationClasses}">
169
- <div class="group relative">
170
- <!-- Image preview (Sanity-style aspect ratio ~2.75:1) -->
171
- <div class="bg-muted flex items-center justify-center" style="aspect-ratio: 2.75 / 1;">
172
- {#if loadingAsset}
173
- <div class="text-muted-foreground flex flex-col items-center gap-2">
174
- <div class="border-primary h-8 w-8 animate-spin rounded-full border-b-2"></div>
175
- <span class="text-sm">Loading image...</span>
176
- </div>
177
- {:else if previewUrl}
178
- <img
179
- src={previewUrl}
180
- alt={assetData?.alt || 'Uploaded image'}
181
- class="h-full w-full object-contain"
182
- loading="lazy"
183
- />
184
- {:else}
185
- <div class="text-muted-foreground flex flex-col items-center gap-2">
186
- <ImageIcon size={32} />
187
- <span class="text-sm">Image: {value.asset._ref}</span>
188
- <span class="text-xs">Failed to load preview</span>
189
- </div>
190
- {/if}
191
- </div>
192
-
193
- <!-- Overlay controls (hidden for read-only) -->
194
- {#if !readonly}
195
- <div
196
- class="absolute inset-0 flex items-center justify-center gap-2 bg-black/50 opacity-0 transition-opacity group-hover:opacity-100"
197
- >
198
- <Button variant="secondary" size="sm" onclick={openFileDialog} disabled={isUploading}>
199
- <Upload size={16} class="mr-1" />
200
- Replace
201
- </Button>
202
- <Button variant="destructive" size="sm" onclick={removeImage} disabled={isUploading}>
203
- <Trash2 size={16} class="mr-1" />
204
- Remove
205
- </Button>
206
- </div>
207
- {/if}
208
- </div>
209
-
210
- <!-- Additional image controls/metadata could go here -->
211
- {#if field.fields}
212
- <div class="border-border space-y-2 border-t p-3">
213
- <!-- Custom fields like caption, alt text, etc. would be rendered here -->
214
- <p class="text-muted-foreground text-xs">Custom fields coming soon...</p>
215
- </div>
216
- {/if}
217
- </div>
218
- {:else}
219
- <!-- Sanity-style upload bar -->
220
- <div class="border-border overflow-hidden rounded-md border {validationClasses}">
221
- <div class="flex items-center">
222
- <!-- Drag and drop area (left side) -->
223
- <div
224
- class="flex-1 px-4 py-3 transition-colors {readonly
225
- ? ''
226
- : isDragging
227
- ? 'bg-primary/5'
228
- : 'hover:bg-muted/50'}"
229
- ondragover={readonly ? undefined : handleDragOver}
230
- ondragleave={readonly ? undefined : handleDragLeave}
231
- ondrop={readonly ? undefined : handleDrop}
232
- tabindex={readonly ? -1 : 0}
233
- role={readonly ? undefined : 'button'}
234
- >
235
- {#if isUploading}
236
- <div class="flex items-center gap-3">
237
- <div class="border-primary h-5 w-5 animate-spin rounded-full border-b-2"></div>
238
- <span class="text-muted-foreground text-sm">Uploading...</span>
239
- </div>
240
- {:else}
241
- <div class="flex items-center gap-3">
242
- <FileImage size={20} class="text-muted-foreground" />
243
- <span class="text-muted-foreground text-sm">
244
- {readonly ? 'No image' : isDragging ? 'Drop image here' : 'Drag or paste image here'}
245
- </span>
246
- </div>
247
- {/if}
248
- </div>
249
-
250
- <!-- Buttons (right side) -->
251
- <div class="border-border bg-muted/20 flex items-center gap-2 border-l px-3 py-2">
252
- <Button
253
- variant="outline"
254
- size="sm"
255
- onclick={openFileDialog}
256
- disabled={isUploading || readonly}
257
- type="button"
258
- >
259
- <Upload size={16} class="mr-1" />
260
- Upload
261
- </Button>
262
-
263
- <Button
264
- variant="outline"
265
- size="sm"
266
- disabled={isUploading || readonly}
267
- type="button"
268
- onclick={() => {
269
- // TODO: Open asset browser/selector
270
- console.log('Open asset selector');
271
- }}
272
- >
273
- <ImageIcon size={16} class="mr-1" />
274
- Select
275
- </Button>
276
- </div>
277
- </div>
278
- </div>
279
- {/if}
280
-
281
- <!-- Error display -->
282
- {#if uploadError}
283
- <p class="text-destructive mt-2 text-sm">{uploadError}</p>
284
- {/if}
@@ -1,82 +0,0 @@
1
- <script lang="ts">
2
- import { Input } from '@aphexcms/ui/shadcn/input';
3
- import type { Field } from '../../../types/schemas.js';
4
-
5
- interface Props {
6
- field: Field;
7
- value: number | null;
8
- onUpdate: (value: number | null) => void;
9
- validationClasses?: string;
10
- onBlur?: (event: any) => void;
11
- onFocus?: (event: any) => void;
12
- readonly?: boolean;
13
- }
14
-
15
- let {
16
- field,
17
- value,
18
- onUpdate,
19
- validationClasses,
20
- onBlur,
21
- onFocus,
22
- readonly = false
23
- }: Props = $props();
24
-
25
- // Convert value to string for input, handle null/undefined
26
- let inputValue = $derived(value?.toString() || '');
27
-
28
- function handleInput(event: Event) {
29
- const target = event.target as HTMLInputElement;
30
- const newValue = target.value;
31
-
32
- // Convert to number and update
33
- if (newValue === '') {
34
- onUpdate(null);
35
- } else {
36
- const numValue = parseFloat(newValue);
37
- if (!isNaN(numValue)) {
38
- onUpdate(numValue);
39
- }
40
- }
41
- }
42
-
43
- function handleKeydown(event: KeyboardEvent) {
44
- // Allow: backspace, delete, tab, escape, enter
45
- if (
46
- [8, 9, 27, 13, 46].includes(event.keyCode) ||
47
- // Allow: Ctrl+A, Ctrl+C, Ctrl+V, Ctrl+X
48
- (event.keyCode === 65 && event.ctrlKey) ||
49
- (event.keyCode === 67 && event.ctrlKey) ||
50
- (event.keyCode === 86 && event.ctrlKey) ||
51
- (event.keyCode === 88 && event.ctrlKey) ||
52
- // Allow: home, end, left, right
53
- (event.keyCode >= 35 && event.keyCode <= 39)
54
- ) {
55
- return;
56
- }
57
-
58
- // Ensure that it's a number or decimal point and stop the keypress
59
- if (
60
- (event.shiftKey || event.keyCode < 48 || event.keyCode > 57) &&
61
- (event.keyCode < 96 || event.keyCode > 105) &&
62
- event.keyCode !== 190 &&
63
- event.keyCode !== 110
64
- ) {
65
- event.preventDefault();
66
- }
67
- }
68
- </script>
69
-
70
- <Input
71
- id={field.name}
72
- type="number"
73
- step="any"
74
- placeholder={field.description || `Enter ${field.title?.toLowerCase() || 'number'}`}
75
- value={inputValue}
76
- oninput={handleInput}
77
- onkeydown={handleKeydown}
78
- onblur={onBlur}
79
- onfocus={onFocus}
80
- class={validationClasses}
81
- disabled={readonly}
82
- />
@@ -1,260 +0,0 @@
1
- <script lang="ts">
2
- import CheckIcon from '@lucide/svelte/icons/check';
3
- import ChevronsUpDownIcon from '@lucide/svelte/icons/chevrons-up-down';
4
- import PlusIcon from '@lucide/svelte/icons/plus';
5
- import XIcon from '@lucide/svelte/icons/x';
6
- import { tick } from 'svelte';
7
- import * as Command from '@aphexcms/ui/shadcn/command';
8
- import * as Popover from '@aphexcms/ui/shadcn/popover';
9
- import { Button } from '@aphexcms/ui/shadcn/button';
10
- import { cn } from '@aphexcms/ui/utils';
11
- import type { Field, ReferenceField as ReferenceFieldType } from '../../../types/schemas.js';
12
- import { documents } from '../../../api/documents.js';
13
-
14
- interface Props {
15
- field: Field;
16
- value: string | null; // Document ID
17
- onUpdate: (value: string | null) => void;
18
- onOpenReference?: (documentId: string, documentType: string) => void;
19
- readonly?: boolean;
20
- }
21
-
22
- let { field, value, onUpdate, onOpenReference, readonly = false }: Props = $props();
23
-
24
- // Cast to reference field type
25
- const referenceField = field as ReferenceFieldType;
26
- const targetType = referenceField.to?.[0]?.type;
27
-
28
- // State
29
- let open = $state(false);
30
- let searchResults = $state<any[]>([]);
31
- let selectedDocument = $state<any>(null);
32
- let loading = $state(false);
33
- let creating = $state(false);
34
- let triggerRef = $state<HTMLButtonElement>(null!);
35
-
36
- // Load selected document details when value changes
37
- $effect(() => {
38
- async function loadDocument() {
39
- if (value) {
40
- try {
41
- const doc = await documents.getById(value);
42
- if (doc.success) {
43
- selectedDocument = doc.data;
44
- }
45
- } catch (err) {
46
- console.error('Failed to load referenced document:', err);
47
- selectedDocument = null;
48
- }
49
- } else {
50
- selectedDocument = null;
51
- }
52
- }
53
- loadDocument();
54
- });
55
-
56
- // Load documents when dropdown opens
57
- $effect(() => {
58
- async function loadDocuments() {
59
- if (open && targetType) {
60
- console.log('[ReferenceField] Opening select box, loading documents for type:', targetType);
61
- loading = true;
62
- try {
63
- const result = await documents.list({
64
- docType: targetType,
65
- limit: 10
66
- });
67
- console.log('[ReferenceField] Documents loaded:', result);
68
- if (result.success && result.data) {
69
- searchResults = result.data;
70
- console.log('[ReferenceField] Search results:', searchResults.length, 'documents');
71
- }
72
- } catch (err) {
73
- console.error('[ReferenceField] Failed to load documents:', err);
74
- searchResults = [];
75
- } finally {
76
- loading = false;
77
- }
78
- }
79
- }
80
- loadDocuments();
81
- });
82
-
83
- function closeAndFocusTrigger() {
84
- open = false;
85
- tick().then(() => {
86
- triggerRef?.focus();
87
- });
88
- }
89
-
90
- function selectDocument(doc: any) {
91
- if (readonly) return;
92
- onUpdate(doc.id);
93
- closeAndFocusTrigger();
94
- }
95
-
96
- function clearSelection() {
97
- if (readonly) return;
98
- onUpdate(null);
99
- selectedDocument = null;
100
- }
101
-
102
- function openReference() {
103
- if (selectedDocument && targetType && onOpenReference) {
104
- onOpenReference(selectedDocument.id, targetType);
105
- }
106
- }
107
-
108
- async function createNewDocument() {
109
- if (readonly || !targetType) return;
110
-
111
- creating = true;
112
- try {
113
- const result = await documents.create({
114
- type: targetType,
115
- draftData: {
116
- title: 'Untitled'
117
- }
118
- });
119
-
120
- if (result.success && result.data) {
121
- onUpdate(result.data.id);
122
- closeAndFocusTrigger();
123
- }
124
- } catch (err) {
125
- console.error('Failed to create document:', err);
126
- } finally {
127
- creating = false;
128
- }
129
- }
130
-
131
- function getDocumentTitle(doc: any): string {
132
- // Try to get title from draft data first, then published data
133
- const data = doc.draftData || doc.publishedData || {};
134
- return data.title || data.name || data.heading || 'Untitled';
135
- }
136
-
137
- const selectedLabel = $derived(selectedDocument ? getDocumentTitle(selectedDocument) : null);
138
- </script>
139
-
140
- {#if selectedDocument}
141
- <!-- Selected document display -->
142
- <div class="border-border bg-muted/30 flex items-center gap-2 rounded-md border p-3">
143
- <div class="flex-1">
144
- <div class="text-sm font-medium">{getDocumentTitle(selectedDocument)}</div>
145
- <div class="text-muted-foreground text-xs">
146
- {targetType} • {selectedDocument.status === 'published' ? '🟢' : '🟡'}
147
- {selectedDocument.status}
148
- </div>
149
- </div>
150
- <Button
151
- variant="ghost"
152
- size="sm"
153
- onclick={openReference}
154
- class="text-muted-foreground hover:text-foreground h-8 w-8 p-0"
155
- title="Edit referenced document"
156
- >
157
- <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
158
- <path
159
- stroke-linecap="round"
160
- stroke-linejoin="round"
161
- stroke-width="2"
162
- d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
163
- />
164
- </svg>
165
- </Button>
166
- {#if !readonly}
167
- <Button
168
- variant="ghost"
169
- size="sm"
170
- onclick={clearSelection}
171
- class="text-muted-foreground hover:text-destructive h-8 w-8 p-0"
172
- title="Clear selection"
173
- >
174
- <XIcon class="h-4 w-4" />
175
- </Button>
176
- {/if}
177
- </div>
178
- {:else}
179
- <!-- Search/select interface -->
180
- {#if readonly}
181
- <!-- Read-only state: show placeholder -->
182
- <div
183
- class="border-input bg-muted/50 flex h-10 w-full items-center justify-between rounded-md border px-3 py-2 text-sm"
184
- >
185
- <span class="text-muted-foreground">No reference selected</span>
186
- </div>
187
- {:else}
188
- <Popover.Root bind:open>
189
- <Popover.Trigger bind:ref={triggerRef}>
190
- {#snippet child({ props })}
191
- <Button
192
- {...props}
193
- variant="outline"
194
- class="w-full justify-between"
195
- role="combobox"
196
- aria-expanded={open}
197
- >
198
- {selectedLabel || `Select ${targetType}...`}
199
- <ChevronsUpDownIcon class="opacity-50" />
200
- </Button>
201
- {/snippet}
202
- </Popover.Trigger>
203
- <Popover.Content class="!z-[9999] w-[400px] p-0">
204
- <Command.Root>
205
- <Command.List>
206
- {#if loading}
207
- <Command.Loading>Loading...</Command.Loading>
208
- {:else if searchResults.length === 0}
209
- <Command.Empty>
210
- <div class="flex flex-col items-center gap-2 py-4">
211
- <p class="text-muted-foreground text-sm">
212
- No {targetType}s found
213
- </p>
214
- <Button size="sm" onclick={createNewDocument} disabled={creating} class="gap-1">
215
- <PlusIcon class="h-3 w-3" />
216
- {creating ? 'Creating...' : `Create new ${targetType}`}
217
- </Button>
218
- </div>
219
- </Command.Empty>
220
- {:else if searchResults.length > 0}
221
- <Command.Group>
222
- {#each searchResults as doc (doc.id)}
223
- <Command.Item
224
- value={doc.id}
225
- onSelect={() => selectDocument(doc)}
226
- class="flex items-center justify-between"
227
- >
228
- <div class="flex items-center gap-2">
229
- <CheckIcon class={cn('h-4 w-4', value !== doc.id && 'text-transparent')} />
230
- <div>
231
- <div class="text-sm font-medium">{getDocumentTitle(doc)}</div>
232
- <div class="text-muted-foreground text-xs">
233
- {doc.status === 'published' ? '🟢' : '🟡'}
234
- {doc.status}
235
- </div>
236
- </div>
237
- </div>
238
- </Command.Item>
239
- {/each}
240
- </Command.Group>
241
- <Command.Separator />
242
- <Command.Group>
243
- <Command.Item onSelect={createNewDocument} class="justify-center">
244
- <div class="flex items-center gap-1">
245
- <PlusIcon class="h-3 w-3" />
246
- {creating ? 'Creating...' : `Create new ${targetType}`}
247
- </div>
248
- </Command.Item>
249
- </Command.Group>
250
- {:else}
251
- <Command.Empty>
252
- No {targetType}s available
253
- </Command.Empty>
254
- {/if}
255
- </Command.List>
256
- </Command.Root>
257
- </Popover.Content>
258
- </Popover.Root>
259
- {/if}
260
- {/if}
@@ -1,74 +0,0 @@
1
- <script lang="ts">
2
- import { Input } from '@aphexcms/ui/shadcn/input';
3
- import { Button } from '@aphexcms/ui/shadcn/button';
4
- import type { Field } from '../../../types/schemas.js';
5
- import { generateSlug } from '../../../utils/index.js';
6
-
7
- interface Props {
8
- field: Field;
9
- value: any;
10
- documentData?: Record<string, any>;
11
- onUpdate: (value: any) => void;
12
- validationClasses?: string;
13
- onBlur?: (event: any) => void;
14
- onFocus?: (event: any) => void;
15
- readonly?: boolean;
16
- }
17
-
18
- let {
19
- field,
20
- value,
21
- documentData,
22
- onUpdate,
23
- validationClasses,
24
- onBlur,
25
- onFocus,
26
- readonly = false
27
- }: Props = $props();
28
-
29
- function handleInputChange(event: Event) {
30
- const target = event.target as HTMLInputElement;
31
- onUpdate(target.value);
32
- }
33
-
34
- // Generate slug from title
35
- function generateSlugFromTitle() {
36
- if (documentData?.title) {
37
- const generatedSlug = generateSlug(documentData.title);
38
- onUpdate(generatedSlug);
39
- }
40
- }
41
- </script>
42
-
43
- <div class="space-y-2">
44
- <div class="flex gap-2">
45
- <Input
46
- id={field.name}
47
- value={value || ''}
48
- placeholder="document-slug"
49
- oninput={handleInputChange}
50
- onblur={onBlur}
51
- onfocus={onFocus}
52
- class="flex-1 {validationClasses}"
53
- disabled={readonly}
54
- />
55
- <Button
56
- variant="outline"
57
- size="sm"
58
- onclick={generateSlugFromTitle}
59
- disabled={!documentData?.title || readonly}
60
- class="shrink-0"
61
- >
62
- Generate from Title
63
- </Button>
64
- </div>
65
- {#if documentData?.title}
66
- <p class="text-muted-foreground text-xs">
67
- Click "Generate from Title" to create slug from: "{documentData.title}"
68
- </p>
69
- {:else}
70
- <p class="text-muted-foreground text-xs">
71
- Enter a title first to generate a slug automatically
72
- </p>
73
- {/if}
74
- </div>