feat(doclist): improve sidebar responsiveness and toolbar interaction in document list
Enhance mobile and desktop sidebar behavior by introducing a separate state for mobile sidebar visibility and ensuring it does not persist from desktop state. Update FinderToolbar to visually indicate sidebar open state and make the toolbar sticky for better accessibility. Refine FinderSidebar width handling for improved responsiveness. Remove redundant border in mobile sidebar dialog for a cleaner appearance. These changes provide a more consistent and intuitive sidebar experience across viewports.
This commit is contained in:
parent
0470f044ef
commit
53993aa3c6
4 changed files with 140 additions and 119 deletions
|
|
@ -124,6 +124,7 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) {
|
|||
const [sidebarWidth, setSidebarWidth] = useState(DEFAULT_STATE.sidebarWidth);
|
||||
const [sidebarFilter, setSidebarFilter] = useState<SidebarFilter>('all');
|
||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
||||
const [query, setQuery] = useState('');
|
||||
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
|
|
@ -206,6 +207,12 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) {
|
|||
isInitialized,
|
||||
]);
|
||||
|
||||
// Mobile drawer should never auto-open from persisted desktop state.
|
||||
useEffect(() => {
|
||||
if (!isNarrow) return;
|
||||
setMobileSidebarOpen(false);
|
||||
}, [isNarrow]);
|
||||
|
||||
// Build the union document list.
|
||||
const allDocuments: DocumentListDocument[] = useMemo(
|
||||
() => [
|
||||
|
|
@ -413,6 +420,7 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) {
|
|||
|
||||
const fallbackViewMode: ViewMode =
|
||||
viewMode === 'columns' && isNarrow ? 'list' : viewMode;
|
||||
const effectiveSidebarOpen = isNarrow ? mobileSidebarOpen : sidebarOpen;
|
||||
|
||||
return (
|
||||
<FinderWindow
|
||||
|
|
@ -434,7 +442,12 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) {
|
|||
setNewFolderName('');
|
||||
setManualFolderPrompt(true);
|
||||
}}
|
||||
onToggleSidebar={() => setSidebarOpen((p) => !p)}
|
||||
onToggleSidebar={() =>
|
||||
isNarrow
|
||||
? setMobileSidebarOpen((p) => !p)
|
||||
: setSidebarOpen((p) => !p)
|
||||
}
|
||||
isSidebarOpen={effectiveSidebarOpen}
|
||||
isNarrow={isNarrow}
|
||||
leftSlot={brand}
|
||||
rightSlot={appActions}
|
||||
|
|
@ -460,8 +473,11 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) {
|
|||
summary={summary}
|
||||
/>
|
||||
}
|
||||
sidebarOpen={sidebarOpen}
|
||||
onSidebarOpenChange={setSidebarOpen}
|
||||
sidebarOpen={effectiveSidebarOpen}
|
||||
onSidebarOpenChange={(open) => {
|
||||
if (isNarrow) setMobileSidebarOpen(open);
|
||||
else setSidebarOpen(open);
|
||||
}}
|
||||
>
|
||||
{!isLoading && showHint && allDocuments.length > 1 && (
|
||||
<div className="px-3 pt-3 shrink-0 bg-background">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { useRef, type ReactNode } from 'react';
|
||||
import { useRef, type CSSProperties, type ReactNode } from 'react';
|
||||
import { useDrop } from 'react-dnd';
|
||||
import type { Folder, SidebarFilter } from '@/types/documents';
|
||||
import { PDFIcon, EPUBIcon, FileIcon } from '@/components/icons/Icons';
|
||||
|
|
@ -158,8 +158,8 @@ export function FinderSidebar({
|
|||
|
||||
return (
|
||||
<aside
|
||||
style={{ width }}
|
||||
className="relative h-full bg-base border-r border-offbase shrink-0 overflow-y-auto"
|
||||
style={{ '--sidebar-width': `${width}px` } as CSSProperties}
|
||||
className="relative h-full w-full md:[width:var(--sidebar-width)] bg-base border-r border-offbase shrink-0 overflow-y-auto"
|
||||
>
|
||||
<div className="p-2 flex flex-col gap-0.5">
|
||||
{topSlot && (
|
||||
|
|
@ -234,4 +234,3 @@ export function FinderSidebar({
|
|||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ interface FinderToolbarProps {
|
|||
onQueryChange: (q: string) => void;
|
||||
onNewFolder: () => 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). */
|
||||
|
|
@ -61,6 +62,7 @@ const TOOLBAR_BTN =
|
|||
'inline-flex items-center py-1 px-2 rounded-md border bg-base text-xs transition-all duration-200 ease-out hover:scale-[1.01]';
|
||||
const TOOLBAR_BTN_INACTIVE =
|
||||
'border-offbase text-foreground hover:text-accent hover:border-accent hover:bg-offbase';
|
||||
const TOOLBAR_BTN_ACTIVE = 'border-accent bg-offbase text-accent';
|
||||
|
||||
// Pill-grouped segmented control. Outer pill carries the border; inner segments are
|
||||
// borderless and rely on bg/text color to show active/hover. Sized so the whole pill
|
||||
|
|
@ -84,6 +86,7 @@ export function FinderToolbar({
|
|||
onQueryChange,
|
||||
onNewFolder,
|
||||
onToggleSidebar,
|
||||
isSidebarOpen,
|
||||
isNarrow,
|
||||
leftSlot,
|
||||
rightSlot,
|
||||
|
|
@ -92,138 +95,141 @@ export function FinderToolbar({
|
|||
const directionLabel = sortDirection === 'asc' ? currentSort.asc : currentSort.desc;
|
||||
|
||||
return (
|
||||
<div className="min-h-10 px-2 sm:px-3 py-1 flex items-center gap-1.5 sm:gap-2 bg-base border-b border-offbase">
|
||||
{leftSlot && (
|
||||
<div className="shrink-0 flex items-center gap-2 pr-1 sm:pr-2 sm:border-r sm:border-offbase">
|
||||
{leftSlot}
|
||||
</div>
|
||||
)}
|
||||
<div className="sticky top-0 z-40 w-full border-b border-offbase bg-base">
|
||||
<div className="px-2 sm:px-3 py-1 min-h-10 flex items-center gap-1.5 sm:gap-2">
|
||||
{leftSlot && (
|
||||
<div className="shrink-0 flex items-center gap-2 pr-1 sm:pr-2 sm:border-r sm:border-offbase">
|
||||
{leftSlot}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggleSidebar}
|
||||
className={`${TOOLBAR_BTN} ${TOOLBAR_BTN_INACTIVE} shrink-0`}
|
||||
aria-label="Toggle sidebar"
|
||||
title="Toggle sidebar"
|
||||
>
|
||||
<HamburgerIcon className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggleSidebar}
|
||||
className={`${TOOLBAR_BTN} ${isSidebarOpen ? TOOLBAR_BTN_ACTIVE : TOOLBAR_BTN_INACTIVE} shrink-0`}
|
||||
aria-pressed={isSidebarOpen}
|
||||
aria-label="Toggle sidebar"
|
||||
title="Toggle sidebar"
|
||||
>
|
||||
<HamburgerIcon className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
<div className={PILL}>
|
||||
{VIEW_BUTTONS.map(({ value, label, Icon }) => {
|
||||
const disabled = value === 'columns' && isNarrow;
|
||||
const active = viewMode === value;
|
||||
return (
|
||||
<button
|
||||
key={value}
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => onViewModeChange(value)}
|
||||
aria-pressed={active}
|
||||
aria-label={`${label} view`}
|
||||
title={disabled ? `${label} (desktop only)` : `${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' : '')
|
||||
}
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{viewMode === 'icons' && (
|
||||
<div className={`${PILL} hidden sm:inline-flex`}>
|
||||
{ICON_SIZES.map(({ value, label }) => {
|
||||
const active = iconSize === value;
|
||||
<div className={PILL}>
|
||||
{VIEW_BUTTONS.map(({ value, label, Icon }) => {
|
||||
const disabled = value === 'columns' && isNarrow;
|
||||
const active = viewMode === value;
|
||||
return (
|
||||
<button
|
||||
key={value}
|
||||
type="button"
|
||||
onClick={() => onIconSizeChange(value)}
|
||||
disabled={disabled}
|
||||
onClick={() => onViewModeChange(value)}
|
||||
aria-pressed={active}
|
||||
aria-label={`Icon size ${label}`}
|
||||
aria-label={`${label} view`}
|
||||
title={disabled ? `${label} (desktop only)` : `${label} view`}
|
||||
className={
|
||||
PILL_SEGMENT +
|
||||
' h-6 min-w-[26px] px-1.5 font-semibold tracking-wide ' +
|
||||
(active ? PILL_SEGMENT_ACTIVE : PILL_SEGMENT_INACTIVE)
|
||||
' h-6 w-7 ' +
|
||||
(active ? PILL_SEGMENT_ACTIVE : PILL_SEGMENT_INACTIVE) +
|
||||
(disabled ? ' opacity-40 cursor-not-allowed hover:bg-transparent hover:text-muted' : '')
|
||||
}
|
||||
>
|
||||
{label}
|
||||
<Icon className="w-4 h-4" />
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
{viewMode === 'icons' && (
|
||||
<div className={`${PILL} hidden sm:inline-flex`}>
|
||||
{ICON_SIZES.map(({ value, label }) => {
|
||||
const active = iconSize === value;
|
||||
return (
|
||||
<button
|
||||
key={value}
|
||||
type="button"
|
||||
onClick={() => onIconSizeChange(value)}
|
||||
aria-pressed={active}
|
||||
aria-label={`Icon size ${label}`}
|
||||
className={
|
||||
PILL_SEGMENT +
|
||||
' h-6 min-w-[26px] px-1.5 font-semibold tracking-wide ' +
|
||||
(active ? PILL_SEGMENT_ACTIVE : PILL_SEGMENT_INACTIVE)
|
||||
}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onSortDirectionToggle}
|
||||
className={`${TOOLBAR_BTN} ${TOOLBAR_BTN_INACTIVE} whitespace-nowrap`}
|
||||
title="Toggle sort direction"
|
||||
>
|
||||
{directionLabel}
|
||||
</button>
|
||||
<Listbox value={sortBy} onChange={onSortByChange}>
|
||||
<ListboxButton
|
||||
className={`${TOOLBAR_BTN} ${TOOLBAR_BTN_INACTIVE} 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-background border border-offbase shadow-lg 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-offbase 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-background border border-offbase hover:border-accent focus-within:ring-1 focus-within:ring-accent focus-within:border-accent transition-colors duration-200 ease-out w-[160px] md:w-[200px]">
|
||||
<SearchIcon className="w-3.5 h-3.5 text-muted 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-muted"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={onSortDirectionToggle}
|
||||
className={`${TOOLBAR_BTN} ${TOOLBAR_BTN_INACTIVE} whitespace-nowrap`}
|
||||
title="Toggle sort direction"
|
||||
onClick={onNewFolder}
|
||||
className={`${TOOLBAR_BTN} ${TOOLBAR_BTN_INACTIVE} gap-1 shrink-0`}
|
||||
title="New folder"
|
||||
>
|
||||
{directionLabel}
|
||||
<FolderPlusIcon className="w-4 h-4" />
|
||||
<span className="hidden md:inline">New Folder</span>
|
||||
</button>
|
||||
<Listbox value={sortBy} onChange={onSortByChange}>
|
||||
<ListboxButton
|
||||
className={`${TOOLBAR_BTN} ${TOOLBAR_BTN_INACTIVE} 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-background border border-offbase shadow-lg 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-offbase text-accent' : 'text-foreground'
|
||||
} ${selected ? 'font-semibold' : ''}`
|
||||
}
|
||||
>
|
||||
{opt.label}
|
||||
</ListboxOption>
|
||||
))}
|
||||
</ListboxOptions>
|
||||
</Listbox>
|
||||
|
||||
{rightSlot && (
|
||||
<div className="shrink-0 flex items-center gap-2 pl-1 sm:pl-2 sm:border-l sm:border-offbase ml-0.5">
|
||||
{rightSlot}
|
||||
</div>
|
||||
)}
|
||||
</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-background border border-offbase hover:border-accent focus-within:ring-1 focus-within:ring-accent focus-within:border-accent transition-colors duration-200 ease-out w-[160px] md:w-[200px]">
|
||||
<SearchIcon className="w-3.5 h-3.5 text-muted 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-muted"
|
||||
/>
|
||||
</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 && (
|
||||
<div className="shrink-0 flex items-center gap-2 pl-1 sm:pl-2 sm:border-l sm:border-offbase ml-0.5">
|
||||
{rightSlot}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ export function FinderWindow({
|
|||
leaveTo="-translate-x-full"
|
||||
>
|
||||
<DialogPanel
|
||||
className="w-[80vw] max-w-[280px] h-full bg-base border-r border-offbase shadow-xl"
|
||||
className="w-[80vw] max-w-[280px] h-full bg-base shadow-xl"
|
||||
onClick={() => onSidebarOpenChange(false)}
|
||||
>
|
||||
{sidebar}
|
||||
|
|
|
|||
Loading…
Reference in a new issue