mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 20:43:36 +00:00
feat: Implement responsive mobile header layout with menu consolidation
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
export { BranchSwitchDropdown } from './branch-switch-dropdown';
|
||||
export { WorktreeActionsDropdown } from './worktree-actions-dropdown';
|
||||
export { WorktreeMobileDropdown } from './worktree-mobile-dropdown';
|
||||
export { WorktreeTab } from './worktree-tab';
|
||||
|
||||
@@ -44,6 +44,8 @@ interface WorktreeActionsDropdownProps {
|
||||
isDevServerRunning: boolean;
|
||||
devServerInfo?: DevServerInfo;
|
||||
gitRepoStatus: GitRepoStatus;
|
||||
/** When true, renders as a standalone button (not attached to another element) */
|
||||
standalone?: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onPull: (worktree: WorktreeInfo) => void;
|
||||
onPush: (worktree: WorktreeInfo) => void;
|
||||
@@ -71,6 +73,7 @@ export function WorktreeActionsDropdown({
|
||||
isDevServerRunning,
|
||||
devServerInfo,
|
||||
gitRepoStatus,
|
||||
standalone = false,
|
||||
onOpenChange,
|
||||
onPull,
|
||||
onPush,
|
||||
@@ -115,15 +118,17 @@ export function WorktreeActionsDropdown({
|
||||
<DropdownMenu onOpenChange={onOpenChange}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant={isSelected ? 'default' : 'outline'}
|
||||
variant={standalone ? 'outline' : isSelected ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
className={cn(
|
||||
'h-7 w-7 p-0 rounded-l-none',
|
||||
isSelected && 'bg-primary text-primary-foreground',
|
||||
!isSelected && 'bg-secondary/50 hover:bg-secondary'
|
||||
'h-7 w-7 p-0',
|
||||
!standalone && 'rounded-l-none',
|
||||
standalone && 'h-8 w-8 shrink-0',
|
||||
!standalone && isSelected && 'bg-primary text-primary-foreground',
|
||||
!standalone && !isSelected && 'bg-secondary/50 hover:bg-secondary'
|
||||
)}
|
||||
>
|
||||
<MoreHorizontal className="w-3 h-3" />
|
||||
<MoreHorizontal className="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-56">
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { GitBranch, ChevronDown, Loader2, CircleDot, Check } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { WorktreeInfo } from '../types';
|
||||
|
||||
interface WorktreeMobileDropdownProps {
|
||||
worktrees: WorktreeInfo[];
|
||||
isWorktreeSelected: (worktree: WorktreeInfo) => boolean;
|
||||
hasRunningFeatures: (worktree: WorktreeInfo) => boolean;
|
||||
isActivating: boolean;
|
||||
branchCardCounts?: Record<string, number>;
|
||||
onSelectWorktree: (worktree: WorktreeInfo) => void;
|
||||
}
|
||||
|
||||
export function WorktreeMobileDropdown({
|
||||
worktrees,
|
||||
isWorktreeSelected,
|
||||
hasRunningFeatures,
|
||||
isActivating,
|
||||
branchCardCounts,
|
||||
onSelectWorktree,
|
||||
}: WorktreeMobileDropdownProps) {
|
||||
// Find the currently selected worktree to display in the trigger
|
||||
const selectedWorktree = worktrees.find((w) => isWorktreeSelected(w));
|
||||
const displayBranch = selectedWorktree?.branch || 'Select branch';
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8 px-3 gap-2 font-mono text-xs bg-secondary/50 hover:bg-secondary flex-1 min-w-0"
|
||||
disabled={isActivating}
|
||||
>
|
||||
<GitBranch className="w-3.5 h-3.5 shrink-0" />
|
||||
<span className="truncate">{displayBranch}</span>
|
||||
{isActivating ? (
|
||||
<Loader2 className="w-3 h-3 animate-spin shrink-0" />
|
||||
) : (
|
||||
<ChevronDown className="w-3 h-3 shrink-0 ml-auto" />
|
||||
)}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-64 max-h-80 overflow-y-auto">
|
||||
<DropdownMenuLabel className="text-xs font-normal text-muted-foreground">
|
||||
Branches & Worktrees
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{worktrees.map((worktree) => {
|
||||
const isSelected = isWorktreeSelected(worktree);
|
||||
const isRunning = hasRunningFeatures(worktree);
|
||||
const cardCount = branchCardCounts?.[worktree.branch];
|
||||
const hasChanges = worktree.hasChanges;
|
||||
const changedFilesCount = worktree.changedFilesCount;
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={worktree.path}
|
||||
onClick={() => onSelectWorktree(worktree)}
|
||||
className={cn('flex items-center gap-2 cursor-pointer', isSelected && 'bg-accent')}
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
{isSelected ? (
|
||||
<Check className="w-3.5 h-3.5 shrink-0 text-primary" />
|
||||
) : (
|
||||
<div className="w-3.5 h-3.5 shrink-0" />
|
||||
)}
|
||||
{isRunning && <Loader2 className="w-3 h-3 animate-spin shrink-0" />}
|
||||
<span className={cn('font-mono text-xs truncate', isSelected && 'font-medium')}>
|
||||
{worktree.branch}
|
||||
</span>
|
||||
{worktree.isMain && (
|
||||
<span className="text-[10px] px-1 py-0.5 rounded bg-muted text-muted-foreground shrink-0">
|
||||
main
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 shrink-0">
|
||||
{cardCount !== undefined && cardCount > 0 && (
|
||||
<span className="inline-flex items-center justify-center h-4 min-w-[1rem] px-1 text-[10px] font-medium rounded bg-background/80 text-foreground border border-border">
|
||||
{cardCount}
|
||||
</span>
|
||||
)}
|
||||
{hasChanges && (
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center h-4 min-w-[1rem] px-1 text-[10px] font-medium rounded border',
|
||||
'bg-amber-500/20 text-amber-600 dark:text-amber-400 border-amber-500/30'
|
||||
)}
|
||||
title={`${changedFilesCount ?? 'Some'} uncommitted file${changedFilesCount !== 1 ? 's' : ''}`}
|
||||
>
|
||||
<CircleDot className="w-2.5 h-2.5 mr-0.5" />
|
||||
{changedFilesCount ?? '!'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { GitBranch, Plus, RefreshCw } from 'lucide-react';
|
||||
import { cn, pathsEqual } from '@/lib/utils';
|
||||
import { toast } from 'sonner';
|
||||
import { getHttpApiClient } from '@/lib/http-api-client';
|
||||
import { useIsMobile } from '@/hooks/use-media-query';
|
||||
import type { WorktreePanelProps, WorktreeInfo } from './types';
|
||||
import {
|
||||
useWorktrees,
|
||||
@@ -12,7 +13,7 @@ import {
|
||||
useWorktreeActions,
|
||||
useRunningFeatures,
|
||||
} from './hooks';
|
||||
import { WorktreeTab } from './components';
|
||||
import { WorktreeTab, WorktreeMobileDropdown, WorktreeActionsDropdown } from './components';
|
||||
|
||||
export function WorktreePanel({
|
||||
projectPath,
|
||||
@@ -103,6 +104,8 @@ export function WorktreePanel({
|
||||
checkInitScript();
|
||||
}, [projectPath]);
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
// Periodic interval check (5 seconds) to detect branch changes on disk
|
||||
// Reduced from 1s to 5s to minimize GPU/CPU usage from frequent re-renders
|
||||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
@@ -167,6 +170,85 @@ export function WorktreePanel({
|
||||
const mainWorktree = worktrees.find((w) => w.isMain);
|
||||
const nonMainWorktrees = worktrees.filter((w) => !w.isMain);
|
||||
|
||||
// Mobile view: single dropdown for all worktrees
|
||||
if (isMobile) {
|
||||
// Find the currently selected worktree for the actions menu
|
||||
const selectedWorktree = worktrees.find((w) => isWorktreeSelected(w)) || mainWorktree;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 px-3 py-2 border-b border-border bg-glass/50 backdrop-blur-sm">
|
||||
<WorktreeMobileDropdown
|
||||
worktrees={worktrees}
|
||||
isWorktreeSelected={isWorktreeSelected}
|
||||
hasRunningFeatures={hasRunningFeatures}
|
||||
isActivating={isActivating}
|
||||
branchCardCounts={branchCardCounts}
|
||||
onSelectWorktree={handleSelectWorktree}
|
||||
/>
|
||||
|
||||
{/* Actions menu for the selected worktree */}
|
||||
{selectedWorktree && (
|
||||
<WorktreeActionsDropdown
|
||||
worktree={selectedWorktree}
|
||||
isSelected={true}
|
||||
standalone={true}
|
||||
aheadCount={aheadCount}
|
||||
behindCount={behindCount}
|
||||
isPulling={isPulling}
|
||||
isPushing={isPushing}
|
||||
isStartingDevServer={isStartingDevServer}
|
||||
isDevServerRunning={isDevServerRunning(selectedWorktree)}
|
||||
devServerInfo={getDevServerInfo(selectedWorktree)}
|
||||
gitRepoStatus={gitRepoStatus}
|
||||
onOpenChange={handleActionsDropdownOpenChange(selectedWorktree)}
|
||||
onPull={handlePull}
|
||||
onPush={handlePush}
|
||||
onOpenInEditor={handleOpenInEditor}
|
||||
onCommit={onCommit}
|
||||
onCreatePR={onCreatePR}
|
||||
onAddressPRComments={onAddressPRComments}
|
||||
onResolveConflicts={onResolveConflicts}
|
||||
onDeleteWorktree={onDeleteWorktree}
|
||||
onStartDevServer={handleStartDevServer}
|
||||
onStopDevServer={handleStopDevServer}
|
||||
onOpenDevServerUrl={handleOpenDevServerUrl}
|
||||
/>
|
||||
)}
|
||||
|
||||
{useWorktreesEnabled && (
|
||||
<>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground shrink-0"
|
||||
onClick={onCreateWorktree}
|
||||
title="Create new worktree"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground shrink-0"
|
||||
onClick={async () => {
|
||||
const removedWorktrees = await fetchWorktrees();
|
||||
if (removedWorktrees && removedWorktrees.length > 0 && onRemovedWorktrees) {
|
||||
onRemovedWorktrees(removedWorktrees);
|
||||
}
|
||||
}}
|
||||
disabled={isLoading}
|
||||
title="Refresh worktrees"
|
||||
>
|
||||
<RefreshCw className={cn('w-3.5 h-3.5', isLoading && 'animate-spin')} />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Desktop view: full tabs layout
|
||||
return (
|
||||
<div className="flex items-center gap-2 px-4 py-2 border-b border-border bg-glass/50 backdrop-blur-sm">
|
||||
<GitBranch className="w-4 h-4 text-muted-foreground" />
|
||||
|
||||
Reference in New Issue
Block a user