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}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className="flex w-full max-w-xl flex-col gap-0 overflow-hidden p-0 sm:max-h-[80vh]"
|
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">
|
<DialogHeader className="px-5 pb-2 pt-5">
|
||||||
<div className="flex items-start justify-between gap-4">
|
<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"> & {
|
type DialogContentProps = Omit<React.ComponentProps<typeof BaseDialog.Popup>, "children"> & {
|
||||||
showCloseButton?: boolean
|
showCloseButton?: boolean
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
onOpenAutoFocus?: (event: Event) => void
|
|
||||||
onCloseAutoFocus?: (event: Event) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogContent({
|
function DialogContent({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
showCloseButton = true,
|
showCloseButton = true,
|
||||||
onOpenAutoFocus,
|
|
||||||
onCloseAutoFocus,
|
|
||||||
...props
|
...props
|
||||||
}: DialogContentProps) {
|
}: DialogContentProps) {
|
||||||
void onOpenAutoFocus
|
|
||||||
void onCloseAutoFocus
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ const OpenInAppListIcon = ({ label, iconDataUrl }: { label: string; iconDataUrl?
|
|||||||
<img
|
<img
|
||||||
src={iconDataUrl}
|
src={iconDataUrl}
|
||||||
alt=""
|
alt=""
|
||||||
className="h-4 w-4 rounded-sm"
|
className="size-4 rounded-sm"
|
||||||
onError={() => setFailed(true)}
|
onError={() => setFailed(true)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -117,7 +117,7 @@ const OpenInAppListIcon = ({ label, iconDataUrl }: { label: string; iconDataUrl?
|
|||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
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'
|
'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)',
|
'git-deleted': 'var(--status-error)',
|
||||||
}[status];
|
}[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);
|
const shouldIgnoreEntryName = (name: string): boolean => DEFAULT_IGNORED_DIR_NAMES.has(name);
|
||||||
@@ -358,9 +358,9 @@ const FileRow: React.FC<FileRowProps> = ({
|
|||||||
>
|
>
|
||||||
{isDir ? (
|
{isDir ? (
|
||||||
isExpanded ? (
|
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)
|
getFileIcon(node.path, node.extension)
|
||||||
@@ -392,16 +392,16 @@ const FileRow: React.FC<FileRowProps> = ({
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-6 w-6"
|
className="size-6"
|
||||||
onClick={handleMenuButtonClick}
|
onClick={handleMenuButtonClick}
|
||||||
>
|
>
|
||||||
<Icon name="more-2-fill" className="h-4 w-4" />
|
<Icon name="more-2-fill" className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" side={isMobile ? "bottom" : "bottom"} onCloseAutoFocus={() => setContextMenuPath(null)}>
|
<DropdownMenuContent align="end" side={isMobile ? "bottom" : "bottom"} onCloseAutoFocus={() => setContextMenuPath(null)}>
|
||||||
{canRename && (
|
{canRename && (
|
||||||
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); onOpenDialog('rename', node); }}>
|
<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>
|
||||||
)}
|
)}
|
||||||
<DropdownMenuItem onClick={(e) => {
|
<DropdownMenuItem onClick={(e) => {
|
||||||
@@ -414,7 +414,7 @@ const FileRow: React.FC<FileRowProps> = ({
|
|||||||
toast.error(t('sidebarFilesTree.toast.copyFailed'));
|
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>
|
||||||
<DropdownMenuItem onClick={(e) => {
|
<DropdownMenuItem onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -427,19 +427,19 @@ const FileRow: React.FC<FileRowProps> = ({
|
|||||||
toast.error(t('sidebarFilesTree.toast.copyFailed'));
|
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>
|
</DropdownMenuItem>
|
||||||
{!isDir && downloadFile && (
|
{!isDir && downloadFile && (
|
||||||
<DropdownMenuItem onClick={(e) => {
|
<DropdownMenuItem onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
void downloadFile(node.path);
|
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>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
{canReveal && (
|
{canReveal && (
|
||||||
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); onRevealPath(node.path); }}>
|
<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>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
{isDir && (canCreateFile || canCreateFolder) && (
|
{isDir && (canCreateFile || canCreateFolder) && (
|
||||||
@@ -447,12 +447,12 @@ const FileRow: React.FC<FileRowProps> = ({
|
|||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
{canCreateFile && (
|
{canCreateFile && (
|
||||||
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); onOpenDialog('createFile', node); }}>
|
<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>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
{canCreateFolder && (
|
{canCreateFolder && (
|
||||||
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); onOpenDialog('createFolder', node); }}>
|
<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>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -464,7 +464,7 @@ const FileRow: React.FC<FileRowProps> = ({
|
|||||||
onClick={(e) => { e.stopPropagation(); onOpenDialog('delete', node); }}
|
onClick={(e) => { e.stopPropagation(); onOpenDialog('delete', node); }}
|
||||||
className="text-destructive focus:text-destructive"
|
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>
|
</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 {
|
interface FilesViewProps {
|
||||||
mode?: 'full' | 'editor-only';
|
mode?: 'full' | 'editor-only';
|
||||||
}
|
}
|
||||||
@@ -602,18 +678,21 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
right: el.scrollLeft + el.clientWidth < el.scrollWidth - 2,
|
right: el.scrollLeft + el.clientWidth < el.scrollWidth - 2,
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
const updateEditorTabsOverflowRef = React.useRef(updateEditorTabsOverflow);
|
||||||
|
updateEditorTabsOverflowRef.current = updateEditorTabsOverflow;
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const el = editorTabsScrollRef.current;
|
const el = editorTabsScrollRef.current;
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
updateEditorTabsOverflow();
|
const handler = () => updateEditorTabsOverflowRef.current();
|
||||||
el.addEventListener('scroll', updateEditorTabsOverflow, { passive: true });
|
handler();
|
||||||
const ro = new ResizeObserver(updateEditorTabsOverflow);
|
el.addEventListener('scroll', handler, { passive: true });
|
||||||
|
const ro = new ResizeObserver(handler);
|
||||||
ro.observe(el);
|
ro.observe(el);
|
||||||
return () => {
|
return () => {
|
||||||
el.removeEventListener('scroll', updateEditorTabsOverflow);
|
el.removeEventListener('scroll', handler);
|
||||||
ro.disconnect();
|
ro.disconnect();
|
||||||
};
|
};
|
||||||
}, [updateEditorTabsOverflow, openFiles.length]);
|
}, [openFiles.length]);
|
||||||
|
|
||||||
const [childrenByDir, setChildrenByDir] = React.useState<Record<string, FileNode[]>>({});
|
const [childrenByDir, setChildrenByDir] = React.useState<Record<string, FileNode[]>>({});
|
||||||
const loadedDirsRef = React.useRef<Set<string>>(new Set());
|
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 [draftContent, setDraftContent] = React.useState('');
|
||||||
const [isSaving, setIsSaving] = React.useState(false);
|
const [isSaving, setIsSaving] = React.useState(false);
|
||||||
|
const dialogInputRef = React.useRef<HTMLInputElement>(null);
|
||||||
const autoSaveTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
const autoSaveTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const lastLoadedFileStatRef = React.useRef<FileStatSnapshot | null>(null);
|
const lastLoadedFileStatRef = React.useRef<FileStatSnapshot | null>(null);
|
||||||
const activeFileLoadIdRef = React.useRef(0);
|
const activeFileLoadIdRef = React.useRef(0);
|
||||||
@@ -836,27 +916,22 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
}, [lineSelection, saveComment]);
|
}, [lineSelection, saveComment]);
|
||||||
|
|
||||||
const mapDirectoryEntries = React.useCallback((dirPath: string, entries: Array<{ name: string; path: string; isDirectory: boolean }>): FileNode[] => {
|
const mapDirectoryEntries = React.useCallback((dirPath: string, entries: Array<{ name: string; path: string; isDirectory: boolean }>): FileNode[] => {
|
||||||
const nodes = entries
|
const nodes: FileNode[] = [];
|
||||||
.filter((entry) => entry && typeof entry.name === 'string' && entry.name.length > 0)
|
for (const entry of entries) {
|
||||||
.filter((entry) => showHidden || !entry.name.startsWith('.'))
|
if (!(entry && typeof entry.name === 'string' && entry.name.length > 0)) continue;
|
||||||
.filter((entry) => showGitignored || !shouldIgnoreEntryName(entry.name))
|
if (!showHidden && entry.name.startsWith('.')) continue;
|
||||||
.map<FileNode>((entry) => {
|
if (!showGitignored && shouldIgnoreEntryName(entry.name)) continue;
|
||||||
const name = entry.name;
|
const name = entry.name;
|
||||||
const normalizedEntryPath = normalizePath(entry.path || '');
|
const normalizedEntryPath = normalizePath(entry.path || '');
|
||||||
const path = normalizedEntryPath
|
const path = normalizedEntryPath
|
||||||
? (isAbsolutePath(normalizedEntryPath)
|
? (isAbsolutePath(normalizedEntryPath)
|
||||||
? normalizedEntryPath
|
? normalizedEntryPath
|
||||||
: normalizePath(`${dirPath}/${normalizedEntryPath}`))
|
: normalizePath(`${dirPath}/${normalizedEntryPath}`))
|
||||||
: normalizePath(`${dirPath}/${name}`);
|
: normalizePath(`${dirPath}/${name}`);
|
||||||
const type = entry.isDirectory ? 'directory' : 'file';
|
const type = entry.isDirectory ? 'directory' : 'file';
|
||||||
const extension = type === 'file' && name.includes('.') ? name.split('.').pop()?.toLowerCase() : undefined;
|
const extension = type === 'file' && name.includes('.') ? name.split('.').pop()?.toLowerCase() : undefined;
|
||||||
return {
|
nodes.push({ name, path, type, extension });
|
||||||
name,
|
}
|
||||||
path,
|
|
||||||
type,
|
|
||||||
extension,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return sortNodes(nodes);
|
return sortNodes(nodes);
|
||||||
}, [showGitignored, showHidden]);
|
}, [showGitignored, showHidden]);
|
||||||
@@ -1272,13 +1347,14 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
return null;
|
return null;
|
||||||
}, [files]);
|
}, [files]);
|
||||||
|
|
||||||
const displayedContent = React.useMemo(() => {
|
const displayedContent = React.useMemo(() =>
|
||||||
return fileContent.length > MAX_VIEW_CHARS
|
fileContent.length > MAX_VIEW_CHARS
|
||||||
? `${fileContent.slice(0, MAX_VIEW_CHARS)}\n\n… truncated …`
|
? `${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 () => {
|
const saveDraft = React.useCallback(async () => {
|
||||||
if (!selectedFile || !files.writeFile) {
|
if (!selectedFile || !files.writeFile) {
|
||||||
@@ -1540,11 +1616,13 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
expandPaths(root, pathsToExpand);
|
expandPaths(root, pathsToExpand);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const path of pathsToExpand) {
|
const loadPromises = pathsToExpand.map((path) => {
|
||||||
if (!loadedDirsRef.current.has(path)) {
|
if (!loadedDirsRef.current.has(path)) {
|
||||||
await loadDirectory(path);
|
return loadDirectory(path);
|
||||||
}
|
}
|
||||||
}
|
return undefined;
|
||||||
|
}).filter(Boolean);
|
||||||
|
await Promise.all(loadPromises);
|
||||||
}, [expandPaths, loadDirectory, root]);
|
}, [expandPaths, loadDirectory, root]);
|
||||||
|
|
||||||
const getNextOpenFile = React.useCallback((path: string, filesList: FileNode[]) => {
|
const getNextOpenFile = React.useCallback((path: string, filesList: FileNode[]) => {
|
||||||
@@ -2293,7 +2371,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
|
|
||||||
const viewport = window.visualViewport;
|
const viewport = window.visualViewport;
|
||||||
viewport?.addEventListener('resize', runNudge);
|
viewport?.addEventListener('resize', runNudge);
|
||||||
viewport?.addEventListener('scroll', runNudge);
|
viewport?.addEventListener('scroll', runNudge, { passive: true });
|
||||||
document.addEventListener('selectionchange', runNudge);
|
document.addEventListener('selectionchange', runNudge);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -2431,57 +2509,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
};
|
};
|
||||||
}, [files, isSelectedImage, isSelectedSvg, runtime.isDesktop, selectedFile?.path, selectedFileReadOptions, t]);
|
}, [files, isSelectedImage, isSelectedSvg, runtime.isDesktop, selectedFile?.path, selectedFileReadOptions, t]);
|
||||||
|
|
||||||
const renderDialogs = () => (
|
const handleCloseDialog = React.useCallback(() => setActiveDialog(null), []);
|
||||||
<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 blockWidgets = React.useMemo(() => {
|
const blockWidgets = React.useMemo(() => {
|
||||||
return buildCodeMirrorCommentWidgets({
|
return buildCodeMirrorCommentWidgets({
|
||||||
@@ -2539,12 +2567,12 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
<>
|
<>
|
||||||
{isSaving ? (
|
{isSaving ? (
|
||||||
<span className="flex items-center gap-1 px-1 text-muted-foreground typography-meta">
|
<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')}
|
{t('filesView.editor.saving')}
|
||||||
</span>
|
</span>
|
||||||
) : autoSaveEnabled && autoSaveStatus === 'saved' && !isDirty ? (
|
) : autoSaveEnabled && autoSaveStatus === 'saved' && !isDirty ? (
|
||||||
<span className="flex items-center gap-1 px-1 text-[color:var(--status-success)] typography-meta">
|
<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')}
|
{t('filesView.editor.saved')}
|
||||||
</span>
|
</span>
|
||||||
) : isDirty ? (
|
) : 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` })}
|
title={t(autoSaveEnabled ? 'filesView.editor.saveNowTitle' : 'filesView.editor.saveNowManualTitle', { shortcut: `${getModifierLabel()}+S` })}
|
||||||
aria-label={t('filesView.editor.saveAria', { 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>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
<Button
|
<Button
|
||||||
@@ -2564,7 +2592,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setAutoSaveEnabled((enabled) => !enabled)}
|
onClick={() => setAutoSaveEnabled((enabled) => !enabled)}
|
||||||
className={cn(
|
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'
|
autoSaveEnabled ? 'text-foreground opacity-100' : 'text-muted-foreground opacity-65 hover:opacity-100'
|
||||||
)}
|
)}
|
||||||
title={autoSaveEnabled ? t('filesView.editor.autoSaveOn') : t('filesView.editor.manualSave')}
|
title={autoSaveEnabled ? t('filesView.editor.autoSaveOn') : t('filesView.editor.manualSave')}
|
||||||
@@ -2580,11 +2608,11 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
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')}
|
title={t('filesView.editor.openInDesktopApp')}
|
||||||
aria-label={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>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="w-56 max-h-[70vh] overflow-y-auto">
|
<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"
|
className="flex items-center gap-2"
|
||||||
onClick={() => void loadOpenInApps(true)}
|
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>
|
<span className="typography-ui-label text-foreground">{t('filesView.editor.refreshApps')}</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -2617,7 +2645,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setWrapLines(!wrapLines)}
|
onClick={() => setWrapLines(!wrapLines)}
|
||||||
className={cn(
|
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'
|
wrapLines ? 'text-foreground opacity-100' : 'text-muted-foreground opacity-65 hover:opacity-100'
|
||||||
)}
|
)}
|
||||||
title={wrapLines ? t('filesView.editor.disableLineWrap') : t('filesView.editor.enableLineWrap')}
|
title={wrapLines ? t('filesView.editor.disableLineWrap') : t('filesView.editor.enableLineWrap')}
|
||||||
@@ -2633,7 +2661,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
setIsSearchOpen(!isSearchOpen);
|
setIsSearchOpen(!isSearchOpen);
|
||||||
event.currentTarget.blur();
|
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')}
|
title={t('filesView.editor.findInFile')}
|
||||||
>
|
>
|
||||||
<Icon name="search" className="size-4" />
|
<Icon name="search" className="size-4" />
|
||||||
@@ -2645,7 +2673,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
setIsGoToLineOpen((open) => !open);
|
setIsGoToLineOpen((open) => !open);
|
||||||
event.currentTarget.blur();
|
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')}
|
title={t('filesView.editor.goToLine')}
|
||||||
>
|
>
|
||||||
<Icon name="menu-fold-2" className="size-4" />
|
<Icon name="menu-fold-2" className="size-4" />
|
||||||
@@ -2688,7 +2716,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => saveJsonViewMode(jsonViewMode === 'tree' ? 'text' : 'tree')}
|
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')}
|
title={jsonViewMode === 'tree' ? t('filesView.editor.switchToTextView') : t('filesView.editor.switchToTreeView')}
|
||||||
>
|
>
|
||||||
{jsonViewMode === 'tree' ? (
|
{jsonViewMode === 'tree' ? (
|
||||||
@@ -2717,14 +2745,14 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
toast.error(t('filesView.toast.copyFailed'));
|
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')}
|
title={t('filesView.editor.copyFileContents')}
|
||||||
aria-label={t('filesView.editor.copyFileContents')}
|
aria-label={t('filesView.editor.copyFileContents')}
|
||||||
>
|
>
|
||||||
{copiedContent ? (
|
{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>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -2747,14 +2775,14 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
toast.error(t('filesView.toast.copyFailed'));
|
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 })}
|
title={t('filesView.editor.copyFilePathTitle', { path: displaySelectedPath })}
|
||||||
aria-label={t('filesView.editor.copyFilePathTitle', { path: displaySelectedPath })}
|
aria-label={t('filesView.editor.copyFilePathTitle', { path: displaySelectedPath })}
|
||||||
>
|
>
|
||||||
{copiedPath ? (
|
{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>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -2767,11 +2795,11 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
const fn = files.downloadFile;
|
const fn = files.downloadFile;
|
||||||
if (fn) void fn(selectedFile.path);
|
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')}
|
title={t('filesView.editor.saveFile')}
|
||||||
aria-label={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>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -2780,25 +2808,25 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setIsFullscreen(false)}
|
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')}
|
title={t('filesView.editor.exitFullscreen')}
|
||||||
aria-label={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>
|
</Button>
|
||||||
) : (!isMobile && mode === 'full' && (
|
) : (!isMobile && mode === 'full' && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setIsFullscreen(!isFullscreen)}
|
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')}
|
title={isFullscreen ? t('filesView.editor.exitFullscreen') : t('filesView.editor.fullscreen')}
|
||||||
aria-label={isFullscreen ? t('filesView.editor.exitFullscreen') : t('filesView.editor.fullscreen')}
|
aria-label={isFullscreen ? t('filesView.editor.exitFullscreen') : t('filesView.editor.fullscreen')}
|
||||||
>
|
>
|
||||||
{isFullscreen ? (
|
{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>
|
</Button>
|
||||||
))}
|
))}
|
||||||
@@ -2845,9 +2873,9 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowMobilePageContent(false)}
|
onClick={() => setShowMobilePageContent(false)}
|
||||||
aria-label={t('filesView.editor.back')}
|
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>
|
</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"
|
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')}
|
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>
|
<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>
|
</button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="start" className="min-w-[16rem]">
|
<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">
|
<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 className="min-w-0 flex-1 truncate">{file.name}</span>
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
@@ -2902,10 +2930,10 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
handleCloseFile(file.path);
|
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 })}
|
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>
|
</button>
|
||||||
</DropdownMenuItem>
|
</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)]'
|
: '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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -2966,7 +2994,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
)}
|
)}
|
||||||
aria-label={t('filesView.editor.closeFileAria', { name: file.name })}
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -3000,11 +3028,11 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setIsFloatingToolbarOpen(true)}
|
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')}
|
aria-label={t('filesView.editor.showControlsAria')}
|
||||||
title={t('filesView.editor.controlsTitle')}
|
title={t('filesView.editor.controlsTitle')}
|
||||||
>
|
>
|
||||||
<Icon name="more-2-fill" className="h-4 w-4" />
|
<Icon name="more-2-fill" className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -3017,7 +3045,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
? <div className="p-3" />
|
? <div className="p-3" />
|
||||||
: (
|
: (
|
||||||
<div className="p-3 flex items-center gap-2 typography-ui text-muted-foreground">
|
<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')}
|
{t('filesView.state.loading')}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -3195,7 +3223,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
{shouldMaskEditorForPendingNavigation && (
|
{shouldMaskEditorForPendingNavigation && (
|
||||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center bg-background">
|
<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">
|
<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')}
|
{t('filesView.state.openingFileAtChange')}
|
||||||
</div>
|
</div>
|
||||||
</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={cn("flex flex-col gap-2 py-2", isMobile ? "px-3" : "px-2")}>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="relative flex-1 min-w-0">
|
<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
|
<Input
|
||||||
ref={searchInputRef}
|
ref={searchInputRef}
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
@@ -3229,13 +3257,13 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={t('filesView.tree.search.clearAria')}
|
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={() => {
|
onClick={() => {
|
||||||
setSearchQuery('');
|
setSearchQuery('');
|
||||||
searchInputRef.current?.focus();
|
searchInputRef.current?.focus();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon name="close" className="h-4 w-4" />
|
<Icon name="close" className="size-4" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -3243,22 +3271,22 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleOpenDialog('createFile', { path: currentDirectory, type: 'directory' })}
|
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')}
|
title={t('filesView.tree.actions.newFileTitle')}
|
||||||
>
|
>
|
||||||
<Icon name="file-add" className="h-4 w-4" />
|
<Icon name="file-add" className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleOpenDialog('createFolder', { path: currentDirectory, type: 'directory' })}
|
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')}
|
title={t('filesView.tree.actions.newFolderTitle')}
|
||||||
>
|
>
|
||||||
<Icon name="folder-add" className="h-4 w-4" />
|
<Icon name="folder-add" className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="sm" onClick={() => void refreshRoot()} className="h-8 w-8 p-0 flex-shrink-0">
|
<Button variant="ghost" size="sm" onClick={() => void refreshRoot()} className="size-8 p-0 flex-shrink-0">
|
||||||
<Icon name="refresh" className="h-4 w-4" />
|
<Icon name="refresh" className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -3267,7 +3295,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
<ul className="flex flex-col">
|
<ul className="flex flex-col">
|
||||||
{searching ? (
|
{searching ? (
|
||||||
<li className="flex items-center gap-1.5 px-2 py-1 typography-meta text-muted-foreground">
|
<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')}
|
{t('filesView.tree.search.searching')}
|
||||||
</li>
|
</li>
|
||||||
) : searchResults.length > 0 ? (
|
) : searchResults.length > 0 ? (
|
||||||
@@ -3319,7 +3347,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
? <div className="p-4" />
|
? <div className="p-4" />
|
||||||
: (
|
: (
|
||||||
<div className="p-4 flex items-center gap-2 typography-ui text-muted-foreground">
|
<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…
|
Loading…
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -3384,7 +3412,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
{shouldMaskEditorForPendingNavigation && (
|
{shouldMaskEditorForPendingNavigation && (
|
||||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center bg-background">
|
<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">
|
<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')}
|
{t('filesView.state.openingFileAtChange')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -3398,7 +3426,16 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full min-h-0 overflow-hidden bg-background relative">
|
<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}
|
{fullscreenViewer}
|
||||||
{isMobile ? (
|
{isMobile ? (
|
||||||
showMobilePageContent ? (
|
showMobilePageContent ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user