Virtualizing BooknoteView (#4352) dropped the auto-scroll that centers the
note nearest the current reading position, leaving the list stranded at the
top. The single scrollToIndex that replaced the per-item useScrollToItem was
missing the machinery TOCView already uses for virtualized auto-scroll:
- Re-apply the scroll inside the OverlayScrollbars `initialized` callback
(read via a ref): its deferred init resets the viewport scrollTop to 0, and
the lastScrolledCfiRef guard otherwise blocked any retry (reload case).
- Mount Virtuoso natively centered via initialTopMostItemIndex with a
skip-gate, so opening the panel while reading doesn't fire a scrollToIndex
that races and wedges the freshly mounted, unmeasured list (tab-switch case).
- Jump instantly (behavior 'auto') for far moves and on eink, animating
'smooth' only for short in-session updates — mirroring TOCView.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(agent): add agent notes for cache, reading-ruler, foliate touch
Add project-memory notes and index entries:
- manage-cache-ios-layout: iOS container layout and what Manage Cache clears
- reading-ruler-line-aware: line/column-aware reading ruler internals
- foliate-touch-listener-capture-phase: capture-phase gesture suppression
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(reader): pad sidebar and notebook for the device status bar (#4089)
Top-anchored slide-in panels (sidebar, notebook) only applied status-bar
top padding when isFullHeightInMobile was true. On a tablet/desktop
(isMobile === false) that gate collapsed the padding to 0, so a visible
system status bar overlapped the panel's top toolbar and made its icons
inaccessible.
Extract the inset math into getPanelTopInset() and gate it on
(!isMobile || isFullHeightInMobile) so non-mobile panels clear the status
bar like the reader header, while a partial-height mobile bottom sheet
(which doesn't reach the top of the screen) stays flush.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(reader): keep footer bar clear of the pinned sidebar
On a mobile tablet in portrait, forceMobileLayout renders the footer bar
with position: fixed, anchored to the viewport, so left-0 w-full spans
the whole window and slides under a pinned sidebar — the progress / font
/ TTS controls end up obscured.
Anchor the footer inside the book's grid cell (position: absolute) when
the sidebar is pinned, mirroring the header bar. The flex layout already
offsets the grid cell by the sidebar's real rendered width, which honors
the sidebar's min-w-60 floor and 45% cap that a stored-width offset would
miss. The slide-up panels are absolute within the footer container, so
they shift and narrow with it and their animation is unchanged. The
switch only happens when the sidebar is pinned, so phone (< 640px) and
unpinned tablet-portrait class names stay identical.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ui): refine reader side panels and their empty states
Closes#4089
Add a shared EmptyState component (large muted icon, title, and an
optional hint or action) and use it for the empty annotations, bookmarks,
and notes panels in the sidebar and notebook, replacing the ad-hoc
"No … yet" placeholders.
Polish the surrounding chrome: switch the bookmark toggler to the Ri icon
set with responsive sizing, crop the HighlighterIcon viewBox to its
artwork to remove the asymmetric bottom padding, and tune mobile sizing
and spacing across the panel headers, tab navigation, and footer nav bar.
Translate the new empty-state strings (No Notes, No Annotations, No
Bookmarks, and their hints/action) across all 33 locales and drop the
obsolete "No … yet" keys.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Switching the annotation/bookmark sidebar to a flat virtualized list eliminates
the per-item layout reads that caused multi-second jank when toggling tabs on
books with hundreds of notes.
A. Virtualize the list with react-virtuoso
- Flatten group headers + notes into a single FlatBooknoteRow array.
- Embed an OverlayScrollbars instance inside the tab so scrollbar styling
is preserved while Virtuoso owns the viewport (same nested pattern as
TOCView).
- Track the parent scroll-container's height with ResizeObserver to give
Virtuoso a bounded viewport.
- Replace the per-item useScrollToItem (which called getBoundingClientRect
and closest() on every BooknoteItem on every progress tick — O(n) sync
reflow on 1000+ items) with a single virtuosoRef.scrollToIndex driven by
nearestCfi.
B. Stabilize derivations with useMemo / useCallback
- filteredNotes, sortedGroups, flatItems, nearestCfi all useMemo so an
unrelated config change (e.g. viewSettings autosave) no longer triggers
a full sort + group rebuild.
- handleBrowseBookNotes is now useCallback so BooknoteItem's React.memo
can hit on prop equality.
C. Memoize BooknoteItem
- Wrap the component in React.memo. With stable item / onClick references
from the parent, re-renders triggered by sibling progress updates no
longer cascade across every visible row.
- Cache marked.parse(item.note) and dayjs(item.createdAt).fromNow() in
useMemo. marked is the dominant per-render cost for note rows.
- isCurrent moves to a useMemo over isCfiInLocation; the per-item
scrollIntoView is removed since BooknoteView now drives scrolling.
useScrollToItem is intentionally left intact — SearchResults still uses it
and its smaller list does not exhibit the same jank.
* feat: add empty state hints and loading indicators for annotations, bookmarks, notes, font import, and Moon+ Reader import
- BooknoteView: show 'No annotation yet' / 'No bookmark yet' when empty
- Notebook: show 'No note yet' when no notes/excerpts exist
- Annotator: add loading overlay with spinner during Moon+ Reader import
- mrexpt: yield to event loop every 5 entries to keep spinner animating
- CustomFonts: show in-place loading card during font import, spinner transitions to font name without layout jump
* fix(ui): respect e-ink overlay styling and drop dead className branch
- Annotator: use modal-box on the mrexpt import overlay so eink picks up
the no-shadow + 1px border override automatically.
- CustomFonts: collapse importing-card clsx ternary whose branches were
identical into a flat className.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Huang Xin <chrox.huang@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Add H and L keyboard shortcuts for back and forward navigation, consistent with Vimium
* Fallback bookmark text to page number if current page has no text, closes#51
* Add delete and edit buttons for highlights and booknotes in the sidebar, closes#57
* Each book now has its own sidebar default tab