refactor: integrate go-to-line into files toolbar

Move go-to-line UI into the editor action bar for a unified layout
Keep toolbar height stable with compact inline line input and Go action
Align file action icons to a single always-enabled visual style
This commit is contained in:
Bohdan Triapitsyn
2026-04-17 13:12:53 +03:00
parent bae7cdc234
commit 2bcac9c6f9
2 changed files with 71 additions and 24 deletions
+27 -22
View File
@@ -28,6 +28,7 @@ import {
RiCodeSSlashLine,
RiNodeTree,
RiDownloadLine,
RiMenuFold2Line,
} from '@remixicon/react';
import { toast } from '@/components/ui';
import { copyTextToClipboard } from '@/lib/clipboard';
@@ -2316,7 +2317,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
variant="ghost"
size="sm"
onClick={() => void saveDraft()}
className="h-6 px-1 gap-1 text-muted-foreground opacity-80 hover:opacity-100"
className="h-6 gap-1 px-1 text-muted-foreground opacity-80 hover:bg-transparent hover:opacity-100 focus-visible:bg-transparent active:bg-transparent"
title={`Save now (${getModifierLabel()}+S) - auto-saves after 1.5s`}
aria-label={`Save (${getModifierLabel()}+S)`}
>
@@ -2330,7 +2331,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 text-muted-foreground opacity-80 hover:opacity-100"
className="h-6 w-6 p-0 text-foreground opacity-100 hover:bg-transparent focus-visible:bg-transparent active:bg-transparent"
title="Open in desktop app"
aria-label="Open in desktop app"
>
@@ -2367,7 +2368,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
size="sm"
onClick={() => setWrapLines(!wrapLines)}
className={cn(
'h-6 w-6 p-0 transition-opacity',
'h-6 w-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 ? 'Disable line wrap' : 'Enable line wrap'}
@@ -2379,11 +2380,11 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
<Button
variant="ghost"
size="sm"
onClick={() => setIsSearchOpen(!isSearchOpen)}
className={cn(
'h-6 w-6 p-0 transition-opacity',
isSearchOpen ? 'text-foreground opacity-100' : 'text-muted-foreground opacity-65 hover:opacity-100'
)}
onClick={(event) => {
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"
title="Find in file"
>
<RiSearchLine className="size-4" />
@@ -2391,12 +2392,21 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
<Button
variant="ghost"
size="sm"
onClick={() => setIsGoToLineOpen(true)}
className="h-6 w-6 p-0 text-muted-foreground opacity-65 hover:opacity-100"
onClick={(event) => {
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"
title="Go to line"
>
<RiCommandLine className="size-4" />
<RiMenuFold2Line className="size-4" />
</Button>
<GoToLineDialog
open={isGoToLineOpen}
onOpenChange={setIsGoToLineOpen}
view={editorViewRef.current}
variant="inline"
/>
</>
)}
</>
@@ -2420,7 +2430,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:opacity-100"
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"
title={jsonViewMode === 'tree' ? 'Switch to Text View' : 'Switch to Tree View'}
>
{jsonViewMode === 'tree' ? (
@@ -2449,7 +2459,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
toast.error('Copy failed');
}
}}
className="h-6 w-6 p-0"
className="h-6 w-6 p-0 hover:bg-transparent focus-visible:bg-transparent active:bg-transparent"
title="Copy file contents"
aria-label="Copy file contents"
>
@@ -2479,7 +2489,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
toast.error('Copy failed');
}
}}
className="h-6 w-6 p-0"
className="h-6 w-6 p-0 hover:bg-transparent focus-visible:bg-transparent active:bg-transparent"
title={`Copy file path (${displaySelectedPath})`}
aria-label={`Copy file path (${displaySelectedPath})`}
>
@@ -2499,7 +2509,7 @@ 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"
className="h-6 w-6 p-0 hover:bg-transparent focus-visible:bg-transparent active:bg-transparent"
title="Save file"
aria-label="Save file"
>
@@ -2512,7 +2522,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
variant="ghost"
size="sm"
onClick={() => setIsFullscreen(false)}
className="h-6 w-6 p-0"
className="h-6 w-6 p-0 hover:bg-transparent focus-visible:bg-transparent active:bg-transparent"
title="Exit fullscreen"
aria-label="Exit fullscreen"
>
@@ -2523,7 +2533,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
variant="ghost"
size="sm"
onClick={() => setIsFullscreen(!isFullscreen)}
className="h-6 w-6 p-0"
className="h-6 w-6 p-0 hover:bg-transparent focus-visible:bg-transparent active:bg-transparent"
title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}
aria-label={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}
>
@@ -2809,11 +2819,6 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
data-keyboard-avoid="none"
style={isMobile ? { height: 'calc(100% - var(--oc-keyboard-inset, 0px))' } : undefined}
>
<GoToLineDialog
open={isGoToLineOpen}
onOpenChange={setIsGoToLineOpen}
view={editorViewRef.current}
/>
<div className={cn('h-full', shouldMaskEditorForPendingNavigation && 'invisible')}>
<CodeMirrorEditor
value={draftContent}
@@ -4,12 +4,14 @@ import { EditorSelection } from '@codemirror/state';
import { EditorView } from '@codemirror/view';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
type GoToLineDialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
view: EditorView | null;
variant?: 'overlay' | 'inline';
};
type CursorSnapshot = {
@@ -51,7 +53,7 @@ const moveSelectionToLine = (view: EditorView, lineNumber: number, preferredChar
});
};
export function GoToLineDialog({ open, onOpenChange, view }: GoToLineDialogProps) {
export function GoToLineDialog({ open, onOpenChange, view, variant = 'overlay' }: GoToLineDialogProps) {
const [inputValue, setInputValue] = React.useState('');
const initialCursorRef = React.useRef<CursorSnapshot | null>(null);
const committedRef = React.useRef(false);
@@ -159,7 +161,7 @@ export function GoToLineDialog({ open, onOpenChange, view }: GoToLineDialogProps
document.removeEventListener('pointerdown', handlePointerDown, true);
document.removeEventListener('keydown', handleKeyDown, true);
};
}, [handleOpenChange, open]);
}, [handleOpenChange, open, variant]);
const helperText = React.useMemo(() => {
if (!view) {
@@ -174,6 +176,46 @@ export function GoToLineDialog({ open, onOpenChange, view }: GoToLineDialogProps
return `Go to line ${lineNumber}`;
}, [lineNumber, view]);
if (variant === 'inline') {
if (!open) {
return null;
}
return (
<div
ref={panelRef}
className="ml-1 flex h-6 items-center gap-1"
>
<Input
ref={inputRef}
type="number"
min={1}
step={1}
inputMode="numeric"
value={inputValue}
onChange={(event) => setInputValue(event.target.value)}
onKeyDown={(event) => {
if (event.key === 'Enter') {
event.preventDefault();
handleSubmit();
}
}}
placeholder="Line"
className="h-6 w-20 rounded-md border-border/70 bg-transparent px-2 typography-meta"
/>
<Button
variant="outline"
size="xs"
onClick={handleSubmit}
disabled={!view || lineNumber === null}
className="h-6 px-2"
>
Go
</Button>
</div>
);
}
return (
<div
ref={panelRef}