@morphika/andami 0.5.0 → 0.5.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 (122) hide show
  1. package/README.md +151 -36
  2. package/app/admin/assets/page.tsx +6 -6
  3. package/app/admin/database/page.tsx +302 -302
  4. package/app/admin/error.tsx +53 -53
  5. package/app/admin/layout.tsx +320 -327
  6. package/app/admin/navigation/page.tsx +255 -255
  7. package/app/admin/pages/[slug]/page.tsx +6 -6
  8. package/app/admin/pages/page.tsx +11 -11
  9. package/app/admin/projects/page.tsx +14 -14
  10. package/app/admin/setup/page.tsx +1 -1
  11. package/app/admin/styles/page.tsx +1 -1
  12. package/components/admin/MetadataEditor.tsx +6 -6
  13. package/components/admin/nav-builder/NavBuilder.tsx +1 -1
  14. package/components/admin/nav-builder/NavBuilderGrid.tsx +3 -3
  15. package/components/admin/nav-builder/NavGridCell.tsx +48 -48
  16. package/components/admin/nav-builder/NavGridItem.tsx +4 -4
  17. package/components/admin/nav-builder/NavItemSettings.tsx +331 -331
  18. package/components/admin/nav-builder/NavItemTypePicker.tsx +102 -102
  19. package/components/admin/nav-builder/NavLivePreview.tsx +1 -1
  20. package/components/admin/nav-builder/NavMobileLivePreview.tsx +226 -226
  21. package/components/admin/nav-builder/NavMobileSettings.tsx +242 -242
  22. package/components/admin/nav-builder/NavSettingsFields.tsx +514 -514
  23. package/components/admin/setup-wizard/BrandingStep.tsx +3 -3
  24. package/components/admin/setup-wizard/DatabaseStep.tsx +2 -2
  25. package/components/admin/setup-wizard/DoneStep.tsx +1 -1
  26. package/components/admin/setup-wizard/SetupWizard.tsx +4 -4
  27. package/components/admin/setup-wizard/StorageStep.tsx +2 -2
  28. package/components/admin/setup-wizard/WelcomeStep.tsx +2 -2
  29. package/components/admin/styles/ColorsEditor.tsx +2 -2
  30. package/components/admin/styles/FontsEditor.tsx +6 -6
  31. package/components/admin/styles/GridLayoutEditor.tsx +9 -9
  32. package/components/admin/styles/LinksButtonsEditor.tsx +5 -5
  33. package/components/admin/styles/TypographyEditor.tsx +6 -6
  34. package/components/admin/styles/shared.tsx +68 -68
  35. package/components/blocks/AudioBlockRenderer.tsx +286 -0
  36. package/components/blocks/BeforeAfterBlockRenderer.tsx +274 -0
  37. package/components/blocks/MarqueeBlockRenderer.tsx +316 -0
  38. package/components/blocks/ProjectCarouselBlockRenderer.tsx +1 -1
  39. package/components/builder/BlockCardIcons.tsx +316 -227
  40. package/components/builder/BlockTypePicker.tsx +3 -1
  41. package/components/builder/BubbleIcons.tsx +90 -0
  42. package/components/builder/BuilderCanvas.tsx +2 -0
  43. package/components/builder/CanvasMinimap.tsx +2 -2
  44. package/components/builder/CoverSectionCanvas.tsx +363 -275
  45. package/components/builder/DeviceFrame.tsx +1 -1
  46. package/components/builder/DndWrapper.tsx +3 -3
  47. package/components/builder/InsertionLines.tsx +1 -1
  48. package/components/builder/SectionCardIcons.tsx +421 -320
  49. package/components/builder/SectionEditorBar.tsx +1 -1
  50. package/components/builder/SectionTypePicker.tsx +4 -4
  51. package/components/builder/SectionV2Canvas.tsx +20 -4
  52. package/components/builder/SectionV2Column.tsx +74 -68
  53. package/components/builder/SortableBlock.tsx +93 -73
  54. package/components/builder/SortableRow.tsx +27 -26
  55. package/components/builder/VirtualAssetGrid.tsx +2 -2
  56. package/components/builder/asset-browser/R2BrowserContent.tsx +34 -17
  57. package/components/builder/asset-browser/helpers.ts +4 -0
  58. package/components/builder/asset-browser/types.ts +2 -1
  59. package/components/builder/blockStyles.tsx +192 -173
  60. package/components/builder/color-picker/AlphaSlider.tsx +141 -141
  61. package/components/builder/color-picker/ColorInputs.tsx +105 -105
  62. package/components/builder/color-picker/EyedropperButton.tsx +74 -74
  63. package/components/builder/color-picker/HueSlider.tsx +124 -124
  64. package/components/builder/color-picker/SaturationCanvas.tsx +142 -142
  65. package/components/builder/color-picker/SwatchBar.tsx +93 -93
  66. package/components/builder/editors/AudioBlockEditor.tsx +242 -0
  67. package/components/builder/editors/BeforeAfterBlockEditor.tsx +360 -0
  68. package/components/builder/editors/ButtonBlockEditor.tsx +4 -4
  69. package/components/builder/editors/EnterAnimationPicker.tsx +2 -2
  70. package/components/builder/editors/HoverEffectPicker.tsx +2 -2
  71. package/components/builder/editors/ImageBlockEditor.tsx +2 -2
  72. package/components/builder/editors/ImageGridBlockEditor.tsx +4 -4
  73. package/components/builder/editors/MarqueeBlockEditor.tsx +621 -0
  74. package/components/builder/editors/ProjectCarouselBlockEditor.tsx +443 -443
  75. package/components/builder/editors/ProjectGridEditor.tsx +9 -9
  76. package/components/builder/editors/SpacerBlockEditor.tsx +5 -5
  77. package/components/builder/editors/StaggerSettings.tsx +109 -109
  78. package/components/builder/editors/TextBlockEditor.tsx +3 -3
  79. package/components/builder/editors/TextStylePicker.tsx +1 -1
  80. package/components/builder/editors/VideoBlockEditor.tsx +2 -2
  81. package/components/builder/editors/index.ts +11 -10
  82. package/components/builder/editors/shared.tsx +7 -7
  83. package/components/builder/live-preview/LiveAudioPreview.tsx +120 -0
  84. package/components/builder/live-preview/LiveBeforeAfterPreview.tsx +176 -0
  85. package/components/builder/live-preview/LiveImageGridPreview.tsx +10 -2
  86. package/components/builder/live-preview/LiveImagePreview.tsx +1 -1
  87. package/components/builder/live-preview/LiveMarqueePreview.tsx +39 -0
  88. package/components/builder/live-preview/LiveProjectCarouselPreview.tsx +1 -1
  89. package/components/builder/live-preview/LiveVideoPreview.tsx +1 -1
  90. package/components/builder/live-preview/ProjectCardWrapper.tsx +291 -291
  91. package/components/builder/settings-panel/AnimationTab.tsx +138 -138
  92. package/components/builder/settings-panel/BlockLayoutTab.tsx +7 -7
  93. package/components/builder/settings-panel/CardEntranceSection.tsx +114 -114
  94. package/components/builder/settings-panel/ColumnV2Settings.tsx +5 -5
  95. package/components/builder/settings-panel/CoverSectionLayoutTab.tsx +71 -71
  96. package/components/builder/settings-panel/CoverSectionSettings.tsx +335 -335
  97. package/components/builder/settings-panel/PageSettings.tsx +3 -3
  98. package/components/builder/settings-panel/ParallaxSlideSettings.tsx +2 -2
  99. package/components/builder/settings-panel/SectionV2AnimationTab.tsx +4 -4
  100. package/components/builder/settings-panel/SectionV2LayoutTab.tsx +356 -356
  101. package/components/builder/settings-panel/SectionV2Settings.tsx +14 -14
  102. package/components/builder/settings-panel/TRBLInputs.tsx +1 -1
  103. package/lib/animation/enter-types.ts +3 -0
  104. package/lib/animation/hover-effect-presets.ts +210 -210
  105. package/lib/animation/hover-effect-types.ts +3 -0
  106. package/lib/builder/block-registrations.ts +468 -335
  107. package/lib/builder/constants.ts +111 -111
  108. package/lib/builder/store-sections.ts +2 -2
  109. package/lib/builder/types-slices.ts +414 -414
  110. package/lib/builder/types.ts +6 -1
  111. package/lib/config/index.ts +27 -27
  112. package/lib/sanity/types.ts +156 -1
  113. package/lib/version.ts +1 -1
  114. package/package.json +1 -1
  115. package/sanity/schemas/blocks/audioBlock.ts +69 -0
  116. package/sanity/schemas/blocks/beforeAfterBlock.ts +121 -0
  117. package/sanity/schemas/blocks/index.ts +12 -9
  118. package/sanity/schemas/blocks/marqueeBlock.ts +292 -0
  119. package/sanity/schemas/index.ts +120 -111
  120. package/styles/admin.css +85 -85
  121. package/styles/animations.css +237 -237
  122. package/styles/base.css +114 -114
