Skip to content

Find in document: minimal floating Find bar (⌘F), then Find and Replace #393

Description

@HMarzban

Summary

Find already ships, but as a docked strip under the toolbar. The maintainer wants a different shape. This issue revises and refactors Find in two steps.

  1. Find bar. A small floating bar at the top right of the editor column, below the toolbar. It holds the query, a match count, Previous, Next and Close. ⌘F / Ctrl+F opens it, and Escape closes it.
  2. Replace row, later. A chevron opens a second row with Replace and Replace all. Match case, Whole word and Regex toggles come with it.

The UI and UX team owns the look, the micro-animations and the micro-interactions. The research team looked at a TOC match signal. It recommends no TOC signal for now (see TOC match signal).

The maintainer will add the four screenshots to this issue, because gh cannot attach them:

  1. The pad toolbar "Find in document (⌘+F)" button.
  2. The Google Chrome find bar.
  3. The VS Code find widget, collapsed.
  4. The VS Code find widget, expanded with a Replace row.

Maintainer request (verbatim quote)

alrigth let's work on this featue, this is not the feature I expected, let's review related github issue for this feature, we have to revise and refactor this feature, the first refactor part is related to the UI and UX. so we have to create a floating input bar, simple and minimal, that will be show up in right top bellow the toolbar, like the chrome find popover component (second image) it must include previous and next button and also the close button, also we have to map the short key to this component in order when user hit the short key cmd+f then the search bar apear, also we need to expand it later and make it like vscode in order to let people to find and replace (third and fourth pictures). let's work on this github issue and create a gtihub issue and ensure the UI and UX team will be work on it and polish the idea and make the UX greate withe micro-animation and also micro-intraction. also we have to cover the ESC button in order for dismiss, this component must be fully mature. also when user search through the entire document, and for those content find and hilight, it must highlight the TOC contents to let user see which heading might include the text that user wants to find (thsi idea is not mature and you have to run team of reserch and see what is the best way to have this idea, or it is not needed at all.)

Related issues

Issue What it asked What shipped Gap
#250 Find text in the open pad (open) Caret find, Mod-f, Enter / Shift-Enter, Escape, fold snapshot, 200-match cap, polite count, no replace, no regex All of it, in commit f167903dd (2026-09-23). Restyled in 7369172ec (2026-10-01) The bar is a docked strip, not a floating bar. No motion. Escape works only from the input. Details below
#254 Command jump A "Find" row that opens the #250 bar Shipped (commandJump/buildPlaceRows.ts) None. Keep the row working
#175 App keyboard shortcuts Theme chord and help dialog Open A #250 comment says: do not add Find to the #175 help table. This issue keeps that rule
#248 Pad skip, named dialogs, focus traps Focus traps for the TOC drawer and sheets Open #248 does not cover the find bar. This issue owns the bar's focus rules
#338 Heading link lands on a folded heading Unfold for the session only Open It plans to move foldedSectionsAt next to the fold plugin. Find imports it. Coordinate the move
#276 Run the webapp Cypress suite in CI, #311 Filter apply test never passed CI coverage Open New find specs gate nothing in CI until #276 lands

#250 is still open, but its code shipped. The maintainer can link or close it. This issue does not change it.

Why it is "not the feature I expected". #250 built the right engine with the wrong shape. The maintainer asked for a compact floating bar, like the Google Chrome find bar. #250 built a full-width row with a bottom border inside the toolbar wrapper. Opening it pushes the document down. Replace was out of scope on purpose, and it is now wanted as a later step.

Current behaviour

All items are read from code. Nothing here was measured in a browser.

