@@ -2,6 +2,7 @@ import { memo, useEffect, useMemo, useState, type ReactNode } from 'react';
22import type { FileMap } from '~/lib/stores/files' ;
33import { classNames } from '~/utils/classNames' ;
44import { createScopedLogger , renderLogger } from '~/utils/logger' ;
5+ import * as ContextMenu from '@radix-ui/react-context-menu' ;
56
67const logger = createScopedLogger ( 'FileTree' ) ;
78
@@ -110,6 +111,22 @@ export const FileTree = memo(
110111 } ) ;
111112 } ;
112113
114+ const onCopyPath = ( fileOrFolder : FileNode | FolderNode ) => {
115+ try {
116+ navigator . clipboard . writeText ( fileOrFolder . fullPath ) ;
117+ } catch ( error ) {
118+ logger . error ( error ) ;
119+ }
120+ } ;
121+
122+ const onCopyRelativePath = ( fileOrFolder : FileNode | FolderNode ) => {
123+ try {
124+ navigator . clipboard . writeText ( fileOrFolder . fullPath . substring ( ( rootFolder || '' ) . length ) ) ;
125+ } catch ( error ) {
126+ logger . error ( error ) ;
127+ }
128+ } ;
129+
113130 return (
114131 < div className = { classNames ( 'text-sm' , className , 'overflow-y-auto' ) } >
115132 { filteredFileList . map ( ( fileOrFolder ) => {
@@ -121,6 +138,12 @@ export const FileTree = memo(
121138 selected = { selectedFile === fileOrFolder . fullPath }
122139 file = { fileOrFolder }
123140 unsavedChanges = { unsavedFiles ?. has ( fileOrFolder . fullPath ) }
141+ onCopyPath = { ( ) => {
142+ onCopyPath ( fileOrFolder ) ;
143+ } }
144+ onCopyRelativePath = { ( ) => {
145+ onCopyRelativePath ( fileOrFolder ) ;
146+ } }
124147 onClick = { ( ) => {
125148 onFileSelect ?.( fileOrFolder . fullPath ) ;
126149 } }
@@ -134,6 +157,12 @@ export const FileTree = memo(
134157 folder = { fileOrFolder }
135158 selected = { allowFolderSelection && selectedFile === fileOrFolder . fullPath }
136159 collapsed = { collapsedFolders . has ( fileOrFolder . fullPath ) }
160+ onCopyPath = { ( ) => {
161+ onCopyPath ( fileOrFolder ) ;
162+ } }
163+ onCopyRelativePath = { ( ) => {
164+ onCopyRelativePath ( fileOrFolder ) ;
165+ } }
137166 onClick = { ( ) => {
138167 toggleCollapseState ( fileOrFolder . fullPath ) ;
139168 } }
@@ -156,58 +185,111 @@ interface FolderProps {
156185 folder : FolderNode ;
157186 collapsed : boolean ;
158187 selected ?: boolean ;
188+ onCopyPath : ( ) => void ;
189+ onCopyRelativePath : ( ) => void ;
159190 onClick : ( ) => void ;
160191}
161192
162- function Folder ( { folder : { depth, name } , collapsed, selected = false , onClick } : FolderProps ) {
193+ interface FolderContextMenuProps {
194+ onCopyPath ?: ( ) => void ;
195+ onCopyRelativePath ?: ( ) => void ;
196+ children : ReactNode ;
197+ }
198+
199+ function ContextMenuItem ( { onSelect, children } : { onSelect ?: ( ) => void ; children : ReactNode } ) {
163200 return (
164- < NodeButton
165- className = { classNames ( 'group' , {
166- 'bg-transparent text-bolt-elements-item-contentDefault hover:text-bolt-elements-item-contentActive hover:bg-bolt-elements-item-backgroundActive' :
167- ! selected ,
168- 'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent' : selected ,
169- } ) }
170- depth = { depth }
171- iconClasses = { classNames ( {
172- 'i-ph:caret-right scale-98' : collapsed ,
173- 'i-ph:caret-down scale-98' : ! collapsed ,
174- } ) }
175- onClick = { onClick }
201+ < ContextMenu . Item
202+ onSelect = { onSelect }
203+ className = "flex items-center gap-2 px-2 py-1.5 outline-0 text-sm text-bolt-elements-textPrimary cursor-pointer ws-nowrap text-bolt-elements-item-contentDefault hover:text-bolt-elements-item-contentActive hover:bg-bolt-elements-item-backgroundActive rounded-md"
176204 >
177- { name }
178- </ NodeButton >
205+ < span className = "size-4 shrink-0" > </ span >
206+ < span > { children } </ span >
207+ </ ContextMenu . Item >
208+ ) ;
209+ }
210+
211+ function FileContextMenu ( { onCopyPath, onCopyRelativePath, children } : FolderContextMenuProps ) {
212+ return (
213+ < ContextMenu . Root >
214+ < ContextMenu . Trigger > { children } </ ContextMenu . Trigger >
215+ < ContextMenu . Portal >
216+ < ContextMenu . Content
217+ style = { { zIndex : 998 } }
218+ className = "border border-bolt-elements-borderColor rounded-md z-context-menu bg-bolt-elements-background-depth-1 dark:bg-bolt-elements-background-depth-2 data-[state=open]:animate-in animate-duration-100 data-[state=open]:fade-in-0 data-[state=open]:zoom-in-98 w-56"
219+ >
220+ < ContextMenu . Group className = "p-1 border-b-px border-solid border-bolt-elements-borderColor" >
221+ < ContextMenuItem onSelect = { onCopyPath } > Copy path</ ContextMenuItem >
222+ < ContextMenuItem onSelect = { onCopyRelativePath } > Copy relative path</ ContextMenuItem >
223+ </ ContextMenu . Group >
224+ </ ContextMenu . Content >
225+ </ ContextMenu . Portal >
226+ </ ContextMenu . Root >
227+ ) ;
228+ }
229+
230+ function Folder ( { folder, collapsed, selected = false , onCopyPath, onCopyRelativePath, onClick } : FolderProps ) {
231+ return (
232+ < FileContextMenu onCopyPath = { onCopyPath } onCopyRelativePath = { onCopyRelativePath } >
233+ < NodeButton
234+ className = { classNames ( 'group' , {
235+ 'bg-transparent text-bolt-elements-item-contentDefault hover:text-bolt-elements-item-contentActive hover:bg-bolt-elements-item-backgroundActive' :
236+ ! selected ,
237+ 'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent' : selected ,
238+ } ) }
239+ depth = { folder . depth }
240+ iconClasses = { classNames ( {
241+ 'i-ph:caret-right scale-98' : collapsed ,
242+ 'i-ph:caret-down scale-98' : ! collapsed ,
243+ } ) }
244+ onClick = { onClick }
245+ >
246+ { folder . name }
247+ </ NodeButton >
248+ </ FileContextMenu >
179249 ) ;
180250}
181251
182252interface FileProps {
183253 file : FileNode ;
184254 selected : boolean ;
185255 unsavedChanges ?: boolean ;
256+ onCopyPath : ( ) => void ;
257+ onCopyRelativePath : ( ) => void ;
186258 onClick : ( ) => void ;
187259}
188260
189- function File ( { file : { depth, name } , onClick, selected, unsavedChanges = false } : FileProps ) {
261+ function File ( {
262+ file : { depth, name } ,
263+ onClick,
264+ onCopyPath,
265+ onCopyRelativePath,
266+ selected,
267+ unsavedChanges = false ,
268+ } : FileProps ) {
190269 return (
191- < NodeButton
192- className = { classNames ( 'group' , {
193- 'bg-transparent hover:bg-bolt-elements-item-backgroundActive text-bolt-elements-item-contentDefault' : ! selected ,
194- 'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent' : selected ,
195- } ) }
196- depth = { depth }
197- iconClasses = { classNames ( 'i-ph:file-duotone scale-98' , {
198- 'group-hover:text-bolt-elements-item-contentActive' : ! selected ,
199- } ) }
200- onClick = { onClick }
201- >
202- < div
203- className = { classNames ( 'flex items-center' , {
270+ < FileContextMenu onCopyPath = { onCopyPath } onCopyRelativePath = { onCopyRelativePath } >
271+ < NodeButton
272+ className = { classNames ( 'group' , {
273+ 'bg-transparent hover:bg-bolt-elements-item-backgroundActive text-bolt-elements-item-contentDefault' :
274+ ! selected ,
275+ 'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent' : selected ,
276+ } ) }
277+ depth = { depth }
278+ iconClasses = { classNames ( 'i-ph:file-duotone scale-98' , {
204279 'group-hover:text-bolt-elements-item-contentActive' : ! selected ,
205280 } ) }
281+ onClick = { onClick }
206282 >
207- < div className = "flex-1 truncate pr-2" > { name } </ div >
208- { unsavedChanges && < span className = "i-ph:circle-fill scale-68 shrink-0 text-orange-500" /> }
209- </ div >
210- </ NodeButton >
283+ < div
284+ className = { classNames ( 'flex items-center' , {
285+ 'group-hover:text-bolt-elements-item-contentActive' : ! selected ,
286+ } ) }
287+ >
288+ < div className = "flex-1 truncate pr-2" > { name } </ div >
289+ { unsavedChanges && < span className = "i-ph:circle-fill scale-68 shrink-0 text-orange-500" /> }
290+ </ div >
291+ </ NodeButton >
292+ </ FileContextMenu >
211293 ) ;
212294}
213295
0 commit comments