feat(KB-264): add word wrap toggle and fix markdown preview scrolling

- Add word wrap toggle button with WrapText icon to FileEditor

- Fix markdown preview scrolling with height: 100% CSS

- Add comprehensive tests for word wrap toggle and scrollability
This commit is contained in:
gsxdsm
2026-03-30 23:39:40 -07:00
parent 2cd382b7eb
commit 8d8ccef302
3 changed files with 116 additions and 4 deletions

View File

@@ -177,4 +177,81 @@ describe("FileEditor", () => {
expect(editButton).toBeDisabled(); expect(editButton).toBeDisabled();
}); });
}); });
describe("word wrap toggle", () => {
it("shows word wrap toggle button for markdown files in edit mode", () => {
render(<FileEditor content="# Hello" onChange={vi.fn()} filePath="readme.md" />);
expect(screen.getByRole("button", { name: /toggle word wrap/i })).toBeInTheDocument();
});
it("shows word wrap toggle button for non-markdown files", () => {
render(<FileEditor content="const x = 1;" onChange={vi.fn()} filePath="script.ts" />);
expect(screen.getByRole("button", { name: /toggle word wrap/i })).toBeInTheDocument();
});
it("does not show word wrap toggle button in readOnly mode", () => {
render(<FileEditor content="# Hello" onChange={vi.fn()} filePath="readme.md" readOnly />);
expect(screen.queryByRole("button", { name: /toggle word wrap/i })).not.toBeInTheDocument();
});
it("word wrap is enabled by default", () => {
render(<FileEditor content="const x = 1;" onChange={vi.fn()} filePath="script.ts" />);
const textarea = screen.getByRole("textbox");
expect(textarea.classList.contains("file-editor-textarea--wrap")).toBe(true);
});
it("toggle button shows active state when word wrap is enabled", () => {
render(<FileEditor content="const x = 1;" onChange={vi.fn()} filePath="script.ts" />);
const wrapButton = screen.getByRole("button", { name: /toggle word wrap/i });
expect(wrapButton.classList.contains("btn-primary")).toBe(true);
});
it("clicking toggle button disables word wrap", () => {
render(<FileEditor content="const x = 1;" onChange={vi.fn()} filePath="script.ts" />);
const wrapButton = screen.getByRole("button", { name: /toggle word wrap/i });
fireEvent.click(wrapButton);
const textarea = screen.getByRole("textbox");
expect(textarea.classList.contains("file-editor-textarea--wrap")).toBe(false);
});
it("clicking toggle button again re-enables word wrap", () => {
render(<FileEditor content="const x = 1;" onChange={vi.fn()} filePath="script.ts" />);
const wrapButton = screen.getByRole("button", { name: /toggle word wrap/i });
fireEvent.click(wrapButton); // turn off
fireEvent.click(wrapButton); // turn on
const textarea = screen.getByRole("textbox");
expect(textarea.classList.contains("file-editor-textarea--wrap")).toBe(true);
});
it("toggle button loses active state when word wrap is disabled", () => {
render(<FileEditor content="const x = 1;" onChange={vi.fn()} filePath="script.ts" />);
const wrapButton = screen.getByRole("button", { name: /toggle word wrap/i });
fireEvent.click(wrapButton);
expect(wrapButton.classList.contains("btn-primary")).toBe(false);
});
});
describe("markdown preview scrollability", () => {
it("preview container has correct CSS classes for scrolling", () => {
render(<FileEditor content="# Hello World" onChange={vi.fn()} filePath="readme.md" />);
// Switch to preview mode
const previewButton = screen.getByRole("button", { name: /preview/i });
fireEvent.click(previewButton);
const preview = document.querySelector(".file-editor-preview");
expect(preview).toBeInTheDocument();
});
});
}); });

View File

@@ -1,7 +1,7 @@
import { useState, useCallback } from "react"; import { useState, useCallback } from "react";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
import { FileEdit, Eye } from "lucide-react"; import { FileEdit, Eye, WrapText } from "lucide-react";
interface FileEditorProps { interface FileEditorProps {
content: string; content: string;
@@ -18,6 +18,7 @@ function isMarkdownFile(filePath?: string): boolean {
export function FileEditor({ content, onChange, readOnly, filePath }: FileEditorProps) { export function FileEditor({ content, onChange, readOnly, filePath }: FileEditorProps) {
const [showPreview, setShowPreview] = useState(false); const [showPreview, setShowPreview] = useState(false);
const [wordWrap, setWordWrap] = useState(true);
const isMarkdown = isMarkdownFile(filePath); const isMarkdown = isMarkdownFile(filePath);
// For markdown files in readOnly mode, default to preview // For markdown files in readOnly mode, default to preview
@@ -31,9 +32,13 @@ export function FileEditor({ content, onChange, readOnly, filePath }: FileEditor
setShowPreview(true); setShowPreview(true);
}, []); }, []);
const handleWordWrapToggle = useCallback(() => {
setWordWrap((prev) => !prev);
}, []);
return ( return (
<div className="file-editor-container"> <div className="file-editor-container">
{isMarkdown && ( {isMarkdown ? (
<div className="file-editor-toolbar"> <div className="file-editor-toolbar">
<div className="file-editor-mode-toggle"> <div className="file-editor-mode-toggle">
{!readOnly && ( {!readOnly && (
@@ -57,7 +62,31 @@ export function FileEditor({ content, onChange, readOnly, filePath }: FileEditor
Preview Preview
</button> </button>
</div> </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> </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>
)
)} )}
{effectiveShowPreview ? ( {effectiveShowPreview ? (
@@ -68,7 +97,7 @@ export function FileEditor({ content, onChange, readOnly, filePath }: FileEditor
</div> </div>
) : ( ) : (
<textarea <textarea
className="file-editor-textarea" className={`file-editor-textarea ${wordWrap ? "file-editor-textarea--wrap" : ""}`}
value={content} value={content}
onChange={(e) => onChange(e.target.value)} onChange={(e) => onChange(e.target.value)}
readOnly={readOnly} readOnly={readOnly}

View File

@@ -6279,9 +6279,10 @@ html .column.drag-over * {
/* File Editor Preview */ /* File Editor Preview */
.file-editor-preview { .file-editor-preview {
flex: 1; flex: 1;
height: 100%;
min-height: 0;
overflow-y: auto; overflow-y: auto;
padding: 16px; padding: 16px;
min-height: 0;
} }
/* Textarea-based file editor */ /* Textarea-based file editor */
@@ -6307,6 +6308,11 @@ html .column.drag-over * {
tab-size: 2; tab-size: 2;
} }
.file-editor-textarea.file-editor-textarea--wrap {
white-space: pre-wrap;
overflow-wrap: break-word;
}
.file-editor-textarea:focus { .file-editor-textarea:focus {
outline: 1px solid var(--border); outline: 1px solid var(--border);
outline-offset: -1px; outline-offset: -1px;