diff --git a/apps/web/src/features/studio/components/cloud-file-browser/CloudFileBrowser.stories.tsx b/apps/web/src/features/studio/components/cloud-file-browser/CloudFileBrowser.stories.tsx new file mode 100644 index 000000000..eb63a40f2 --- /dev/null +++ b/apps/web/src/features/studio/components/cloud-file-browser/CloudFileBrowser.stories.tsx @@ -0,0 +1,21 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { CloudFileBrowser } from './CloudFileBrowser'; + +const meta: Meta = { + title: 'Components/Features/Studio/CloudFileBrowser', + component: CloudFileBrowser, + tags: ['autodocs'], + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/apps/web/src/features/studio/components/cloud-file-browser/CloudFileBrowser.tsx b/apps/web/src/features/studio/components/cloud-file-browser/CloudFileBrowser.tsx new file mode 100644 index 000000000..8fc389b60 --- /dev/null +++ b/apps/web/src/features/studio/components/cloud-file-browser/CloudFileBrowser.tsx @@ -0,0 +1,188 @@ +import React, { useState, useEffect, useMemo } from 'react'; +import { useToast } from '@/components/feedback/ToastProvider'; +import { FileDetailsView } from '@/components/views/FileDetailsView'; +import { AutoMetadataDetectionModal } from '@/components/library/AutoMetadataDetectionModal'; +import { WatermarkSettingsModal } from '@/components/library/WatermarkSettingsModal'; +import { storageService } from '@/services/storageService'; +import { logger } from '@/utils/logger'; +import { FileToolbar } from './FileToolbar'; +import { FileTable } from './FileTable'; +import { FileGrid } from './FileGrid'; +import { CloudFileBrowserSkeleton } from './CloudFileBrowserSkeleton'; +import type { CloudFileNode, SortField, SortOrder, ViewMode } from './types'; +import { FILE_BROWSER_DEFAULT_TAGS } from './types'; + +export const CloudFileBrowser: React.FC = () => { + const { addToast } = useToast(); + const [viewMode, setViewMode] = useState('list'); + const [searchQuery, setSearchQuery] = useState(''); + const [selectedFiles, setSelectedFiles] = useState([]); + const [, setCurrentFolder] = useState('Root'); + const [files, setFiles] = useState([]); + const [loading, setLoading] = useState(true); + const [selectedFileId, setSelectedFileId] = useState(null); + const [sortField, setSortField] = useState('modified'); + const [sortOrder, setSortOrder] = useState('desc'); + const [activeTags, setActiveTags] = useState([]); + const [showMetadataModal, setShowMetadataModal] = useState(false); + const [showWatermarkModal, setShowWatermarkModal] = useState(false); + + useEffect(() => { + const loadFiles = async () => { + setLoading(true); + try { + const data = await storageService.listFiles(); + setFiles(data); + } catch (e) { + logger.error('Error loading files', { + error: e instanceof Error ? e.message : String(e), + stack: e instanceof Error ? e.stack : undefined, + }); + } finally { + setLoading(false); + } + }; + loadFiles(); + }, []); + + const filteredFiles = useMemo(() => { + return files + .filter((f) => f.name.toLowerCase().includes(searchQuery.toLowerCase())) + .filter( + (f) => + activeTags.length === 0 || f.tags?.some((t) => activeTags.includes(t)) + ) + .sort((a, b) => { + let valA: string | number = a[sortField] ?? ''; + let valB: string | number = b[sortField] ?? ''; + if (sortField === 'size') { + valA = parseInt(String(a.size || '0'), 10) || 0; + valB = parseInt(String(b.size || '0'), 10) || 0; + } + if (valA < valB) return sortOrder === 'asc' ? -1 : 1; + if (valA > valB) return sortOrder === 'asc' ? 1 : -1; + return 0; + }); + }, [files, searchQuery, activeTags, sortField, sortOrder]); + + const handleSort = (field: SortField) => { + setSortField((prev) => { + if (prev === field) { + setSortOrder((o) => (o === 'asc' ? 'desc' : 'asc')); + return prev; + } + setSortOrder('asc'); + return field; + }); + }; + const toggleTag = (tag: string) => { + setActiveTags((prev) => + prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag] + ); + }; + const toggleSelection = (id: string) => { + setSelectedFiles((prev) => + prev.includes(id) ? prev.filter((f) => f !== id) : [...prev, id] + ); + }; + const selectAll = () => { + if (selectedFiles.length === filteredFiles.length) setSelectedFiles([]); + else setSelectedFiles(filteredFiles.map((f) => f.id)); + }; + const handleFileClick = (file: CloudFileNode) => { + if (file.type === 'folder') { + setCurrentFolder(file.name); + addToast(`Navigated to ${file.name}`, 'info'); + } else { + setSelectedFileId(file.id); + } + }; + const handleBulkAction = (action: string) => { + if (selectedFiles.length === 0) return; + addToast(`${action} ${selectedFiles.length} items`, 'success'); + setSelectedFiles([]); + }; + + if (selectedFileId) { + return ( + setSelectedFileId(null)} + /> + ); + } + + if (loading) { + return ; + } + + return ( +
+ + setSortOrder((o) => (o === 'asc' ? 'desc' : 'asc')) + } + viewMode={viewMode} + onViewModeChange={setViewMode} + selectedCount={selectedFiles.length} + onBulkDownload={() => handleBulkAction('Downloaded')} + onBulkTag={() => handleBulkAction('Tagged')} + onBulkDelete={() => handleBulkAction('Deleted')} + onMetadataClick={() => setShowMetadataModal(true)} + onWatermarkClick={() => setShowWatermarkModal(true)} + availableTags={FILE_BROWSER_DEFAULT_TAGS} + /> +
+ {viewMode === 'list' ? ( + { + if (action === 'ai') addToast('Sent to AI Tools'); + }} + /> + ) : ( + + )} +
+ {showMetadataModal && ( + f.id === selectedFileId)?.name ?? + 'Selected File' + : 'Scan Library' + } + onClose={() => setShowMetadataModal(false)} + onApply={(data) => { + addToast(`Applied: ${data.genre} - ${data.bpm}BPM`, 'success'); + setShowMetadataModal(false); + }} + /> + )} + {showWatermarkModal && ( + setShowWatermarkModal(false)} + onSave={() => addToast('Watermark settings updated', 'success')} + /> + )} +
+ ); +}; diff --git a/apps/web/src/features/studio/components/cloud-file-browser/CloudFileBrowserSkeleton.stories.tsx b/apps/web/src/features/studio/components/cloud-file-browser/CloudFileBrowserSkeleton.stories.tsx new file mode 100644 index 000000000..4a7c1a851 --- /dev/null +++ b/apps/web/src/features/studio/components/cloud-file-browser/CloudFileBrowserSkeleton.stories.tsx @@ -0,0 +1,31 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { CloudFileBrowserSkeleton } from './CloudFileBrowserSkeleton'; + +const meta: Meta = { + title: 'Components/Features/Studio/CloudFileBrowser/CloudFileBrowserSkeleton', + component: CloudFileBrowserSkeleton, + tags: ['autodocs'], + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; + +type Story = StoryObj; + +export const ListView: Story = { + args: { + viewMode: 'list', + }, +}; + +export const GridView: Story = { + args: { + viewMode: 'grid', + }, +}; diff --git a/apps/web/src/features/studio/components/cloud-file-browser/CloudFileBrowserSkeleton.tsx b/apps/web/src/features/studio/components/cloud-file-browser/CloudFileBrowserSkeleton.tsx new file mode 100644 index 000000000..17ad296e5 --- /dev/null +++ b/apps/web/src/features/studio/components/cloud-file-browser/CloudFileBrowserSkeleton.tsx @@ -0,0 +1,89 @@ +import React from 'react'; +import { cn } from '@/lib/utils'; + +const ROW_COUNT = 6; + +export interface CloudFileBrowserSkeletonProps { + viewMode?: 'list' | 'grid'; + className?: string; +} + +/** + * Skeleton that mirrors CloudFileBrowser layout (toolbar + content) to avoid layout shift. + * Uses min-h-layout-page-sm for content area. + */ +export function CloudFileBrowserSkeleton({ + viewMode = 'list', + className, +}: CloudFileBrowserSkeletonProps) { + return ( +
+ {/* Toolbar skeleton */} +
+
+
+
+
+
+ {[1, 2, 3, 4, 5].map((i) => ( +
+ ))} +
+
+
+
+
+
+
+
+
+ + {/* Content skeleton */} +
+ {viewMode === 'list' ? ( +
+
+
+
+
+
+
+
+
+ {Array.from({ length: ROW_COUNT }).map((_, i) => ( +
+
+
+
+
+
+
+
+
+
+ ))} +
+
+ ) : ( +
+ {Array.from({ length: 8 }).map((_, i) => ( +
+
+
+
+
+
+
+
+
+
+ ))} +
+ )} +
+
+ ); +} diff --git a/apps/web/src/features/studio/components/cloud-file-browser/FileGrid.stories.tsx b/apps/web/src/features/studio/components/cloud-file-browser/FileGrid.stories.tsx new file mode 100644 index 000000000..09a8bba57 --- /dev/null +++ b/apps/web/src/features/studio/components/cloud-file-browser/FileGrid.stories.tsx @@ -0,0 +1,55 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { FileGrid } from './FileGrid'; +import { mockCloudFiles } from './mockFiles'; + +const meta: Meta = { + title: 'Components/Features/Studio/CloudFileBrowser/FileGrid', + component: FileGrid, + tags: ['autodocs'], + decorators: [ + (Story) => ( +
+ +
+ ), + ], + argTypes: { + onFileClick: { action: 'fileClick' }, + onToggleSelect: { action: 'toggleSelect' }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + files: mockCloudFiles, + selectedIds: [], + onFileClick: () => {}, + onToggleSelect: () => {}, + }, +}; + +export const WithSelection: Story = { + args: { + ...Default.args, + selectedIds: ['1', '4'], + }, +}; + +export const Loading: Story = { + args: { + ...Default.args, + files: [], + isLoading: true, + }, +}; + +export const Empty: Story = { + args: { + ...Default.args, + files: [], + }, +}; diff --git a/apps/web/src/features/studio/components/cloud-file-browser/FileGrid.tsx b/apps/web/src/features/studio/components/cloud-file-browser/FileGrid.tsx new file mode 100644 index 000000000..146dc099c --- /dev/null +++ b/apps/web/src/features/studio/components/cloud-file-browser/FileGrid.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import { cn } from '@/lib/utils'; +import { FileGridCard } from './FileGridCard'; +import type { CloudFileNode } from './types'; + +export interface FileGridProps { + files: CloudFileNode[]; + selectedIds: string[]; + onFileClick: (file: CloudFileNode) => void; + onToggleSelect: (id: string) => void; + isLoading?: boolean; + emptyMessage?: string; + className?: string; +} + +export function FileGrid({ + files, + selectedIds, + onFileClick, + onToggleSelect, + isLoading = false, + emptyMessage = 'No files match your filters.', + className, +}: FileGridProps) { + if (isLoading) { + return ( +
+ {Array.from({ length: 8 }).map((_, i) => ( + {}} + onClick={() => {}} + isLoading + /> + ))} +
+ ); + } + + if (files.length === 0) { + return ( +
+ {emptyMessage} +
+ ); + } + + return ( +
+ {files.map((file) => ( + + ))} +
+ ); +} diff --git a/apps/web/src/features/studio/components/cloud-file-browser/FileGridCard.stories.tsx b/apps/web/src/features/studio/components/cloud-file-browser/FileGridCard.stories.tsx new file mode 100644 index 000000000..fe6b17aef --- /dev/null +++ b/apps/web/src/features/studio/components/cloud-file-browser/FileGridCard.stories.tsx @@ -0,0 +1,63 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { FileGridCard } from './FileGridCard'; +import { mockCloudFiles } from './mockFiles'; + +const meta: Meta = { + title: 'Components/Features/Studio/CloudFileBrowser/FileGridCard', + component: FileGridCard, + tags: ['autodocs'], + decorators: [ + (Story) => ( +
+
+ +
+
+ ), + ], + argTypes: { + onSelect: { action: 'select' }, + onClick: { action: 'click' }, + }, +}; + +export default meta; + +type Story = StoryObj; + +const file = mockCloudFiles[0]; + +export const Default: Story = { + args: { + file, + selected: false, + onSelect: () => {}, + onClick: () => {}, + }, +}; + +export const Selected: Story = { + args: { + ...Default.args, + selected: true, + }, +}; + +export const Folder: Story = { + args: { + file: mockCloudFiles[1], + selected: false, + onSelect: () => {}, + onClick: () => {}, + }, +}; + +export const Loading: Story = { + args: { + file, + selected: false, + onSelect: () => {}, + onClick: () => {}, + isLoading: true, + }, +}; diff --git a/apps/web/src/features/studio/components/cloud-file-browser/FileGridCard.tsx b/apps/web/src/features/studio/components/cloud-file-browser/FileGridCard.tsx new file mode 100644 index 000000000..96de5b991 --- /dev/null +++ b/apps/web/src/features/studio/components/cloud-file-browser/FileGridCard.tsx @@ -0,0 +1,105 @@ +import React from 'react'; +import { Card } from '@/components/ui/card'; +import { CheckSquare, Square, Folder, Music, Image as ImageIcon, File } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import type { CloudFileNode } from './types'; + +export interface FileGridCardProps { + file: CloudFileNode; + selected: boolean; + onSelect: (id: string) => void; + onClick: (file: CloudFileNode) => void; + isLoading?: boolean; + className?: string; +} + +function FileTypeIcon({ type, size = 'md' }: { type: CloudFileNode['type']; size?: 'md' | 'lg' }) { + const cl = size === 'lg' ? 'w-8 h-8' : 'w-5 h-5'; + if (type === 'folder') return ; + if (type === 'audio') return ; + if (type === 'image') return ; + if (['document', 'archive', 'project'].includes(type)) { + return ; + } + return ; +} + +export function FileGridCard({ + file, + selected, + onSelect, + onClick, + isLoading = false, + className, +}: FileGridCardProps) { + if (isLoading) { + return ( + +
+
+
+
+
+
+
+
+ + ); + } + + return ( + onClick(file)} + > + + +
+ +
+
+

+ {file.name} +

+
+ {file.tags?.slice(0, 2).map((t) => ( + + {t} + + ))} +
+
+
+ ); +} diff --git a/apps/web/src/features/studio/components/cloud-file-browser/FileTable.stories.tsx b/apps/web/src/features/studio/components/cloud-file-browser/FileTable.stories.tsx new file mode 100644 index 000000000..38856f8d2 --- /dev/null +++ b/apps/web/src/features/studio/components/cloud-file-browser/FileTable.stories.tsx @@ -0,0 +1,61 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { FileTable } from './FileTable'; +import { mockCloudFiles } from './mockFiles'; + +const meta: Meta = { + title: 'Components/Features/Studio/CloudFileBrowser/FileTable', + component: FileTable, + tags: ['autodocs'], + decorators: [ + (Story) => ( +
+ +
+ ), + ], + argTypes: { + onSelectAll: { action: 'selectAll' }, + onToggleSelect: { action: 'toggleSelect' }, + onSort: { action: 'sort' }, + onFileClick: { action: 'fileClick' }, + onRowAction: { action: 'rowAction' }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + files: mockCloudFiles, + selectedIds: [], + sortField: 'modified', + onSelectAll: () => {}, + onToggleSelect: () => {}, + onSort: () => {}, + onFileClick: () => {}, + }, +}; + +export const WithSelection: Story = { + args: { + ...Default.args, + selectedIds: ['1', '3', '5'], + }, +}; + +export const Loading: Story = { + args: { + ...Default.args, + files: [], + isLoading: true, + }, +}; + +export const Empty: Story = { + args: { + ...Default.args, + files: [], + }, +}; diff --git a/apps/web/src/features/studio/components/cloud-file-browser/FileTable.tsx b/apps/web/src/features/studio/components/cloud-file-browser/FileTable.tsx new file mode 100644 index 000000000..f25ca2993 --- /dev/null +++ b/apps/web/src/features/studio/components/cloud-file-browser/FileTable.tsx @@ -0,0 +1,117 @@ +import React from 'react'; +import { CheckSquare, Square } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { FileTableRow } from './FileTableRow'; +import type { CloudFileNode, SortField } from './types'; + +export interface FileTableProps { + files: CloudFileNode[]; + selectedIds: string[]; + onSelectAll: () => void; + onToggleSelect: (id: string) => void; + sortField: SortField; + onSort: (field: SortField) => void; + onFileClick: (file: CloudFileNode) => void; + onRowAction?: (action: string, file: CloudFileNode) => void; + isLoading?: boolean; + className?: string; +} + +export function FileTable({ + files, + selectedIds, + onSelectAll, + onToggleSelect, + sortField, + onSort, + onFileClick, + onRowAction, + isLoading = false, + className, +}: FileTableProps) { + const allSelected = files.length > 0 && selectedIds.length === files.length; + + return ( +
+ + + + + + + + + + + + + {isLoading + ? Array.from({ length: 5 }).map((_, i) => ( + {}} + onFileClick={() => {}} + isLoading + /> + )) + : files.length === 0 + ? ( + + + + ) + : files.map((file) => ( + + ))} + +
+ + onSort('name')} + > + Name + Tags onSort('size')} + > + Size + onSort('modified')} + > + Modified + Actions
+ No files match your filters. +
+
+ ); +} diff --git a/apps/web/src/features/studio/components/cloud-file-browser/FileTableRow.stories.tsx b/apps/web/src/features/studio/components/cloud-file-browser/FileTableRow.stories.tsx new file mode 100644 index 000000000..743fc6714 --- /dev/null +++ b/apps/web/src/features/studio/components/cloud-file-browser/FileTableRow.stories.tsx @@ -0,0 +1,80 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { FileTableRow } from './FileTableRow'; +import { mockCloudFiles } from './mockFiles'; + +const meta: Meta = { + title: 'Components/Features/Studio/CloudFileBrowser/FileTableRow', + component: FileTableRow, + tags: ['autodocs'], + decorators: [ + (Story) => ( +
+ + + + + + + + + + + + + +
+ NameTagsSizeModifiedActions
+
+ ), + ], + argTypes: { + onToggleSelect: { action: 'toggleSelect' }, + onFileClick: { action: 'fileClick' }, + onAction: { action: 'action' }, + }, +}; + +export default meta; + +type Story = StoryObj; + +const file = mockCloudFiles[0]; + +export const Default: Story = { + args: { + file, + selected: false, + onToggleSelect: () => {}, + onFileClick: () => {}, + }, + render: (args) => , +}; + +export const Selected: Story = { + args: { + ...Default.args, + selected: true, + }, + render: (args) => , +}; + +export const Folder: Story = { + args: { + file: mockCloudFiles[1], + selected: false, + onToggleSelect: () => {}, + onFileClick: () => {}, + }, + render: (args) => , +}; + +export const Loading: Story = { + args: { + file, + selected: false, + onToggleSelect: () => {}, + onFileClick: () => {}, + isLoading: true, + }, + render: (args) => , +}; diff --git a/apps/web/src/features/studio/components/cloud-file-browser/FileTableRow.tsx b/apps/web/src/features/studio/components/cloud-file-browser/FileTableRow.tsx new file mode 100644 index 000000000..c60217bbf --- /dev/null +++ b/apps/web/src/features/studio/components/cloud-file-browser/FileTableRow.tsx @@ -0,0 +1,155 @@ +import React from 'react'; +import { Button } from '@/components/ui/button'; +import { + CheckSquare, + Square, + Folder, + Music, + Image as ImageIcon, + File, + Share2, + MoreVertical, + Wand2, +} from 'lucide-react'; +import { cn } from '@/lib/utils'; +import type { CloudFileNode } from './types'; + +export interface FileTableRowProps { + file: CloudFileNode; + selected: boolean; + onToggleSelect: (id: string) => void; + onFileClick: (file: CloudFileNode) => void; + onAction?: (action: string, file: CloudFileNode) => void; + isLoading?: boolean; +} + +function FileTypeIcon({ type }: { type: CloudFileNode['type'] }) { + if (type === 'folder') return ; + if (type === 'audio') return ; + if (type === 'image') return ; + if (['document', 'archive', 'project'].includes(type)) { + return ; + } + return ; +} + +export function FileTableRow({ + file, + selected, + onToggleSelect, + onFileClick, + onAction, + isLoading = false, +}: FileTableRowProps) { + if (isLoading) { + return ( + + +
+ + +
+
+
+
+ + +
+
+
+
+ + +
+ + +
+ + +
+ + + ); + } + + return ( + + + + + + + + +
+ {file.tags?.map((t) => ( + + {t} + + ))} +
+ + {file.size} + {file.modified} + +
+ {file.type === 'audio' && ( + + )} + + +
+ + + ); +} diff --git a/apps/web/src/features/studio/components/cloud-file-browser/FileToolbar.stories.tsx b/apps/web/src/features/studio/components/cloud-file-browser/FileToolbar.stories.tsx new file mode 100644 index 000000000..c3328ad6d --- /dev/null +++ b/apps/web/src/features/studio/components/cloud-file-browser/FileToolbar.stories.tsx @@ -0,0 +1,75 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { FileToolbar } from './FileToolbar'; +import { FILE_BROWSER_DEFAULT_TAGS } from './types'; + +const meta: Meta = { + title: 'Components/Features/Studio/CloudFileBrowser/FileToolbar', + component: FileToolbar, + tags: ['autodocs'], + decorators: [ + (Story) => ( +
+ +
+ ), + ], + argTypes: { + onSearchChange: { action: 'search' }, + onTagToggle: { action: 'tagToggle' }, + onSortFieldChange: { action: 'sortField' }, + onSortOrderToggle: { action: 'sortOrder' }, + onViewModeChange: { action: 'viewMode' }, + onBulkDownload: { action: 'bulkDownload' }, + onBulkTag: { action: 'bulkTag' }, + onBulkDelete: { action: 'bulkDelete' }, + onMetadataClick: { action: 'metadata' }, + onWatermarkClick: { action: 'watermark' }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + searchQuery: '', + activeTags: [], + sortField: 'modified', + sortOrder: 'desc', + viewMode: 'list', + selectedCount: 0, + availableTags: FILE_BROWSER_DEFAULT_TAGS, + }, +}; + +export const WithSearchAndTags: Story = { + args: { + ...Default.args, + searchQuery: 'Vocals', + activeTags: ['Vocals', 'Stem'], + }, +}; + +export const WithSelection: Story = { + args: { + ...Default.args, + selectedCount: 3, + }, +}; + +export const Loading: Story = { + args: { + ...Default.args, + isLoading: true, + }, +}; + +export const Empty: Story = { + args: { + ...Default.args, + searchQuery: '', + activeTags: [], + selectedCount: 0, + }, +}; diff --git a/apps/web/src/features/studio/components/cloud-file-browser/FileToolbar.tsx b/apps/web/src/features/studio/components/cloud-file-browser/FileToolbar.tsx new file mode 100644 index 000000000..ba41c86e6 --- /dev/null +++ b/apps/web/src/features/studio/components/cloud-file-browser/FileToolbar.tsx @@ -0,0 +1,237 @@ +import React from 'react'; +import { Button } from '@/components/ui/button'; +import { SearchInput } from '@/components/ui/input'; +import { + LayoutGrid, + List, + Filter, + Download, + Trash2, + Tag, + ArrowUp, + ArrowDown, + Wand2, + Stamp, +} from 'lucide-react'; +import { cn } from '@/lib/utils'; +import type { SortField, SortOrder, ViewMode } from './types'; +import { FILE_BROWSER_DEFAULT_TAGS } from './types'; + +const TAG_SLICE = 5; + +export interface FileToolbarProps { + searchQuery: string; + onSearchChange: (value: string) => void; + activeTags: string[]; + onTagToggle: (tag: string) => void; + sortField: SortField; + sortOrder: SortOrder; + onSortFieldChange: (field: SortField) => void; + onSortOrderToggle: () => void; + viewMode: ViewMode; + onViewModeChange: (mode: ViewMode) => void; + selectedCount: number; + onBulkDownload?: () => void; + onBulkTag?: () => void; + onBulkDelete?: () => void; + onMetadataClick?: () => void; + onWatermarkClick?: () => void; + availableTags?: readonly string[]; + isLoading?: boolean; + className?: string; +} + +export function FileToolbar({ + searchQuery, + onSearchChange, + activeTags, + onTagToggle, + sortField, + sortOrder, + onSortFieldChange, + onSortOrderToggle, + viewMode, + onViewModeChange, + selectedCount, + onBulkDownload, + onBulkTag, + onBulkDelete, + onMetadataClick, + onWatermarkClick, + availableTags = FILE_BROWSER_DEFAULT_TAGS, + isLoading = false, + className, +}: FileToolbarProps) { + const tagsToShow = availableTags.slice(0, TAG_SLICE); + + if (isLoading) { + return ( +
+
+
+
+
+
+ {tagsToShow.map((_, i) => ( +
+ ))} +
+
+
+
+
+
+
+
+
+ ); + } + + return ( +
+
+
+ onSearchChange(e.target.value)} + /> +
+
+ + {tagsToShow.map((tag) => ( + + ))} +
+
+ +
+ {selectedCount > 0 && ( +
+ + + +
+ )} + +
+ + +
+ + Sort: + + + +
+ +
+ + +
+
+
+
+ ); +} diff --git a/apps/web/src/features/studio/components/cloud-file-browser/index.ts b/apps/web/src/features/studio/components/cloud-file-browser/index.ts new file mode 100644 index 000000000..71e5a3a67 --- /dev/null +++ b/apps/web/src/features/studio/components/cloud-file-browser/index.ts @@ -0,0 +1,21 @@ +export { CloudFileBrowser } from './CloudFileBrowser'; +export { FileToolbar } from './FileToolbar'; +export { FileTable } from './FileTable'; +export { FileTableRow } from './FileTableRow'; +export { FileGrid } from './FileGrid'; +export { FileGridCard } from './FileGridCard'; +export { CloudFileBrowserSkeleton } from './CloudFileBrowserSkeleton'; +export { mockCloudFiles } from './mockFiles'; +export { + FILE_BROWSER_DEFAULT_TAGS, + type CloudFileNode, + type SortField, + type SortOrder, + type ViewMode, +} from './types'; +export type { FileToolbarProps } from './FileToolbar'; +export type { FileTableProps } from './FileTable'; +export type { FileTableRowProps } from './FileTableRow'; +export type { FileGridProps } from './FileGrid'; +export type { FileGridCardProps } from './FileGridCard'; +export type { CloudFileBrowserSkeletonProps } from './CloudFileBrowserSkeleton'; diff --git a/apps/web/src/features/studio/components/cloud-file-browser/mockFiles.ts b/apps/web/src/features/studio/components/cloud-file-browser/mockFiles.ts new file mode 100644 index 000000000..6d2929203 --- /dev/null +++ b/apps/web/src/features/studio/components/cloud-file-browser/mockFiles.ts @@ -0,0 +1,67 @@ +import type { CloudFileNode } from './types'; + +export const mockCloudFiles: CloudFileNode[] = [ + { + id: '1', + name: 'Vocals_Main.wav', + type: 'audio', + size: '24 MB', + modified: '2023-10-25', + status: 'ready', + tags: ['Vocals', 'Raw'], + }, + { + id: '2', + name: 'Project_Alpha_v3', + type: 'folder', + size: '-', + modified: '2023-10-24', + status: 'ready', + tags: ['Project'], + }, + { + id: '3', + name: 'Bass_Drop_F#m.wav', + type: 'audio', + size: '12 MB', + modified: '2023-10-22', + status: 'processing', + tags: ['Bass', 'One-Shot'], + }, + { + id: '4', + name: 'Album_Cover_Art.png', + type: 'image', + size: '4 MB', + modified: '2023-10-15', + status: 'ready', + tags: ['Art', 'Final'], + }, + { + id: '5', + name: 'Reference_Track_01.mp3', + type: 'audio', + size: '8 MB', + modified: '2023-10-10', + status: 'ready', + tags: ['Reference'], + }, + { + id: '6', + name: 'Legal_Contracts.pdf', + type: 'document', + size: '2 MB', + modified: '2023-09-01', + status: 'archived', + tags: ['Legal'], + }, + { + id: '7', + name: 'Drums_Stem_02.wav', + type: 'audio', + size: '45 MB', + modified: '2023-10-25', + status: 'ready', + tags: ['Drums', 'Stem'], + }, +]; diff --git a/apps/web/src/features/studio/components/cloud-file-browser/types.ts b/apps/web/src/features/studio/components/cloud-file-browser/types.ts new file mode 100644 index 000000000..13f821918 --- /dev/null +++ b/apps/web/src/features/studio/components/cloud-file-browser/types.ts @@ -0,0 +1,20 @@ +import type { FileNode } from '@/types'; + +/** File node with optional tags for cloud browser */ +export type CloudFileNode = FileNode & { tags?: string[] }; + +export type SortField = 'name' | 'size' | 'modified' | 'type'; +export type SortOrder = 'asc' | 'desc'; +export type ViewMode = 'list' | 'grid'; + +export const FILE_BROWSER_DEFAULT_TAGS = [ + 'Vocals', + 'Bass', + 'Drums', + 'Project', + 'Art', + 'Legal', + 'Reference', + 'Stem', + 'Raw', +] as const;