diff --git a/apps/readest-app/.claude/memory/MEMORY.md b/apps/readest-app/.claude/memory/MEMORY.md new file mode 100644 index 00000000..d68dc1bd --- /dev/null +++ b/apps/readest-app/.claude/memory/MEMORY.md @@ -0,0 +1,26 @@ +# Readest Project Memory + +## Key Reference Documents +- [Bug Fixing Patterns](bug-patterns.md) - Common bug categories, root causes, and fix strategies +- [CSS & Style Fixes](css-style-fixes.md) - EPUB CSS override patterns and the style.ts pipeline +- [TTS Fixes](tts-fixes.md) - Text-to-Speech architecture and bug patterns +- [Layout & UI Fixes](layout-ui-fixes.md) - Safe insets, z-index, platform-specific UI issues +- [Platform Compat Fixes](platform-compat-fixes.md) - Android, iOS, Linux, macOS platform-specific bugs +- [Annotator & Reader Fixes](annotator-reader-fixes.md) - Highlight, selection, accessibility bugs + +## Critical Files (Most Bug-Prone) +- `src/utils/style.ts` - Central EPUB CSS transformation hub (14+ bug fixes) +- `packages/foliate-js/paginator.js` - Page layout, image sizing, backgrounds +- `src/services/tts/TTSController.ts` - TTS state machine, section tracking +- `src/hooks/useSafeAreaInsets.ts` - Safe area inset management +- `src/app/reader/components/FoliateViewer.tsx` - Reader view orchestration +- `src/app/reader/components/annotator/Annotator.tsx` - Annotation lifecycle + +## Architecture Notes +- foliate-js is a git submodule at `packages/foliate-js/` +- Multiview paginator: loads adjacent sections in background, multiple View/Overlayer instances per book +- Style overrides: `getLayoutStyles()` (always), `getColorStyles()` (when overriding color) +- `transformStylesheet()` does regex-based EPUB CSS rewriting at load time +- TTS uses independent section tracking (`#ttsSectionIndex`) decoupled from view +- Safe area insets flow: Native plugin -> useSafeAreaInsets hook -> component styles +- Dropdown menus use `DropdownContext` (not blur-based) for screen reader compat diff --git a/apps/readest-app/.claude/memory/annotator-reader-fixes.md b/apps/readest-app/.claude/memory/annotator-reader-fixes.md new file mode 100644 index 00000000..769b4136 --- /dev/null +++ b/apps/readest-app/.claude/memory/annotator-reader-fixes.md @@ -0,0 +1,90 @@ +# Annotator & Reader Fixes Reference + +## Annotation System Architecture + +### Key Components +- `Annotator.tsx` - Annotation lifecycle, popup display, style/color management +- `AnnotationRangeEditor.tsx` - Drag handles for adjusting selection range +- `MagnifierLoupe.tsx` - Magnifying glass during handle drag (mobile only) +- `useTextSelector.ts` - Text selection detection and processing +- `useAnnotationEditor.ts` - Editing existing annotations +- `useInstantAnnotation.ts` - Creating new annotations on selection + +### Highlight Rendering +- Highlights rendered by foliate-js `Overlayer` (SVG overlayer in paginator shadow DOM, not iframe) +- Each view in multiview paginator has its own `Overlayer` instance with unique clipPath ID +- `Overlayer.add()` stores range + draw function; `redraw()` recalculates positions from stored ranges +- Colors stored as color names mapped to custom hex via `globalReadSettings.customHighlightColors` +- Sidebar uses `color-mix()` CSS function with custom colors, not Tailwind utility classes (#3273) +- Rounded highlight style supported via `vertical` option passed to overlayer (#3208) + +### Multiview Overlayer Pitfalls +- **Duplicate SVG IDs**: Each overlayer creates `` for loupe hole — IDs MUST be unique per instance or `url(#id)` resolves to wrong element, clipping everything +- **docLoadHandler scope**: `FoliateViewer.tsx` re-adds annotations on `load` event — MUST filter by `detail.index` (loaded section), not re-add ALL annotations (overwrites drag edits) +- **MagnifierLoupe lifecycle**: Don't destroy/recreate loupe on every drag tick — `hideLoupe()` should only run on unmount, `showLoupe()` fast path updates position only +- **Stale closures in useTextSelector**: `getProgress()` must be called inside callbacks, not captured at hook top-level (useFoliateEvents deps are `[view]` only) + +## Fix History + +| Issue | Problem | Root Cause | Fix | +|-------|---------|------------|-----| +| #3286 | Selection stuck on first annotation | `initializedRef` guard blocked re-computation | Remove guard, consolidate style/color effects | +| #3273 | Custom colors not in sidebar | Hardcoded Tailwind classes | Use inline `style` with `color-mix()` | +| #3234 | Letter-by-letter selection on mobile | No word boundary snapping | Add `snapRangeToWords()` using `Intl.Segmenter` | +| #3208 | Hard rectangular highlights | No border radius support | Pass `vertical` option, update foliate-js | +| #3002 | Can't see text under finger | No magnification UI | New `MagnifierLoupe` component using `view.renderer.showLoupe()` | +| #3082 | No page numbers on annotations | `pageNumber` field missing | Add `pageNumber` to BookNote type, compute on create | +| #3225 | Android tools unresponsive | Premature `makeSelection()` call | Remove premature re-selection in Android path | + +## Common Annotation Bugs + +### Selection Issues +- **Word snapping**: Uses `Intl.Segmenter` with `granularity: 'word'` to snap selection to word boundaries +- **Android re-selection**: Don't call `makeSelection(sel, index, true)` immediately on pointer-up; let the popup flow complete +- **Range editor handles**: Remove `initializedRef` guards that prevent re-computation when switching annotations + +### Color/Style Issues +- **Custom colors in sidebar**: Use inline `style={{ backgroundColor: 'color-mix(...)' }}` not Tailwind classes +- **Style synchronization**: Consolidate `selectedStyle` and `selectedColor` into one `useEffect` +- **Switching annotations**: Must call `setShowAnnotPopup(false)` and `setEditingAnnotation(null)` before setting up new annotation + +## Reader/Content Fixes + +### Progress Display +- Use physical `view.renderer.page` and `view.renderer.pages` for page counts (#3213, #3200) +- Last page shows 100% by fixing boundary condition (#3383) +- FB2 subsections need special handling for progress (#3136) + +### Translation View (#3078) +- Problem: Page jumps back during full-text translation +- Root cause: DOM mutations from sequential translation insertions cause paginator relayout +- Fix: Batch DOM updates with 50ms timer, use bounded concurrent queue (max 5), show loading overlay + +### TOC Navigation (#3124) +- Problem: Expanding TOC chapter scrolls back to current chapter +- Fix: Only scroll-into-view on navigation, not on expand/collapse + +## Accessibility (a11y) Fixes + +### Screen Reader (TalkBack) Support +- **Page indicator updates** (#2276): Add focus handlers on `

