Files
readest/apps/readest-app/.claude/memory/bug-patterns.md
T
Huang Xin 78794499a2 fix(dictionary): correct System Dictionary platform gating on web and iPad (#4362)
* fix(dictionary): keep other dictionaries usable when System Dictionary syncs to an unsupported platform

`dictionarySettings.providerEnabled` is whole-field synced across devices, so enabling System Dictionary on macOS/iOS sets the flag on web/Linux/Windows too. There the row is hidden and the feature is a no-op, but the settings UI read the raw flag and locked every other dictionary's toggle read-only. Gate the lock on `isSystemDictionaryEnabled(settings)` — the same platform-aware check the annotator uses — so it matches real lookup behavior and never triggers where the system dictionary can't run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(dictionary): dispatch system dictionary handoff by native OS (fixes iPad)

iPadOS sends a desktop "Macintosh" user agent, so the UA-based `getOSPlatform()` reported iPad as 'macos' and the handoff invoked the macOS-only `show_lookup_popover` Rust command that iOS never registers ("Command show_lookup_popover not found"). Derive the OS from the app service's `is*App` capability flags (sourced from the Tauri OS plugin, correct on iPad) via a new synchronous `getInitializedAppService()` accessor, so iPad routes to the iOS plugin command path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ui): constrain reader View Options dropdown to h-8

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(agent): note System Dictionary platform-detection and synced-flag patterns

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 09:51:22 +02:00

9.0 KiB

Bug Fixing Patterns & Strategies

Common Root Cause Categories

1. Overly Broad CSS Selectors

Pattern: A CSS rule targets too many elements, causing unintended visual side effects. Examples:

  • hr { mix-blend-mode: multiply } applied to ALL hr elements instead of only decorative ones (#3086)
  • p img { mix-blend-mode } applied to block images, not just inline (#3112)
  • svg, img { height: auto; width: auto } overrode explicit HTML width/height attributes (#3274)
  • Background-color override applied unconditionally instead of only when user enabled color override (#3316)

Fix Strategy: Narrow selectors with class qualifiers (.background-img, .has-text-siblings) or attribute pseudo-selectors (:where(:not([width]))). Check if the rule should be conditional on a user setting.

2. Conditional vs Unconditional Style Overrides

Pattern: CSS rules meant for "Override Book Color/Layout" mode are placed in the always-active stylesheet. Examples:

  • Calibre .calibre { color: unset } was in getLayoutStyles() instead of getColorStyles() (#3448)
  • Image background-color override applied without checking overrideColor flag (#3316, #3377)

Fix Strategy: Move rules to the correct conditional block: getColorStyles() for color overrides, getLayoutStyles() for layout overrides. Check the overrideColor/overrideLayout flags.

3. Missing EPUB Stylesheet Transformations

Pattern: EPUB stylesheets contain CSS that conflicts with app functionality. Examples:

  • user-select: none prevents text selection (#3370) -> regex replace in transformStylesheet()
  • font-family: serif/sans-serif on body bypasses user font (#3334) -> detect and unset
  • Hardcoded Calibre backgrounds persist in dark mode (#3448) -> unset in color override

Fix Strategy: Add regex-based transformation passes in transformStylesheet() in style.ts.

4. Stale State / Refs Not Reset

Pattern: A useRef or state variable is set once and never properly reset, blocking re-entry. Examples:

  • TTS ttsOnRef prevented restarting TTS from a new location (#3292)
  • initializedRef in AnnotationRangeEditor prevented handle position updates (#3286)
  • view.tts not nulled on shutdown prevented clean TTS restart (#3400)
  • TTS safety timeout fired after pause, advancing to next sentence (#3244)

Fix Strategy: Check all refs/guards in the affected flow. Ensure cleanup in shutdown/unmount. Remove overly aggressive guards that prevent re-entry.

5. Platform API Differences

Pattern: A Web API behaves differently or is unavailable on certain platforms. Examples:

  • navigator.getGamepads() returns null on older Android WebView (#3245)
  • CompressionStream unavailable on some Android versions (#3255)
  • btoa() throws on non-ASCII characters (#3436)
  • View Transitions API unsupported in WebKitGTK/Linux (#3417)
  • document.startViewTransition() crashes on Linux

Fix Strategy: Always check API availability before use. Add fallback paths. Use feature detection, not platform detection when possible.

6. Safe Area Inset Issues

Pattern: UI elements overlap system bars (status bar, navigation bar, notch) on mobile. Examples:

  • Zoom controls behind status bar (#3426)
  • Android navigation bar overlap (#3466)
  • iPad sidebar insets incorrect (#3395)
  • Reader page layout jump after system UI change (#3469)

Fix Strategy: Use gridInsets and statusBarHeight from useSafeAreaInsets. Use env(safe-area-inset-*) CSS functions. Call onUpdateInsets() after system UI visibility changes. See docs/safe-area-insets.md.

7. Z-Index Layering Issues

Pattern: Interactive elements rendered behind other layers, becoming unclickable. Examples:

  • Navigation buttons invisible on mobile (#3201) -> added z-10
  • Annotation nav bar too prominent (#3386) -> reduced from z-30 to z-10
  • Page nav buttons behind TTS control (#3184)

Fix Strategy: Check z-index ordering. Use minimum necessary z-index. Reference the z-index hierarchy in the codebase.

8. Event Handling Race Conditions

Pattern: Timing issues between pointer events, native menus, and React state updates. Examples:

  • macOS context menu steals pointer event loop (#3324) -> 100ms setTimeout delay
  • Traffic light buttons flicker due to timeout race (#3488, #3129)
  • Android tool buttons unresponsive due to premature re-selection (#3225)

Fix Strategy: Add small delays before native menu calls. Check event state machine consistency. Remove premature re-triggers on Android.

9. foliate-js Rendering Issues

Pattern: Bugs in the lower-level EPUB renderer (paginator.js, epub.js). Examples:

  • Image size not constrained in double-page mode (#3432)
  • Background not shown in scrolled mode (#3344)
  • Section content cached incorrectly after mode switch (#3242, #3206)
  • Swipe sensitivity too low for non-animated paging (#3310)

Fix Strategy: Check both columnize() and scrolled() code paths in paginator.js. Verify CSS variables (--available-width, --available-height) are computed correctly. Test in both paginated and scrolled modes.

10. Progress/Navigation Calculation Errors

Pattern: Page counts, progress percentages, or position tracking are wrong. Examples:

  • Progress shows 99.9% at last page (#3383) -> boundary condition
  • Pages left shows estimated instead of physical count (#3213, #3200)
  • FB2 subsection progress wrong (#3136) -> nested structure not handled
  • TOC auto-scrolls on expand (#3124) -> scroll-into-view triggered too broadly

Fix Strategy: Use physical view.renderer.page/view.renderer.pages instead of estimated section metadata. Check boundary conditions (0-indexed vs 1-indexed, inclusive vs exclusive).

11. Debounced State Stale on User-Initiated Layout Change

Pattern: A scroll/resize handler is debounced for performance, but during the debounce window any code path that re-runs layout based on saved state (e.g. #anchor, #primaryIndex) sees stale values. Example:

  • Scrolled-mode toggle reverted to previous chapter (#3987): the paginator's scroll handler is debounced 250 ms, so toggling flow=scrolled → flow=paginated within that window made render() → scrollToAnchor(#anchor) restore the anchor from before the user scrolled into the next section. Both #anchor and #primaryIndex were stale together, sending the position back.

Fix Strategy: When an external trigger forces a re-render (here, setAttribute('flow', ...)), flush the debounced state synchronously before changing the layout. In paginator.js this means overriding setAttribute and calling #detectPrimaryView() + #getVisibleRange() while this.scrolled is still true.

12. Multiview Paginator Side Effects

Pattern: The multiview paginator (e925e9d+) loads adjacent sections in background. Events from these loads can interfere with user interactions on the primary section. Examples:

  • load event from adjacent section triggers docLoadHandler which re-adds ALL annotations, overwriting drag edits
  • Multiple overlayers with duplicate SVG <clipPath> IDs cause url(#id) to resolve to wrong element
  • MagnifierLoupe destroying/recreating body clone on every drag tick triggers ResizeObserver → expand → redraw

Fix Strategy: Scope event handlers to the loaded section's index. Use unique IDs for SVG elements across overlayer instances. Minimize iframe DOM mutations during drag operations.

13. Whole-Field-Synced Flag Reaches an Unsupported Platform

Pattern: A setting is whole-field synced across devices (e.g. dictionarySettings.providerEnabled), so a flag enabled on one platform arrives true on a platform where that feature isn't supported. The lookup/runtime path correctly gates on platform support, but a secondary consumer (usually UI gating) reads the raw synced flag and misbehaves. Example:

  • System Dictionary enabled on macOS synced to web → web's CustomDictionaries.tsx locked all other dictionary toggles read-only (lockedBySystem) even though System Dictionary is hidden + a no-op there. The annotator's lookup path used the platform-gated isSystemDictionaryEnabled(settings) (registry.ts, gates on isSystemDictionarySupported()), but the settings UI compared the raw providerEnabled[systemDictionary] === true.

Fix Strategy: Every consumer of a synced flag for a platform-specific feature must route through the same platform-aware gate the runtime uses — not the raw providerEnabled[...]/setting value. Here: lockedBySystem = isSystemDictionaryEnabled(settings) && .... Search for other readers of the raw flag when fixing one.

Debugging Workflow

  1. Identify the category from the issue description
  2. Check style.ts first for any CSS-related visual bugs
  3. Check foliate-js for rendering/layout bugs
  4. Check platform-specific code for mobile/desktop differences
  5. Write a failing test before implementing the fix
  6. Test in both paginated and scrolled modes for layout changes
  7. Test on multiple platforms for any UI change
  8. Run pnpm build-check before submitting