Skip to content

feat: implement find and replace functionality#7

Merged
gameroman merged 9 commits intomainfrom
feat/find-replace
Mar 22, 2026
Merged

feat: implement find and replace functionality#7
gameroman merged 9 commits intomainfrom
feat/find-replace

Conversation

@gameroman
Copy link
Member

@gameroman gameroman commented Mar 22, 2026

Closes #5

Summary by CodeRabbit

  • New Features

    • Find & replace UI with highlighting, match counter, navigation, single/replace-all actions, and editor caret preservation.
    • Toolbar adds a search button to open the find/replace modal.
    • SVG icons accept a configurable viewBox.
  • Bug Fixes

    • Safer page-update logic to avoid applying edits to missing entries.
  • Tests

    • Added unit tests for match-finding behavior.
  • Documentation

    • Added a brief guide listing lint/format/test commands.
  • Chores

    • Added a test script and new imports alias; CI now runs tests.

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Mar 22, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
editor b198652 Commit Preview URL

Branch Preview URL
Mar 22 2026, 10:10 PM

@coderabbitai
Copy link

coderabbitai bot commented Mar 22, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds find-and-replace: new getMatches utility and tests, FindReplaceModal UI, App search state/handlers, Editor switched to contentEditable with highlighting and caret management, Toolbar search button and SvgIcon viewBox prop, package.json imports alias and test script, and CI updated to run tests.

Changes

Cohort / File(s) Summary
Config & CI
package.json, .github/workflows/ci.yml, biome.jsonc
Added #lib/*./src/lib/*.ts imports alias and test script (bun test); CI job renamed to "Test and Build" and now runs tests between lint and build; new Biome style/complexity rules added.
Search Utility & Tests
src/lib/get-matches.ts, tests/get-matches.lib.test.ts
New exported getMatches(text, term, caseSensitive?) that returns all (including overlapping) match start indices; Bun tests added covering case sensitivity, empties, no-matches, and multiples.
Find/Replace UI
src/components/FindReplaceModal.tsx
New modal component with controlled find/replace inputs, navigation, case-toggle, Enter-key handlers, replace/replace-all callbacks, and exported Direction type.
Editor
src/components/Editor.tsx
Replaced textarea with contentEditable div; introduced EditorProps including searchTerm, caseSensitive, currentMatchIndex, and isSearchOpen; escapes HTML and highlights matches with <mark>; preserves/restores caret and moves caret to selected match.
App Integration
src/components/App.tsx
Added search signals and handlers (isSearchOpen, searchTerm, currentMatchIndex, navigation, replace, replaceAll), computes matches via getMatches, wires FindReplaceModal, and passes search props to Editor.
Toolbar & Icons
src/components/Toolbar.tsx, src/components/ui/SvgIcon.tsx
Added SearchSvgIcon and search button; Toolbar accepts onSearchClick. SvgIcon gains optional viewBox prop (fallback preserved).
Hooks
src/hooks/usePages.ts
Guarded updatePageContent and renamePage to ensure target page exists before cloning/updating (removed non-null assertion overwrites).
Docs
AGENTS.md
Added documentation referencing Bun commands including bun run test.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Toolbar
    participant App
    participant Modal as FindReplaceModal
    participant Matches as getMatches
    participant Editor

    User->>Toolbar: click search
    Toolbar->>App: onSearchClick()
    App->>App: set isSearchOpen = true
    App->>Modal: render (isOpen = true)

    User->>Modal: type search term
    Modal->>App: onSearchTermChange(term)
    App->>Matches: getMatches(content, term, caseSensitive)
    Matches-->>App: indices[]
    App->>Editor: update props (searchTerm, currentMatchIndex, isSearchOpen)
    Editor->>Editor: highlight matches & manage caret

    User->>Modal: click "Replace" / "Replace All"
    Modal->>App: onReplace(repl) / onReplaceAll(repl)
    App->>App: update content (replace at indices)
    App->>Editor: render updated content
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through lines to find each clue,

Marked the matches in a bright-yellow hue,
One nibble here or sweep them all away,
Tests and CI hum as they dance and play,
Hoppy replaces — code sprints into day!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: implement find and replace functionality' is concise, clear, and directly describes the main change: adding find and replace capability to the editor.
Linked Issues check ✅ Passed The pull request implements comprehensive find and replace functionality including a new modal UI, match navigation, case-sensitive matching, and replace/replace-all operations, fully addressing issue #5's objective.
Out of Scope Changes check ✅ Passed All changes are scoped to find and replace functionality implementation, with supporting changes to configuration, testing infrastructure, and minor refactoring of usePages logic aligned with the feature requirement.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/find-replace

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (4)
src/components/FindReplaceModal.tsx (2)

5-16: Remove unused content prop.

The content prop is defined in FindReplaceModalProps (line 8) but is never used within the component. This creates unnecessary coupling.

