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,
|
RiCodeSSlashLine,
|
||||||
RiNodeTree,
|
RiNodeTree,
|
||||||
RiDownloadLine,
|
RiDownloadLine,
|
||||||
|
RiMenuFold2Line,
|
||||||
} from '@remixicon/react';
|
} from '@remixicon/react';
|
||||||
import { toast } from '@/components/ui';
|
import { toast } from '@/components/ui';
|
||||||
import { copyTextToClipboard } from '@/lib/clipboard';
|
import { copyTextToClipboard } from '@/lib/clipboard';
|
||||||
@@ -2316,7 +2317,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => void saveDraft()}
|
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`}
|
title={`Save now (${getModifierLabel()}+S) - auto-saves after 1.5s`}
|
||||||
aria-label={`Save (${getModifierLabel()}+S)`}
|
aria-label={`Save (${getModifierLabel()}+S)`}
|
||||||
>
|
>
|
||||||
@@ -2330,7 +2331,7 @@ 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-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"
|
title="Open in desktop app"
|
||||||
aria-label="Open in desktop app"
|
aria-label="Open in desktop app"
|
||||||
>
|
>
|
||||||
@@ -2367,7 +2368,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',
|
'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'
|
wrapLines ? 'text-foreground opacity-100' : 'text-muted-foreground opacity-65 hover:opacity-100'
|
||||||
)}
|
)}
|
||||||
title={wrapLines ? 'Disable line wrap' : 'Enable line wrap'}
|
title={wrapLines ? 'Disable line wrap' : 'Enable line wrap'}
|
||||||
@@ -2379,11 +2380,11 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setIsSearchOpen(!isSearchOpen)}
|
onClick={(event) => {
|
||||||
className={cn(
|
setIsSearchOpen(!isSearchOpen);
|
||||||
'h-6 w-6 p-0 transition-opacity',
|
event.currentTarget.blur();
|
||||||
isSearchOpen ? 'text-foreground opacity-100' : 'text-muted-foreground opacity-65 hover:opacity-100'
|
}}
|
||||||
)}
|
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"
|
title="Find in file"
|
||||||
>
|
>
|
||||||
<RiSearchLine className="size-4" />
|
<RiSearchLine className="size-4" />
|
||||||
@@ -2391,12 +2392,21 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setIsGoToLineOpen(true)}
|
onClick={(event) => {
|
||||||
className="h-6 w-6 p-0 text-muted-foreground opacity-65 hover:opacity-100"
|
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"
|
title="Go to line"
|
||||||
>
|
>
|
||||||
<RiCommandLine className="size-4" />
|
<RiMenuFold2Line className="size-4" />
|
||||||
</Button>
|
</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"
|
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: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'}
|
title={jsonViewMode === 'tree' ? 'Switch to Text View' : 'Switch to Tree View'}
|
||||||
>
|
>
|
||||||
{jsonViewMode === 'tree' ? (
|
{jsonViewMode === 'tree' ? (
|
||||||
@@ -2449,7 +2459,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
toast.error('Copy failed');
|
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"
|
title="Copy file contents"
|
||||||
aria-label="Copy file contents"
|
aria-label="Copy file contents"
|
||||||
>
|
>
|
||||||
@@ -2479,7 +2489,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
toast.error('Copy failed');
|
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})`}
|
title={`Copy file path (${displaySelectedPath})`}
|
||||||
aria-label={`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;
|
const fn = files.downloadFile;
|
||||||
if (fn) void fn(selectedFile.path);
|
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"
|
title="Save file"
|
||||||
aria-label="Save file"
|
aria-label="Save file"
|
||||||
>
|
>
|
||||||
@@ -2512,7 +2522,7 @@ 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"
|
className="h-6 w-6 p-0 hover:bg-transparent focus-visible:bg-transparent active:bg-transparent"
|
||||||
title="Exit fullscreen"
|
title="Exit fullscreen"
|
||||||
aria-label="Exit fullscreen"
|
aria-label="Exit fullscreen"
|
||||||
>
|
>
|
||||||
@@ -2523,7 +2533,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setIsFullscreen(!isFullscreen)}
|
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'}
|
title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}
|
||||||
aria-label={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"
|
data-keyboard-avoid="none"
|
||||||
style={isMobile ? { height: 'calc(100% - var(--oc-keyboard-inset, 0px))' } : undefined}
|
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')}>
|
<div className={cn('h-full', shouldMaskEditorForPendingNavigation && 'invisible')}>
|
||||||
<CodeMirrorEditor
|
<CodeMirrorEditor
|
||||||
value={draftContent}
|
value={draftContent}
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ import { EditorSelection } from '@codemirror/state';
|
|||||||
import { EditorView } from '@codemirror/view';
|
import { EditorView } from '@codemirror/view';
|
||||||
|
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
type GoToLineDialogProps = {
|
type GoToLineDialogProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
view: EditorView | null;
|
view: EditorView | null;
|
||||||
|
variant?: 'overlay' | 'inline';
|
||||||
};
|
};
|
||||||
|
|
||||||
type CursorSnapshot = {
|
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 [inputValue, setInputValue] = React.useState('');
|
||||||
const initialCursorRef = React.useRef<CursorSnapshot | null>(null);
|
const initialCursorRef = React.useRef<CursorSnapshot | null>(null);
|
||||||
const committedRef = React.useRef(false);
|
const committedRef = React.useRef(false);
|
||||||
@@ -159,7 +161,7 @@ export function GoToLineDialog({ open, onOpenChange, view }: GoToLineDialogProps
|
|||||||
document.removeEventListener('pointerdown', handlePointerDown, true);
|
document.removeEventListener('pointerdown', handlePointerDown, true);
|
||||||
document.removeEventListener('keydown', handleKeyDown, true);
|
document.removeEventListener('keydown', handleKeyDown, true);
|
||||||
};
|
};
|
||||||
}, [handleOpenChange, open]);
|
}, [handleOpenChange, open, variant]);
|
||||||
|
|
||||||
const helperText = React.useMemo(() => {
|
const helperText = React.useMemo(() => {
|
||||||
if (!view) {
|
if (!view) {
|
||||||
@@ -174,6 +176,46 @@ export function GoToLineDialog({ open, onOpenChange, view }: GoToLineDialogProps
|
|||||||
return `Go to line ${lineNumber}`;
|
return `Go to line ${lineNumber}`;
|
||||||
}, [lineNumber, view]);
|
}, [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 (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={panelRef}
|
ref={panelRef}
|
||||||
|
|||||||
Reference in New Issue
Block a user