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:
committed by
GitHub
parent
6369cf76a7
commit
2290cf4b59
@@ -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">
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
Reference in New Issue
Block a user