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 CommonPrefix = styled . div `
136- padding: 6px 12px;
137- background: #1e1e1e;
138- border-bottom: 1px solid #3e3e42;
139- font-size: 11px;
140- color: #888;
141- font-family: var(--font-monospace);
142- ` ;
143-
144- const EmptyState = styled . div `
145- padding: 20px;
146- color: #888;
147- text-align: center;
148- ` ;
9+ import { cn } from "@/lib/utils" ;
14910
15011/**
15112 * Compute read status for a directory by recursively checking all descendant files
@@ -265,48 +126,77 @@ const TreeNodeContent: React.FC<{
265126
266127 return (
267128 < >
268- < 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+ >
269137 { node . isDirectory ? (
270138 < >
271- < 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+ >
272145 ▶
273- </ ToggleIcon >
274- < DirectoryName isFullyRead = { isFullyRead } isUnknownState = { isUnknownState } >
146+ </ span >
147+ < span
148+ className = { cn (
149+ "flex-1" ,
150+ isFullyRead && "text-[#666] line-through [text-decoration-color:var(--color-read)] [text-decoration-thickness:2px]" ,
151+ isUnknownState && ! isFullyRead && "text-[#666]" ,
152+ ! isFullyRead && ! isUnknownState && "text-[#888]"
153+ ) }
154+ >
275155 { node . name || "/" }
276- </ DirectoryName >
156+ </ span >
277157 { node . totalStats &&
278158 ( node . totalStats . additions > 0 || node . totalStats . deletions > 0 ) && (
279- < DirectoryStats isOpen = { isOpen } >
159+ < span
160+ className = "flex gap-2 text-[11px] opacity-70"
161+ style = { { color : isOpen ? "#666" : "inherit" } }
162+ >
280163 { node . totalStats . additions > 0 &&
281164 ( isOpen ? (
282165 < span > +{ node . totalStats . additions } </ span >
283166 ) : (
284- < Additions > +{ node . totalStats . additions } </ Additions >
167+ < span className = "text-[#4ade80]" > +{ node . totalStats . additions } </ span >
285168 ) ) }
286169 { node . totalStats . deletions > 0 &&
287170 ( isOpen ? (
288171 < span > -{ node . totalStats . deletions } </ span >
289172 ) : (
290- < Deletions > -{ node . totalStats . deletions } </ Deletions >
173+ < span className = "text-[#f87171]" > -{ node . totalStats . deletions } </ span >
291174 ) ) }
292- </ DirectoryStats >
175+ </ span >
293176 ) }
294177 </ >
295178 ) : (
296179 < >
297180 < span style = { { width : "12px" } } />
298- < FileName isFullyRead = { isFullyRead } isUnknownState = { isUnknownState } >
181+ < span
182+ className = { cn (
183+ "flex-1" ,
184+ isFullyRead && "text-[#666] line-through [text-decoration-color:var(--color-read)] [text-decoration-thickness:2px]" ,
185+ isUnknownState && ! isFullyRead && "text-[#666]" ,
186+ ! isFullyRead && ! isUnknownState && "text-[#ccc]"
187+ ) }
188+ >
299189 { node . name }
300- </ FileName >
190+ </ span >
301191 { node . stats && (
302- < Stats >
303- { node . stats . additions > 0 && < Additions > +{ node . stats . additions } </ Additions > }
304- { node . stats . deletions > 0 && < Deletions > -{ node . stats . deletions } </ Deletions > }
305- </ Stats >
192+ < span className = "flex gap-2 text-[11px]" >
193+ { node . stats . additions > 0 && < span className = "text-[#4ade80]" > +{ node . stats . additions } </ span > }
194+ { node . stats . deletions > 0 && < span className = "text-[#f87171]" > -{ node . stats . deletions } </ span > }
195+ </ span >
306196 ) }
307197 </ >
308198 ) }
309- </ TreeNode >
199+ </ div >
310200
311201 { node . isDirectory &&
312202 isOpen &&
@@ -372,14 +262,25 @@ export const FileTree: React.FC<FileTreeExternalProps> = ({
372262
373263 return (
374264 < >
375- < TreeHeader >
265+ < div className = "py-2 px-3 border-b border-[#3e3e42] text-xs font-medium text-[#ccc] font-primary flex items-center gap-2" >
376266 < span > Files Changed</ span >
377- { selectedPath && < ClearButton onClick = { ( ) => onSelectFile ( null ) } > Clear filter</ ClearButton > }
378- </ TreeHeader >
379- { commonPrefix && < CommonPrefix > { commonPrefix } /</ CommonPrefix > }
380- < TreeContainer >
267+ { selectedPath && (
268+ < button
269+ 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]"
270+ onClick = { ( ) => onSelectFile ( null ) }
271+ >
272+ Clear filter
273+ </ button >
274+ ) }
275+ </ div >
276+ { commonPrefix && (
277+ < div className = "py-1.5 px-3 bg-[#1e1e1e] border-b border-[#3e3e42] text-[11px] text-[#888] font-monospace" >
278+ { commonPrefix } /
279+ </ div >
280+ ) }
281+ < div className = "flex-1 min-h-0 p-3 overflow-y-auto font-monospace text-xs" >
381282 { isLoading && ! startNode ? (
382- < EmptyState > Loading file tree...</ EmptyState >
283+ < div className = "py-5 text-[#888] text-center" > Loading file tree...</ div >
383284 ) : startNode ? (
384285 startNode . children . map ( ( child ) => (
385286 < TreeNodeContent
@@ -395,9 +296,9 @@ export const FileTree: React.FC<FileTreeExternalProps> = ({
395296 />
396297 ) )
397298 ) : (
398- < EmptyState > No files changed</ EmptyState >
299+ < div className = "py-5 text-[#888] text-center" > No files changed</ div >
399300 ) }
400- </ TreeContainer >
301+ </ div >
401302 </ >
402303 ) ;
403304} ;
0 commit comments