Files

  • Engine: apps/webapp/src/components/TipTap/extensions/caret-find/caret-find-plugin.ts (316 lines) and caret-find.ts (commands openCaretFind, closeCaretFind, setCaretFindQuery, stepCaretFind).
  • UI: apps/webapp/src/components/TipTap/find/FindBar.tsx, gate canOpenFind.ts.
  • Styles: apps/webapp/src/styles/editor/_caret-find.scss.
  • Mounts: desktop toolbar/desktop/EditorToolbar.tsx (toolbar button data-testid="toolbar-find", then <FindBar variant="desktop" /> after the toolbar row). Phone pages/document/layouts/MobileLayout.tsx (under MobilePadTitle), opened from the TocModal.tsx footer.
  • Test: apps/webapp/cypress/e2e/editor/find/caret-find.cy.ts (6 specs).
  • Glossary: CONTEXT.md §Pad tools (Find, Filter, Command jump).

How it works

  • The engine is a ProseMirror plugin with inline decorations. A match never enters the document or its history.
  • The search is a literal, case-insensitive substring per textblock, Title included. Media and other leaf nodes count as one object character, so no match spans them.
  • Painting and stepping stop at 200 matches (CARET_FIND_HIT_CAP). The count then reads 1 of 200+. A match after the 200th cannot be reached.
  • Other matches use warning at 28%. The current match uses warning at 60% plus a 1px warning ring. This is separate from the Filter highlight and the Highlight mark.
  • Each keystroke dispatches a transaction. That transaction rescans the whole document and moves the editor selection to the first match after the caret.
  • The current match scrolls with scrollIntoView({ block: 'center' }). On a phone it uses scrollElementInMobilePadEditor.
  • A match in a folded section unfolds that section for the session only (persist: false). Close restores the folds. The Cypress spec covers this.

Shortcut

  • A window keydown listener in the capture phase handles Mod-f (isModShortcut). It calls preventDefault, so the browser find never opens.
  • It skips the key when a modal makes the editor aria-hidden or inert.
  • On desktop it takes Mod-f from every focus target on the pad, the docked chat composer included.
  • On a phone the bar yields while the chat pane is open (selectPadOwnsKeyboard).
  • A second Mod-f while the bar is open focuses the input and selects the query.

UI

  • Desktop: a row border-b px-3 py-1.5, content right-aligned (justify-end), under the toolbar. It pushes the editor well down by about 45px (read from code, not measured).
  • The row is role="search" with a w-64 TextInput. The count is role="status" outside .ProseMirror. Previous, Next and Close are ToolbarButtons.
  • The bar mounts and unmounts at once. It has no motion.
  • Phone: the same row, full width, under the pad title. _caret-find.scss hides the bottom toolbar while the bar has focus.

Gaps and defects found in review

  1. The current match resets on a remote edit (likely bug). @tiptap/y-tiptap applies every remote change as one replace of the whole document (_typeChanged). The plugin maps the old current match through that step, so it lands at the document end. firstHitFrom then wraps to index 0. "7 of 12" becomes "1 of 12" whenever a collaborator types. Local undo and redo take the same path. Read from code, not measured.
  2. The caret moves on every keystroke. Each query change sets the editor selection. The caret then sends one awareness cursor update per keystroke and per step. Read from code, not measured.
  3. Escape works only in the input. Focus on Previous, Next or Close ignores Escape.
  4. Close leaves a collapsed caret at the match start. The match text is not selected, so the user cannot type over it.
  5. No ⌘G / Shift+⌘G or F3. No selection prefill.
  6. The first count can go unspoken. The status element mounts with its text already set when the bar reopens with an old query. Screen readers may skip that first value.
  7. No debounce. A query that matches nothing walks the whole document on every keystroke and on every remote edit. The cost on a large pad is not measured.
  8. No catalog row. .cursor/docs/design-system.md has no row for the find bar or for the match colours.

Proposed behaviour — Find bar

