Reduce React Doctor diagnostics in FilesView (#1271)

* Reduce React Doctor diagnostics in FilesView

* fix: address Greptile review feedback — use Base UI initialFocus and restore useMemo for displayedContent

- Replace useEffect-based dialog focus with Base UI Popup's initialFocus
  prop to avoid race with built-in focus management
- Drop unused no-op onOpenAutoFocus/onCloseAutoFocus from dialog wrapper
- Restore useMemo for displayedContent (200K char slice) to avoid
  per-render string allocation for large files
- Update DirectoryExplorerDialog to use initialFocus={false}
This commit is contained in:
Bohdan Triapitsyn
2026-05-17 23:31:28 +03:00
committed by GitHub
parent 6369cf76a7
commit 2290cf4b59
3 changed files with 189 additions and 158 deletions
@@ -707,7 +707,7 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
className="flex w-full max-w-xl flex-col gap-0 overflow-hidden p-0 sm:max-h-[80vh]"
onOpenAutoFocus={(event) => event.preventDefault()}
initialFocus={false}
>
<DialogHeader className="px-5 pb-2 pt-5">
<div className="flex items-start justify-between gap-4">
-6
View File
@@ -84,20 +84,14 @@ DialogOverlay.displayName = "DialogOverlay";
type DialogContentProps = Omit<React.ComponentProps<typeof BaseDialog.Popup>, "children"> & {
showCloseButton?: boolean
children?: React.ReactNode
onOpenAutoFocus?: (event: Event) => void
onCloseAutoFocus?: (event: Event) => void
}
function DialogContent({
className,
children,
showCloseButton = true,
onOpenAutoFocus,
onCloseAutoFocus,
...props
}: DialogContentProps) {
void onOpenAutoFocus
void onCloseAutoFocus
const { t } = useI18n()
return (
+188 -151
View File
@@ -108,7 +108,7 @@ const OpenInAppListIcon = ({ label, iconDataUrl }: { label: string; iconDataUrl?
<img
src={iconDataUrl}
alt=""
className="h-4 w-4 rounded-sm"
className="size-4 rounded-sm"
onError={() => setFailed(true)}
/>
);
@@ -117,7 +117,7 @@ const OpenInAppListIcon = ({ label, iconDataUrl }: { label: string; iconDataUrl?
return (
<span
className={cn(
'h-4 w-4 rounded-sm flex items-center justify-center',
'size-4 rounded-sm flex items-center justify-center',
'bg-[var(--surface-muted)] text-[9px] font-medium text-muted-foreground'
)}
>
@@ -221,7 +221,7 @@ const FileStatusDot: React.FC<{ status: FileStatus }> = ({ status }) => {
'git-deleted': 'var(--status-error)',
}[status];
return <span className="h-2 w-2 rounded-full" style={{ backgroundColor: color }} />;
return <span className="size-2 rounded-full" style={{ backgroundColor: color }} />;
};
const shouldIgnoreEntryName = (name: string): boolean => DEFAULT_IGNORED_DIR_NAMES.has(name);
@@ -358,9 +358,9 @@ const FileRow: React.FC<FileRowProps> = ({
>
{isDir ? (
isExpanded ? (
<Icon name="folder-open-fill" className="h-4 w-4 flex-shrink-0 text-primary/60" />
<Icon name="folder-open-fill" className="size-4 flex-shrink-0 text-primary/60" />
) : (
<Icon name="folder-3-fill" className="h-4 w-4 flex-shrink-0 text-primary/60" />
<Icon name="folder-3-fill" className="size-4 flex-shrink-0 text-primary/60" />
)
) : (
getFileIcon(node.path, node.extension)
@@ -392,16 +392,16 @@ const FileRow: React.FC<FileRowProps> = ({
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
className="size-6"
onClick={handleMenuButtonClick}
>
<Icon name="more-2-fill" className="h-4 w-4" />
<Icon name="more-2-fill" className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" side={isMobile ? "bottom" : "bottom"} onCloseAutoFocus={() => setContextMenuPath(null)}>
{canRename && (
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); onOpenDialog('rename', node); }}>
<Icon name="edit" className="mr-2 h-4 w-4" /> {t('sidebarFilesTree.menu.rename')}
<Icon name="edit" className="mr-2 size-4" /> {t('sidebarFilesTree.menu.rename')}
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={(e) => {
@@ -414,7 +414,7 @@ const FileRow: React.FC<FileRowProps> = ({
toast.error(t('sidebarFilesTree.toast.copyFailed'));
});
}}>
<Icon name="file-copy" className="mr-2 h-4 w-4" /> {t('sidebarFilesTree.menu.copyPath')}
<Icon name="file-copy" className="mr-2 size-4" /> {t('sidebarFilesTree.menu.copyPath')}
</DropdownMenuItem>
<DropdownMenuItem onClick={(e) => {
e.stopPropagation();
@@ -427,19 +427,19 @@ const FileRow: React.FC<FileRowProps> = ({
toast.error(t('sidebarFilesTree.toast.copyFailed'));
});
}}>
<Icon name="file-copy-2" className="mr-2 h-4 w-4" /> {t('filesView.tree.menu.copyRelativePath')}
<Icon name="file-copy-2" className="mr-2 size-4" /> {t('filesView.tree.menu.copyRelativePath')}
</DropdownMenuItem>
{!isDir && downloadFile && (
<DropdownMenuItem onClick={(e) => {
e.stopPropagation();
void downloadFile(node.path);
}}>
<Icon name="download" className="mr-2 h-4 w-4" /> {t('sidebarFilesTree.menu.save')}
<Icon name="download" className="mr-2 size-4" /> {t('sidebarFilesTree.menu.save')}
</DropdownMenuItem>
)}
{canReveal && (
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); onRevealPath(node.path); }}>
<Icon name="folder-received" className="mr-2 h-4 w-4" /> {t(getRevealLabelKey())}
<Icon name="folder-received" className="mr-2 size-4" /> {t(getRevealLabelKey())}
</DropdownMenuItem>
)}
{isDir && (canCreateFile || canCreateFolder) && (
@@ -447,12 +447,12 @@ const FileRow: React.FC<FileRowProps> = ({
<DropdownMenuSeparator />
{canCreateFile && (
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); onOpenDialog('createFile', node); }}>
<Icon name="file-add" className="mr-2 h-4 w-4" /> {t('sidebarFilesTree.menu.newFile')}
<Icon name="file-add" className="mr-2 size-4" /> {t('sidebarFilesTree.menu.newFile')}
</DropdownMenuItem>
)}
{canCreateFolder && (
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); onOpenDialog('createFolder', node); }}>
<Icon name="folder-add" className="mr-2 h-4 w-4" /> {t('sidebarFilesTree.menu.newFolder')}
<Icon name="folder-add" className="mr-2 size-4" /> {t('sidebarFilesTree.menu.newFolder')}
</DropdownMenuItem>
)}
</>
@@ -464,7 +464,7 @@ const FileRow: React.FC<FileRowProps> = ({
onClick={(e) => { e.stopPropagation(); onOpenDialog('delete', node); }}
className="text-destructive focus:text-destructive"
>
<Icon name="delete-bin" className="mr-2 h-4 w-4" /> {t('sidebarFilesTree.menu.delete')}
<Icon name="delete-bin" className="mr-2 size-4" /> {t('sidebarFilesTree.menu.delete')}
</DropdownMenuItem>
</>
)}
@@ -476,6 +476,82 @@ const FileRow: React.FC<FileRowProps> = ({
);
};
interface DialogsProps {
activeDialog: 'createFile' | 'createFolder' | 'rename' | 'delete' | null;
dialogData: { path: string; name?: string; type?: 'file' | 'directory' } | null;
dialogInputValue: string;
onDialogInputChange: (value: string) => void;
isDialogSubmitting: boolean;
onDialogSubmit: (e?: React.FormEvent) => Promise<void>;
onClose: () => void;
inputRef: React.RefObject<HTMLInputElement | null>;
}
const Dialogs: React.FC<DialogsProps> = ({
activeDialog,
dialogData,
dialogInputValue,
onDialogInputChange,
isDialogSubmitting,
onDialogSubmit,
onClose,
inputRef,
}) => {
const { t } = useI18n();
return (
<Dialog open={!!activeDialog} onOpenChange={(open) => !open && onClose()}>
<DialogContent initialFocus={inputRef}>
<DialogHeader>
<DialogTitle>
{activeDialog === 'createFile' && t('filesView.dialog.createFile.title')}
{activeDialog === 'createFolder' && t('filesView.dialog.createFolder.title')}
{activeDialog === 'rename' && t('filesView.dialog.rename.title')}
{activeDialog === 'delete' && t('filesView.dialog.delete.title')}
</DialogTitle>
<DialogDescription>
{activeDialog === 'createFile' && t('filesView.dialog.createFile.description', { path: dialogData?.path ?? t('filesView.dialog.rootFallback') })}
{activeDialog === 'createFolder' && t('filesView.dialog.createFolder.description', { path: dialogData?.path ?? t('filesView.dialog.rootFallback') })}
{activeDialog === 'rename' && t('filesView.dialog.rename.description', { name: dialogData?.name ?? '' })}
{activeDialog === 'delete' && t('filesView.dialog.delete.description', { name: dialogData?.name ?? '' })}
</DialogDescription>
</DialogHeader>
{activeDialog !== 'delete' && (
<div className="py-4">
<Input
value={dialogInputValue}
onChange={(e) => onDialogInputChange(e.target.value)}
placeholder={activeDialog === 'rename' ? t('filesView.dialog.rename.placeholder') : t('filesView.dialog.namePlaceholder')}
onKeyDown={(e) => {
if (e.key === 'Enter') {
void onDialogSubmit();
}
}}
ref={inputRef}
/>
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={onClose} disabled={isDialogSubmitting}>
{t('filesView.dialog.cancel')}
</Button>
<Button
variant={activeDialog === 'delete' ? 'destructive' : 'default'}
onClick={() => void onDialogSubmit()}
disabled={isDialogSubmitting || (activeDialog !== 'delete' && !dialogInputValue.trim())}
>
{isDialogSubmitting ? <Icon name="loader-4" className="animate-spin" /> : (
activeDialog === 'delete' ? t('filesView.dialog.delete.confirm') : t('filesView.dialog.confirm')
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
interface FilesViewProps {
mode?: 'full' | 'editor-only';
}
@@ -602,18 +678,21 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
right: el.scrollLeft + el.clientWidth < el.scrollWidth - 2,
});
}, []);
const updateEditorTabsOverflowRef = React.useRef(updateEditorTabsOverflow);
updateEditorTabsOverflowRef.current = updateEditorTabsOverflow;
React.useEffect(() => {
const el = editorTabsScrollRef.current;
if (!el) return;
updateEditorTabsOverflow();
el.addEventListener('scroll', updateEditorTabsOverflow, { passive: true });
const ro = new ResizeObserver(updateEditorTabsOverflow);
const handler = () => updateEditorTabsOverflowRef.current();
handler();
el.addEventListener('scroll', handler, { passive: true });
const ro = new ResizeObserver(handler);
ro.observe(el);
return () => {
el.removeEventListener('scroll', updateEditorTabsOverflow);
el.removeEventListener('scroll', handler);
ro.disconnect();
};
}, [updateEditorTabsOverflow, openFiles.length]);
}, [openFiles.length]);
const [childrenByDir, setChildrenByDir] = React.useState<Record<string, FileNode[]>>({});
const loadedDirsRef = React.useRef<Set<string>>(new Set());
@@ -633,6 +712,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
const [draftContent, setDraftContent] = React.useState('');
const [isSaving, setIsSaving] = React.useState(false);
const dialogInputRef = React.useRef<HTMLInputElement>(null);
const autoSaveTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
const lastLoadedFileStatRef = React.useRef<FileStatSnapshot | null>(null);
const activeFileLoadIdRef = React.useRef(0);
@@ -836,27 +916,22 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
}, [lineSelection, saveComment]);
const mapDirectoryEntries = React.useCallback((dirPath: string, entries: Array<{ name: string; path: string; isDirectory: boolean }>): FileNode[] => {
const nodes = entries
.filter((entry) => entry && typeof entry.name === 'string' && entry.name.length > 0)
.filter((entry) => showHidden || !entry.name.startsWith('.'))
.filter((entry) => showGitignored || !shouldIgnoreEntryName(entry.name))
.map<FileNode>((entry) => {
const name = entry.name;
const normalizedEntryPath = normalizePath(entry.path || '');
const path = normalizedEntryPath
? (isAbsolutePath(normalizedEntryPath)
? normalizedEntryPath
: normalizePath(`${dirPath}/${normalizedEntryPath}`))
: normalizePath(`${dirPath}/${name}`);
const type = entry.isDirectory ? 'directory' : 'file';
const extension = type === 'file' && name.includes('.') ? name.split('.').pop()?.toLowerCase() : undefined;
return {
name,
path,
type,
extension,
};
});
const nodes: FileNode[] = [];
for (const entry of entries) {
if (!(entry && typeof entry.name === 'string' && entry.name.length > 0)) continue;
if (!showHidden && entry.name.startsWith('.')) continue;
if (!showGitignored && shouldIgnoreEntryName(entry.name)) continue;
const name = entry.name;
const normalizedEntryPath = normalizePath(entry.path || '');
const path = normalizedEntryPath
? (isAbsolutePath(normalizedEntryPath)
? normalizedEntryPath
: normalizePath(`${dirPath}/${normalizedEntryPath}`))
: normalizePath(`${dirPath}/${name}`);
const type = entry.isDirectory ? 'directory' : 'file';
const extension = type === 'file' && name.includes('.') ? name.split('.').pop()?.toLowerCase() : undefined;
nodes.push({ name, path, type, extension });
}
return sortNodes(nodes);
}, [showGitignored, showHidden]);
@@ -1272,13 +1347,14 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
return null;
}, [files]);
const displayedContent = React.useMemo(() => {
return fileContent.length > MAX_VIEW_CHARS
const displayedContent = React.useMemo(() =>
fileContent.length > MAX_VIEW_CHARS
? `${fileContent.slice(0, MAX_VIEW_CHARS)}\n\n… truncated …`
: fileContent;
}, [fileContent]);
: fileContent,
[fileContent]
);
const isDirty = React.useMemo(() => draftContent !== displayedContent, [draftContent, displayedContent]);
const isDirty = draftContent !== displayedContent;
const saveDraft = React.useCallback(async () => {
if (!selectedFile || !files.writeFile) {
@@ -1540,11 +1616,13 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
expandPaths(root, pathsToExpand);
}
for (const path of pathsToExpand) {
const loadPromises = pathsToExpand.map((path) => {
if (!loadedDirsRef.current.has(path)) {
await loadDirectory(path);
return loadDirectory(path);
}
}
return undefined;
}).filter(Boolean);
await Promise.all(loadPromises);
}, [expandPaths, loadDirectory, root]);
const getNextOpenFile = React.useCallback((path: string, filesList: FileNode[]) => {
@@ -2293,7 +2371,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
const viewport = window.visualViewport;
viewport?.addEventListener('resize', runNudge);
viewport?.addEventListener('scroll', runNudge);
viewport?.addEventListener('scroll', runNudge, { passive: true });
document.addEventListener('selectionchange', runNudge);
return () => {
@@ -2431,57 +2509,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
};
}, [files, isSelectedImage, isSelectedSvg, runtime.isDesktop, selectedFile?.path, selectedFileReadOptions, t]);
const renderDialogs = () => (
<Dialog open={!!activeDialog} onOpenChange={(open) => !open && setActiveDialog(null)}>
<DialogContent>
<DialogHeader>
<DialogTitle>
{activeDialog === 'createFile' && t('filesView.dialog.createFile.title')}
{activeDialog === 'createFolder' && t('filesView.dialog.createFolder.title')}
{activeDialog === 'rename' && t('filesView.dialog.rename.title')}
{activeDialog === 'delete' && t('filesView.dialog.delete.title')}
</DialogTitle>
<DialogDescription>
{activeDialog === 'createFile' && t('filesView.dialog.createFile.description', { path: dialogData?.path ?? t('filesView.dialog.rootFallback') })}
{activeDialog === 'createFolder' && t('filesView.dialog.createFolder.description', { path: dialogData?.path ?? t('filesView.dialog.rootFallback') })}
{activeDialog === 'rename' && t('filesView.dialog.rename.description', { name: dialogData?.name ?? '' })}
{activeDialog === 'delete' && t('filesView.dialog.delete.description', { name: dialogData?.name ?? '' })}
</DialogDescription>
</DialogHeader>
{activeDialog !== 'delete' && (
<div className="py-4">
<Input
value={dialogInputValue}
onChange={(e) => setDialogInputValue(e.target.value)}
placeholder={activeDialog === 'rename' ? t('filesView.dialog.rename.placeholder') : t('filesView.dialog.namePlaceholder')}
onKeyDown={(e) => {
if (e.key === 'Enter') {
void handleDialogSubmit();
}
}}
autoFocus
/>
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={() => setActiveDialog(null)} disabled={isDialogSubmitting}>
{t('filesView.dialog.cancel')}
</Button>
<Button
variant={activeDialog === 'delete' ? 'destructive' : 'default'}
onClick={() => void handleDialogSubmit()}
disabled={isDialogSubmitting || (activeDialog !== 'delete' && !dialogInputValue.trim())}
>
{isDialogSubmitting ? <Icon name="loader-4" className="animate-spin" /> : (
activeDialog === 'delete' ? t('filesView.dialog.delete.confirm') : t('filesView.dialog.confirm')
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
const handleCloseDialog = React.useCallback(() => setActiveDialog(null), []);
const blockWidgets = React.useMemo(() => {
return buildCodeMirrorCommentWidgets({
@@ -2539,12 +2567,12 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
<>
{isSaving ? (
<span className="flex items-center gap-1 px-1 text-muted-foreground typography-meta">
<Icon name="loader-4" className="h-3.5 w-3.5 animate-spin" />
<Icon name="loader-4" className="size-3.5 animate-spin" />
{t('filesView.editor.saving')}
</span>
) : autoSaveEnabled && autoSaveStatus === 'saved' && !isDirty ? (
<span className="flex items-center gap-1 px-1 text-[color:var(--status-success)] typography-meta">
<Icon name="check" className="h-3.5 w-3.5" />
<Icon name="check" className="size-3.5" />
{t('filesView.editor.saved')}
</span>
) : isDirty ? (
@@ -2556,7 +2584,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
title={t(autoSaveEnabled ? 'filesView.editor.saveNowTitle' : 'filesView.editor.saveNowManualTitle', { shortcut: `${getModifierLabel()}+S` })}
aria-label={t('filesView.editor.saveAria', { shortcut: `${getModifierLabel()}+S` })}
>
<Icon name="save-3" className="h-4 w-4" />
<Icon name="save-3" className="size-4" />
</Button>
) : null}
<Button
@@ -2564,7 +2592,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
size="sm"
onClick={() => setAutoSaveEnabled((enabled) => !enabled)}
className={cn(
'h-6 w-6 p-0 transition-opacity hover:bg-transparent focus-visible:bg-transparent active:bg-transparent',
'size-6 p-0 transition-opacity hover:bg-transparent focus-visible:bg-transparent active:bg-transparent',
autoSaveEnabled ? 'text-foreground opacity-100' : 'text-muted-foreground opacity-65 hover:opacity-100'
)}
title={autoSaveEnabled ? t('filesView.editor.autoSaveOn') : t('filesView.editor.manualSave')}
@@ -2580,11 +2608,11 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 text-foreground opacity-100 hover:bg-transparent focus-visible:bg-transparent active:bg-transparent"
className="size-6 p-0 text-foreground opacity-100 hover:bg-transparent focus-visible:bg-transparent active:bg-transparent"
title={t('filesView.editor.openInDesktopApp')}
aria-label={t('filesView.editor.openInDesktopApp')}
>
<Icon name="file-transfer" className="h-4 w-4" />
<Icon name="file-transfer" className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56 max-h-[70vh] overflow-y-auto">
@@ -2603,7 +2631,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
className="flex items-center gap-2"
onClick={() => void loadOpenInApps(true)}
>
<Icon name="refresh" className="h-4 w-4" />
<Icon name="refresh" className="size-4" />
<span className="typography-ui-label text-foreground">{t('filesView.editor.refreshApps')}</span>
</DropdownMenuItem>
) : null}
@@ -2617,7 +2645,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
size="sm"
onClick={() => setWrapLines(!wrapLines)}
className={cn(
'h-6 w-6 p-0 transition-opacity hover:bg-transparent focus-visible:bg-transparent active:bg-transparent',
'size-6 p-0 transition-opacity hover:bg-transparent focus-visible:bg-transparent active:bg-transparent',
wrapLines ? 'text-foreground opacity-100' : 'text-muted-foreground opacity-65 hover:opacity-100'
)}
title={wrapLines ? t('filesView.editor.disableLineWrap') : t('filesView.editor.enableLineWrap')}
@@ -2633,7 +2661,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
setIsSearchOpen(!isSearchOpen);
event.currentTarget.blur();
}}
className="h-6 w-6 p-0 text-foreground opacity-100 transition-opacity hover:bg-transparent focus-visible:bg-transparent active:bg-transparent"
className="size-6 p-0 text-foreground opacity-100 transition-opacity hover:bg-transparent focus-visible:bg-transparent active:bg-transparent"
title={t('filesView.editor.findInFile')}
>
<Icon name="search" className="size-4" />
@@ -2645,7 +2673,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
setIsGoToLineOpen((open) => !open);
event.currentTarget.blur();
}}
className="h-6 w-6 p-0 text-foreground opacity-100 transition-opacity hover:bg-transparent focus-visible:bg-transparent active:bg-transparent"
className="size-6 p-0 text-foreground opacity-100 transition-opacity hover:bg-transparent focus-visible:bg-transparent active:bg-transparent"
title={t('filesView.editor.goToLine')}
>
<Icon name="menu-fold-2" className="size-4" />
@@ -2688,7 +2716,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
variant="ghost"
size="sm"
onClick={() => saveJsonViewMode(jsonViewMode === 'tree' ? 'text' : 'tree')}
className="h-6 w-6 p-0 text-muted-foreground opacity-65 hover:bg-transparent hover:opacity-100 focus-visible:bg-transparent active:bg-transparent"
className="size-6 p-0 text-muted-foreground opacity-65 hover:bg-transparent hover:opacity-100 focus-visible:bg-transparent active:bg-transparent"
title={jsonViewMode === 'tree' ? t('filesView.editor.switchToTextView') : t('filesView.editor.switchToTreeView')}
>
{jsonViewMode === 'tree' ? (
@@ -2717,14 +2745,14 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
toast.error(t('filesView.toast.copyFailed'));
}
}}
className="h-6 w-6 p-0 hover:bg-transparent focus-visible:bg-transparent active:bg-transparent"
className="size-6 p-0 hover:bg-transparent focus-visible:bg-transparent active:bg-transparent"
title={t('filesView.editor.copyFileContents')}
aria-label={t('filesView.editor.copyFileContents')}
>
{copiedContent ? (
<Icon name="check" className="h-4 w-4 text-[color:var(--status-success)]" />
<Icon name="check" className="size-4 text-[color:var(--status-success)]" />
) : (
<Icon name="clipboard" className="h-4 w-4" />
<Icon name="clipboard" className="size-4" />
)}
</Button>
)}
@@ -2747,14 +2775,14 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
toast.error(t('filesView.toast.copyFailed'));
}
}}
className="h-6 w-6 p-0 hover:bg-transparent focus-visible:bg-transparent active:bg-transparent"
className="size-6 p-0 hover:bg-transparent focus-visible:bg-transparent active:bg-transparent"
title={t('filesView.editor.copyFilePathTitle', { path: displaySelectedPath })}
aria-label={t('filesView.editor.copyFilePathTitle', { path: displaySelectedPath })}
>
{copiedPath ? (
<Icon name="check" className="h-4 w-4 text-[color:var(--status-success)]" />
<Icon name="check" className="size-4 text-[color:var(--status-success)]" />
) : (
<Icon name="file-copy-2" className="h-4 w-4" />
<Icon name="file-copy-2" className="size-4" />
)}
</Button>
)}
@@ -2767,11 +2795,11 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
const fn = files.downloadFile;
if (fn) void fn(selectedFile.path);
}}
className="h-6 w-6 p-0 hover:bg-transparent focus-visible:bg-transparent active:bg-transparent"
className="size-6 p-0 hover:bg-transparent focus-visible:bg-transparent active:bg-transparent"
title={t('filesView.editor.saveFile')}
aria-label={t('filesView.editor.saveFile')}
>
<Icon name="download" className="h-4 w-4" />
<Icon name="download" className="size-4" />
</Button>
)}
@@ -2780,25 +2808,25 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
variant="ghost"
size="sm"
onClick={() => setIsFullscreen(false)}
className="h-6 w-6 p-0 hover:bg-transparent focus-visible:bg-transparent active:bg-transparent"
className="size-6 p-0 hover:bg-transparent focus-visible:bg-transparent active:bg-transparent"
title={t('filesView.editor.exitFullscreen')}
aria-label={t('filesView.editor.exitFullscreen')}
>
<Icon name="fullscreen-exit" className="h-4 w-4" />
<Icon name="fullscreen-exit" className="size-4" />
</Button>
) : (!isMobile && mode === 'full' && (
<Button
variant="ghost"
size="sm"
onClick={() => setIsFullscreen(!isFullscreen)}
className="h-6 w-6 p-0 hover:bg-transparent focus-visible:bg-transparent active:bg-transparent"
className="size-6 p-0 hover:bg-transparent focus-visible:bg-transparent active:bg-transparent"
title={isFullscreen ? t('filesView.editor.exitFullscreen') : t('filesView.editor.fullscreen')}
aria-label={isFullscreen ? t('filesView.editor.exitFullscreen') : t('filesView.editor.fullscreen')}
>
{isFullscreen ? (
<Icon name="fullscreen-exit" className="h-4 w-4" />
<Icon name="fullscreen-exit" className="size-4" />
) : (
<Icon name="fullscreen" className="h-4 w-4" />
<Icon name="fullscreen" className="size-4" />
)}
</Button>
))}
@@ -2845,9 +2873,9 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
type="button"
onClick={() => setShowMobilePageContent(false)}
aria-label={t('filesView.editor.back')}
className="inline-flex h-7 w-7 flex-shrink-0 items-center justify-center mr-1 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
className="inline-flex size-7 flex-shrink-0 items-center justify-center mr-1 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
>
<Icon name="arrow-left-s" className="h-5 w-5" />
<Icon name="arrow-left-s" className="size-5" />
</button>
)}
@@ -2860,9 +2888,9 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
className="inline-flex min-w-0 max-w-full items-center gap-1 text-left typography-ui-label font-medium"
aria-label={t('filesView.editor.openFilesAria')}
>
<FileTypeIcon filePath={selectedFile.path} extension={selectedFile.extension} className="h-3.5 w-3.5 flex-shrink-0" />
<FileTypeIcon filePath={selectedFile.path} extension={selectedFile.extension} className="size-3.5 flex-shrink-0" />
<span className="min-w-0 flex-1 truncate">{selectedFile.name}</span>
<Icon name="arrow-down-s" className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
<Icon name="arrow-down-s" className="size-4 flex-shrink-0 text-muted-foreground" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="min-w-[16rem]">
@@ -2887,7 +2915,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
)}
>
<span className="flex min-w-0 flex-1 items-center gap-2 truncate">
<FileTypeIcon filePath={file.path} extension={file.extension} className="h-3.5 w-3.5 flex-shrink-0" />
<FileTypeIcon filePath={file.path} extension={file.extension} className="size-3.5 flex-shrink-0" />
<span className="min-w-0 flex-1 truncate">{file.name}</span>
</span>
<button
@@ -2902,10 +2930,10 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
event.stopPropagation();
handleCloseFile(file.path);
}}
className="inline-flex h-6 w-6 items-center justify-center rounded-md text-[var(--surface-muted-foreground)] hover:text-[var(--surface-foreground)]"
className="inline-flex size-6 items-center justify-center rounded-md text-[var(--surface-muted-foreground)] hover:text-[var(--surface-foreground)]"
aria-label={t('filesView.editor.closeFileAria', { name: file.name })}
>
<Icon name="close" className="h-3.5 w-3.5" />
<Icon name="close" className="size-3.5" />
</button>
</DropdownMenuItem>
);
@@ -2942,7 +2970,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
: 'bg-transparent border-[var(--interactive-border)] text-[var(--surface-muted-foreground)] hover:bg-[var(--interactive-hover)] hover:text-[var(--surface-foreground)]'
)}
>
<FileTypeIcon filePath={file.path} extension={file.extension} className="h-3.5 w-3.5 flex-shrink-0" />
<FileTypeIcon filePath={file.path} extension={file.extension} className="size-3.5 flex-shrink-0" />
<button
type="button"
onClick={() => {
@@ -2966,7 +2994,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
)}
aria-label={t('filesView.editor.closeFileAria', { name: file.name })}
>
<Icon name="close" className="h-3.5 w-3.5" />
<Icon name="close" className="size-3.5" />
</button>
</div>
);
@@ -3000,11 +3028,11 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
variant="ghost"
size="sm"
onClick={() => setIsFloatingToolbarOpen(true)}
className="h-8 w-8 rounded-lg border border-[var(--interactive-border)] bg-[var(--surface-elevated)] p-0 text-muted-foreground shadow-sm hover:text-foreground"
className="size-8 rounded-lg border border-[var(--interactive-border)] bg-[var(--surface-elevated)] p-0 text-muted-foreground shadow-sm hover:text-foreground"
aria-label={t('filesView.editor.showControlsAria')}
title={t('filesView.editor.controlsTitle')}
>
<Icon name="more-2-fill" className="h-4 w-4" />
<Icon name="more-2-fill" className="size-4" />
</Button>
)}
</div>
@@ -3017,7 +3045,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
? <div className="p-3" />
: (
<div className="p-3 flex items-center gap-2 typography-ui text-muted-foreground">
<Icon name="loader-4" className="h-4 w-4 animate-spin" />
<Icon name="loader-4" className="size-4 animate-spin" />
{t('filesView.state.loading')}
</div>
)
@@ -3195,7 +3223,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
{shouldMaskEditorForPendingNavigation && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center bg-background">
<div className="flex items-center gap-2 typography-ui text-muted-foreground">
<Icon name="loader-4" className="h-4 w-4 animate-spin" />
<Icon name="loader-4" className="size-4 animate-spin" />
{t('filesView.state.openingFileAtChange')}
</div>
</div>
@@ -3217,7 +3245,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
<div className={cn("flex flex-col gap-2 py-2", isMobile ? "px-3" : "px-2")}>
<div className="flex items-center gap-2">
<div className="relative flex-1 min-w-0">
<Icon name="search" className="pointer-events-none absolute left-2 top-2 h-4 w-4 text-muted-foreground" />
<Icon name="search" className="pointer-events-none absolute left-2 top-2 size-4 text-muted-foreground" />
<Input
ref={searchInputRef}
value={searchQuery}
@@ -3229,13 +3257,13 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
<button
type="button"
aria-label={t('filesView.tree.search.clearAria')}
className="absolute right-2 top-2 inline-flex h-4 w-4 items-center justify-center text-muted-foreground hover:text-foreground"
className="absolute right-2 top-2 inline-flex size-4 items-center justify-center text-muted-foreground hover:text-foreground"
onClick={() => {
setSearchQuery('');
searchInputRef.current?.focus();
}}
>
<Icon name="close" className="h-4 w-4" />
<Icon name="close" className="size-4" />
</button>
)}
</div>
@@ -3243,22 +3271,22 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
variant="ghost"
size="sm"
onClick={() => handleOpenDialog('createFile', { path: currentDirectory, type: 'directory' })}
className="h-8 w-8 p-0 flex-shrink-0"
className="size-8 p-0 flex-shrink-0"
title={t('filesView.tree.actions.newFileTitle')}
>
<Icon name="file-add" className="h-4 w-4" />
<Icon name="file-add" className="size-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleOpenDialog('createFolder', { path: currentDirectory, type: 'directory' })}
className="h-8 w-8 p-0 flex-shrink-0"
className="size-8 p-0 flex-shrink-0"
title={t('filesView.tree.actions.newFolderTitle')}
>
<Icon name="folder-add" className="h-4 w-4" />
<Icon name="folder-add" className="size-4" />
</Button>
<Button variant="ghost" size="sm" onClick={() => void refreshRoot()} className="h-8 w-8 p-0 flex-shrink-0">
<Icon name="refresh" className="h-4 w-4" />
<Button variant="ghost" size="sm" onClick={() => void refreshRoot()} className="size-8 p-0 flex-shrink-0">
<Icon name="refresh" className="size-4" />
</Button>
</div>
</div>
@@ -3267,7 +3295,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
<ul className="flex flex-col">
{searching ? (
<li className="flex items-center gap-1.5 px-2 py-1 typography-meta text-muted-foreground">
<Icon name="loader-4" className="h-4 w-4 animate-spin" />
<Icon name="loader-4" className="size-4 animate-spin" />
{t('filesView.tree.search.searching')}
</li>
) : searchResults.length > 0 ? (
@@ -3319,7 +3347,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
? <div className="p-4" />
: (
<div className="p-4 flex items-center gap-2 typography-ui text-muted-foreground">
<Icon name="loader-4" className="h-4 w-4 animate-spin" />
<Icon name="loader-4" className="size-4 animate-spin" />
Loading
</div>
)
@@ -3384,7 +3412,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
{shouldMaskEditorForPendingNavigation && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center bg-background">
<div className="flex items-center gap-2 typography-ui text-muted-foreground">
<Icon name="loader-4" className="h-4 w-4 animate-spin" />
<Icon name="loader-4" className="size-4 animate-spin" />
{t('filesView.state.openingFileAtChange')}
</div>
</div>
@@ -3398,7 +3426,16 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
return (
<div className="flex h-full min-h-0 overflow-hidden bg-background relative">
{renderDialogs()}
<Dialogs
activeDialog={activeDialog}
dialogData={dialogData}
dialogInputValue={dialogInputValue}
onDialogInputChange={setDialogInputValue}
isDialogSubmitting={isDialogSubmitting}
onDialogSubmit={handleDialogSubmit}
onClose={handleCloseDialog}
inputRef={dialogInputRef}
/>
{fullscreenViewer}
{isMobile ? (
showMobilePageContent ? (