This commit is contained in:
@@ -66,7 +66,6 @@ Platform-specific code lives in `src-tauri/src/{macos,windows,android,ios}/`. Cu
|
||||
- No lookbehind regex in build output — verified by `check:lookbehind-regex`.
|
||||
- Run `pnpm build-check` (builds both targets + runs all checks) before submitting.
|
||||
|
||||
### Formatting
|
||||
### Safe Area Insets
|
||||
|
||||
- Prettier is enforced via husky pre-commit hook + lint-staged.
|
||||
- Run `pnpm format` to auto-fix.
|
||||
See [docs/safe-area-insets.md](docs/safe-area-insets.md) for rules on handling top/bottom insets for UI elements near screen edges.
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
## 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):
|
||||
|
||||
```tsx
|
||||
// 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:
|
||||
|
||||
```tsx
|
||||
style={{
|
||||
paddingTop: appService?.hasSafeAreaInset ? `${gridInsets.top}px` : '0px',
|
||||
}}
|
||||
```
|
||||
|
||||
### 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:
|
||||
|
||||
```tsx
|
||||
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:
|
||||
|
||||
```tsx
|
||||
<ImageViewer gridInsets={gridInsets} ... />
|
||||
<TableViewer gridInsets={gridInsets} ... />
|
||||
<ZoomControls gridInsets={gridInsets} ... />
|
||||
```
|
||||
@@ -594,6 +594,7 @@ const FoliateViewer: React.FC<{
|
||||
<>
|
||||
{selectedImage && (
|
||||
<ImageViewer
|
||||
gridInsets={gridInsets}
|
||||
src={selectedImage}
|
||||
onClose={handleCloseImage}
|
||||
onPrevious={currentImageIndex > 0 ? handlePreviousImage : undefined}
|
||||
@@ -602,6 +603,7 @@ const FoliateViewer: React.FC<{
|
||||
)}
|
||||
{selectedTableHtml && (
|
||||
<TableViewer
|
||||
gridInsets={gridInsets}
|
||||
html={selectedTableHtml}
|
||||
isDarkMode={isDarkMode}
|
||||
onClose={() => setSelectedTableHtml(null)}
|
||||
|
||||
@@ -3,9 +3,11 @@ import React, { useState, useRef, useEffect } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { IoChevronBack, IoChevronForward } from 'react-icons/io5';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { Insets } from '@/types/misc';
|
||||
import ZoomControls from './ZoomControls';
|
||||
|
||||
interface ImageViewerProps {
|
||||
gridInsets: Insets;
|
||||
src: string | null;
|
||||
onClose: () => void;
|
||||
onPrevious?: () => void;
|
||||
@@ -18,7 +20,13 @@ const ZOOM_SPEED = 0.1;
|
||||
const MOBILE_ZOOM_SPEED = 0.001;
|
||||
const ZOOM_BIAS = 1.05;
|
||||
|
||||
const ImageViewer: React.FC<ImageViewerProps> = ({ src, onClose, onPrevious, onNext }) => {
|
||||
const ImageViewer: React.FC<ImageViewerProps> = ({
|
||||
src,
|
||||
onClose,
|
||||
onPrevious,
|
||||
onNext,
|
||||
gridInsets,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const [scale, setScale] = useState(1);
|
||||
const [zoomSpeed, setZoomSpeed] = useState(0.1);
|
||||
@@ -391,6 +399,7 @@ const ImageViewer: React.FC<ImageViewerProps> = ({ src, onClose, onPrevious, onN
|
||||
}}
|
||||
/>
|
||||
<ZoomControls
|
||||
gridInsets={gridInsets}
|
||||
onClose={onClose}
|
||||
onZoomIn={handleZoomIn}
|
||||
onZoomOut={handleZoomOut}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { Insets } from '@/types/misc';
|
||||
import ZoomControls from './ZoomControls';
|
||||
|
||||
interface TableViewerProps {
|
||||
gridInsets: Insets;
|
||||
html: string | null;
|
||||
isDarkMode: boolean;
|
||||
onClose: () => void;
|
||||
@@ -13,7 +15,7 @@ const MIN_SCALE = 0.5;
|
||||
const MAX_SCALE = 4;
|
||||
const ZOOM_SPEED = 0.1;
|
||||
|
||||
const TableViewer: React.FC<TableViewerProps> = ({ html, isDarkMode, onClose }) => {
|
||||
const TableViewer: React.FC<TableViewerProps> = ({ gridInsets, html, isDarkMode, onClose }) => {
|
||||
const _ = useTranslation();
|
||||
const [scale, setScale] = useState(1);
|
||||
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||
@@ -188,6 +190,7 @@ const TableViewer: React.FC<TableViewerProps> = ({ html, isDarkMode, onClose })
|
||||
}}
|
||||
/>
|
||||
<ZoomControls
|
||||
gridInsets={gridInsets}
|
||||
onClose={onClose}
|
||||
onZoomIn={handleZoomIn}
|
||||
onZoomOut={handleZoomOut}
|
||||
|
||||
@@ -1,18 +1,35 @@
|
||||
import React from 'react';
|
||||
import { IoClose, IoExpand, IoAdd, IoRemove } from 'react-icons/io5';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useThemeStore } from '@/store/themeStore';
|
||||
import { Insets } from '@/types/misc';
|
||||
|
||||
interface ZoomControlsProps {
|
||||
gridInsets: Insets;
|
||||
onClose: () => void;
|
||||
onZoomIn: () => void;
|
||||
onZoomOut: () => void;
|
||||
onReset: () => void;
|
||||
}
|
||||
|
||||
const ZoomControls: React.FC<ZoomControlsProps> = ({ onClose, onZoomIn, onZoomOut, onReset }) => {
|
||||
const ZoomControls: React.FC<ZoomControlsProps> = ({
|
||||
gridInsets,
|
||||
onClose,
|
||||
onZoomIn,
|
||||
onZoomOut,
|
||||
onReset,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const { systemUIVisible, statusBarHeight } = useThemeStore();
|
||||
return (
|
||||
<div className='absolute right-4 top-4 z-10 grid grid-cols-1 gap-4 text-white'>
|
||||
<div
|
||||
className='absolute right-4 top-2 z-10 grid grid-cols-1 gap-4 text-white'
|
||||
style={{
|
||||
marginTop: systemUIVisible
|
||||
? `${Math.max(gridInsets.top, statusBarHeight)}px`
|
||||
: `${gridInsets.top}px`,
|
||||
}}
|
||||
>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className='eink-bordered flex h-10 w-10 cursor-pointer items-center justify-center rounded-full bg-black/50 transition-colors hover:bg-black/70'
|
||||
|
||||
Reference in New Issue
Block a user