feat(FN-4125): align mobile editor toolbar toggles and add line number togg
Completes the FileEditor mobile toolbar refactor (FN-4125) by wiring up the line-number toggle with mobile-responsive styling and adding tests for both the toggle and the FileBrowserModal changes. Fusion-Task-Id: FN-4125
This commit is contained in:
@@ -40,12 +40,6 @@
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.file-browser-line-numbers-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.file-browser-modal-header .modal-close {
|
||||
position: static;
|
||||
}
|
||||
@@ -395,7 +389,16 @@
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.file-browser-line-numbers-toggle {
|
||||
.file-editor-toolbar {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.file-editor-toolbar-actions {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.file-editor-toolbar-actions .btn {
|
||||
min-height: var(--mobile-nav-height);
|
||||
}
|
||||
|
||||
@@ -674,13 +677,28 @@
|
||||
.file-editor-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-md);
|
||||
flex-wrap: wrap;
|
||||
padding: var(--space-sm) var(--space-lg);
|
||||
border-bottom: calc(var(--space-xs) * 0.25) solid var(--border);
|
||||
background: var(--surface);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.file-editor-toolbar-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.file-editor-line-numbers-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.file-editor-mode-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import "./FileBrowser.css";
|
||||
import { useState, useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { X, Save, RotateCcw, Folder, FileType, ArrowLeft, ListOrdered } from "lucide-react";
|
||||
import { X, Save, RotateCcw, Folder, FileType, ArrowLeft } from "lucide-react";
|
||||
import { useWorkspaceFileBrowser } from "../hooks/useWorkspaceFileBrowser";
|
||||
import { useWorkspaceFileEditor } from "../hooks/useWorkspaceFileEditor";
|
||||
import { useWorkspaces } from "../hooks/useWorkspaces";
|
||||
@@ -288,16 +288,6 @@ export function FileBrowserModal({
|
||||
)}
|
||||
</div>
|
||||
<div className="file-browser-header-actions">
|
||||
<button
|
||||
className={`btn btn-sm file-browser-line-numbers-toggle ${showLineNumbers ? "btn-primary" : ""}`}
|
||||
onClick={handleToggleLineNumbers}
|
||||
aria-label="Toggle line numbers"
|
||||
aria-pressed={showLineNumbers}
|
||||
title="Toggle line numbers"
|
||||
>
|
||||
<ListOrdered size={14} />
|
||||
<span>Line #</span>
|
||||
</button>
|
||||
<WorkspaceSelector
|
||||
currentWorkspace={currentWorkspace}
|
||||
projectName={projectName}
|
||||
@@ -419,6 +409,8 @@ export function FileBrowserModal({
|
||||
filePath={selectedFile}
|
||||
readOnly={isBinaryFile(selectedFile)}
|
||||
showLineNumbers={showLineNumbers && !isBinaryFile(selectedFile)}
|
||||
onToggleLineNumbers={handleToggleLineNumbers}
|
||||
canToggleLineNumbers={!isBinaryFile(selectedFile)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useCallback, useMemo, useRef, type UIEvent } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { FileEdit, Eye, WrapText } from "lucide-react";
|
||||
import { FileEdit, Eye, ListOrdered, WrapText } from "lucide-react";
|
||||
|
||||
interface FileEditorProps {
|
||||
content: string;
|
||||
@@ -9,6 +9,8 @@ interface FileEditorProps {
|
||||
readOnly?: boolean;
|
||||
filePath?: string;
|
||||
showLineNumbers?: boolean;
|
||||
onToggleLineNumbers?: () => void;
|
||||
canToggleLineNumbers?: boolean;
|
||||
}
|
||||
|
||||
function isMarkdownFile(filePath?: string): boolean {
|
||||
@@ -17,7 +19,15 @@ function isMarkdownFile(filePath?: string): boolean {
|
||||
return lowerPath.endsWith(".md") || lowerPath.endsWith(".markdown") || lowerPath.endsWith(".mdx");
|
||||
}
|
||||
|
||||
export function FileEditor({ content, onChange, readOnly, filePath, showLineNumbers = false }: FileEditorProps) {
|
||||
export function FileEditor({
|
||||
content,
|
||||
onChange,
|
||||
readOnly,
|
||||
filePath,
|
||||
showLineNumbers = false,
|
||||
onToggleLineNumbers,
|
||||
canToggleLineNumbers = true,
|
||||
}: FileEditorProps) {
|
||||
const [showPreview, setShowPreview] = useState(false);
|
||||
const [wordWrap, setWordWrap] = useState(true);
|
||||
const lineNumbersRef = useRef<HTMLDivElement>(null);
|
||||
@@ -26,6 +36,7 @@ export function FileEditor({ content, onChange, readOnly, filePath, showLineNumb
|
||||
// For markdown files in readOnly mode, default to preview
|
||||
const effectiveShowPreview = isMarkdown && (readOnly ? true : showPreview);
|
||||
const shouldRenderLineNumbers = showLineNumbers && !readOnly && !effectiveShowPreview;
|
||||
const shouldShowLineNumbersToggle = Boolean(onToggleLineNumbers) && canToggleLineNumbers && !readOnly && !effectiveShowPreview;
|
||||
const lineCount = useMemo(() => {
|
||||
if (!shouldRenderLineNumbers) {
|
||||
return 0;
|
||||
@@ -81,28 +92,56 @@ export function FileEditor({ content, onChange, readOnly, filePath, showLineNumb
|
||||
</button>
|
||||
</div>
|
||||
{!readOnly && (
|
||||
<button
|
||||
className={`btn btn-sm ${wordWrap ? "btn-primary" : ""}`}
|
||||
onClick={handleWordWrapToggle}
|
||||
aria-label="Toggle word wrap"
|
||||
title="Toggle word wrap"
|
||||
>
|
||||
<WrapText size={14} />
|
||||
</button>
|
||||
<div className="file-editor-toolbar-actions">
|
||||
{shouldShowLineNumbersToggle && (
|
||||
<button
|
||||
className={`btn btn-sm file-editor-line-numbers-button ${showLineNumbers ? "btn-primary" : ""}`}
|
||||
onClick={onToggleLineNumbers}
|
||||
aria-label="Toggle line numbers"
|
||||
aria-pressed={showLineNumbers}
|
||||
title="Toggle line numbers"
|
||||
>
|
||||
<ListOrdered size={14} />
|
||||
<span>Line #</span>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className={`btn btn-sm ${wordWrap ? "btn-primary" : ""}`}
|
||||
onClick={handleWordWrapToggle}
|
||||
aria-label="Toggle word wrap"
|
||||
title="Toggle word wrap"
|
||||
>
|
||||
<WrapText size={14} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
!readOnly && (
|
||||
<div className="file-editor-toolbar">
|
||||
<div className="file-editor-mode-toggle" />
|
||||
<button
|
||||
className={`btn btn-sm ${wordWrap ? "btn-primary" : ""}`}
|
||||
onClick={handleWordWrapToggle}
|
||||
aria-label="Toggle word wrap"
|
||||
title="Toggle word wrap"
|
||||
>
|
||||
<WrapText size={14} />
|
||||
</button>
|
||||
<div className="file-editor-toolbar-actions">
|
||||
{shouldShowLineNumbersToggle && (
|
||||
<button
|
||||
className={`btn btn-sm file-editor-line-numbers-button ${showLineNumbers ? "btn-primary" : ""}`}
|
||||
onClick={onToggleLineNumbers}
|
||||
aria-label="Toggle line numbers"
|
||||
aria-pressed={showLineNumbers}
|
||||
title="Toggle line numbers"
|
||||
>
|
||||
<ListOrdered size={14} />
|
||||
<span>Line #</span>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className={`btn btn-sm ${wordWrap ? "btn-primary" : ""}`}
|
||||
onClick={handleWordWrapToggle}
|
||||
aria-label="Toggle word wrap"
|
||||
title="Toggle word wrap"
|
||||
>
|
||||
<WrapText size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
@@ -724,7 +724,16 @@ describe("FileBrowserModal", () => {
|
||||
});
|
||||
|
||||
describe("line number toggle", () => {
|
||||
it("renders a header toggle and persists preference per project", async () => {
|
||||
const clickFileEntry = async (name: string) => {
|
||||
const fileEntry = screen.getAllByText(name).find((element) => element.classList.contains("file-node-name"));
|
||||
expect(fileEntry).toBeTruthy();
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(fileEntry!);
|
||||
});
|
||||
};
|
||||
|
||||
it("renders an editor toggle and persists preference per project", async () => {
|
||||
render(
|
||||
<FileBrowserModal
|
||||
initialWorkspace="project"
|
||||
@@ -734,6 +743,8 @@ describe("FileBrowserModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
await clickFileEntry("file1.ts");
|
||||
|
||||
const toggle = screen.getByRole("button", { name: /toggle line numbers/i });
|
||||
expect(toggle).toHaveAttribute("aria-pressed", "false");
|
||||
|
||||
@@ -742,7 +753,7 @@ describe("FileBrowserModal", () => {
|
||||
expect(localStorage.getItem("kb:proj-1:kb-files-line-numbers")).toBe("true");
|
||||
});
|
||||
|
||||
it("loads persisted preference when project changes", () => {
|
||||
it("loads persisted preference when project changes", async () => {
|
||||
localStorage.setItem("kb:proj-a:kb-files-line-numbers", "true");
|
||||
localStorage.setItem("kb:proj-b:kb-files-line-numbers", "false");
|
||||
|
||||
@@ -755,6 +766,8 @@ describe("FileBrowserModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
await clickFileEntry("file1.ts");
|
||||
|
||||
expect(screen.getByRole("button", { name: /toggle line numbers/i })).toHaveAttribute("aria-pressed", "true");
|
||||
|
||||
rerender(
|
||||
@@ -766,6 +779,8 @@ describe("FileBrowserModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
await clickFileEntry("file1.ts");
|
||||
|
||||
expect(screen.getByRole("button", { name: /toggle line numbers/i })).toHaveAttribute("aria-pressed", "false");
|
||||
});
|
||||
|
||||
@@ -786,18 +801,18 @@ describe("FileBrowserModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /toggle line numbers/i }));
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText("editable.ts"));
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /toggle line numbers/i }));
|
||||
expect(document.querySelector(".file-editor-line-numbers")).toBeInTheDocument();
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText("readme.pdf"));
|
||||
});
|
||||
|
||||
expect(screen.queryByRole("button", { name: /toggle line numbers/i })).not.toBeInTheDocument();
|
||||
expect(document.querySelector(".file-editor-line-numbers")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -243,6 +243,57 @@ describe("FileEditor", () => {
|
||||
});
|
||||
|
||||
describe("line numbers", () => {
|
||||
it("shows the line number toggle button when toggle support is provided", () => {
|
||||
render(
|
||||
<FileEditor
|
||||
content={"first\nsecond\nthird"}
|
||||
onChange={vi.fn()}
|
||||
filePath="src/app.ts"
|
||||
showLineNumbers={false}
|
||||
onToggleLineNumbers={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole("button", { name: /toggle line numbers/i })).toHaveAttribute("aria-pressed", "false");
|
||||
expect(screen.getByRole("button", { name: /toggle line numbers/i })).toHaveAttribute("title", "Toggle line numbers");
|
||||
});
|
||||
|
||||
it("hides the line number toggle button when toggle support is not provided", () => {
|
||||
render(<FileEditor content="first\nsecond" onChange={vi.fn()} filePath="src/app.ts" showLineNumbers={false} />);
|
||||
|
||||
expect(screen.queryByRole("button", { name: /toggle line numbers/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("calls onToggleLineNumbers when the toggle button is clicked", () => {
|
||||
const onToggleLineNumbers = vi.fn();
|
||||
render(
|
||||
<FileEditor
|
||||
content="first\nsecond"
|
||||
onChange={vi.fn()}
|
||||
filePath="src/app.ts"
|
||||
onToggleLineNumbers={onToggleLineNumbers}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /toggle line numbers/i }));
|
||||
expect(onToggleLineNumbers).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("hides the line number toggle button for read-only files", () => {
|
||||
render(
|
||||
<FileEditor
|
||||
content={"one\ntwo"}
|
||||
onChange={vi.fn()}
|
||||
filePath="file.bin"
|
||||
readOnly
|
||||
showLineNumbers
|
||||
onToggleLineNumbers={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByRole("button", { name: /toggle line numbers/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows line numbers for editable text mode when enabled", () => {
|
||||
render(
|
||||
<FileEditor
|
||||
@@ -250,6 +301,7 @@ describe("FileEditor", () => {
|
||||
onChange={vi.fn()}
|
||||
filePath="src/app.ts"
|
||||
showLineNumbers
|
||||
onToggleLineNumbers={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -262,16 +314,30 @@ describe("FileEditor", () => {
|
||||
|
||||
it("hides line numbers in markdown preview mode", () => {
|
||||
render(
|
||||
<FileEditor content="# Heading" onChange={vi.fn()} filePath="readme.md" showLineNumbers />,
|
||||
<FileEditor
|
||||
content="# Heading"
|
||||
onChange={vi.fn()}
|
||||
filePath="readme.md"
|
||||
showLineNumbers
|
||||
onToggleLineNumbers={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /preview mode/i }));
|
||||
expect(document.querySelector(".file-editor-line-numbers")).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: /toggle line numbers/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("hides line numbers for read-only files", () => {
|
||||
render(
|
||||
<FileEditor content={"one\ntwo"} onChange={vi.fn()} filePath="file.bin" readOnly showLineNumbers />,
|
||||
<FileEditor
|
||||
content={"one\ntwo"}
|
||||
onChange={vi.fn()}
|
||||
filePath="file.bin"
|
||||
readOnly
|
||||
showLineNumbers
|
||||
onToggleLineNumbers={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(document.querySelector(".file-editor-line-numbers")).not.toBeInTheDocument();
|
||||
|
||||
Reference in New Issue
Block a user