@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.
Files changed (100) hide show
  1. package/dist/index.cjs +11960 -11006
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.css +67 -65
  4. package/dist/index.css.map +1 -1
  5. package/dist/index.d.cts +563 -0
  6. package/dist/index.d.ts +7 -7
  7. package/dist/index.js +12183 -11238
  8. package/dist/index.js.map +1 -1
  9. package/package.json +15 -11
  10. package/src/components/ActionBar.tsx +27 -14
  11. package/src/components/Admin.tsx +1 -1
  12. package/src/components/ApiKeysManager.tsx +5 -5
  13. package/src/components/AutoForm.tsx +585 -369
  14. package/src/components/BrandingHub.tsx +7 -4
  15. package/src/components/CreateView.tsx +2 -0
  16. package/src/components/DetailView.tsx +71 -56
  17. package/src/components/DeveloperCenter.tsx +8 -6
  18. package/src/components/FieldRenderer.tsx +94 -19
  19. package/src/components/ListView.tsx +33 -20
  20. package/src/components/MediaGallery.tsx +219 -194
  21. package/src/components/PluginsManager.tsx +197 -70
  22. package/src/components/RestPlayground.tsx +7 -7
  23. package/src/components/SessionsManager.tsx +1 -1
  24. package/src/components/SettingsPage.tsx +22 -0
  25. package/src/components/Sidebar.astro +13 -41
  26. package/src/components/UserManagement.tsx +153 -15
  27. package/src/components/UserMenu.tsx +30 -4
  28. package/src/components/VersionHistoryPanel.tsx +112 -119
  29. package/src/components/WebhookManager.tsx +6 -4
  30. package/src/components/blocks/ArrayBlock.tsx +6 -23
  31. package/src/components/blocks/BlockEditModal.tsx +82 -309
  32. package/src/components/blocks/CardBlock.tsx +35 -0
  33. package/src/components/blocks/ChildBlocksTree.tsx +57 -31
  34. package/src/components/blocks/GenericBlock.tsx +44 -0
  35. package/src/components/blocks/HeadingSubheadingBlock.tsx +32 -0
  36. package/src/components/blocks/HeroBlock.tsx +5 -14
  37. package/src/components/blocks/RichTextBlock.tsx +5 -5
  38. package/src/components/blocks/index.ts +5 -3
  39. package/src/components/fields/AccordionField.tsx +2 -2
  40. package/src/components/fields/ArrayField.tsx +1 -1
  41. package/src/components/fields/ArrayLayout.tsx +120 -29
  42. package/src/components/fields/BlocksField.tsx +430 -50
  43. package/src/components/fields/CardField.tsx +73 -0
  44. package/src/components/fields/CheckboxField.tsx +7 -3
  45. package/src/components/fields/DateField.tsx +4 -1
  46. package/src/components/fields/GroupLayout.tsx +2 -2
  47. package/src/components/fields/HeadingSubheadingField.tsx +43 -0
  48. package/src/components/fields/ListField.tsx +2 -2
  49. package/src/components/fields/NumberField.tsx +4 -1
  50. package/src/components/fields/RelationshipField.tsx +153 -87
  51. package/src/components/fields/RichTextField.tsx +781 -0
  52. package/src/components/fields/SecretField.tsx +102 -0
  53. package/src/components/fields/SelectField.tsx +19 -6
  54. package/src/components/fields/TabsLayout.tsx +19 -9
  55. package/src/components/fields/TextField.tsx +4 -1
  56. package/src/components/fields/UploadField.tsx +122 -56
  57. package/src/components/fields/extensions/blockComponents.tsx +103 -174
  58. package/src/components/fields/extensions/blocksStore.ts +8 -1
  59. package/src/components/fields/index.ts +4 -2
  60. package/src/components/ui/PageHeader.tsx +5 -5
  61. package/src/components/ui/SlidePanel.tsx +8 -3
  62. package/src/components/ui/icons.tsx +109 -109
  63. package/src/components/users/UserDetail.tsx +79 -16
  64. package/src/hooks/useAutoFormState.ts +125 -62
  65. package/src/integration.ts +148 -46
  66. package/src/kyro-cms.d.ts +7 -2
  67. package/src/layouts/AuthLayout.astro +14 -2
  68. package/src/lib/autoform-store.ts +85 -52
  69. package/src/lib/change-source.ts +9 -0
  70. package/src/lib/config.ts +104 -8
  71. package/src/lib/globals.ts +44 -9
  72. package/src/lib/normalize-upload-fields.ts +41 -0
  73. package/src/lib/paths.ts +2 -2
  74. package/src/lib/resolve-field-value.ts +110 -0
  75. package/src/lib/shim/use-sync-external-store-with-selector.js +30 -0
  76. package/src/lib/shim/use-sync-external-store.js +1 -0
  77. package/src/lib/stores/index.ts +1 -0
  78. package/src/lib/useResourceManager.ts +4 -4
  79. package/src/lib/vite-shim-plugin.ts +100 -0
  80. package/src/pages/[collection]/[id].astro +1 -1
  81. package/src/pages/preview/[collection]/[id].astro +4 -4
  82. package/src/pages/settings/[slug].astro +2 -2
  83. package/src/styles/main.css +60 -54
  84. package/README.md +0 -46
  85. package/dist/EditorClient-Q23UXR37.cjs +0 -468
  86. package/dist/EditorClient-Q23UXR37.cjs.map +0 -1
  87. package/dist/EditorClient-T5PASFNR.js +0 -466
  88. package/dist/EditorClient-T5PASFNR.js.map +0 -1
  89. package/dist/chunk-3BGDYKTD.cjs +0 -348
  90. package/dist/chunk-3BGDYKTD.cjs.map +0 -1
  91. package/dist/chunk-EEFXLQVT.js +0 -3
  92. package/dist/chunk-EEFXLQVT.js.map +0 -1
  93. package/src/components/blocks/ButtonBlock.tsx +0 -64
  94. package/src/components/blocks/ColumnsBlock.tsx +0 -55
  95. package/src/components/blocks/DividerBlock.tsx +0 -43
  96. package/src/components/blocks/LinkBlock.tsx +0 -65
  97. package/src/components/blocks/VStackBlock.tsx +0 -29
  98. package/src/components/fields/EditorClient.tsx +0 -535
  99. package/src/components/fields/PortableTextField.tsx +0 -155
  100. 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: string): string {
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 = false,
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
- const newProgress = { ...uploadProgress };
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
- alert({ title: "Upload Failed", message: `Failed to upload ${file.name}` });
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
- alert({ title: "Error", message: "Failed to delete some items" });
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
- alert({ title: "Error", message: "Failed to delete folder" });
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
- onDragOver={(e) => {
418
- e.preventDefault();
419
- setIsDragging(true);
420
- }}
421
- onDragLeave={() => setIsDragging(false)}
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="flex flex-col lg:flex-row lg:items-center justify-between p-6 gap-6 border-b border-[var(--kyro-border)] surface-tile backdrop-blur-md sticky top-0 z-40 rounded-xl">
430
- <div className="flex items-center gap-4">
431
- <div>
432
- <h2 className="text-xl font-bold tracking-tighter text-[var(--kyro-text-primary)]">
433
- Media Library
434
- </h2>
435
- <div className="flex items-center gap-3 mt-1">
436
- <span className="text-[10px] font-bold tracking-widest text-[var(--kyro-text-secondary)] opacity-50">
437
- {total} Items · {formatFileSize(stats.totalSize)}
438
- </span>
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
- </div>
454
+ )}
442
455
 
