Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions frontend/src/components/navigation/DesktopSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
Sidebar,
SidebarSection,
SidebarItem,
SidebarCollapseToggle,
} from '@/components/ui/sidebar'
import { FolderGit2 } from 'lucide-react'

Expand Down Expand Up @@ -78,10 +77,6 @@ export function DesktopSidebar() {
return (
<>
<Sidebar collapsed={collapsed} onToggle={toggle} className='mt-2'>
<div className="flex justify-end px-2 py-1.5">
<SidebarCollapseToggle collapsed={collapsed} onToggle={toggle} />
</div>

{primary.length > 0 && (
<SidebarSection collapsed={collapsed}>
{primary.map((item: NavPrimaryCta) => (
Expand Down
67 changes: 23 additions & 44 deletions frontend/src/components/source-control/BranchesTab.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { useMemo, useState } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { listBranches, switchBranch, GitAuthError, getRepo } from '@/api/repos'
import { fetchGitStatus, useGitStatus } from '@/api/git'
import { fetchGitStatus } from '@/api/git'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Loader2, GitBranch, GitBranchPlus, Check, Plus, AlertCircle, ArrowUp, ArrowDown, Globe } from 'lucide-react'
import { Loader2, GitBranch, GitBranchPlus, Check, Plus, AlertCircle, Globe } from 'lucide-react'
import { cn } from '@/lib/utils'
import { showToast } from '@/lib/toast'
import { useGit } from '@/hooks/useGit'
Expand All @@ -25,8 +25,6 @@ export function BranchesTab({ repoId, currentBranch }: BranchesTabProps) {
const [worktreeDialogOpen, setWorktreeDialogOpen] = useState(false)
const git = useGit(repoId)

const { data: status } = useGitStatus(repoId)

const { data: branches, isLoading, error, refetch } = useQuery({
queryKey: ['branches', repoId],
queryFn: () => listBranches(repoId),
Expand Down Expand Up @@ -103,36 +101,18 @@ export function BranchesTab({ repoId, currentBranch }: BranchesTabProps) {
}

const activeBranch = branches?.branches?.find(b => b.current)?.name || currentBranch
const hasBranches = (branches?.branches?.length ?? 0) > 0

return (
<div className="flex flex-col h-full">
<div className="p-3 border-b border-border space-y-3 flex-shrink-0">
<div className="flex items-center gap-2">
<GitBranch className="w-4 h-4 text-muted-foreground" />
<span className="text-sm font-medium">Current: {activeBranch}</span>
{status && (status.ahead > 0 || status.behind > 0) && (
<div className="flex items-center gap-1 text-xs text-muted-foreground">
{status.ahead > 0 && (
<span className="flex items-center gap-0.5">
<ArrowUp className="w-3 h-3" />{status.ahead}
</span>
)}
{status.behind > 0 && (
<span className="flex items-center gap-0.5">
<ArrowDown className="w-3 h-3" />{status.behind}
</span>
)}
</div>
)}
</div>

<div className="p-3 border-b border-border flex-shrink-0">
{isCreating ? (
<div className="flex items-center gap-2">
<Input
placeholder="New branch name..."
value={newBranchName}
onChange={(e) => setNewBranchName(e.target.value)}
className="h-8 md:text-sm"
className="h-10 md:h-8 md:text-sm"
autoFocus
onKeyDown={(e) => {
if (e.key === 'Enter') handleCreateBranch()
Expand All @@ -144,7 +124,7 @@ export function BranchesTab({ repoId, currentBranch }: BranchesTabProps) {
/>
<Button
size="sm"
className="h-8"
className="h-10 md:h-8"
onClick={handleCreateBranch}
disabled={!newBranchName.trim() || git.createBranch.isPending}
>
Expand All @@ -157,7 +137,7 @@ export function BranchesTab({ repoId, currentBranch }: BranchesTabProps) {
<Button
size="sm"
variant="ghost"
className="h-8"
className="h-10 md:h-8"
onClick={() => {
setIsCreating(false)
setNewBranchName('')
Expand All @@ -168,39 +148,38 @@ export function BranchesTab({ repoId, currentBranch }: BranchesTabProps) {
</div>
) : (
<div className="flex items-center gap-2">
{hasBranches && (
<Input
placeholder="Search branches..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="h-10 md:h-8 md:text-sm flex-1 min-w-0"
/>
)}
<Button
size="sm"
variant="outline"
className="flex-1 h-8"
className="h-10 md:h-8 flex-shrink-0"
onClick={() => setIsCreating(true)}
title="Create branch"
>
<Plus className="w-4 h-4 mr-2" />
Create Branch
<Plus className="w-4 h-4" />
<span className="hidden sm:inline ml-1">Branch</span>
</Button>
{repoUrl && (
<Button
size="sm"
variant="outline"
className="flex-1 h-8"
className="h-10 md:h-8 flex-shrink-0"
onClick={() => setWorktreeDialogOpen(true)}
title="Create as a separate worktree workspace"
>
<GitBranchPlus className="w-4 h-4 mr-2" />
Create Worktree
<GitBranchPlus className="w-4 h-4" />
<span className="hidden sm:inline ml-1">Worktree</span>
</Button>
)}
</div>
)}
{branches?.branches && branches.branches.length > 0 && (
<div className="px-3 pb-1">
<Input
placeholder="Search branches..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="h-8 md:text-sm"
/>
</div>
)}
</div>

<div className="flex-1 overflow-y-auto relative">
Expand Down Expand Up @@ -228,7 +207,7 @@ export function BranchesTab({ repoId, currentBranch }: BranchesTabProps) {
key={branch.name}
className={cn(
'flex items-center gap-2 px-3 py-2 w-full text-left transition-colors',
isCurrent && 'bg-accent',
isCurrent && 'bg-orange-500/10',
isCheckedOutElsewhere ? 'opacity-60 cursor-not-allowed' : 'hover:bg-accent/50'
)}
onClick={handleClick}
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/source-control/SourceControlPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ export function SourceControlPanel({
<div className="flex items-center justify-between px-3 py-2 border-b border-border flex-shrink-0">
<div className="flex items-center gap-2">
<div className="flex items-center gap-2">
<GitBranch className="w-4 h-4 text-muted-foreground" />
<span className="text-sm font-medium">{displayBranch}</span>
<GitBranch className={cn('w-4 h-4', GIT_UI_COLORS.current)} />
<span className={cn('text-sm font-medium', GIT_UI_COLORS.current)}>{displayBranch}</span>
</div>
{status && (status.ahead > 0 || status.behind > 0) && (
<div className="flex items-center gap-1 text-xs text-muted-foreground">
Expand Down
27 changes: 27 additions & 0 deletions frontend/src/components/ui/sidebar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,31 @@ describe('SidebarCollapseToggle', () => {
fireEvent.click(screen.getByRole('button'))
expect(handleToggle).toHaveBeenCalledTimes(1)
})

it('calls onToggle when clicked while collapsed', () => {
const handleToggle = vi.fn()

render(
<SidebarCollapseToggle collapsed={true} onToggle={handleToggle} />
)

fireEvent.click(screen.getByRole('button', { name: 'Expand sidebar' }))
expect(handleToggle).toHaveBeenCalledTimes(1)
})

it('exposes expanded state via accessible name and aria-expanded', () => {
const { rerender } = render(
<SidebarCollapseToggle collapsed={false} onToggle={vi.fn()} />
)

expect(
screen.getByRole('button', { name: 'Collapse sidebar' })
).toHaveAttribute('aria-expanded', 'true')

rerender(<SidebarCollapseToggle collapsed={true} onToggle={vi.fn()} />)

expect(
screen.getByRole('button', { name: 'Expand sidebar' })
).toHaveAttribute('aria-expanded', 'false')
})
})
38 changes: 31 additions & 7 deletions frontend/src/components/ui/sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { LucideIcon } from 'lucide-react'
import { PanelLeft, PanelLeftClose } from 'lucide-react'
import { ChevronLeft, ChevronRight } from 'lucide-react'
import { cn } from '@/lib/utils'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'

export interface SidebarProps {
collapsed: boolean
onToggle: () => void
onToggle?: () => void
widthClass?: string
collapsedWidthClass?: string
className?: string
Expand All @@ -15,6 +15,7 @@ export interface SidebarProps {

export function Sidebar({
collapsed,
onToggle,
widthClass = 'w-60',
collapsedWidthClass = 'w-14',
className,
Expand All @@ -24,12 +25,13 @@ export function Sidebar({
return (
<aside
className={cn(
'flex-shrink-0 border-r border-border bg-card/50 backdrop-blur-sm h-dvh flex flex-col pt-safe pb-safe transition-[width] duration-200',
'relative z-20 flex-shrink-0 border-r border-border bg-card/50 backdrop-blur-sm h-dvh flex flex-col pt-safe pb-safe transition-[width] duration-200',
collapsed ? collapsedWidthClass : widthClass,
className
)}
aria-label={ariaLabel}
>
{onToggle && <SidebarCollapseToggle collapsed={collapsed} onToggle={onToggle} />}
{children}
</aside>
)
Expand Down Expand Up @@ -125,14 +127,36 @@ export function SidebarCollapseToggle({ collapsed, onToggle }: SidebarCollapseTo
type="button"
onClick={onToggle}
className={cn(
'rounded-md p-2.5',
'hover:bg-accent hover:text-accent-foreground',
'transition-colors duration-150'
'group absolute -right-3 top-1/2 z-20 h-16 w-6 -translate-y-1/2',
'flex items-center justify-center bg-transparent',
'focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background'
)}
title={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
aria-expanded={!collapsed}
>
{collapsed ? <PanelLeft className="h-5 w-5" /> : <PanelLeftClose className="h-5 w-5" />}
<span aria-hidden="true" className="relative flex items-center justify-center">
<span
className={cn(
'h-8 w-1 rounded-full bg-orange-500/60 transition-colors duration-150',
'group-hover:bg-orange-500 group-focus-visible:bg-orange-500'
)}
/>
<ChevronLeft
className={cn(
'absolute left-full ml-0.5 h-3.5 w-3.5 text-muted-foreground opacity-0 transition-opacity duration-150',
'group-hover:opacity-100 group-focus-visible:opacity-100',
collapsed && 'hidden'
)}
/>
<ChevronRight
className={cn(
'absolute left-full ml-0.5 h-3.5 w-3.5 text-muted-foreground opacity-0 transition-opacity duration-150',
'group-hover:opacity-100 group-focus-visible:opacity-100',
!collapsed && 'hidden'
)}
/>
</span>
</button>
)
}
2 changes: 1 addition & 1 deletion frontend/src/lib/git-status-styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const GIT_STATUS_COLORS = {
export const GIT_UI_COLORS = {
ahead: 'text-emerald-600 dark:text-emerald-400',
behind: 'text-amber-600 dark:text-amber-400',
current: 'text-emerald-600 dark:text-emerald-400',
current: 'text-orange-600 dark:text-orange-400',
remote: 'text-blue-600 dark:text-blue-400',
stage: 'text-emerald-600 dark:text-emerald-400',
unstage: 'text-rose-600 dark:text-rose-400',
Expand Down