refactor(reader): vocab highlights via CSS Custom Highlight API#153
Merged
Conversation
…erlay) core bug: inline translations disappear on scroll due to chapter eviction wiping imperatively-inserted <mark> nodes. Full refactor to declarative CSS Custom Highlight API + React overlay, with defense-in-depth rollout (feature flag, killswitch, error boundary, oracle shadow-mode, legacy fallback) to guarantee zero user-visible regression. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- vocabHighlightEngine: Range[] from TreeWalker, zero DOM mutation. - customHighlightRegistry: CSS.highlights wrapper, feature-detect, silent fail. - vocabHighlightTelemetry: counter API, never throws, console default. - 40 unit tests green, jsdom mocks for CSS.highlights absence. Foundations only — not wired to reader yet. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- useContainerMutationObserver: RAF-debounced, swallows callback errors, disconnects on unmount. Core recovery signal for innerHTML wipes. - VocabHighlightErrorBoundary: renders legacy fallback on throw, emits error.boundary telemetry, supports resetKey for retry across chapters. - 14 unit tests green. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- VocabHighlightLayer: declarative replacement for imperative VocabWordLayer. Zero DOM mutation; registers Range[] via CSS Custom Highlight API. MutationObserver auto-recovers after innerHTML wipes. - VocabTranslationOverlay: absolute-positioned overlay, RAF-throttled reflow on scroll/resize, viewport coords via CSS var transforms. - vocab-highlights.css: ::highlight(vocab-*) pseudos mirror legacy colors; .vocab-translation-overlay styles. - 16 unit tests green. Not wired to reader yet. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- features.ts: VITE_READER_CUSTOM_HIGHLIGHTS (default on), VITE_READER_HIGHLIGHTS_ORACLE (default off). Runtime killswitch via window.__textstackDisableCustomHighlights. - VocabHighlightDispatcher: boot-time decision (flag + support + killswitch). Wraps new layer in ErrorBoundary with legacy as fallback. - VocabHighlightOracle: shadow-mode observer. Diffs engine matches vs legacy <mark> count, logs via oracle.diff telemetry. - ReaderHighlights now mounts the dispatcher. App.tsx imports vocab CSS. - 12 new tests; 176 total green. tsc + vite build clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Covers: new-path render, eviction+return scroll, killswitch → legacy, CSS.highlights delete → legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…lback Same dispatcher pattern as web: feature-detect CSS.highlights + window.__textstackDisableCustomHighlights killswitch → new path; else legacy <mark> path. MutationObserver reapplies after appendChapter so vocab marks survive chapter scroll. Exterior API (markVocabWords/addVocabWord/removeVocabMarks/setShowInlineTranslations) unchanged → reader pages need no edits. Translation overlay: absolute-positioned spans on a body-child overlay container (data-vocab-overlay), repositioned via RAF on scroll/resize. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
E2E: hard-coded words weren't in chapter 1 "welcome" of test book → totalHighlightSize stayed 0. Now pick a 5-8 letter word from rendered content at test time. Mobile: MutationObserver watched document.body, so overlay span appends (inside [data-vocab-overlay]) and legacy <mark> wraps re-triggered markVocabWords each RAF forever. Filter out mutations that originate in our own overlay container / vocab marks before re-applying. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace isOwnMutation filter (missed text-node additions from legacy unwrap) with disconnect/reconnect around markVocabWords body. Simpler, immune to edge cases. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Test-scoped request fixture has no cookies — beforeAll's testLogin only auths the worker context. Switch to page.request which shares storage state with authedPage. Drop beforeAll/afterAll, clean per-test instead. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes vocab-highlight underlines disappearing after scrolling past the chapter-eviction window. Replaces imperative
<mark>wrapping with CSS Custom Highlight API + Range-based engine. Zero DOM mutation in chapter text → survivesdangerouslySetInnerHTMLreload, font-size change, resize.Defense-in-depth (priority #1 per user: "надежно стабильно безотказно"):
VITE_READER_CUSTOM_HIGHLIGHTS(default on)window.__textstackDisableCustomHighlights = true→ legacy pathCSS.highlightsunsupported → legacy pathVITE_READER_HIGHLIGHTS_ORACLE=trueruns engine alongside legacy and logsoracle.diffon divergenceVocabWordLayerpreserved as fallback until Slice 10 (4 weeks clean prod)Scope: web + mobile parity. Mobile WebView gets the same dispatcher inline in readerHtml.ts. Exterior API unchanged (
markVocabWords/addVocabWord/setShowInlineTranslations) so all reader call sites unmodified.Slices landed
VocabHighlightLayer+VocabTranslationOverlay+ CSS rulesRemaining (post-merge, time-based gates)
Test plan
vite buildclean<mark>path activatesRollback
Flip
VITE_READER_CUSTOM_HIGHLIGHTS=false→ redeploy. Legacy takes over with no user-visible change.🤖 Generated with Claude Code