443
- <div className="flex items-center gap-3 flex-wrap lg:flex-nowrap">
444
- <div className="relative group flex-1 min-w-[240px]">
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
- <div className="flex bg-[var(--kyro-surface-accent)] p-1 rounded-xl border border-[var(--kyro-border)]">
468
- <button
469
- onClick={() => setView("grid")}
470
- 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"}`}
471
- >
472
- <Grid className="w-4 h-4" />
473
- </button>
474
- <button
475
- onClick={() => setView("list")}
476
- 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"}`}
477
- >
478
- <FileIcon className="w-4 h-4" />
479
- </button>
480
- </div>
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
- {canUpload && (
483
- <button
484
- onClick={() => fileInputRef.current?.click()}
485
- 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"
486
- >
487
- <Maximize2 className="w-4 h-4" />
488
- Upload
489
- </button>
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
- <div className="w-64 border-r border-[var(--kyro-border)] surface-tile mt-6 overflow-y-auto hidden md:block">
497
- <div className="p-6 space-y-6">
498
- <div>
499
- <span className="text-[10px] font-bold tracking-[0.2em] text-[var(--kyro-text-secondary)] opacity-40 block mb-4">
500
- Quick Filters
501
- </span>
502
- <div className="space-y-1">
503
- {(
504
- [
505
- "all",
506
- "image",
507
- "video",
508
- "audio",
509
- "document",
510
- "archive",
511
- ] as const
512
- ).map((t) => (
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
- key={t}
515
- onClick={() => setFilter(t)}
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
- <span
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
- <div className="pt-6 border-t border-[var(--kyro-border)]">
528
- <div className="flex items-center justify-between mb-4">
529
- <span className="text-[10px] font-bold tracking-[0.2em] text-[var(--kyro-text-secondary)] opacity-40">
530
- Folders
531
- </span>
532
- <button
533
- onClick={() => setShowNewFolderModal(true)}
534
- className="p-1.5 hover:bg-[var(--kyro-surface-accent)] rounded-lg transition-colors text-[var(--kyro-text-primary)]"
535
- >
536
- <FolderPlus className="w-4 h-4" />
537
- </button>
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
- </div>
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="flex-1 overflow-y-auto py-8 px-4 custom-scrollbar">
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) ? "bg-[var(--kyro-primary)] text-white" : "bg-white/10 text-white hover:bg-white/20"}`}
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) ? "bg-[var(--kyro-primary)] text-white" : "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] opacity-0 group-hover:opacity-100"}`}
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
- alert({ title: "Copied", message: "URL copied to clipboard" });
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
- <div className="pt-8 border-t border-[var(--kyro-border)] mt-8 flex gap-3 pb-8">
1020
- <button
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
- confirm({
1044
- title: "Delete Asset",
1045
- message: `Are you sure you want to delete ${panelItem.filename}? This cannot be undone.`,
1046
- variant: "danger",
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="p-3 border border-red-100 text-red-500 rounded-xl hover:bg-red-50 transition-all"
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
- <Trash2 className="w-4 h-4" />
1049
+ <Download className="w-4 h-4" />
1050
+ Download
1062
1051
  </button>
1063
- )}
1064
- </div>
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
- <PromptModal
1159
- open={showNewFolderModal}
1160
- onClose={() => setShowNewFolderModal(false)}
1161
- onSubmit={createFolder}
1162
- title="Create New Folder"
1163
- placeholder="Folder name"
1164
- />
1165
- {showStorageConfigModal &&
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
- <input
1227
- type="file"
1228
- ref={fileInputRef}
1229
- onChange={(e) => {
1230
- if (e.target.files) handleUpload(e.target.files);
1231
- }}
1232
- multiple
1233
- className="hidden"
1234
- accept="image/*,video/*,audio/*,.pdf,.doc,.docx,.txt,.zip,.rar,.tar"
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
  }