@kyro-cms/admin 0.1.7 → 0.1.9
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.
- package/package.json +7 -2
- package/src/components/Admin.tsx +1 -1
- package/src/components/AutoForm.tsx +966 -337
- package/src/components/CreateView.tsx +1 -1
- package/src/components/DetailView.tsx +1 -1
- package/src/components/EnhancedListView.tsx +156 -52
- package/src/components/ListView.tsx +1 -1
- package/src/components/Modal.tsx +65 -8
- package/src/components/Sidebar.astro +2 -2
- package/src/components/ThemeProvider.tsx +8 -2
- package/src/components/blocks/AccordionBlock.tsx +20 -52
- package/src/components/blocks/ArrayBlock.tsx +40 -31
- package/src/components/blocks/BlockEditModal.tsx +170 -581
- package/src/components/blocks/ButtonBlock.tsx +27 -128
- package/src/components/blocks/CodeBlock.tsx +88 -40
- package/src/components/blocks/ColumnsBlock.tsx +27 -85
- package/src/components/blocks/FileBlock.tsx +38 -39
- package/src/components/blocks/HeadingBlock.tsx +9 -31
- package/src/components/blocks/HeroBlock.tsx +42 -100
- package/src/components/blocks/ImageBlock.tsx +6 -7
- package/src/components/blocks/LinkBlock.tsx +27 -33
- package/src/components/blocks/ListBlock.tsx +47 -26
- package/src/components/blocks/RelationshipBlock.tsx +26 -233
- package/src/components/blocks/RichTextBlock.tsx +66 -0
- package/src/components/blocks/VStackBlock.tsx +23 -37
- package/src/components/blocks/VideoBlock.tsx +52 -32
- package/src/components/fields/AccordionField.tsx +213 -0
- package/src/components/fields/ArrayField.tsx +241 -0
- package/src/components/fields/BlocksField.tsx +5 -5
- package/src/components/fields/ButtonField.tsx +53 -0
- package/src/components/fields/CheckboxField.tsx +7 -3
- package/src/components/fields/ChildrenField.tsx +48 -0
- package/src/components/fields/CodeField.tsx +154 -94
- package/src/components/fields/ColumnsField.tsx +137 -0
- package/src/components/fields/DateField.tsx +9 -24
- package/src/components/fields/EditorClient.tsx +426 -160
- package/src/components/fields/HeadingField.tsx +31 -0
- package/src/components/fields/HeroField.tsx +101 -0
- package/src/components/fields/JSONField.tsx +7 -27
- package/src/components/fields/LinkField.tsx +81 -0
- package/src/components/fields/ListField.tsx +74 -0
- package/src/components/fields/MarkdownField.tsx +4 -26
- package/src/components/fields/NumberField.tsx +9 -27
- package/src/components/fields/PortableTextField.tsx +61 -49
- package/src/components/fields/RelationshipBlockField.tsx +233 -0
- package/src/components/fields/RelationshipField.tsx +59 -13
- package/src/components/fields/SelectField.tsx +6 -4
- package/src/components/fields/TextField.tsx +9 -24
- package/src/components/fields/UploadField.tsx +613 -0
- package/src/components/fields/VideoField.tsx +73 -0
- package/src/components/fields/extensions/blockComponents.tsx +11 -1
- package/src/components/fields/extensions/blocksStore.ts +1 -1
- package/src/components/fields/index.ts +12 -1
- package/src/components/layout/Layout.tsx +1 -1
- package/src/lib/api.ts +163 -0
- package/src/lib/config.ts +1 -1
- package/src/lib/dataStore.ts +87 -30
- package/src/lib/date-utils.ts +69 -0
- package/src/lib/db/version-adapter.ts +248 -0
- package/src/lib/i18n.tsx +353 -0
- package/src/lib/slugify.ts +15 -0
- package/src/lib/validation.ts +250 -0
- package/src/pages/api/[collection]/[id]/publish.ts +12 -4
- package/src/pages/api/[collection]/[id]/versions.ts +39 -9
- package/src/pages/api/[collection]/[id].ts +13 -1
- package/src/pages/api/[collection]/index.ts +5 -6
- package/src/styles/main.css +12 -2
- package/src/components/blocks/BlockEditModal.MARKER +0 -12
- package/src/components/fields/FileField.tsx +0 -390
- package/src/components/fields/HybridContentField.tsx +0 -109
- package/src/components/fields/ImageField.tsx +0 -429
|
@@ -1,429 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect, useRef } from "react";
|
|
2
|
-
|
|
3
|
-
interface ImageFieldProps {
|
|
4
|
-
field: any;
|
|
5
|
-
value: any;
|
|
6
|
-
onChange: (value: any) => void;
|
|
7
|
-
disabled?: boolean;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
interface MediaItem {
|
|
11
|
-
id: string;
|
|
12
|
-
filename: string;
|
|
13
|
-
url: string;
|
|
14
|
-
thumbnailUrl?: string;
|
|
15
|
-
mimeType: string;
|
|
16
|
-
title?: string;
|
|
17
|
-
folder?: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
interface MediaFolder {
|
|
21
|
-
name: string;
|
|
22
|
-
path: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function ImageField({
|
|
26
|
-
field,
|
|
27
|
-
value,
|
|
28
|
-
onChange,
|
|
29
|
-
disabled,
|
|
30
|
-
}: ImageFieldProps) {
|
|
31
|
-
const inputRef = useRef<HTMLInputElement>(null);
|
|
32
|
-
const urlInputRef = useRef<HTMLInputElement>(null);
|
|
33
|
-
const [uploading, setUploading] = useState(false);
|
|
34
|
-
const [showPicker, setShowPicker] = useState(false);
|
|
35
|
-
const [mediaItems, setMediaItems] = useState<MediaItem[]>([]);
|
|
36
|
-
const [folders, setFolders] = useState<MediaFolder[]>([]);
|
|
37
|
-
const [selectedFolder, setSelectedFolder] = useState<string>("");
|
|
38
|
-
const [mediaLoading, setMediaLoading] = useState(false);
|
|
39
|
-
const [pickerSearch, setPickerSearch] = useState("");
|
|
40
|
-
const [showUrlInput, setShowUrlInput] = useState(false);
|
|
41
|
-
const [urlValue, setUrlValue] = useState("");
|
|
42
|
-
const [urlError, setUrlError] = useState("");
|
|
43
|
-
|
|
44
|
-
const fieldLabel = field?.label || field?.name || "Image";
|
|
45
|
-
const maxCount = field.maxCount || 1;
|
|
46
|
-
const isMultiple = maxCount > 1;
|
|
47
|
-
const currentValue = Array.isArray(value) ? value : value ? [value] : [];
|
|
48
|
-
const canAddMore = currentValue.length < maxCount;
|
|
49
|
-
|
|
50
|
-
useEffect(() => {
|
|
51
|
-
if (showPicker) {
|
|
52
|
-
loadFolders();
|
|
53
|
-
loadMedia();
|
|
54
|
-
}
|
|
55
|
-
}, [showPicker, selectedFolder]);
|
|
56
|
-
|
|
57
|
-
const loadFolders = async () => {
|
|
58
|
-
try {
|
|
59
|
-
const resp = await fetch("/api/media/folders?t=" + Date.now(), {
|
|
60
|
-
credentials: "include",
|
|
61
|
-
});
|
|
62
|
-
const result = await resp.json();
|
|
63
|
-
setFolders(result.folders || []);
|
|
64
|
-
} catch {
|
|
65
|
-
setFolders([]);
|
|
66
|
-
}
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
const loadMedia = async () => {
|
|
70
|
-
setMediaLoading(true);
|
|
71
|
-
try {
|
|
72
|
-
let url = `/api/media?limit=60&sortBy=createdAt&sortDir=desc&t=${Date.now()}`;
|
|
73
|
-
if (selectedFolder) {
|
|
74
|
-
url += "&folder=" + encodeURIComponent(selectedFolder);
|
|
75
|
-
}
|
|
76
|
-
const resp = await fetch(url, { credentials: "include" });
|
|
77
|
-
const result = await resp.json();
|
|
78
|
-
setMediaItems(result.docs || []);
|
|
79
|
-
} catch {
|
|
80
|
-
setMediaItems([]);
|
|
81
|
-
} finally {
|
|
82
|
-
setMediaLoading(false);
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
const uploadFile = async (file: File) => {
|
|
87
|
-
setUploading(true);
|
|
88
|
-
try {
|
|
89
|
-
const formData = new FormData();
|
|
90
|
-
formData.append("file", file);
|
|
91
|
-
if (selectedFolder) {
|
|
92
|
-
formData.append("folder", selectedFolder);
|
|
93
|
-
}
|
|
94
|
-
const resp = await fetch("/api/upload", {
|
|
95
|
-
method: "POST",
|
|
96
|
-
body: formData,
|
|
97
|
-
credentials: "include",
|
|
98
|
-
});
|
|
99
|
-
if (!resp.ok) throw new Error("Upload failed");
|
|
100
|
-
const result = await resp.json();
|
|
101
|
-
const newImage = {
|
|
102
|
-
id: result.id,
|
|
103
|
-
filename: result.filename,
|
|
104
|
-
originalName: result.originalName ?? file.name,
|
|
105
|
-
url: result.url,
|
|
106
|
-
mimeType: file.type,
|
|
107
|
-
};
|
|
108
|
-
if (isMultiple) {
|
|
109
|
-
onChange([...currentValue, newImage]);
|
|
110
|
-
} else {
|
|
111
|
-
onChange(newImage);
|
|
112
|
-
}
|
|
113
|
-
} catch (err) {
|
|
114
|
-
console.error("Upload failed:", err);
|
|
115
|
-
} finally {
|
|
116
|
-
setUploading(false);
|
|
117
|
-
}
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
const addByUrl = async () => {
|
|
121
|
-
const url = urlValue.trim();
|
|
122
|
-
if (!url) return;
|
|
123
|
-
|
|
124
|
-
setUrlError("");
|
|
125
|
-
try {
|
|
126
|
-
const resp = await fetch("/api/upload", {
|
|
127
|
-
method: "POST",
|
|
128
|
-
headers: { "Content-Type": "application/json" },
|
|
129
|
-
body: JSON.stringify({ url }),
|
|
130
|
-
credentials: "include",
|
|
131
|
-
});
|
|
132
|
-
if (!resp.ok) {
|
|
133
|
-
const data = await resp.json();
|
|
134
|
-
throw new Error(data.error || "Failed to add URL");
|
|
135
|
-
}
|
|
136
|
-
const result = await resp.json();
|
|
137
|
-
const originalName = (() => {
|
|
138
|
-
try {
|
|
139
|
-
return (
|
|
140
|
-
new URL(url).pathname.split("/").pop() ||
|
|
141
|
-
result.originalName ||
|
|
142
|
-
"url-image"
|
|
143
|
-
);
|
|
144
|
-
} catch {
|
|
145
|
-
return result.originalName || "url-image";
|
|
146
|
-
}
|
|
147
|
-
})();
|
|
148
|
-
const newImage = {
|
|
149
|
-
id: result.id,
|
|
150
|
-
filename: result.filename,
|
|
151
|
-
originalName,
|
|
152
|
-
url: result.url,
|
|
153
|
-
mimeType: result.mimeType || "image/*",
|
|
154
|
-
};
|
|
155
|
-
if (isMultiple) {
|
|
156
|
-
onChange([...currentValue, newImage]);
|
|
157
|
-
} else {
|
|
158
|
-
onChange(newImage);
|
|
159
|
-
}
|
|
160
|
-
setUrlValue("");
|
|
161
|
-
setShowUrlInput(false);
|
|
162
|
-
} catch (err: any) {
|
|
163
|
-
setUrlError(err.message || "Invalid URL");
|
|
164
|
-
}
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
const selectFromLibrary = (item: MediaItem) => {
|
|
168
|
-
const newImage = {
|
|
169
|
-
id: item.id,
|
|
170
|
-
filename: item.filename,
|
|
171
|
-
url: item.url,
|
|
172
|
-
mimeType: item.mimeType,
|
|
173
|
-
};
|
|
174
|
-
if (isMultiple) {
|
|
175
|
-
onChange([...currentValue, newImage]);
|
|
176
|
-
} else {
|
|
177
|
-
onChange(newImage);
|
|
178
|
-
}
|
|
179
|
-
setShowPicker(false);
|
|
180
|
-
setPickerSearch("");
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
const removeImage = (index: number) => {
|
|
184
|
-
const newValue = [...currentValue];
|
|
185
|
-
newValue.splice(index, 1);
|
|
186
|
-
onChange(isMultiple ? newValue : newValue[0] || null);
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
const filteredMedia = mediaItems.filter(
|
|
190
|
-
(item) =>
|
|
191
|
-
!pickerSearch ||
|
|
192
|
-
item.filename?.toLowerCase().includes(pickerSearch.toLowerCase()) ||
|
|
193
|
-
item.title?.toLowerCase().includes(pickerSearch.toLowerCase()),
|
|
194
|
-
);
|
|
195
|
-
|
|
196
|
-
if (uploading) {
|
|
197
|
-
return (
|
|
198
|
-
<div className="text-xs text-[var(--kyro-text-muted)] p-2">
|
|
199
|
-
Uploading...
|
|
200
|
-
</div>
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const renderImagePreview = (img: any, index?: number) => {
|
|
205
|
-
const isImage = img?.url?.match(/\.(jpe?g|png|gif|webp|avif|svg)(\?|$)/i);
|
|
206
|
-
return (
|
|
207
|
-
<div
|
|
208
|
-
key={index}
|
|
209
|
-
className="flex items-center gap-2 p-2 bg-[var(--kyro-surface-accent)] rounded-lg"
|
|
210
|
-
>
|
|
211
|
-
{isImage && (
|
|
212
|
-
<img
|
|
213
|
-
src={img.url}
|
|
214
|
-
alt={img.filename || "Image"}
|
|
215
|
-
className="w-10 h-10 object-cover rounded border border-[var(--kyro-border)]"
|
|
216
|
-
/>
|
|
217
|
-
)}
|
|
218
|
-
<div className="flex-1 min-w-0">
|
|
219
|
-
<div className="text-xs truncate text-[var(--kyro-text-primary)] overflow-hidden text-ellipsis whitespace-nowrap">
|
|
220
|
-
{img?.originalName || img?.filename || "Image"}
|
|
221
|
-
</div>
|
|
222
|
-
<button
|
|
223
|
-
type="button"
|
|
224
|
-
onClick={() =>
|
|
225
|
-
index !== undefined ? removeImage(index) : onChange(null)
|
|
226
|
-
}
|
|
227
|
-
className="text-xs text-red-600 hover:text-red-700 bg-transparent border-none cursor-pointer p-0"
|
|
228
|
-
>
|
|
229
|
-
Remove
|
|
230
|
-
</button>
|
|
231
|
-
</div>
|
|
232
|
-
</div>
|
|
233
|
-
);
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
if (value) {
|
|
237
|
-
return (
|
|
238
|
-
<div className="space-y-2">
|
|
239
|
-
{isMultiple ? (
|
|
240
|
-
<div className="grid grid-cols-2 gap-2">
|
|
241
|
-
{currentValue.map((img: any, i: number) =>
|
|
242
|
-
renderImagePreview(img, i),
|
|
243
|
-
)}
|
|
244
|
-
{canAddMore && (
|
|
245
|
-
<button
|
|
246
|
-
type="button"
|
|
247
|
-
onClick={() => inputRef.current?.click()}
|
|
248
|
-
disabled={disabled}
|
|
249
|
-
className="flex items-center justify-center h-12 border-2 border-dashed border-[var(--kyro-border)] rounded-lg text-sm text-[var(--kyro-text-secondary)] hover:border-[var(--kyro-border-active)] cursor-pointer transition-colors"
|
|
250
|
-
>
|
|
251
|
-
+ Add {fieldLabel}
|
|
252
|
-
</button>
|
|
253
|
-
)}
|
|
254
|
-
</div>
|
|
255
|
-
) : (
|
|
256
|
-
renderImagePreview(value)
|
|
257
|
-
)}
|
|
258
|
-
<input
|
|
259
|
-
ref={inputRef}
|
|
260
|
-
type="file"
|
|
261
|
-
accept="image/*"
|
|
262
|
-
onChange={(e) => {
|
|
263
|
-
const file = e.target.files?.[0];
|
|
264
|
-
if (file) uploadFile(file);
|
|
265
|
-
}}
|
|
266
|
-
disabled={disabled}
|
|
267
|
-
className="hidden"
|
|
268
|
-
/>
|
|
269
|
-
</div>
|
|
270
|
-
);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
return (
|
|
274
|
-
<div className="space-y-2">
|
|
275
|
-
<input
|
|
276
|
-
ref={inputRef}
|
|
277
|
-
type="file"
|
|
278
|
-
accept="image/*"
|
|
279
|
-
onChange={(e) => {
|
|
280
|
-
const file = e.target.files?.[0];
|
|
281
|
-
if (file) uploadFile(file);
|
|
282
|
-
}}
|
|
283
|
-
disabled={disabled}
|
|
284
|
-
className="hidden"
|
|
285
|
-
/>
|
|
286
|
-
<div className="flex gap-2 flex-wrap">
|
|
287
|
-
<button
|
|
288
|
-
type="button"
|
|
289
|
-
onClick={() => inputRef.current?.click()}
|
|
290
|
-
disabled={disabled}
|
|
291
|
-
className="px-3 py-1.5 text-xs rounded border border-dashed border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] cursor-pointer hover:border-[var(--kyro-border-active)] transition-colors"
|
|
292
|
-
>
|
|
293
|
-
+ Upload {fieldLabel}
|
|
294
|
-
</button>
|
|
295
|
-
<button
|
|
296
|
-
type="button"
|
|
297
|
-
onClick={() => setShowPicker(true)}
|
|
298
|
-
disabled={disabled}
|
|
299
|
-
className="px-3 py-1.5 text-xs rounded border border-[var(--kyro-border)] bg-[var(--kyro-surface)] text-[var(--kyro-text-secondary)] cursor-pointer hover:border-[var(--kyro-border-active)] transition-colors"
|
|
300
|
-
>
|
|
301
|
-
Library
|
|
302
|
-
</button>
|
|
303
|
-
<button
|
|
304
|
-
type="button"
|
|
305
|
-
onClick={() => setShowUrlInput(!showUrlInput)}
|
|
306
|
-
disabled={disabled}
|
|
307
|
-
className="px-3 py-1.5 text-xs rounded border border-[var(--kyro-border)] bg-[var(--kyro-surface)] text-[var(--kyro-text-secondary)] cursor-pointer hover:border-[var(--kyro-border-active)] transition-colors"
|
|
308
|
-
>
|
|
309
|
-
URL
|
|
310
|
-
</button>
|
|
311
|
-
</div>
|
|
312
|
-
|
|
313
|
-
{showUrlInput && (
|
|
314
|
-
<div className="flex gap-2 items-center">
|
|
315
|
-
<input
|
|
316
|
-
ref={urlInputRef}
|
|
317
|
-
type="url"
|
|
318
|
-
placeholder="https://example.com/image.jpg"
|
|
319
|
-
value={urlValue}
|
|
320
|
-
onChange={(e) => {
|
|
321
|
-
setUrlValue(e.target.value);
|
|
322
|
-
setUrlError("");
|
|
323
|
-
}}
|
|
324
|
-
onKeyDown={(e) => e.key === "Enter" && addByUrl()}
|
|
325
|
-
disabled={disabled}
|
|
326
|
-
className="flex-1 px-2 py-1.5 text-xs rounded border border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-primary)]"
|
|
327
|
-
/>
|
|
328
|
-
<button
|
|
329
|
-
type="button"
|
|
330
|
-
onClick={addByUrl}
|
|
331
|
-
disabled={disabled || !urlValue.trim()}
|
|
332
|
-
className="px-3 py-1.5 text-xs rounded bg-[var(--kyro-primary)] text-white cursor-pointer hover:opacity-90 transition-opacity disabled:opacity-50"
|
|
333
|
-
>
|
|
334
|
-
Add
|
|
335
|
-
</button>
|
|
336
|
-
{urlError && <span className="text-xs text-red-600">{urlError}</span>}
|
|
337
|
-
</div>
|
|
338
|
-
)}
|
|
339
|
-
|
|
340
|
-
{showPicker && (
|
|
341
|
-
<div className="absolute z-50 w-[360px] max-h-[400px] overflow-hidden bg-[var(--kyro-surface)] border border-[var(--kyro-border)] rounded-lg shadow-lg mt-1 flex flex-col">
|
|
342
|
-
<div className="p-2 border-b border-[var(--kyro-border)] flex flex-col gap-2">
|
|
343
|
-
<input
|
|
344
|
-
type="text"
|
|
345
|
-
placeholder="Search media..."
|
|
346
|
-
value={pickerSearch}
|
|
347
|
-
onChange={(e) => setPickerSearch(e.target.value)}
|
|
348
|
-
className="w-full px-2 py-1.5 text-xs rounded border border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-primary)]"
|
|
349
|
-
/>
|
|
350
|
-
{folders.length > 0 && (
|
|
351
|
-
<div className="flex gap-1 flex-wrap">
|
|
352
|
-
<button
|
|
353
|
-
type="button"
|
|
354
|
-
onClick={() => setSelectedFolder("")}
|
|
355
|
-
className={`px-2 py-1 text-xs rounded transition-colors ${
|
|
356
|
-
selectedFolder === ""
|
|
357
|
-
? "bg-[var(--kyro-primary)] text-white"
|
|
358
|
-
: "bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-border)]"
|
|
359
|
-
}`}
|
|
360
|
-
>
|
|
361
|
-
All
|
|
362
|
-
</button>
|
|
363
|
-
{folders.slice(0, 6).map((folder) => (
|
|
364
|
-
<button
|
|
365
|
-
key={folder.path}
|
|
366
|
-
type="button"
|
|
367
|
-
onClick={() => setSelectedFolder(folder.path)}
|
|
368
|
-
className={`px-2 py-1 text-xs rounded transition-colors ${
|
|
369
|
-
selectedFolder === folder.path
|
|
370
|
-
? "bg-[var(--kyro-primary)] text-white"
|
|
371
|
-
: "bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-border)]"
|
|
372
|
-
}`}
|
|
373
|
-
>
|
|
374
|
-
{folder.name}
|
|
375
|
-
</button>
|
|
376
|
-
))}
|
|
377
|
-
</div>
|
|
378
|
-
)}
|
|
379
|
-
</div>
|
|
380
|
-
<div className="flex-1 overflow-auto p-2">
|
|
381
|
-
{mediaLoading ? (
|
|
382
|
-
<div className="text-center py-5 text-xs text-[var(--kyro-text-muted)]">
|
|
383
|
-
Loading...
|
|
384
|
-
</div>
|
|
385
|
-
) : filteredMedia.length === 0 ? (
|
|
386
|
-
<div className="text-center py-5 text-xs text-[var(--kyro-text-muted)]">
|
|
387
|
-
No media found
|
|
388
|
-
</div>
|
|
389
|
-
) : (
|
|
390
|
-
<div className="grid grid-cols-3 gap-1">
|
|
391
|
-
{filteredMedia.map((item) => (
|
|
392
|
-
<button
|
|
393
|
-
key={item.id}
|
|
394
|
-
type="button"
|
|
395
|
-
onClick={() => selectFromLibrary(item)}
|
|
396
|
-
className="border border-[var(--kyro-border)] rounded overflow-hidden cursor-pointer p-0 bg-none hover:border-[var(--kyro-primary)] transition-colors relative group"
|
|
397
|
-
>
|
|
398
|
-
<img
|
|
399
|
-
src={item.thumbnailUrl || item.url}
|
|
400
|
-
alt={item.filename}
|
|
401
|
-
className="w-full h-[80px] object-cover"
|
|
402
|
-
/>
|
|
403
|
-
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
|
|
404
|
-
<span className="text-white text-xs px-1 text-center truncate">
|
|
405
|
-
{item.filename}
|
|
406
|
-
</span>
|
|
407
|
-
</div>
|
|
408
|
-
</button>
|
|
409
|
-
))}
|
|
410
|
-
</div>
|
|
411
|
-
)}
|
|
412
|
-
</div>
|
|
413
|
-
<div className="p-2 border-t border-[var(--kyro-border)] flex justify-between items-center">
|
|
414
|
-
<span className="text-xs text-[var(--kyro-text-muted)]">
|
|
415
|
-
{filteredMedia.length} items
|
|
416
|
-
</span>
|
|
417
|
-
<button
|
|
418
|
-
type="button"
|
|
419
|
-
onClick={() => setShowPicker(false)}
|
|
420
|
-
className="text-xs text-[var(--kyro-text-secondary)] bg-transparent border-none cursor-pointer hover:text-[var(--kyro-text-primary)]"
|
|
421
|
-
>
|
|
422
|
-
Close
|
|
423
|
-
</button>
|
|
424
|
-
</div>
|
|
425
|
-
</div>
|
|
426
|
-
)}
|
|
427
|
-
</div>
|
|
428
|
-
);
|
|
429
|
-
}
|