* 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>
2.4 KiB
Safe Area Insets
The app runs on devices with notches, status bars, and rounded corners (iOS, Android). UI elements near screen edges must account for safe area insets to avoid being obscured.
Key Concepts
gridInsets: Insets— Per-view insets derived from view settings (header/footer visibility, margins). Calculated bygetViewInsets()insrc/utils/insets.ts. Passed as a prop fromBooksGrid→ child components.statusBarHeight: number— OS status bar height (default 24px). Stored inthemeStore.systemUIVisible: boolean— Whether the system UI (status bar, navigation bar) is currently shown. Stored inthemeStore.appService?.hasSafeAreaInset— Whether the platform requires safe area handling (mobile devices).
Top Inset Rules
For UI elements anchored to the top of the screen (headers, close buttons, overlays):
// When system UI is visible, use the larger of gridInsets.top and statusBarHeight
// When system UI is hidden, use gridInsets.top alone
style={{
marginTop: systemUIVisible
? `${Math.max(gridInsets.top, statusBarHeight)}px`
: `${gridInsets.top}px`,
}}
For containers that need safe area padding at the top:
style={{
paddingTop: appService?.hasSafeAreaInset ? `${gridInsets.top}px` : '0px',
}}
For top-anchored slide-in panels (sidebar, notebook), use getPanelTopInset() from src/utils/insets.ts. It clears the status bar on tablet/desktop and full-height mobile sheets, but stays flush for a partial-height mobile bottom sheet (which doesn't reach the top of the screen). Gating only on isFullHeightInMobile is wrong — a non-mobile panel is also top-anchored and would let the status bar obscure its toolbar.
Bottom Inset Rules
For UI elements anchored to the bottom of the screen (footer bars, controls, progress indicators), use gridInsets.bottom * 0.33 as padding — a fraction of the full inset since bottom bars don't need as much clearance as the home indicator area:
style={{
paddingBottom: appService?.hasSafeAreaInset ? `${gridInsets.bottom * 0.33}px` : 0,
}}
Passing gridInsets
When creating overlay components (image viewers, table viewers, zoom controls, etc.), always pass gridInsets as a prop so they can position their controls correctly:
<ImageViewer gridInsets={gridInsets} ... />
<TableViewer gridInsets={gridInsets} ... />
<ZoomControls gridInsets={gridInsets} ... />