openreader/src/components/doclist/window/FinderToolbar.tsx
2026-06-01 02:41:48 -06:00

196 lines
7.4 KiB
TypeScript

'use client';
import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react';
import type { IconSize, SortBy, SortDirection, ViewMode } from '@/types/documents';
import {
IconsViewIcon,
ListViewIcon,
GalleryViewIcon,
SearchIcon,
HamburgerIcon,
} from './finderIcons';
import { ChevronUpDownIcon } from '@/components/icons/Icons';
import { Toolbar, ToolbarButton, ToolbarGroup, ToolbarSegment, toolbarButtonStyles } from '@/components/ui';
import type { ReactNode } from 'react';
interface FinderToolbarProps {
viewMode: ViewMode;
onViewModeChange: (mode: ViewMode) => void;
iconSize: IconSize;
onIconSizeChange: (size: IconSize) => void;
sortBy: SortBy;
sortDirection: SortDirection;
onSortByChange: (s: SortBy) => void;
onSortDirectionToggle: () => void;
query: string;
onQueryChange: (q: string) => void;
onToggleSidebar: () => void;
isSidebarOpen: boolean;
showSortControls?: boolean;
/** App-level content rendered at the far left (brand/logo). */
leftSlot?: ReactNode;
/** App-level content rendered at the far right (settings, user menu). */
rightSlot?: ReactNode;
}
const VIEW_BUTTONS: Array<{ value: ViewMode; label: string; Icon: typeof IconsViewIcon }> = [
{ value: 'icons', label: 'Icons', Icon: IconsViewIcon },
{ value: 'list', label: 'List', Icon: ListViewIcon },
{ value: 'gallery', label: 'Gallery', Icon: GalleryViewIcon },
];
const SORT_OPTIONS: Array<{ value: SortBy; label: string; asc: string; desc: string }> = [
{ value: 'name', label: 'Name', asc: 'A → Z', desc: 'Z → A' },
{ value: 'type', label: 'Kind', asc: 'A → Z', desc: 'Z → A' },
{ value: 'date', label: 'Modified', asc: 'Oldest', desc: 'Newest' },
{ value: 'size', label: 'Size', asc: 'Smallest', desc: 'Largest' },
];
const ICON_SIZES: Array<{ value: IconSize; label: string }> = [
{ value: 'sm', label: 'S' },
{ value: 'md', label: 'M' },
{ value: 'lg', label: 'L' },
{ value: 'xl', label: 'XL' },
];
export function FinderToolbar({
viewMode,
onViewModeChange,
iconSize,
onIconSizeChange,
sortBy,
sortDirection,
onSortByChange,
onSortDirectionToggle,
query,
onQueryChange,
onToggleSidebar,
isSidebarOpen,
showSortControls = true,
leftSlot,
rightSlot,
}: FinderToolbarProps) {
const currentSort = SORT_OPTIONS.find((o) => o.value === sortBy) ?? SORT_OPTIONS[0];
const directionLabel = sortDirection === 'asc' ? currentSort.asc : currentSort.desc;
return (
<Toolbar>
{leftSlot && (
<div className="shrink-0 flex items-center gap-2 pr-1 sm:pr-2 sm:border-r sm:border-line">
{leftSlot}
</div>
)}
<ToolbarButton
onClick={onToggleSidebar}
active={isSidebarOpen}
className="shrink-0"
aria-pressed={isSidebarOpen}
aria-label="Toggle sidebar"
title="Toggle sidebar"
>
<HamburgerIcon className="w-4 h-4" />
</ToolbarButton>
<ToolbarGroup>
{VIEW_BUTTONS.map(({ value, label, Icon }) => {
const active = viewMode === value;
const isIconsToggle = value === 'icons';
return (
<div
key={value}
className={isIconsToggle ? 'relative group/icons inline-flex items-center' : 'inline-flex items-center'}
>
<ToolbarSegment
onClick={() => onViewModeChange(value)}
active={active}
aria-pressed={active}
aria-label={`${label} view`}
title={`${label} view`}
className="w-7"
>
<Icon className="w-4 h-4" />
</ToolbarSegment>
{isIconsToggle && viewMode === 'icons' && (
<div
className="absolute top-full left-1/2 z-30 -translate-x-1/2 pt-1 opacity-0 pointer-events-none transition-opacity duration-fast group-hover/icons:opacity-100 group-hover/icons:pointer-events-auto group-focus-within/icons:opacity-100 group-focus-within/icons:pointer-events-auto"
>
<ToolbarGroup className="shadow-elev-2">
{ICON_SIZES.map(({ value: sizeValue, label: sizeLabel }) => {
const sizeActive = iconSize === sizeValue;
return (
<ToolbarSegment
key={sizeValue}
onClick={() => onIconSizeChange(sizeValue)}
active={sizeActive}
aria-pressed={sizeActive}
aria-label={`Icon size ${sizeLabel}`}
className="min-w-[26px] px-1.5 font-semibold tracking-wide"
>
{sizeLabel}
</ToolbarSegment>
);
})}
</ToolbarGroup>
</div>
)}
</div>
);
})}
</ToolbarGroup>
{showSortControls && (
<div className="flex items-center gap-1 shrink-0">
<ToolbarButton onClick={onSortDirectionToggle} className="whitespace-nowrap" title="Toggle sort direction">
{directionLabel}
</ToolbarButton>
<Listbox value={sortBy} onChange={onSortByChange}>
<ListboxButton
className={toolbarButtonStyles({ className: 'gap-1 min-w-[90px] justify-between' })}
>
<span>{currentSort.label}</span>
<ChevronUpDownIcon className="h-3 w-3 opacity-60" />
</ListboxButton>
<ListboxOptions
anchor="bottom end"
className="z-50 mt-1 rounded-md bg-surface-sunken border border-line shadow-elev-2 p-1 focus:outline-none"
>
{SORT_OPTIONS.map((opt) => (
<ListboxOption
key={opt.value}
value={opt.value}
className={({ active, selected }) =>
`cursor-pointer select-none rounded-sm py-1.5 px-2.5 text-xs ${
active ? 'bg-surface-sunken text-accent' : 'text-foreground'
} ${selected ? 'font-semibold' : ''}`
}
>
{opt.label}
</ListboxOption>
))}
</ListboxOptions>
</Listbox>
</div>
)}
<div className="flex-1 min-w-0" />
<div className="hidden sm:flex items-center gap-1.5 px-2 py-1 rounded-md bg-surface-sunken border border-line hover:border-accent-line focus-within:ring-1 focus-within:ring-accent-line focus-within:border-accent-line transition-colors duration-base ease-standard w-[160px] md:w-[200px]">
<SearchIcon className="w-3.5 h-3.5 text-soft shrink-0" />
<input
type="search"
value={query}
onChange={(e) => onQueryChange(e.target.value)}
placeholder="Search"
className="flex-1 min-w-0 bg-transparent outline-none text-xs text-foreground placeholder:text-soft"
/>
</div>
{rightSlot && (
<div className="shrink-0 flex items-center gap-2 pl-1 sm:pl-2 sm:border-l sm:border-line ml-0.5">
{rightSlot}
</div>
)}
</Toolbar>
);
}