1- import { useState , useRef , useCallback } from '@lynx-js/react' ;
1+ import { useState , useRef , useCallback , useEffect } from '@lynx-js/react' ;
22import { Button , Icon , InputGroup , AppToaster } from '../bp' ;
33import { isSafeRelativePath } from '../state/FiddleState' ;
44import { searchNpm , parseDependencies , addDependency , removeDependency , type NpmSearchResult } from './npm-search' ;
5+ import { flattenFileTree } from './file-tree' ;
56import { DEFAULT_EDITORS } from '../types' ;
67import type { FiddleFile } from '../state/FiddleState' ;
78import './FiddleSidebar.css' ;
@@ -39,6 +40,7 @@ export interface FiddleSidebarProps {
3940 */
4041export function FiddleSidebar ( props : FiddleSidebarProps ) {
4142 const editors = Array . from ( props . files . values ( ) ) . sort ( ( a , b ) => a . id . localeCompare ( b . id ) ) ;
43+ const [ collapsedDirectories , setCollapsedDirectories ] = useState < Set < string > > ( ( ) => new Set ( ) ) ;
4244 const [ moduleQuery , setModuleQuery ] = useState ( '' ) ;
4345 const [ searchResults , setSearchResults ] = useState < NpmSearchResult [ ] > ( [ ] ) ;
4446 const [ searching , setSearching ] = useState ( false ) ;
@@ -106,6 +108,24 @@ export function FiddleSidebar(props: FiddleSidebarProps) {
106108 if ( next ) props . onSetFileContent ( DEFAULT_EDITORS . PACKAGE , next ) ;
107109 // eslint-disable-next-line react-hooks/exhaustive-deps
108110 } , [ packageJson , props . onSetFileContent ] ) ;
111+ const toggleDirectory = useCallback ( ( path : string ) => {
112+ setCollapsedDirectories ( previous => {
113+ const next = new Set ( previous ) ;
114+ if ( next . has ( path ) ) next . delete ( path ) ;
115+ else next . add ( path ) ;
116+ return next ;
117+ } ) ;
118+ } , [ ] ) ;
119+
120+ // A Gallery case owns its own tree state. Do not carry collapsed paths or an
121+ // inline add/rename operation into the next showcase.
122+ useEffect ( ( ) => {
123+ setCollapsedDirectories ( new Set ( ) ) ;
124+ setAddingName ( null ) ;
125+ setRenaming ( null ) ;
126+ } , [ props . rootPath ] ) ;
127+
128+ const treeRows = flattenFileTree ( editors . map ( editor => editor . id ) , collapsedDirectories ) ;
109129
110130 return (
111131 < view className = "FiddleSidebar" >
@@ -122,15 +142,44 @@ export function FiddleSidebar(props: FiddleSidebarProps) {
122142 </ view >
123143 </ view >
124144 < scroll-view className = "FiddleSidebar-List" scroll-orientation = "vertical" >
125- { editors . map ( f => {
145+ { treeRows . map ( row => {
146+ if ( row . kind === 'directory' ) {
147+ return (
148+ < view
149+ key = { `directory:${ row . path } ` }
150+ className = "FiddleSidebar-Directory"
151+ style = { { paddingLeft : `${ 12 + row . depth * 14 } px` } }
152+ bindtap = { ( ) => toggleDirectory ( row . path ) }
153+ >
154+ < Icon
155+ icon = { row . expanded ? 'chevron-down' : 'chevron-right' }
156+ size = { 11 }
157+ className = "FiddleSidebar-DirectoryChevron"
158+ />
159+ < Icon
160+ icon = { row . expanded ? 'folder-open' : 'folder-close' }
161+ size = { 14 }
162+ className = "FiddleSidebar-DirectoryIcon"
163+ />
164+ < text className = "FiddleSidebar-DirectoryName" text-maxline = "1" > { row . name } </ text >
165+ </ view >
166+ ) ;
167+ }
168+
169+ const f = props . files . get ( row . path ) ;
170+ if ( ! f ) return null ;
126171 const isActive = f . id === props . activeEditorId ;
127172 const canDelete = f . id !== DEFAULT_EDITORS . MAIN && f . id !== DEFAULT_EDITORS . PACKAGE ;
128173 const cls = 'FiddleSidebar-Item'
129174 + ( isActive ? ' FiddleSidebar-Item--active' : '' )
130175 + ( f . isDirty ? ' FiddleSidebar-Item--dirty' : '' ) ;
131176 if ( renaming ?. id === f . id ) {
132177 return (
133- < view key = { f . id } className = "FiddleSidebar-AddRow" >
178+ < view
179+ key = { f . id }
180+ className = "FiddleSidebar-AddRow"
181+ style = { { paddingLeft : `${ 8 + row . depth * 14 } px` } }
182+ >
134183 < view className = "FiddleSidebar-AddRowInput" >
135184 < Icon icon = "document" size = { 14 } className = "FiddleSidebar-ItemIcon" />
136185 < InputGroup
@@ -157,11 +206,12 @@ export function FiddleSidebar(props: FiddleSidebarProps) {
157206 < view
158207 key = { f . id }
159208 className = { cls }
209+ style = { { paddingLeft : `${ ( isActive ? 9 : 12 ) + row . depth * 14 } px` } }
160210 bindtap = { ( ) => props . onSelectEditor ( f . id ) }
161211 >
162212 < Icon icon = "document" size = { 14 } className = "FiddleSidebar-ItemIcon" />
163213 < view className = "FiddleSidebar-ItemLabel" >
164- < text className = "FiddleSidebar-ItemName" text-maxline = "1" > { f . id } </ text >
214+ < text className = "FiddleSidebar-ItemName" text-maxline = "1" > { row . name } </ text >
165215 </ view >
166216 { f . isDirty ? < text className = "FiddleSidebar-Dot" > ●</ text > : null }
167217 { /* rename/delete only on the active row — upstream uses a
0 commit comments