Files
codeg/src/components/layout/folder-title-bar.tsx

454 lines
15 KiB
TypeScript

"use client"
import {
useCallback,
useEffect,
useRef,
useState,
type KeyboardEvent as ReactKeyboardEvent,
} from "react"
import {
Columns2,
EllipsisVertical,
FileCode2,
Menu,
MessageSquare,
PanelLeft,
PanelRight,
Search,
Settings,
SquareTerminal,
} from "lucide-react"
import { useTranslations } from "next-intl"
import { openSettingsWindow } from "@/lib/api"
import { useAppWorkspace } from "@/contexts/app-workspace-context"
import { useActiveFolder } from "@/contexts/active-folder-context"
import { isDesktop, openFileDialog } from "@/lib/platform"
import { Button } from "@/components/ui/button"
import { useSidebarContext } from "@/contexts/sidebar-context"
import { useAuxPanelContext } from "@/contexts/aux-panel-context"
import { useTerminalContext } from "@/contexts/terminal-context"
import { useTabContext } from "@/contexts/tab-context"
import { useWorkspaceContext } from "@/contexts/workspace-context"
import { useIsMac } from "@/hooks/use-is-mac"
import { useShortcutSettings } from "@/hooks/use-shortcut-settings"
import {
formatShortcutLabel,
matchShortcutEvent,
} from "@/lib/keyboard-shortcuts"
import { AppTitleBar } from "./app-title-bar"
import { BranchDropdown } from "./branch-dropdown"
import { CommandDropdown } from "./command-dropdown"
import { NewFolderDropdown } from "./new-folder-dropdown"
import { SearchCommandDialog } from "@/components/conversations/search-command-dialog"
import { DirectoryBrowserDialog } from "@/components/shared/directory-browser-dialog"
import { cn } from "@/lib/utils"
import { useIsMobile } from "@/hooks/use-mobile"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
const MODE_TABS = [
{
mode: "conversation",
titleKey: "conversation",
icon: MessageSquare,
},
{
mode: "fusion",
titleKey: "fusion",
icon: Columns2,
},
{
mode: "files",
titleKey: "files",
icon: FileCode2,
},
] as const
export function FolderTitleBar() {
const tModes = useTranslations("Folder.modes")
const tTitleBar = useTranslations("Folder.folderTitleBar")
const { openFolder } = useAppWorkspace()
const { activeFolder } = useActiveFolder()
const { isOpen, toggle } = useSidebarContext()
const { isOpen: auxPanelOpen, toggle: toggleAuxPanel } = useAuxPanelContext()
const { isOpen: terminalOpen, toggle: toggleTerminal } = useTerminalContext()
const { openNewConversationTab } = useTabContext()
const { mode, setMode } = useWorkspaceContext()
const isMac = useIsMac()
const { shortcuts } = useShortcutSettings()
const [searchOpen, setSearchOpen] = useState(false)
const [browserOpen, setBrowserOpen] = useState(false)
const handleOpenFolder = useCallback(async () => {
if (isDesktop()) {
try {
const result = await openFileDialog({
directory: true,
multiple: false,
})
if (!result) return
const selected = Array.isArray(result) ? result[0] : result
await openFolder(selected)
} catch (err) {
console.error("[FolderTitleBar] failed to open folder:", err)
}
} else {
setBrowserOpen(true)
}
}, [openFolder])
const handleOpenSettings = useCallback(() => {
openSettingsWindow().catch((err) => {
console.error("[FolderTitleBar] failed to open settings:", err)
})
}, [])
useEffect(() => {
function handleKeyDown(e: KeyboardEvent) {
if (matchShortcutEvent(e, shortcuts.toggle_search)) {
e.preventDefault()
setSearchOpen((prev) => !prev)
return
}
if (matchShortcutEvent(e, shortcuts.toggle_sidebar)) {
e.preventDefault()
toggle()
return
}
if (matchShortcutEvent(e, shortcuts.toggle_terminal)) {
e.preventDefault()
toggleTerminal()
return
}
if (matchShortcutEvent(e, shortcuts.toggle_aux_panel)) {
e.preventDefault()
toggleAuxPanel()
return
}
if (matchShortcutEvent(e, shortcuts.new_conversation)) {
if (!activeFolder) return
e.preventDefault()
openNewConversationTab(activeFolder.id, activeFolder.path)
return
}
if (matchShortcutEvent(e, shortcuts.open_folder)) {
e.preventDefault()
void handleOpenFolder()
return
}
if (matchShortcutEvent(e, shortcuts.open_settings)) {
e.preventDefault()
handleOpenSettings()
}
}
document.addEventListener("keydown", handleKeyDown)
return () => document.removeEventListener("keydown", handleKeyDown)
}, [
activeFolder,
handleOpenFolder,
handleOpenSettings,
openNewConversationTab,
shortcuts,
toggle,
toggleAuxPanel,
toggleTerminal,
])
const isMobile = useIsMobile()
const modeContainerRef = useRef<HTMLDivElement>(null)
const modeItemRefs = useRef<Map<string, HTMLDivElement>>(new Map())
const [modeIndicator, setModeIndicator] = useState<{
left: number
width: number
} | null>(null)
useEffect(() => {
const container = modeContainerRef.current
if (!container) return
const measure = () => {
const btn = modeItemRefs.current.get(mode)
if (!btn || !container) {
setModeIndicator(null)
return
}
const containerRect = container.getBoundingClientRect()
const btnRect = btn.getBoundingClientRect()
setModeIndicator({
left: btnRect.left - containerRect.left,
width: btnRect.width,
})
}
const ro = new ResizeObserver(() => measure())
for (const btn of modeItemRefs.current.values()) {
ro.observe(btn)
}
ro.observe(container)
measure()
return () => {
ro.disconnect()
}
}, [mode])
const handleModeKeyDown = useCallback(
(event: ReactKeyboardEvent<HTMLDivElement>, nextMode: typeof mode) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault()
setMode(nextMode)
}
},
[setMode]
)
const modeTabsElement = (
<div
ref={modeContainerRef}
role="tablist"
aria-label={tModes("workspaceModesAria")}
className="relative inline-flex h-[1.6875rem] items-center rounded-full border border-border/50 bg-muted/50 p-0.5"
>
{modeIndicator && (
<div
className="pointer-events-none absolute top-0.5 bottom-0.5 rounded-full bg-background shadow-sm ring-1 ring-border/50 transition-all duration-300 ease-[cubic-bezier(0.4,0,0.2,1)]"
style={{
left: modeIndicator.left,
width: modeIndicator.width,
}}
/>
)}
{MODE_TABS.map((item) => {
const Icon = item.icon
const isActive = mode === item.mode
const title = tModes(item.titleKey)
return (
<div
key={item.mode}
ref={(el) => {
if (el) {
modeItemRefs.current.set(item.mode, el)
} else {
modeItemRefs.current.delete(item.mode)
}
}}
role="tab"
tabIndex={0}
className={cn(
"relative z-10 m-0 flex h-[1.4375rem] cursor-pointer select-none items-center justify-center gap-1 rounded-full border-0 bg-transparent p-0 align-middle text-xs font-medium leading-none transition-all duration-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/60",
isActive ? "px-2.5" : "px-2",
isActive
? "text-foreground"
: "text-muted-foreground hover:text-foreground/70"
)}
onClick={() => setMode(item.mode)}
onKeyDown={(event) => handleModeKeyDown(event, item.mode)}
onMouseDown={(event) => event.preventDefault()}
title={!isActive ? title : undefined}
aria-label={title}
aria-selected={isActive}
>
<Icon
className="block h-3 w-3 shrink-0"
shapeRendering="geometricPrecision"
/>
{!isMobile && (
<span
className={cn(
"grid transition-[grid-template-columns] duration-300",
isActive ? "grid-cols-[1fr]" : "grid-cols-[0fr]"
)}
>
<span
className={cn(
"min-w-0 overflow-hidden whitespace-nowrap transition-opacity duration-300",
isActive ? "opacity-100" : "opacity-0"
)}
>
{title}
</span>
</span>
)}
</div>
)
})}
</div>
)
return (
<>
<AppTitleBar
centerInteractive
left={
isMobile ? (
<div className="flex min-w-0 items-center gap-2">
<Button
variant="ghost"
size="icon"
className="h-8 w-8 shrink-0"
onClick={toggle}
>
<Menu className="h-4 w-4" />
</Button>
<NewFolderDropdown />
<BranchDropdown />
</div>
) : (
<div className="flex h-8 flex-1 items-center gap-6">
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="icon"
className="h-6 w-6 hover:text-foreground/80"
onClick={toggle}
title={tTitleBar("withShortcut", {
label: tTitleBar(isOpen ? "hideSidebar" : "showSidebar"),
shortcut: formatShortcutLabel(
shortcuts.toggle_sidebar,
isMac
),
})}
>
<PanelLeft className="h-3.5 w-3.5" />
</Button>
<NewFolderDropdown />
</div>
<BranchDropdown />
<div data-tauri-drag-region className="h-8 flex-1" />
</div>
)
}
center={isMobile ? undefined : modeTabsElement}
right={
isMobile ? (
<div className="flex items-center gap-1">
<CommandDropdown />
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => setSearchOpen(true)}
title={tTitleBar("search")}
>
<Search className="h-4 w-4" />
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8">
<EllipsisVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={toggleAuxPanel}
disabled={!activeFolder}
>
<PanelRight className="h-3.5 w-3.5" />
{tTitleBar("toggleAuxPanel")}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => toggleTerminal()}
disabled={!activeFolder}
>
<SquareTerminal className="h-3.5 w-3.5" />
{tTitleBar("toggleTerminal")}
</DropdownMenuItem>
<DropdownMenuItem onClick={handleOpenSettings}>
<Settings className="h-3.5 w-3.5" />
{tTitleBar("openSettings")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
) : (
<div className="flex items-center gap-10">
<div className="flex items-center gap-2">
<CommandDropdown />
</div>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="icon"
className={`h-6 w-6 hover:text-foreground/80 ${terminalOpen ? "bg-accent" : ""}`}
onClick={() => toggleTerminal()}
disabled={!activeFolder}
title={tTitleBar("withShortcut", {
label: tTitleBar("toggleTerminal"),
shortcut: formatShortcutLabel(
shortcuts.toggle_terminal,
isMac
),
})}
>
<SquareTerminal className="h-3.5 w-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
className={`h-6 w-6 hover:text-foreground/80 ${auxPanelOpen ? "bg-accent" : ""}`}
onClick={toggleAuxPanel}
disabled={!activeFolder}
title={tTitleBar("withShortcut", {
label: tTitleBar("toggleAuxPanel"),
shortcut: formatShortcutLabel(
shortcuts.toggle_aux_panel,
isMac
),
})}
>
<PanelRight className="h-3.5 w-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 hover:text-foreground/80"
onClick={() => setSearchOpen(true)}
title={tTitleBar("withShortcut", {
label: tTitleBar("search"),
shortcut: formatShortcutLabel(
shortcuts.toggle_search,
isMac
),
})}
>
<Search className="h-3.5 w-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 hover:text-foreground/80"
onClick={handleOpenSettings}
title={tTitleBar("withShortcut", {
label: tTitleBar("openSettings"),
shortcut: formatShortcutLabel(
shortcuts.open_settings,
isMac
),
})}
>
<Settings className="h-3.5 w-3.5" />
</Button>
</div>
</div>
)
}
/>
<SearchCommandDialog open={searchOpen} onOpenChange={setSearchOpen} />
<DirectoryBrowserDialog
open={browserOpen}
onOpenChange={setBrowserOpen}
onSelect={(path) => {
openFolder(path).catch((err) => {
console.error("[FolderTitleBar] failed to open folder:", err)
})
}}
/>
</>
)
}