@@ -14,6 +14,7 @@ import { getRowLayoutStyles } from "../../lib/builder/layout-styles";
14
14
  import { normalizeMinHeight } from "../../lib/builder/utils";
15
15
  import { getSectionV2SettingValue } from "./settings-panel/responsive-helpers";
16
16
  import { formatRowPercent } from "../../lib/builder/format";
17
+ import { ArrowDownIcon, ArrowUpIcon, BubbleTooltip, CloseIcon, CopyIcon } from "./BubbleIcons";
17
18
 
18
19
  /**
19
20
  * Convert vh-based CSS values to pixels using the simulated device viewport height.
@@ -261,7 +262,7 @@ export default function SortableRow({
261
262
  showToolbar ? "opacity-100" : "opacity-0 pointer-events-none"
262
263
  }`}
263
264
  style={{
264
- transform: `translateX(calc(-100% - 8px)) scale(${Math.min(1.5, 1 / canvasZoom)})`,
265
+ transform: `translateX(calc(-100% - 8px)) scale(${Math.min(2, 1 / canvasZoom)})`,
265
266
  transformOrigin: "top right",
266
267
  width: "90px",
267
268
  }}
@@ -279,7 +280,7 @@ export default function SortableRow({
279
280
  className="flex flex-col items-stretch rounded-lg py-2 px-2.5 gap-1 cursor-grab active:cursor-grabbing"
280
281
  style={{
281
282
  background: "#e0daff",
282
- border: "1px solid #7500d5",
283
+ border: "1.5px solid #7500d5",
283
284
  }}
284
285
  {...attributes}
285
286
  {...listeners}
@@ -294,40 +295,40 @@ export default function SortableRow({
294
295
  <button
295
296
  onClick={(e) => { e.stopPropagation(); onDuplicate(); }}
296
297
  onPointerDown={(e) => e.stopPropagation()}
297
- className="flex items-center justify-center text-[12px] transition-colors"
298
+ className="group/bb relative flex items-center justify-center transition-colors"
298
299
  style={{ color: "rgba(117, 0, 213, 0.6)" }}
299
300
  onMouseEnter={(e) => { e.currentTarget.style.color = "#7500d5"; }}
300
301
  onMouseLeave={(e) => { e.currentTarget.style.color = "rgba(117, 0, 213, 0.6)"; }}
301
- title="Duplicate section"
302
302
  aria-label="Duplicate section"
303
303
  >
304
-
304
+ <CopyIcon size={14} />
305
+ <BubbleTooltip>Duplicate</BubbleTooltip>
305
306
  </button>
306
307
  <button
307
308
  onClick={(e) => { e.stopPropagation(); onMoveUp(); }}
308
309
  onPointerDown={(e) => e.stopPropagation()}
309
310
  disabled={isFirst}
310
- className="flex items-center justify-center text-[12px] transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
311
+ className="group/bb relative flex items-center justify-center transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
311
312
  style={{ color: "rgba(117, 0, 213, 0.6)" }}
312
313
  onMouseEnter={(e) => { if (!e.currentTarget.disabled) e.currentTarget.style.color = "#7500d5"; }}
313
314
  onMouseLeave={(e) => { if (!e.currentTarget.disabled) e.currentTarget.style.color = "rgba(117, 0, 213, 0.6)"; }}
314
- title="Move up"
315
315
  aria-label="Move section up"
316
316
  >
317
-
317
+ <ArrowUpIcon size={14} />
318
+ {!isFirst && <BubbleTooltip>Move up</BubbleTooltip>}
318
319
  </button>
319
320
  <button
320
321
  onClick={(e) => { e.stopPropagation(); onMoveDown(); }}
321
322
  onPointerDown={(e) => e.stopPropagation()}
322
323
  disabled={isLast}
323
- className="flex items-center justify-center text-[12px] transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
324
+ className="group/bb relative flex items-center justify-center transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
324
325
  style={{ color: "rgba(117, 0, 213, 0.6)" }}
325
326
  onMouseEnter={(e) => { if (!e.currentTarget.disabled) e.currentTarget.style.color = "#7500d5"; }}
326
327
  onMouseLeave={(e) => { if (!e.currentTarget.disabled) e.currentTarget.style.color = "rgba(117, 0, 213, 0.6)"; }}
327
- title="Move down"
328
328
  aria-label="Move section down"
329
329
  >
330
-
330
+ <ArrowDownIcon size={14} />
331
+ {!isLast && <BubbleTooltip>Move down</BubbleTooltip>}
331
332
  </button>
332
333
  </div>
333
334
 
@@ -358,7 +359,7 @@ export default function SortableRow({
358
359
  title="Delete section"
359
360
  aria-label="Delete section"
360
361
  >
361
- <span style={{ color: "rgba(117, 0, 213, 0.4)" }}>-</span> Delete
362
+ <CloseIcon size={12} /> Delete
362
363
  </button>
363
364
  </div>
364
365
 
@@ -375,7 +376,7 @@ export default function SortableRow({
375
376
  className="flex flex-col items-stretch rounded-lg py-1.5 px-2 mt-2 gap-0.5"
376
377
  style={{
377
378
  background: "#e0daff",
378
- border: "1px solid #7500d5",
379
+ border: "1.5px solid #7500d5",
379
380
  }}
380
381
  onClick={(e) => e.stopPropagation()}
381
382
  >
@@ -391,14 +392,14 @@ export default function SortableRow({
391
392
  }}
392
393
  onPointerDown={(e) => e.stopPropagation()}
393
394
  disabled={!canRemoveRow}
394
- className="flex items-center justify-center text-[12px] leading-none transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
395
+ className="group/bb relative flex items-center justify-center leading-none transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
395
396
  style={{ color: "rgba(117, 0, 213, 0.6)" }}
396
397
  onMouseEnter={(e) => { if (!e.currentTarget.disabled) e.currentTarget.style.color = "#7500d5"; }}
397
398
  onMouseLeave={(e) => { if (!e.currentTarget.disabled) e.currentTarget.style.color = "rgba(117, 0, 213, 0.6)"; }}
398
- title={canRemoveRow ? "Remove row" : "Cover must have at least 1 row"}
399
399
  aria-label="Remove row"
400
400
  >
401
- ×
401
+ <CloseIcon size={12} />
402
+ {canRemoveRow && <BubbleTooltip>Remove row</BubbleTooltip>}
402
403
  </button>
403
404
  </div>
404
405
  ))}
@@ -436,7 +437,7 @@ export default function SortableRow({
436
437
  className="flex flex-col items-stretch rounded-lg py-1.5 px-2 mt-2 gap-0.5"
437
438
  style={{
438
439
  background: "#e0daff",
439
- border: "1px solid #7500d5",
440
+ border: "1.5px solid #7500d5",
440
441
  }}
441
442
  onClick={(e) => e.stopPropagation()}
442
443
  >
@@ -462,14 +463,14 @@ export default function SortableRow({
462
463
  }}
463
464
  onPointerDown={(e) => e.stopPropagation()}
464
465
  disabled={idx === 0}
465
- className="flex items-center justify-center text-[10px] leading-none transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
466
+ className="group/bb relative flex items-center justify-center leading-none transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
466
467
  style={{ color: "rgba(117, 0, 213, 0.6)" }}
467
468
  onMouseEnter={(e) => { if (!e.currentTarget.disabled) e.currentTarget.style.color = "#7500d5"; }}
468
469
  onMouseLeave={(e) => { if (!e.currentTarget.disabled) e.currentTarget.style.color = "rgba(117, 0, 213, 0.6)"; }}
469
- title="Move slide up"
470
470
  aria-label="Move slide up"
471
471
  >
472
-
472
+ <ArrowUpIcon size={12} />
473
+ {idx > 0 && <BubbleTooltip>Move up</BubbleTooltip>}
473
474
  </button>
474
475
  <button
475
476
  onClick={(e) => {
@@ -478,14 +479,14 @@ export default function SortableRow({
478
479
  }}
479
480
  onPointerDown={(e) => e.stopPropagation()}
480
481
  disabled={idx === slides.length - 1}
481
- className="flex items-center justify-center text-[10px] leading-none transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
482
+ className="group/bb relative flex items-center justify-center leading-none transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
482
483
  style={{ color: "rgba(117, 0, 213, 0.6)" }}
483
484
  onMouseEnter={(e) => { if (!e.currentTarget.disabled) e.currentTarget.style.color = "#7500d5"; }}
484
485
  onMouseLeave={(e) => { if (!e.currentTarget.disabled) e.currentTarget.style.color = "rgba(117, 0, 213, 0.6)"; }}
485
- title="Move slide down"
486
486
  aria-label="Move slide down"
487
487
  >
488
-
488
+ <ArrowDownIcon size={12} />
489
+ {idx < slides.length - 1 && <BubbleTooltip>Move down</BubbleTooltip>}
489
490
  </button>
490
491
  <button
491
492
  onClick={(e) => {
@@ -494,14 +495,14 @@ export default function SortableRow({
494
495
  }}
495
496
  onPointerDown={(e) => e.stopPropagation()}
496
497
  disabled={!canRemoveSlide}
497
- className="flex items-center justify-center text-[12px] leading-none transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
498
+ className="group/bb relative flex items-center justify-center leading-none transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
498
499
  style={{ color: "rgba(117, 0, 213, 0.6)" }}
499
500
  onMouseEnter={(e) => { if (!e.currentTarget.disabled) e.currentTarget.style.color = "#7500d5"; }}
500
501
  onMouseLeave={(e) => { if (!e.currentTarget.disabled) e.currentTarget.style.color = "rgba(117, 0, 213, 0.6)"; }}
501
- title={canRemoveSlide ? "Remove slide" : "Parallax must have at least 1 slide"}
502
502
  aria-label="Remove slide"
503
503
  >
504
- ×
504
+ <CloseIcon size={12} />
505
+ {canRemoveSlide && <BubbleTooltip>Remove slide</BubbleTooltip>}
505
506
  </button>
506
507
  </div>
507
508
  </div>
@@ -315,7 +315,7 @@ function AssetGridItem({
315
315
  onContextMenu={onContextMenu ? (e) => onContextMenu(e, asset) : undefined}
316
316
  className={`relative flex flex-col rounded-lg overflow-hidden transition-all ${
317
317
  isSelected
318
- ? "ring-2 ring-[#076bff] ring-offset-2 shadow-lg"
318
+ ? "ring-2 ring-[#3580f9] ring-offset-2 shadow-lg"
319
319
  : "hover:shadow-md"
320
320
  }`}
321
321
  >
@@ -323,7 +323,7 @@ function AssetGridItem({
323
323
  {multiSelect && (
324
324
  <div
325
325
  className={`absolute top-1.5 left-1.5 z-10 w-5 h-5 rounded flex items-center justify-center text-white text-[10px] font-bold transition-colors ${
326
- isSelected ? "bg-[#076bff]" : "bg-black/30 border border-white/50"
326
+ isSelected ? "bg-[#3580f9]" : "bg-black/30 border border-white/50"
327
327
  }`}
328
328
  >
329
329
  {isSelected && (
@@ -4,7 +4,7 @@ import { useState, useMemo, useCallback, useRef, useEffect, Fragment } from "rea
4
4
  import type { RegisteredAsset } from "../../../lib/sanity/types";
5
5
  import { VirtualAssetGrid } from "../VirtualAssetGrid";
6
6
  import type { UploadingFile } from "./types";
7
- import { formatFileSize, isImageType, isVideoType, isFontType, buildFolderTree } from "./helpers";
7
+ import { formatFileSize, isImageType, isVideoType, isAudioType, isFontType, buildFolderTree } from "./helpers";
8
8
  import { FolderTreeItem } from "./FolderTreeItem";
9
9
  import { VideoThumbnail } from "./VideoThumbnail";
10
10
  import { FileLightbox } from "./FileLightbox";
@@ -34,7 +34,7 @@ export function R2BrowserContent({
34
34
  setSelectedAsset: (a: RegisteredAsset | null) => void;
35
35
  onRetry: () => void;
36
36
  onDoubleClick?: (asset: RegisteredAsset) => void;
37
- filterType?: "image" | "video" | "all";
37
+ filterType?: "image" | "video" | "audio" | "all";
38
38
  multiSelect?: boolean;
39
39
  selectedAssets?: RegisteredAsset[];
40
40
  setSelectedAssets?: (assets: RegisteredAsset[]) => void;
@@ -99,6 +99,7 @@ export function R2BrowserContent({
99
99
 
100
100
  if (filterType === "image") filtered = filtered.filter((a) => isImageType(a.extension));
101
101
  else if (filterType === "video") filtered = filtered.filter((a) => isVideoType(a.extension));
102
+ else if (filterType === "audio") filtered = filtered.filter((a) => isAudioType(a.extension));
102
103
 
103
104
  if (searchQuery.trim()) {
104
105
  const q = searchQuery.trim().normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
@@ -106,8 +107,12 @@ export function R2BrowserContent({
106
107
  (a) => {
107
108
  const nameNorm = a.filename.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
108
109
  const pathNorm = a.path.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
109
- return a.filename !== ".folder" && (nameNorm.includes(q) || pathNorm.includes(q)) &&
110
- (filterType === "all" || (filterType === "image" ? isImageType(a.extension) : isVideoType(a.extension)));
110
+ const matchesType =
111
+ filterType === "all" ||
112
+ (filterType === "image" && isImageType(a.extension)) ||
113
+ (filterType === "video" && isVideoType(a.extension)) ||
114
+ (filterType === "audio" && isAudioType(a.extension));
115
+ return a.filename !== ".folder" && (nameNorm.includes(q) || pathNorm.includes(q)) && matchesType;
111
116
  }
112
117
  );
113
118
  }
@@ -185,6 +190,18 @@ export function R2BrowserContent({
185
190
  );
186
191
  }
187
192
 
193
+ if (isAudioType(asset.extension)) {
194
+ return (
195
+ <div className="w-full h-full flex items-center justify-center bg-neutral-50">
196
+ <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className="text-neutral-300">
197
+ <path d="M9 18V5l12-2v13" />
198
+ <circle cx="6" cy="18" r="3" />
199
+ <circle cx="18" cy="16" r="3" />
200
+ </svg>
201
+ </div>
202
+ );
203
+ }
204
+
188
205
  return (
189
206
  <div className="w-full h-full flex items-center justify-center bg-neutral-50">
190
207
  <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="text-neutral-300">
@@ -208,26 +225,26 @@ export function R2BrowserContent({
208
225
  ref={ops.fileInputRef}
209
226
  type="file"
210
227
  multiple
211
- accept="image/jpeg,image/png,image/webp,image/gif,image/svg+xml,video/mp4,video/webm,video/quicktime"
228
+ accept="image/jpeg,image/png,image/webp,image/gif,image/svg+xml,video/mp4,video/webm,video/quicktime,audio/mpeg,audio/wav,audio/ogg,audio/mp4,audio/aac,audio/flac"
212
229
  className="hidden"
213
230
  onChange={dnd.handleFileInputChange}
214
231
  />
215
232
 
216
233
  {/* Drag & drop overlay */}
