"use client"; import { useMemo, useState } from "react"; import { useTranslations } from "next-intl"; import { Menu, ChevronRight, Search, X, ArrowUpDown, Check, LayoutGrid, LayoutList, PanelRight, Download, Trash2, Scissors, Copy, Pencil, Share2, Clipboard, MoreHorizontal, FolderOpen, Folder, ArrowUp, ArrowDown, RefreshCw, } from "@/components/icons"; import { Button } from "@/components/ui/button"; import { ContextMenu, ContextMenuItem, ContextMenuSeparator } from "@/components/ui/context-menu"; import { useContextMenu } from "@/hooks/use-context-menu"; import { cn } from "@/lib/utils"; import { anchorEvent, isInternalDrag, readDraggedNames } from "@/components/files/file-item-parts"; import { PluginActions } from "@/components/plugins/plugin-actions"; import type { FileResourceView } from "@/lib/plugin-types"; export type SortKey = "name" | "size" | "modified"; export type SortDir = "asc" | "desc"; export type ViewMode = "list" | "grid"; export interface Crumb { /** Folder id; null for the root. */ id: string | null; name: string; path: string; } interface SelectionActions { count: number; /** Files among the selection, for the download count. */ fileCount: number; canOpen: boolean; canRename: boolean; canShare: boolean; onOpen: () => void; onDownload: () => void; onShare: () => void; onRename: () => void; onCut: () => void; onCopy: () => void; onDelete: () => void; onClear: () => void; /** The selection as plugins see it (file-actions). */ pluginFiles: FileResourceView[]; /** The one selected item's type, so plugins can offer type-specific actions; undefined for several. */ pluginMimeType?: string; } interface FilesHeaderProps { crumbs: Crumb[]; /** Pro shell: a label between the root and the account's folders. */ accountLabel?: string | null; onNavigateCrumb: (crumb: Crumb) => void; onNavigateAccountRoot?: () => void; onDropOnCrumb: (names: string[], targetId: string | null) => void; onMenuClick?: () => void; menuOpen?: boolean; searchQuery: string; onSearchChange: (q: string) => void; searchInputRef: React.RefObject; sortKey: SortKey; sortDir: SortDir; onSortChange: (key: SortKey, dir: SortDir) => void; viewMode: ViewMode; onViewModeChange: (mode: ViewMode) => void; showDetails: boolean; onToggleDetails: () => void; clipboardCount: number; onPaste: () => void; onRefresh: () => void; isLoading: boolean; compact: boolean; /** Hide the tools that only apply to a folder listing (Pro account picker). */ toolsHidden?: boolean; selection: SelectionActions | null; } const MAX_VISIBLE_CRUMBS = 3; export function FilesHeader({ crumbs, accountLabel, onNavigateCrumb, onNavigateAccountRoot, onDropOnCrumb, onMenuClick, menuOpen, searchQuery, onSearchChange, searchInputRef, sortKey, sortDir, onSortChange, viewMode, onViewModeChange, showDetails, onToggleDetails, clipboardCount, onPaste, onRefresh, isLoading, compact, toolsHidden, selection, }: FilesHeaderProps) { const t = useTranslations("files"); const sortMenu = useContextMenu(); const overflowMenu = useContextMenu(); const [searchOpen, setSearchOpen] = useState(false); const [dropCrumb, setDropCrumb] = useState(null); const headerClass = "flex items-center gap-1 px-3 h-14 box-content border-b border-border min-w-0 flex-shrink-0"; if (selection) { return (
{t("selected_count", { count: selection.count })}
{selection.canOpen && ( )} {selection.fileCount > 0 && ( )} {selection.canShare && ( )} {selection.canRename && ( )} {!compact && }
{/* Outside the scrolling row, so the "more" button never scrolls out of sight. */} {compact && }
); } // Deep paths keep the root and the last folders; the rest go in a menu. const hidden = crumbs.length > MAX_VISIBLE_CRUMBS + 1 ? crumbs.slice(1, crumbs.length - MAX_VISIBLE_CRUMBS + 1) : []; const visible = hidden.length > 0 ? [crumbs[0], ...crumbs.slice(crumbs.length - MAX_VISIBLE_CRUMBS + 1)] : crumbs; const crumbDropProps = (crumb: Crumb, isLast: boolean) => isLast ? {} : { onDragOver: (e: React.DragEvent) => { if (!isInternalDrag(e)) return; e.preventDefault(); e.dataTransfer.dropEffect = "move"; setDropCrumb(crumb.path); }, onDragLeave: () => setDropCrumb((cur) => (cur === crumb.path ? null : cur)), onDrop: (e: React.DragEvent) => { e.preventDefault(); setDropCrumb(null); const names = readDraggedNames(e); if (names.length > 0) onDropOnCrumb(names, crumb.id); }, }; const renderCrumb = (crumb: Crumb, index: number) => { const isLast = crumb === crumbs[crumbs.length - 1]; return (
  • {index > 0 && }
  • ); }; const searchField = (
    onSearchChange(e.target.value)} onKeyDown={(e) => { if (e.key === "Escape") { onSearchChange(""); if (compact) setSearchOpen(false); (e.target as HTMLInputElement).blur(); } }} placeholder={t("search_placeholder")} autoFocus={compact} className="w-full h-8 rounded-md border border-input bg-background ps-8 pe-8 text-sm outline-none placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring [&::-webkit-search-cancel-button]:hidden" /> {(searchQuery || compact) && ( )}
    ); if (compact && searchOpen && !toolsHidden) { return (
    {searchField}
    ); } return (
    {onMenuClick && ( )} {!toolsHidden && (
    {clipboardCount > 0 && ( )} {compact ? ( ) : searchField}
    {([["list", LayoutList, t("list_view")], ["grid", LayoutGrid, t("grid_view")]] as const).map(([mode, Icon, label]) => ( ))}
    {!compact && ( )}
    )} {([["name", t("name")], ["modified", t("modified")], ["size", t("size")]] as const).map(([key, label]) => ( { onSortChange(key, sortKey === key ? sortDir : key === "name" ? "asc" : "desc"); sortMenu.closeContextMenu(); }} /> ))} { onSortChange(sortKey, "asc"); sortMenu.closeContextMenu(); }} /> { onSortChange(sortKey, "desc"); sortMenu.closeContextMenu(); }} /> {(overflowMenu.contextMenu.data ?? []).map((crumb) => ( { overflowMenu.closeContextMenu(); onNavigateCrumb(crumb); }} /> ))}
    ); } function ToolButton({ icon: Icon, label, onClick, compact, destructive }: { icon: React.ComponentType<{ className?: string }>; label: string; onClick: () => void; compact?: boolean; destructive?: boolean; }) { return ( ); } function DetailsToggle({ showDetails, onToggle, label }: { showDetails: boolean; onToggle: () => void; label: string }) { return ( ); } function SortItem({ label, checked, onClick, icon: Icon }: { label: string; checked: boolean; onClick: () => void; icon?: React.ComponentType<{ className?: string }>; }) { return ( ); } /** * Plugin buttons in the selection toolbar, icon-only like the app's own. On * phones the toolbar has no room for them: they go behind one "more" button. */ function SelectionPluginActions({ files, mimeType, overflow }: { files: FileResourceView[]; mimeType?: string; overflow?: boolean }) { const context = useMemo(() => ({ files }), [files]); return ; }