feat(documents): enhance DocumentNode and FolderNode components with mobile responsiveness and improved checkbox interactions

This commit is contained in:
Anish Sarkar 2026-07-14 03:25:22 +05:30
parent 6d0e151b5f
commit 8220bfda60
2 changed files with 115 additions and 91 deletions

View file

@ -37,6 +37,7 @@ import {
import { Spinner } from "@/components/ui/spinner"; import { Spinner } from "@/components/ui/spinner";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import type { DocumentTypeEnum } from "@/contracts/types/document.types"; import type { DocumentTypeEnum } from "@/contracts/types/document.types";
import { useIsMobile } from "@/hooks/use-mobile";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { SidebarListItem } from "../layout/ui/sidebar/SidebarListItem"; import { SidebarListItem } from "../layout/ui/sidebar/SidebarListItem";
import { DND_TYPES } from "./FolderNode"; import { DND_TYPES } from "./FolderNode";
@ -92,6 +93,7 @@ export const DocumentNode = React.memo(function DocumentNode({
const isMemoryDocument = const isMemoryDocument =
doc.document_type === "USER_MEMORY" || doc.document_type === "TEAM_MEMORY"; doc.document_type === "USER_MEMORY" || doc.document_type === "TEAM_MEMORY";
const isSelectable = canMention && !isUnavailable; const isSelectable = canMention && !isUnavailable;
const isMobile = useIsMobile();
const handleCheckChange = useCallback(() => { const handleCheckChange = useCallback(() => {
if (isSelectable) { if (isSelectable) {
@ -164,9 +166,9 @@ export const DocumentNode = React.memo(function DocumentNode({
<ContextMenuTrigger asChild> <ContextMenuTrigger asChild>
<SidebarListItem <SidebarListItem
ref={attachRef} ref={attachRef}
active={isMentioned} active={isMentioned || dropdownOpen}
dragging={isDragging} dragging={isDragging}
className="gap-2.5 px-1" className="group/item relative gap-2.5 px-1"
style={{ paddingLeft: `${depth * 16 + 4}px` }} style={{ paddingLeft: `${depth * 16 + 4}px` }}
role="button" role="button"
tabIndex={isUnavailable ? -1 : 0} tabIndex={isUnavailable ? -1 : 0}
@ -214,32 +216,36 @@ export const DocumentNode = React.memo(function DocumentNode({
); );
} }
return ( return (
<> <span className="relative flex h-3.5 w-3.5 shrink-0 items-center justify-center">
{isMemoryDocument ? ( <span
<span aria-disabled="true" className="h-3.5 w-3.5 shrink-0 cursor-default"> className={cn(
<Checkbox "absolute inset-0 flex items-center justify-center transition-opacity",
checked={false} canMention &&
disabled (isMentioned
aria-disabled ? "opacity-0"
className="h-3.5 w-3.5 pointer-events-none" : "max-sm:opacity-0 group-hover/item:opacity-0")
/> )}
</span> >
) : canMention ? ( {getDocumentTypeIcon(
doc.document_type as DocumentTypeEnum,
"h-3.5 w-3.5 text-muted-foreground"
)}
</span>
{canMention ? (
<Checkbox <Checkbox
checked={isMentioned} checked={isMentioned}
aria-label={`Select ${doc.title}`}
onCheckedChange={handleCheckChange} onCheckedChange={handleCheckChange}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
className="h-3.5 w-3.5 shrink-0" className={cn(
/> "absolute h-3.5 w-3.5 transition-opacity max-sm:pointer-events-auto max-sm:opacity-100",
) : ( isMentioned
<span className="flex h-3.5 w-3.5 shrink-0 items-center justify-center"> ? "opacity-100"
{getDocumentTypeIcon( : "pointer-events-none opacity-0 group-hover/item:pointer-events-auto group-hover/item:opacity-100 focus-visible:pointer-events-auto focus-visible:opacity-100"
doc.document_type as DocumentTypeEnum,
"h-3.5 w-3.5 text-muted-foreground"
)} )}
</span> />
)} ) : null}
</> </span>
); );
})()} })()}
@ -260,36 +266,30 @@ export const DocumentNode = React.memo(function DocumentNode({
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
<span className="relative shrink-0 flex items-center justify-center h-6 w-6"> <div
{getDocumentTypeIcon( className={cn(
doc.document_type as DocumentTypeEnum, "pointer-events-none absolute top-0 right-0 bottom-0 flex items-center rounded-r-md pr-1 pl-6",
"h-3.5 w-3.5 text-muted-foreground" isMentioned || dropdownOpen
) && ( ? "bg-gradient-to-l from-accent from-60% to-transparent"
<span : "bg-gradient-to-l from-sidebar from-60% to-transparent group-hover/item:from-accent",
className={cn( isMobile
"absolute inset-0 flex items-center justify-center transition-opacity pointer-events-none", ? "opacity-0"
dropdownOpen ? "opacity-0" : "group-hover:opacity-0" : isMentioned || dropdownOpen
)} ? "opacity-100"
> : "opacity-0 group-hover/item:opacity-100"
{getDocumentTypeIcon(
doc.document_type as DocumentTypeEnum,
"h-3.5 w-3.5 text-muted-foreground"
)}
</span>
)} )}
>
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}> <DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className={cn( className={cn(
"hidden sm:inline-flex h-6 w-6 shrink-0 hover:bg-transparent", "pointer-events-auto hidden h-6 w-6 shrink-0 hover:bg-transparent sm:inline-flex",
dropdownOpen dropdownOpen && "bg-accent hover:bg-accent"
? "opacity-100 bg-accent hover:bg-accent"
: "opacity-0 group-hover:opacity-100"
)} )}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
aria-label={`Document actions for ${doc.title}`}
> >
<MoreHorizontal className="h-3.5 w-3.5 text-muted-foreground" /> <MoreHorizontal className="h-3.5 w-3.5 text-muted-foreground" />
</Button> </Button>
@ -341,7 +341,7 @@ export const DocumentNode = React.memo(function DocumentNode({
)} )}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</span> </div>
</SidebarListItem> </SidebarListItem>
</ContextMenuTrigger> </ContextMenuTrigger>

View file

@ -34,6 +34,7 @@ import {
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { Spinner } from "@/components/ui/spinner"; import { Spinner } from "@/components/ui/spinner";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useIsMobile } from "@/hooks/use-mobile";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { FolderSelectionState } from "./FolderTreeView"; import type { FolderSelectionState } from "./FolderTreeView";
@ -124,6 +125,7 @@ export const FolderNode = React.memo(function FolderNode({
onStopWatching, onStopWatching,
onExportFolder, onExportFolder,
}: FolderNodeProps) { }: FolderNodeProps) {
const isMobile = useIsMobile();
const [renameValue, setRenameValue] = useState(folder.name); const [renameValue, setRenameValue] = useState(folder.name);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const rowRef = useRef<HTMLDivElement>(null); const rowRef = useRef<HTMLDivElement>(null);
@ -261,8 +263,9 @@ export const FolderNode = React.memo(function FolderNode({
tabIndex={0} tabIndex={0}
dragging={isDragging} dragging={isDragging}
className={cn( className={cn(
"relative gap-1 px-1", "group/item relative gap-1 px-1",
isExpanded && "font-medium", isExpanded && "font-medium",
dropdownOpen && "bg-accent text-accent-foreground",
isOver && canDrop && dropZone === "middle" && "bg-accent ring-1 ring-primary/40", isOver && canDrop && dropZone === "middle" && "bg-accent ring-1 ring-primary/40",
isOver && canDrop && dropZone === "top" && "border-t-2 border-primary", isOver && canDrop && dropZone === "top" && "border-t-2 border-primary",
isOver && canDrop && dropZone === "bottom" && "border-b-2 border-primary", isOver && canDrop && dropZone === "bottom" && "border-b-2 border-primary",
@ -291,11 +294,11 @@ export const FolderNode = React.memo(function FolderNode({
)} )}
</span> </span>
{processingState !== "idle" && selectionState === "none" ? ( <span className="relative flex h-4 w-4 shrink-0 items-center justify-center">
<> {processingState !== "idle" && selectionState === "none" ? (
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<span className="flex h-3.5 w-3.5 shrink-0 items-center justify-center group-hover:hidden"> <span className="flex h-3.5 w-3.5 items-center justify-center">
{processingState === "processing" ? ( {processingState === "processing" ? (
<Spinner size="xs" className="text-primary" /> <Spinner size="xs" className="text-primary" />
) : ( ) : (
@ -309,29 +312,37 @@ export const FolderNode = React.memo(function FolderNode({
: "Some files failed to process"} : "Some files failed to process"}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
<Checkbox ) : (
checked={false} <>
onCheckedChange={handleCheckChange} <FolderIcon
onClick={(e) => e.stopPropagation()} className={cn(
className="h-3.5 w-3.5 shrink-0 hidden group-hover:flex" "absolute h-4 w-4 text-muted-foreground transition-opacity",
/> selectionState === "none"
</> ? "max-sm:opacity-0 group-hover/item:opacity-0"
) : ( : "opacity-0"
<Checkbox )}
checked={ />
selectionState === "all" <Checkbox
? true checked={
: selectionState === "some" selectionState === "all"
? "indeterminate" ? true
: false : selectionState === "some"
} ? "indeterminate"
onCheckedChange={handleCheckChange} : false
onClick={(e) => e.stopPropagation()} }
className="h-3.5 w-3.5 shrink-0" aria-label={`Select ${folder.name}`}
/> onCheckedChange={handleCheckChange}
)} onClick={(e) => e.stopPropagation()}
className={cn(
<FolderIcon className="h-4 w-4 shrink-0 text-muted-foreground" /> "absolute h-3.5 w-3.5 transition-opacity max-sm:pointer-events-auto max-sm:opacity-100",
selectionState === "none"
? "pointer-events-none opacity-0 group-hover/item:pointer-events-auto group-hover/item:opacity-100 focus-visible:pointer-events-auto focus-visible:opacity-100"
: "opacity-100"
)}
/>
</>
)}
</span>
{isRenaming ? ( {isRenaming ? (
<input <input
@ -350,23 +361,35 @@ export const FolderNode = React.memo(function FolderNode({
)} )}
{!isRenaming && ( {!isRenaming && (
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}> <div
<DropdownMenuTrigger asChild> className={cn(
<Button "pointer-events-none absolute top-0 right-0 bottom-0 flex items-center rounded-r-md pr-1 pl-6",
variant="ghost" dropdownOpen
size="icon" ? "bg-gradient-to-l from-accent from-60% to-transparent"
className={cn( : "bg-gradient-to-l from-sidebar from-60% to-transparent group-hover/item:from-accent",
"hidden sm:inline-flex h-6 w-6 shrink-0 hover:bg-transparent transition-opacity", isMobile
dropdownOpen ? "opacity-0"
? "opacity-100 bg-accent hover:bg-accent" : dropdownOpen
: "opacity-0 group-hover:opacity-100" ? "opacity-100"
)} : "opacity-0 group-hover/item:opacity-100"
onClick={(e) => e.stopPropagation()} )}
> >
<MoreHorizontal className="h-3.5 w-3.5 text-muted-foreground" /> <DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
</Button> <DropdownMenuTrigger asChild>
</DropdownMenuTrigger> <Button
<DropdownMenuContent align="end" className="w-40"> variant="ghost"
size="icon"
className={cn(
"pointer-events-auto hidden h-6 w-6 shrink-0 hover:bg-transparent transition-opacity sm:inline-flex",
dropdownOpen && "bg-accent hover:bg-accent"
)}
onClick={(e) => e.stopPropagation()}
aria-label={`Folder actions for ${folder.name}`}
>
<MoreHorizontal className="h-3.5 w-3.5 text-muted-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40">
{isWatched && onRescan && ( {isWatched && onRescan && (
<DropdownMenuItem <DropdownMenuItem
onClick={(e) => { onClick={(e) => {
@ -436,8 +459,9 @@ export const FolderNode = React.memo(function FolderNode({
<Trash2 className="mr-2 h-4 w-4" /> <Trash2 className="mr-2 h-4 w-4" />
Delete Delete
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div>
)} )}
</SidebarListItem> </SidebarListItem>
</ContextMenuTrigger> </ContextMenuTrigger>