Layout

  • Desktop position. The bar floats at the top right of the editor column, under the toolbar. It does not span the window. It sits inside the editor-column wrapper in DesktopEditor.tsx (the relative flex min-h-0 min-w-0 flex-1 flex-col box), with absolute placement. It does not portal.
  • That placement follows the TOC width, the tick rail and docked chat for free. It never covers the TOC. Docked chat sits at the bottom of the same column. Check that a tall chat panel never meets the bar.
  • The right inset clears the editor scrollbar, which is --scrollbar-size-thin (6px) wide. The bar never covers the thumb.
  • Species. L1 anchored (design-system.md §Elevation species). Frame: popoverPanelClassName, merged through @utils/twMerge with a width override only. The catalog already lists other consumers that override only size. No new radius, border or shadow.
  • Width. About 22rem, capped at calc(100% - 1.5rem) on a narrow column. This is a proposed value; the visual preview settles it.
  • Paint order. The floating tier z-50, above the docked z-[42] band.
  • Phone. Keep the docked L0 row under MobilePadTitle. See Mobile.

Content (left to right)

  1. Query input: TextInput size="sm", placeholder "Find in document", aria-label="Find in document". The ghost variant lets the bar read as one box. The preview confirms that focus stays visible.
  2. Count: text-meta text-base-content/60 tabular-nums, with a fixed minimum width. The bar does not shift when "9 of 12" becomes "10 of 12".
  3. ToolbarDivider (the screenshot 2 divider).
  4. Previous (Icons.chevronUp), Next (Icons.chevronDown), Close (Icons.close), all ToolbarButton. Tooltips: "Previous match (⇧Enter)", "Next match (Enter)", "Close (Esc)".

The toolbar Find button shows the is-active recipe while the bar is open. It sets aria-expanded and aria-controls to the bar.

Keyboard map

Key Where Action
⌘F / Ctrl+F Anywhere on the pad page, unless a modal makes the editor aria-hidden or inert Open the bar, focus the input, select its text. Prefill: see open decision 3
⌘F / Ctrl+F Bar already open Focus the input and select the query. Do not reopen. Do not replay the open motion
Enter Input Next match
Shift+Enter Input Previous match
⌘G / Ctrl+G, F3 Bar open, focus in the input or the editor Next match
Shift+⌘G / Shift+Ctrl+G, Shift+F3 Bar open, focus in the input or the editor Previous match
Escape Any control in the bar Close. Select the current match in the editor and focus the editor when it is editable. Otherwise return focus to the opener
Escape Editor, bar open, no editor menu or popover open Close the bar (open decision 6)
Enter with IME composition Input Ignore (already guarded by isComposing)

No ⌘G or F3 binding exists in the webapp today, so these chords do not collide. ⌘G / Ctrl+G and F3 follow the browser's own find keys. Bind them only while the bar is open, so the browser keeps them otherwise.

States

State Input Count Previous / Next Editor
Idle (open, empty) Focused, placeholder Empty Disabled (/40) No paint
Typing Value updates Updates on each keystroke Enabled when matches exist All matches paint. The current match scrolls into view. The caret does not move (open decision 7)
Results Value 3 of 12, or 3 of 200+ at the paint cap Enabled. Wraps at both ends Current match uses the current paint
No results Value stays No results Disabled No paint
Closed — — — Paint cleared. Folds that Find opened fold again. The current match is selected

Motion and micro-interactions

All values come from apps/webapp/CLAUDE.md §Motion System and utils/motion.ts. No new duration, easing or keyframe.

  • Open (desktop). Overlay tier: 120ms --motion-overlay-in ease-out. Opacity plus scale 0.96, with transform-origin: top right (the anchored side). Mount through useEntryExitTransition. The transform sits on the bar only, never on an ancestor of .ProseMirror.
  • Close (desktop). 80ms --motion-overlay-out ease-in, opacity only.
  • Open and close (phone). Opacity only (doc-content-in 120ms), because the bar sits in the visual-viewport shell. The hard rule bans transforms there.
  • Reopen with ⌘F. No replay. One-shot reveals must not replay on re-render.
  • Count. No animation. tabular-nums and a fixed width keep it still. RollingNumber is for unread badges, and a reel on every keystroke is noise.
  • No results. No shake. A shake fires while the user is still typing a word. It needs a new keyframe, which the lock forbids. The count text and the disabled arrows carry the state.
  • Step. Scroll the current match with behavior: 'instant'. A held Enter repeats the key, and smooth scrolls then queue and lag. An explicit behavior also beats the wrapper's scroll-smooth class, so the two do not fight. The paint swap from "other" to "current" is the visual feedback.
  • Toolbar button. is-active while open. That is the existing toolbar recipe.
  • Reduced motion. useEntryExitTransition already skips to the final state. The step scroll is instant in every mode.

