Skip to content

feat(browse): highlight search matches and disable infinite scroll while searching#111

Merged
mikkisguy merged 2 commits into
mainfrom
issue/24-realtime-search
Jun 24, 2026
Merged

feat(browse): highlight search matches and disable infinite scroll while searching#111
mikkisguy merged 2 commits into
mainfrom
issue/24-realtime-search

Conversation

@mikkisguy

Copy link
Copy Markdown
Owner

Closes #24 — Real-time search integration

Most of issue #24's plumbing already existed (debounced 300 ms input, ?q= URL sync, /api/search routing, loading/empty states, clear button). This PR closes the two genuinely-unmet acceptance criteria.

What changed

1. Matching text highlighted in results

  • /api/search already returns an FTS5 snippet with <mark>…</mark> match markers, but the client was deliberately stripping it. stripSearchOnly now preserves snippet (still drops rank), and BrowseItem carries it.
  • ContentCard renders the snippet with <mark> highlighting, falling back to the plain content preview when there is none.

2. When searching, infinite scroll disabled

  • New infiniteScroll prop on ContentFeed; browse-page passes !filters.q. The IntersectionObserver sentinel is suspended during an active search (per the spec, "results appear in feed, replacing infinite scroll"), while a manual "Load more" still paginates so results are never cut off. Clearing q re-enables infinite scroll.

3. Consolidated the XSS-safe snippet parser (src/lib/snippet.ts)

  • The browse card's snippet rendering and the command palette's renderSnippet were two parallel implementations of the same security-critical transform. Extracted one shared parseSnippet (token-walk, no regex) used by both — a single source of truth for the safety property.
  • Snippets are parsed into plain/highlight segments rendered as React text children, so React escapes any markup in the source content. There is no dangerouslySetInnerHTML anywhere in the path (FTS5's snippet() does not HTML-escape its input, so this matters).

4. Housekeeping

  • <mark> styled with the --primary-muted design token.
  • Version 0.10.00.11.0 (user-facing feature) + README badge.

Verification

  • pnpm verify green: lint, typecheck, build, 767 tests, knip.
  • New/updated tests: API client (snippet preserved, rank dropped), card snippet render + XSS-safety (a <script> in content renders as inert text), shared parseSnippet unit tests, and the feed's infiniteScroll on→off disconnect() transition.

@oracle review

Two-pass @oracle review (triggered by the >200-line size and the XSS security boundary). Verdict: XSS handling sound, infinite-scroll gating correct, no must-fix or should-fix findings — clear to open PR. Both should-fix items from pass 1 (a stale safeSnippetHtml doc reference, and the duplicated parser) were resolved and confirmed on pass 2.

…ile searching (Closes #24)

Wire the FTS5 snippet from /api/search into the browse feed so matched
terms render with <mark> highlighting, and suspend the scroll-triggered
auto-load while a search query is active.

- Preserve the FTS5 snippet through the API client (drop only rank) and
  add it to BrowseItem
- Render highlighted snippets in ContentCard, falling back to the plain
  preview when there is no snippet
- Consolidate the XSS-safe snippet parser into src/lib/snippet.ts, shared
  by the browse card and the command palette; segments render as React
  text children so no dangerouslySetInnerHTML ever touches untrusted
  content (single source of truth for the safety property)
- Add an infiniteScroll prop to ContentFeed; browse-page passes !filters.q
  so scrolling no longer auto-loads mid-search (a manual Load more still
  paginates)
- Style <mark> with the --primary-muted design token
- Bump 0.10.0 -> 0.11.0 + README badge

Closes #24
…s ring

Review feedback on #24:
- Suppress the native ::-webkit-search-cancel-button on search inputs so
  only the toolbar's custom clear button shows (was rendering two X icons
  in Chrome/Safari).
- Switch --color-ring from --primary (blue) to --foreground (cream) so
  focus indicators are neutral and on-brand, matching the ::selection
  treatment. Blue stays reserved for primary actions/links/selected-nav.
  design-tokens.md updated to match.
@mikkisguy mikkisguy merged commit 2f12784 into main Jun 24, 2026
3 checks passed
@mikkisguy mikkisguy deleted the issue/24-realtime-search branch June 24, 2026 21:40
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.

Real-time search integration

1 participant