cleanup doclist formatting helpers and view props
This commit is contained in:
parent
eb8820d888
commit
46d5bab647
8 changed files with 84 additions and 109 deletions
|
|
@ -310,6 +310,10 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) {
|
|||
}
|
||||
}, [documentToDelete, removePDFDocument, removeEPUBDocument, removeHTMLDocument]);
|
||||
|
||||
const handleDeleteDoc = useCallback((doc: DocumentListDocument) => {
|
||||
setDocumentToDelete({ id: doc.id, name: doc.name, type: doc.type });
|
||||
}, []);
|
||||
|
||||
const handleDropOnFolder = useCallback(
|
||||
(folderId: string, item: DocumentDragItem) => {
|
||||
setFolders((prev) =>
|
||||
|
|
@ -478,7 +482,7 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => setShowHint(false)}
|
||||
className="h-6 w-6 inline-flex items-center justify-center text-muted hover:text-accent hover:bg-base hover:scale-[1.02] rounded transition-all duration-200 ease-out"
|
||||
className="h-6 w-6 inline-flex items-center justify-center text-muted hover:text-accent hover:bg-base hover:scale-[1.01] rounded transition-all duration-200 ease-out"
|
||||
aria-label="Dismiss hint"
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
|
@ -498,9 +502,7 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) {
|
|||
collapsedFolders={collapsedFolders}
|
||||
onToggleCollapse={toggleFolderCollapse}
|
||||
onDeleteFolder={handleDeleteFolder}
|
||||
onDeleteDoc={(doc) =>
|
||||
setDocumentToDelete({ id: doc.id, name: doc.name, type: doc.type })
|
||||
}
|
||||
onDeleteDoc={handleDeleteDoc}
|
||||
onDropOnFolder={handleDropOnFolder}
|
||||
onMergeIntoFolder={handleMergeIntoFolder}
|
||||
/>
|
||||
|
|
@ -518,9 +520,7 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) {
|
|||
collapsedFolders={collapsedFolders}
|
||||
onToggleCollapse={toggleFolderCollapse}
|
||||
onDeleteFolder={handleDeleteFolder}
|
||||
onDeleteDoc={(doc) =>
|
||||
setDocumentToDelete({ id: doc.id, name: doc.name, type: doc.type })
|
||||
}
|
||||
onDeleteDoc={handleDeleteDoc}
|
||||
onDropOnFolder={handleDropOnFolder}
|
||||
onMergeIntoFolder={handleMergeIntoFolder}
|
||||
/>
|
||||
|
|
@ -529,9 +529,7 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) {
|
|||
<ColumnsView
|
||||
folders={visibleFolders}
|
||||
unfolderedDocs={unfolderedDocs}
|
||||
onDeleteDoc={(doc) =>
|
||||
setDocumentToDelete({ id: doc.id, name: doc.name, type: doc.type })
|
||||
}
|
||||
onDeleteDoc={handleDeleteDoc}
|
||||
onDropOnFolder={handleDropOnFolder}
|
||||
onMergeIntoFolder={handleMergeIntoFolder}
|
||||
/>
|
||||
|
|
@ -540,9 +538,7 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) {
|
|||
<GalleryView
|
||||
folders={visibleFolders}
|
||||
unfolderedDocs={unfolderedDocs}
|
||||
onDeleteDoc={(doc) =>
|
||||
setDocumentToDelete({ id: doc.id, name: doc.name, type: doc.type })
|
||||
}
|
||||
onDeleteDoc={handleDeleteDoc}
|
||||
onDropOnFolder={handleDropOnFolder}
|
||||
onMergeIntoFolder={handleMergeIntoFolder}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
primeDocumentPreviewCache,
|
||||
setInMemoryDocumentPreviewUrl,
|
||||
} from '@/lib/client/cache/previews';
|
||||
import { formatDocumentSize } from './formatSize';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
|
||||
|
|
@ -332,13 +333,7 @@ export function DocumentPreview({ doc }: DocumentPreviewProps) {
|
|||
<div className="absolute left-1 bottom-1 z-20 rounded bg-black/45 px-1.5 py-0.5 text-[9px] font-semibold tracking-wide text-white/90">
|
||||
{isGenerating
|
||||
? '…'
|
||||
: `${typeLabel} • ${
|
||||
doc.size >= 1024 * 1024 * 1024
|
||||
? `${(doc.size / 1024 / 1024 / 1024).toFixed(2)} GB`
|
||||
: doc.size >= 1024 * 1024
|
||||
? `${(doc.size / 1024 / 1024).toFixed(2)} MB`
|
||||
: `${(doc.size / 1024).toFixed(1)} KB`
|
||||
}`}
|
||||
: `${typeLabel} • ${formatDocumentSize(doc.size)}`}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
9
src/components/doclist/formatSize.ts
Normal file
9
src/components/doclist/formatSize.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
export function formatDocumentSize(bytes: number): string {
|
||||
if (bytes >= 1024 * 1024 * 1024) {
|
||||
return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`;
|
||||
}
|
||||
if (bytes >= 1024 * 1024) {
|
||||
return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
|
||||
}
|
||||
return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import type { DocumentListDocument, Folder } from '@/types/documents';
|
|||
import { PDFIcon, EPUBIcon, FileIcon } from '@/components/icons/Icons';
|
||||
import { FolderIcon, ChevronRightSmall } from '../window/finderIcons';
|
||||
import { DocumentPreview } from '@/components/doclist/DocumentPreview';
|
||||
import { formatDocumentSize } from '@/components/doclist/formatSize';
|
||||
import { useDocumentSelection } from '../dnd/DocumentSelectionContext';
|
||||
import { DND_DOCUMENT, type DocumentDragItem } from '../dnd/dndTypes';
|
||||
|
||||
|
|
@ -216,24 +217,19 @@ export function ColumnsView({
|
|||
{selected.doc.name}
|
||||
</h3>
|
||||
<p className="text-[11px] text-muted">
|
||||
{selected.doc.type.toUpperCase()} •{' '}
|
||||
{selected.doc.size >= 1024 * 1024 * 1024
|
||||
? `${(selected.doc.size / 1024 / 1024 / 1024).toFixed(2)} GB`
|
||||
: selected.doc.size >= 1024 * 1024
|
||||
? `${(selected.doc.size / 1024 / 1024).toFixed(2)} MB`
|
||||
: `${(selected.doc.size / 1024).toFixed(1)} KB`}
|
||||
{selected.doc.type.toUpperCase()} • {formatDocumentSize(selected.doc.size)}
|
||||
</p>
|
||||
<div className="mt-3 flex gap-2">
|
||||
<Link
|
||||
href={`/${selected.doc.type}/${encodeURIComponent(selected.doc.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.02] 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
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onDeleteDoc(selected.doc)}
|
||||
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.03] 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
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -20,8 +20,49 @@ interface DocumentTileProps {
|
|||
onMergeIntoFolder: (source: DocumentListDocument[], target: DocumentListDocument) => void;
|
||||
}
|
||||
|
||||
const SIZE_LABEL: Record<IconSize, string> = { sm: 'S', md: 'M', lg: 'L', xl: 'XL' };
|
||||
void SIZE_LABEL;
|
||||
const NAME_SIZE_CLASSES: Record<IconSize, string> = {
|
||||
sm: 'text-[10px]',
|
||||
md: 'text-[11px]',
|
||||
lg: 'text-[12px]',
|
||||
xl: 'text-[13px]',
|
||||
};
|
||||
|
||||
const BOTTOM_PADDING_CLASSES: Record<IconSize, string> = {
|
||||
sm: 'px-[7px] py-[4px]',
|
||||
md: 'px-[8px] py-[5px]',
|
||||
lg: 'px-[9px] py-[5px]',
|
||||
xl: 'px-[10px] py-[6px]',
|
||||
};
|
||||
|
||||
const LINK_PADDING_CLASS = 'px-[2px] py-[2px]';
|
||||
|
||||
const GAP_CLASSES: Record<IconSize, string> = {
|
||||
sm: 'gap-1',
|
||||
md: 'gap-1.5',
|
||||
lg: 'gap-2',
|
||||
xl: 'gap-2',
|
||||
};
|
||||
|
||||
const FILE_ICON_CLASSES: Record<IconSize, string> = {
|
||||
sm: 'w-3 h-3',
|
||||
md: 'w-3.5 h-3.5',
|
||||
lg: 'w-3.5 h-3.5',
|
||||
xl: 'w-4 h-4',
|
||||
};
|
||||
|
||||
const TRASH_BTN_CLASSES: Record<IconSize, string> = {
|
||||
sm: 'ml-0.5 h-[18px] w-[18px] rounded-sm',
|
||||
md: 'ml-0.5 h-[21px] w-[21px] rounded-sm',
|
||||
lg: 'ml-1 h-[23px] w-[23px] rounded',
|
||||
xl: 'ml-1.5 h-[25px] w-[25px] rounded',
|
||||
};
|
||||
|
||||
const TRASH_ICON_CLASSES: Record<IconSize, string> = {
|
||||
sm: 'w-[10px] h-[10px]',
|
||||
md: 'w-[11px] h-[11px]',
|
||||
lg: 'w-[12px] h-[12px]',
|
||||
xl: 'w-[13px] h-[13px]',
|
||||
};
|
||||
|
||||
export function DocumentTile({
|
||||
doc,
|
||||
|
|
@ -108,55 +149,6 @@ export function DocumentTile({
|
|||
return () => clearTimeout(t);
|
||||
}, [loading]);
|
||||
|
||||
const sizeClasses: Record<IconSize, string> = {
|
||||
sm: 'text-[10px]',
|
||||
md: 'text-[11px]',
|
||||
lg: 'text-[12px]',
|
||||
xl: 'text-[13px]',
|
||||
};
|
||||
|
||||
const bottomPaddingClasses: Record<IconSize, string> = {
|
||||
sm: 'px-[7px] py-[4px]',
|
||||
md: 'px-[8px] py-[5px]',
|
||||
lg: 'px-[9px] py-[5px]',
|
||||
xl: 'px-[10px] py-[6px]',
|
||||
};
|
||||
|
||||
const linkPaddingClasses: Record<IconSize, string> = {
|
||||
sm: 'px-[2px] py-[2px]',
|
||||
md: 'px-[2px] py-[2px]',
|
||||
lg: 'px-[2px] py-[2px]',
|
||||
xl: 'px-[2px] py-[2px]',
|
||||
};
|
||||
|
||||
const gapClasses: Record<IconSize, string> = {
|
||||
sm: 'gap-1',
|
||||
md: 'gap-1.5',
|
||||
lg: 'gap-2',
|
||||
xl: 'gap-2',
|
||||
};
|
||||
|
||||
const fileIconClasses: Record<IconSize, string> = {
|
||||
sm: 'w-3 h-3',
|
||||
md: 'w-3.5 h-3.5',
|
||||
lg: 'w-3.5 h-3.5',
|
||||
xl: 'w-4 h-4',
|
||||
};
|
||||
|
||||
const trashBtnClasses: Record<IconSize, string> = {
|
||||
sm: 'ml-0.5 h-[18px] w-[18px] rounded-sm',
|
||||
md: 'ml-0.5 h-[21px] w-[21px] rounded-sm',
|
||||
lg: 'ml-1 h-[23px] w-[23px] rounded',
|
||||
xl: 'ml-1.5 h-[25px] w-[25px] rounded',
|
||||
};
|
||||
|
||||
const trashIconClasses: Record<IconSize, string> = {
|
||||
sm: 'w-[10px] h-[10px]',
|
||||
md: 'w-[11px] h-[11px]',
|
||||
lg: 'w-[12px] h-[12px]',
|
||||
xl: 'w-[13px] h-[13px]',
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setRefs}
|
||||
|
|
@ -181,26 +173,26 @@ export function DocumentTile({
|
|||
>
|
||||
<DocumentPreview doc={doc} />
|
||||
</Link>
|
||||
<div className={`flex items-center w-full ${bottomPaddingClasses[iconSize]}`}>
|
||||
<div className={`flex items-center w-full ${BOTTOM_PADDING_CLASSES[iconSize]}`}>
|
||||
<Link
|
||||
href={href}
|
||||
draggable={false}
|
||||
className={`flex items-center flex-1 min-w-0 rounded-md ${linkPaddingClasses[iconSize]} ${gapClasses[iconSize]}`}
|
||||
className={`flex items-center flex-1 min-w-0 rounded-md ${LINK_PADDING_CLASS} ${GAP_CLASSES[iconSize]}`}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<span className="flex-shrink-0 flex items-center">
|
||||
{doc.type === 'pdf' ? (
|
||||
<PDFIcon className={`${fileIconClasses[iconSize]} text-red-500`} />
|
||||
<PDFIcon className={`${FILE_ICON_CLASSES[iconSize]} text-red-500`} />
|
||||
) : doc.type === 'epub' ? (
|
||||
<EPUBIcon className={`${fileIconClasses[iconSize]} text-blue-500`} />
|
||||
<EPUBIcon className={`${FILE_ICON_CLASSES[iconSize]} text-blue-500`} />
|
||||
) : (
|
||||
<FileIcon className={`${fileIconClasses[iconSize]} text-muted`} />
|
||||
<FileIcon className={`${FILE_ICON_CLASSES[iconSize]} text-muted`} />
|
||||
)}
|
||||
</span>
|
||||
<span
|
||||
className={
|
||||
'leading-none font-medium truncate flex-1 min-w-0 ' +
|
||||
sizeClasses[iconSize] +
|
||||
NAME_SIZE_CLASSES[iconSize] +
|
||||
' ' +
|
||||
(isSelected ? 'text-accent' : 'text-foreground group-hover:text-accent')
|
||||
}
|
||||
|
|
@ -211,10 +203,10 @@ export function DocumentTile({
|
|||
{showDeleteButton && (
|
||||
<Button
|
||||
onClick={() => onDelete(doc)}
|
||||
className={`inline-flex items-center justify-center text-muted hover:text-accent hover:bg-base focus:outline-none transition-colors duration-200 ${trashBtnClasses[iconSize]}`}
|
||||
className={`inline-flex items-center justify-center text-muted hover:text-accent hover:bg-base focus:outline-none transition-colors duration-200 ${TRASH_BTN_CLASSES[iconSize]}`}
|
||||
aria-label={`Delete ${doc.name}`}
|
||||
>
|
||||
<svg className={trashIconClasses[iconSize]} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg className={TRASH_ICON_CLASSES[iconSize]} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import type { DocumentListDocument, Folder } from '@/types/documents';
|
|||
import { PDFIcon, EPUBIcon, FileIcon } from '@/components/icons/Icons';
|
||||
import { FolderIcon } from '../window/finderIcons';
|
||||
import { DocumentPreview } from '@/components/doclist/DocumentPreview';
|
||||
import { formatDocumentSize } from '@/components/doclist/formatSize';
|
||||
import { useDocumentSelection } from '../dnd/DocumentSelectionContext';
|
||||
import { DND_DOCUMENT, type DocumentDragItem } from '../dnd/dndTypes';
|
||||
|
||||
|
|
@ -180,25 +181,20 @@ export function GalleryView({
|
|||
{activeDoc.name}
|
||||
</h2>
|
||||
<p className="text-[11px] text-muted">
|
||||
{activeDoc.type.toUpperCase()} •{' '}
|
||||
{activeDoc.size >= 1024 * 1024 * 1024
|
||||
? `${(activeDoc.size / 1024 / 1024 / 1024).toFixed(2)} GB`
|
||||
: activeDoc.size >= 1024 * 1024
|
||||
? `${(activeDoc.size / 1024 / 1024).toFixed(2)} MB`
|
||||
: `${(activeDoc.size / 1024).toFixed(1)} KB`}
|
||||
{activeDoc.type.toUpperCase()} • {formatDocumentSize(activeDoc.size)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Link
|
||||
href={`/${activeDoc.type}/${encodeURIComponent(activeDoc.id)}`}
|
||||
className="h-8 px-4 inline-flex items-center justify-center rounded-md bg-accent text-background text-[12px] font-medium hover:bg-secondary-accent hover:scale-[1.02] transition-all duration-200 ease-out"
|
||||
className="h-8 px-4 inline-flex items-center justify-center 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
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onDeleteDoc(activeDoc)}
|
||||
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.02] 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
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import type {
|
|||
SortDirection,
|
||||
} from '@/types/documents';
|
||||
import { PDFIcon, EPUBIcon, FileIcon } from '@/components/icons/Icons';
|
||||
import { formatDocumentSize } from '@/components/doclist/formatSize';
|
||||
import { FolderIcon } from '../window/finderIcons';
|
||||
import { useDocumentSelection } from '../dnd/DocumentSelectionContext';
|
||||
import { DND_DOCUMENT, type DocumentDragItem } from '../dnd/dndTypes';
|
||||
|
|
@ -34,14 +35,6 @@ function formatDate(ms: number): string {
|
|||
return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
|
||||
}
|
||||
|
||||
function formatSize(bytes: number): string {
|
||||
const mb = bytes / 1024 / 1024;
|
||||
if (mb >= 1024) {
|
||||
return `${(mb / 1024).toFixed(2)} GB`;
|
||||
}
|
||||
return mb >= 1 ? `${mb.toFixed(2)} MB` : `${(bytes / 1024).toFixed(1)} KB`;
|
||||
}
|
||||
|
||||
function KindIcon({ doc }: { doc: DocumentListDocument }) {
|
||||
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" />;
|
||||
|
|
@ -180,7 +173,7 @@ function DocRow({
|
|||
</Link>
|
||||
<span className="px-2 text-[11px] text-muted uppercase tracking-wide">{doc.type}</span>
|
||||
<span className="px-2 text-[11px] text-muted text-right tabular-nums">
|
||||
{formatSize(doc.size)}
|
||||
{formatDocumentSize(doc.size)}
|
||||
</span>
|
||||
<span className="px-2 text-[11px] text-muted tabular-nums">
|
||||
{formatDate(doc.lastModified)}
|
||||
|
|
@ -259,7 +252,7 @@ function FolderRowGroup({
|
|||
</button>
|
||||
<span className="px-2 text-[11px] text-muted">Folder</span>
|
||||
<span className="px-2 text-[11px] text-muted text-right tabular-nums">
|
||||
{formatSize(totalSize)}
|
||||
{formatDocumentSize(totalSize)}
|
||||
</span>
|
||||
<span className="px-2 text-[11px] text-muted tabular-nums">
|
||||
{folder.documents.length} items
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { useDocuments } from '@/contexts/DocumentContext';
|
|||
import { uploadDocxAsPdf } from '@/lib/client/api/documents';
|
||||
import { useFeatureFlag } from '@/contexts/RuntimeConfigContext';
|
||||
|
||||
|
||||
interface DocumentUploaderProps {
|
||||
className?: string;
|
||||
variant?: 'default' | 'compact' | 'overlay';
|
||||
|
|
@ -93,14 +92,13 @@ export function DocumentUploader({ className = '', variant = 'default', children
|
|||
: `${
|
||||
isDragActive
|
||||
? 'border-2 border-dashed border-accent bg-base text-foreground'
|
||||
: 'border-2 border-dashed border-muted bg-transparent text-foreground hover:border-accent hover:bg-base hover:scale-[1.008]'
|
||||
: 'border-2 border-dashed border-muted bg-transparent text-foreground hover:border-accent hover:bg-base hover:scale-[1.01]'
|
||||
}`;
|
||||
|
||||
const paddingClass = variant === 'compact' ? 'py-1 px-2 rounded-md' : 'py-5 px-3 rounded-lg';
|
||||
|
||||
if (variant === 'overlay') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { onClick, onKeyDown, ...rootProps } = getRootProps();
|
||||
const rootProps = getRootProps();
|
||||
return (
|
||||
<div {...rootProps} className={`relative w-full h-full ${className}`}>
|
||||
<input {...getInputProps()} />
|
||||
|
|
|
|||
Loading…
Reference in a new issue