refactor(doclist): remove columns view and update text labeling

Eliminate the deprecated "columns" view mode from the document list, toolbar,
icons, and skeleton components. Update all related types and logic to only
support "icons", "list", and "gallery" views. Standardize user-facing labels
from "HTML" to "Text" for improved clarity across the sidebar, segment
locator labels, and document counts. Clean up associated code and types to
reflect these changes.
This commit is contained in:
Richard R 2026-05-28 17:20:02 -06:00
parent c12f795026
commit 98736bf6e3
8 changed files with 11 additions and 247 deletions

View file

@ -33,7 +33,6 @@ import { FinderSidebar } from './window/FinderSidebar';
import { FinderStatusBar } from './window/FinderStatusBar';
import { IconsView } from './views/IconsView';
import { ListView } from './views/ListView';
import { ColumnsView } from './views/ColumnsView';
import { GalleryView } from './views/GalleryView';
let cachedDocumentListState: DocumentListState | null = null;
@ -71,7 +70,8 @@ const DEFAULT_STATE: Required<
function normalizeViewMode(stored: DocumentListState['viewMode']): ViewMode {
if (stored === 'grid' || stored === undefined) return 'icons';
if (stored === 'list') return 'list';
return stored;
if (stored === 'gallery') return 'gallery';
return 'icons';
}
function generateDefaultFolderName(
@ -463,7 +463,7 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) {
const parts: string[] = [];
if (counts.pdf) parts.push(`${counts.pdf} PDF${counts.pdf === 1 ? '' : 's'}`);
if (counts.epub) parts.push(`${counts.epub} EPUB${counts.epub === 1 ? '' : 's'}`);
if (counts.html) parts.push(`${counts.html} HTML${counts.html === 1 ? '' : 's'}`);
if (counts.html) parts.push(`${counts.html} Text${counts.html === 1 ? ' Doc' : ' Docs'}`);
return parts.join(' • ');
}, [counts]);
@ -474,8 +474,7 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) {
const isLoading = isPDFLoading || isEPUBLoading || isHTMLLoading;
const fallbackViewMode: ViewMode =
viewMode === 'columns' && isNarrow ? 'list' : viewMode;
const fallbackViewMode: ViewMode = viewMode;
const effectiveSidebarOpen = isNarrow ? mobileSidebarOpen : sidebarOpen;
return (
@ -500,7 +499,6 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) {
: setSidebarOpen((p) => !p)
}
isSidebarOpen={effectiveSidebarOpen}
isNarrow={isNarrow}
leftSlot={brand}
/>
}
@ -588,13 +586,6 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) {
onMergeIntoFolder={handleMergeIntoFolder}
/>
)}
{fallbackViewMode === 'columns' && (
<ColumnsView
documents={sortedVisible}
onDeleteDoc={handleDeleteDoc}
onMergeIntoFolder={handleMergeIntoFolder}
/>
)}
{fallbackViewMode === 'gallery' && (
<GalleryView
documents={sortedVisible}

View file

@ -75,36 +75,6 @@ function ListSkeleton() {
);
}
function ColumnsSkeleton() {
return (
<div className="h-full min-h-0 flex overflow-x-auto overflow-y-hidden">
<div className="w-[260px] shrink-0 h-full bg-base border-r border-offbase overflow-y-auto">
<div className="sticky top-0 px-3 py-1.5 border-b border-offbase bg-base">
<div className="h-2.5 w-20 rounded bg-offbase" />
</div>
<div className="p-1.5 flex flex-col gap-1">
{Array.from({ length: 11 }).map((_, rowIndex) => (
<div key={rowIndex} className="h-7 rounded-md bg-offbase" />
))}
</div>
</div>
<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="rounded-lg overflow-hidden border border-offbase bg-base">
<div className="aspect-[3/4] w-full bg-offbase" />
</div>
<div className="mt-3 h-3.5 w-4/5 rounded bg-offbase" />
<div className="mt-2 h-2.5 w-1/3 rounded bg-offbase" />
<div className="mt-3 flex gap-2">
<div className="flex-1 h-8 rounded-md bg-offbase" />
<div className="w-20 h-8 rounded-md bg-offbase" />
</div>
</div>
</div>
</div>
);
}
function GallerySkeleton() {
return (
<div className="h-full min-h-0 flex flex-col">
@ -142,8 +112,6 @@ export function DocumentListSkeleton({
let body;
if (viewMode === 'list') {
body = <ListSkeleton />;
} else if (viewMode === 'columns') {
body = <ColumnsSkeleton />;
} else if (viewMode === 'gallery') {
body = <GallerySkeleton />;
} else {

View file

@ -1,179 +0,0 @@
'use client';
import Link from 'next/link';
import { useEffect, useState } from 'react';
import { useDrag, useDrop } from 'react-dnd';
import type { DocumentListDocument } from '@/types/documents';
import { PDFIcon, EPUBIcon, FileIcon } from '@/components/icons/Icons';
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';
interface ColumnsViewProps {
documents: DocumentListDocument[];
onDeleteDoc: (doc: DocumentListDocument) => void;
onMergeIntoFolder: (sources: DocumentListDocument[], target: DocumentListDocument) => void;
}
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" />;
return <FileIcon className="w-4 h-4 text-muted" />;
}
function ColumnDocRow({
doc,
active,
onClick,
onMergeIntoFolder,
}: {
doc: DocumentListDocument;
active: boolean;
onClick: () => void;
onMergeIntoFolder: (sources: DocumentListDocument[], target: DocumentListDocument) => void;
}) {
const selection = useDocumentSelection();
const isSelected = selection.isSelected(doc);
const isInFolder = Boolean(doc.folderId);
const [{ isDragging }, dragRef] = useDrag<DocumentDragItem, void, { isDragging: boolean }>(() => ({
type: DND_DOCUMENT,
item: () => {
const sel = selection.getSelectedDocs();
const dragging = isSelected && sel.length > 1 ? sel : [doc];
if (!isSelected) selection.replace([doc]);
return { ids: dragging.map((d) => d.id), docs: dragging, fromFolderId: doc.folderId };
},
collect: (m) => ({ isDragging: m.isDragging() }),
}), [doc, isSelected]);
const [{ isOver, canDrop }, dropRef] = useDrop<DocumentDragItem, void, { isOver: boolean; canDrop: boolean }>(() => ({
accept: DND_DOCUMENT,
canDrop: (item) => !isInFolder && !item.ids.includes(doc.id),
drop: (item) => onMergeIntoFolder(item.docs, doc),
collect: (m) => ({ isOver: m.isOver({ shallow: true }), canDrop: m.canDrop() }),
}), [doc, isInFolder, onMergeIntoFolder]);
const setRefs = (node: HTMLDivElement | null) => {
dragRef(node);
dropRef(node);
};
return (
<div
ref={setRefs}
data-doc-tile
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 || isSelected
? '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' : '') +
(isDragging ? ' opacity-50' : '')
}
>
<span className={'w-4 h-4 shrink-0 flex items-center justify-center transition-colors duration-200 ' + ((active || isSelected) ? 'text-accent' : 'text-muted group-hover:text-accent')}>
<KindIcon doc={doc} />
</span>
<span className="truncate flex-1">{doc.name}</span>
</div>
);
}
function Column({ children, title }: { children: React.ReactNode; title?: string }) {
return (
<div className="w-[260px] shrink-0 h-full bg-base border-r border-offbase overflow-y-auto">
{title && (
<div className="sticky top-0 px-3 py-1.5 text-[10px] uppercase tracking-wider text-muted bg-base border-b border-offbase">
{title}
</div>
)}
<div className="p-1.5 flex flex-col gap-0.5">{children}</div>
</div>
);
}
export function ColumnsView({
documents,
onDeleteDoc,
onMergeIntoFolder,
}: ColumnsViewProps) {
const { setVisibleOrder } = useDocumentSelection();
const [selectedDoc, setSelectedDoc] = useState<DocumentListDocument | null>(null);
const openHref = selectedDoc
? `/${selectedDoc.type}/${encodeURIComponent(selectedDoc.id)}`
: null;
useEffect(() => {
setVisibleOrder(documents);
}, [documents, setVisibleOrder]);
useEffect(() => {
if (documents.length === 0) {
if (selectedDoc !== null) setSelectedDoc(null);
return;
}
if (!selectedDoc) {
setSelectedDoc(documents[0]);
return;
}
const selectedStillExists = documents.some(
(d) => d.id === selectedDoc.id && d.type === selectedDoc.type,
);
if (!selectedStillExists) {
setSelectedDoc(documents[0]);
}
}, [documents, selectedDoc]);
return (
<div className="flex-1 min-h-0 flex overflow-x-auto overflow-y-hidden">
<Column title="Documents">
{documents.map((doc) => (
<ColumnDocRow
key={`${doc.type}-${doc.id}`}
doc={doc}
active={selectedDoc?.id === doc.id && selectedDoc?.type === doc.type}
onClick={() => setSelectedDoc(doc)}
onMergeIntoFolder={onMergeIntoFolder}
/>
))}
</Column>
{selectedDoc && (
<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="rounded-lg overflow-hidden border border-offbase">
<DocumentPreview doc={selectedDoc} />
</div>
<h3 className="mt-3 text-[13px] font-semibold text-foreground truncate">
{selectedDoc.name}
</h3>
<p className="text-[11px] text-muted">
{selectedDoc.type.toUpperCase()} {formatDocumentSize(selectedDoc.size)}
</p>
<div className="mt-3 flex gap-2">
<Link
href={openHref || '/app'}
prefetch={false}
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(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"
>
Delete
</button>
</div>
</div>
</div>
)}
</div>
);
}

View file

@ -246,7 +246,7 @@ export function FinderSidebar({
active={filter === 'html'}
onClick={() => onFilterChange('html')}
icon={<FileIcon className="w-3.5 h-3.5" />}
label="HTML / Text"
label="Text"
count={counts.html}
/>

View file

@ -5,7 +5,6 @@ import type { IconSize, SortBy, SortDirection, ViewMode } from '@/types/document
import {
IconsViewIcon,
ListViewIcon,
ColumnsViewIcon,
GalleryViewIcon,
SearchIcon,
HamburgerIcon,
@ -26,8 +25,6 @@ interface FinderToolbarProps {
onQueryChange: (q: string) => void;
onToggleSidebar: () => void;
isSidebarOpen: boolean;
/** True when the columns view should be disabled (mobile viewport). */
isNarrow: boolean;
/** App-level content rendered at the far left (brand/logo). */
leftSlot?: ReactNode;
/** App-level content rendered at the far right (settings, user menu). */
@ -37,7 +34,6 @@ interface FinderToolbarProps {
const VIEW_BUTTONS: Array<{ value: ViewMode; label: string; Icon: typeof IconsViewIcon }> = [
{ value: 'icons', label: 'Icons', Icon: IconsViewIcon },
{ value: 'list', label: 'List', Icon: ListViewIcon },
{ value: 'columns', label: 'Columns', Icon: ColumnsViewIcon },
{ value: 'gallery', label: 'Gallery', Icon: GalleryViewIcon },
];
@ -84,7 +80,6 @@ export function FinderToolbar({
onQueryChange,
onToggleSidebar,
isSidebarOpen,
isNarrow,
leftSlot,
rightSlot,
}: FinderToolbarProps) {
@ -113,7 +108,6 @@ export function FinderToolbar({
<div className={PILL}>
{VIEW_BUTTONS.map(({ value, label, Icon }) => {
const disabled = value === 'columns' && isNarrow;
const active = viewMode === value;
const isIconsToggle = value === 'icons';
return (
@ -123,16 +117,14 @@ export function FinderToolbar({
>
<button
type="button"
disabled={disabled}
onClick={() => onViewModeChange(value)}
aria-pressed={active}
aria-label={`${label} view`}
title={disabled ? `${label} (desktop only)` : `${label} view`}
title={`${label} view`}
className={
PILL_SEGMENT +
' h-6 w-7 ' +
(active ? PILL_SEGMENT_ACTIVE : PILL_SEGMENT_INACTIVE) +
(disabled ? ' opacity-40 cursor-not-allowed hover:bg-transparent hover:text-muted' : '')
(active ? PILL_SEGMENT_ACTIVE : PILL_SEGMENT_INACTIVE)
}
>
<Icon className="w-4 h-4" />

View file

@ -57,14 +57,6 @@ export const ListViewIcon = (props: IconProps) => (
</svg>
);
export const ColumnsViewIcon = (props: IconProps) => (
<svg {...baseSvg(props)}>
<rect x="3" y="4" width="5" height="16" rx="1" />
<rect x="10" y="4" width="5" height="16" rx="1" />
<rect x="17" y="4" width="4" height="16" rx="1" />
</svg>
);
export const GalleryViewIcon = (props: IconProps) => (
<svg {...baseSvg(props)}>
<rect x="3" y="4" width="18" height="11" rx="1.5" />

View file

@ -118,9 +118,9 @@ function formatLocatorGroupLabel(locator: TTSSegmentLocator | null): string {
}
if (isHtmlLocator(locator)) {
if (/^\d+$/.test(locator.location)) {
return `Block ${locator.location} · HTML`;
return `Block ${locator.location} · Text`;
}
return `${locator.location} · HTML`;
return `${locator.location} · Text`;
}
return 'Unknown location';
}

View file

@ -61,7 +61,7 @@ export interface Folder {
export type SortBy = 'name' | 'type' | 'date' | 'size';
export type SortDirection = 'asc' | 'desc';
export type ViewMode = 'icons' | 'list' | 'columns' | 'gallery';
export type ViewMode = 'icons' | 'list' | 'gallery';
export type IconSize = 'sm' | 'md' | 'lg' | 'xl';
// Filter applied from the sidebar.
@ -74,7 +74,7 @@ export interface DocumentListState {
folders: Folder[];
collapsedFolders: string[];
showHint: boolean;
viewMode?: ViewMode | 'list' | 'grid';
viewMode?: ViewMode | 'grid';
iconSize?: IconSize;
sidebarWidth?: number;
sidebarFilter?: SidebarFilter;