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:
@@ -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} />
|
||||
|
||||
Reference in New Issue
Block a user