217
234
  {dnd.dragOver && (
218
- <div className="absolute inset-0 z-50 flex items-center justify-center bg-[#076bff]/10 border-2 border-dashed border-[#076bff] rounded-lg backdrop-blur-[2px]">
235
+ <div className="absolute inset-0 z-50 flex items-center justify-center bg-[#3580f9]/10 border-2 border-dashed border-[#3580f9] rounded-lg backdrop-blur-[2px]">
219
236
  <div className="flex flex-col items-center gap-3">
220
- <div className="w-16 h-16 rounded-full bg-[#076bff]/10 flex items-center justify-center">
237
+ <div className="w-16 h-16 rounded-full bg-[#3580f9]/10 flex items-center justify-center">
221
238
  <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke={ADMIN_ACCENT} strokeWidth="1.5">
222
239
  <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
223
240
  <polyline points="17 8 12 3 7 8" />
224
241
  <line x1="12" y1="3" x2="12" y2="15" />
225
242
  </svg>
226
243
  </div>
227
- <p className="text-sm font-medium text-[#076bff]">
244
+ <p className="text-sm font-medium text-[#3580f9]">
228
245
  Drop files or folders here{currentFolder ? ` to ${currentFolder}` : ""}
229
246
  </p>
230
- <p className="text-xs text-neutral-500">Supported formats: JPG, PNG, WebP, GIF, SVG, MP4, WebM, MOV</p>
247
+ <p className="text-xs text-neutral-500">Supported formats: JPG, PNG, WebP, GIF, SVG, MP4, WebM, MOV, MP3, WAV, OGG, M4A, AAC, FLAC</p>
231
248
  <p className="text-xs text-neutral-400">Maximum file size: 500 MB</p>
232
249
  </div>
233
250
  </div>
@@ -284,7 +301,7 @@ export function R2BrowserContent({
284
301
  <button
285
302
  onClick={() => ops.fileInputRef.current?.click()}
286
303
  disabled={uploading.some((u) => u.status === "uploading" || u.status === "registering")}
287
- className="inline-flex items-center gap-1.5 rounded-lg bg-[#076bff] px-3 py-1.5 text-[11px] text-white font-medium uppercase tracking-wider hover:bg-[#076bff]/90 transition-colors disabled:opacity-50"
304
+ className="inline-flex items-center gap-1.5 rounded-lg bg-[#3580f9] px-3 py-1.5 text-[11px] text-white font-medium uppercase tracking-wider hover:bg-[#3580f9]/90 transition-colors disabled:opacity-50"
288
305
  title={`Upload files${currentFolder ? ` to ${currentFolder}` : ""}`}
289
306
  type="button"
290
307
  >
@@ -310,7 +327,7 @@ export function R2BrowserContent({
310
327
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#ef4444" strokeWidth="2"><line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /></svg>
311
328
  </button>
312
329
  ) : (
313
- <div className="w-3.5 h-3.5 border-2 border-[#076bff] border-t-transparent rounded-full animate-spin" />
330
+ <div className="w-3.5 h-3.5 border-2 border-[#3580f9] border-t-transparent rounded-full animate-spin" />
314
331
  )}
315
332
  <span className="text-[11px] text-neutral-600 truncate flex-1 min-w-0">
316
333
  {u.file.name}
@@ -319,7 +336,7 @@ export function R2BrowserContent({
319
336
  </span>
320
337
  {(u.status === "uploading" || u.status === "registering") && (
321
338
  <div className="w-24 h-1.5 bg-neutral-200 rounded-full overflow-hidden">
322
- <div className="h-full bg-[#076bff] rounded-full transition-all duration-300" style={{ width: `${u.progress}%` }} />
339
+ <div className="h-full bg-[#3580f9] rounded-full transition-all duration-300" style={{ width: `${u.progress}%` }} />
323
340
  </div>
324
341
  )}
325
342
  <span className="text-[10px] text-neutral-400 tabular-nums">{formatFileSize(u.file.size)}</span>
@@ -339,7 +356,7 @@ export function R2BrowserContent({
339
356
  {error && (
340
357
  <div className="flex flex-col items-center justify-center h-40 gap-3 px-8">
341
358
  <span className="text-xs text-red-500 text-center max-w-md leading-relaxed">{error}</span>
342
- <button onClick={onRetry} className="text-xs text-[#076bff] hover:underline">Retry</button>
359
+ <button onClick={onRetry} className="text-xs text-[#3580f9] hover:underline">Retry</button>
343
360
  </div>
344
361
  )}
345
362
 
@@ -355,9 +372,9 @@ export function R2BrowserContent({
355
372
  ref={newFolderInputRef}
356
373
  type="text" value={ops.newFolderName} onChange={(e) => ops.setNewFolderName(e.target.value)}
357
374
  onKeyDown={(e) => { e.stopPropagation(); if (e.key === "Enter") ops.handleCreateFolder(); if (e.key === "Escape") ops.cancelNewFolderInput(); }}
358
- placeholder="Folder name..." className="flex-1 text-sm text-neutral-900 bg-white border border-neutral-300 rounded px-2 py-1 focus:outline-none focus:border-[#076bff]"
375
+ placeholder="Folder name..." className="flex-1 text-sm text-neutral-900 bg-white border border-neutral-300 rounded px-2 py-1 focus:outline-none focus:border-[#3580f9]"
359
376
  />
360
- <button onClick={ops.handleCreateFolder} disabled={!ops.newFolderName.trim() || ops.actionLoading} className="text-xs px-3 py-1 rounded bg-[#076bff] text-white disabled:opacity-50" type="button">Create</button>
377
+ <button onClick={ops.handleCreateFolder} disabled={!ops.newFolderName.trim() || ops.actionLoading} className="text-xs px-3 py-1 rounded bg-[#3580f9] text-white disabled:opacity-50" type="button">Create</button>
361
378
  <button onClick={ops.cancelNewFolderInput} className="text-xs px-2 py-1 text-neutral-500 hover:text-neutral-800" type="button">Cancel</button>
362
379
  </div>
363
380
  )}
@@ -370,9 +387,9 @@ export function R2BrowserContent({
370
387
  ref={renameInputRef}
371
388
  type="text" value={ops.renameValue} onChange={(e) => ops.setRenameValue(e.target.value)}
372
389
  onKeyDown={(e) => { e.stopPropagation(); if (e.key === "Enter") ops.handleRename(); if (e.key === "Escape") ops.cancelRename(); }}
373
- className="flex-1 text-sm text-neutral-900 bg-white border border-neutral-300 rounded px-2 py-1 focus:outline-none focus:border-[#076bff]"
390
+ className="flex-1 text-sm text-neutral-900 bg-white border border-neutral-300 rounded px-2 py-1 focus:outline-none focus:border-[#3580f9]"
374
391
  />
375
- <button onClick={ops.handleRename} disabled={!ops.renameValue.trim() || ops.actionLoading} className="text-xs px-3 py-1 rounded bg-[#076bff] text-white disabled:opacity-50" type="button">Rename</button>
392
+ <button onClick={ops.handleRename} disabled={!ops.renameValue.trim() || ops.actionLoading} className="text-xs px-3 py-1 rounded bg-[#3580f9] text-white disabled:opacity-50" type="button">Rename</button>
376
393
  <button onClick={ops.cancelRename} className="text-xs px-2 py-1 text-neutral-500 hover:text-neutral-800" type="button">Cancel</button>
377
394
  </div>
378
395
  )}
@@ -62,6 +62,10 @@ export function isVideoType(ext: string): boolean {
62
62
  return ["mp4", "webm", "mov"].includes(ext);
63
63
  }
64
64
 
65
+ export function isAudioType(ext: string): boolean {
66
+ return ["mp3", "wav", "ogg", "m4a", "aac", "flac"].includes(ext);
67
+ }
68
+
65
69
  export function isFontType(ext: string): boolean {
66
70
  return ["otf", "ttf", "woff", "woff2"].includes(ext);
67
71
  }
@@ -17,6 +17,7 @@ export const MAX_UPLOAD_SIZE = 500 * 1024 * 1024; // 500 MB per file
17
17
  export const ALLOWED_EXTENSIONS = new Set([
18
18
  "jpg", "jpeg", "png", "webp", "gif", "svg",
19
19
  "mp4", "webm", "mov",
20
+ "mp3", "wav", "ogg", "m4a", "aac", "flac",
20
21
  ]);
21
22
 
22
23
  // ============================================
@@ -27,7 +28,7 @@ export interface AssetBrowserProps {
27
28
  open: boolean;
28
29
  onSelect: (path: string) => void;
29
30
  onClose: () => void;
30
- filterType?: "image" | "video" | "all";
31
+ filterType?: "image" | "video" | "audio" | "all";
31
32
  /** Enable multi-select mode: user can pick multiple assets at once */
32
33
  multiSelect?: boolean;
33
34
  /** Called with all selected paths when multiSelect is true */