diff --git a/src/components/DocumentTabs.tsx b/src/components/DocumentTabs.tsx --- a/src/components/DocumentTabs.tsx +++ b/src/components/DocumentTabs.tsx @@ -81,7 +81,7 @@ const classes = useMemo(() => { const base = [ - "flex items-center gap-1.5 px-3 min-w-[120px] max-w-[200px] shrink-0 cursor-pointer border-r border-border-subtle select-none transition-all duration-150", + "group flex items-center gap-1.5 px-3 min-w-[120px] max-w-[200px] shrink-0 cursor-pointer border-r border-border-subtle select-none transition-all duration-150", ]; if (isActive) { @@ -106,7 +106,7 @@ () => ( @@ -263,17 +263,6 @@ )} - ); } diff --git a/src/components/SearchPanel.tsx b/src/components/SearchPanel.tsx --- a/src/components/SearchPanel.tsx +++ b/src/components/SearchPanel.tsx @@ -140,35 +140,33 @@ ); } -function SearchInput( +const SearchInput = ( { query, handleQueryChange, clearQuery }: { query: string; handleQueryChange: ChangeEventHandler; clearQuery: MouseEventHandler; }, -) { - return ( -
- - - {query && ( - - )} -
- ); -} +) => ( +
+ + + {query && ( + + )} +
+); function FilterLocation( { location, filters, handleToggleLocation }: { diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx deleted file mode 100644 --- a/src/components/Sidebar.tsx +++ /dev/null @@ -1,418 +0,0 @@ -import type { ChangeEventHandler, CSSProperties, MouseEventHandler } from "react"; -import { useCallback, useMemo, useState } from "react"; -import type { DocMeta, LocationDescriptor } from "../ports"; -import { - ChevronRightIcon, - FileTextIcon, - FolderIcon, - LibraryIcon, - MoreVerticalIcon, - PlusIcon, - SearchIcon, - TrashIcon, -} from "./icons"; - -export type SidebarProps = { - locations: LocationDescriptor[]; - selectedLocationId?: number; - selectedDocPath?: string; - documents: DocMeta[]; - isCollapsed?: boolean; - isLoading?: boolean; - onAddLocation: () => void; - onRemoveLocation: (locationId: number) => void; - onSelectLocation: (locationId: number) => void; - onSelectDocument: (locationId: number, path: string) => void; - filterText?: string; - onFilterChange?: (text: string) => void; -}; - -type TreeItemProps = { - icon: { Component: (props: { size: number }) => React.ReactNode; size: number }; - label: string; - isSelected?: boolean; - isExpanded?: boolean; - hasChildren?: boolean; - level?: number; - onClick?: () => void; - onToggle?: () => void; - Actions?: React.ComponentType; -}; - -function TreeItem( - { icon, label, isSelected = false, isExpanded = false, hasChildren = false, level = 0, onClick, onToggle, Actions }: - TreeItemProps, -) { - const paddingLeft = level * 16 + 12; - - const handleMouseEnter: MouseEventHandler = useCallback((e) => { - if (!isSelected) { - (e.currentTarget as HTMLDivElement).classList.add("bg-layer-hover-01"); - } - }, []); - - const handleMouseLeave: MouseEventHandler = useCallback((e) => { - if (!isSelected) { - (e.currentTarget as HTMLDivElement).classList.remove("bg-layer-hover-01"); - } - }, []); - - const handleButtonClick: MouseEventHandler = useCallback((e) => { - e.stopPropagation(); - onToggle?.(); - }, []); - - const containerStyle: CSSProperties = useMemo( - () => ({ paddingLeft: `${paddingLeft}px`, paddingRight: "8px", paddingTop: "6px", paddingBottom: "6px" }), - [paddingLeft], - ); - - const buttonStyle: CSSProperties = useMemo(() => ({ transform: isExpanded ? "rotate(90deg)" : "rotate(0deg)" }), [ - isExpanded, - ]); - - const labelStyle: CSSProperties = useMemo(() => ({ fontWeight: isSelected ? 500 : 400 }), [isSelected]); - - return ( -
- {hasChildren && ( - - )} - {!hasChildren && } - - - - - {label} - - {Actions && } -
- ); -} - -function DocumentItem( - { doc, isSelected, selectedDocPath, onSelectDocument, id }: { - doc: DocMeta; - isSelected: boolean; - selectedDocPath?: string; - onSelectDocument: (id: number, path: string) => void; - id: number; - }, -) { - const fileTextIcon = useMemo(() => ({ Component: FileTextIcon, size: 14 }), []); - const handleClick = useCallback(() => onSelectDocument(id, doc.rel_path), [id, onSelectDocument]); - return ( - - ); -} - -type SidebarLocationItemProps = { - location: LocationDescriptor; - isSelected: boolean; - selectedDocPath?: string; - isExpanded: boolean; - onSelect: (id: number) => void; - onToggle: (id: number) => void; - onRemove: (id: number) => void; - onSelectDocument: (id: number, path: string) => void; - setShowLocationMenu: (id: number | null) => void; - documents: DocMeta[]; - filterText: string; - isMenuOpen: boolean; -}; - -function SidebarLocationItem( - { - location, - isSelected, - selectedDocPath, - isExpanded, - onSelect, - onToggle, - onRemove, - onSelectDocument, - setShowLocationMenu, - documents, - filterText, - isMenuOpen, - }: SidebarLocationItemProps, -) { - const handleRemoveClick = useCallback(() => { - onRemove(location.id); - setShowLocationMenu(null); - }, [location.id, setShowLocationMenu]); - - const handleMenuClick = useCallback(() => { - setShowLocationMenu(location.id); - }, [location.id, setShowLocationMenu]); - - const handleMouseEnter: MouseEventHandler = useCallback((e) => { - (e.currentTarget as HTMLButtonElement).classList.add("bg-support-error", "text-white"); - (e.currentTarget as HTMLButtonElement).classList.remove("text-support-error"); - }, []); - - const handleMouseLeave: MouseEventHandler = useCallback((e) => { - (e.currentTarget as HTMLButtonElement).classList.remove("bg-support-error", "text-white"); - (e.currentTarget as HTMLButtonElement).classList.add("text-support-error"); - }, []); - - const RemoveButton = useCallback( - () => - isMenuOpen - ? ( -
- -
- ) - : null, - [isMenuOpen, handleRemoveClick, handleMouseEnter, handleMouseLeave], - ); - - const LocationActions = useCallback(() => ( -
- - -
- ), [setShowLocationMenu, onRemove]); - - const onItemClick = useCallback(() => { - onSelect(location.id); - }, [location.id, onSelect]); - - const onToggleClick = useCallback(() => { - onToggle(location.id); - }, [location.id, onToggle]); - - const folderIcon = useMemo(() => ({ Component: FolderIcon, size: 16 }), []); - - return ( -
-
- - -
- - {isExpanded && isSelected && ( -
- {documents.length === 0 - ? ( -
- {filterText ? "No matching documents" : "No documents found"} -
- ) - : (documents.map((doc) => ( - - )))} -
- )} -
- ); -} - -export function Sidebar( - { - locations, - selectedLocationId, - selectedDocPath, - documents, - isCollapsed = false, - isLoading = false, - onAddLocation, - onRemoveLocation, - onSelectLocation, - onSelectDocument, - filterText = "", - onFilterChange, - }: SidebarProps, -) { - const [expandedLocations, setExpandedLocations] = useState>(() => new Set(locations.map((l) => l.id))); - const [showLocationMenu, setShowLocationMenu] = useState(null); - - const toggleLocation = useCallback((locationId: number) => { - setExpandedLocations((prev) => { - const next = new Set(prev); - if (next.has(locationId)) { - next.delete(locationId); - } else { - next.add(locationId); - } - return next; - }); - }, []); - - const filteredDocuments = useMemo( - () => - filterText - ? documents.filter((doc) => - doc.title.toLowerCase().includes(filterText.toLowerCase()) - || doc.rel_path.toLowerCase().includes(filterText.toLowerCase()) - ) - : documents, - [documents, filterText], - ); - - const handleMouseEnter: MouseEventHandler = useCallback((e) => { - (e.currentTarget as HTMLButtonElement).classList.add("bg-layer-hover-01", "text-icon-primary"); - }, []); - - const handleMouseLeave: MouseEventHandler = useCallback((e) => { - (e.currentTarget as HTMLButtonElement).classList.remove("bg-layer-hover-01", "text-icon-primary"); - }, []); - - const handleInputChange: ChangeEventHandler = useCallback((e) => { - onFilterChange?.(e.currentTarget.value); - }, []); - - const Title = useCallback( - () => ( -

- Library - {isLoading && (loading...)} -

- ), - [isLoading], - ); - - const AddButton = useCallback( - () => ( - - ), - [onAddLocation, handleMouseEnter, handleMouseLeave], - ); - - const SearchInput = useCallback(() => ( -
-
- - -
-
- ), [filterText, handleInputChange]); - - const EmptyLocations = useCallback( - () => ( -
- -

No locations added

- -
- ), - [onAddLocation], - ); - - if (isCollapsed) { - return ( - - ); - } - - return ( -