"use client"; import { SymlinkIndicator } from "@/components/shared/symlink-indicator"; import { IconArrowBackUp, IconPlus, IconMinus, IconCheck, IconLoader2, IconPencil, } from "@tabler/icons-react"; import { Button } from "@kandev/ui/button"; import { cn } from "@/lib/utils"; import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip"; import { LineStat } from "@/components/diff-stat"; import { FileStatusIcon } from "@/components/shared/file-status-icon"; import { FileIcon } from "@/components/ui/file-icon"; import { getFileCategory } from "@/lib/utils/file-types"; import type { ChangedFile } from "./changes-panel-helpers"; import type { OpenDiffOptions } from "./changes-diff-target"; import { useTranslation } from "react-i18next"; import { useResponsiveBreakpoint } from "@/hooks/use-responsive-breakpoint"; const COARSE_POINTER_TARGET_CLASS = "min-h-11 min-w-11"; const splitPath = (path: string) => { const lastSlash = path.lastIndexOf("/"); if (lastSlash === -1) return { folder: "", file: path }; return { folder: path.slice(0, lastSlash), file: path.slice(lastSlash + 1), }; }; type FileRowProps = { file: ChangedFile; isPending: boolean; isSelected?: boolean; /** True when this file's diff/editor tab is the currently active dockview panel. */ isActive?: boolean; onSelect?: (path: string, e: React.MouseEvent) => boolean; onOpenDiff: (path: string, options?: OpenDiffOptions) => void; // Multi-repo: handlers receive the file's repository_name so the per-file // staging op routes to the right git repo. Same-named files (e.g. README.md // in two repos) cannot be disambiguated by path alone. onStage: (path: string, repo?: string) => void; onUnstage: (path: string, repo?: string) => void; onDiscard: (path: string, repo?: string, anchor?: HTMLElement) => void; onEditFile: (path: string, repo?: string) => void; /** * Tree mode: skip the folder prefix, swap the left-side stage button for a * filetype icon, and surface the stage action only on row hover (VS Code- * style). The Unstaged / Staged section split keeps the staged state * visually obvious even without the always-on left chip. */ treeMode?: boolean; /** Tree mode: left padding in pixels driven by depth. */ indentPx?: number; }; export function FileRow({ file, isPending, isSelected, isActive, onSelect, onOpenDiff, onStage, onUnstage, onDiscard, onEditFile, treeMode, indentPx, }: FileRowProps) { const { isFinePointer } = useResponsiveBreakpoint(); const { folder, file: name } = splitPath(file.path); const showFolder = !treeMode && folder; const handleClick = (e: React.MouseEvent) => { if (e.button === 2) return; const consumed = onSelect?.(file.path, e); if (!consumed) { if (getFileCategory(file.path) === "image") { onEditFile(file.path, file.repositoryName); return; } onOpenDiff(file.path, { source: "uncommitted", repositoryName: file.repositoryName, ...(file.changeLayer ? { changeLayer: file.changeLayer } : {}), }); } }; return (
  • {treeMode ? ( ) : ( )}
  • ); } function FileRowStats({ file, isFinePointer }: { file: ChangedFile; isFinePointer: boolean }) { return (
    ); } function TreeModeFileActionSlot({ name, isPending, isFinePointer, staged, path, repo, onStage, onUnstage, }: { name: string; isPending: boolean; isFinePointer: boolean; staged: boolean; path: string; repo?: string; onStage: (path: string, repo?: string) => void; onUnstage: (path: string, repo?: string) => void; }) { return (
    *]:col-start-1 [&>*]:row-start-1", isFinePointer ? "size-4" : "size-11", )} > {isFinePointer && ( )}
    ); } function StageButton({ isPending, staged, touchSized, path, repo, onStage, onUnstage, }: { isPending: boolean; staged: boolean; touchSized?: boolean; path: string; repo?: string; onStage: (path: string, repo?: string) => void; onUnstage: (path: string, repo?: string) => void; }) { const { t } = useTranslation(); if (isPending) { return (
    ); } if (staged) { return ( ); } return ( ); } function FileRowActions({ path, repo, isFinePointer, onDiscard, onEditFile, }: { path: string; repo?: string; isFinePointer: boolean; onDiscard: (path: string, repo?: string, anchor?: HTMLElement) => void; onEditFile: (path: string, repo?: string) => void; }) { const { t } = useTranslation(); return (
    {t("task:discardChanges2")} {t("common:edit")}
    ); } // --- Bulk action components (used by FileListSection) --- export function DefaultActionButtons({ actionLabel, isActionLoading, onAction, secondaryActionLabel, isSecondaryActionLoading, onSecondaryAction, }: { actionLabel: string; isActionLoading?: boolean; onAction: () => void; secondaryActionLabel?: string; isSecondaryActionLoading?: boolean; onSecondaryAction?: () => void; }) { return ( <> {onSecondaryAction && secondaryActionLabel && ( )} ); } export function BulkActionBar({ variant, selectionCount, selectedPaths, onBulkStage, onBulkUnstage, onBulkDiscard, }: { variant: "unstaged" | "staged"; selectionCount: number; selectedPaths: Set; onBulkStage?: (paths: string[]) => void; onBulkUnstage?: (paths: string[]) => void; onBulkDiscard?: (paths: string[], anchor?: HTMLElement) => void; }) { const { t } = useTranslation(); const paths = [...selectedPaths]; return (
    {selectionCount} selected {variant === "unstaged" && onBulkStage && ( )} {variant === "staged" && onBulkUnstage && ( )} {onBulkDiscard && ( )}
    ); }