@kyro-cms/admin 0.9.0 → 0.9.2

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 (114) hide show
  1. package/dist/index.cjs +11715 -11292
  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 +564 -0
  6. package/dist/index.d.ts +11 -10
  7. package/dist/index.js +11326 -10912
  8. package/dist/index.js.map +1 -1
  9. package/package.json +16 -12
  10. package/src/components/ActionBar.tsx +25 -161
  11. package/src/components/Admin.tsx +2 -4
  12. package/src/components/ApiKeysManager.tsx +5 -5
  13. package/src/components/AuditLogsPage.tsx +2 -13
  14. package/src/components/AutoForm.tsx +572 -461
  15. package/src/components/BrandingHub.tsx +7 -4
  16. package/src/components/CreateView.tsx +2 -0
  17. package/src/components/DetailView.tsx +52 -65
  18. package/src/components/DeveloperCenter.tsx +8 -6
  19. package/src/components/FieldRenderer.tsx +94 -19
  20. package/src/components/ListView.tsx +57 -216
  21. package/src/components/MediaGallery.tsx +334 -367
  22. package/src/components/PluginsManager.tsx +197 -70
  23. package/src/components/RestPlayground.tsx +59 -52
  24. package/src/components/SessionsManager.tsx +1 -1
  25. package/src/components/SettingsPage.tsx +22 -0
  26. package/src/components/Sidebar.astro +13 -41
  27. package/src/components/UserManagement.tsx +153 -15
  28. package/src/components/UserMenu.tsx +30 -4
  29. package/src/components/VersionHistoryPanel.tsx +112 -119
  30. package/src/components/WebhookManager.tsx +6 -4
  31. package/src/components/blocks/ArrayBlock.tsx +6 -23
  32. package/src/components/blocks/BlockEditModal.tsx +82 -309
  33. package/src/components/blocks/CardBlock.tsx +35 -0
  34. package/src/components/blocks/ChildBlocksTree.tsx +57 -31
  35. package/src/components/blocks/GenericBlock.tsx +44 -0
  36. package/src/components/blocks/HeadingSubheadingBlock.tsx +32 -0
  37. package/src/components/blocks/HeroBlock.tsx +5 -14
  38. package/src/components/blocks/RichTextBlock.tsx +5 -5
  39. package/src/components/blocks/index.ts +5 -3
  40. package/src/components/fields/AccordionField.tsx +2 -2
  41. package/src/components/fields/ArrayField.tsx +1 -1
  42. package/src/components/fields/ArrayLayout.tsx +120 -29
  43. package/src/components/fields/BlocksField.tsx +433 -55
  44. package/src/components/fields/CardField.tsx +73 -0
  45. package/src/components/fields/CheckboxField.tsx +7 -3
  46. package/src/components/fields/DateField.tsx +4 -1
  47. package/src/components/fields/GroupLayout.tsx +2 -2
  48. package/src/components/fields/HeadingSubheadingField.tsx +43 -0
  49. package/src/components/fields/ListField.tsx +2 -2
  50. package/src/components/fields/NumberField.tsx +4 -1
  51. package/src/components/fields/RelationshipBlockField.tsx +2 -3
  52. package/src/components/fields/RelationshipField.tsx +155 -90
  53. package/src/components/fields/RichTextField.tsx +781 -0
  54. package/src/components/fields/SecretField.tsx +102 -0
  55. package/src/components/fields/SelectField.tsx +19 -6
  56. package/src/components/fields/TabsLayout.tsx +19 -9
  57. package/src/components/fields/TextField.tsx +4 -1
  58. package/src/components/fields/UploadField.tsx +122 -56
  59. package/src/components/fields/extensions/blockComponents.tsx +103 -174
  60. package/src/components/fields/extensions/blocksStore.ts +8 -1
  61. package/src/components/fields/index.ts +4 -2
  62. package/src/components/fix_imports.cjs +23 -0
  63. package/src/components/fix_imports2.cjs +19 -0
  64. package/src/components/replace_svgs.cjs +63 -0
  65. package/src/components/ui/Dropdown.tsx +7 -2
  66. package/src/components/ui/Modal.tsx +24 -27
  67. package/src/components/ui/PageHeader.tsx +5 -5
  68. package/src/components/ui/PromptModal.tsx +2 -10
  69. package/src/components/ui/SlidePanel.tsx +10 -13
  70. package/src/components/ui/SplitButton.tsx +107 -0
  71. package/src/components/ui/Toaster.tsx +0 -1
  72. package/src/components/ui/icons.tsx +110 -109
  73. package/src/components/users/UserDetail.tsx +79 -16
  74. package/src/components/users/UsersList.tsx +8 -85
  75. package/src/hooks/useAutoFormState.ts +187 -196
  76. package/src/hooks/useQueue.ts +60 -0
  77. package/src/integration.ts +148 -46
  78. package/src/kyro-cms.d.ts +7 -2
  79. package/src/layouts/AdminLayout.astro +22 -2
  80. package/src/layouts/AuthLayout.astro +67 -7
  81. package/src/lib/autoform-store.ts +90 -53
  82. package/src/lib/change-source.ts +9 -0
  83. package/src/lib/config.ts +104 -8
  84. package/src/lib/globals.ts +48 -11
  85. package/src/lib/normalize-upload-fields.ts +41 -0
  86. package/src/lib/paths.ts +2 -2
  87. package/src/lib/resolve-field-value.ts +110 -0
  88. package/src/lib/shim/use-sync-external-store-with-selector.js +30 -0
  89. package/src/lib/shim/use-sync-external-store.js +1 -0
  90. package/src/lib/stores/index.ts +1 -0
  91. package/src/lib/useResourceManager.ts +4 -4
  92. package/src/lib/vite-shim-plugin.ts +100 -0
  93. package/src/pages/[collection]/[id].astro +1 -1
  94. package/src/pages/auth/register.astro +5 -2
  95. package/src/pages/preview/[collection]/[id].astro +4 -4
  96. package/src/pages/settings/[slug].astro +2 -2
  97. package/src/styles/main.css +60 -54
  98. package/README.md +0 -46
  99. package/dist/EditorClient-Q23UXR37.cjs +0 -468
  100. package/dist/EditorClient-Q23UXR37.cjs.map +0 -1
  101. package/dist/EditorClient-T5PASFNR.js +0 -466
  102. package/dist/EditorClient-T5PASFNR.js.map +0 -1
  103. package/dist/chunk-3BGDYKTD.cjs +0 -348
  104. package/dist/chunk-3BGDYKTD.cjs.map +0 -1
  105. package/dist/chunk-EEFXLQVT.js +0 -3
  106. package/dist/chunk-EEFXLQVT.js.map +0 -1
  107. package/src/components/blocks/ButtonBlock.tsx +0 -64
  108. package/src/components/blocks/ColumnsBlock.tsx +0 -55
  109. package/src/components/blocks/DividerBlock.tsx +0 -43
  110. package/src/components/blocks/LinkBlock.tsx +0 -65
  111. package/src/components/blocks/VStackBlock.tsx +0 -29
  112. package/src/components/fields/EditorClient.tsx +0 -535
  113. package/src/components/fields/PortableTextField.tsx +0 -155
  114. package/src/components/fields/PortableTextRenderer.tsx +0 -68