♻️ Remove unused prop
 interface FindReplaceModalProps {
   isOpen: boolean;
   onClose: () => void;
-  content: string;
   searchTerm: string;
   onSearchTermChange: (term: string) => void;

Also update the call site in App.tsx to remove content={content()}.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/FindReplaceModal.tsx` around lines 5 - 16, The
FindReplaceModalProps interface declares an unused prop named content; remove
the content field from the interface and any corresponding prop usage (there
should be none inside FindReplaceModal component) and update the component call
site (e.g., in App.tsx) to stop passing content={content()} so the prop
signature and callers remain consistent; reference the FindReplaceModalProps
type and the FindReplaceModal component as the locations to change.

57-76: Add Escape key handler to close the modal.

Users typically expect pressing Escape to close modal dialogs. Consider adding keyboard support for better UX.

⌨️ Add Escape handler
+ import { createSignal, type JSX, Show, onMount, onCleanup } from "solid-js";

 const FindReplaceModal = (props: FindReplaceModalProps): JSX.Element => {
   const [replaceText, setReplaceText] = createSignal("");

+  const handleKeyDown = (e: KeyboardEvent) => {
+    if (e.key === "Escape" && props.isOpen) {
+      handleClose();
+    }
+  };
+
+  onMount(() => document.addEventListener("keydown", handleKeyDown));
+  onCleanup(() => document.removeEventListener("keydown", handleKeyDown));

   const handleFindKeyDown = (e: KeyboardEvent) => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/FindReplaceModal.tsx` around lines 57 - 76, Add Escape key
handling so pressing Escape closes the modal: update the keyboard handlers
(handleFindKeyDown and handleReplaceKeyDown) to detect e.key === "Escape" (or
"Esc" for cross-browser) and call the existing handleClose (which clears replace
text and calls props.onClose()). Alternatively attach a single keydown listener
that calls handleClose on Escape; ensure you preventDefault if needed and clean
up any global listener on unmount.
tests/get-matches.lib.test.ts (1)

4-7: Expand test coverage for edge cases.

The current test only covers the basic happy path. Consider adding tests for:

  • Empty search term (should return [])
  • No matches found
  • Multiple matches
  • Overlapping matches (e.g., "aaa" in "aaaa" should return [0, 1, 2] based on current implementation)
  • Case-insensitive matching
🧪 Additional test cases
it("should return empty array for empty search term", () => {
  expect(getMatches("hello world", "")).toEqual([]);
});

it("should return empty array when no matches", () => {
  expect(getMatches("hello world", "foo")).toEqual([]);
});

it("should find multiple matches", () => {
  expect(getMatches("hello hello", "hello")).toEqual([0, 6]);
});

it("should find overlapping matches", () => {
  expect(getMatches("aaaa", "aa")).toEqual([0, 1, 2]);
});

it("should be case insensitive", () => {
  expect(getMatches("Hello HELLO", "hello")).toEqual([0, 6]);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/get-matches.lib.test.ts` around lines 4 - 7, Add comprehensive unit
tests for getMatches to cover edge cases beyond the happy path: add tests
asserting that an empty search term returns [], a search with no matches returns
[], multiple non-overlapping matches (e.g., "hello hello" vs "hello") returns
[0,6], overlapping matches (e.g., "aaaa" vs "aa") returns [0,1,2], and
case-insensitive matching (e.g., "Hello HELLO" vs "hello") returns [0,6]; update
tests in tests/get-matches.lib.test.ts using the existing describe("getMatches",
...) block and the getMatches function name so CI validates these behaviors.
src/components/Editor.tsx (1)

90-100: Duplicate getMatches implementation with incompatible return type.

There's a local getMatches function (lines 132-145) returning Match[] with {start, end} objects, while src/lib/get-matches.ts exports a function returning number[]. The createEffect at line 92 uses the local version since there's no import, but this creates maintenance burden and inconsistency.

Consider importing and adapting the shared utility, or extracting this version to the shared module if the Match interface is needed elsewhere.

♻️ Suggested approach

Either update src/lib/get-matches.ts to return Match[] objects, or import it and map the results:

+import { getMatches as getMatchPositions } from "#lib/get-matches";

// Then in createEffect:
-      const matches = getMatches(props.content, props.searchTerm);
+      const positions = getMatchPositions(props.content, props.searchTerm);
+      const matches: Match[] = positions.map(pos => ({ 
+        start: pos, 
+        end: pos + props.searchTerm.length 
+      }));

And remove the local getMatches function (lines 132-145).

Also applies to: 132-145

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Editor.tsx` around lines 90 - 100, The Editor component
defines a local getMatches (returning Match[] with {start,end}) that conflicts
with the shared src/lib/get-matches.ts (returns number[]), causing duplication
and inconsistent types; fix by removing the local getMatches from Editor.tsx,
import the shared getMatches from src/lib/get-matches.ts into Editor.tsx, and
then either (A) update the shared getMatches to return Match[] (preferred if
multiple callers need ranges) or (B) map the imported number[] to Match[] inside
Editor (e.g., numbers -> {start: n, end: n + term.length}) so createEffect,
setCaretPosition, and any use of matches/currentMatchIndex continue to work with
the Match shape; ensure you update any types/usages (Match interface,
createEffect usage) accordingly and delete the now-unused local function.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/App.tsx`:
- Around line 78-81: The replace-all logic in handleReplaceAll creates a RegExp
with only the "g" flag, causing replacements to be case-sensitive while
getMatches is case-insensitive; update the RegExp creation for newContent (the
call to text.replace using new RegExp(RegExp.escape(searchTerm()), "g")) to
include the "i" flag (e.g., use "gi") so replacements mirror getMatches'
case-insensitive behavior while still escaping the searchTerm().

In `@src/components/Editor.tsx`:
- Around line 67-82: renderContent currently builds the regex from the raw
props.searchTerm but runs it against the HTML-escaped text, so terms with HTML
entities (e.g. "<" or "&") won't match; fix by HTML-escaping the search term
(via escapeHtml) first, then escape regex metacharacters on that
escapedSearchTerm before constructing the RegExp, and use that regex to replace
within the escaped text; update references in renderContent to use
escapedSearchTerm (and still handle empty/null searchTerm/text as before).
- Around line 104-121: The issue is that binding innerHTML={renderContent()} on
the contentEditable div resets the caret on every keystroke; stop setting
innerHTML on every render and only mutate the DOM when necessary (e.g., when
searchTerm or content-formatting changes). Replace the direct innerHTML prop
with a controlled update inside an effect: use editorRef to set innerHTML inside
a useEffect that depends on props.searchTerm (and initial mount/format changes)
rather than props.content on every keystroke, and ensure handleInput still calls
props.onChange without overriding the live DOM; also update setCaretPosition to
run when you intentionally update DOM for search highlights so selection
restoration occurs when you change innerHTML. Use the function names editorRef,
handleInput, renderContent, setCaretPosition, props.searchTerm, props.content,
and props.onChange to locate where to apply this change.

In `@src/components/Toolbar.tsx`:
- Around line 79-84: SearchSvgIcon currently draws using 24×24 coordinates but
inherits SvgIcon's default viewBox="0 0 240 240", causing it to render too
small; fix by adding an explicit viewBox="0 0 24 24" prop to the SvgIcon element
inside the SearchSvgIcon component (SearchSvgIcon and SvgIcon are the
identifiers to update) so the circle (cx=11) and path coordinates render at the
correct scale.

---

Nitpick comments:
In `@src/components/Editor.tsx`:
- Around line 90-100: The Editor component defines a local getMatches (returning
Match[] with {start,end}) that conflicts with the shared src/lib/get-matches.ts
(returns number[]), causing duplication and inconsistent types; fix by removing
the local getMatches from Editor.tsx, import the shared getMatches from
src/lib/get-matches.ts into Editor.tsx, and then either (A) update the shared
getMatches to return Match[] (preferred if multiple callers need ranges) or (B)
map the imported number[] to Match[] inside Editor (e.g., numbers -> {start: n,
end: n + term.length}) so createEffect, setCaretPosition, and any use of
matches/currentMatchIndex continue to work with the Match shape; ensure you
update any types/usages (Match interface, createEffect usage) accordingly and
delete the now-unused local function.

In `@src/components/FindReplaceModal.tsx`:
- Around line 5-16: The FindReplaceModalProps interface declares an unused prop
named content; remove the content field from the interface and any corresponding
prop usage (there should be none inside FindReplaceModal component) and update
the component call site (e.g., in App.tsx) to stop passing content={content()}
so the prop signature and callers remain consistent; reference the
FindReplaceModalProps type and the FindReplaceModal component as the locations
to change.
- Around line 57-76: Add Escape key handling so pressing Escape closes the
modal: update the keyboard handlers (handleFindKeyDown and handleReplaceKeyDown)
to detect e.key === "Escape" (or "Esc" for cross-browser) and call the existing
handleClose (which clears replace text and calls props.onClose()). Alternatively
attach a single keydown listener that calls handleClose on Escape; ensure you
preventDefault if needed and clean up any global listener on unmount.

In `@tests/get-matches.lib.test.ts`:
- Around line 4-7: Add comprehensive unit tests for getMatches to cover edge
cases beyond the happy path: add tests asserting that an empty search term
returns [], a search with no matches returns [], multiple non-overlapping
matches (e.g., "hello hello" vs "hello") returns [0,6], overlapping matches
(e.g., "aaaa" vs "aa") returns [0,1,2], and case-insensitive matching (e.g.,
"Hello HELLO" vs "hello") returns [0,6]; update tests in
tests/get-matches.lib.test.ts using the existing describe("getMatches", ...)
block and the getMatches function name so CI validates these behaviors.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4541fde7-464c-41a0-97d5-254352e994ac

📥 Commits

Reviewing files that changed from the base of the PR and between 4add06f and 03ea892.

📒 Files selected for processing (7)
  • package.json
  • src/components/App.tsx
  • src/components/Editor.tsx
  • src/components/FindReplaceModal.tsx
  • src/components/Toolbar.tsx
  • src/lib/get-matches.ts
  • tests/get-matches.lib.test.ts

Comment on lines +104 to 121
<div
ref={editorRef}
contentEditable={true}
spellcheck={props.settings.spellcheck}
onInput={handleInput}
class="w-full absolute h-screen overflow-y-auto overflow-x-hidden
[word-break:break-word] text-black dark:text-white caret-blue-500
bg-transparent delay-500 [scrollbar-width:thin] scroll-smooth
bg-transparent [scrollbar-width:thin] scroll-smooth
p-[calc(min(1em,20vh)+72px)_max(-372px+50vw,1em)_min(5em,15vh)]
scroll-pb-0 left-0 top-0"
scroll-pb-0 left-0 top-0 outline-none whitespace-pre-wrap"
style={{
"font-size": `${props.settings.fontSize}px`,
"font-family": props.settings.fontFamily,
"text-align": props.settings.textAlign,
}}
spellcheck={props.settings.spellcheck}
onInput={(e) => props.onChange(e.currentTarget.value)}
value={props.content}
placeholder="Start writing..."
data-placeholder="Start writing..."
innerHTML={renderContent()}
/>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if there's any existing mechanism to preserve cursor position during typing
rg -n "selection|caret|cursor" src/components/Editor.tsx

Repository: rmandotdev/editor

Length of output: 259


🏁 Script executed:

wc -l src/components/Editor.tsx

Repository: rmandotdev/editor

Length of output: 90


🏁 Script executed:

cat -n src/components/Editor.tsx

Repository: rmandotdev/editor

Length of output: 5045


Setting innerHTML on contentEditable resets cursor position on every keystroke.

When the user types, handleInput triggers props.onChange, which changes props.content. This causes renderContent() to re-evaluate and innerHTML is set, resetting the cursor. The setCaretPosition effect (lines 90-100) only executes its cursor restoration logic when props.searchTerm is truthy, so during normal typing without an active search, the cursor is never restored.

Consider using a diffing approach to update only the changed text nodes, or only update innerHTML when searchTerm changes (not on every keystroke).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Editor.tsx` around lines 104 - 121, The issue is that binding
innerHTML={renderContent()} on the contentEditable div resets the caret on every
keystroke; stop setting innerHTML on every render and only mutate the DOM when
necessary (e.g., when searchTerm or content-formatting changes). Replace the
direct innerHTML prop with a controlled update inside an effect: use editorRef
to set innerHTML inside a useEffect that depends on props.searchTerm (and
initial mount/format changes) rather than props.content on every keystroke, and
ensure handleInput still calls props.onChange without overriding the live DOM;
also update setCaretPosition to run when you intentionally update DOM for search
highlights so selection restoration occurs when you change innerHTML. Use the
function names editorRef, handleInput, renderContent, setCaretPosition,
props.searchTerm, props.content, and props.onChange to locate where to apply
this change.

@gameroman gameroman self-assigned this Mar 22, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (3)
src/components/App.tsx (1)

78-87: ⚠️ Potential issue | 🟠 Major

Replace All should respect case sensitivity setting.

handleReplaceAll always uses just the "g" flag, but getMatches (which determines visible matches) respects caseSensitive(). When case-insensitive matching is active, replacements won't match all highlighted occurrences.

🔧 Proposed fix
  const handleReplaceAll = (replacement: string) => {
    if (!searchTerm()) return;
    const text = content();
+   const flags = caseSensitive() ? "g" : "gi";
    const newContent = text.replace(
-     new RegExp(RegExp.escape(searchTerm()), "g"),
+     new RegExp(RegExp.escape(searchTerm()), flags),
      replacement,
    );
    updatePageContent(newContent);
    setCurrentMatchIndex(0);
  };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/App.tsx` around lines 78 - 87, handleReplaceAll currently
always builds the replacement RegExp with only the "g" flag, so replacements
don't honor the caseSensitive() setting used by getMatches; change the RegExp
construction to include the "i" flag when caseSensitive() is false (e.g.,
compute flags = caseSensitive() ? "g" : "gi") and use new
RegExp(RegExp.escape(searchTerm()), flags) so replacements match the same items
getMatches highlights; keep the rest of the flow (updatePageContent and
setCurrentMatchIndex).
src/components/Editor.tsx (2)

89-111: ⚠️ Potential issue | 🟠 Major

Cursor resets during normal typing when search is inactive.

The effect updates innerHTML on every content change, but cursor restoration (line 108-110) only runs when searchTerm is truthy. During normal typing without an active search, the cursor will jump to the end after each keystroke.

Consider either:

  1. Always restoring the cursor position, or
  2. Skipping the innerHTML update when there's no active search and content hasn't changed structurally
🔧 Proposed fix — always restore cursor
    editorRef.innerHTML = html;

-   if (searchTerm && hadSelection) {
+   if (hadSelection) {
      setCaretPosition(editorRef, savedOffset);
    }
  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Editor.tsx` around lines 89 - 111, The effect replaces
editorRef.innerHTML on every run but only restores the caret when
props.searchTerm is set, causing the cursor to jump during normal typing; update
the logic in createEffect so after computing html via renderContent() and
assigning editorRef.innerHTML = html you always restore the caret when a prior
selection was detected (use the savedOffset/hadSelection variables and call
setCaretPosition(editorRef, savedOffset) regardless of props.searchTerm), and
optionally avoid unnecessary DOM rewrites by comparing the new html with
editorRef.innerHTML before assigning to skip innerHTML updates when unchanged.

68-81: ⚠️ Potential issue | 🟡 Minor

Search term with HTML entities still won't match correctly.

The previous review flagged this issue: if searchTerm contains <, >, or &, the regex is built from the raw term but applied against HTML-escaped content. For example, searching for <div> looks for <div> in text containing &lt;div&gt;.

The fix should HTML-escape the search term before regex-escaping it.

🔧 Proposed fix
  const renderContent = (): string => {
    const text = props.content;
    if (!props.searchTerm || !text) return escapeHtml(text);

    const escaped = escapeHtml(text);
-   const searchEscaped = RegExp.escape(props.searchTerm);
+   const searchEscaped = RegExp.escape(escapeHtml(props.searchTerm));
    const flags = props.caseSensitive ? "" : "i";
    const regex = new RegExp(`(${searchEscaped})`, `g${flags}`);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Editor.tsx` around lines 68 - 81, The current renderContent
builds a regex from props.searchTerm and applies it to HTML-escaped content, so
terms like "<div>" won't match; fix by HTML-escaping the search term before
regex-escaping: call escapeHtml on props.searchTerm first, then pass that result
to RegExp.escape (preserving caseSensitive flag logic) so the constructed regex
(in renderContent) matches the escaped content; update the sequence around
RegExp.escape(props.searchTerm) to use escapeHtml(props.searchTerm) and keep
existing RegExp/String handling.
🧹 Nitpick comments (1)
src/components/FindReplaceModal.tsx (1)

20-36: Consider reducing stroke-width for the icons.

The stroke-width="20" on a 24x24 viewBox will render extremely thick strokes (nearly solid shapes). Icons at this scale typically use a stroke-width of 1.5–2.5. The toolbar's SearchSvgIcon uses stroke-width={20} on the same coordinates but with a 240x240 viewBox, so the relative thickness is 10× less.

♻️ Suggested fix
  const SearchIcon = (): JSX.Element => (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      width="18"
      height="18"
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
-     stroke-width="20"
+     stroke-width="2"
      stroke-linecap="round"
      stroke-linejoin="round"
      aria-label="Search"
    >
  const CloseIcon = (): JSX.Element => (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      width="18"
      height="18"
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
-     stroke-width="20"
+     stroke-width="2"
      stroke-linecap="round"
      stroke-linejoin="round"
      aria-label="Close"
    >

Also applies to: 38-54

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/FindReplaceModal.tsx` around lines 20 - 36, The SVG SearchIcon
uses an excessively large stroke-width ("20") for a 24x24 viewBox; update the
SVG props on SearchIcon (and the other icons at 38-54) to use a normal icon
stroke thickness (e.g., 1.5–2) and use JSX prop names (strokeWidth,
strokeLinecap, strokeLinejoin) instead of the HTML-style hyphenated attributes
so the rendered stroke is correctly sized and the JSX compiles.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/App.tsx`:
- Around line 72-76: The call to getMatches when recomputing after a replacement
is missing the caseSensitive argument, causing inconsistent match counts; update
the call in the block that defines newMatches (the code that currently does
const newMatches = getMatches(newContent, searchTerm()); and then
setCurrentMatchIndex(...)) to pass the same case sensitivity flag used elsewhere
(the caseSensitive state/selector used in the memo on lines ~42-44), i.e. call
getMatches(newContent, searchTerm(), caseSensitive) (or caseSensitive()) so
newMatches uses the same matching rules before calling setCurrentMatchIndex.

---

Duplicate comments:
In `@src/components/App.tsx`:
- Around line 78-87: handleReplaceAll currently always builds the replacement
RegExp with only the "g" flag, so replacements don't honor the caseSensitive()
setting used by getMatches; change the RegExp construction to include the "i"
flag when caseSensitive() is false (e.g., compute flags = caseSensitive() ? "g"
: "gi") and use new RegExp(RegExp.escape(searchTerm()), flags) so replacements
match the same items getMatches highlights; keep the rest of the flow
(updatePageContent and setCurrentMatchIndex).

In `@src/components/Editor.tsx`:
- Around line 89-111: The effect replaces editorRef.innerHTML on every run but
only restores the caret when props.searchTerm is set, causing the cursor to jump
during normal typing; update the logic in createEffect so after computing html
via renderContent() and assigning editorRef.innerHTML = html you always restore
the caret when a prior selection was detected (use the savedOffset/hadSelection
variables and call setCaretPosition(editorRef, savedOffset) regardless of
props.searchTerm), and optionally avoid unnecessary DOM rewrites by comparing
the new html with editorRef.innerHTML before assigning to skip innerHTML updates
when unchanged.
- Around line 68-81: The current renderContent builds a regex from
props.searchTerm and applies it to HTML-escaped content, so terms like "<div>"
won't match; fix by HTML-escaping the search term before regex-escaping: call
escapeHtml on props.searchTerm first, then pass that result to RegExp.escape
(preserving caseSensitive flag logic) so the constructed regex (in
renderContent) matches the escaped content; update the sequence around
RegExp.escape(props.searchTerm) to use escapeHtml(props.searchTerm) and keep
existing RegExp/String handling.

---

Nitpick comments:
In `@src/components/FindReplaceModal.tsx`:
- Around line 20-36: The SVG SearchIcon uses an excessively large stroke-width
("20") for a 24x24 viewBox; update the SVG props on SearchIcon (and the other
icons at 38-54) to use a normal icon stroke thickness (e.g., 1.5–2) and use JSX
prop names (strokeWidth, strokeLinecap, strokeLinejoin) instead of the
HTML-style hyphenated attributes so the rendered stroke is correctly sized and
the JSX compiles.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6efae633-a790-4a81-a5f2-a3d3b4c90d5c

📥 Commits

Reviewing files that changed from the base of the PR and between 89f9e7b and c4c396a.

📒 Files selected for processing (8)
  • AGENTS.md
  • src/components/App.tsx
  • src/components/Editor.tsx
  • src/components/FindReplaceModal.tsx
  • src/components/Toolbar.tsx
  • src/components/ui/SvgIcon.tsx
  • src/lib/get-matches.ts
  • tests/get-matches.lib.test.ts
✅ Files skipped from review due to trivial changes (1)
  • AGENTS.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/lib/get-matches.ts
  • tests/get-matches.lib.test.ts

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/components/Editor.tsx (1)

132-148: Consider adding accessibility attributes.

The contentEditable div acts as a text input but lacks ARIA attributes to communicate its role to assistive technologies.

♿ Suggested accessibility attributes
       <div
         ref={editorRef}
         contentEditable={true}
+        role="textbox"
+        aria-multiline="true"
+        aria-label="Document editor"
         spellcheck={props.settings.spellcheck}
         onInput={handleInput}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Editor.tsx` around lines 132 - 148, The contentEditable div
used with editorRef and handleInput should expose proper ARIA and focus
attributes so assistive tech recognizes it as an editable text field—add
role="textbox", aria-multiline="true", a descriptive aria-label or
aria-labelledby (e.g., referencing a visible label), and tabIndex={0} so it’s
keyboard-focusable; also include an accessible placeholder via aria-placeholder
or ensure the placeholder text is exposed through aria-label/aria-labelledby
when empty and keep spellcheck/font props from props.settings unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/Editor.tsx`:
- Around line 114-128: The effect that calls getMatches and setCaretPosition
(createEffect using editorRef, props.searchTerm, props.content, getMatches,
props.currentMatchIndex) should only run when the search index actually changes
to avoid jumping caret while typing; implement a previous-index tracker (a local
mutable ref or a createSignal to hold prevIndex) and early-return unless
props.currentMatchIndex is defined and differs from prevIndex, then compute
matches (or reuse cached matches) and call setCaretPosition(editorRef,
match.start) only when the index changed, finally update prevIndex to
props.currentMatchIndex; also remove props.content from the reactive
dependencies for this navigation effect so typing doesn’t trigger it.
- Line 73: The usage of the ES2025 RegExp.escape in Editor.tsx (the line
creating searchEscaped via RegExp.escape(escapeHtml(props.searchTerm))) can
throw in older browsers; replace it with a safe fallback or polyfill: add a
small compatibility check at module scope (e.g., if (typeof RegExp.escape !==
"function") { RegExp.escape = /* fallback implementation */ }) or implement a
local helper function (e.g., safeRegExpEscape) and use that instead of
RegExp.escape when building searchEscaped; update the code that references
RegExp.escape (the line using escapeHtml(props.searchTerm)) to call the
fallback/helper to ensure runtime compatibility.

---

Nitpick comments:
In `@src/components/Editor.tsx`:
- Around line 132-148: The contentEditable div used with editorRef and
handleInput should expose proper ARIA and focus attributes so assistive tech
recognizes it as an editable text field—add role="textbox",
aria-multiline="true", a descriptive aria-label or aria-labelledby (e.g.,
referencing a visible label), and tabIndex={0} so it’s keyboard-focusable; also
include an accessible placeholder via aria-placeholder or ensure the placeholder
text is exposed through aria-label/aria-labelledby when empty and keep
spellcheck/font props from props.settings unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8cbca455-6ede-4cb3-89f5-571a506381e9

📥 Commits

Reviewing files that changed from the base of the PR and between c4c396a and 6d6f176.

📒 Files selected for processing (5)
  • biome.jsonc
  • src/components/App.tsx
  • src/components/Editor.tsx
  • src/components/FindReplaceModal.tsx
  • src/hooks/usePages.ts
✅ Files skipped from review due to trivial changes (1)
  • biome.jsonc
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/components/FindReplaceModal.tsx
  • src/components/App.tsx

if (!props.searchTerm || !text) return escapeHtml(text);

const escaped = escapeHtml(text);
const searchEscaped = RegExp.escape(escapeHtml(props.searchTerm));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

RegExp.escape browser support ES2024

💡 Result:

RegExp.escape() is not an ES2024 feature; it’s standardized in ECMAScript 2025 (ES2025) (added to the spec as RegExp.escape). [3]

Browser support (from “Can I use”): [1]

  • Chrome: 136+
  • Edge: 136+
  • Firefox: 134+
  • Safari (macOS): 18.2+
  • iOS Safari: 18.2+
  • Samsung Internet: 29+

MDN summarizes this as becoming broadly available across “latest” browsers since May 2025. [2]

Sources:
[1] Can I use: RegExp.escape() (caniuse.com)
[2] MDN: RegExp.escape() (Baseline 2025 / since May 2025) (developer.mozilla.org)
[3] ECMA-262 (ECMAScript 2025) PDF mentioning RegExp.escape (ecma-international.org)

Citations:


Add a polyfill or compatibility check for RegExp.escape.

RegExp.escape is an ES2025 feature with limited browser support (Chrome 136+, Firefox 134+, Safari 18.2+, and others as of May 2025). Browsers below these versions will throw a runtime error. Either add a polyfill or verify your target environments support it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Editor.tsx` at line 73, The usage of the ES2025 RegExp.escape
in Editor.tsx (the line creating searchEscaped via
RegExp.escape(escapeHtml(props.searchTerm))) can throw in older browsers;
replace it with a safe fallback or polyfill: add a small compatibility check at
module scope (e.g., if (typeof RegExp.escape !== "function") { RegExp.escape =
/* fallback implementation */ }) or implement a local helper function (e.g.,
safeRegExpEscape) and use that instead of RegExp.escape when building
searchEscaped; update the code that references RegExp.escape (the line using
escapeHtml(props.searchTerm)) to call the fallback/helper to ensure runtime
compatibility.

Comment on lines +114 to +128
createEffect(() => {
if (editorRef && props.searchTerm) {
const matches = getMatches(
props.content,
props.searchTerm,
props.caseSensitive,
);
if (matches.length > 0 && props.currentMatchIndex !== undefined) {
const match = matches[props.currentMatchIndex];
if (match) {
setCaretPosition(editorRef, match.start);
}
}
}
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Effect reacts to content changes, causing caret to jump during typing.

This effect tracks props.content (via getMatches). When the user types with an active search, both effects run: the first effect restores caret to typing position, then this effect immediately moves it to the current match position—disrupting typing.

Consider using a previous-value pattern to only navigate when currentMatchIndex actually changes:

🔧 Proposed fix using previous index tracking
+import { createEffect, createSignal, type JSX } from "solid-js";
...

 function Editor(props: EditorProps): JSX.Element {
   let editorRef: HTMLDivElement | undefined;
+  const [prevMatchIndex, setPrevMatchIndex] = createSignal<number | undefined>(undefined);

   // ... existing code ...

   createEffect(() => {
-    if (editorRef && props.searchTerm) {
+    const currentIndex = props.currentMatchIndex;
+    const searchTerm = props.searchTerm;
+    
+    if (editorRef && searchTerm && currentIndex !== undefined && currentIndex !== prevMatchIndex()) {
+      setPrevMatchIndex(currentIndex);
       const matches = getMatches(
         props.content,
-        props.searchTerm,
+        searchTerm,
         props.caseSensitive,
       );
-      if (matches.length > 0 && props.currentMatchIndex !== undefined) {
-        const match = matches[props.currentMatchIndex];
+      if (matches.length > 0) {
+        const match = matches[currentIndex];
         if (match) {
           setCaretPosition(editorRef, match.start);
         }
       }
     }
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Editor.tsx` around lines 114 - 128, The effect that calls
getMatches and setCaretPosition (createEffect using editorRef, props.searchTerm,
props.content, getMatches, props.currentMatchIndex) should only run when the
search index actually changes to avoid jumping caret while typing; implement a
previous-index tracker (a local mutable ref or a createSignal to hold prevIndex)
and early-return unless props.currentMatchIndex is defined and differs from
prevIndex, then compute matches (or reuse cached matches) and call
setCaretPosition(editorRef, match.start) only when the index changed, finally
update prevIndex to props.currentMatchIndex; also remove props.content from the
reactive dependencies for this navigation effect so typing doesn’t trigger it.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (2)
src/components/Editor.tsx (2)

74-76: ⚠️ Potential issue | 🟠 Major

Avoid hard-requiring RegExp.escape() in the render path.

Older runtimes will throw here before search highlighting even renders. A tiny local escape helper gives the same behavior without depending on that newer API.

What is the current browser support for `RegExp.escape()` across Chrome, Firefox, Safari, and Edge?
🛡️ Compatibility-safe alternative
+function escapeRegExp(value: string): string {
+  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+}
+
 function Editor(props: EditorProps): JSX.Element {
   let editorRef: HTMLDivElement | undefined;
@@
-    const searchEscaped = RegExp.escape(escapeHtml(props.searchTerm));
+    const searchEscaped = escapeRegExp(escapeHtml(props.searchTerm));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Editor.tsx` around lines 74 - 76, The render code calls
RegExp.escape which can throw in older runtimes; create a small local helper
(e.g., escapeRegExp) near the component and use it instead of RegExp.escape when
computing searchEscaped (still wrap props.searchTerm with escapeHtml first),
then build the regex the same way using the flags logic (props.caseSensitive ?
"" : "i") and new RegExp(`(${searchEscaped})`, `g${flags}`); update any
references to searchEscaped to use the helper so you avoid relying on the newer
RegExp.escape API.

115-129: ⚠️ Potential issue | 🟠 Major

Only run match navigation when the selected match actually changes.

This effect still tracks props.content. While the editor is focused, every keystroke with an active search can snap the caret back to currentMatchIndex; while the modal input has focus, the document.activeElement === editorRef guard means next/prev won't reveal anything at all. Gate this on index changes, and scroll the selected <mark> into view without depending on editor focus.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Editor.tsx` around lines 115 - 129, The effect should only run
when the selected match index (and search term) change instead of on every
content change; update the createEffect to depend on props.currentMatchIndex and
props.searchTerm, compute matches via getMatches(props.content,
props.searchTerm, props.caseSensitive), locate the corresponding match by index,
then find the rendered <mark> for that match (e.g. via a data-match-index
attribute or querySelector) and call element.scrollIntoView({ block: 'nearest',
inline: 'nearest', behavior: 'auto' }); only call setCaretPosition(editorRef,
match.start) if document.activeElement === editorRef; this prevents caret
snapping on unrelated edits while still scrolling the highlighted match into
view when navigation index changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/Editor.tsx`:
- Around line 69-82: renderContent() and getMatches() use different matching
rules causing mismatched highlights vs match counts (e.g., overlapping matches
like "ana" in "banana"); make them consistent by changing renderContent() to use
the same overlapping-match logic as getMatches() — use a lookahead-based regex
(e.g., (?=(...))) or iterate with regex.exec advancing by 1 char to find every
overlapping match, then build the escaped output by slicing and inserting the
<mark> wrappers at each match offset so the number and positions of rendered
marks match getMatches() and currentMatchIndex.

In `@src/components/FindReplaceModal.tsx`:
- Around line 88-95: Wrap the floating container div in dialog semantics by
adding role="dialog", aria-modal="true", and aria-labelledby pointing to the
Find & Replace title span (give that span a stable id) so assistive tech
recognizes it as a dialog; also ensure the dialog gets keyboard focus when
opened. For the case-sensitivity control (the "Aa" button) expose its pressed
state with aria-pressed (true/false) and include an accessible name (aria-label
like "Toggle case sensitivity") so screen readers announce its state; apply the
same changes to the duplicate popup instance in this component.

In `@src/components/Toolbar.tsx`:
- Around line 95-98: The icon-only search Button (the Button element with
variant="toolbar" and onClick={props.onSearchClick} in Toolbar.tsx) lacks an
accessible name; add an explicit aria-label (e.g., aria-label="Search" or a
localized label) to that Button so assistive tech can announce it, ensuring you
place the aria-label prop on the same Button element that renders <SearchSvgIcon
/>.

---

Duplicate comments:
In `@src/components/Editor.tsx`:
- Around line 74-76: The render code calls RegExp.escape which can throw in
older runtimes; create a small local helper (e.g., escapeRegExp) near the
component and use it instead of RegExp.escape when computing searchEscaped
(still wrap props.searchTerm with escapeHtml first), then build the regex the
same way using the flags logic (props.caseSensitive ? "" : "i") and new
RegExp(`(${searchEscaped})`, `g${flags}`); update any references to
searchEscaped to use the helper so you avoid relying on the newer RegExp.escape
API.
- Around line 115-129: The effect should only run when the selected match index
(and search term) change instead of on every content change; update the
createEffect to depend on props.currentMatchIndex and props.searchTerm, compute
matches via getMatches(props.content, props.searchTerm, props.caseSensitive),
locate the corresponding match by index, then find the rendered <mark> for that
match (e.g. via a data-match-index attribute or querySelector) and call
element.scrollIntoView({ block: 'nearest', inline: 'nearest', behavior: 'auto'
}); only call setCaretPosition(editorRef, match.start) if document.activeElement
=== editorRef; this prevents caret snapping on unrelated edits while still
scrolling the highlighted match into view when navigation index changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 69610b12-9945-4c99-b89b-2a7fd32281c6

📥 Commits

Reviewing files that changed from the base of the PR and between 6d6f176 and 4a21d3a.

📒 Files selected for processing (5)
  • biome.jsonc
  • src/components/App.tsx
  • src/components/Editor.tsx
  • src/components/FindReplaceModal.tsx
  • src/components/Toolbar.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • biome.jsonc
  • src/components/App.tsx

Comment on lines +69 to +82
const renderContent = (): string => {
const text = props.content;
if (!props.searchTerm || !text) return escapeHtml(text);

const escaped = escapeHtml(text);
const searchEscaped = RegExp.escape(escapeHtml(props.searchTerm));
const flags = props.caseSensitive ? "" : "i";
const regex = new RegExp(`(${searchEscaped})`, `g${flags}`);

return escaped.replace(
regex,
'<mark class="bg-yellow-300 dark:bg-yellow-600 rounded px-0.5">$1</mark>',
);
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Unify the matching rules used for highlights and indexes.

renderContent() uses global regex replacement, so it only marks non-overlapping hits, but getMatches() advances by one character and counts overlaps. Searches like ana in banana can report two matches while only one <mark> is rendered, and currentMatchIndex can point to a match the user never sees.

🔧 Minimal fix if overlaps are not intended
   while (found !== -1) {
     matches.push({ start: found, end: found + searchTerm.length });
-    pos = found + 1;
+    pos = found + searchTerm.length;
     found = searchContent.indexOf(search, pos);
   }

Also applies to: 172-186

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Editor.tsx` around lines 69 - 82, renderContent() and
getMatches() use different matching rules causing mismatched highlights vs match
counts (e.g., overlapping matches like "ana" in "banana"); make them consistent
by changing renderContent() to use the same overlapping-match logic as
getMatches() — use a lookahead-based regex (e.g., (?=(...))) or iterate with
regex.exec advancing by 1 char to find every overlapping match, then build the
escaped output by slicing and inserting the <mark> wrappers at each match offset
so the number and positions of rendered marks match getMatches() and
currentMatchIndex.

Comment on lines +88 to +95
<div
class="fixed top-14 right-4 z-50 bg-white dark:bg-[#1a1a1a]
border border-[#ccc] dark:border-[#444] rounded-lg shadow-xl p-4 min-w-80"
>
<div class="flex justify-between items-center mb-3">
<span class="text-black dark:text-white font-medium">
Find &amp; Replace
</span>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Expose the popup as a dialog and the case control as a toggle.

Assistive tech currently just gets a floating <div> and an Aa button with no state. Adding dialog semantics on the container plus aria-pressed on the case-sensitivity control makes the feature understandable without sight.

♿ Suggested accessibility fix
       <div
+        role="dialog"
+        aria-modal="true"
+        aria-labelledby="find-replace-title"
         class="fixed top-14 right-4 z-50 bg-white dark:bg-[`#1a1a1a`] 
                border border-[`#ccc`] dark:border-[`#444`] rounded-lg shadow-xl p-4 min-w-80"
       >
         <div class="flex justify-between items-center mb-3">
-          <span class="text-black dark:text-white font-medium">
+          <span
+            id="find-replace-title"
+            class="text-black dark:text-white font-medium"
+          >
             Find &amp; Replace
           </span>
@@
               <button
                 type="button"
                 onClick={() =>
                   props.onCaseSensitiveChange(!props.caseSensitive)
                 }
+                aria-label="Match case"
+                aria-pressed={props.caseSensitive}
                 class="px-2 py-1 text-xs font-mono bg-[`#f5f5f5`] dark:bg-[`#2a2a2a`] border border-[`#ddd`] 
                        dark:border-[`#444`] rounded text-black dark:text-white cursor-pointer
                        hover:bg-[`#eee`] dark:hover:bg-[`#333`]"

Also applies to: 151-162

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/FindReplaceModal.tsx` around lines 88 - 95, Wrap the floating
container div in dialog semantics by adding role="dialog", aria-modal="true",
and aria-labelledby pointing to the Find & Replace title span (give that span a
stable id) so assistive tech recognizes it as a dialog; also ensure the dialog
gets keyboard focus when opened. For the case-sensitivity control (the "Aa"
button) expose its pressed state with aria-pressed (true/false) and include an
accessible name (aria-label like "Toggle case sensitivity") so screen readers
announce its state; apply the same changes to the duplicate popup instance in
this component.

Comment on lines +95 to +98
<div class="flex gap-2">
<Button variant="toolbar" onClick={props.onSearchClick}>
<SearchSvgIcon />
</Button>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Label the new icon-only search button.

Right now this control renders as SVG-only content, so assistive tech will likely announce an unnamed button. Please pass an explicit aria-label here.

♿ Suggested fix
-    <Button variant="toolbar" onClick={props.onSearchClick}>
+    <Button
+      variant="toolbar"
+      onClick={props.onSearchClick}
+      aria-label="Find and replace"
+    >
       <SearchSvgIcon />
     </Button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Toolbar.tsx` around lines 95 - 98, The icon-only search Button
(the Button element with variant="toolbar" and onClick={props.onSearchClick} in
Toolbar.tsx) lacks an accessible name; add an explicit aria-label (e.g.,
aria-label="Search" or a localized label) to that Button so assistive tech can
announce it, ensuring you place the aria-label prop on the same Button element
that renders <SearchSvgIcon />.

@gameroman gameroman merged commit b69a204 into main Mar 22, 2026
3 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Mar 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add find and replace functionality

1 participant