feat(ui): add responsive layout support for mobile and small screens

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
xintaofei
2026-04-07 15:47:16 +08:00
parent dd659dcaa5
commit 768d1326b1
17 changed files with 664 additions and 216 deletions

View File

@@ -9,7 +9,9 @@ import {
} from "react"
import {
Columns2,
EllipsisVertical,
FileCode2,
Menu,
MessageSquare,
PanelLeft,
PanelRight,
@@ -40,6 +42,13 @@ import { CommandDropdown } from "./command-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 = [
{
@@ -196,6 +205,7 @@ export function FolderTitleBar() {
setBranch(null)
}
}, [folderPath])
const isMobile = useIsMobile()
const modeContainerRef = useRef<HTMLDivElement>(null)
const modeItemRefs = useRef<Map<string, HTMLDivElement>>(new Map())
const [modeIndicator, setModeIndicator] = useState<{
@@ -243,168 +253,234 @@ export function FolderTitleBar() {
[setMode]
)
const modeTabsElement = (
<div
ref={modeContainerRef}
role="tablist"
aria-label={tModes("workspaceModesAria")}
className="relative inline-flex h-[27px] 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-[23px] 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"
/>
{/* Hide text labels on mobile to save space */}
{!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={
<div className="flex min-w-0 items-center gap-4">
<FolderNameDropdown />
<BranchDropdown
branch={branch}
parentBranch={folder?.parent_branch ?? null}
onBranchChange={refreshBranch}
/>
<div data-tauri-drag-region className="h-8 flex-1" />
</div>
}
center={
<div
ref={modeContainerRef}
role="tablist"
aria-label={tModes("workspaceModesAria")}
className="relative inline-flex h-[27px] 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-[23px] 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"
/>
<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>
}
right={
<div className="flex items-center gap-10">
<div className="flex items-center gap-2">
<CommandDropdown />
</div>
<div className="flex items-center gap-2">
isMobile ? (
<div className="flex min-w-0 items-center gap-2">
<Button
variant="ghost"
size="icon"
className="h-6 w-6 hover:text-foreground/80"
className="h-8 w-8 shrink-0"
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>
<Button
variant="ghost"
size="icon"
className={`h-6 w-6 hover:text-foreground/80 ${auxPanelOpen ? "bg-accent" : ""}`}
onClick={toggleAuxPanel}
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 ${terminalOpen ? "bg-accent" : ""}`}
onClick={() => toggleTerminal()}
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"
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" />
<Menu className="h-4 w-4" />
</Button>
<FolderNameDropdown />
<BranchDropdown
branch={branch}
parentBranch={folder?.parent_branch ?? null}
onBranchChange={refreshBranch}
/>
</div>
</div>
) : (
<div className="flex min-w-0 items-center gap-4">
<FolderNameDropdown />
<BranchDropdown
branch={branch}
parentBranch={folder?.parent_branch ?? null}
onBranchChange={refreshBranch}
/>
<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}>
<PanelRight className="h-3.5 w-3.5" />
{tTitleBar("toggleAuxPanel")}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => toggleTerminal()}>
<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"
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>
<Button
variant="ghost"
size="icon"
className={`h-6 w-6 hover:text-foreground/80 ${auxPanelOpen ? "bg-accent" : ""}`}
onClick={toggleAuxPanel}
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 ${terminalOpen ? "bg-accent" : ""}`}
onClick={() => toggleTerminal()}
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"
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} />