Accessibility

  • Container: role="search" with aria-label="Find in document". It is non-modal and has no focus trap. Tab moves input → Previous → Next → Close, then on through the page.
  • Count: one role="status" element. Render it empty when the bar mounts, then write the count. This fixes gap 6. While the user types, update the spoken text after a short pause and keep the visible count instant. The pause length is the team's call.
  • Every icon button has an aria-label. On touch, Tooltip does not name anything, so labels stay on the buttons.
  • The live region stays outside .ProseMirror (TipTap CLAUDE.md §Editor Performance).
  • Check match-paint contrast in all 7 themes, including docsplus-dark-hc. Not measured yet.

Mobile

The mobile shell is user-agent gated. A narrow desktop window never shows it.

  • Keep the docked L0 row under MobilePadTitle, opened from the TocModal footer Find button. Find text in the open pad #250 rules apply: no BottomSheet, because a sheet drops the iOS keyboard.
  • Use the same content, states and keyboard rules as desktop. Only the frame and the motion differ.
  • The shell is sized by --visual-viewport-height, so Previous and Next stay above the keyboard. Keep text-base on the input to stop iOS zoom.
  • Controls keep the settled 32px docked size.
  • The bar still yields to an open chat pane.
  • On iPhone and many iPads, ⌘F never reaches JS. The Find button stays the entry point there. A missed chord is not a bug.

Later — Find and Replace

Ship after the Find bar, as its own slices.

Scope

  • A chevron button at the left of the bar. It has aria-expanded and aria-controls. It shows the Replace row, as in screenshot 4.
  • Replace row: a replace input, then Replace (current match, then step to the next) and Replace all.
  • Toggles on the find input: Match case, Whole word, Regex. Each is a ToolbarButton with aria-pressed and the is-active recipe. Defaults stay off, so the current literal, case-insensitive search does not change.
  • Whole word must use Unicode letter classes (\p{L} with the u flag). A plain \b does not see Persian or Arabic letters.
  • An invalid regex shows an inline error and paints nothing. It never throws.

Read-only and access rules

  • Show the chevron and the Replace row only when the editor is editable. Hide them under Read-only for a non-owner, under the editing lock (selectDocumentEditingLocked), and in History.
  • Find itself stays available to every viewer, as Find text in the open pad #250 requires.
  • Replace all is a large edit. Open decision 10 asks whether it needs a signed-in user (isVisitor(), then openInlineSignInDialog()).

One transaction

  • Replace all scans the whole document at click time, with no 200 cap. It applies every change in one ProseMirror transaction, in reverse document order, with tr.insertText.
  • One transaction becomes one Yjs update. Collaborators see one change. One Mod-z undoes it all.
  • After the transaction, the count shows "No results" or the new matches. The editor keeps focus.

Risks

  • Sections. Each heading is a section with its own chatroom, keyed by toc-id. A match never crosses a textblock, so a replace never joins or splits blocks. The heading node and its toc-id survive. A test must pin this.
  • Heading text. A replace inside a heading changes the TOC title and the ?h= slug trail. Links use id=, so they still resolve.
  • Marks. insertText takes the marks at the match start. A match that crosses a hyperlink edge can lose part of the link. Decide whether to refuse such a match or to keep the first mark.
  • Comment anchors. scrollToCommentAnchor finds a text comment by its quoted text. A replace that changes that text sends the jump to the heading fallback.
  • Title. Replace may empty Title. Title must stay a heading, as it does today.
  • Regex cost. A slow pattern blocks the main thread of the user's own tab. Cap the scan and fail closed.
  • Concurrent edits. A peer who types inside a match during Replace all merges through Yjs. Accept that.
  • Chord. Mod-Shift-h is already Highlight (@tiptap/extension-highlight), so the Google Docs replace chord is taken. See open decision 10.

