@kyro-cms/admin 0.8.0 → 0.9.1
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/dist/index.cjs +11960 -11006
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +67 -65
- package/dist/index.css.map +1 -1
- package/dist/index.d.cts +563 -0
- package/dist/index.d.ts +7 -7
- package/dist/index.js +12183 -11238
- package/dist/index.js.map +1 -1
- package/package.json +15 -11
- package/src/components/ActionBar.tsx +27 -14
- package/src/components/Admin.tsx +1 -1
- package/src/components/ApiKeysManager.tsx +5 -5
- package/src/components/AutoForm.tsx +585 -369
- package/src/components/BrandingHub.tsx +7 -4
- package/src/components/CreateView.tsx +2 -0
- package/src/components/DetailView.tsx +71 -56
- package/src/components/DeveloperCenter.tsx +8 -6
- package/src/components/FieldRenderer.tsx +94 -19
- package/src/components/ListView.tsx +33 -20
- package/src/components/MediaGallery.tsx +219 -194
- package/src/components/PluginsManager.tsx +197 -70
- package/src/components/RestPlayground.tsx +7 -7
- package/src/components/SessionsManager.tsx +1 -1
- package/src/components/SettingsPage.tsx +22 -0
- package/src/components/Sidebar.astro +13 -41
- package/src/components/UserManagement.tsx +153 -15
- package/src/components/UserMenu.tsx +30 -4
- package/src/components/VersionHistoryPanel.tsx +112 -119
- package/src/components/WebhookManager.tsx +6 -4
- package/src/components/blocks/ArrayBlock.tsx +6 -23
- package/src/components/blocks/BlockEditModal.tsx +82 -309
- package/src/components/blocks/CardBlock.tsx +35 -0
- package/src/components/blocks/ChildBlocksTree.tsx +57 -31
- package/src/components/blocks/GenericBlock.tsx +44 -0
- package/src/components/blocks/HeadingSubheadingBlock.tsx +32 -0
- package/src/components/blocks/HeroBlock.tsx +5 -14
- package/src/components/blocks/RichTextBlock.tsx +5 -5
- package/src/components/blocks/index.ts +5 -3
- package/src/components/fields/AccordionField.tsx +2 -2
- package/src/components/fields/ArrayField.tsx +1 -1
- package/src/components/fields/ArrayLayout.tsx +120 -29
- package/src/components/fields/BlocksField.tsx +430 -50
- package/src/components/fields/CardField.tsx +73 -0
- package/src/components/fields/CheckboxField.tsx +7 -3
- package/src/components/fields/DateField.tsx +4 -1
- package/src/components/fields/GroupLayout.tsx +2 -2
- package/src/components/fields/HeadingSubheadingField.tsx +43 -0
- package/src/components/fields/ListField.tsx +2 -2
- package/src/components/fields/NumberField.tsx +4 -1
- package/src/components/fields/RelationshipField.tsx +153 -87
- package/src/components/fields/RichTextField.tsx +781 -0
- package/src/components/fields/SecretField.tsx +102 -0
- package/src/components/fields/SelectField.tsx +19 -6
- package/src/components/fields/TabsLayout.tsx +19 -9
- package/src/components/fields/TextField.tsx +4 -1
- package/src/components/fields/UploadField.tsx +122 -56
- package/src/components/fields/extensions/blockComponents.tsx +103 -174
- package/src/components/fields/extensions/blocksStore.ts +8 -1
- package/src/components/fields/index.ts +4 -2
- package/src/components/ui/PageHeader.tsx +5 -5
- package/src/components/ui/SlidePanel.tsx +8 -3
- package/src/components/ui/icons.tsx +109 -109
- package/src/components/users/UserDetail.tsx +79 -16
- package/src/hooks/useAutoFormState.ts +125 -62
- package/src/integration.ts +148 -46
- package/src/kyro-cms.d.ts +7 -2
- package/src/layouts/AuthLayout.astro +14 -2
- package/src/lib/autoform-store.ts +85 -52
- package/src/lib/change-source.ts +9 -0
- package/src/lib/config.ts +104 -8
- package/src/lib/globals.ts +44 -9
- package/src/lib/normalize-upload-fields.ts +41 -0
- package/src/lib/paths.ts +2 -2
- package/src/lib/resolve-field-value.ts +110 -0
- package/src/lib/shim/use-sync-external-store-with-selector.js +30 -0
- package/src/lib/shim/use-sync-external-store.js +1 -0
- package/src/lib/stores/index.ts +1 -0
- package/src/lib/useResourceManager.ts +4 -4
- package/src/lib/vite-shim-plugin.ts +100 -0
- package/src/pages/[collection]/[id].astro +1 -1
- package/src/pages/preview/[collection]/[id].astro +4 -4
- package/src/pages/settings/[slug].astro +2 -2
- package/src/styles/main.css +60 -54
- package/README.md +0 -46
- package/dist/EditorClient-Q23UXR37.cjs +0 -468
- package/dist/EditorClient-Q23UXR37.cjs.map +0 -1
- package/dist/EditorClient-T5PASFNR.js +0 -466
- package/dist/EditorClient-T5PASFNR.js.map +0 -1
- package/dist/chunk-3BGDYKTD.cjs +0 -348
- package/dist/chunk-3BGDYKTD.cjs.map +0 -1
- package/dist/chunk-EEFXLQVT.js +0 -3
- package/dist/chunk-EEFXLQVT.js.map +0 -1
- package/src/components/blocks/ButtonBlock.tsx +0 -64
- package/src/components/blocks/ColumnsBlock.tsx +0 -55
- package/src/components/blocks/DividerBlock.tsx +0 -43
- package/src/components/blocks/LinkBlock.tsx +0 -65
- package/src/components/blocks/VStackBlock.tsx +0 -29
- package/src/components/fields/EditorClient.tsx +0 -535
- package/src/components/fields/PortableTextField.tsx +0 -155
- package/src/components/fields/PortableTextRenderer.tsx +0 -68
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
withCacheBust,
|
|
39
39
|
apiUpload,
|
|
40
40
|
} from "../lib/api";
|
|
41
|
-
import { useAuthStore, useUIStore } from "../lib/stores";
|
|
41
|
+
import { useAuthStore, useUIStore, toast } from "../lib/stores";
|
|
42
42
|
|
|
43
43
|
interface MediaItem {
|
|
44
44
|
id: string;
|
|
@@ -57,7 +57,8 @@ interface MediaItem {
|
|
|
57
57
|
updatedAt?: string;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
function getAbsoluteUrl(relativeUrl:
|
|
60
|
+
function getAbsoluteUrl(relativeUrl: unknown): string {
|
|
61
|
+
if (typeof relativeUrl !== "string" || !relativeUrl) return "";
|
|
61
62
|
if (typeof window === "undefined") return relativeUrl;
|
|
62
63
|
// Remote URLs and blob URLs are returned as-is
|
|
63
64
|
if (relativeUrl.startsWith("http") || relativeUrl.startsWith("blob:")) {
|
|
@@ -115,10 +116,12 @@ function getFileType(mimeType: string): FilterType {
|
|
|
115
116
|
|
|
116
117
|
export function MediaGallery({
|
|
117
118
|
onSelect,
|
|
118
|
-
multiple =
|
|
119
|
+
multiple = true,
|
|
120
|
+
pickerMode = false,
|
|
119
121
|
}: {
|
|
120
122
|
onSelect?: (items: MediaItem[]) => void;
|
|
121
123
|
multiple?: boolean;
|
|
124
|
+
pickerMode?: boolean;
|
|
122
125
|
}) {
|
|
123
126
|
const { permissions } = useAuthStore();
|
|
124
127
|
const canUpload = permissions?.media?.create !== false;
|
|
@@ -201,15 +204,16 @@ export function MediaGallery({
|
|
|
201
204
|
}, []);
|
|
202
205
|
|
|
203
206
|
useEffect(() => {
|
|
204
|
-
checkStorage();
|
|
205
|
-
}, [checkStorage]);
|
|
207
|
+
if (!pickerMode) checkStorage();
|
|
208
|
+
}, [checkStorage, pickerMode]);
|
|
206
209
|
|
|
207
210
|
useEffect(() => {
|
|
211
|
+
if (pickerMode) return;
|
|
208
212
|
if (storageConfigured === false && !storageChecked) {
|
|
209
213
|
setStorageChecked(true);
|
|
210
214
|
setShowStorageConfigModal(true);
|
|
211
215
|
}
|
|
212
|
-
}, [storageConfigured, storageChecked]);
|
|
216
|
+
}, [pickerMode, storageConfigured, storageChecked]);
|
|
213
217
|
|
|
214
218
|
useEffect(() => {
|
|
215
219
|
loadMedia();
|
|
@@ -220,6 +224,7 @@ export function MediaGallery({
|
|
|
220
224
|
}, [loadFolders]);
|
|
221
225
|
|
|
222
226
|
useEffect(() => {
|
|
227
|
+
if (pickerMode) return;
|
|
223
228
|
const handlePaste = (e: ClipboardEvent) => {
|
|
224
229
|
const files = e.clipboardData?.files;
|
|
225
230
|
if (files && files.length > 0) {
|
|
@@ -228,7 +233,7 @@ export function MediaGallery({
|
|
|
228
233
|
};
|
|
229
234
|
window.addEventListener("paste", handlePaste);
|
|
230
235
|
return () => window.removeEventListener("paste", handlePaste);
|
|
231
|
-
}, [currentFolder, storageConfigured]);
|
|
236
|
+
}, [pickerMode, currentFolder, storageConfigured]);
|
|
232
237
|
|
|
233
238
|
const handleUpload = async (files: FileList | File[]) => {
|
|
234
239
|
if (!storageConfigured) {
|
|
@@ -237,7 +242,8 @@ export function MediaGallery({
|
|
|
237
242
|
}
|
|
238
243
|
|
|
239
244
|
setUploading(true);
|
|
240
|
-
|
|
245
|
+
let successCount = 0;
|
|
246
|
+
let failCount = 0;
|
|
241
247
|
|
|
242
248
|
for (let i = 0; i < files.length; i++) {
|
|
243
249
|
const file = files[i];
|
|
@@ -252,9 +258,10 @@ export function MediaGallery({
|
|
|
252
258
|
[file.name]: progress,
|
|
253
259
|
}));
|
|
254
260
|
});
|
|
261
|
+
successCount++;
|
|
255
262
|
} catch (error) {
|
|
256
263
|
console.error(`Upload failed for ${file.name}:`, error);
|
|
257
|
-
|
|
264
|
+
failCount++;
|
|
258
265
|
}
|
|
259
266
|
}
|
|
260
267
|
|
|
@@ -262,6 +269,12 @@ export function MediaGallery({
|
|
|
262
269
|
setUploadProgress({});
|
|
263
270
|
loadMedia();
|
|
264
271
|
loadFolders();
|
|
272
|
+
if (failCount > 0) {
|
|
273
|
+
toast.error(`${failCount} file(s) failed to upload`);
|
|
274
|
+
}
|
|
275
|
+
if (successCount > 0) {
|
|
276
|
+
toast.success(`${successCount} file(s) uploaded successfully`);
|
|
277
|
+
}
|
|
265
278
|
};
|
|
266
279
|
|
|
267
280
|
const handleBulkDelete = () => {
|
|
@@ -276,9 +289,10 @@ export function MediaGallery({
|
|
|
276
289
|
}
|
|
277
290
|
setSelectedIds(new Set());
|
|
278
291
|
loadMedia();
|
|
292
|
+
toast.success(`${selectedIds.size} item(s) deleted`);
|
|
279
293
|
} catch (error) {
|
|
280
294
|
console.error("Bulk delete failed:", error);
|
|
281
|
-
|
|
295
|
+
toast.error("Failed to delete some items");
|
|
282
296
|
}
|
|
283
297
|
}
|
|
284
298
|
});
|
|
@@ -303,6 +317,7 @@ export function MediaGallery({
|
|
|
303
317
|
setShowNewFolderModal(false);
|
|
304
318
|
} catch (error) {
|
|
305
319
|
console.error("Failed to create folder:", error);
|
|
320
|
+
toast.error("Failed to create folder");
|
|
306
321
|
}
|
|
307
322
|
};
|
|
308
323
|
|
|
@@ -320,7 +335,7 @@ export function MediaGallery({
|
|
|
320
335
|
loadMedia();
|
|
321
336
|
} catch (error) {
|
|
322
337
|
console.error("Failed to delete folder:", error);
|
|
323
|
-
|
|
338
|
+
toast.error("Failed to delete folder");
|
|
324
339
|
}
|
|
325
340
|
}
|
|
326
341
|
});
|
|
@@ -396,6 +411,7 @@ export function MediaGallery({
|
|
|
396
411
|
}
|
|
397
412
|
} catch (err) {
|
|
398
413
|
console.error("Crop failed:", err);
|
|
414
|
+
toast.error("Crop failed");
|
|
399
415
|
} finally {
|
|
400
416
|
setUploading(false);
|
|
401
417
|
}
|
|
@@ -414,34 +430,31 @@ export function MediaGallery({
|
|
|
414
430
|
return (
|
|
415
431
|
<div
|
|
416
432
|
className={`flex flex-col h-full bg-[var(--kyro-bg)] transition-all duration-300 ${isDragging ? "ring-4 ring-[var(--kyro-sidebar-active)] ring-inset" : ""}`}
|
|
417
|
-
|
|
418
|
-
e.preventDefault();
|
|
419
|
-
setIsDragging(
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
onDrop={(e) => {
|
|
423
|
-
e.preventDefault();
|
|
424
|
-
setIsDragging(false);
|
|
425
|
-
if (e.dataTransfer.files) handleUpload(e.dataTransfer.files);
|
|
426
|
-
}}
|
|
433
|
+
{...(pickerMode ? {} : {
|
|
434
|
+
onDragOver: (e) => { e.preventDefault(); setIsDragging(true); },
|
|
435
|
+
onDragLeave: () => setIsDragging(false),
|
|
436
|
+
onDrop: (e) => { e.preventDefault(); setIsDragging(false); if (e.dataTransfer.files) handleUpload(e.dataTransfer.files); },
|
|
437
|
+
})}
|
|
427
438
|
>
|
|
428
439
|
{/* Top Bar */}
|
|
429
|
-
<div className=
|
|
430
|
-
|
|
431
|
-
<div>
|
|
432
|
-
<
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
<
|
|
437
|
-
|
|
438
|
-
|
|
440
|
+
<div className={`flex flex-col lg:flex-row lg:items-center justify-between gap-6 border-b border-[var(--kyro-border)] backdrop-blur-md sticky top-0 z-40 ${pickerMode ? "p-2" : "p-6 rounded-xl surface-tile"}`}>
|
|
441
|
+
{!pickerMode && (
|
|
442
|
+
<div className="flex items-center gap-4">
|
|
443
|
+
<div>
|
|
444
|
+
<h2 className="text-xl font-bold tracking-tighter text-[var(--kyro-text-primary)]">
|
|
445
|
+
Media Library
|
|
446
|
+
</h2>
|
|
447
|
+
<div className="flex items-center gap-3 mt-1">
|
|
448
|
+
<span className="text-[10px] font-bold tracking-widest text-[var(--kyro-text-secondary)] opacity-50">
|
|
449
|
+
{total} Items · {formatFileSize(stats.totalSize)}
|
|
450
|
+
</span>
|
|
451
|
+
</div>
|
|
439
452
|
</div>
|
|
440
453
|
</div>
|
|
441
|
-
|
|
454
|
+
)}
|
|
442
455
|
|
|
443
|
-
<div className=
|
|
444
|
-
<div className="relative group flex-1 min-w-[
|
|
456
|
+
<div className={`flex items-center gap-3 flex-wrap lg:flex-nowrap ${pickerMode ? "w-full" : ""}`}>
|
|
457
|
+
<div className="relative group flex-1 min-w-[200px]">
|
|
445
458
|
<svg
|
|
446
459
|
className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--kyro-text-secondary)] opacity-40 group-focus-within:opacity-100 transition-opacity"
|
|
447
460
|
fill="none"
|
|
@@ -464,117 +477,123 @@ export function MediaGallery({
|
|
|
464
477
|
/>
|
|
465
478
|
</div>
|
|
466
479
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
480
|
+
{!pickerMode && (
|
|
481
|
+
<>
|
|
482
|
+
<div className="flex bg-[var(--kyro-surface-accent)] p-1 rounded-xl border border-[var(--kyro-border)]">
|
|
483
|
+
<button
|
|
484
|
+
onClick={() => setView("grid")}
|
|
485
|
+
className={`p-2 rounded-lg transition-all ${view === "grid" ? "bg-[var(--kyro-surface)] shadow-sm text-[var(--kyro-text-primary)]" : "text-[var(--kyro-text-secondary)] opacity-50 hover:opacity-100"}`}
|
|
486
|
+
>
|
|
487
|
+
<Grid className="w-4 h-4" />
|
|
488
|
+
</button>
|
|
489
|
+
<button
|
|
490
|
+
onClick={() => setView("list")}
|
|
491
|
+
className={`p-2 rounded-lg transition-all ${view === "list" ? "bg-[var(--kyro-surface)] shadow-sm text-[var(--kyro-text-primary)]" : "text-[var(--kyro-text-secondary)] opacity-50 hover:opacity-100"}`}
|
|
492
|
+
>
|
|
493
|
+
<FileIcon className="w-4 h-4" />
|
|
494
|
+
</button>
|
|
495
|
+
</div>
|
|
481
496
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
497
|
+
{canUpload && (
|
|
498
|
+
<button
|
|
499
|
+
onClick={() => fileInputRef.current?.click()}
|
|
500
|
+
className="flex items-center gap-2 px-6 py-2.5 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-xl font-bold text-xs shadow-lg active:scale-95 transition-all"
|
|
501
|
+
>
|
|
502
|
+
<Maximize2 className="w-4 h-4" />
|
|
503
|
+
Upload
|
|
504
|
+
</button>
|
|
505
|
+
)}
|
|
506
|
+
</>
|
|
490
507
|
)}
|
|
491
508
|
</div>
|
|
492
509
|
</div>
|
|
493
510
|
|
|
494
511
|
<div className="flex flex-1 min-h-0 overflow-hidden">
|
|
495
512
|
{/* Folders Sidebar */}
|
|
496
|
-
|
|
497
|
-
<div className="
|
|
498
|
-
<div>
|
|
499
|
-
<
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
+
{!pickerMode && (
|
|
514
|
+
<div className="w-64 border-r border-[var(--kyro-border)] surface-tile mt-6 overflow-y-auto hidden md:block">
|
|
515
|
+
<div className="p-6 space-y-6">
|
|
516
|
+
<div>
|
|
517
|
+
<span className="text-[10px] font-bold tracking-[0.2em] text-[var(--kyro-text-secondary)] opacity-40 block mb-4">
|
|
518
|
+
Quick Filters
|
|
519
|
+
</span>
|
|
520
|
+
<div className="space-y-1">
|
|
521
|
+
{(
|
|
522
|
+
[
|
|
523
|
+
"all",
|
|
524
|
+
"image",
|
|
525
|
+
"video",
|
|
526
|
+
"audio",
|
|
527
|
+
"document",
|
|
528
|
+
"archive",
|
|
529
|
+
] as const
|
|
530
|
+
).map((t) => (
|
|
531
|
+
<button
|
|
532
|
+
key={t}
|
|
533
|
+
onClick={() => setFilter(t)}
|
|
534
|
+
className={`w-full flex items-center gap-3 px-4 py-2 rounded-xl text-[11px] font-bold capitalize transition-all ${filter === t ? "text-[var(--kyro-text-primary)] bg-[var(--kyro-surface-accent)]" : "text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]/50"}`}
|
|
535
|
+
>
|
|
536
|
+
<span
|
|
537
|
+
className={`w-1.5 h-1.5 rounded-full ${filter === t ? "bg-[var(--kyro-primary)]" : "bg-transparent border border-current opacity-30"}`}
|
|
538
|
+
/>
|
|
539
|
+
{t}
|
|
540
|
+
</button>
|
|
541
|
+
))}
|
|
542
|
+
</div>
|
|
543
|
+
</div>
|
|
544
|
+
|
|
545
|
+
<div className="pt-6 border-t border-[var(--kyro-border)]">
|
|
546
|
+
<div className="flex items-center justify-between mb-4">
|
|
547
|
+
<span className="text-[10px] font-bold tracking-[0.2em] text-[var(--kyro-text-secondary)] opacity-40">
|
|
548
|
+
Folders
|
|
549
|
+
</span>
|
|
513
550
|
<button
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
className={`w-full flex items-center gap-3 px-4 py-2 rounded-xl text-[11px] font-bold capitalize transition-all ${filter === t ? "text-[var(--kyro-text-primary)] bg-[var(--kyro-surface-accent)]" : "text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]/50"}`}
|
|
551
|
+
onClick={() => setShowNewFolderModal(true)}
|
|
552
|
+
className="p-1.5 hover:bg-[var(--kyro-surface-accent)] rounded-lg transition-colors text-[var(--kyro-text-primary)]"
|
|
517
553
|
>
|
|
518
|
-
<
|
|
519
|
-
className={`w-1.5 h-1.5 rounded-full ${filter === t ? "bg-[var(--kyro-primary)]" : "bg-transparent border border-current opacity-30"}`}
|
|
520
|
-
/>
|
|
521
|
-
{t}
|
|
554
|
+
<FolderPlus className="w-4 h-4" />
|
|
522
555
|
</button>
|
|
523
|
-
|
|
524
|
-
</div>
|
|
525
|
-
</div>
|
|
556
|
+
</div>
|
|
526
557
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
558
|
+
<nav className="space-y-1">
|
|
559
|
+
<button
|
|
560
|
+
onClick={() => setCurrentFolder("")}
|
|
561
|
+
className={`w-full flex items-center gap-3 px-4 py-2.5 rounded-xl text-xs font-bold transition-all ${currentFolder === "" ? "bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] shadow-md" : "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] hover:text-[var(--kyro-text-primary)]"}`}
|
|
562
|
+
>
|
|
563
|
+
<FolderInput className="w-4 h-4 opacity-70" />
|
|
564
|
+
All Assets
|
|
565
|
+
</button>
|
|
566
|
+
{folders.map((folder) => (
|
|
567
|
+
<div key={folder} className="group relative">
|
|
568
|
+
<button
|
|
569
|
+
onClick={() => setCurrentFolder(folder)}
|
|
570
|
+
className={`w-full flex items-center gap-3 px-4 py-2.5 rounded-xl text-xs font-bold transition-all ${currentFolder === folder ? "bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] shadow-md" : "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] hover:text-[var(--kyro-text-primary)]"}`}
|
|
571
|
+
>
|
|
572
|
+
<div className="w-4 h-4 flex items-center justify-center opacity-70">
|
|
573
|
+
<Folder fill={currentFolder === folder ? "currentColor" : "none"} />
|
|
574
|
+
</div>
|
|
575
|
+
{folder}
|
|
576
|
+
</button>
|
|
577
|
+
<button
|
|
578
|
+
onClick={(e) => {
|
|
579
|
+
e.stopPropagation();
|
|
580
|
+
handleDeleteFolder(folder);
|
|
581
|
+
}}
|
|
582
|
+
className="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 text-red-500 opacity-0 group-hover:opacity-100 transition-all hover:bg-red-50 rounded-lg"
|
|
583
|
+
>
|
|
584
|
+
<Trash2 className="w-3.5 h-3.5" />
|
|
585
|
+
</button>
|
|
586
|
+
</div>
|
|
587
|
+
))}
|
|
588
|
+
</nav>
|
|
538
589
|
</div>
|
|
539
|
-
|
|
540
|
-
<nav className="space-y-1">
|
|
541
|
-
<button
|
|
542
|
-
onClick={() => setCurrentFolder("")}
|
|
543
|
-
className={`w-full flex items-center gap-3 px-4 py-2.5 rounded-xl text-xs font-bold transition-all ${currentFolder === "" ? "bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] shadow-md" : "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] hover:text-[var(--kyro-text-primary)]"}`}
|
|
544
|
-
>
|
|
545
|
-
<FolderInput className="w-4 h-4 opacity-70" />
|
|
546
|
-
All Assets
|
|
547
|
-
</button>
|
|
548
|
-
{folders.map((folder) => (
|
|
549
|
-
<div key={folder} className="group relative">
|
|
550
|
-
<button
|
|
551
|
-
onClick={() => setCurrentFolder(folder)}
|
|
552
|
-
className={`w-full flex items-center gap-3 px-4 py-2.5 rounded-xl text-xs font-bold transition-all ${currentFolder === folder ? "bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] shadow-md" : "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] hover:text-[var(--kyro-text-primary)]"}`}
|
|
553
|
-
>
|
|
554
|
-
<div className="w-4 h-4 flex items-center justify-center opacity-70">
|
|
555
|
-
<Folder fill={currentFolder === folder ? "currentColor" : "none"} />
|
|
556
|
-
</div>
|
|
557
|
-
{folder}
|
|
558
|
-
</button>
|
|
559
|
-
<button
|
|
560
|
-
onClick={(e) => {
|
|
561
|
-
e.stopPropagation();
|
|
562
|
-
handleDeleteFolder(folder);
|
|
563
|
-
}}
|
|
564
|
-
className="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 text-red-500 opacity-0 group-hover:opacity-100 transition-all hover:bg-red-50 rounded-lg"
|
|
565
|
-
>
|
|
566
|
-
<Trash2 className="w-3.5 h-3.5" />
|
|
567
|
-
</button>
|
|
568
|
-
</div>
|
|
569
|
-
))}
|
|
570
|
-
</nav>
|
|
571
590
|
</div>
|
|
572
591
|
</div>
|
|
573
|
-
|
|
592
|
+
)}
|
|
574
593
|
|
|
575
594
|
{/* Main Content Area */}
|
|
576
595
|
<div className="flex-1 flex flex-col min-h-0 bg-[var(--kyro-bg)]">
|
|
577
|
-
<div className=
|
|
596
|
+
<div className={`flex-1 overflow-y-auto custom-scrollbar ${pickerMode ? "px-2 py-4" : "py-8 px-4"}`}>
|
|
578
597
|
{loading ? (
|
|
579
598
|
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
|
580
599
|
<Shimmer variant="media-card" count={12} />
|
|
@@ -591,7 +610,7 @@ export function MediaGallery({
|
|
|
591
610
|
Upload your first file or create a folder to organize your
|
|
592
611
|
media assets.
|
|
593
612
|
</p>
|
|
594
|
-
{canUpload && (
|
|
613
|
+
{!pickerMode && canUpload && (
|
|
595
614
|
<button
|
|
596
615
|
onClick={() => fileInputRef.current?.click()}
|
|
597
616
|
className="mt-8 px-8 py-3 bg-[var(--kyro-text-primary)] text-[var(--kyro-bg)] rounded-2xl font-bold text-xs hover:scale-105 transition-all shadow-xl"
|
|
@@ -648,7 +667,7 @@ export function MediaGallery({
|
|
|
648
667
|
<div className="flex gap-1">
|
|
649
668
|
<button
|
|
650
669
|
onClick={(e) => handleSelectOne(item.id, e)}
|
|
651
|
-
className={`p-1.5 rounded-lg transition-all ${selectedIds.has(item.id) ? "
|
|
670
|
+
className={`kyro-btn-primary p-1.5 rounded-lg transition-all ${selectedIds.has(item.id) ? "" : "bg-white/10 text-white hover:bg-white/20"}`}
|
|
652
671
|
>
|
|
653
672
|
<svg
|
|
654
673
|
className="w-3 h-3"
|
|
@@ -770,7 +789,7 @@ export function MediaGallery({
|
|
|
770
789
|
e.stopPropagation();
|
|
771
790
|
handleSelectOne(item.id, e);
|
|
772
791
|
}}
|
|
773
|
-
className={`p-2 rounded-lg transition-all ${selectedIds.has(item.id) ? "
|
|
792
|
+
className={`kyro-btn-primary p-2 rounded-lg transition-all ${selectedIds.has(item.id) ? "" : "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] opacity-0 group-hover:opacity-100"}`}
|
|
774
793
|
>
|
|
775
794
|
<Grid className="w-4 h-4" />
|
|
776
795
|
</button>
|
|
@@ -811,7 +830,7 @@ export function MediaGallery({
|
|
|
811
830
|
</div>
|
|
812
831
|
|
|
813
832
|
{/* Upload Banner */}
|
|
814
|
-
{uploading && (
|
|
833
|
+
{!pickerMode && uploading && (
|
|
815
834
|
<div className="fixed bottom-12 left-1/2 -translate-x-1/2 z-[60] w-full max-w-lg">
|
|
816
835
|
<div className="bg-[var(--kyro-surface)] border border-[var(--kyro-border)] rounded-[2rem] shadow-2xl p-6 ring-1 ring-white/10 animate-in slide-in-from-bottom-12 duration-700">
|
|
817
836
|
<div className="flex items-center justify-between mb-4">
|
|
@@ -882,7 +901,7 @@ export function MediaGallery({
|
|
|
882
901
|
Confirm Selection
|
|
883
902
|
</button>
|
|
884
903
|
)}
|
|
885
|
-
{canDelete && (
|
|
904
|
+
{!pickerMode && canDelete && (
|
|
886
905
|
<button
|
|
887
906
|
onClick={handleBulkDelete}
|
|
888
907
|
className="p-3 bg-red-500/10 text-red-500 hover:bg-red-500 hover:text-white rounded-full transition-all active:scale-90"
|
|
@@ -974,7 +993,7 @@ export function MediaGallery({
|
|
|
974
993
|
navigator.clipboard.writeText(
|
|
975
994
|
getAbsoluteUrl(panelItem.url),
|
|
976
995
|
);
|
|
977
|
-
|
|
996
|
+
toast.success("URL copied to clipboard");
|
|
978
997
|
}}
|
|
979
998
|
className="p-3 bg-[var(--kyro-surface-accent)] hover:bg-[var(--kyro-border)] border border-[var(--kyro-border)] rounded-xl transition-all"
|
|
980
999
|
>
|
|
@@ -1016,52 +1035,54 @@ export function MediaGallery({
|
|
|
1016
1035
|
</div>
|
|
1017
1036
|
</div>
|
|
1018
1037
|
|
|
1019
|
-
|
|
1020
|
-
<
|
|
1021
|
-
onClick={() => {
|
|
1022
|
-
const link = document.createElement("a");
|
|
1023
|
-
link.href = getAbsoluteUrl(panelItem.url);
|
|
1024
|
-
link.download = panelItem.filename;
|
|
1025
|
-
link.click();
|
|
1026
|
-
}}
|
|
1027
|
-
className="flex-1 flex items-center justify-center gap-2 px-6 py-3 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-xl font-bold text-xs shadow-lg hover:opacity-90 transition-all"
|
|
1028
|
-
>
|
|
1029
|
-
<Download className="w-4 h-4" />
|
|
1030
|
-
Download
|
|
1031
|
-
</button>
|
|
1032
|
-
{panelItem.type === "image" && canUpdate && (
|
|
1033
|
-
<button
|
|
1034
|
-
onClick={() => setShowCrop(true)}
|
|
1035
|
-
className="p-3 border border-[var(--kyro-border)] rounded-xl text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)] transition-all"
|
|
1036
|
-
>
|
|
1037
|
-
<CropIcon className="w-4 h-4" />
|
|
1038
|
-
</button>
|
|
1039
|
-
)}
|
|
1040
|
-
{canDelete && (
|
|
1038
|
+
{!pickerMode && (
|
|
1039
|
+
<div className="pt-8 border-t border-[var(--kyro-border)] mt-8 flex gap-3 pb-8">
|
|
1041
1040
|
<button
|
|
1042
1041
|
onClick={() => {
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
onConfirm: async () => {
|
|
1048
|
-
try {
|
|
1049
|
-
await apiDelete(`/api/media/${panelItem.id}`);
|
|
1050
|
-
setPanelItem(null);
|
|
1051
|
-
loadMedia();
|
|
1052
|
-
} catch (error) {
|
|
1053
|
-
console.error("Delete failed:", error);
|
|
1054
|
-
alert({ title: "Error", message: "Failed to delete asset" });
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
});
|
|
1042
|
+
const link = document.createElement("a");
|
|
1043
|
+
link.href = getAbsoluteUrl(panelItem.url);
|
|
1044
|
+
link.download = panelItem.filename;
|
|
1045
|
+
link.click();
|
|
1058
1046
|
}}
|
|
1059
|
-
className="
|
|
1047
|
+
className="flex-1 flex items-center justify-center gap-2 px-6 py-3 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-xl font-bold text-xs shadow-lg hover:opacity-90 transition-all"
|
|
1060
1048
|
>
|
|
1061
|
-
<
|
|
1049
|
+
<Download className="w-4 h-4" />
|
|
1050
|
+
Download
|
|
1062
1051
|
</button>
|
|
1063
|
-
|
|
1064
|
-
|
|
1052
|
+
{panelItem.type === "image" && canUpdate && (
|
|
1053
|
+
<button
|
|
1054
|
+
onClick={() => setShowCrop(true)}
|
|
1055
|
+
className="p-3 border border-[var(--kyro-border)] rounded-xl text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)] transition-all"
|
|
1056
|
+
>
|
|
1057
|
+
<CropIcon className="w-4 h-4" />
|
|
1058
|
+
</button>
|
|
1059
|
+
)}
|
|
1060
|
+
{canDelete && (
|
|
1061
|
+
<button
|
|
1062
|
+
onClick={() => {
|
|
1063
|
+
confirm({
|
|
1064
|
+
title: "Delete Asset",
|
|
1065
|
+
message: `Are you sure you want to delete ${panelItem.filename}? This cannot be undone.`,
|
|
1066
|
+
variant: "danger",
|
|
1067
|
+
onConfirm: async () => {
|
|
1068
|
+
try {
|
|
1069
|
+
await apiDelete(`/api/media/${panelItem.id}`);
|
|
1070
|
+
setPanelItem(null);
|
|
1071
|
+
loadMedia();
|
|
1072
|
+
} catch (error) {
|
|
1073
|
+
console.error("Delete failed:", error);
|
|
1074
|
+
toast.error("Failed to delete asset");
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
});
|
|
1078
|
+
}}
|
|
1079
|
+
className="p-3 border border-red-100 text-red-500 rounded-xl hover:bg-red-50 transition-all"
|
|
1080
|
+
>
|
|
1081
|
+
<Trash2 className="w-4 h-4" />
|
|
1082
|
+
</button>
|
|
1083
|
+
)}
|
|
1084
|
+
</div>
|
|
1085
|
+
)}
|
|
1065
1086
|
</div>
|
|
1066
1087
|
)}
|
|
1067
1088
|
</SlidePanel>
|
|
@@ -1115,7 +1136,7 @@ export function MediaGallery({
|
|
|
1115
1136
|
)}
|
|
1116
1137
|
|
|
1117
1138
|
{/* Crop Modal */}
|
|
1118
|
-
{showCrop &&
|
|
1139
|
+
{!pickerMode && showCrop &&
|
|
1119
1140
|
panelItem &&
|
|
1120
1141
|
createPortal(
|
|
1121
1142
|
<div className="fixed inset-0 z-[9999] bg-black/95 flex flex-col p-8">
|
|
@@ -1155,14 +1176,16 @@ export function MediaGallery({
|
|
|
1155
1176
|
</div>,
|
|
1156
1177
|
document.body,
|
|
1157
1178
|
)}
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1179
|
+
{!pickerMode && (
|
|
1180
|
+
<PromptModal
|
|
1181
|
+
open={showNewFolderModal}
|
|
1182
|
+
onClose={() => setShowNewFolderModal(false)}
|
|
1183
|
+
onSubmit={createFolder}
|
|
1184
|
+
title="Create New Folder"
|
|
1185
|
+
placeholder="Folder name"
|
|
1186
|
+
/>
|
|
1187
|
+
)}
|
|
1188
|
+
{!pickerMode && showStorageConfigModal &&
|
|
1166
1189
|
createPortal(
|
|
1167
1190
|
<div className="fixed inset-0 z-[9999] bg-black/80 flex items-center justify-center p-4">
|
|
1168
1191
|
<div className="bg-[var(--kyro-surface)] border border-[var(--kyro-border)] rounded-2xl p-8 max-w-md w-full shadow-2xl">
|
|
@@ -1223,16 +1246,18 @@ export function MediaGallery({
|
|
|
1223
1246
|
</div>,
|
|
1224
1247
|
document.body,
|
|
1225
1248
|
)}
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1249
|
+
{!pickerMode && (
|
|
1250
|
+
<input
|
|
1251
|
+
type="file"
|
|
1252
|
+
ref={fileInputRef}
|
|
1253
|
+
onChange={(e) => {
|
|
1254
|
+
if (e.target.files) handleUpload(e.target.files);
|
|
1255
|
+
}}
|
|
1256
|
+
multiple
|
|
1257
|
+
className="hidden"
|
|
1258
|
+
accept="image/*,video/*,audio/*,.pdf,.doc,.docx,.txt,.zip,.rar,.tar"
|
|
1259
|
+
/>
|
|
1260
|
+
)}
|
|
1236
1261
|
</div>
|
|
1237
1262
|
);
|
|
1238
1263
|
}
|