refactor(doclist): flatten foldered document structure and remove folder UI
Eliminate folder grouping logic and related UI from all document list views. Switch to a flat document array model for rendering, selection, and filtering. Remove folder collapse/expand state and folder drag-and-drop handling. Update sidebar and toolbar components to remove folder creation and management controls. Simplify props and internal state across all views for consistency.
This commit is contained in:
parent
d59e911ca2
commit
85aff2c1be
7 changed files with 176 additions and 580 deletions
|
|
@ -123,7 +123,6 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) {
|
||||||
const [viewMode, setViewMode] = useState<ViewMode>(DEFAULT_STATE.viewMode);
|
const [viewMode, setViewMode] = useState<ViewMode>(DEFAULT_STATE.viewMode);
|
||||||
const [iconSize, setIconSize] = useState<IconSize>(DEFAULT_STATE.iconSize);
|
const [iconSize, setIconSize] = useState<IconSize>(DEFAULT_STATE.iconSize);
|
||||||
const [folders, setFolders] = useState<Folder[]>(DEFAULT_STATE.folders);
|
const [folders, setFolders] = useState<Folder[]>(DEFAULT_STATE.folders);
|
||||||
const [collapsedFolders, setCollapsedFolders] = useState<Set<string>>(new Set());
|
|
||||||
const [showHint, setShowHint] = useState(true);
|
const [showHint, setShowHint] = useState(true);
|
||||||
const [sidebarWidth, setSidebarWidth] = useState(DEFAULT_STATE.sidebarWidth);
|
const [sidebarWidth, setSidebarWidth] = useState(DEFAULT_STATE.sidebarWidth);
|
||||||
const [sidebarFilter, setSidebarFilter] = useState<SidebarFilter>('all');
|
const [sidebarFilter, setSidebarFilter] = useState<SidebarFilter>('all');
|
||||||
|
|
@ -167,7 +166,6 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) {
|
||||||
setSortBy(saved.sortBy);
|
setSortBy(saved.sortBy);
|
||||||
setSortDirection(saved.sortDirection);
|
setSortDirection(saved.sortDirection);
|
||||||
setFolders(saved.folders ?? []);
|
setFolders(saved.folders ?? []);
|
||||||
setCollapsedFolders(new Set(saved.collapsedFolders ?? []));
|
|
||||||
setShowHint(saved.showHint ?? true);
|
setShowHint(saved.showHint ?? true);
|
||||||
setViewMode(normalizeViewMode(saved.viewMode));
|
setViewMode(normalizeViewMode(saved.viewMode));
|
||||||
setIconSize(saved.iconSize ?? DEFAULT_STATE.iconSize);
|
setIconSize(saved.iconSize ?? DEFAULT_STATE.iconSize);
|
||||||
|
|
@ -189,7 +187,7 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) {
|
||||||
sortBy,
|
sortBy,
|
||||||
sortDirection,
|
sortDirection,
|
||||||
folders,
|
folders,
|
||||||
collapsedFolders: Array.from(collapsedFolders),
|
collapsedFolders: [],
|
||||||
showHint,
|
showHint,
|
||||||
viewMode,
|
viewMode,
|
||||||
iconSize,
|
iconSize,
|
||||||
|
|
@ -202,7 +200,6 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) {
|
||||||
sortBy,
|
sortBy,
|
||||||
sortDirection,
|
sortDirection,
|
||||||
folders,
|
folders,
|
||||||
collapsedFolders,
|
|
||||||
showHint,
|
showHint,
|
||||||
viewMode,
|
viewMode,
|
||||||
iconSize,
|
iconSize,
|
||||||
|
|
@ -257,22 +254,45 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) {
|
||||||
[rawDocuments, recentlyOpenedById],
|
[rawDocuments, recentlyOpenedById],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Reconcile folders against server.
|
const allDocumentsById = useMemo(() => {
|
||||||
useEffect(() => {
|
const map = new Map<string, DocumentListDocument>();
|
||||||
if (!isInitialized) return;
|
for (const doc of allDocuments) map.set(doc.id, doc);
|
||||||
const ids = new Set(allDocuments.map((d) => d.id));
|
return map;
|
||||||
setFolders((prev) =>
|
}, [allDocuments]);
|
||||||
prev.map((f) => ({
|
|
||||||
...f,
|
const foldersWithLiveDocs = useMemo(
|
||||||
documents: f.documents.filter((d) => ids.has(d.id)),
|
() =>
|
||||||
|
folders.map((folder) => ({
|
||||||
|
...folder,
|
||||||
|
documents: folder.documents
|
||||||
|
.map((d) => allDocumentsById.get(d.id))
|
||||||
|
.filter((d): d is DocumentListDocument => Boolean(d))
|
||||||
|
.map((d) => ({ ...d, folderId: folder.id })),
|
||||||
})),
|
})),
|
||||||
);
|
[folders, allDocumentsById],
|
||||||
}, [isInitialized, allDocuments]);
|
);
|
||||||
|
|
||||||
|
const folderIdByDocId = useMemo(() => {
|
||||||
|
const map = new Map<string, string>();
|
||||||
|
for (const folder of foldersWithLiveDocs) {
|
||||||
|
for (const doc of folder.documents) map.set(doc.id, folder.id);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}, [foldersWithLiveDocs]);
|
||||||
|
|
||||||
|
const allDocumentsWithFolder = useMemo(
|
||||||
|
() =>
|
||||||
|
allDocuments.map((doc) => ({
|
||||||
|
...doc,
|
||||||
|
folderId: folderIdByDocId.get(doc.id),
|
||||||
|
})),
|
||||||
|
[allDocuments, folderIdByDocId],
|
||||||
|
);
|
||||||
|
|
||||||
// Filter based on sidebar selection + search query.
|
// Filter based on sidebar selection + search query.
|
||||||
const visibleAll = useMemo(() => {
|
const visibleDocuments = useMemo(() => {
|
||||||
const q = query.trim().toLowerCase();
|
const q = query.trim().toLowerCase();
|
||||||
let docs = allDocuments;
|
let docs = allDocumentsWithFolder;
|
||||||
if (sidebarFilter === 'pdf') docs = docs.filter((d) => d.type === 'pdf');
|
if (sidebarFilter === 'pdf') docs = docs.filter((d) => d.type === 'pdf');
|
||||||
else if (sidebarFilter === 'epub') docs = docs.filter((d) => d.type === 'epub');
|
else if (sidebarFilter === 'epub') docs = docs.filter((d) => d.type === 'epub');
|
||||||
else if (sidebarFilter === 'html') docs = docs.filter((d) => d.type === 'html');
|
else if (sidebarFilter === 'html') docs = docs.filter((d) => d.type === 'html');
|
||||||
|
|
@ -283,42 +303,23 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) {
|
||||||
.slice(0, 20);
|
.slice(0, 20);
|
||||||
} else if (sidebarFilter.startsWith('folder:')) {
|
} else if (sidebarFilter.startsWith('folder:')) {
|
||||||
const fid = sidebarFilter.slice('folder:'.length);
|
const fid = sidebarFilter.slice('folder:'.length);
|
||||||
const folder = folders.find((f) => f.id === fid);
|
const folder = foldersWithLiveDocs.find((f) => f.id === fid);
|
||||||
docs = folder ? folder.documents : [];
|
docs = folder
|
||||||
|
? folder.documents
|
||||||
|
.map((d) => allDocumentsById.get(d.id))
|
||||||
|
.filter((d): d is DocumentListDocument => Boolean(d))
|
||||||
|
.map((d) => ({ ...d, folderId: fid }))
|
||||||
|
: [];
|
||||||
}
|
}
|
||||||
if (q) docs = docs.filter((d) => d.name.toLowerCase().includes(q));
|
if (q) docs = docs.filter((d) => d.name.toLowerCase().includes(q));
|
||||||
return docs;
|
return docs;
|
||||||
}, [allDocuments, sidebarFilter, query, folders]);
|
}, [allDocumentsWithFolder, sidebarFilter, query, foldersWithLiveDocs, allDocumentsById]);
|
||||||
|
|
||||||
// Apply sort.
|
// Apply sort.
|
||||||
const sortedVisible = useMemo(() => {
|
const sortedVisible = useMemo(() => {
|
||||||
if (sidebarFilter === 'recents') return visibleAll;
|
if (sidebarFilter === 'recents') return visibleDocuments;
|
||||||
return sortDocs(visibleAll, sortBy, sortDirection);
|
return sortDocs(visibleDocuments, sortBy, sortDirection);
|
||||||
}, [visibleAll, sidebarFilter, sortBy, sortDirection]);
|
}, [visibleDocuments, sidebarFilter, sortBy, sortDirection]);
|
||||||
|
|
||||||
// Split into folders + unfoldered for the current filter context.
|
|
||||||
const showAllScope = sidebarFilter === 'all';
|
|
||||||
const visibleFolders = useMemo<Folder[]>(() => {
|
|
||||||
if (!showAllScope) return [];
|
|
||||||
// Within a kind-filter (pdf/epub/html), still show folders that contain matches.
|
|
||||||
return folders.map((f) => ({
|
|
||||||
...f,
|
|
||||||
documents: sortDocs(
|
|
||||||
f.documents.filter((d) =>
|
|
||||||
query ? d.name.toLowerCase().includes(query.toLowerCase()) : true,
|
|
||||||
),
|
|
||||||
sortBy,
|
|
||||||
sortDirection,
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
}, [folders, showAllScope, query, sortBy, sortDirection]);
|
|
||||||
|
|
||||||
const unfolderedDocs = useMemo(() => {
|
|
||||||
if (sidebarFilter === 'recents') return sortedVisible;
|
|
||||||
const inFolder = new Set<string>();
|
|
||||||
folders.forEach((f) => f.documents.forEach((d) => inFolder.add(d.id)));
|
|
||||||
return sortedVisible.filter((d) => !inFolder.has(d.id));
|
|
||||||
}, [folders, sidebarFilter, sortedVisible]);
|
|
||||||
|
|
||||||
const counts = useMemo(
|
const counts = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
|
@ -376,6 +377,7 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) {
|
||||||
return { ...f, documents: [...f.documents, ...newDocs] };
|
return { ...f, documents: [...f.documents, ...newDocs] };
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
setSidebarFilter(`folder:${folderId}`);
|
||||||
selection.clear();
|
selection.clear();
|
||||||
},
|
},
|
||||||
[selection],
|
[selection],
|
||||||
|
|
@ -412,6 +414,7 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) {
|
||||||
setPendingMerge(null);
|
setPendingMerge(null);
|
||||||
setNewFolderName('');
|
setNewFolderName('');
|
||||||
setShowHint(false);
|
setShowHint(false);
|
||||||
|
setSidebarFilter(`folder:${folderId}`);
|
||||||
selection.clear();
|
selection.clear();
|
||||||
}, [pendingMerge, newFolderName, selection]);
|
}, [pendingMerge, newFolderName, selection]);
|
||||||
|
|
||||||
|
|
@ -421,6 +424,7 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) {
|
||||||
setFolders((prev) => [...prev, { id: folderId, name, documents: [] }]);
|
setFolders((prev) => [...prev, { id: folderId, name, documents: [] }]);
|
||||||
setNewFolderName('');
|
setNewFolderName('');
|
||||||
setManualFolderPrompt(false);
|
setManualFolderPrompt(false);
|
||||||
|
setSidebarFilter(`folder:${folderId}`);
|
||||||
}, [newFolderName]);
|
}, [newFolderName]);
|
||||||
|
|
||||||
const handleDeleteFolder = useCallback((folderId: string) => {
|
const handleDeleteFolder = useCallback((folderId: string) => {
|
||||||
|
|
@ -428,15 +432,6 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) {
|
||||||
if (sidebarFilter === `folder:${folderId}`) setSidebarFilter('all');
|
if (sidebarFilter === `folder:${folderId}`) setSidebarFilter('all');
|
||||||
}, [sidebarFilter]);
|
}, [sidebarFilter]);
|
||||||
|
|
||||||
const toggleFolderCollapse = useCallback((folderId: string) => {
|
|
||||||
setCollapsedFolders((prev) => {
|
|
||||||
const next = new Set(prev);
|
|
||||||
if (next.has(folderId)) next.delete(folderId);
|
|
||||||
else next.add(folderId);
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Status bar summary.
|
// Status bar summary.
|
||||||
const summary = useMemo(() => {
|
const summary = useMemo(() => {
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
|
|
@ -473,10 +468,6 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) {
|
||||||
}
|
}
|
||||||
query={query}
|
query={query}
|
||||||
onQueryChange={setQuery}
|
onQueryChange={setQuery}
|
||||||
onNewFolder={() => {
|
|
||||||
setNewFolderName('');
|
|
||||||
setManualFolderPrompt(true);
|
|
||||||
}}
|
|
||||||
onToggleSidebar={() =>
|
onToggleSidebar={() =>
|
||||||
isNarrow
|
isNarrow
|
||||||
? setMobileSidebarOpen((p) => !p)
|
? setMobileSidebarOpen((p) => !p)
|
||||||
|
|
@ -492,8 +483,13 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) {
|
||||||
<FinderSidebar
|
<FinderSidebar
|
||||||
filter={sidebarFilter}
|
filter={sidebarFilter}
|
||||||
onFilterChange={setSidebarFilter}
|
onFilterChange={setSidebarFilter}
|
||||||
folders={folders}
|
folders={foldersWithLiveDocs}
|
||||||
counts={counts}
|
counts={counts}
|
||||||
|
onDeleteFolder={handleDeleteFolder}
|
||||||
|
onNewFolder={() => {
|
||||||
|
setNewFolderName('');
|
||||||
|
setManualFolderPrompt(true);
|
||||||
|
}}
|
||||||
onDropOnFolder={handleDropOnFolder}
|
onDropOnFolder={handleDropOnFolder}
|
||||||
width={sidebarWidth}
|
width={sidebarWidth}
|
||||||
onWidthChange={setSidebarWidth}
|
onWidthChange={setSidebarWidth}
|
||||||
|
|
@ -546,50 +542,36 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) {
|
||||||
<DocumentUploader variant="overlay" className="flex-1 min-h-0 flex flex-col">
|
<DocumentUploader variant="overlay" className="flex-1 min-h-0 flex flex-col">
|
||||||
{fallbackViewMode === 'icons' && (
|
{fallbackViewMode === 'icons' && (
|
||||||
<IconsView
|
<IconsView
|
||||||
folders={visibleFolders}
|
documents={sortedVisible}
|
||||||
unfolderedDocs={unfolderedDocs}
|
|
||||||
iconSize={iconSize}
|
iconSize={iconSize}
|
||||||
collapsedFolders={collapsedFolders}
|
|
||||||
onToggleCollapse={toggleFolderCollapse}
|
|
||||||
onDeleteFolder={handleDeleteFolder}
|
|
||||||
onDeleteDoc={handleDeleteDoc}
|
onDeleteDoc={handleDeleteDoc}
|
||||||
onDropOnFolder={handleDropOnFolder}
|
|
||||||
onMergeIntoFolder={handleMergeIntoFolder}
|
onMergeIntoFolder={handleMergeIntoFolder}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{fallbackViewMode === 'list' && (
|
{fallbackViewMode === 'list' && (
|
||||||
<ListView
|
<ListView
|
||||||
folders={visibleFolders}
|
documents={sortedVisible}
|
||||||
unfolderedDocs={unfolderedDocs}
|
|
||||||
sortBy={sortBy}
|
sortBy={sortBy}
|
||||||
sortDirection={sortDirection}
|
sortDirection={sortDirection}
|
||||||
onSortChange={(b, d) => {
|
onSortChange={(b, d) => {
|
||||||
setSortBy(b);
|
setSortBy(b);
|
||||||
setSortDirection(d);
|
setSortDirection(d);
|
||||||
}}
|
}}
|
||||||
collapsedFolders={collapsedFolders}
|
|
||||||
onToggleCollapse={toggleFolderCollapse}
|
|
||||||
onDeleteFolder={handleDeleteFolder}
|
|
||||||
onDeleteDoc={handleDeleteDoc}
|
onDeleteDoc={handleDeleteDoc}
|
||||||
onDropOnFolder={handleDropOnFolder}
|
|
||||||
onMergeIntoFolder={handleMergeIntoFolder}
|
onMergeIntoFolder={handleMergeIntoFolder}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{fallbackViewMode === 'columns' && (
|
{fallbackViewMode === 'columns' && (
|
||||||
<ColumnsView
|
<ColumnsView
|
||||||
folders={visibleFolders}
|
documents={sortedVisible}
|
||||||
unfolderedDocs={unfolderedDocs}
|
|
||||||
onDeleteDoc={handleDeleteDoc}
|
onDeleteDoc={handleDeleteDoc}
|
||||||
onDropOnFolder={handleDropOnFolder}
|
|
||||||
onMergeIntoFolder={handleMergeIntoFolder}
|
onMergeIntoFolder={handleMergeIntoFolder}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{fallbackViewMode === 'gallery' && (
|
{fallbackViewMode === 'gallery' && (
|
||||||
<GalleryView
|
<GalleryView
|
||||||
folders={visibleFolders}
|
documents={sortedVisible}
|
||||||
unfolderedDocs={unfolderedDocs}
|
|
||||||
onDeleteDoc={handleDeleteDoc}
|
onDeleteDoc={handleDeleteDoc}
|
||||||
onDropOnFolder={handleDropOnFolder}
|
|
||||||
onMergeIntoFolder={handleMergeIntoFolder}
|
onMergeIntoFolder={handleMergeIntoFolder}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,21 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useDrag, useDrop } from 'react-dnd';
|
import { useDrag, useDrop } from 'react-dnd';
|
||||||
import type { DocumentListDocument, Folder } from '@/types/documents';
|
import type { DocumentListDocument } from '@/types/documents';
|
||||||
import { PDFIcon, EPUBIcon, FileIcon } from '@/components/icons/Icons';
|
import { PDFIcon, EPUBIcon, FileIcon } from '@/components/icons/Icons';
|
||||||
import { FolderIcon, ChevronRightSmall } from '../window/finderIcons';
|
|
||||||
import { DocumentPreview } from '@/components/doclist/DocumentPreview';
|
import { DocumentPreview } from '@/components/doclist/DocumentPreview';
|
||||||
import { formatDocumentSize } from '@/components/doclist/formatSize';
|
import { formatDocumentSize } from '@/components/doclist/formatSize';
|
||||||
import { useDocumentSelection } from '../dnd/DocumentSelectionContext';
|
import { useDocumentSelection } from '../dnd/DocumentSelectionContext';
|
||||||
import { DND_DOCUMENT, type DocumentDragItem } from '../dnd/dndTypes';
|
import { DND_DOCUMENT, type DocumentDragItem } from '../dnd/dndTypes';
|
||||||
|
|
||||||
interface ColumnsViewProps {
|
interface ColumnsViewProps {
|
||||||
folders: Folder[];
|
documents: DocumentListDocument[];
|
||||||
unfolderedDocs: DocumentListDocument[];
|
|
||||||
onDeleteDoc: (doc: DocumentListDocument) => void;
|
onDeleteDoc: (doc: DocumentListDocument) => void;
|
||||||
onDropOnFolder: (folderId: string, item: DocumentDragItem) => void;
|
|
||||||
onMergeIntoFolder: (sources: DocumentListDocument[], target: DocumentListDocument) => void;
|
onMergeIntoFolder: (sources: DocumentListDocument[], target: DocumentListDocument) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Selected =
|
|
||||||
| { kind: 'folder'; id: string }
|
|
||||||
| { kind: 'doc'; doc: DocumentListDocument }
|
|
||||||
| null;
|
|
||||||
|
|
||||||
function KindIcon({ doc }: { doc: DocumentListDocument }) {
|
function KindIcon({ doc }: { doc: DocumentListDocument }) {
|
||||||
if (doc.type === 'pdf') return <PDFIcon className="w-4 h-4 text-red-500" />;
|
if (doc.type === 'pdf') return <PDFIcon className="w-4 h-4 text-red-500" />;
|
||||||
if (doc.type === 'epub') return <EPUBIcon className="w-4 h-4 text-blue-500" />;
|
if (doc.type === 'epub') return <EPUBIcon className="w-4 h-4 text-blue-500" />;
|
||||||
|
|
@ -90,46 +82,6 @@ function ColumnDocRow({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ColumnFolderRow({
|
|
||||||
folder,
|
|
||||||
active,
|
|
||||||
onClick,
|
|
||||||
onDropOnFolder,
|
|
||||||
}: {
|
|
||||||
folder: Folder;
|
|
||||||
active: boolean;
|
|
||||||
onClick: () => void;
|
|
||||||
onDropOnFolder: (folderId: string, item: DocumentDragItem) => void;
|
|
||||||
}) {
|
|
||||||
const [{ isOver, canDrop }, dropRef] = useDrop<DocumentDragItem, void, { isOver: boolean; canDrop: boolean }>(() => ({
|
|
||||||
accept: DND_DOCUMENT,
|
|
||||||
canDrop: (item) => item.docs.some((d) => d.folderId !== folder.id),
|
|
||||||
drop: (item) => onDropOnFolder(folder.id, item),
|
|
||||||
collect: (m) => ({ isOver: m.isOver({ shallow: true }), canDrop: m.canDrop() }),
|
|
||||||
}), [folder.id, onDropOnFolder]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={dropRef as unknown as React.RefObject<HTMLDivElement>}
|
|
||||||
onClick={onClick}
|
|
||||||
className={
|
|
||||||
'group flex items-center gap-2 px-2 py-1 rounded-md text-[12px] border transform transition-all duration-200 ease-out cursor-pointer text-left hover:scale-[1.01] ' +
|
|
||||||
(active
|
|
||||||
? 'border-accent bg-offbase text-accent'
|
|
||||||
: 'border-transparent text-foreground hover:border-accent hover:bg-offbase hover:text-accent') +
|
|
||||||
(isOver && canDrop ? ' ring-1 ring-accent' : '')
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FolderIcon className={'w-4 h-4 ' + (active ? 'text-accent' : 'text-muted group-hover:text-accent')} />
|
|
||||||
<span className="truncate flex-1">{folder.name}</span>
|
|
||||||
<span className={'text-[10px] ' + (active ? 'text-accent' : 'text-muted')}>
|
|
||||||
{folder.documents.length}
|
|
||||||
</span>
|
|
||||||
<ChevronRightSmall className={'w-3 h-3 ' + (active ? 'text-accent' : 'text-muted group-hover:text-accent')} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Column({ children, title }: { children: React.ReactNode; title?: string }) {
|
function Column({ children, title }: { children: React.ReactNode; title?: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="w-[260px] shrink-0 h-full bg-base border-r border-offbase overflow-y-auto">
|
<div className="w-[260px] shrink-0 h-full bg-base border-r border-offbase overflow-y-auto">
|
||||||
|
|
@ -144,115 +96,72 @@ function Column({ children, title }: { children: React.ReactNode; title?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ColumnsView({
|
export function ColumnsView({
|
||||||
folders,
|
documents,
|
||||||
unfolderedDocs,
|
|
||||||
onDeleteDoc,
|
onDeleteDoc,
|
||||||
onDropOnFolder,
|
|
||||||
onMergeIntoFolder,
|
onMergeIntoFolder,
|
||||||
}: ColumnsViewProps) {
|
}: ColumnsViewProps) {
|
||||||
const { setVisibleOrder } = useDocumentSelection();
|
const { setVisibleOrder } = useDocumentSelection();
|
||||||
const [selected, setSelected] = useState<Selected>(null);
|
const [selectedDoc, setSelectedDoc] = useState<DocumentListDocument | null>(null);
|
||||||
const allDocs = useMemo(
|
|
||||||
() => [...folders.flatMap((f) => f.documents), ...unfolderedDocs],
|
|
||||||
[folders, unfolderedDocs],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setVisibleOrder(allDocs);
|
setVisibleOrder(documents);
|
||||||
}, [allDocs, setVisibleOrder]);
|
}, [documents, setVisibleOrder]);
|
||||||
|
|
||||||
// Keep selection valid and default to the first document when entering this view.
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (allDocs.length === 0) {
|
if (documents.length === 0) {
|
||||||
if (selected !== null) setSelected(null);
|
if (selectedDoc !== null) setSelectedDoc(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!selected) {
|
if (!selectedDoc) {
|
||||||
setSelected({ kind: 'doc', doc: allDocs[0] });
|
setSelectedDoc(documents[0]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selected.kind === 'folder') {
|
const selectedStillExists = documents.some(
|
||||||
if (!folders.find((f) => f.id === selected.id)) {
|
(d) => d.id === selectedDoc.id && d.type === selectedDoc.type,
|
||||||
setSelected({ kind: 'doc', doc: allDocs[0] });
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedStillExists = allDocs.some(
|
|
||||||
(d) => d.id === selected.doc.id && d.type === selected.doc.type,
|
|
||||||
);
|
);
|
||||||
if (!selectedStillExists) {
|
if (!selectedStillExists) {
|
||||||
setSelected({ kind: 'doc', doc: allDocs[0] });
|
setSelectedDoc(documents[0]);
|
||||||
}
|
}
|
||||||
}, [allDocs, folders, selected]);
|
}, [documents, selectedDoc]);
|
||||||
|
|
||||||
const selectedFolder =
|
|
||||||
selected?.kind === 'folder' ? folders.find((f) => f.id === selected.id) : undefined;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 min-h-0 flex overflow-x-auto overflow-y-hidden">
|
<div className="flex-1 min-h-0 flex overflow-x-auto overflow-y-hidden">
|
||||||
<Column title="Library">
|
<Column title="Documents">
|
||||||
{folders.map((f) => (
|
{documents.map((doc) => (
|
||||||
<ColumnFolderRow
|
|
||||||
key={f.id}
|
|
||||||
folder={f}
|
|
||||||
active={selected?.kind === 'folder' && selected.id === f.id}
|
|
||||||
onClick={() => setSelected({ kind: 'folder', id: f.id })}
|
|
||||||
onDropOnFolder={onDropOnFolder}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{unfolderedDocs.map((d) => (
|
|
||||||
<ColumnDocRow
|
<ColumnDocRow
|
||||||
key={`${d.type}-${d.id}`}
|
key={`${doc.type}-${doc.id}`}
|
||||||
doc={d}
|
doc={doc}
|
||||||
active={selected?.kind === 'doc' && selected.doc.id === d.id}
|
active={selectedDoc?.id === doc.id && selectedDoc?.type === doc.type}
|
||||||
onClick={() => setSelected({ kind: 'doc', doc: d })}
|
onClick={() => setSelectedDoc(doc)}
|
||||||
onMergeIntoFolder={onMergeIntoFolder}
|
onMergeIntoFolder={onMergeIntoFolder}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Column>
|
</Column>
|
||||||
|
|
||||||
{selectedFolder && (
|
{selectedDoc && (
|
||||||
<Column title={selectedFolder.name}>
|
|
||||||
{selectedFolder.documents.length === 0 && (
|
|
||||||
<p className="px-2 py-3 text-[11px] text-muted text-center">Empty folder</p>
|
|
||||||
)}
|
|
||||||
{selectedFolder.documents.map((d) => (
|
|
||||||
<ColumnDocRow
|
|
||||||
key={`${d.type}-${d.id}`}
|
|
||||||
doc={d}
|
|
||||||
active={selected?.kind === 'doc' && selected.doc.id === d.id}
|
|
||||||
onClick={() => setSelected({ kind: 'doc', doc: d })}
|
|
||||||
onMergeIntoFolder={onMergeIntoFolder}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Column>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selected?.kind === 'doc' && (
|
|
||||||
<div className="flex-1 min-w-[280px] h-full bg-background overflow-y-auto p-4">
|
<div className="flex-1 min-w-[280px] h-full bg-background overflow-y-auto p-4">
|
||||||
<div className="max-w-[360px] mx-auto">
|
<div className="max-w-[360px] mx-auto">
|
||||||
<div className="rounded-lg overflow-hidden border border-offbase">
|
<div className="rounded-lg overflow-hidden border border-offbase">
|
||||||
<DocumentPreview doc={selected.doc} />
|
<DocumentPreview doc={selectedDoc} />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="mt-3 text-[13px] font-semibold text-foreground truncate">
|
<h3 className="mt-3 text-[13px] font-semibold text-foreground truncate">
|
||||||
{selected.doc.name}
|
{selectedDoc.name}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-[11px] text-muted">
|
<p className="text-[11px] text-muted">
|
||||||
{selected.doc.type.toUpperCase()} • {formatDocumentSize(selected.doc.size)}
|
{selectedDoc.type.toUpperCase()} • {formatDocumentSize(selectedDoc.size)}
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-3 flex gap-2">
|
<div className="mt-3 flex gap-2">
|
||||||
<Link
|
<Link
|
||||||
href={`/${selected.doc.type}/${encodeURIComponent(selected.doc.id)}`}
|
href={`/${selectedDoc.type}/${encodeURIComponent(selectedDoc.id)}`}
|
||||||
className="flex-1 inline-flex items-center justify-center h-8 rounded-md bg-accent text-background text-[12px] font-medium hover:bg-secondary-accent hover:scale-[1.01] transition-all duration-200 ease-out"
|
className="flex-1 inline-flex items-center justify-center h-8 rounded-md bg-accent text-background text-[12px] font-medium hover:bg-secondary-accent hover:scale-[1.01] transition-all duration-200 ease-out"
|
||||||
>
|
>
|
||||||
Open
|
Open
|
||||||
</Link>
|
</Link>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onDeleteDoc(selected.doc)}
|
onClick={() => onDeleteDoc(selectedDoc)}
|
||||||
className="h-8 px-3 rounded-md border border-offbase bg-base text-[12px] text-muted hover:text-accent hover:border-accent hover:bg-offbase hover:scale-[1.01] transition-all duration-200 ease-out"
|
className="h-8 px-3 rounded-md border border-offbase bg-base text-[12px] text-muted hover:text-accent hover:border-accent hover:bg-offbase hover:scale-[1.01] transition-all duration-200 ease-out"
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
|
|
|
||||||
|
|
@ -3,19 +3,16 @@
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useDrag, useDrop } from 'react-dnd';
|
import { useDrag, useDrop } from 'react-dnd';
|
||||||
import type { DocumentListDocument, Folder } from '@/types/documents';
|
import type { DocumentListDocument } from '@/types/documents';
|
||||||
import { PDFIcon, EPUBIcon, FileIcon } from '@/components/icons/Icons';
|
import { PDFIcon, EPUBIcon, FileIcon } from '@/components/icons/Icons';
|
||||||
import { FolderIcon } from '../window/finderIcons';
|
|
||||||
import { DocumentPreview } from '@/components/doclist/DocumentPreview';
|
import { DocumentPreview } from '@/components/doclist/DocumentPreview';
|
||||||
import { formatDocumentSize } from '@/components/doclist/formatSize';
|
import { formatDocumentSize } from '@/components/doclist/formatSize';
|
||||||
import { useDocumentSelection } from '../dnd/DocumentSelectionContext';
|
import { useDocumentSelection } from '../dnd/DocumentSelectionContext';
|
||||||
import { DND_DOCUMENT, type DocumentDragItem } from '../dnd/dndTypes';
|
import { DND_DOCUMENT, type DocumentDragItem } from '../dnd/dndTypes';
|
||||||
|
|
||||||
interface GalleryViewProps {
|
interface GalleryViewProps {
|
||||||
folders: Folder[];
|
documents: DocumentListDocument[];
|
||||||
unfolderedDocs: DocumentListDocument[];
|
|
||||||
onDeleteDoc: (doc: DocumentListDocument) => void;
|
onDeleteDoc: (doc: DocumentListDocument) => void;
|
||||||
onDropOnFolder: (folderId: string, item: DocumentDragItem) => void;
|
|
||||||
onMergeIntoFolder: (sources: DocumentListDocument[], target: DocumentListDocument) => void;
|
onMergeIntoFolder: (sources: DocumentListDocument[], target: DocumentListDocument) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -89,84 +86,38 @@ function GalleryThumb({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function GalleryFolderThumb({
|
|
||||||
folder,
|
|
||||||
active,
|
|
||||||
onClick,
|
|
||||||
onDropOnFolder,
|
|
||||||
}: {
|
|
||||||
folder: Folder;
|
|
||||||
active: boolean;
|
|
||||||
onClick: () => void;
|
|
||||||
onDropOnFolder: (folderId: string, item: DocumentDragItem) => void;
|
|
||||||
}) {
|
|
||||||
const [{ isOver, canDrop }, dropRef] = useDrop<DocumentDragItem, void, { isOver: boolean; canDrop: boolean }>(() => ({
|
|
||||||
accept: DND_DOCUMENT,
|
|
||||||
canDrop: (item) => item.docs.some((d) => d.folderId !== folder.id),
|
|
||||||
drop: (item) => onDropOnFolder(folder.id, item),
|
|
||||||
collect: (m) => ({ isOver: m.isOver({ shallow: true }), canDrop: m.canDrop() }),
|
|
||||||
}), [folder.id, onDropOnFolder]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={dropRef as unknown as React.RefObject<HTMLDivElement>}
|
|
||||||
onClick={onClick}
|
|
||||||
className={
|
|
||||||
'shrink-0 cursor-pointer rounded-md overflow-hidden border transition-all duration-200 ease-out snap-start flex flex-col items-center justify-center gap-2 ' +
|
|
||||||
(active
|
|
||||||
? 'border-accent ring-1 ring-accent w-[110px]'
|
|
||||||
: 'border-offbase hover:border-accent hover:scale-[1.01] w-[88px]') +
|
|
||||||
(isOver && canDrop ? ' ring-1 ring-accent' : '')
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="aspect-[3/4] w-full bg-base flex items-center justify-center">
|
|
||||||
<FolderIcon className="w-10 h-10 text-accent" />
|
|
||||||
</div>
|
|
||||||
<div className="px-1.5 py-1 w-full text-center bg-base">
|
|
||||||
<span className="text-[10px] text-foreground truncate block">{folder.name}</span>
|
|
||||||
<span className="text-[9px] text-muted">{folder.documents.length} items</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function GalleryView({
|
export function GalleryView({
|
||||||
folders,
|
documents,
|
||||||
unfolderedDocs,
|
|
||||||
onDeleteDoc,
|
onDeleteDoc,
|
||||||
onDropOnFolder,
|
|
||||||
onMergeIntoFolder,
|
onMergeIntoFolder,
|
||||||
}: GalleryViewProps) {
|
}: GalleryViewProps) {
|
||||||
const { setVisibleOrder } = useDocumentSelection();
|
const { setVisibleOrder } = useDocumentSelection();
|
||||||
const allDocs = useMemo(
|
|
||||||
() => [...folders.flatMap((f) => f.documents), ...unfolderedDocs],
|
|
||||||
[folders, unfolderedDocs],
|
|
||||||
);
|
|
||||||
const [activeIdx, setActiveIdx] = useState(0);
|
const [activeIdx, setActiveIdx] = useState(0);
|
||||||
|
const activeDoc = useMemo(() => documents[activeIdx], [documents, activeIdx]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setVisibleOrder(allDocs);
|
setVisibleOrder(documents);
|
||||||
}, [allDocs, setVisibleOrder]);
|
}, [documents, setVisibleOrder]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeIdx >= allDocs.length) setActiveIdx(Math.max(0, allDocs.length - 1));
|
if (activeIdx >= documents.length) {
|
||||||
}, [allDocs.length, activeIdx]);
|
setActiveIdx(Math.max(0, documents.length - 1));
|
||||||
|
}
|
||||||
|
}, [documents.length, activeIdx]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onKey = (e: KeyboardEvent) => {
|
const onKey = (e: KeyboardEvent) => {
|
||||||
const target = e.target as HTMLElement;
|
const target = e.target as HTMLElement;
|
||||||
if (target?.closest('input, textarea, [contenteditable]')) return;
|
if (target?.closest('input, textarea, [contenteditable]')) return;
|
||||||
if (e.key === 'ArrowRight') {
|
if (e.key === 'ArrowRight') {
|
||||||
setActiveIdx((i) => Math.min(allDocs.length - 1, i + 1));
|
setActiveIdx((i) => Math.min(documents.length - 1, i + 1));
|
||||||
} else if (e.key === 'ArrowLeft') {
|
} else if (e.key === 'ArrowLeft') {
|
||||||
setActiveIdx((i) => Math.max(0, i - 1));
|
setActiveIdx((i) => Math.max(0, i - 1));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
window.addEventListener('keydown', onKey);
|
window.addEventListener('keydown', onKey);
|
||||||
return () => window.removeEventListener('keydown', onKey);
|
return () => window.removeEventListener('keydown', onKey);
|
||||||
}, [allDocs.length]);
|
}, [documents.length]);
|
||||||
|
|
||||||
const activeDoc = allDocs[activeIdx];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 min-h-0 flex flex-col">
|
<div className="flex-1 min-h-0 flex flex-col">
|
||||||
|
|
@ -207,16 +158,7 @@ export function GalleryView({
|
||||||
|
|
||||||
<div className="shrink-0 border-t border-offbase bg-base">
|
<div className="shrink-0 border-t border-offbase bg-base">
|
||||||
<div className="flex gap-2 overflow-x-auto p-2 snap-x snap-mandatory">
|
<div className="flex gap-2 overflow-x-auto p-2 snap-x snap-mandatory">
|
||||||
{folders.map((f) => (
|
{documents.map((doc, i) => (
|
||||||
<GalleryFolderThumb
|
|
||||||
key={f.id}
|
|
||||||
folder={f}
|
|
||||||
active={false}
|
|
||||||
onClick={() => { /* could expand later */ }}
|
|
||||||
onDropOnFolder={onDropOnFolder}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{allDocs.map((doc, i) => (
|
|
||||||
<GalleryThumb
|
<GalleryThumb
|
||||||
key={`${doc.type}-${doc.id}`}
|
key={`${doc.type}-${doc.id}`}
|
||||||
doc={doc}
|
doc={doc}
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,14 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect, type CSSProperties } from 'react';
|
||||||
import { useDrop } from 'react-dnd';
|
import type { DocumentListDocument, IconSize } from '@/types/documents';
|
||||||
import { Transition } from '@headlessui/react';
|
|
||||||
import type {
|
|
||||||
DocumentListDocument,
|
|
||||||
Folder,
|
|
||||||
IconSize,
|
|
||||||
} from '@/types/documents';
|
|
||||||
import { formatDocumentSize } from '@/components/doclist/formatSize';
|
|
||||||
import { Button } from '@headlessui/react';
|
|
||||||
import { DocumentTile } from './DocumentTile';
|
import { DocumentTile } from './DocumentTile';
|
||||||
import { FolderIcon } from '../window/finderIcons';
|
|
||||||
import { useDocumentSelection } from '../dnd/DocumentSelectionContext';
|
import { useDocumentSelection } from '../dnd/DocumentSelectionContext';
|
||||||
import { DND_DOCUMENT, type DocumentDragItem } from '../dnd/dndTypes';
|
|
||||||
|
|
||||||
interface IconsViewProps {
|
interface IconsViewProps {
|
||||||
folders: Folder[];
|
documents: DocumentListDocument[];
|
||||||
unfolderedDocs: DocumentListDocument[];
|
|
||||||
iconSize: IconSize;
|
iconSize: IconSize;
|
||||||
collapsedFolders: Set<string>;
|
|
||||||
onToggleCollapse: (folderId: string) => void;
|
|
||||||
onDeleteFolder: (folderId: string) => void;
|
|
||||||
onDeleteDoc: (doc: DocumentListDocument) => void;
|
onDeleteDoc: (doc: DocumentListDocument) => void;
|
||||||
onDropOnFolder: (folderId: string, item: DocumentDragItem) => void;
|
|
||||||
onMergeIntoFolder: (sources: DocumentListDocument[], target: DocumentListDocument) => void;
|
onMergeIntoFolder: (sources: DocumentListDocument[], target: DocumentListDocument) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,7 +31,7 @@ function responsiveGridTemplate(iconSize: IconSize, itemCount: number): string {
|
||||||
|
|
||||||
const gridGap = '12px';
|
const gridGap = '12px';
|
||||||
|
|
||||||
function gridStyle(iconSize: IconSize, itemCount: number): React.CSSProperties {
|
function gridStyle(iconSize: IconSize, itemCount: number): CSSProperties {
|
||||||
return {
|
return {
|
||||||
gridTemplateColumns: responsiveGridTemplate(iconSize, itemCount),
|
gridTemplateColumns: responsiveGridTemplate(iconSize, itemCount),
|
||||||
gap: gridGap,
|
gap: gridGap,
|
||||||
|
|
@ -54,131 +39,17 @@ function gridStyle(iconSize: IconSize, itemCount: number): React.CSSProperties {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function FolderGridRow({
|
|
||||||
folder,
|
|
||||||
collapsed,
|
|
||||||
onToggleCollapse,
|
|
||||||
onDelete,
|
|
||||||
iconSize,
|
|
||||||
onDropOnFolder,
|
|
||||||
onDeleteDoc,
|
|
||||||
onMergeIntoFolder,
|
|
||||||
}: {
|
|
||||||
folder: Folder;
|
|
||||||
collapsed: boolean;
|
|
||||||
onToggleCollapse: () => void;
|
|
||||||
onDelete: () => void;
|
|
||||||
iconSize: IconSize;
|
|
||||||
onDropOnFolder: (folderId: string, item: DocumentDragItem) => void;
|
|
||||||
onDeleteDoc: (doc: DocumentListDocument) => void;
|
|
||||||
onMergeIntoFolder: (sources: DocumentListDocument[], target: DocumentListDocument) => void;
|
|
||||||
}) {
|
|
||||||
const [{ isOver, canDrop }, dropRef] = useDrop<
|
|
||||||
DocumentDragItem,
|
|
||||||
void,
|
|
||||||
{ isOver: boolean; canDrop: boolean }
|
|
||||||
>(() => ({
|
|
||||||
accept: DND_DOCUMENT,
|
|
||||||
canDrop: (item) => item.docs.some((d) => d.folderId !== folder.id),
|
|
||||||
drop: (item) => onDropOnFolder(folder.id, item),
|
|
||||||
collect: (m) => ({ isOver: m.isOver({ shallow: true }), canDrop: m.canDrop() }),
|
|
||||||
}), [folder.id, onDropOnFolder]);
|
|
||||||
|
|
||||||
const totalSize = folder.documents.reduce((acc, d) => acc + d.size, 0);
|
|
||||||
const isTarget = isOver && canDrop;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={dropRef as unknown as React.RefObject<HTMLDivElement>}
|
|
||||||
className={
|
|
||||||
'col-span-full rounded-md border border-offbase overflow-hidden bg-base ' +
|
|
||||||
(isTarget ? 'ring-1 ring-accent' : '')
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between px-2 py-1 bg-offbase border-b border-offbase">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onToggleCollapse}
|
|
||||||
className="flex items-center gap-1.5 text-[12px] font-semibold text-foreground hover:text-accent transition-colors duration-200 ease-out"
|
|
||||||
aria-expanded={!collapsed}
|
|
||||||
>
|
|
||||||
<FolderIcon className="w-3.5 h-3.5 text-accent" />
|
|
||||||
{folder.name}
|
|
||||||
<span className="text-[10px] font-normal text-muted ml-1">
|
|
||||||
{folder.documents.length} • {formatDocumentSize(totalSize)}
|
|
||||||
</span>
|
|
||||||
<svg
|
|
||||||
className={`w-3 h-3 transition-transform ${collapsed ? '-rotate-90' : ''}`}
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<Button
|
|
||||||
onClick={onDelete}
|
|
||||||
className="p-1 text-muted hover:text-accent hover:bg-base hover:scale-[1.02] rounded-md transition-all duration-200 ease-out"
|
|
||||||
aria-label={`Delete ${folder.name}`}
|
|
||||||
>
|
|
||||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth="2"
|
|
||||||
d="M6 18L18 6M6 6l12 12"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Transition
|
|
||||||
show={!collapsed}
|
|
||||||
enter="transition-all duration-200 ease-out"
|
|
||||||
enterFrom="opacity-0 max-h-0"
|
|
||||||
enterTo="opacity-100 max-h-[2000px]"
|
|
||||||
leave="transition-all duration-150 ease-in"
|
|
||||||
leaveFrom="opacity-100 max-h-[2000px]"
|
|
||||||
leaveTo="opacity-0 max-h-0"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="grid p-2"
|
|
||||||
style={gridStyle(iconSize, folder.documents.length)}
|
|
||||||
>
|
|
||||||
{folder.documents.map((doc) => (
|
|
||||||
<DocumentTile
|
|
||||||
key={`${doc.type}-${doc.id}`}
|
|
||||||
doc={doc}
|
|
||||||
iconSize={iconSize}
|
|
||||||
onDelete={onDeleteDoc}
|
|
||||||
onMergeIntoFolder={onMergeIntoFolder}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function IconsView({
|
export function IconsView({
|
||||||
folders,
|
documents,
|
||||||
unfolderedDocs,
|
|
||||||
iconSize,
|
iconSize,
|
||||||
collapsedFolders,
|
|
||||||
onToggleCollapse,
|
|
||||||
onDeleteFolder,
|
|
||||||
onDeleteDoc,
|
onDeleteDoc,
|
||||||
onDropOnFolder,
|
|
||||||
onMergeIntoFolder,
|
onMergeIntoFolder,
|
||||||
}: IconsViewProps) {
|
}: IconsViewProps) {
|
||||||
const { setVisibleOrder, clear } = useDocumentSelection();
|
const { setVisibleOrder, clear } = useDocumentSelection();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const all: DocumentListDocument[] = [
|
setVisibleOrder(documents);
|
||||||
...folders.flatMap((f) => f.documents),
|
}, [documents, setVisibleOrder]);
|
||||||
...unfolderedDocs,
|
|
||||||
];
|
|
||||||
setVisibleOrder(all);
|
|
||||||
}, [folders, unfolderedDocs, setVisibleOrder]);
|
|
||||||
|
|
||||||
const handleBackgroundClick: React.MouseEventHandler = (e) => {
|
const handleBackgroundClick: React.MouseEventHandler = (e) => {
|
||||||
if ((e.target as HTMLElement).closest('[data-doc-tile]')) return;
|
if ((e.target as HTMLElement).closest('[data-doc-tile]')) return;
|
||||||
|
|
@ -190,24 +61,8 @@ export function IconsView({
|
||||||
onClick={handleBackgroundClick}
|
onClick={handleBackgroundClick}
|
||||||
className="flex-1 min-h-0 overflow-y-auto p-3"
|
className="flex-1 min-h-0 overflow-y-auto p-3"
|
||||||
>
|
>
|
||||||
<div
|
<div className="grid" style={gridStyle(iconSize, documents.length)}>
|
||||||
className="grid"
|
{documents.map((doc) => (
|
||||||
style={gridStyle(iconSize, unfolderedDocs.length)}
|
|
||||||
>
|
|
||||||
{folders.map((folder) => (
|
|
||||||
<FolderGridRow
|
|
||||||
key={folder.id}
|
|
||||||
folder={folder}
|
|
||||||
collapsed={collapsedFolders.has(folder.id)}
|
|
||||||
onToggleCollapse={() => onToggleCollapse(folder.id)}
|
|
||||||
onDelete={() => onDeleteFolder(folder.id)}
|
|
||||||
iconSize={iconSize}
|
|
||||||
onDropOnFolder={onDropOnFolder}
|
|
||||||
onDeleteDoc={onDeleteDoc}
|
|
||||||
onMergeIntoFolder={onMergeIntoFolder}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{unfolderedDocs.map((doc) => (
|
|
||||||
<DocumentTile
|
<DocumentTile
|
||||||
key={`${doc.type}-${doc.id}`}
|
key={`${doc.type}-${doc.id}`}
|
||||||
doc={doc}
|
doc={doc}
|
||||||
|
|
|
||||||
|
|
@ -3,30 +3,23 @@
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useDrag, useDrop } from 'react-dnd';
|
import { useDrag, useDrop } from 'react-dnd';
|
||||||
import { Button, Transition } from '@headlessui/react';
|
import { Button } from '@headlessui/react';
|
||||||
import type {
|
import type {
|
||||||
DocumentListDocument,
|
DocumentListDocument,
|
||||||
Folder,
|
|
||||||
SortBy,
|
SortBy,
|
||||||
SortDirection,
|
SortDirection,
|
||||||
} from '@/types/documents';
|
} from '@/types/documents';
|
||||||
import { PDFIcon, EPUBIcon, FileIcon } from '@/components/icons/Icons';
|
import { PDFIcon, EPUBIcon, FileIcon } from '@/components/icons/Icons';
|
||||||
import { formatDocumentSize } from '@/components/doclist/formatSize';
|
import { formatDocumentSize } from '@/components/doclist/formatSize';
|
||||||
import { FolderIcon } from '../window/finderIcons';
|
|
||||||
import { useDocumentSelection } from '../dnd/DocumentSelectionContext';
|
import { useDocumentSelection } from '../dnd/DocumentSelectionContext';
|
||||||
import { DND_DOCUMENT, type DocumentDragItem } from '../dnd/dndTypes';
|
import { DND_DOCUMENT, type DocumentDragItem } from '../dnd/dndTypes';
|
||||||
|
|
||||||
interface ListViewProps {
|
interface ListViewProps {
|
||||||
folders: Folder[];
|
documents: DocumentListDocument[];
|
||||||
unfolderedDocs: DocumentListDocument[];
|
|
||||||
sortBy: SortBy;
|
sortBy: SortBy;
|
||||||
sortDirection: SortDirection;
|
sortDirection: SortDirection;
|
||||||
onSortChange: (sortBy: SortBy, direction: SortDirection) => void;
|
onSortChange: (sortBy: SortBy, direction: SortDirection) => void;
|
||||||
collapsedFolders: Set<string>;
|
|
||||||
onToggleCollapse: (folderId: string) => void;
|
|
||||||
onDeleteFolder: (folderId: string) => void;
|
|
||||||
onDeleteDoc: (doc: DocumentListDocument) => void;
|
onDeleteDoc: (doc: DocumentListDocument) => void;
|
||||||
onDropOnFolder: (folderId: string, item: DocumentDragItem) => void;
|
|
||||||
onMergeIntoFolder: (sources: DocumentListDocument[], target: DocumentListDocument) => void;
|
onMergeIntoFolder: (sources: DocumentListDocument[], target: DocumentListDocument) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -84,12 +77,10 @@ function HeaderCell({
|
||||||
|
|
||||||
function DocRow({
|
function DocRow({
|
||||||
doc,
|
doc,
|
||||||
isFirstColumnIndented,
|
|
||||||
onDeleteDoc,
|
onDeleteDoc,
|
||||||
onMergeIntoFolder,
|
onMergeIntoFolder,
|
||||||
}: {
|
}: {
|
||||||
doc: DocumentListDocument;
|
doc: DocumentListDocument;
|
||||||
isFirstColumnIndented?: boolean;
|
|
||||||
onDeleteDoc: (d: DocumentListDocument) => void;
|
onDeleteDoc: (d: DocumentListDocument) => void;
|
||||||
onMergeIntoFolder: (sources: DocumentListDocument[], target: DocumentListDocument) => void;
|
onMergeIntoFolder: (sources: DocumentListDocument[], target: DocumentListDocument) => void;
|
||||||
}) {
|
}) {
|
||||||
|
|
@ -163,10 +154,7 @@ function DocRow({
|
||||||
href={href}
|
href={href}
|
||||||
draggable={false}
|
draggable={false}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
className={
|
className="flex items-center gap-2 min-w-0 px-2 py-1.5"
|
||||||
'flex items-center gap-2 min-w-0 px-2 py-1.5 ' +
|
|
||||||
(isFirstColumnIndented ? 'pl-8' : '')
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<KindIcon doc={doc} />
|
<KindIcon doc={doc} />
|
||||||
<span className="truncate">{doc.name}</span>
|
<span className="truncate">{doc.name}</span>
|
||||||
|
|
@ -199,118 +187,19 @@ function DocRow({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function FolderRowGroup({
|
|
||||||
folder,
|
|
||||||
collapsed,
|
|
||||||
onToggleCollapse,
|
|
||||||
onDelete,
|
|
||||||
onDeleteDoc,
|
|
||||||
onDropOnFolder,
|
|
||||||
onMergeIntoFolder,
|
|
||||||
}: {
|
|
||||||
folder: Folder;
|
|
||||||
collapsed: boolean;
|
|
||||||
onToggleCollapse: () => void;
|
|
||||||
onDelete: () => void;
|
|
||||||
onDeleteDoc: (d: DocumentListDocument) => void;
|
|
||||||
onDropOnFolder: (folderId: string, item: DocumentDragItem) => void;
|
|
||||||
onMergeIntoFolder: (sources: DocumentListDocument[], target: DocumentListDocument) => void;
|
|
||||||
}) {
|
|
||||||
const [{ isOver, canDrop }, dropRef] = useDrop<DocumentDragItem, void, { isOver: boolean; canDrop: boolean }>(() => ({
|
|
||||||
accept: DND_DOCUMENT,
|
|
||||||
canDrop: (item) => item.docs.some((d) => d.folderId !== folder.id),
|
|
||||||
drop: (item) => onDropOnFolder(folder.id, item),
|
|
||||||
collect: (m) => ({ isOver: m.isOver({ shallow: true }), canDrop: m.canDrop() }),
|
|
||||||
}), [folder.id, onDropOnFolder]);
|
|
||||||
|
|
||||||
const isTarget = isOver && canDrop;
|
|
||||||
const totalSize = folder.documents.reduce((acc, d) => acc + d.size, 0);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={dropRef as unknown as React.RefObject<HTMLDivElement>}>
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
'grid grid-cols-[minmax(0,1fr)_72px_88px_120px_28px] sm:grid-cols-[minmax(0,1fr)_88px_96px_140px_32px] items-center text-[12px] border-b border-offbase bg-base ' +
|
|
||||||
(isTarget ? 'ring-1 ring-accent ring-inset' : '')
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onToggleCollapse}
|
|
||||||
className="flex items-center gap-1.5 min-w-0 px-2 py-1.5 text-left font-semibold hover:text-accent transition-colors duration-200 ease-out"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
className={`w-3 h-3 text-muted transition-transform ${collapsed ? '-rotate-90' : ''}`}
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" />
|
|
||||||
</svg>
|
|
||||||
<FolderIcon className="w-3.5 h-3.5 text-accent" />
|
|
||||||
<span className="truncate">{folder.name}</span>
|
|
||||||
</button>
|
|
||||||
<span className="px-2 text-[11px] text-muted">Folder</span>
|
|
||||||
<span className="px-2 text-[11px] text-muted text-right tabular-nums">
|
|
||||||
{formatDocumentSize(totalSize)}
|
|
||||||
</span>
|
|
||||||
<span className="px-2 text-[11px] text-muted tabular-nums">
|
|
||||||
{folder.documents.length} items
|
|
||||||
</span>
|
|
||||||
<Button
|
|
||||||
onClick={onDelete}
|
|
||||||
className="h-7 w-7 flex items-center justify-center text-muted hover:text-accent hover:bg-offbase hover:scale-[1.02] rounded transition-all duration-200 ease-out"
|
|
||||||
aria-label={`Delete ${folder.name}`}
|
|
||||||
>
|
|
||||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
|
|
||||||
</svg>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Transition
|
|
||||||
show={!collapsed}
|
|
||||||
enter="transition-all duration-200"
|
|
||||||
enterFrom="opacity-0 max-h-0"
|
|
||||||
enterTo="opacity-100 max-h-[2000px]"
|
|
||||||
leave="transition-all duration-150"
|
|
||||||
leaveFrom="opacity-100 max-h-[2000px]"
|
|
||||||
leaveTo="opacity-0 max-h-0"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
{folder.documents.map((doc) => (
|
|
||||||
<DocRow
|
|
||||||
key={`${doc.type}-${doc.id}`}
|
|
||||||
doc={doc}
|
|
||||||
isFirstColumnIndented
|
|
||||||
onDeleteDoc={onDeleteDoc}
|
|
||||||
onMergeIntoFolder={onMergeIntoFolder}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ListView({
|
export function ListView({
|
||||||
folders,
|
documents,
|
||||||
unfolderedDocs,
|
|
||||||
sortBy,
|
sortBy,
|
||||||
sortDirection,
|
sortDirection,
|
||||||
onSortChange,
|
onSortChange,
|
||||||
collapsedFolders,
|
|
||||||
onToggleCollapse,
|
|
||||||
onDeleteFolder,
|
|
||||||
onDeleteDoc,
|
onDeleteDoc,
|
||||||
onDropOnFolder,
|
|
||||||
onMergeIntoFolder,
|
onMergeIntoFolder,
|
||||||
}: ListViewProps) {
|
}: ListViewProps) {
|
||||||
const { setVisibleOrder, clear } = useDocumentSelection();
|
const { setVisibleOrder, clear } = useDocumentSelection();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const all = [...folders.flatMap((f) => f.documents), ...unfolderedDocs];
|
setVisibleOrder(documents);
|
||||||
setVisibleOrder(all);
|
}, [documents, setVisibleOrder]);
|
||||||
}, [folders, unfolderedDocs, setVisibleOrder]);
|
|
||||||
|
|
||||||
const handleBackgroundClick: React.MouseEventHandler = (e) => {
|
const handleBackgroundClick: React.MouseEventHandler = (e) => {
|
||||||
if ((e.target as HTMLElement).closest('[data-doc-tile]')) return;
|
if ((e.target as HTMLElement).closest('[data-doc-tile]')) return;
|
||||||
|
|
@ -327,19 +216,7 @@ export function ListView({
|
||||||
<span />
|
<span />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{folders.map((folder) => (
|
{documents.map((doc) => (
|
||||||
<FolderRowGroup
|
|
||||||
key={folder.id}
|
|
||||||
folder={folder}
|
|
||||||
collapsed={collapsedFolders.has(folder.id)}
|
|
||||||
onToggleCollapse={() => onToggleCollapse(folder.id)}
|
|
||||||
onDelete={() => onDeleteFolder(folder.id)}
|
|
||||||
onDeleteDoc={onDeleteDoc}
|
|
||||||
onDropOnFolder={onDropOnFolder}
|
|
||||||
onMergeIntoFolder={onMergeIntoFolder}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{unfolderedDocs.map((doc) => (
|
|
||||||
<DocRow
|
<DocRow
|
||||||
key={`${doc.type}-${doc.id}`}
|
key={`${doc.type}-${doc.id}`}
|
||||||
doc={doc}
|
doc={doc}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { useRef, type CSSProperties, type ReactNode } from 'react';
|
||||||
import { useDrop } from 'react-dnd';
|
import { useDrop } from 'react-dnd';
|
||||||
import type { Folder, SidebarFilter } from '@/types/documents';
|
import type { Folder, SidebarFilter } from '@/types/documents';
|
||||||
import { PDFIcon, EPUBIcon, FileIcon } from '@/components/icons/Icons';
|
import { PDFIcon, EPUBIcon, FileIcon } from '@/components/icons/Icons';
|
||||||
import { FolderIcon, HomeIcon, ClockIcon } from './finderIcons';
|
import { FolderIcon, HomeIcon, ClockIcon, FolderPlusIcon } from './finderIcons';
|
||||||
import { DND_DOCUMENT, type DocumentDragItem } from '../dnd/dndTypes';
|
import { DND_DOCUMENT, type DocumentDragItem } from '../dnd/dndTypes';
|
||||||
|
|
||||||
interface FinderSidebarProps {
|
interface FinderSidebarProps {
|
||||||
|
|
@ -12,6 +12,8 @@ interface FinderSidebarProps {
|
||||||
onFilterChange: (filter: SidebarFilter) => void;
|
onFilterChange: (filter: SidebarFilter) => void;
|
||||||
folders: Folder[];
|
folders: Folder[];
|
||||||
counts: { all: number; pdf: number; epub: number; html: number };
|
counts: { all: number; pdf: number; epub: number; html: number };
|
||||||
|
onDeleteFolder: (folderId: string) => void;
|
||||||
|
onNewFolder: () => void;
|
||||||
/** When dragging onto a folder row, move dropped docs into that folder. */
|
/** When dragging onto a folder row, move dropped docs into that folder. */
|
||||||
onDropOnFolder: (folderId: string, item: DocumentDragItem) => void;
|
onDropOnFolder: (folderId: string, item: DocumentDragItem) => void;
|
||||||
/** Width controls (desktop only). */
|
/** Width controls (desktop only). */
|
||||||
|
|
@ -29,6 +31,7 @@ interface SidebarRowProps {
|
||||||
icon: ReactNode;
|
icon: ReactNode;
|
||||||
label: string;
|
label: string;
|
||||||
count?: number;
|
count?: number;
|
||||||
|
countClassName?: string;
|
||||||
trailing?: ReactNode;
|
trailing?: ReactNode;
|
||||||
isDropTarget?: boolean;
|
isDropTarget?: boolean;
|
||||||
}
|
}
|
||||||
|
|
@ -39,6 +42,7 @@ function SidebarRow({
|
||||||
icon,
|
icon,
|
||||||
label,
|
label,
|
||||||
count,
|
count,
|
||||||
|
countClassName,
|
||||||
trailing,
|
trailing,
|
||||||
isDropTarget,
|
isDropTarget,
|
||||||
}: SidebarRowProps) {
|
}: SidebarRowProps) {
|
||||||
|
|
@ -64,7 +68,11 @@ function SidebarRow({
|
||||||
</span>
|
</span>
|
||||||
<span className="truncate flex-1">{label}</span>
|
<span className="truncate flex-1">{label}</span>
|
||||||
{typeof count === 'number' && count > 0 && (
|
{typeof count === 'number' && count > 0 && (
|
||||||
<span className="text-[10px] text-muted tabular-nums">{count}</span>
|
<span
|
||||||
|
className={`text-[10px] text-muted tabular-nums transition-transform duration-200 ease-out ${countClassName ?? ''}`}
|
||||||
|
>
|
||||||
|
{count}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
{trailing}
|
{trailing}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -75,11 +83,13 @@ function FolderRow({
|
||||||
folder,
|
folder,
|
||||||
active,
|
active,
|
||||||
onClick,
|
onClick,
|
||||||
|
onDelete,
|
||||||
onDropOnFolder,
|
onDropOnFolder,
|
||||||
}: {
|
}: {
|
||||||
folder: Folder;
|
folder: Folder;
|
||||||
active: boolean;
|
active: boolean;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
|
onDelete: () => void;
|
||||||
onDropOnFolder: (folderId: string, item: DocumentDragItem) => void;
|
onDropOnFolder: (folderId: string, item: DocumentDragItem) => void;
|
||||||
}) {
|
}) {
|
||||||
const [{ isOver, canDrop }, dropRef] = useDrop<
|
const [{ isOver, canDrop }, dropRef] = useDrop<
|
||||||
|
|
@ -103,15 +113,33 @@ function FolderRow({
|
||||||
|
|
||||||
const isDropTarget = isOver && canDrop;
|
const isDropTarget = isOver && canDrop;
|
||||||
return (
|
return (
|
||||||
<div ref={dropRef as unknown as React.RefObject<HTMLDivElement>}>
|
<div
|
||||||
|
ref={dropRef as unknown as React.RefObject<HTMLDivElement>}
|
||||||
|
className="group/folder relative"
|
||||||
|
>
|
||||||
<SidebarRow
|
<SidebarRow
|
||||||
active={active}
|
active={active}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
icon={<FolderIcon className="w-3.5 h-3.5" />}
|
icon={<FolderIcon className="w-3.5 h-3.5" />}
|
||||||
label={folder.name}
|
label={folder.name}
|
||||||
count={folder.documents.length}
|
count={folder.documents.length}
|
||||||
|
countClassName="group-hover/folder:-translate-x-6 group-focus-within/folder:-translate-x-6"
|
||||||
isDropTarget={isDropTarget}
|
isDropTarget={isDropTarget}
|
||||||
/>
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDelete();
|
||||||
|
}}
|
||||||
|
className="absolute right-1 top-1/2 -translate-y-1/2 h-5 w-5 inline-flex items-center justify-center rounded text-muted opacity-0 group-hover/folder:opacity-100 group-focus-within/folder:opacity-100 hover:text-accent hover:bg-offbase transition"
|
||||||
|
aria-label={`Delete ${folder.name}`}
|
||||||
|
title={`Delete ${folder.name}`}
|
||||||
|
>
|
||||||
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -134,6 +162,8 @@ export function FinderSidebar({
|
||||||
onFilterChange,
|
onFilterChange,
|
||||||
folders,
|
folders,
|
||||||
counts,
|
counts,
|
||||||
|
onDeleteFolder,
|
||||||
|
onNewFolder,
|
||||||
onDropOnFolder,
|
onDropOnFolder,
|
||||||
width,
|
width,
|
||||||
onWidthChange,
|
onWidthChange,
|
||||||
|
|
@ -205,19 +235,33 @@ export function FinderSidebar({
|
||||||
count={counts.html}
|
count={counts.html}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{folders.length > 0 && (
|
<div className="px-2 pt-3 pb-1 flex items-center justify-between">
|
||||||
<>
|
<p className="text-[10px] uppercase tracking-[0.08em] text-muted font-semibold">
|
||||||
<SectionLabel>Folders</SectionLabel>
|
Folders
|
||||||
{folders.map((folder) => (
|
</p>
|
||||||
<FolderRow
|
<button
|
||||||
key={folder.id}
|
type="button"
|
||||||
folder={folder}
|
onClick={onNewFolder}
|
||||||
active={filter === `folder:${folder.id}`}
|
className="inline-flex items-center justify-center h-6 w-6 rounded-md border border-offbase bg-base text-foreground hover:text-accent hover:border-accent hover:bg-offbase transition-all duration-200 ease-out hover:scale-[1.01]"
|
||||||
onClick={() => onFilterChange(`folder:${folder.id}`)}
|
title="New folder"
|
||||||
onDropOnFolder={onDropOnFolder}
|
aria-label="New folder"
|
||||||
/>
|
>
|
||||||
))}
|
<FolderPlusIcon className="w-4 h-4" />
|
||||||
</>
|
</button>
|
||||||
|
</div>
|
||||||
|
{folders.length === 0 ? (
|
||||||
|
<p className="px-2 py-1 text-[11px] text-muted">No folders yet</p>
|
||||||
|
) : (
|
||||||
|
folders.map((folder) => (
|
||||||
|
<FolderRow
|
||||||
|
key={folder.id}
|
||||||
|
folder={folder}
|
||||||
|
active={filter === `folder:${folder.id}`}
|
||||||
|
onClick={() => onFilterChange(`folder:${folder.id}`)}
|
||||||
|
onDelete={() => onDeleteFolder(folder.id)}
|
||||||
|
onDropOnFolder={onDropOnFolder}
|
||||||
|
/>
|
||||||
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import {
|
||||||
ColumnsViewIcon,
|
ColumnsViewIcon,
|
||||||
GalleryViewIcon,
|
GalleryViewIcon,
|
||||||
SearchIcon,
|
SearchIcon,
|
||||||
FolderPlusIcon,
|
|
||||||
HamburgerIcon,
|
HamburgerIcon,
|
||||||
} from './finderIcons';
|
} from './finderIcons';
|
||||||
import { ChevronUpDownIcon } from '@/components/icons/Icons';
|
import { ChevronUpDownIcon } from '@/components/icons/Icons';
|
||||||
|
|
@ -25,7 +24,6 @@ interface FinderToolbarProps {
|
||||||
onSortDirectionToggle: () => void;
|
onSortDirectionToggle: () => void;
|
||||||
query: string;
|
query: string;
|
||||||
onQueryChange: (q: string) => void;
|
onQueryChange: (q: string) => void;
|
||||||
onNewFolder: () => void;
|
|
||||||
onToggleSidebar: () => void;
|
onToggleSidebar: () => void;
|
||||||
isSidebarOpen: boolean;
|
isSidebarOpen: boolean;
|
||||||
/** True when the columns view should be disabled (mobile viewport). */
|
/** True when the columns view should be disabled (mobile viewport). */
|
||||||
|
|
@ -84,7 +82,6 @@ export function FinderToolbar({
|
||||||
onSortDirectionToggle,
|
onSortDirectionToggle,
|
||||||
query,
|
query,
|
||||||
onQueryChange,
|
onQueryChange,
|
||||||
onNewFolder,
|
|
||||||
onToggleSidebar,
|
onToggleSidebar,
|
||||||
isSidebarOpen,
|
isSidebarOpen,
|
||||||
isNarrow,
|
isNarrow,
|
||||||
|
|
@ -222,16 +219,6 @@ export function FinderToolbar({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onNewFolder}
|
|
||||||
className={`${TOOLBAR_BTN} ${TOOLBAR_BTN_INACTIVE} gap-1 shrink-0`}
|
|
||||||
title="New folder"
|
|
||||||
>
|
|
||||||
<FolderPlusIcon className="w-4 h-4" />
|
|
||||||
<span className="hidden md:inline">New Folder</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{rightSlot && (
|
{rightSlot && (
|
||||||
<div className="shrink-0 flex items-center gap-2 pl-1 sm:pl-2 sm:border-l sm:border-offbase ml-0.5">
|
<div className="shrink-0 flex items-center gap-2 pl-1 sm:pl-2 sm:border-l sm:border-offbase ml-0.5">
|
||||||
{rightSlot}
|
{rightSlot}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue