cleanup doclist formatting helpers and view props

This commit is contained in:
Richard R 2026-05-27 19:40:12 -06:00
parent eb8820d888
commit 46d5bab647
8 changed files with 84 additions and 109 deletions

View file

@ -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}
/>

View file

@ -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>
);

View 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`;
}

View file

@ -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>

View file

@ -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"

View file

@ -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>

View file

@ -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

View file

@ -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()} />