Files
readest/apps/readest-app/docs/safe-area-insets.md
T
Huang Xin f1ae050768 fix(ui): refine reader side panels and their empty states (#4361)
* 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>
2026-05-30 08:29:41 +02:00

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 by getViewInsets() in src/utils/insets.ts. Passed as a prop from BooksGrid → child components.
  • statusBarHeight: number — OS status bar height (default 24px). Stored in themeStore.
  • systemUIVisible: boolean — Whether the system UI (status bar, navigation bar) is currently shown. Stored in themeStore.
  • 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} ... />