22
33import type { DevframeRpcClient } from 'devframe/client'
44import type { PointerEvent as ReactPointerEvent } from 'react'
5- import type { Branch , GitBranches } from '../../index'
5+ import type { GitBranches } from '../../index'
66import { useCallback , useEffect , useRef , useState } from 'react'
77import { nav as navBar , navBrand , tab as tabClass , tabsList } from '../lib/design'
8- import { cn } from '../lib/utils'
98import { CommitDetailsPanel } from './commit-details-panel'
10- import { DiffPanel } from './diff-panel'
119import { LogPanel } from './log-panel'
1210import { RpcProvider , useRpc } from './rpc-provider'
1311import { StatusPanel } from './status-panel'
1412import { useTheme } from './theme'
1513import { Badge } from './ui/badge'
1614import { IconButton } from './ui/button'
1715import { Icon } from './ui/icon'
18- import { Skeleton } from './ui/skeleton'
1916import { useRpcResource } from './use-rpc-resource'
2017
21- type DashboardPane = 'status' | ' commits' | 'diff '
18+ type DashboardPane = 'commits' | 'changes '
2219
2320interface NavItem {
2421 id : DashboardPane
@@ -27,9 +24,8 @@ interface NavItem {
2724}
2825
2926const NAV_ITEMS : NavItem [ ] = [
30- { id : 'status' , label : 'Status' , icon : 'i-ph-tree-view-duotone' } ,
3127 { id : 'commits' , label : 'Commits' , icon : 'i-ph-git-commit-duotone' } ,
32- { id : 'diff ' , label : 'Diff ' , icon : 'i-ph-git-diff-duotone' } ,
28+ { id : 'changes ' , label : 'Changes ' , icon : 'i-ph-git-diff-duotone' } ,
3329]
3430
3531function clamp ( value : number , min : number , max : number ) : number {
@@ -117,47 +113,36 @@ function ThemeToggle() {
117113 )
118114}
119115
120- function BranchRow ( {
121- branch,
122- selected,
116+ /** Branch picker, living in the nav bar. */
117+ function BranchSelect ( {
118+ branches,
119+ disabled,
120+ value,
123121 onSelect,
124122} : {
125- branch : Branch
126- selected : boolean
123+ branches : GitBranches | null
124+ disabled : boolean
125+ value : string | null
127126 onSelect : ( name : string ) => void
128127} ) {
129128 return (
130- < li >
131- < button
132- type = "button"
133- onClick = { ( ) => onSelect ( branch . name ) }
134- className = { cn (
135- 'hover:bg-active w-full rounded-md px-2 py-1.5 text-left transition-colors' ,
136- selected && 'bg-active' ,
137- ) }
129+ < label className = "border-base bg-base focus-within:ring-primary-500/40 flex h-7 min-w-0 items-center gap-1.5 rounded-md border pr-1.5 pl-2 focus-within:ring-2" >
130+ < Icon name = "i-ph-git-branch-duotone" className = "color-active size-3.5 shrink-0" />
131+ < select
132+ aria-label = "Branch"
133+ value = { value ?? '' }
134+ onChange = { event => onSelect ( event . target . value ) }
135+ disabled = { disabled }
136+ className = "color-base h-full max-w-44 min-w-0 cursor-pointer appearance-none truncate bg-transparent pr-4 text-sm outline-none disabled:cursor-default"
138137 >
139- < div className = "flex items-center gap-2" >
140- < Icon name = "i-ph-git-branch-duotone" className = { cn ( 'size-3.5' , branch . current ? 'color-active' : 'color-muted' ) } />
141- < span className = "truncate font-mono text-xs" title = { branch . name } > { branch . name } </ span >
142- { branch . current && < Badge variant = "success" className = "px-1 py-0 text-[10px]" > current</ Badge > }
143- </ div >
144- { ( branch . ahead > 0 || branch . behind > 0 ) && (
145- < p className = "color-muted mt-0.5 text-[11px] tabular-nums" >
146- { branch . ahead > 0 && `ahead ${ branch . ahead } ` }
147- { branch . ahead > 0 && branch . behind > 0 && ' · ' }
148- { branch . behind > 0 && `behind ${ branch . behind } ` }
149- </ p >
150- ) }
151- </ button >
152- </ li >
153- )
154- }
155-
156- function PanelHeading ( { children } : { children : React . ReactNode } ) {
157- return (
158- < div className = "flex h-9 shrink-0 items-center justify-between gap-2 border-b px-3" >
159- { children }
160- </ div >
138+ { ! branches ?. isRepo && < option value = "" > Not a repository</ option > }
139+ { branches ?. isRepo && branches . branches . length === 0 && < option value = "" > No branches</ option > }
140+ { branches ?. isRepo && branches . branches . map ( branch => (
141+ < option key = { branch . name } value = { branch . name } > { branch . name } </ option >
142+ ) ) }
143+ </ select >
144+ < Icon name = "i-ph-caret-up-down" className = "color-faint pointer-events-none -ml-4 size-3.5 shrink-0" />
145+ </ label >
161146 )
162147}
163148
@@ -166,15 +151,12 @@ function DashboardBody() {
166151 const [ selectedBranch , setSelectedBranch ] = useState < string | null > ( null )
167152 const [ selectedCommit , setSelectedCommit ] = useState < string | null > ( null )
168153
169- const leftRail = useRailWidth ( 'devframe-git:rail-left' , 264 , 200 , 420 , 1 )
170- const rightRail = useRailWidth ( 'devframe-git:rail-right' , 360 , 280 , 560 , - 1 )
154+ const rightRail = useRailWidth ( 'devframe-git:rail-right' , 480 , 340 , 760 , - 1 )
171155
172156 const branchesLoader = useCallback ( ( rpc : DevframeRpcClient ) => rpc . call ( 'git:branches' ) , [ ] )
173157 const {
174158 data : branches ,
175159 loading : branchesLoading ,
176- error : branchesError ,
177- refresh : refreshBranches ,
178160 } = useRpcResource < GitBranches > ( branchesLoader )
179161
180162 useEffect ( ( ) => {
@@ -202,9 +184,16 @@ function DashboardBody() {
202184 < header className = { navBar ( ) } >
203185 < div className = { navBrand ( ) } >
204186 < Icon name = "i-ph-git-fork-duotone" className = "text-base color-active" />
205- < span > Git Dashboard </ span >
187+ < span > Git</ span >
206188 </ div >
207189
190+ < BranchSelect
191+ branches = { branches }
192+ disabled = { branchesLoading || ! branches ?. isRepo || branches . branches . length === 0 }
193+ value = { selectedBranch }
194+ onSelect = { selectBranch }
195+ />
196+
208197 < nav className = { tabsList ( ) } role = "tablist" aria-label = "Git views" >
209198 { NAV_ITEMS . map ( ( { id, label, icon } ) => (
210199 < button
@@ -228,100 +217,22 @@ function DashboardBody() {
228217 </ header >
229218
230219 < div className = "flex min-h-0 flex-1" >
231- { /* Left rail: views + branch picker, then the branch list. */ }
232- < aside className = "flex min-h-0 flex-col" style = { { width : leftRail . width } } >
233- < div className = "shrink-0 space-y-3 border-b p-3" >
234- < div className = "space-y-1.5" >
235- < label htmlFor = "branch-select" className = "color-muted text-[11px] font-medium tracking-wide uppercase" >
236- Branch
237- </ label >
238- < select
239- id = "branch-select"
240- value = { selectedBranch ?? '' }
241- onChange = { event => selectBranch ( event . target . value ) }
242- disabled = { branchesLoading || ! branches ?. isRepo || branches . branches . length === 0 }
243- className = "bg-base border-base focus:ring-primary-500/40 h-9 w-full rounded-md border px-2 text-sm outline-none focus:ring-2"
244- >
245- { ! branches ?. isRepo && < option value = "" > Not a repository</ option > }
246- { branches ?. isRepo && branches . branches . length === 0 && < option value = "" > No branches</ option > }
247- { branches ?. isRepo && branches . branches . map ( branch => (
248- < option key = { branch . name } value = { branch . name } > { branch . name } </ option >
249- ) ) }
250- </ select >
251- </ div >
252-
253- { branchesError && < p className = "text-error text-xs" > { branchesError } </ p > }
254- </ div >
255-
256- < div className = "flex min-h-0 flex-1 flex-col" >
257- < PanelHeading >
258- < span className = "text-xs font-medium" > Branches</ span >
259- < div className = "flex items-center gap-1" >
260- < span className = "color-muted text-[11px] tabular-nums" >
261- { branches ?. isRepo ? branches . branches . length : '' }
262- </ span >
263- < IconButton
264- variant = "ghost"
265- size = "sm"
266- onClick = { refreshBranches }
267- disabled = { branchesLoading }
268- aria-label = "Refresh branches"
269- >
270- < Icon name = "i-ph-arrows-clockwise" className = { cn ( 'size-4' , branchesLoading && 'animate-spin' ) } />
271- </ IconButton >
272- </ div >
273- </ PanelHeading >
274-
275- < div className = "min-h-0 flex-1 overflow-y-auto p-2" >
276- { ! branches && (
277- < div className = "space-y-2" >
278- { Array . from ( { length : 6 } ) . map ( ( _ , i ) => < Skeleton key = { i } className = "h-9 w-full" /> ) }
279- </ div >
280- ) }
281-
282- { branches && ! branches . isRepo && (
283- < p className = "color-muted p-2 text-sm" > The working directory is not a git repository.</ p >
284- ) }
285-
286- { branches ?. isRepo && (
287- < ul className = "space-y-0.5" >
288- { branches . branches . map ( branch => (
289- < BranchRow
290- key = { branch . name }
291- branch = { branch }
292- selected = { branch . name === selectedBranch }
293- onSelect = { selectBranch }
294- />
295- ) ) }
296- </ ul >
297- ) }
298- </ div >
299- </ div >
300- </ aside >
301-
302- < Resizer onPointerDown = { leftRail . onPointerDown } label = "Resize sidebar" />
303-
304220 { /* Center: the active pane, scrolling inside its own region. */ }
305221 < section className = "flex min-h-0 min-w-0 flex-1 flex-col" >
306222 { pane === 'commits' && (
307- < div className = "flex min-h-0 flex-1 flex-col p-3 " >
223+ < div className = "flex min-h-0 flex-1 flex-col px-3 py-2.5 " >
308224 < LogPanel
309225 branch = { selectedBranch }
310226 selectedHash = { selectedCommit }
311227 onSelectCommit = { setSelectedCommit }
312228 />
313229 </ div >
314230 ) }
315- { pane === 'status ' && (
316- < div className = "flex min-h-0 flex-1 flex-col p-3 " >
231+ { pane === 'changes ' && (
232+ < div className = "flex min-h-0 flex-1 flex-col px-3 py-2.5 " >
317233 < StatusPanel />
318234 </ div >
319235 ) }
320- { pane === 'diff' && (
321- < div className = "min-h-0 flex-1 overflow-y-auto p-3" >
322- < DiffPanel />
323- </ div >
324- ) }
325236 </ section >
326237
327238 { showCommitDetails && selectedCommit && (
0 commit comments