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:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user