TOC match signal — research and recommendation

Question. Should the TOC show which headings hold matches?

What other tools do. Sources are vendor help pages, read on 2026-10-05.

Only Word for Windows marks headings. Most tools mark positions next to the scrollbar, or nothing.

Options

Option For Against
A. No TOC signal Zero clutter. No new design-system recipe. Today, a match in a folded section already unfolds that section in the TOC, and the scroll spy follows the scrolled match No overview of where matches sit
B. Match count badge per row Word-like overview Collides with the red unread badge in TocRowTrail. Counts are wrong past the 200 cap. A new badge recipe is locked
C. Small dot on rows with matches Light overview A dot is a new state recipe (locked). The red indicatorDot means attention, so it cannot be reused. A dot under a folded parent needs a roll-up rule
D. Dim rows with no matches Strong overview Reads like Filter, which Find must not imitate (#250). /40 means disabled in the ink ladder. Close to the banned scroll-spy wash
E. Filter the TOC to matching sections Short list Duplicates Filter. Breaks the Find versus Filter split
F. Mark only the current match's section One row, no counting Needs the spy state or a new recipe. Driving menu-focus from Find fights the scroll spy
G. Marks on the tick rail (like a scrollbar overview) Close to VS Code Touches settled tick-rail rules. Desktop rail only. A new colour is locked

Constraints that weigh in. The root CLAUDE.md settled list bans the data-level type ladder, the chat-open accent bar and the scroll-spy wash. Presence overhang is rejected. TocRowTrail already holds chat, unread and presence. On a phone the TOC is a modal drawer. It is closed while the find bar is open, so nobody would see a signal there. The cost of counting matches per section is small next to the scan itself. Clutter and meaning are the real cost.

Recommendation: A, no TOC signal, for now. The new bar answers the request without it. Most tools the team checked do not mark the outline. Every visible option needs a locked design-system change or bends a settled TOC rule. If readers still ask for an overview after the new bar ships, open an Ideas discussion. Bring option F or C with a design-system ruling. Do not bring D or E. Open decision 11 asks the maintainer to confirm.

Plan

  • 0. Visual preview (UI and UX team). Before any code, build one interactive HTML demo or Cursor canvas, per AGENTS.md §Workflow And Review Expectations. Show the desktop bar in light, dark and one premium theme, in idle, results, no-results and capped states. Show it with and without docked chat, on a narrow column, open and close motion, and the phone row. The maintainer approves the look. Blocks 2 and 5.
  • 1. Engine fixes (caret-find-plugin.ts). Keep the current index through a whole-document replace: on a y-sync change, anchor on the restored selection, not on the mapped position. Apply open decisions 7 (caret on step only) and 8 (cap). Select the match range on close. Add query debounce only if the large-pad measure in step 6 fails. No blocker.
  • 2. Desktop floating bar. Move FindBar out of the toolbar wrapper into the editor column. Use the L1 frame, the overlay motion, the is-active toolbar button and the fixed-width count. Blocked by 0.
  • 3. Keyboard map. ⌘G / F3, Escape from any bar control, Escape from the editor (decision 6), prefill (decision 3), the ⌘F scope (decision 5). Blocked by 2.
  • 4. Accessibility. Status element rendered empty, then written. Spoken-count pause. Labels. aria-expanded and aria-controls on the toolbar button. Blocked by 2.
  • 5. Phone polish. Same states and rules on the docked row, opacity-only motion. Blocked by 0.
  • 6. Tests and verification (sections below). Measure on a large pad. Add the design-system catalog row for the find bar and its match colours, in the same change, as the routine catalog edit. Blocked by 1–5.
  • 7. Toggles (Match case, Whole word, Regex). Blocked by 6 and decision 9.
  • 8. Replace row (Replace, Replace all, one transaction). Blocked by 7 and decision 10.
  • TOC signal. Not planned. See decision 11.

Scope

  • The desktop Find bar refactor and its keyboard, motion, accessibility and engine fixes.
  • Phone row polish.
  • Later slices: toggles, then the Replace row.

Out of scope

  • A TOC match signal (recommendation A).
  • Searching chat, Pad title or History. Find in the chat feed is a separate feature.
  • Merging Find with Filter or with Command jump.
  • Adding Find to the App keyboard shortcuts: theme flip and a help dialog #175 help table.
  • A BottomSheet find on the phone.
  • New tokens, species, keyframes or shadows. Anything that needs one becomes an open decision.
  • The ⌘ glyph in Windows toolbar tooltips. Every toolbar tooltip has it; fix it in its own issue.
  • Persian and Arabic letter folding (for example ی with ي, ک with ك) and diacritic-insensitive search. Track them as a later idea if readers ask.

Open decisions

Each decision needs the maintainer. Each has a recommendation.

  1. Position on a narrow desktop and with docked chat. Recommend: always top right of the editor column. Below the sm breakpoint the desktop toolbar moves to the window bottom (fixed bottom-0 in DesktopEditor.tsx), and the bar still stays top right. With docked chat, the bar stays in the editor area above the chat.
  2. Find and Filter. Recommend: keep both, side by side, as Find text in the open pad #250 ruled. Find steps the caret through text. Filter hides sections.
  3. Prefill. Recommend: when the editor selection is not empty, sits in one textblock and is short, prefill it and select it. Otherwise keep the last query of the session, selected. Do not seed the word under the caret. A good length limit is about 100 characters, a proposed value.
  4. Case sensitivity default. Recommend: insensitive, as today. Match case arrives as a toggle in slice 7.
  5. ⌘F scope. Recommend: keep ⌘F on the pad find everywhere on the pad page, the docked chat composer included. Skip it only when a modal hides the editor. The chat feed is virtualized, so the browser find cannot see most messages either. One rule is easier to learn.
  6. Escape in the editor. Recommend: Escape in the editor closes the bar. An open editor menu or popover takes Escape first: the slash menu, a hyperlink popover, the media toolbar.
  7. Caret while typing. Recommend: while the user types, paint and scroll the current match but leave the caret. Move the caret on a step and on close. This cuts the awareness cursor updates sent per keystroke.
  8. Match cap. Recommend: keep the 200-match paint cap. Count and step through every match up to a hard limit, about 9,999, a proposed value. Then the count reads 9999+. Today a match after the 200th cannot be reached.
  9. Toggles in the first slice? Recommend: no. Find text in the open pad #250 ruled a literal search. Ship toggles with Replace (slice 7).
  10. Replace rules. Recommend: Replace all needs a signed-in user. Single Replace follows the normal edit rule. Chord: ⌥⌘F on macOS (VS Code uses it). On Windows and Linux, choose between Ctrl+H (the browser's History key) and Ctrl+Alt+F (AltGr on some layouts). Mod-Shift-h is taken by Highlight.
  11. TOC signal. Recommend: none for now (option A). Revisit only if readers ask, with a design-system ruling.
  12. No-results look. Recommend: neutral "No results" text and disabled arrows. Do not use the input-error border or a shake.
  13. Design system (§Change protocol (locked)). Confirm three things. (a) The match colours stay the warning mixes in _caret-find.scss (28% for other matches, 60% plus a 1px ring for the current one). The catalog row records them, and no new token is added. (b) The find bar joins the L1 members list as code that follows an existing species. (c) No current-match flash keyframe. A flash would be a new motion and needs its own ruling.

Acceptance criteria

  • On macOS, ⌘F opens the bar, focuses the input and selects its text. The browser find does not open.
  • On Windows and Linux, Ctrl+F does the same.
  • A second ⌘F / Ctrl+F while the bar is open selects the query. The open motion does not replay.
  • The bar floats at the top right of the editor column, under the toolbar. Opening it does not move the document.
  • Enter and ⌘G / Ctrl+G / F3 go to the next match. Shift+Enter and Shift+⌘G / Shift+Ctrl+G / Shift+F3 go to the previous match. Both wrap.
  • Escape closes the bar from the input and from Previous, Next and Close.
  • After Escape on an editable pad, the editor has focus and the current match is selected.
  • After Escape on a read-only pad, focus returns to the opener.
  • The count matches the real number of matches, Title included, and shows the paint cap rule from decision 8.
  • An empty query shows no count. A query with no match shows "No results" and disables Previous and Next.
  • A match inside a folded section unfolds only that section. Close folds it again. Stored folds do not change.
  • A collaborator edit elsewhere keeps the current index ("7 of 12" stays "7 of 12" when the count does not change).
  • A collaborator edit that adds a match updates the count without moving the view.
  • Use a large pad of about 1,500 headings, the size HeadingScale was measured at. One keystroke in the find input fits in one frame. Record the measured number in the PR.
  • A Read-only viewer and a visitor can find. Replace controls never show for them (slice 8).
  • The bar looks correct in docsplus, docsplus-dark, docsplus-dark-hc, both Graphite themes and both Paper themes. Match paint is readable in each.
  • With reduced motion on, the bar opens and closes with no animation, and steps scroll at once.
  • Keyboard only: Tab reaches every control in order, and the focus ring shows on each.
  • VoiceOver and NVDA read the bar name, every button name, and each count change, the first count included.
  • Check desktop with docked chat open and closed, with the wide TOC and with the tick rail. The bar never covers the TOC, the chat or the editor scrollbar.
  • iOS Safari and Android Chrome with a mobile user agent: the TOC drawer Find button opens the docked row. The keyboard stays up. Previous and Next stay tappable above the keyboard. The row yields to the chat pane.
  • No live region or role="status" appears inside .ProseMirror.
  • The document JSON never contains find paint or the Highlight mark after a find session.

Tests

Follow AGENTS.md §Test Policy. Each test names the one failure it pins. Prefer Cypress. Extend apps/webapp/cypress/e2e/editor/find/caret-find.cy.ts; do not add a new suite for the same surface.

Test Failure it pins Kind
Current index survives a whole-document replace transaction Gap 1: the index resets to 1 when y-tiptap replaces the document Jest unit on caret-find-plugin.ts. This is a dense mapping branch that an E2E cannot isolate
Escape from the Next button closes the bar and the selection equals the current match range Gaps 3 and 4 Cypress
⌘G with focus in the editor steps to the next match while the bar is open Chord not bound outside the input Cypress
Reopen with an old query announces the first count Gap 6 Cypress, assert the status text changes after mount
Replace all is one undo step, and every heading keeps its toc-id Replace breaks sections or needs many undos (slice 8) Cypress

Cypress notes:

  • cy.realPress and cy.realType are parent commands. A chained subject is discarded. Assert focus first, for example cy.get(INPUT).should('have.focus'), then call cy.realPress(...).
  • The mobile shell needs a mobile user agent. cy.viewport alone renders the desktop shell.
  • For scroll checks, set scrollBehavior = 'auto' before reading scrollTop, or the read races scroll-smooth.
  • Observe every new test pass locally. Prove each by sabotage. These specs gate nothing in CI until Run the webapp Cypress suite in CI #276 lands.

Do not write snapshot tests, "renders" tests or tests of the animation.

Verification

For the implementer. Find the webapp port first; it is not always 3000 under make dev-local.

  1. Open a long pad on desktop at 1280px or wider, light theme. Press ⌘F. Check the bar position, the motion and that the document does not move.
  2. Type a word that appears in Title, in a folded section and many times elsewhere. Check the count, the paint, Enter, Shift+Enter, ⌘G and F3.
  3. Tab to Next and press Escape. Check that the match is selected and the editor has focus. Type a letter, and check that it replaces the match.
  4. Open a second browser profile on the same pad. Type there while the first tab shows "n of m". Check that n does not reset.
  5. Open docked chat, then the tick rail, then a narrow window under 640px. Check the bar each time.
  6. Repeat 1 to 3 in dark, high contrast, Graphite and Paper themes. Repeat with reduced motion on.
  7. Run VoiceOver on macOS Safari: open, type, step, close.
  8. Phone: set a mobile user agent through CDP Network.setUserAgentOverride. Open the TOC drawer, tap Find, type and step with the keyboard up. Then the maintainer checks on a real iPhone and an Android phone.
  9. Make the pad Read-only as owner, and open it as a non-owner. Find works. Replace controls stay hidden (slice 8).
  10. Profile one keystroke in the find input on a pad of about 1,500 headings. Record the result.

References

Activity

  1. HMarzban commented on Oct 5, 2026

    @HMarzban
    CollaboratorAuthor

    Maintainer ruling, 2026-10-05

    The maintainer approved the Find bar from a local visual preview as "good and mature". The preview is not linked, because it stays local. Each Find-bar item takes the recommendation in Open decisions.

    • 393-position (1) → top right of the editor column, on L1, above docked chat. → Narrow windows and docked chat do not move it.
    • 393-find-filter (2) → keep both. → Find steps through text. Filter hides sections.
    • 393-prefill (3) → a short selection in one textblock, else the last query. The limit is about 100 characters. → No word under the caret is seeded.
    • 393-cmdf-scope (5) → ⌘F everywhere on the pad, the docked chat composer included, unless a modal hides the editor. → One rule.
    • 393-escape-editor (6) → yes. → An open editor menu or popover takes Escape first.
    • 393-caret (7) → the caret does not move while typing. It moves on a step and on close. → Fewer awareness cursor updates.
    • 393-cap (8) → paint 200 matches. Count and step up to about 9,999. → A match after the 200th can be reached.
    • 393-toc-signal (11) → no TOC signal. → Option A.
    • 393-no-results (12) → neutral "No results" text and disabled arrows. → No input-error border, no shake.
    • 393-design-system (13) → confirm (a), (b) and (c): the warning mixes, the L1 species, and no flash keyframe. → No new token.
    • Motion → as previewed. Open: 120ms, opacity plus scale from top right. Close: 80ms, opacity only. Reduced motion is honoured. → Existing motion tokens only. The phone row stays opacity only.
    • New scope: remove the Find button from the pad toolbar. → Desktop opens Find with ⌘F / Ctrl+F only. Google Docs has no toolbar Find button either. The tooltip Find in document (⌘+F) goes away.
    • Replace and the toggles (393-case-default, 393-toggles-timing, 393-replace-all-auth, 393-replace-chord, 393-replace-link-edge) → moved to Find and Replace: Replace row, Replace all and match toggles (backlog, lowest priority) #394, lowest priority. → Slices 7 and 8 leave this issue.
    • 393-250-status → not ruled. → Find text in the open pad #250 stays open.

    Assumed defaults

    Acceptance changes

    • The pad toolbar has no Find button. data-testid="toolbar-find" and its tooltip are gone.
    • Drop the toolbar is-active, aria-expanded and aria-controls items from Plan steps 2 and 4.
    • caret-find.cy.ts opens Find with ⌘F / Ctrl+F, not the toolbar button.
    • Command jump's Find row opens the floating bar.
    • While typing, the caret does not move. A step or a close moves it.
    • Past 200 matches, the count and the steps continue to about 9,999, then read 9999+.
    • Remove the Replace parts of the Read-only criterion, the Replace all test row and verification step 9. Find and Replace: Replace row, Replace all and match toggles (backlog, lowest priority) #394 owns them.

    Next

    1. Start Plan step 1, engine fixes. Then steps 2 to 6. The visual preview gate (step 0) is done.
    2. Remove the toolbar Find button in step 2. Update caret-find.cy.ts in the same change.
    3. Leave 393-250-status open until the maintainer rules on it.
  2. HMarzban commented on Oct 5, 2026

    @HMarzban
    CollaboratorAuthor

    Maintainer ruling update, 2026-10-05

    Acceptance changes

  3. added 4 commits that reference this issue on Oct 6, 2026
    f083ac6
    e2ee1a2
    c56a8f5
    23b8939
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions