fix(documents-ui): a11y, mobile, realtime lift, type-safety, UI polish
- A2: lift useRealtimeInvalidation to DocumentsHub (covers all 3 render modes) - B1-B4: aria-label, aria-pressed, aria-expanded, Lock SVG aria-hidden - C1-C7: Sheet wrap for mobile sidebar, border axis fix, 44x44 tap targets - Mobile Important: useMobileChrome title, FolderActionsMenu icon size, breadcrumb tap targets, signer email truncate - Type-safety: ENTITY_TYPES guard, AggregatedSection discriminated union - UI/UX: em-dash to colon in SigningDetailsDialog, Loading state normalize, StatusPill on HubRootView, view signing details as Button Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { ChevronRight, Folder, FolderOpen, Inbox, Lock } from 'lucide-react';
|
||||
import { ChevronRight, Folder, FolderOpen, FolderTree, Inbox, Lock } from 'lucide-react';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useDocumentFolders, type FolderNode } from '@/hooks/use-document-folders';
|
||||
|
||||
@@ -24,15 +25,73 @@ interface FolderTreeSidebarProps {
|
||||
*
|
||||
* Designed for unlimited depth — only the top level renders by default
|
||||
* so deep trees don't blow out the page; reps drill in by expanding.
|
||||
*
|
||||
* On mobile (< sm) the sidebar collapses into a Sheet drawer triggered by
|
||||
* a "Show folders" button so the main listing isn't pushed below a
|
||||
* full-width folder stack.
|
||||
*/
|
||||
export function FolderTreeSidebar({ selectedFolderId, onSelect, footer }: FolderTreeSidebarProps) {
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
|
||||
const handleMobileSelect = (id: string | null | undefined) => {
|
||||
onSelect(id);
|
||||
setMobileOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Mobile-only trigger that opens the drawer; hidden at sm+. */}
|
||||
<div className="sm:hidden px-3 pt-3">
|
||||
<Sheet open={mobileOpen} onOpenChange={setMobileOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="min-h-[44px]">
|
||||
<FolderTree className="mr-2 h-4 w-4" />
|
||||
Show folders
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="left" className="w-3/4 max-w-xs p-0">
|
||||
<SheetHeader className="border-b px-3 py-3">
|
||||
<SheetTitle className="text-sm">Folders</SheetTitle>
|
||||
</SheetHeader>
|
||||
<div className="p-2 overflow-y-auto">
|
||||
<TreeBody
|
||||
selectedFolderId={selectedFolderId}
|
||||
onSelect={handleMobileSelect}
|
||||
footer={footer}
|
||||
/>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
|
||||
{/* Desktop sidebar: hidden on mobile (the Sheet trigger replaces it). */}
|
||||
<aside className="hidden sm:block w-60 shrink-0 border-b sm:border-b-0 sm:border-r bg-muted/40 p-2">
|
||||
<div className="mb-2 px-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||
Folders
|
||||
</div>
|
||||
<TreeBody
|
||||
selectedFolderId={selectedFolderId}
|
||||
onSelect={onSelect}
|
||||
footer={footer}
|
||||
/>
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function TreeBody({
|
||||
selectedFolderId,
|
||||
onSelect,
|
||||
footer,
|
||||
}: {
|
||||
selectedFolderId: string | null | undefined;
|
||||
onSelect: (folderId: string | null | undefined) => void;
|
||||
footer?: React.ReactNode;
|
||||
}) {
|
||||
const { data: tree = [], isLoading, isError } = useDocumentFolders();
|
||||
|
||||
return (
|
||||
<aside className="w-full sm:w-60 shrink-0 border-r bg-muted/40 p-2">
|
||||
<div className="mb-2 px-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||
Folders
|
||||
</div>
|
||||
<>
|
||||
<div className="space-y-0.5">
|
||||
<PseudoRow
|
||||
label="All documents"
|
||||
@@ -49,7 +108,7 @@ export function FolderTreeSidebar({ selectedFolderId, onSelect, footer }: Folder
|
||||
</div>
|
||||
<div className="mt-3 space-y-0.5">
|
||||
{isLoading ? (
|
||||
<p className="px-2 text-xs text-muted-foreground">Loading…</p>
|
||||
<p className="px-2 text-xs text-muted-foreground">Loading...</p>
|
||||
) : isError ? (
|
||||
<p className="px-2 text-xs text-destructive">Failed to load folders.</p>
|
||||
) : tree.length === 0 ? (
|
||||
@@ -67,7 +126,7 @@ export function FolderTreeSidebar({ selectedFolderId, onSelect, footer }: Folder
|
||||
)}
|
||||
</div>
|
||||
{footer ? <div className="mt-4 border-t pt-3">{footer}</div> : null}
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -86,7 +145,10 @@ function PseudoRow({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={cn('w-full justify-start font-normal', active && 'bg-accent text-foreground')}
|
||||
className={cn(
|
||||
'w-full min-h-[44px] justify-start font-normal',
|
||||
active && 'bg-accent text-foreground',
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Icon className="mr-2 h-4 w-4" />
|
||||
@@ -121,12 +183,13 @@ function FolderRow({
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={open ? 'Collapse' : 'Expand'}
|
||||
aria-label={`${open ? 'Collapse' : 'Expand'} ${node.name}`}
|
||||
aria-expanded={hasChildren ? open : undefined}
|
||||
aria-hidden={!hasChildren}
|
||||
tabIndex={hasChildren ? 0 : -1}
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
className={cn(
|
||||
'flex h-5 w-5 items-center justify-center text-muted-foreground hover:text-foreground',
|
||||
'flex min-h-[44px] min-w-[44px] items-center justify-center text-muted-foreground hover:text-foreground',
|
||||
!hasChildren && 'invisible',
|
||||
)}
|
||||
>
|
||||
@@ -136,7 +199,7 @@ function FolderRow({
|
||||
type="button"
|
||||
onClick={() => onSelect(node.id)}
|
||||
className={cn(
|
||||
'flex flex-1 items-center gap-1.5 truncate text-left',
|
||||
'flex min-h-[44px] flex-1 items-center gap-1.5 truncate py-2 text-left',
|
||||
node.archivedAt != null && 'text-muted-foreground',
|
||||
)}
|
||||
>
|
||||
@@ -145,12 +208,12 @@ function FolderRow({
|
||||
) : (
|
||||
<Folder className="h-4 w-4 shrink-0" />
|
||||
)}
|
||||
<span className="truncate">{node.name}</span>
|
||||
<span className="truncate">
|
||||
{node.name}
|
||||
{node.systemManaged ? <span className="sr-only"> (system folder)</span> : null}
|
||||
</span>
|
||||
{node.systemManaged ? (
|
||||
<Lock
|
||||
className="ml-1 h-3 w-3 shrink-0 text-muted-foreground"
|
||||
aria-label="System folder"
|
||||
/>
|
||||
<Lock className="ml-1 h-3 w-3 shrink-0 text-muted-foreground" aria-hidden="true" />
|
||||
) : null}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user