33 */
44
55import React from "react" ;
6- import styled from "@emotion/styled" ;
76import type { FileTreeNode } from "@/utils/git/numstatParser" ;
87import { usePersistedState } from "@/hooks/usePersistedState" ;
98import { getFileTreeExpandStateKey } from "@/constants/storage" ;
10-
11- const TreeContainer = styled . div `
12- flex: 1;
13- min-height: 0;
14- padding: 12px;
15- overflow-y: auto;
16- font-family: var(--font-monospace);
17- font-size: 12px;
18- ` ;
19-
20- const TreeNode = styled . div < { depth : number ; isSelected : boolean } > `
21- padding: 4px 8px;
22- padding-left: ${ ( props ) => props . depth * 16 + 8 } px;
23- cursor: pointer;
24- user-select: none;
25- display: flex;
26- align-items: center;
27- gap: 8px;
28- background: ${ ( props ) => ( props . isSelected ? "rgba(100, 150, 255, 0.2)" : "transparent" ) } ;
29- border-radius: 4px;
30- margin: 2px 0;
31-
32- &:hover {
33- background: ${ ( props ) =>
34- props . isSelected ? "rgba(100, 150, 255, 0.2)" : "rgba(255, 255, 255, 0.05)" } ;
35- }
36- ` ;
37-
38- const FileName = styled . span < { isFullyRead ?: boolean ; isUnknownState ?: boolean } > `
39- color: #ccc;
40- flex: 1;
41- ${ ( props ) =>
42- props . isFullyRead &&
43- `
44- color: #666;
45- text-decoration: line-through;
46- text-decoration-color: var(--color-read);
47- text-decoration-thickness: 2px;
48- ` }
49- ${ ( props ) =>
50- props . isUnknownState &&
51- ! props . isFullyRead &&
52- `
53- color: #666;
54- ` }
55- ` ;
56-
57- const DirectoryName = styled . span < { isFullyRead ?: boolean ; isUnknownState ?: boolean } > `
58- color: #888;
59- flex: 1;
60- ${ ( props ) =>
61- props . isFullyRead &&
62- `
63- color: #666;
64- text-decoration: line-through;
65- text-decoration-color: var(--color-read);
66- text-decoration-thickness: 2px;
67- ` }
68- ${ ( props ) =>
69- props . isUnknownState &&
70- ! props . isFullyRead &&
71- `
72- color: #666;
73- ` }
74- ` ;
75-
76- const DirectoryStats = styled . span < { isOpen : boolean } > `
77- display: flex;
78- gap: 8px;
79- font-size: 11px;
80- color: ${ ( props ) => ( props . isOpen ? "#666" : "inherit" ) } ;
81- opacity: 0.7;
82- ` ;
83-
84- const Stats = styled . span `
85- display: flex;
86- gap: 8px;
87- font-size: 11px;
88- ` ;
89-
90- const Additions = styled . span `
91- color: #4ade80;
92- ` ;
93-
94- const Deletions = styled . span `
95- color: #f87171;
96- ` ;
97-
98- const ToggleIcon = styled . span < { isOpen : boolean } > `
99- width: 12px;
100- display: inline-block;
101- transform: ${ ( props ) => ( props . isOpen ? "rotate(90deg)" : "rotate(0deg)" ) } ;
102- transition: transform 0.2s ease;
103- ` ;
104-
105- const ClearButton = styled . button `
106- padding: 2px 8px;
107- background: transparent;
108- color: #888;
109- border: none;
110- border-radius: 3px;
111- font-size: 11px;
112- cursor: pointer;
113- transition: all 0.2s ease;
114- font-family: var(--font-primary);
115- margin-left: auto;
116-
117- &:hover {
118- background: rgba(255, 255, 255, 0.05);
119- color: #ccc;
120- }
121- ` ;
122-
123- const TreeHeader = styled . div `
124- padding: 8px 12px;
125- border-bottom: 1px solid #3e3e42;
126- font-size: 12px;
127- font-weight: 500;
128- color: #ccc;
129- font-family: var(--font-primary);
130- display: flex;
131- align-items: center;
132- gap: 8px;
133- ` ;
134-
135- const EmptyState = styled . div `
136- padding: 20px;
137- color: #888;
138- text-align: center;
139- ` ;
9+ import { cn } from "@/lib/utils" ;
14010
14111/**
14212 * Compute read status for a directory by recursively checking all descendant files
@@ -185,6 +55,7 @@ const TreeNodeContent: React.FC<{
18555 depth : number ;
18656 selectedPath : string | null ;
18757 onSelectFile : ( path : string | null ) => void ;
58+ commonPrefix : string | null ;
18859 getFileReadStatus ?: ( filePath : string ) => { total : number ; read : number } | null ;
18960 expandStateMap : Record < string , boolean > ;
19061 setExpandStateMap : (
@@ -195,6 +66,7 @@ const TreeNodeContent: React.FC<{
19566 depth,
19667 selectedPath,
19768 onSelectFile,
69+ commonPrefix,
19870 getFileReadStatus,
19971 expandStateMap,
20072 setExpandStateMap,
@@ -254,48 +126,83 @@ const TreeNodeContent: React.FC<{
254126
255127 return (
256128 < >
257- < TreeNode depth = { depth } isSelected = { isSelected } onClick = { handleClick } >
129+ < div
130+ className = { cn (
131+ "py-1 px-2 cursor-pointer select-none flex items-center gap-2 rounded my-0.5" ,
132+ isSelected ? "bg-[rgba(100,150,255,0.2)]" : "bg-transparent hover:bg-white/5"
133+ ) }
134+ style = { { paddingLeft : `${ depth * 16 + 8 } px` } }
135+ onClick = { handleClick }
136+ >
258137 { node . isDirectory ? (
259138 < >
260- < ToggleIcon isOpen = { isOpen } data-toggle onClick = { handleToggleClick } >
139+ < span
140+ className = "w-3 inline-block transition-transform duration-200"
141+ style = { { transform : isOpen ? "rotate(90deg)" : "rotate(0deg)" } }
142+ data-toggle
143+ onClick = { handleToggleClick }
144+ >
261145 ▶
262- </ ToggleIcon >
263- < DirectoryName isFullyRead = { isFullyRead } isUnknownState = { isUnknownState } >
146+ </ span >
147+ < span
148+ className = { cn (
149+ "flex-1" ,
150+ isFullyRead &&
151+ "text-[#666] line-through [text-decoration-color:var(--color-read)] [text-decoration-thickness:2px]" ,
152+ isUnknownState && ! isFullyRead && "text-[#666]" ,
153+ ! isFullyRead && ! isUnknownState && "text-[#888]"
154+ ) }
155+ >
264156 { node . name || "/" }
265- </ DirectoryName >
157+ </ span >
266158 { node . totalStats &&
267159 ( node . totalStats . additions > 0 || node . totalStats . deletions > 0 ) && (
268- < DirectoryStats isOpen = { isOpen } >
160+ < span
161+ className = "flex gap-2 text-[11px] opacity-70"
162+ style = { { color : isOpen ? "#666" : "inherit" } }
163+ >
269164 { node . totalStats . additions > 0 &&
270165 ( isOpen ? (
271166 < span > +{ node . totalStats . additions } </ span >
272167 ) : (
273- < Additions > +{ node . totalStats . additions } </ Additions >
168+ < span className = "text-[#4ade80]" > +{ node . totalStats . additions } </ span >
274169 ) ) }
275170 { node . totalStats . deletions > 0 &&
276171 ( isOpen ? (
277172 < span > -{ node . totalStats . deletions } </ span >
278173 ) : (
279- < Deletions > -{ node . totalStats . deletions } </ Deletions >
174+ < span className = "text-[#f87171]" > -{ node . totalStats . deletions } </ span >
280175 ) ) }
281- </ DirectoryStats >
176+ </ span >
282177 ) }
283178 </ >
284179 ) : (
285180 < >
286181 < span style = { { width : "12px" } } />
287- < FileName isFullyRead = { isFullyRead } isUnknownState = { isUnknownState } >
182+ < span
183+ className = { cn (
184+ "flex-1" ,
185+ isFullyRead &&
186+ "text-[#666] line-through [text-decoration-color:var(--color-read)] [text-decoration-thickness:2px]" ,
187+ isUnknownState && ! isFullyRead && "text-[#666]" ,
188+ ! isFullyRead && ! isUnknownState && "text-[#ccc]"
189+ ) }
190+ >
288191 { node . name }
289- </ FileName >
192+ </ span >
290193 { node . stats && (
291- < Stats >
292- { node . stats . additions > 0 && < Additions > +{ node . stats . additions } </ Additions > }
293- { node . stats . deletions > 0 && < Deletions > -{ node . stats . deletions } </ Deletions > }
294- </ Stats >
194+ < span className = "flex gap-2 text-[11px]" >
195+ { node . stats . additions > 0 && (
196+ < span className = "text-[#4ade80]" > +{ node . stats . additions } </ span >
197+ ) }
198+ { node . stats . deletions > 0 && (
199+ < span className = "text-[#f87171]" > -{ node . stats . deletions } </ span >
200+ ) }
201+ </ span >
295202 ) }
296203 </ >
297204 ) }
298- </ TreeNode >
205+ </ div >
299206
300207 { node . isDirectory &&
301208 isOpen &&
@@ -306,6 +213,7 @@ const TreeNodeContent: React.FC<{
306213 depth = { depth + 1 }
307214 selectedPath = { selectedPath }
308215 onSelectFile = { onSelectFile }
216+ commonPrefix = { commonPrefix }
309217 getFileReadStatus = { getFileReadStatus }
310218 expandStateMap = { expandStateMap }
311219 setExpandStateMap = { setExpandStateMap }
@@ -320,6 +228,7 @@ interface FileTreeExternalProps {
320228 selectedPath : string | null ;
321229 onSelectFile : ( path : string | null ) => void ;
322230 isLoading ?: boolean ;
231+ commonPrefix ?: string | null ;
323232 getFileReadStatus ?: ( filePath : string ) => { total : number ; read : number } | null ;
324233 workspaceId : string ;
325234}
@@ -329,6 +238,7 @@ export const FileTree: React.FC<FileTreeExternalProps> = ({
329238 selectedPath,
330239 onSelectFile,
331240 isLoading = false ,
241+ commonPrefix = null ,
332242 getFileReadStatus,
333243 workspaceId,
334244} ) => {
@@ -339,32 +249,62 @@ export const FileTree: React.FC<FileTreeExternalProps> = ({
339249 { listener : true }
340250 ) ;
341251
252+ // Find the node at the common prefix path to start rendering from
253+ const startNode = React . useMemo ( ( ) => {
254+ if ( ! commonPrefix || ! root ) return root ;
255+
256+ // Navigate to the node at the common prefix path
257+ const parts = commonPrefix . split ( "/" ) ;
258+ let current = root ;
259+
260+ for ( const part of parts ) {
261+ const child = current . children . find ( ( c ) => c . name === part ) ;
262+ if ( ! child ) return root ; // Fallback if path not found
263+ current = child ;
264+ }
265+
266+ return current ;
267+ } , [ root , commonPrefix ] ) ;
268+
342269 return (
343270 < >
344- < TreeHeader >
271+ < div className = "py-2 px-3 border-b border-[#3e3e42] text-xs font-medium text-[#ccc] font-primary flex items-center gap-2" >
345272 < span > Files Changed</ span >
346- { selectedPath && < ClearButton onClick = { ( ) => onSelectFile ( null ) } > Clear filter</ ClearButton > }
347- </ TreeHeader >
348- < TreeContainer >
349- { isLoading && ! root ? (
350- < EmptyState > Loading file tree...</ EmptyState >
351- ) : root ? (
352- root . children . map ( ( child ) => (
273+ { selectedPath && (
274+ < button
275+ className = "py-0.5 px-2 bg-transparent text-[#888] border-none rounded-[3px] text-[11px] cursor-pointer transition-all duration-200 font-primary ml-auto hover:bg-white/5 hover:text-[#ccc]"
276+ onClick = { ( ) => onSelectFile ( null ) }
277+ >
278+ Clear filter
279+ </ button >
280+ ) }
281+ </ div >
282+ { commonPrefix && (
283+ < div className = "py-1.5 px-3 bg-[#1e1e1e] border-b border-[#3e3e42] text-[11px] text-[#888] font-monospace" >
284+ { commonPrefix } /
285+ </ div >
286+ ) }
287+ < div className = "flex-1 min-h-0 p-3 overflow-y-auto font-monospace text-xs" >
288+ { isLoading && ! startNode ? (
289+ < div className = "py-5 text-[#888] text-center" > Loading file tree...</ div >
290+ ) : startNode ? (
291+ startNode . children . map ( ( child ) => (
353292 < TreeNodeContent
354293 key = { child . path }
355294 node = { child }
356295 depth = { 0 }
357296 selectedPath = { selectedPath }
358297 onSelectFile = { onSelectFile }
298+ commonPrefix = { commonPrefix }
359299 getFileReadStatus = { getFileReadStatus }
360300 expandStateMap = { expandStateMap }
361301 setExpandStateMap = { setExpandStateMap }
362302 />
363303 ) )
364304 ) : (
365- < EmptyState > No files changed</ EmptyState >
305+ < div className = "py-5 text-[#888] text-center" > No files changed</ div >
366306 ) }
367- </ TreeContainer >
307+ </ div >
368308 </ >
369309 ) ;
370310} ;
0 commit comments