import { rem } from "@/lib/rem"; import { useState } from "react"; import { useTranslation } from "react-i18next"; import { ChevronRight, Folder, FolderOpen, Pencil, Trash2, X, } from "lucide-react"; import type { CredentialFolder } from "../visible-rows"; const UNCATEGORIZED = "Uncategorized"; export function CredentialFolderItem({ folder, isOpen, stripeIndex, query = "", editingFolderName, editingFolderValue, onToggleFolder, onEditingFolderNameChange, onEditingFolderValueChange, onRenameFolder, onDeleteFolder, depth = 0, arrangeMode = false, draggedCredentialId, onDropCredential, }: { folder: CredentialFolder; isOpen: boolean; stripeIndex?: number; query?: string; editingFolderName: string | null; editingFolderValue: string; onToggleFolder: (name: string) => void; onEditingFolderNameChange: (name: string | null) => void; onEditingFolderValueChange: (value: string) => void; onRenameFolder: (folder: string, newName: string) => Promise; onDeleteFolder?: (folder: string) => void; depth?: number; /** When true (rearranging unlocked), the header accepts dropped * credentials. Credential folders themselves have no stored order, so * they are never draggable. */ arrangeMode?: boolean; /** Id of the credential currently being dragged (outside manual sort * mode), if any -- drives the drop-target highlight below. */ draggedCredentialId?: string | null; /** Fires when a dragged credential is dropped on this folder header to * reassign it, outside manual sort mode. */ onDropCredential?: (credentialId: string) => void; }) { const { t } = useTranslation(); const [dragOver, setDragOver] = useState(false); const isRenaming = editingFolderName === folder.name; const canManage = folder.name !== UNCATEGORIZED; const actionButtonClass = "flex items-center justify-center size-6 text-muted-foreground/60 hover:text-foreground hover:bg-background/80 transition-colors"; const acceptsCredentialDrop = arrangeMode && !!draggedCredentialId; return (
0 ? { paddingLeft: rem(depth * 12) } : undefined} >
{ if (!acceptsCredentialDrop) return; e.preventDefault(); e.stopPropagation(); setDragOver(true); }} onDragLeave={(e) => { if (e.currentTarget === e.target) setDragOver(false); }} onDrop={(e) => { if (!acceptsCredentialDrop || !draggedCredentialId) return; e.preventDefault(); e.stopPropagation(); setDragOver(false); onDropCredential?.(draggedCredentialId); }} onClick={() => !query && !isRenaming && onToggleFolder(folder.name)} className={`group/folder flex items-center gap-2 w-full pl-2.5 pr-2 py-1.5 transition-colors text-left cursor-pointer ${ isOpen ? "bg-muted/40" : "hover:bg-muted/30" } ${(stripeIndex ?? 0) % 2 === 1 && !isOpen ? "bg-muted/[0.08]" : ""} ${dragOver ? "ring-1 ring-inset ring-accent-brand bg-accent-brand/10" : ""}`} > {isOpen ? ( ) : ( )} {isRenaming ? ( <> onEditingFolderValueChange(e.target.value)} onBlur={async () => { const newName = editingFolderValue.trim(); onEditingFolderNameChange(null); if (newName && newName !== folder.name) { await onRenameFolder(folder.name, newName); } }} onKeyDown={(e) => { if (e.key === "Enter") e.currentTarget.blur(); if (e.key === "Escape") onEditingFolderNameChange(null); }} onClick={(e) => e.stopPropagation()} className="text-[13px] font-bold bg-background border border-accent-brand/60 px-1 outline-none text-foreground min-w-0 flex-1" /> ) : ( <> {folder.name} {folder.children.length} {canManage && ( {onDeleteFolder && ( )} )} )}
); }