` elements that call `view.goTo(cfi)` to update position +- **Navigation buttons** (#3036): Always show prev/next buttons when screen reader active; `PageNavigationButtons.tsx` +- **Dropdown menus** (#3035): Use `DropdownContext` with overlay dismiss instead of blur-based closing + +### Dropdown Architecture for a11y +- `DropdownContext` (`src/context/DropdownContext.tsx`) manages which dropdown is open globally +- Uses `useId()` for unique identification +- One dropdown open at a time +- `` for dismissal (tap/click outside) instead of `onBlur` +- `

` element with `open={isOpen}` for semantic structure +- No auto-focus-first-item (conflicts with TalkBack) + +## E-ink Readability +- Use `not-eink:` Tailwind variant for colors and opacity (#3258) +- Don't use `text-primary` (blue) or low opacity on e-ink +- Highlights use foreground color in dark mode e-ink (#3299) + +## Key Utility Functions +- `snapRangeToWords()` in `src/utils/sel.ts` - Word boundary snapping +- `handleAccessibilityEvents()` in `src/utils/a11y.ts` - Screen reader focus handling +- `color-mix()` CSS function for custom highlight colors with opacity diff --git a/apps/readest-app/.claude/memory/bug-patterns.md b/apps/readest-app/.claude/memory/bug-patterns.md new file mode 100644 index 00000000..4b55cd0d --- /dev/null +++ b/apps/readest-app/.claude/memory/bug-patterns.md @@ -0,0 +1,119 @@ +# 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. 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 `` 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. + +## 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 diff --git a/apps/readest-app/.claude/memory/css-style-fixes.md b/apps/readest-app/.claude/memory/css-style-fixes.md new file mode 100644 index 00000000..6fb818de --- /dev/null +++ b/apps/readest-app/.claude/memory/css-style-fixes.md @@ -0,0 +1,74 @@ +# CSS & Style Fixes Reference + +## The `style.ts` Pipeline (`src/utils/style.ts`) + +This is the most bug-prone file in the codebase (14+ fixes). It handles all EPUB CSS transformations. + +### Key Functions + +#### `getLayoutStyles()` +- Always-active styles applied to every EPUB section +- Controls: line-height, hyphens, image sizing, table display +- Rules here should NOT be conditional on user settings +- Common mistake: putting color-related rules here instead of `getColorStyles()` + +#### `getColorStyles()` +- Conditionally applied when user enables "Override Book Color" +- Controls: foreground/background colors, mix-blend modes, image backgrounds +- Gate rules on `overrideColor` flag + +#### `transformStylesheet()` +- Regex-based rewriting of EPUB CSS at load time +- Runs on every stylesheet loaded from the EPUB +- Used to neutralize problematic EPUB CSS declarations + +#### `applyTableStyle()` +- Post-render function that scales tables to fit available width +- Uses `getComputedStyle()` (not inline `style.width`) to read actual width +- Has two scaling paths: column-width-based and parent-container-based + +### Fix History by Issue + +| Issue | Problem | Fix in style.ts | +|-------|---------|-----------------| +| #3494 | Line spacing not on `
  • ` | Added `li` CSS rule for `line-height` and `hyphens` | +| #3448 | Calibre colors persist | Moved `.calibre` unset to `getColorStyles()`, added `background-color: unset` | +| #3441 | Body padding/margin | Added `padding: unset; margin: unset` to body in `getLayoutStyles()` | +| #3316 | Image bg unconditional | Made `background-color` rule conditional on `overrideColor` | +| #3377 | Image bg override | Same pattern as #3316, only override when `overrideColor` is true | +| #3334 | Generic font-family | `transformStylesheet()` replaces `font-family: serif/sans-serif` with `unset` on body | +| #3370 | user-select: none | `transformStylesheet()` replaces all `user-select: none` with `unset` | +| #3284 | Table scaling | Added fallback when `totalTableWidth` is 0 but parent has width | +| #3351 | Table display broken | Added `display: table !important` to table rule | +| #3274 | Image dimensions | Changed selectors to `:where(:not([width]))` and `:where(:not([height]))` | +| #3205 | Table width reading | Changed from `style.width` to `getComputedStyle().width`, fixed CSS var unit | +| #3112 | Mix-blend on all images | Narrowed selector to `.has-text-siblings` class | +| #3086 | Mix-blend on hr | Narrowed selector to `hr.background-img` | +| #3012 | Vertical alignment | Fixed available dimensions (subtract insets), replaced `100vw/vh` with CSS vars | + +### Common Patterns + +1. **Adding new element rules:** Copy the pattern from `p` rules (e.g., adding `li` for #3494) +2. **EPUB CSS neutralization:** Add regex in `transformStylesheet()` to replace problematic declarations +3. **Conditional overrides:** Use `overrideColor`/`overrideLayout` flags to gate rules +4. **Selector narrowing:** Use class qualifiers or attribute pseudo-selectors to avoid over-matching +5. **Table fixes:** Always use `getComputedStyle()`, not inline style. Check both width paths. + +### CSS Variables from foliate-js + +- `--available-width` - Usable content width (set by paginator.js) +- `--available-height` - Usable content height +- `--full-width` - Full viewport width (numeric, multiply by 1px) +- `--full-height` - Full viewport height (numeric, multiply by 1px) +- `--overlayer-highlight-opacity` - Highlight transparency (default 0.3) + +### foliate-js Rendering (`packages/foliate-js/paginator.js`) + +Key functions: +- `columnize()` - Paginated layout path +- `scrolled()` - Scrolled layout path +- `setImageSize()` - Constrains image dimensions to available space +- `#replaceBackground()` - Transfers EPUB backgrounds to paginator layer +- `snap()` - Swipe gesture detection for page turning + +Common issue: A fix applied to `columnize()` but not `scrolled()` (or vice versa). Always check both paths. diff --git a/apps/readest-app/.claude/memory/layout-ui-fixes.md b/apps/readest-app/.claude/memory/layout-ui-fixes.md new file mode 100644 index 00000000..5a163538 --- /dev/null +++ b/apps/readest-app/.claude/memory/layout-ui-fixes.md @@ -0,0 +1,70 @@ +# Layout & UI Fixes Reference + +## Safe Area Insets + +### Architecture +- Native plugins push inset values: iOS (`NativeBridgePlugin.swift`), Android (`NativeBridgePlugin.kt`) +- `useSafeAreaInsets` hook (`src/hooks/useSafeAreaInsets.ts`) reads and caches values +- Components use `gridInsets` for positioning relative to safe areas +- CSS: `env(safe-area-inset-top/bottom/left/right)` for CSS-level insets + +### Common Issues +- **Stale insets after system UI change** (#3469): Call `onUpdateInsets()` after `setSystemUIVisibility()` +- **iPad sidebar insets wrong** (#3395): Different inset handling needed for sidebar vs main view +- **Android nav bar overlap** (#3466): Use `calc(env(safe-area-inset-bottom) + 16px)` for Android bottom padding +- **Zoom controls behind status bar** (#3426): Pass `gridInsets` through component chain, use `Math.max(gridInsets.top, statusBarHeight)` + +### Rules (see also `docs/safe-area-insets.md`) +- Always pass `gridInsets` to overlay/floating components near screen edges +- On Android, account for system navigation bar with `env(safe-area-inset-bottom)` +- After toggling system UI visibility, force-refresh insets via `onUpdateInsets()` + +## Z-Index Hierarchy +- Navigation buttons: `z-10` (when visible) +- Annotation nav bar: `z-10` (reduced from z-30 in #3386) +- TTS control button: ensure above page nav buttons +- Floating overlays: check against `gridInsets` positioning + +## macOS Traffic Lights +- Managed by `trafficLightStore.ts` and `HeaderBar.tsx` +- **Fullscreen check required** (#3129): `await currentWindow.isFullscreen()` before hiding +- **Timeout for visibility toggle** (#3488): Use 100ms delay to prevent flickering +- **Sidebar interaction** (#3488): Check `getIsSideBarVisible()` before hiding traffic lights +- Never hide traffic lights when sidebar is open + +## Touch/Input Issues +- **Slider hit area on iOS** (#3382): Use min-h-12, strip browser appearance with CSS +- **Context menu on macOS** (#3324): 100ms delay before `onContextMenu()` to let pointer events complete +- **Swipe sensitivity** (#3310): Use average velocity (distance/time) instead of instantaneous velocity for non-animated paging +- **Touchpad natural scrolling** (#3127): Respect system natural scrolling setting in `usePagination.ts` + +## Dialog/Menu Layout +- **Dialog header** (#3352): Use `px-2 sm:pe-3 sm:ps-2` padding to align with border radius +- **Settings alignment** (#3151): Use `Menu` component instead of raw `div` for consistent styling +- **Dropdown for screen readers** (#3035): Use `DropdownContext` with overlay dismiss, not blur-based closing + +## Component-Specific Fixes + +### HeaderBar.tsx +- Traffic light visibility management +- Sidebar toggle persistent position (#3193) +- Library button placement + +### PageNavigationButtons.tsx +- z-10 when visible (#3201) +- Always shown for screen readers (#3036) +- Toggle via `showPageNavButtons` setting + +### ProgressInfo.tsx +- Use physical `page`/`pages` from renderer, not estimated values (#3213, #3200) +- CSS classes: `time-left-label`, `pages-left-label`, `progress-info-label` (#3343) + +### ReadingRuler.tsx +- Remove `containerStyle` from overlay so dimmed area covers full screen (#3304) + +### NavigationBar.tsx +- Handle `gridInsets` internally, not via pre-computed `navPadding` (#3466) + +### ContentNavBar.tsx (annotation search results) +- Floating buttons with drop shadow, not full-width bar (#3386) +- z-10 z-index diff --git a/apps/readest-app/.claude/memory/platform-compat-fixes.md b/apps/readest-app/.claude/memory/platform-compat-fixes.md new file mode 100644 index 00000000..feb5531f --- /dev/null +++ b/apps/readest-app/.claude/memory/platform-compat-fixes.md @@ -0,0 +1,85 @@ +# Platform Compatibility Fixes Reference + +## Android + +### WebView API Issues +- **`navigator.getGamepads()`** returns null on older WebView (#3245): Always null-check before `.some()` +- **`CompressionStream`** unavailable on some Android WebView versions (#3255): Add fallback zip compression path +- **Annotation tools unresponsive** (#3225): Don't call `makeSelection()` immediately on pointer-up; let popup flow complete naturally +- **Safe inset updates** (#3469): Call `onUpdateInsets()` after `setSystemUIVisibility()` +- **Navigation bar overlap** (#3466): Use `calc(env(safe-area-inset-bottom) + 16px)` for bottom padding + +### General Android Rules +- Test with older WebView versions (97+) +- Always check API availability before calling Web APIs +- Touch event handling differs from iOS - avoid premature re-selection + +## iOS + +### Common Issues +- **Slider touch dead zones** (#3382): Strip native appearance, use larger hit areas (min-h-12) +- **Safe area insets stale** (#3395): Native Swift plugin must push updated insets +- **Section content caching** (#3242, #3206): Don't cache section content in foliate-js when updating subitems; cached content retains stale styles after mode switch +- **CompressionStream** (#3255): Also broken on iOS 15.x; zip.js has its own native API disable +- **zip.js native API** (#3170): Disable native `CompressionStream`/`DecompressionStream` on iOS 15.x + +### iOS-Specific Code +- `src-tauri/plugins/tauri-plugin-native-bridge/ios/Sources/NativeBridgePlugin.swift` +- Slider CSS: `-webkit-appearance: none; appearance: none` in globals.css +- `useSafeAreaInsets.ts` hook + +## macOS + +### Traffic Lights (Window Controls) +- Check `isFullscreen()` before hiding (#3129) +- Use timeouts (100ms) for visibility transitions (#3488) +- Don't hide when sidebar is open (#3488) + +### Input Issues +- **Context menu steals event loop** (#3324): 100ms setTimeout before calling menu.popup() +- **Touchpad natural scrolling** (#3127): Respect system setting in `usePagination.ts` +- **Two-finger swipe** (#3127): Support trackpad two-finger swipe for pagination + +### macOS-Specific Code +- `src-tauri/src/macos/` - Platform-specific Rust code +- `src/store/trafficLightStore.ts` +- `src/hooks/useTrafficLight.ts` + +## Linux + +### WebKitGTK Issues +- **View Transitions API unsupported** (#3417): Feature-detect `document.startViewTransition` before calling +- Use `useAppRouter.ts` to avoid transitions on Linux + +## E-ink Devices + +### Legibility Issues +- **Low contrast colors** (#3258): Use `not-eink:` Tailwind variant prefix for colors/opacity +- **Links invisible** (#3258): Don't apply `text-primary` (blue) on e-ink; use default text color +- **Opacity too low** (#3258): Don't apply `opacity-60`/`opacity-75` on e-ink devices +- **Highlight visibility** (#3299): Use foreground color for highlights in dark mode e-ink + +### E-ink CSS Pattern +``` +// Instead of: +className="text-primary opacity-60" +// Use: +className="not-eink:text-primary not-eink:opacity-60" +``` + +## Docker/Self-Hosting +- **Missing submodules** (#3233): Run `git submodule update --init --recursive` before build +- Simplecc WASM module must be initialized + +## OPDS +- **Non-ASCII credentials** (#3436): Use `TextEncoder` + manual Base64 instead of `btoa()` +- **Author parsing** (#3120): Handle varied metadata structures in OPDS 2.0 feeds +- **Responsive layout** (#3418): Ensure catalog and download button layout works on small screens + +## Cross-Platform Testing Checklist +1. Android (old WebView + current) +2. iOS (15.x + current) +3. macOS (traffic lights, trackpad) +4. Linux (WebKitGTK) +5. E-ink devices (contrast, colors) +6. Web (CloudFlare Workers deployment) diff --git a/apps/readest-app/.claude/memory/tts-fixes.md b/apps/readest-app/.claude/memory/tts-fixes.md new file mode 100644 index 00000000..0e56a189 --- /dev/null +++ b/apps/readest-app/.claude/memory/tts-fixes.md @@ -0,0 +1,50 @@ +# TTS (Text-to-Speech) Fixes Reference + +## Architecture + +### Key Components +- `TTSController` (`src/services/tts/TTSController.ts`) - Core state machine +- `EdgeTTSClient` (`src/services/tts/EdgeTTSClient.ts`) - Edge TTS provider +- `useTTSControl` hook (`src/app/reader/hooks/useTTSControl.ts`) - React integration +- `useTTSMediaSession` hook (`src/app/reader/hooks/useTTSMediaSession.ts`) - Media controls + +### Section-Aware TTS Model +TTS tracks its own section independently from the view via `#ttsSectionIndex`: +- `#initTTSForSection()` - Creates TTS document for a section without changing the view +- `#initTTSForNextSection()` / `#initTTSForPrevSection()` - Navigate TTS across sections +- `#getHighlighter()` - Only returns highlighter if view section matches TTS section +- `onSectionChange` callback - Notifies UI when TTS crosses section boundary +- Highlights use CFI strings (not raw Range objects) for cross-section compatibility + +### State Management Pitfalls +1. **`#ttsSectionIndex` must match view section for highlights to work** + - If `-1`, all highlight calls are suppressed + - `shutdown()` sets it to `-1` but must also null out `this.view.tts` + +2. **Guards/Refs that block re-entry:** + - The old `ttsOnRef` guard blocked TTS restart from annotations (removed in #3292) + - `view.tts` reference surviving shutdown blocked re-initialization (#3400) + +3. **Timeouts that fire after pause:** + - Edge TTS had a safety timeout that advanced sentences even when paused (#3244) + - Solution: removed the entire `ontimeupdate` safety timeout mechanism + +## Fix History + +| Issue | Problem | Root Cause | Fix | +|-------|---------|------------|-----| +| #3100 | TTS scrolls too far | TTS coupled to view section | Added `#ttsSectionIndex`, "Back to TTS Location" button | +| #3198 | TTS doesn't follow to next section | No `onSectionChange` callback | Added section change notification, extracted hooks | +| #3244 | Paused TTS advances | Safety timeout fires after pause | Removed `ontimeupdate` timeout mechanism | +| #3291 | TTS fails without lang attribute | Invalid SSML from missing lang | Set lang/xml:lang on html element from `ttsLang` | +| #3292 | Can't restart TTS from annotation | `ttsOnRef` blocks re-entry | Removed the guard ref entirely | +| #3400 | TTS highlight stops after restart | `view.tts` not nulled on shutdown | Added `this.view.tts = null` in `shutdown()` | + +## Debugging TTS Issues + +1. **TTS doesn't start:** Check `#initTTSForSection()` - does `view.tts.doc === doc` shortcut early? +2. **No highlights:** Check `#ttsSectionIndex` matches view's section index +3. **Advances when paused:** Look for setTimeout/timer callbacks that bypass pause state +4. **Can't restart:** Check for refs/guards that prevent re-entry into speak handlers +5. **Fails on some chapters:** Check if chapter has lang attribute and XHTML namespace +6. **SSML errors:** Check `src/utils/ssml.ts` for proper namespace/lang handling