@@ -6,6 +6,7 @@ import { searchNpm, parseDependencies, addDependency, removeDependency, type Npm
66import { fileIcon } from '../../store' ;
77import { DEFAULT_EDITORS } from '../types' ;
88import type { FiddleFile } from '../state/FiddleState' ;
9+ import { buildCompactFileTree , type FileTreeNode } from './file-tree' ;
910import './FiddleSidebar.css' ;
1011
1112// Upstream sidebar-file-tree validation: supported editor extensions only,
@@ -42,6 +43,7 @@ export interface FiddleSidebarProps {
4243 */
4344export function FiddleSidebar ( props : FiddleSidebarProps ) {
4445 const editors = Array . from ( props . files . values ( ) ) . sort ( ( a , b ) => a . id . localeCompare ( b . id ) ) ;
46+ const editorTree = buildCompactFileTree ( editors . map ( editor => editor . id ) ) ;
4547 const [ moduleQuery , setModuleQuery ] = useState ( '' ) ;
4648 const [ searchResults , setSearchResults ] = useState < NpmSearchResult [ ] > ( [ ] ) ;
4749 const [ searching , setSearching ] = useState ( false ) ;
@@ -50,6 +52,7 @@ export function FiddleSidebar(props: FiddleSidebarProps) {
5052 // the native editors never need to detach for either flow.
5153 const [ addingName , setAddingName ] = useState < string | null > ( null ) ;
5254 const [ renaming , setRenaming ] = useState < { id : string ; name : string } | null > ( null ) ;
55+ const [ collapsedFolders , setCollapsedFolders ] = useState < Set < string > > ( new Set ( ) ) ;
5356 const addError = addingName && addingName . trim ( )
5457 ? validateNewFileName ( addingName . trim ( ) , editors . map ( e => e . id ) )
5558 : null ;
@@ -110,6 +113,121 @@ export function FiddleSidebar(props: FiddleSidebarProps) {
110113 // eslint-disable-next-line react-hooks/exhaustive-deps
111114 } , [ packageJson , props . onSetFileContent ] ) ;
112115
116+ const toggleFolder = useCallback ( ( path : string ) => {
117+ setCollapsedFolders ( previous => {
118+ const next = new Set ( previous ) ;
119+ if ( next . has ( path ) ) next . delete ( path ) ;
120+ else next . add ( path ) ;
121+ return next ;
122+ } ) ;
123+ } , [ ] ) ;
124+
125+ const renderEditorNode = ( node : FileTreeNode , depth : number ) : any => {
126+ const indent = 10 + depth * 14 ;
127+ if ( node . kind === 'folder' ) {
128+ const collapsed = collapsedFolders . has ( node . path ) ;
129+ return (
130+ < view key = { `folder:${ node . path } ` } className = "FiddleSidebar-TreeGroup" >
131+ < view
132+ className = "FiddleSidebar-FolderRow"
133+ style = { { paddingLeft : `${ indent } px` } }
134+ bindtap = { ( ) => toggleFolder ( node . path ) }
135+ >
136+ < Icon
137+ icon = { collapsed ? 'chevron-right' : 'chevron-down' }
138+ size = { 10 }
139+ className = "FiddleSidebar-FolderChevron"
140+ />
141+ < Icon
142+ icon = { collapsed ? 'folder-close' : 'folder-open' }
143+ size = { 13 }
144+ className = "FiddleSidebar-TreeFolderIcon"
145+ />
146+ < text className = "FiddleSidebar-FolderName" text-maxline = "1" > { node . name } </ text >
147+ </ view >
148+ { collapsed ? null : node . children . map ( child => renderEditorNode ( child , depth + 1 ) ) }
149+ </ view >
150+ ) ;
151+ }
152+
153+ const f = props . files . get ( node . id ) ;
154+ if ( ! f ) return null ;
155+ const isActive = f . id === props . activeEditorId ;
156+ const canDelete = f . id !== DEFAULT_EDITORS . MAIN && f . id !== DEFAULT_EDITORS . PACKAGE ;
157+ const cls = 'FiddleSidebar-Item'
158+ + ( isActive ? ' FiddleSidebar-Item--active' : '' )
159+ + ( f . isDirty ? ' FiddleSidebar-Item--dirty' : '' ) ;
160+
161+ if ( renaming ?. id === f . id ) {
162+ return (
163+ < view
164+ key = { f . id }
165+ className = "FiddleSidebar-AddRow FiddleSidebar-AddRow--tree"
166+ style = { { marginLeft : `${ indent + 10 } px` } }
167+ >
168+ < view className = "FiddleSidebar-AddRowInput" >
169+ < text className = "FiddleSidebar-ItemGlyph" > { fileIcon ( renaming . name || f . id ) } </ text >
170+ < InputGroup
171+ fill
172+ placeholder = { f . id }
173+ value = { renaming . name }
174+ onChange = { ( v ) => setRenaming ( r => ( r ? { ...r , name : v } : r ) ) }
175+ onSubmit = { commitRename }
176+ />
177+ < view className = "FiddleSidebar-EyeBtn" catchtap = { ( ) => commitRename ( renaming . name ) } >
178+ < Icon icon = "tick" size = { 14 } color = "#9feafa" />
179+ </ view >
180+ < view className = "FiddleSidebar-EyeBtn" catchtap = { ( ) => setRenaming ( null ) } >
181+ < Icon icon = "cross" size = { 14 } color = "#a7b6c2" />
182+ </ view >
183+ </ view >
184+ { renameError ? < text className = "FiddleSidebar-AddRowError" > { renameError } </ text > : null }
185+ </ view >
186+ ) ;
187+ }
188+
189+ return (
190+ < view
191+ key = { f . id }
192+ className = { cls }
193+ style = { { paddingLeft : `${ indent + 16 } px` } }
194+ bindtap = { ( ) => props . onSelectEditor ( f . id ) }
195+ >
196+ < text className = "FiddleSidebar-ItemGlyph" > { fileIcon ( f . id ) } </ text >
197+ < view className = "FiddleSidebar-ItemLabel" >
198+ < text className = "FiddleSidebar-ItemName" text-maxline = "1" > { node . name } </ text >
199+ </ view >
200+ { f . isDirty ? < text className = "FiddleSidebar-Dot" > ●</ text > : null }
201+ < view className = "FiddleSidebar-RowActions" >
202+ { isActive ? (
203+ < view className = "FiddleSidebar-ActiveActions" >
204+ { f . id !== DEFAULT_EDITORS . PACKAGE ? (
205+ < view
206+ className = "FiddleSidebar-EyeBtn"
207+ catchtap = { ( ) => setRenaming ( { id : f . id , name : f . id } ) }
208+ >
209+ < Icon icon = "edit" size = { 12 } color = "#8ac7d6" />
210+ </ view >
211+ ) : null }
212+ { canDelete ? (
213+ < view className = "FiddleSidebar-EyeBtn" catchtap = { ( ) => props . onRemoveFile ( f . id ) } >
214+ < Icon icon = "trash" size = { 12 } color = "#df3434" />
215+ </ view >
216+ ) : null }
217+ </ view >
218+ ) : null }
219+ < view className = "FiddleSidebar-EyeBtn" catchtap = { ( ) => props . onToggleEditor ( f . id ) } >
220+ < Icon
221+ icon = { f . visible ? 'eye-open' : 'eye-off' }
222+ size = { 14 }
223+ color = { f . visible ? '#dcdcdc' : '#5c5f71' }
224+ />
225+ </ view >
226+ </ view >
227+ </ view >
228+ ) ;
229+ } ;
230+
113231 return (
114232 < view className = "FiddleSidebar" >
115233 { /* Editors and Modules are two panels sharing one column, and the line
@@ -132,97 +250,7 @@ export function FiddleSidebar(props: FiddleSidebarProps) {
132250 </ view >
133251 </ view >
134252 < scroll-view className = "FiddleSidebar-List" scroll-orientation = "vertical" >
135- { editors . map ( f => {
136- const isActive = f . id === props . activeEditorId ;
137- const canDelete = f . id !== DEFAULT_EDITORS . MAIN && f . id !== DEFAULT_EDITORS . PACKAGE ;
138- const cls = 'FiddleSidebar-Item'
139- + ( isActive ? ' FiddleSidebar-Item--active' : '' )
140- + ( f . isDirty ? ' FiddleSidebar-Item--dirty' : '' ) ;
141- if ( renaming ?. id === f . id ) {
142- return (
143- < view key = { f . id } className = "FiddleSidebar-AddRow" >
144- < view className = "FiddleSidebar-AddRowInput" >
145- { /* Of the name being typed, not of the file as it stands: rename
146- main.js to main.css and the glyph turns over before you commit,
147- which is the cheapest possible confirmation that the extension
148- landed the way you meant it to. */ }
149- < text className = "FiddleSidebar-ItemGlyph" > { fileIcon ( renaming . name || f . id ) } </ text >
150- < InputGroup
151- fill
152- placeholder = { f . id }
153- value = { renaming . name }
154- onChange = { ( v ) => setRenaming ( r => ( r ? { ...r , name : v } : r ) ) }
155- onSubmit = { commitRename }
156- />
157- < view className = "FiddleSidebar-EyeBtn" catchtap = { ( ) => commitRename ( renaming . name ) } >
158- < Icon icon = "tick" size = { 14 } color = "#9feafa" />
159- </ view >
160- < view className = "FiddleSidebar-EyeBtn" catchtap = { ( ) => setRenaming ( null ) } >
161- < Icon icon = "cross" size = { 14 } color = "#a7b6c2" />
162- </ view >
163- </ view >
164- { renameError ? (
165- < text className = "FiddleSidebar-AddRowError" > { renameError } </ text >
166- ) : null }
167- </ view >
168- ) ;
169- }
170- return (
171- < view
172- key = { f . id }
173- className = { cls }
174- bindtap = { ( ) => props . onSelectEditor ( f . id ) }
175- >
176- { /* The same glyph Quick Open uses for the same file. Two file
177- lists in one app were speaking two icon languages — a tinted
178- monochrome document here, an emoji there — and the one you
179- reach for by keyboard is not the one you reach for by eye, so
180- the mismatch showed up every time you used both. One list,
181- one language; `fileIcon` is the single map. */ }
182- < text className = "FiddleSidebar-ItemGlyph" > { fileIcon ( f . id ) } </ text >
183- < view className = "FiddleSidebar-ItemLabel" >
184- < text className = "FiddleSidebar-ItemName" text-maxline = "1" > { f . id } </ text >
185- </ view >
186- { f . isDirty ? < text className = "FiddleSidebar-Dot" > ●</ text > : null }
187- { /* rename/delete only on the active row — upstream uses a
188- right-click context menu, which Lynx doesn't deliver, and
189- CSS :hover display-flips leave stale paint behind (icons
190- ghost over the eye toggle after unhover) */ }
191- < view className = "FiddleSidebar-RowActions" >
192- { isActive ? (
193- < view className = "FiddleSidebar-ActiveActions" >
194- { f . id !== DEFAULT_EDITORS . PACKAGE ? (
195- < view
196- className = "FiddleSidebar-EyeBtn"
197- catchtap = { ( ) => setRenaming ( { id : f . id , name : f . id } ) }
198- >
199- < Icon icon = "edit" size = { 12 } color = "#8ac7d6" />
200- </ view >
201- ) : null }
202- { canDelete ? (
203- < view
204- className = "FiddleSidebar-EyeBtn"
205- catchtap = { ( ) => props . onRemoveFile ( f . id ) }
206- >
207- < Icon icon = "trash" size = { 12 } color = "#df3434" />
208- </ view >
209- ) : null }
210- </ view >
211- ) : null }
212- < view
213- className = "FiddleSidebar-EyeBtn"
214- catchtap = { ( ) => props . onToggleEditor ( f . id ) }
215- >
216- < Icon
217- icon = { f . visible ? 'eye-open' : 'eye-off' }
218- size = { 14 }
219- color = { f . visible ? '#dcdcdc' : '#5c5f71' }
220- />
221- </ view >
222- </ view >
223- </ view >
224- ) ;
225- } ) }
253+ { editorTree . map ( node => renderEditorNode ( node , 0 ) ) }
226254 { addingName != null ? (
227255 < view className = "FiddleSidebar-AddRow" >
228256 < view className = "FiddleSidebar-AddRowInput" >
0 commit comments