@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.
- package/README.md +151 -36
- package/app/admin/assets/page.tsx +6 -6
- package/app/admin/database/page.tsx +302 -302
- package/app/admin/error.tsx +53 -53
- package/app/admin/layout.tsx +320 -327
- package/app/admin/navigation/page.tsx +255 -255
- package/app/admin/pages/[slug]/page.tsx +6 -6
- package/app/admin/pages/page.tsx +11 -11
- package/app/admin/projects/page.tsx +14 -14
- package/app/admin/setup/page.tsx +1 -1
- package/app/admin/styles/page.tsx +1 -1
- package/components/admin/MetadataEditor.tsx +6 -6
- package/components/admin/nav-builder/NavBuilder.tsx +1 -1
- package/components/admin/nav-builder/NavBuilderGrid.tsx +3 -3
- package/components/admin/nav-builder/NavGridCell.tsx +48 -48
- package/components/admin/nav-builder/NavGridItem.tsx +4 -4
- package/components/admin/nav-builder/NavItemSettings.tsx +331 -331
- package/components/admin/nav-builder/NavItemTypePicker.tsx +102 -102
- package/components/admin/nav-builder/NavLivePreview.tsx +1 -1
- package/components/admin/nav-builder/NavMobileLivePreview.tsx +226 -226
- package/components/admin/nav-builder/NavMobileSettings.tsx +242 -242
- package/components/admin/nav-builder/NavSettingsFields.tsx +514 -514
- package/components/admin/setup-wizard/BrandingStep.tsx +3 -3
- package/components/admin/setup-wizard/DatabaseStep.tsx +2 -2
- package/components/admin/setup-wizard/DoneStep.tsx +1 -1
- package/components/admin/setup-wizard/SetupWizard.tsx +4 -4
- package/components/admin/setup-wizard/StorageStep.tsx +2 -2
- package/components/admin/setup-wizard/WelcomeStep.tsx +2 -2
- package/components/admin/styles/ColorsEditor.tsx +2 -2
- package/components/admin/styles/FontsEditor.tsx +6 -6
- package/components/admin/styles/GridLayoutEditor.tsx +9 -9
- package/components/admin/styles/LinksButtonsEditor.tsx +5 -5
- package/components/admin/styles/TypographyEditor.tsx +6 -6
- package/components/admin/styles/shared.tsx +68 -68
- package/components/blocks/AudioBlockRenderer.tsx +286 -0
- package/components/blocks/BeforeAfterBlockRenderer.tsx +274 -0
- package/components/blocks/MarqueeBlockRenderer.tsx +316 -0
- package/components/blocks/ProjectCarouselBlockRenderer.tsx +1 -1
- package/components/builder/BlockCardIcons.tsx +316 -227
- package/components/builder/BlockTypePicker.tsx +3 -1
- package/components/builder/BubbleIcons.tsx +90 -0
- package/components/builder/BuilderCanvas.tsx +2 -0
- package/components/builder/CanvasMinimap.tsx +2 -2
- package/components/builder/CoverSectionCanvas.tsx +363 -275
- package/components/builder/DeviceFrame.tsx +1 -1
- package/components/builder/DndWrapper.tsx +3 -3
- package/components/builder/InsertionLines.tsx +1 -1
- package/components/builder/SectionCardIcons.tsx +421 -320
- package/components/builder/SectionEditorBar.tsx +1 -1
- package/components/builder/SectionTypePicker.tsx +4 -4
- package/components/builder/SectionV2Canvas.tsx +20 -4
- package/components/builder/SectionV2Column.tsx +74 -68
- package/components/builder/SortableBlock.tsx +93 -73
- package/components/builder/SortableRow.tsx +27 -26
- package/components/builder/VirtualAssetGrid.tsx +2 -2
- package/components/builder/asset-browser/R2BrowserContent.tsx +34 -17
- package/components/builder/asset-browser/helpers.ts +4 -0
- package/components/builder/asset-browser/types.ts +2 -1
- package/components/builder/blockStyles.tsx +192 -173
- package/components/builder/color-picker/AlphaSlider.tsx +141 -141
- package/components/builder/color-picker/ColorInputs.tsx +105 -105
- package/components/builder/color-picker/EyedropperButton.tsx +74 -74
- package/components/builder/color-picker/HueSlider.tsx +124 -124
- package/components/builder/color-picker/SaturationCanvas.tsx +142 -142
- package/components/builder/color-picker/SwatchBar.tsx +93 -93
- package/components/builder/editors/AudioBlockEditor.tsx +242 -0
- package/components/builder/editors/BeforeAfterBlockEditor.tsx +360 -0
- package/components/builder/editors/ButtonBlockEditor.tsx +4 -4
- package/components/builder/editors/EnterAnimationPicker.tsx +2 -2
- package/components/builder/editors/HoverEffectPicker.tsx +2 -2
- package/components/builder/editors/ImageBlockEditor.tsx +2 -2
- package/components/builder/editors/ImageGridBlockEditor.tsx +4 -4
- package/components/builder/editors/MarqueeBlockEditor.tsx +621 -0
- package/components/builder/editors/ProjectCarouselBlockEditor.tsx +443 -443
- package/components/builder/editors/ProjectGridEditor.tsx +9 -9
- package/components/builder/editors/SpacerBlockEditor.tsx +5 -5
- package/components/builder/editors/StaggerSettings.tsx +109 -109
- package/components/builder/editors/TextBlockEditor.tsx +3 -3
- package/components/builder/editors/TextStylePicker.tsx +1 -1
- package/components/builder/editors/VideoBlockEditor.tsx +2 -2
- package/components/builder/editors/index.ts +11 -10
- package/components/builder/editors/shared.tsx +7 -7
- package/components/builder/live-preview/LiveAudioPreview.tsx +120 -0
- package/components/builder/live-preview/LiveBeforeAfterPreview.tsx +176 -0
- package/components/builder/live-preview/LiveImageGridPreview.tsx +10 -2
- package/components/builder/live-preview/LiveImagePreview.tsx +1 -1
- package/components/builder/live-preview/LiveMarqueePreview.tsx +39 -0
- package/components/builder/live-preview/LiveProjectCarouselPreview.tsx +1 -1
- package/components/builder/live-preview/LiveVideoPreview.tsx +1 -1
- package/components/builder/live-preview/ProjectCardWrapper.tsx +291 -291
- package/components/builder/settings-panel/AnimationTab.tsx +138 -138
- package/components/builder/settings-panel/BlockLayoutTab.tsx +7 -7
- package/components/builder/settings-panel/CardEntranceSection.tsx +114 -114
- package/components/builder/settings-panel/ColumnV2Settings.tsx +5 -5
- package/components/builder/settings-panel/CoverSectionLayoutTab.tsx +71 -71
- package/components/builder/settings-panel/CoverSectionSettings.tsx +335 -335
- package/components/builder/settings-panel/PageSettings.tsx +3 -3
- package/components/builder/settings-panel/ParallaxSlideSettings.tsx +2 -2
- package/components/builder/settings-panel/SectionV2AnimationTab.tsx +4 -4
- package/components/builder/settings-panel/SectionV2LayoutTab.tsx +356 -356
- package/components/builder/settings-panel/SectionV2Settings.tsx +14 -14
- package/components/builder/settings-panel/TRBLInputs.tsx +1 -1
- package/lib/animation/enter-types.ts +3 -0
- package/lib/animation/hover-effect-presets.ts +210 -210
- package/lib/animation/hover-effect-types.ts +3 -0
- package/lib/builder/block-registrations.ts +468 -335
- package/lib/builder/constants.ts +111 -111
- package/lib/builder/store-sections.ts +2 -2
- package/lib/builder/types-slices.ts +414 -414
- package/lib/builder/types.ts +6 -1
- package/lib/config/index.ts +27 -27
- package/lib/sanity/types.ts +156 -1
- package/lib/version.ts +1 -1
- package/package.json +1 -1
- package/sanity/schemas/blocks/audioBlock.ts +69 -0
- package/sanity/schemas/blocks/beforeAfterBlock.ts +121 -0
- package/sanity/schemas/blocks/index.ts +12 -9
- package/sanity/schemas/blocks/marqueeBlock.ts +292 -0
- package/sanity/schemas/index.ts +120 -111
- package/styles/admin.css +85 -85
- package/styles/animations.css +237 -237
- 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(
|
|
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: "
|
|
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
|
|
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
|
|
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
|
|
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
|
-
<
|
|
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: "
|
|
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
|
|
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: "
|
|
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
|
|
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
|
|
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
|
|
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-[#
|
|
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-[#
|
|
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
|
-
|
|
110
|
-
|
|
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-[#
|
|
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-[#
|
|
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-[#
|
|
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-[#
|
|
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-[#
|
|
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-[#
|
|
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-[#
|
|
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-[#
|
|
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-[#
|
|
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-[#
|
|
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-[#
|
|
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 */
|