@@ -1,8 +1,11 @@
1
+ import { Search, Check, Server } from "./ui/icons";
1
2
  import React, { useState, useEffect, useCallback, useRef, useMemo } from "react";
2
3
  import { createPortal } from "react-dom";
3
4
  import { Spinner } from "./ui/Spinner";
4
5
  import { Shimmer } from "./ui/Shimmer";
5
6
  import { SlidePanel } from "./ui/SlidePanel";
7
+ import { Modal } from "./ui/Modal";
8
+ import { Pagination } from "./ui/Pagination";
6
9
  import { Badge } from "./ui/Badge";
7
10
  import { Folder } from "./ui/icons";
8
11
 
@@ -38,7 +41,7 @@ import {
38
41
  withCacheBust,
39
42
  apiUpload,
40
43
  } from "../lib/api";
41
- import { useAuthStore, useUIStore } from "../lib/stores";
44
+ import { useAuthStore, useUIStore, toast } from "../lib/stores";
42
45
 
43
46
  interface MediaItem {
44
47
  id: string;
@@ -57,7 +60,8 @@ interface MediaItem {
57
60
  updatedAt?: string;
58
61
  }
59
62
 
60
- function getAbsoluteUrl(relativeUrl: string): string {
63
+ function getAbsoluteUrl(relativeUrl: unknown): string {
64
+ if (typeof relativeUrl !== "string" || !relativeUrl) return "";
61
65
  if (typeof window === "undefined") return relativeUrl;
62
66
  // Remote URLs and blob URLs are returned as-is
63
67
  if (relativeUrl.startsWith("http") || relativeUrl.startsWith("blob:")) {
@@ -115,10 +119,12 @@ function getFileType(mimeType: string): FilterType {
115
119
 
116
120
  export function MediaGallery({
117
121
  onSelect,
118
- multiple = false,
122
+ multiple = true,
123
+ pickerMode = false,
119
124
  }: {
120
125
  onSelect?: (items: MediaItem[]) => void;
121
126
  multiple?: boolean;
127
+ pickerMode?: boolean;
122
128
  }) {
123
129
  const { permissions } = useAuthStore();
124
130
  const canUpload = permissions?.media?.create !== false;
@@ -201,15 +207,16 @@ export function MediaGallery({
201
207
  }, []);
202
208
 
203
209
  useEffect(() => {
204
- checkStorage();
205
- }, [checkStorage]);
210
+ if (!pickerMode) checkStorage();
211
+ }, [checkStorage, pickerMode]);
206
212
 
207
213
  useEffect(() => {
214
+ if (pickerMode) return;
208
215
  if (storageConfigured === false && !storageChecked) {
209
216
  setStorageChecked(true);
210
217
  setShowStorageConfigModal(true);
211
218
  }
212
- }, [storageConfigured, storageChecked]);
219
+ }, [pickerMode, storageConfigured, storageChecked]);
213
220
 
214
221
  useEffect(() => {
215
222
  loadMedia();
@@ -220,6 +227,7 @@ export function MediaGallery({
220
227
  }, [loadFolders]);
221
228
 
222
229
  useEffect(() => {
230
+ if (pickerMode) return;
223
231
  const handlePaste = (e: ClipboardEvent) => {
224
232
  const files = e.clipboardData?.files;
225
233
  if (files && files.length > 0) {
@@ -228,7 +236,7 @@ export function MediaGallery({
228
236
  };
229
237
  window.addEventListener("paste", handlePaste);
230
238
  return () => window.removeEventListener("paste", handlePaste);
231
- }, [currentFolder, storageConfigured]);
239
+ }, [pickerMode, currentFolder, storageConfigured]);
232
240
 
233
241
  const handleUpload = async (files: FileList | File[]) => {
234
242
  if (!storageConfigured) {
@@ -237,7 +245,8 @@ export function MediaGallery({
237
245
  }
238
246
 
239
247
  setUploading(true);
240
- const newProgress = { ...uploadProgress };
248
+ let successCount = 0;
249
+ let failCount = 0;
241
250
 
242
251
  for (let i = 0; i < files.length; i++) {
243
252
  const file = files[i];
@@ -252,9 +261,10 @@ export function MediaGallery({
252
261
  [file.name]: progress,
253
262
  }));
254
263
  });
264
+ successCount++;
255
265
  } catch (error) {
256
266
  console.error(`Upload failed for ${file.name}:`, error);
257
- alert({ title: "Upload Failed", message: `Failed to upload ${file.name}` });
267
+ failCount++;
258
268
  }
259
269
  }
260
270
 
@@ -262,6 +272,12 @@ export function MediaGallery({
262
272
  setUploadProgress({});
263
273
  loadMedia();
264
274
  loadFolders();
275
+ if (failCount > 0) {
276
+ toast.error(`${failCount} file(s) failed to upload`);
277
+ }
278
+ if (successCount > 0) {
279
+ toast.success(`${successCount} file(s) uploaded successfully`);
280
+ }
265
281
  };
266
282
 
267
283
  const handleBulkDelete = () => {
@@ -276,9 +292,10 @@ export function MediaGallery({
276
292
  }
277
293
  setSelectedIds(new Set());
278
294
  loadMedia();
295
+ toast.success(`${selectedIds.size} item(s) deleted`);
279
296
  } catch (error) {
280
297
  console.error("Bulk delete failed:", error);
281
- alert({ title: "Error", message: "Failed to delete some items" });
298
+ toast.error("Failed to delete some items");
282
299
  }
283
300
  }
284
301
  });
@@ -303,6 +320,7 @@ export function MediaGallery({
303
320
  setShowNewFolderModal(false);
304
321
  } catch (error) {
305
322
  console.error("Failed to create folder:", error);
323
+ toast.error("Failed to create folder");
306
324
  }
307
325
  };
308
326
 
@@ -320,7 +338,7 @@ export function MediaGallery({
320
338
  loadMedia();
321
339
  } catch (error) {
322
340
  console.error("Failed to delete folder:", error);
323
- alert({ title: "Error", message: "Failed to delete folder" });
341
+ toast.error("Failed to delete folder");
324
342
  }
325
343
  }
326
344
  });
@@ -396,6 +414,7 @@ export function MediaGallery({
396
414
  }
397
415
  } catch (err) {
398
416
  console.error("Crop failed:", err);
417
+ toast.error("Crop failed");
399
418
  } finally {
400
419
  setUploading(false);
401
420
  }
@@ -414,47 +433,32 @@ export function MediaGallery({
414
433
  return (
415
434
  <div
416
435
  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
- }}
436
+ {...(pickerMode ? {} : {
437
+ onDragOver: (e) => { e.preventDefault(); setIsDragging(true); },
438
+ onDragLeave: () => setIsDragging(false),
439
+ onDrop: (e) => { e.preventDefault(); setIsDragging(false); if (e.dataTransfer.files) handleUpload(e.dataTransfer.files); },
440
+ })}
427
441
  >
428
442
  {/* 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>
443
+ <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"}`}>
444
+ {!pickerMode && (
445
+ <div className="flex items-center gap-4">
446
+ <div>
447
+ <h2 className="text-xl font-bold tracking-tighter text-[var(--kyro-text-primary)]">
448
+ Media Library
449
+ </h2>
450
+ <div className="flex items-center gap-3 mt-1">
451
+ <span className="text-[10px] font-bold tracking-widest text-[var(--kyro-text-secondary)] opacity-50">
452
+ {total} Items · {formatFileSize(stats.totalSize)}
453
+ </span>
454
+ </div>
439
455
  </div>
440
456
  </div>
441
- </div>
457
+ )}
442
458
 
443
- <div className="flex items-center gap-3 flex-wrap lg:flex-nowrap">
444
- <div className="relative group flex-1 min-w-[240px]">
445
- <svg
446
- 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
- fill="none"
448
- stroke="currentColor"
449
- viewBox="0 0 24 24"
450
- >
451
- <path
452
- strokeLinecap="round"
453
- strokeLinejoin="round"
454
- strokeWidth="2.5"
455
- d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
456
- />
457
- </svg>
459
+ <div className={`flex items-center gap-3 flex-wrap lg:flex-nowrap ${pickerMode ? "w-full" : ""}`}>
460
+ <div className="relative group flex-1 min-w-[200px]">
461
+ <Search className="w-4 h-4" />
458
462
  <input
459
463
  type="text"
460
464
  placeholder="Search assets..."
@@ -464,117 +468,123 @@ export function MediaGallery({
464
468
  />
465
469
  </div>
466
470
 
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>
471
+ {!pickerMode && (
472
+ <>
473
+ <div className="flex bg-[var(--kyro-surface-accent)] p-1 rounded-xl border border-[var(--kyro-border)]">
474
+ <button
475
+ onClick={() => setView("grid")}
476
+ 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"}`}
477
+ >
478
+ <Grid className="w-4 h-4" />
479
+ </button>
480
+ <button
481
+ onClick={() => setView("list")}
482
+ 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"}`}
483
+ >
484
+ <FileIcon className="w-4 h-4" />
485
+ </button>
486
+ </div>
481
487
 
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>
488
+ {canUpload && (
489
+ <button
490
+ onClick={() => fileInputRef.current?.click()}
491
+ 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"
492
+ >
493
+ <Maximize2 className="w-4 h-4" />
494
+ Upload
495
+ </button>
496
+ )}
497
+ </>
490
498
  )}
491
499
  </div>
492
500
  </div>
493
501
 
494
502
  <div className="flex flex-1 min-h-0 overflow-hidden">
495
503
  {/* 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) => (
504
+ {!pickerMode && (
505
+ <div className="w-64 border-r border-[var(--kyro-border)] surface-tile mt-6 overflow-y-auto hidden md:block">
506
+ <div className="p-6 space-y-6">
507
+ <div>
508
+ <span className="text-[10px] font-bold tracking-[0.2em] text-[var(--kyro-text-secondary)] opacity-40 block mb-4">
509
+ Quick Filters
510
+ </span>
511
+ <div className="space-y-1">
512
+ {(
513
+ [
514
+ "all",
515
+ "image",
516
+ "video",
517
+ "audio",
518
+ "document",
519
+ "archive",
520
+ ] as const
521
+ ).map((t) => (
522
+ <button
523
+ key={t}
524
+ onClick={() => setFilter(t)}
525
+ 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"}`}
526
+ >
527
+ <span
528
+ className={`w-1.5 h-1.5 rounded-full ${filter === t ? "bg-[var(--kyro-primary)]" : "bg-transparent border border-current opacity-30"}`}
529
+ />
530
+ {t}
531
+ </button>
532
+ ))}
533
+ </div>
534
+ </div>
535
+
536
+ <div className="pt-6 border-t border-[var(--kyro-border)]">
537
+ <div className="flex items-center justify-between mb-4">
538
+ <span className="text-[10px] font-bold tracking-[0.2em] text-[var(--kyro-text-secondary)] opacity-40">
539
+ Folders
540
+ </span>
513
541
  <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"}`}
542
+ onClick={() => setShowNewFolderModal(true)}
543
+ className="p-1.5 hover:bg-[var(--kyro-surface-accent)] rounded-lg transition-colors text-[var(--kyro-text-primary)]"
517
544
  >
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}
545
+ <FolderPlus className="w-4 h-4" />
522
546
  </button>
523
- ))}
524
- </div>
525
- </div>
547
+ </div>
526
548
 
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>
549
+ <nav className="space-y-1">
550
+ <button
551
+ onClick={() => setCurrentFolder("")}
552
+ 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)]"}`}
553
+ >
554
+ <FolderInput className="w-4 h-4 opacity-70" />
555
+ All Assets
556
+ </button>
557
+ {folders.map((folder) => (
558
+ <div key={folder} className="group relative">
559
+ <button
560
+ onClick={() => setCurrentFolder(folder)}
561
+ 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)]"}`}
562
+ >
563
+ <div className="w-4 h-4 flex items-center justify-center opacity-70">
564
+ <Folder fill={currentFolder === folder ? "currentColor" : "none"} />
565
+ </div>
566
+ {folder}
567
+ </button>
568
+ <button
569
+ onClick={(e) => {
570
+ e.stopPropagation();
571
+ handleDeleteFolder(folder);
572
+ }}
573
+ 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"
574
+ >
575
+ <Trash2 className="w-3.5 h-3.5" />
576
+ </button>
577
+ </div>
578
+ ))}
579
+ </nav>
538
580
  </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
581
  </div>
572
582
  </div>
573
- </div>
583
+ )}
574
584
 
575
585
  {/* Main Content Area */}
576
586
  <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">
587
+ <div className={`flex-1 overflow-y-auto custom-scrollbar ${pickerMode ? "px-2 py-4" : "py-8 px-4"}`}>
578
588
  {loading ? (
579
589
  <div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
580
590
  <Shimmer variant="media-card" count={12} />
@@ -591,7 +601,7 @@ export function MediaGallery({
591
601
  Upload your first file or create a folder to organize your
592
602
  media assets.
593
603
  </p>
594
- {canUpload && (
604
+ {!pickerMode && canUpload && (
595
605
  <button
596
606
  onClick={() => fileInputRef.current?.click()}
597
607
  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,21 +658,9 @@ export function MediaGallery({
648
658
  <div className="flex gap-1">
649
659
  <button
650
660
  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"}`}
661
+ 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
662
  >
653
- <svg
654
- className="w-3 h-3"
655
- fill="none"
656
- stroke="currentColor"
657
- viewBox="0 0 24 24"
658
- >
659
- <path
660
- strokeLinecap="round"
661
- strokeLinejoin="round"
662
- strokeWidth="3"
663
- d="M5 13l4 4L19 7"
664
- />
665
- </svg>
663
+ <Check className="w-4 h-4" />
666
664
  </button>
667
665
  </div>
668
666
  </div>
@@ -670,19 +668,7 @@ export function MediaGallery({
670
668
 
671
669
  {selectedIds.has(item.id) && (
672
670
  <div className="absolute top-3 left-3 w-6 h-6 rounded-lg bg-[var(--kyro-primary)] text-white flex items-center justify-center shadow-lg border-2 border-white/20 animate-in zoom-in duration-300">
673
- <svg
674
- className="w-3 h-3"
675
- fill="none"
676
- stroke="currentColor"
677
- viewBox="0 0 24 24"
678
- >
679
- <path
680
- strokeLinecap="round"
681
- strokeLinejoin="round"
682
- strokeWidth="3"
683
- d="M5 13l4 4L19 7"
684
- />
685
- </svg>
671
+ <Check className="w-4 h-4" />
686
672
  </div>
687
673
  )}
688
674
  </div>
@@ -770,7 +756,7 @@ export function MediaGallery({
770
756
  e.stopPropagation();
771
757
  handleSelectOne(item.id, e);
772
758
  }}
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"}`}
759
+ 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
760
  >
775
761
  <Grid className="w-4 h-4" />
776
762
  </button>
@@ -783,35 +769,16 @@ export function MediaGallery({
783
769
  )}
784
770
  </div>
785
771
 
786
- {/* Pagination */}
787
- {totalPages > 1 && (
788
- <div className="p-6 border-t border-[var(--kyro-border)] bg-[var(--kyro-surface)]/50 backdrop-blur-md flex items-center justify-between">
789
- <span className="text-[10px] font-bold tracking-widest text-[var(--kyro-text-secondary)] opacity-50">
790
- Page {page} of {totalPages}
791
- </span>
792
- <div className="flex gap-2">
793
- <button
794
- disabled={page === 1}
795
- onClick={() => setPage(page - 1)}
796
- className="px-4 py-2 border border-[var(--kyro-border)] rounded-xl text-xs font-bold text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] disabled:opacity-30 transition-all"
797
- >
798
- Previous
799
- </button>
800
- <button
801
- disabled={page === totalPages}
802
- onClick={() => setPage(page + 1)}
803
- className="px-6 py-2 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-xl text-xs font-bold shadow-lg hover:opacity-90 disabled:opacity-30 transition-all"
804
- >
805
- Next
806
- </button>
807
- </div>
808
- </div>
809
- )}
772
+ <Pagination
773
+ page={page}
774
+ totalPages={totalPages}
775
+ onPageChange={setPage}
776
+ />
810
777
  </div>
811
778
  </div>
812
779
 
813
780
  {/* Upload Banner */}
814
- {uploading && (
781
+ {!pickerMode && uploading && (
815
782
  <div className="fixed bottom-12 left-1/2 -translate-x-1/2 z-[60] w-full max-w-lg">
816
783
  <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
784
  <div className="flex items-center justify-between mb-4">
@@ -882,7 +849,7 @@ export function MediaGallery({
882
849
  Confirm Selection
883
850
  </button>
884
851
  )}
885
- {canDelete && (
852
+ {!pickerMode && canDelete && (
886
853
  <button
887
854
  onClick={handleBulkDelete}
888
855
  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 +941,7 @@ export function MediaGallery({
974
941
  navigator.clipboard.writeText(
975
942
  getAbsoluteUrl(panelItem.url),
976
943
  );
977
- alert({ title: "Copied", message: "URL copied to clipboard" });
944
+ toast.success("URL copied to clipboard");
978
945
  }}
979
946
  className="p-3 bg-[var(--kyro-surface-accent)] hover:bg-[var(--kyro-border)] border border-[var(--kyro-border)] rounded-xl transition-all"
980
947
  >
@@ -1016,109 +983,119 @@ export function MediaGallery({
1016
983
  </div>
1017
984
  </div>
1018
985
 
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 && (
986
+ {!pickerMode && (
987
+ <div className="pt-8 border-t border-[var(--kyro-border)] mt-8 flex gap-3 pb-8">
1041
988
  <button
1042
989
  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
- });
990
+ const link = document.createElement("a");
991
+ link.href = getAbsoluteUrl(panelItem.url);
992
+ link.download = panelItem.filename;
993
+ link.click();
1058
994
  }}
1059
- className="p-3 border border-red-100 text-red-500 rounded-xl hover:bg-red-50 transition-all"
995
+ 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
996
  >
1061
- <Trash2 className="w-4 h-4" />
997
+ <Download className="w-4 h-4" />
998
+ Download
1062
999
  </button>
1063
- )}
1064
- </div>
1000
+ {panelItem.type === "image" && canUpdate && (
1001
+ <button
1002
+ onClick={() => setShowCrop(true)}
1003
+ className="p-3 border border-[var(--kyro-border)] rounded-xl text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)] transition-all"
1004
+ >
1005
+ <CropIcon className="w-4 h-4" />
1006
+ </button>
1007
+ )}
1008
+ {canDelete && (
1009
+ <button
1010
+ onClick={() => {
1011
+ confirm({
1012
+ title: "Delete Asset",
1013
+ message: `Are you sure you want to delete ${panelItem.filename}? This cannot be undone.`,
1014
+ variant: "danger",
1015
+ onConfirm: async () => {
1016
+ try {
1017
+ await apiDelete(`/api/media/${panelItem.id}`);
1018
+ setPanelItem(null);
1019
+ loadMedia();
1020
+ } catch (error) {
1021
+ console.error("Delete failed:", error);
1022
+ toast.error("Failed to delete asset");
1023
+ }
1024
+ }
1025
+ });
1026
+ }}
1027
+ className="p-3 border border-red-100 text-red-500 rounded-xl hover:bg-red-50 transition-all"
1028
+ >
1029
+ <Trash2 className="w-4 h-4" />
1030
+ </button>
1031
+ )}
1032
+ </div>
1033
+ )}
1065
1034
  </div>
1066
1035
  )}
1067
1036
  </SlidePanel>
1068
1037
 
1069
1038
  {/* Preview Modal */}
1070
- {showPreview &&
1071
- panelItem &&
1072
- createPortal(
1073
- <div className="fixed inset-0 z-[9999] bg-black/95 flex flex-col animate-in fade-in duration-500">
1074
- <div className="flex items-center justify-between p-6">
1075
- <div className="flex flex-col">
1076
- <span className="text-white font-bold text-lg tracking-tight">
1077
- {panelItem.filename}
1078
- </span>
1079
- <span className="text-white/40 text-[10px] font-bold tracking-widest mt-1">
1080
- {formatFileSize(panelItem.fileSize)} · {panelItem.mimeType}
1081
- </span>
1082
- </div>
1083
- <button
1084
- onClick={() => setShowPreview(false)}
1085
- className="p-3 bg-white/10 hover:bg-white/20 text-white rounded-2xl transition-all active:scale-90"
1086
- >
1087
- <X className="w-6 h-6" />
1088
- </button>
1089
- </div>
1090
- <div className="flex-1 w-full flex items-center justify-center p-12">
1091
- {panelItem.type === "image" ? (
1092
- <img
1093
- src={getAbsoluteUrl(panelItem.url)}
1094
- alt=""
1095
- className="max-h-full max-w-full object-contain shadow-2xl rounded-lg animate-in zoom-in-95 duration-500"
1096
- />
1097
- ) : panelItem.type === "video" ? (
1098
- <video
1099
- src={getAbsoluteUrl(panelItem.url)}
1100
- controls
1101
- autoPlay
1102
- className="max-h-full max-w-full rounded-lg shadow-2xl"
1103
- />
1104
- ) : (
1105
- <div className="text-white text-center">
1106
- <FileIcon className="w-24 h-24 mx-auto mb-6 opacity-20" />
1107
- <p className="text-xl font-bold opacity-50">
1108
- Preview not available for this file type
1109
- </p>
1110
- </div>
1111
- )}
1039
+ {showPreview && panelItem && (
1040
+ <Modal
1041
+ open={showPreview}
1042
+ onClose={() => setShowPreview(false)}
1043
+ title=""
1044
+ size="full"
1045
+ variant="lightbox"
1046
+ >
1047
+ <div className="flex items-center justify-between p-6">
1048
+ <div className="flex flex-col">
1049
+ <span className="text-white font-bold text-lg tracking-tight">
1050
+ {panelItem.filename}
1051
+ </span>
1052
+ <span className="text-white/40 text-[10px] font-bold tracking-widest mt-1">
1053
+ {formatFileSize(panelItem.fileSize)} · {panelItem.mimeType}
1054
+ </span>
1112
1055
  </div>
1113
- </div>,
1114
- document.body,
1115
- )}
1056
+ <button
1057
+ onClick={() => setShowPreview(false)}
1058
+ className="p-3 bg-white/10 hover:bg-white/20 text-white rounded-2xl transition-all active:scale-90"
1059
+ >
1060
+ <X className="w-6 h-6" />
1061
+ </button>
1062
+ </div>
1063
+ <div className="flex-1 w-full flex items-center justify-center p-12">
1064
+ {panelItem.type === "image" ? (
1065
+ <img
1066
+ src={getAbsoluteUrl(panelItem.url)}
1067
+ alt=""
1068
+ className="max-h-full max-w-full object-contain shadow-2xl rounded-lg animate-in zoom-in-95 duration-500"
1069
+ />
1070
+ ) : panelItem.type === "video" ? (
1071
+ <video
1072
+ src={getAbsoluteUrl(panelItem.url)}
1073
+ controls
1074
+ autoPlay
1075
+ className="max-h-full max-w-full rounded-lg shadow-2xl"
1076
+ />
1077
+ ) : (
1078
+ <div className="text-white text-center">
1079
+ <FileIcon className="w-24 h-24 mx-auto mb-6 opacity-20" />
1080
+ <p className="text-xl font-bold opacity-50">
1081
+ Preview not available for this file type
1082
+ </p>
1083
+ </div>
1084
+ )}
1085
+ </div>
1086
+ </Modal>
1087
+ )}
1116
1088
 
1117
1089
  {/* Crop Modal */}
1118
- {showCrop &&
1119
- panelItem &&
1120
- createPortal(
1121
- <div className="fixed inset-0 z-[9999] bg-black/95 flex flex-col p-8">
1090
+ {!pickerMode && showCrop && panelItem && (
1091
+ <Modal
1092
+ open={showCrop}
1093
+ onClose={() => setShowCrop(false)}
1094
+ title=""
1095
+ size="full"
1096
+ variant="lightbox"
1097
+ >
1098
+ <div className="flex flex-col h-full p-8">
1122
1099
  <div className="flex items-center justify-between mb-8">
1123
1100
  <h3 className="text-white font-bold text-2xl tracking-tighter">
1124
1101
  Crop Image
@@ -1152,87 +1129,77 @@ export function MediaGallery({
1152
1129
  />
1153
1130
  </ReactCrop>
1154
1131
  </div>
1155
- </div>,
1156
- document.body,
1157
- )}
1158
- <PromptModal
1159
- open={showNewFolderModal}
1160
- onClose={() => setShowNewFolderModal(false)}
1161
- onSubmit={createFolder}
1162
- title="Create New Folder"
1163
- placeholder="Folder name"
1164
- />
1165
- {showStorageConfigModal &&
1166
- createPortal(
1167
- <div className="fixed inset-0 z-[9999] bg-black/80 flex items-center justify-center p-4">
1168
- <div className="bg-[var(--kyro-surface)] border border-[var(--kyro-border)] rounded-2xl p-8 max-w-md w-full shadow-2xl">
1169
- <div className="text-center">
1170
- <div className="w-16 h-16 mx-auto mb-4 rounded-full bg-[var(--kyro-sidebar-active)] flex items-center justify-center">
1171
- <svg
1172
- className="w-8 h-8 text-[var(--kyro-sidebar-text-active)]"
1173
- fill="none"
1174
- stroke="currentColor"
1175
- viewBox="0 0 24 24"
1176
- >
1177
- <path
1178
- strokeLinecap="round"
1179
- strokeLinejoin="round"
1180
- strokeWidth={2}
1181
- d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2H5a2 2 0 00-2 2v1m2 2a2 2 0 11-4 0 2 2 0 014 0zm2 2h.008v.008H5v-.008z"
1182
- />
1183
- </svg>
1184
- </div>
1185
- <h3 className="text-xl font-bold text-[var(--kyro-text-primary)] mb-2">
1186
- Storage Not Configured
1187
- </h3>
1188
- <p className="text-[var(--kyro-text-secondary)] mb-6 text-sm">
1189
- Before uploading media, you need to configure your storage
1190
- settings. Choose where files should be stored and how URLs are
1191
- generated.
1192
- </p>
1193
- <div className="flex gap-3">
1194
- <a
1195
- href="/settings/storage-settings"
1196
- className="flex-1 px-4 py-3 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-xl font-bold text-center hover:opacity-90 transition-colors"
1197
- >
1198
- Configure Storage
1199
- </a>
1200
- <button
1201
- type="button"
1202
- onClick={() => {
1203
- // Set default storage config programmatically
1204
- apiPost("/api/globals/storage-settings", {
1205
- provider: "local",
1206
- local: {
1207
- uploadDir: "./public/uploads",
1208
- baseUrl: "/uploads",
1209
- },
1210
- }).then(() => {
1211
- setShowStorageConfigModal(false);
1212
- setStorageConfigured(true);
1213
- window.location.reload();
1214
- });
1215
- }}
1216
- className="flex-1 px-4 py-3 border border-[var(--kyro-border)] text-[var(--kyro-text-secondary)] rounded-xl font-bold hover:bg-[var(--kyro-surface-accent)] transition-colors"
1217
- >
1218
- Use Defaults
1219
- </button>
1220
- </div>
1221
- </div>
1132
+ </div>
1133
+ </Modal>
1134
+ )}
1135
+ {!pickerMode && (
1136
+ <PromptModal
1137
+ open={showNewFolderModal}
1138
+ onClose={() => setShowNewFolderModal(false)}
1139
+ onSubmit={createFolder}
1140
+ title="Create New Folder"
1141
+ placeholder="Folder name"
1142
+ />
1143
+ )}
1144
+ {!pickerMode && (
1145
+ <Modal
1146
+ open={showStorageConfigModal}
1147
+ onClose={() => setShowStorageConfigModal(false)}
1148
+ title="Storage Not Configured"
1149
+ size="md"
1150
+ >
1151
+ <div className="text-center">
1152
+ <div className="w-16 h-16 mx-auto mb-4 rounded-full bg-[var(--kyro-sidebar-active)] flex items-center justify-center">
1153
+ <Server className="w-8 h-8 text-[var(--kyro-sidebar-text-active)]" />
1222
1154
  </div>
1223
- </div>,
1224
- document.body,
1225
- )}
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
- />
1155
+ <p className="text-[var(--kyro-text-secondary)] mb-6 text-sm">
1156
+ Before uploading media, you need to configure your storage
1157
+ settings. Choose where files should be stored and how URLs are
1158
+ generated.
1159
+ </p>
1160
+ <div className="flex gap-3">
1161
+ <a
1162
+ href="/settings/storage-settings"
1163
+ className="flex-1 px-4 py-3 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-xl font-bold text-center hover:opacity-90 transition-colors"
1164
+ >
1165
+ Configure Storage
1166
+ </a>
1167
+ <button
1168
+ type="button"
1169
+ onClick={() => {
1170
+ // Set default storage config programmatically
1171
+ apiPost("/api/globals/storage-settings", {
1172
+ provider: "local",
1173
+ local: {
1174
+ uploadDir: "./public/uploads",
1175
+ baseUrl: "/uploads",
1176
+ },
1177
+ }).then(() => {
1178
+ setShowStorageConfigModal(false);
1179
+ setStorageConfigured(true);
1180
+ window.location.reload();
1181
+ });
1182
+ }}
1183
+ className="flex-1 px-4 py-3 border border-[var(--kyro-border)] text-[var(--kyro-text-secondary)] rounded-xl font-bold hover:bg-[var(--kyro-surface-accent)] transition-colors"
1184
+ >
1185
+ Use Defaults
1186
+ </button>
1187
+ </div>
1188
+ </div>
1189
+ </Modal>
1190
+ )}
1191
+ {!pickerMode && (
1192
+ <input
1193
+ type="file"
1194
+ ref={fileInputRef}
1195
+ onChange={(e) => {
1196
+ if (e.target.files) handleUpload(e.target.files);
1197
+ }}
1198
+ multiple
1199
+ className="hidden"
1200
+ accept="image/*,video/*,audio/*,.pdf,.doc,.docx,.txt,.zip,.rar,.tar"
1201
+ />
1202
+ )}
1236
1203
  </div>
1237
1204
  );
1238
1205
  }