mirror of
https://github.com/bulwarkmail/native.git
synced 2026-10-31 16:06:32 +00:00
feat: centre sheets and float toasts bottom right on tablets
This commit is contained in:
@@ -10,6 +10,7 @@ import { useSheetDrag } from '../lib/use-sheet-drag';
|
||||
import type { Identity } from '../api/types';
|
||||
import { useLocaleStore } from '../stores/locale-store';
|
||||
import { MODAL_ORIENTATIONS } from '../lib/modal-orientations';
|
||||
import { useSheetInsets } from '../lib/use-layout';
|
||||
|
||||
interface IdentitySheetProps {
|
||||
visible: boolean;
|
||||
@@ -26,6 +27,8 @@ export function IdentitySheet({
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const insets = useSafeAreaInsets();
|
||||
const slideY = React.useRef(new Animated.Value(500)).current;
|
||||
// Centred on a wide window, clear of side insets in landscape.
|
||||
const sheetInsets = useSheetInsets();
|
||||
const overlayOpacity = React.useRef(new Animated.Value(0)).current;
|
||||
const dragHandlers = useSheetDrag({ slideY, closedY: 500, onClose });
|
||||
const { t } = useLocaleStore();
|
||||
@@ -52,7 +55,7 @@ export function IdentitySheet({
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.sheet,
|
||||
{ paddingBottom: Math.max(insets.bottom, spacing.md), transform: [{ translateY: slideY }] },
|
||||
{ paddingBottom: Math.max(insets.bottom, spacing.md), transform: [{ translateY: slideY }], ...sheetInsets },
|
||||
]}
|
||||
>
|
||||
<View {...dragHandlers}>
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useLocaleStore } from '../stores/locale-store';
|
||||
import { localizeMailboxName } from '../lib/mailbox-label';
|
||||
import type { Mailbox } from '../api/types';
|
||||
import { MODAL_ORIENTATIONS } from '../lib/modal-orientations';
|
||||
import { useSheetInsets } from '../lib/use-layout';
|
||||
|
||||
function moveTargetIcon(role: string | null | undefined, name: string): LucideIcon {
|
||||
const lower = name.toLowerCase();
|
||||
@@ -44,6 +45,8 @@ export function MoveSheet({
|
||||
const t = useLocaleStore((s) => s.t);
|
||||
const insets = useSafeAreaInsets();
|
||||
const slideY = React.useRef(new Animated.Value(500)).current;
|
||||
// Centred on a wide window, clear of side insets in landscape.
|
||||
const sheetInsets = useSheetInsets();
|
||||
const overlayOpacity = React.useRef(new Animated.Value(0)).current;
|
||||
const dragHandlers = useSheetDrag({ slideY, closedY: 500, onClose });
|
||||
|
||||
@@ -84,7 +87,7 @@ export function MoveSheet({
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.sheet,
|
||||
{ paddingBottom: Math.max(insets.bottom, spacing.md), transform: [{ translateY: slideY }] },
|
||||
{ paddingBottom: Math.max(insets.bottom, spacing.md), transform: [{ translateY: slideY }], ...sheetInsets },
|
||||
]}
|
||||
>
|
||||
<View {...dragHandlers}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { View, Text, StyleSheet, Pressable, Animated, Easing } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useFloatingCardFrame } from '../lib/use-layout';
|
||||
import { BellRing, X } from 'lucide-react-native';
|
||||
import Button from './Button';
|
||||
import { spacing, radius, typography, type ThemePalette } from '../theme/tokens';
|
||||
@@ -24,8 +25,6 @@ import {
|
||||
// Mirrors the webmail's push-notification-prompt: a short delay after login
|
||||
// so the inbox renders first, and never again for an account once dismissed.
|
||||
const PROMPT_DELAY_MS = 1500;
|
||||
// Clears the tab bar, like the toasts (ToastHost).
|
||||
const TAB_BAR_CLEARANCE = 64;
|
||||
|
||||
/**
|
||||
* Once-per-account invitation to turn on background notifications. Shown
|
||||
@@ -51,6 +50,8 @@ export function PushOnboardingPrompt(): React.ReactElement | null {
|
||||
const [busy, setBusy] = React.useState(false);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
const insets = useSafeAreaInsets();
|
||||
// Full width above the tab bar on a phone; bottom right on a tablet.
|
||||
const frame = useFloatingCardFrame();
|
||||
const duration = useAnimDuration(200);
|
||||
const appear = React.useRef(new Animated.Value(0)).current;
|
||||
|
||||
@@ -117,7 +118,7 @@ export function PushOnboardingPrompt(): React.ReactElement | null {
|
||||
style={[
|
||||
styles.wrap,
|
||||
{
|
||||
bottom: insets.bottom + TAB_BAR_CLEARANCE,
|
||||
left: frame.left, right: frame.right, bottom: insets.bottom + frame.bottom,
|
||||
opacity: appear,
|
||||
transform: [{ translateY: appear.interpolate({ inputRange: [0, 1], outputRange: [24, 0] }) }],
|
||||
},
|
||||
@@ -165,8 +166,6 @@ function makeStyles(c: ThemePalette) {
|
||||
return StyleSheet.create({
|
||||
wrap: {
|
||||
position: 'absolute',
|
||||
left: spacing.md,
|
||||
right: spacing.md,
|
||||
zIndex: 50,
|
||||
},
|
||||
card: {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useLocaleStore } from '../stores/locale-store';
|
||||
import { getEmailTagIds } from '../lib/thread-utils';
|
||||
import type { Email } from '../api/types';
|
||||
import { MODAL_ORIENTATIONS } from '../lib/modal-orientations';
|
||||
import { useSheetInsets } from '../lib/use-layout';
|
||||
|
||||
interface TagSheetProps {
|
||||
visible: boolean;
|
||||
@@ -29,6 +30,8 @@ export function TagSheet({ visible, onClose, keywords, selectedEmails, onToggle
|
||||
const t = useLocaleStore((s) => s.t);
|
||||
const insets = useSafeAreaInsets();
|
||||
const slideY = React.useRef(new Animated.Value(500)).current;
|
||||
// Centred on a wide window, clear of side insets in landscape.
|
||||
const sheetInsets = useSheetInsets();
|
||||
const overlayOpacity = React.useRef(new Animated.Value(0)).current;
|
||||
const dragHandlers = useSheetDrag({ slideY, closedY: 500, onClose });
|
||||
|
||||
@@ -73,7 +76,7 @@ export function TagSheet({ visible, onClose, keywords, selectedEmails, onToggle
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.sheet,
|
||||
{ paddingBottom: Math.max(insets.bottom, spacing.md), transform: [{ translateY: slideY }] },
|
||||
{ paddingBottom: Math.max(insets.bottom, spacing.md), transform: [{ translateY: slideY }], ...sheetInsets },
|
||||
]}
|
||||
>
|
||||
<View {...dragHandlers}>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useLocaleStore } from '../stores/locale-store';
|
||||
import { useTemplatesStore, type EmailTemplate } from '../stores/templates-store';
|
||||
import { filterTemplates } from '../lib/template-utils';
|
||||
import { MODAL_ORIENTATIONS } from '../lib/modal-orientations';
|
||||
import { useSheetInsets } from '../lib/use-layout';
|
||||
|
||||
interface TemplateSheetProps {
|
||||
visible: boolean;
|
||||
@@ -33,6 +34,8 @@ export function TemplateSheet({ visible, onClose, onPick }: TemplateSheetProps)
|
||||
const hydrate = useTemplatesStore((s) => s.hydrate);
|
||||
const [query, setQuery] = React.useState('');
|
||||
const slideY = React.useRef(new Animated.Value(500)).current;
|
||||
// Centred on a wide window, clear of side insets in landscape.
|
||||
const sheetInsets = useSheetInsets();
|
||||
const overlayOpacity = React.useRef(new Animated.Value(0)).current;
|
||||
const dragHandlers = useSheetDrag({ slideY, closedY: 500, onClose });
|
||||
|
||||
@@ -71,7 +74,7 @@ export function TemplateSheet({ visible, onClose, onPick }: TemplateSheetProps)
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.sheet,
|
||||
{ paddingBottom: Math.max(insets.bottom, spacing.md), transform: [{ translateY: slideY }] },
|
||||
{ paddingBottom: Math.max(insets.bottom, spacing.md), transform: [{ translateY: slideY }], ...sheetInsets },
|
||||
]}
|
||||
>
|
||||
<View {...dragHandlers}>
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
type AccessibilityActionEvent, type LayoutChangeEvent,
|
||||
} from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useFloatingCardFrame } from '../lib/use-layout';
|
||||
import { AlertCircle, AlertTriangle, CheckCircle2, Info } from 'lucide-react-native';
|
||||
import { spacing, radius, type ThemePalette } from '../theme/tokens';
|
||||
import { useColors, useResolvedTheme } from '../theme/colors';
|
||||
@@ -21,9 +22,14 @@ import { shouldClaimToastSwipe, toastSwipeRelease } from './toast-swipe';
|
||||
export function ToastHost(): React.ReactElement | null {
|
||||
const toasts = useToastStore((s) => s.toasts);
|
||||
const insets = useSafeAreaInsets();
|
||||
// Full width above the tab bar on a phone; bottom right on a tablet.
|
||||
const frame = useFloatingCardFrame();
|
||||
if (toasts.length === 0) return null;
|
||||
return (
|
||||
<View pointerEvents="box-none" style={[styles.wrap, { bottom: insets.bottom + 64 }]}>
|
||||
<View
|
||||
pointerEvents="box-none"
|
||||
style={[styles.wrap, { left: frame.left, right: frame.right, bottom: insets.bottom + frame.bottom }]}
|
||||
>
|
||||
{toasts.map((t) => <ToastCard key={t.id} toast={t} />)}
|
||||
</View>
|
||||
);
|
||||
@@ -226,8 +232,6 @@ function ToastCard({ toast }: { toast: Toast }) {
|
||||
const styles = StyleSheet.create({
|
||||
wrap: {
|
||||
position: 'absolute',
|
||||
left: spacing.md,
|
||||
right: spacing.md,
|
||||
gap: spacing.sm,
|
||||
zIndex: 60,
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { View, Text, StyleSheet, Pressable, Animated, Easing } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useFloatingCardFrame } from '../lib/use-layout';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { spacing, radius, typography, type ThemePalette } from '../theme/tokens';
|
||||
@@ -11,8 +12,6 @@ import { useLocaleStore } from '../stores/locale-store';
|
||||
import type { RootStackParamList } from '../navigation/types';
|
||||
|
||||
const VISIBLE_MS = 5000;
|
||||
// Clears the tab bar and the viewer's action bar, like the toasts (ToastHost).
|
||||
const BOTTOM_CLEARANCE = 64;
|
||||
|
||||
interface Shown {
|
||||
label: string;
|
||||
@@ -32,6 +31,8 @@ export function UndoSnackbar() {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const insets = useSafeAreaInsets();
|
||||
// Full width above the tab bar on a phone; bottom right on a tablet.
|
||||
const frame = useFloatingCardFrame();
|
||||
const t = useLocaleStore((s) => s.t);
|
||||
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
|
||||
const entry = useEmailStore((s) => s.pendingUndo);
|
||||
@@ -128,7 +129,7 @@ export function UndoSnackbar() {
|
||||
pointerEvents="box-none"
|
||||
style={[
|
||||
styles.wrap,
|
||||
{ bottom: insets.bottom + BOTTOM_CLEARANCE, opacity, transform: [{ translateY: slideY }] },
|
||||
{ left: frame.left, right: frame.right, bottom: insets.bottom + frame.bottom, opacity, transform: [{ translateY: slideY }] },
|
||||
]}
|
||||
>
|
||||
<View style={styles.bar}>
|
||||
@@ -160,8 +161,6 @@ function makeStyles(c: ThemePalette) {
|
||||
return StyleSheet.create({
|
||||
wrap: {
|
||||
position: 'absolute',
|
||||
left: spacing.md,
|
||||
right: spacing.md,
|
||||
zIndex: 50,
|
||||
},
|
||||
bar: {
|
||||
|
||||
@@ -18,6 +18,7 @@ import { useLocaleStore } from '../../stores/locale-store';
|
||||
import { CALENDAR_COLOR_PALETTE, calendarColorName, getCalendarColor } from '../../lib/calendar-utils';
|
||||
import Button from '../Button';
|
||||
import { MODAL_ORIENTATIONS } from '../../lib/modal-orientations';
|
||||
import { useSheetInsets } from '../../lib/use-layout';
|
||||
|
||||
export interface CalendarEditValues {
|
||||
name: string;
|
||||
@@ -36,6 +37,8 @@ interface CalendarEditSheetProps {
|
||||
// Create / rename / recolour / describe a calendar. The same fields the
|
||||
// webmail's calendar-management settings offer, as a bottom sheet.
|
||||
export function CalendarEditSheet({ visible, calendar, onSave, onClose }: CalendarEditSheetProps) {
|
||||
// Centred on a wide window, clear of side insets in landscape.
|
||||
const sheetInsets = useSheetInsets();
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const t = useLocaleStore((s) => s.t);
|
||||
@@ -72,7 +75,7 @@ export function CalendarEditSheet({ visible, calendar, onSave, onClose }: Calend
|
||||
return (
|
||||
<Modal supportedOrientations={MODAL_ORIENTATIONS} visible={visible} transparent animationType="slide" onRequestClose={onClose}>
|
||||
<TouchableWithoutFeedback onPress={onClose}>
|
||||
<View style={styles.backdrop}>
|
||||
<View style={[styles.backdrop, { paddingLeft: sheetInsets.left, paddingRight: sheetInsets.right }]}>
|
||||
<TouchableWithoutFeedback>
|
||||
<View style={styles.sheet}>
|
||||
<Text style={styles.title}>
|
||||
|
||||
@@ -18,6 +18,7 @@ import { spacing, radius, typography, type ThemePalette } from '../../theme/toke
|
||||
import { useColors } from '../../theme/colors';
|
||||
import { useLocaleStore } from '../../stores/locale-store';
|
||||
import { MODAL_ORIENTATIONS } from '../../lib/modal-orientations';
|
||||
import { useSheetInsets } from '../../lib/use-layout';
|
||||
|
||||
type RolePreset = 'freeBusy' | 'read' | 'readWrite' | 'manager';
|
||||
|
||||
@@ -70,6 +71,8 @@ interface CalendarShareSheetProps {
|
||||
// principal list comes from the same Principal/query the Files share sheet
|
||||
// uses.
|
||||
export function CalendarShareSheet({ calendar, onShare, onClose }: CalendarShareSheetProps) {
|
||||
// Centred on a wide window, clear of side insets in landscape.
|
||||
const sheetInsets = useSheetInsets();
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const t = useLocaleStore((s) => s.t);
|
||||
@@ -158,7 +161,7 @@ export function CalendarShareSheet({ calendar, onShare, onClose }: CalendarShare
|
||||
return (
|
||||
<Modal supportedOrientations={MODAL_ORIENTATIONS} visible transparent animationType="slide" onRequestClose={onClose}>
|
||||
<TouchableWithoutFeedback onPress={onClose}>
|
||||
<View style={styles.backdrop}>
|
||||
<View style={[styles.backdrop, { paddingLeft: sheetInsets.left, paddingRight: sheetInsets.right }]}>
|
||||
<TouchableWithoutFeedback>
|
||||
<View style={styles.sheet}>
|
||||
<View style={styles.titleRow}>
|
||||
|
||||
@@ -61,6 +61,7 @@ import { useSheetDrag } from '../../lib/use-sheet-drag';
|
||||
import { recurrenceEndLabel, recurrenceIntervalLabel } from '../../lib/recurrence';
|
||||
import type { TranslateFn } from '../../stores/locale-store';
|
||||
import { MODAL_ORIENTATIONS } from '../../lib/modal-orientations';
|
||||
import { useSheetInsets } from '../../lib/use-layout';
|
||||
|
||||
type RsvpStatus = 'accepted' | 'declined' | 'tentative';
|
||||
|
||||
@@ -141,6 +142,8 @@ export function EventDetailSheet({
|
||||
// name) render with the contact card's name instead of the raw email.
|
||||
const resolveContactName = useContactNameResolver();
|
||||
const slideY = React.useRef(new Animated.Value(Dimensions.get('window').height)).current;
|
||||
// Centred on a wide window, clear of side insets in landscape.
|
||||
const sheetInsets = useSheetInsets();
|
||||
const overlayOpacity = React.useRef(new Animated.Value(0)).current;
|
||||
const visible = !!event;
|
||||
const dragHandlers = useSheetDrag({
|
||||
@@ -255,7 +258,7 @@ export function EventDetailSheet({
|
||||
<Pressable style={styles.overlayPress} onPress={onClose} />
|
||||
</Animated.View>
|
||||
|
||||
<Animated.View style={[styles.sheet, { transform: [{ translateY: slideY }] }]}>
|
||||
<Animated.View style={[styles.sheet, { transform: [{ translateY: slideY }], ...sheetInsets }]}>
|
||||
<SafeAreaView edges={['bottom']} style={styles.sheetSafe}>
|
||||
<View {...dragHandlers}>
|
||||
<View style={styles.handleHit}>
|
||||
|
||||
@@ -18,6 +18,7 @@ import { useLocaleStore } from '../../stores/locale-store';
|
||||
import { useCalendarLocale } from '../../lib/calendar-locale';
|
||||
import type { Locale } from 'date-fns';
|
||||
import { MODAL_ORIENTATIONS } from '../../lib/modal-orientations';
|
||||
import { useSheetInsets } from '../../lib/use-layout';
|
||||
|
||||
// expo-document-picker is loaded lazily on first use; its native module is not
|
||||
// linked into every build (matches the FilesScreen / ContactImportSheet flow).
|
||||
@@ -75,6 +76,8 @@ export function ICalImportSheet({ visible, onClose, calendars, onImport, onImpor
|
||||
const [result, setResult] = React.useState<ImportResult | null>(null);
|
||||
|
||||
const slideY = React.useRef(new Animated.Value(900)).current;
|
||||
// Centred on a wide window, clear of side insets in landscape.
|
||||
const sheetInsets = useSheetInsets();
|
||||
const overlayOpacity = React.useRef(new Animated.Value(0)).current;
|
||||
|
||||
const resetState = React.useCallback(() => {
|
||||
@@ -189,7 +192,7 @@ export function ICalImportSheet({ visible, onClose, calendars, onImport, onImpor
|
||||
<Animated.View style={[styles.overlay, { opacity: overlayOpacity }]}>
|
||||
<Pressable style={{ flex: 1 }} onPress={onClose} />
|
||||
</Animated.View>
|
||||
<Animated.View style={[styles.sheet, { transform: [{ translateY: slideY }] }]}>
|
||||
<Animated.View style={[styles.sheet, { transform: [{ translateY: slideY }], ...sheetInsets }]}>
|
||||
<SafeAreaView edges={['bottom']} style={{ flex: 1 }}>
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.title}>{t('calendar.import.title', 'Import Calendar')}</Text>
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from '../../stores/calendar-subscriptions-store';
|
||||
import { jmapClient } from '../../api/jmap-client';
|
||||
import { MODAL_ORIENTATIONS } from '../../lib/modal-orientations';
|
||||
import { useSheetInsets } from '../../lib/use-layout';
|
||||
|
||||
// The webmail's refresh intervals and their labels.
|
||||
const INTERVAL_OPTIONS: { minutes: number; key: string; fallback: string }[] = [
|
||||
@@ -76,6 +77,8 @@ export function ICalSubscriptionSheet({ visible, onClose }: Props) {
|
||||
};
|
||||
|
||||
const slideY = React.useRef(new Animated.Value(900)).current;
|
||||
// Centred on a wide window, clear of side insets in landscape.
|
||||
const sheetInsets = useSheetInsets();
|
||||
const overlayOpacity = React.useRef(new Animated.Value(0)).current;
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -136,7 +139,7 @@ export function ICalSubscriptionSheet({ visible, onClose }: Props) {
|
||||
<Animated.View style={[styles.overlay, { opacity: overlayOpacity }]}>
|
||||
<Pressable style={{ flex: 1 }} onPress={onClose} />
|
||||
</Animated.View>
|
||||
<Animated.View style={[styles.sheet, { transform: [{ translateY: slideY }] }]}>
|
||||
<Animated.View style={[styles.sheet, { transform: [{ translateY: slideY }], ...sheetInsets }]}>
|
||||
<SafeAreaView edges={['bottom']} style={{ flex: 1 }}>
|
||||
<View style={styles.header}>
|
||||
<Rss size={20} color={c.text} />
|
||||
|
||||
@@ -43,6 +43,7 @@ import { isWritableCalendar } from '../../lib/calendar-editability';
|
||||
import { useCalendarLocale } from '../../lib/calendar-locale';
|
||||
import { useSheetDrag } from '../../lib/use-sheet-drag';
|
||||
import { MODAL_ORIENTATIONS } from '../../lib/modal-orientations';
|
||||
import { useSheetInsets } from '../../lib/use-layout';
|
||||
|
||||
type TaskFilter = 'all' | 'pending' | 'completed' | 'overdue';
|
||||
|
||||
@@ -108,6 +109,8 @@ export function TasksSheet({
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const { locale, t } = useCalendarLocale();
|
||||
const slideY = React.useRef(new Animated.Value(Dimensions.get('window').height)).current;
|
||||
// Centred on a wide window, clear of side insets in landscape.
|
||||
const sheetInsets = useSheetInsets();
|
||||
const overlayOpacity = React.useRef(new Animated.Value(0)).current;
|
||||
const dragHandlers = useSheetDrag({
|
||||
slideY,
|
||||
@@ -236,7 +239,7 @@ export function TasksSheet({
|
||||
<Pressable style={styles.overlayPress} onPress={onClose} />
|
||||
</Animated.View>
|
||||
|
||||
<Animated.View style={[styles.sheet, { transform: [{ translateY: slideY }] }]}>
|
||||
<Animated.View style={[styles.sheet, { transform: [{ translateY: slideY }], ...sheetInsets }]}>
|
||||
<SafeAreaView edges={['bottom']} style={styles.sheetSafe}>
|
||||
<View {...dragHandlers}>
|
||||
<View style={styles.handleHit}>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useSheetDrag } from '../../lib/use-sheet-drag';
|
||||
import { useContactsStore } from '../../stores/contacts-store';
|
||||
import { useLocaleStore } from '../../stores/locale-store';
|
||||
import { MODAL_ORIENTATIONS } from '../../lib/modal-orientations';
|
||||
import { useSheetInsets } from '../../lib/use-layout';
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
@@ -35,6 +36,8 @@ export default function AddressBookPickerSheet({
|
||||
const [busy, setBusy] = React.useState(false);
|
||||
|
||||
const slideY = React.useRef(new Animated.Value(600)).current;
|
||||
// Centred on a wide window, clear of side insets in landscape.
|
||||
const sheetInsets = useSheetInsets();
|
||||
const overlayOpacity = React.useRef(new Animated.Value(0)).current;
|
||||
const dragHandlers = useSheetDrag({ slideY, closedY: 600, onClose });
|
||||
|
||||
@@ -78,7 +81,7 @@ export default function AddressBookPickerSheet({
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.sheet,
|
||||
{ paddingBottom: Math.max(insets.bottom, spacing.md), transform: [{ translateY: slideY }] },
|
||||
{ paddingBottom: Math.max(insets.bottom, spacing.md), transform: [{ translateY: slideY }], ...sheetInsets },
|
||||
]}
|
||||
>
|
||||
<View {...dragHandlers}>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { spacing, radius, typography, type ThemePalette } from '../../theme/toke
|
||||
import { useColors } from '../../theme/colors';
|
||||
import { useLocaleStore } from '../../stores/locale-store';
|
||||
import { MODAL_ORIENTATIONS } from '../../lib/modal-orientations';
|
||||
import { useSheetInsets } from '../../lib/use-layout';
|
||||
|
||||
// expo-document-picker is loaded lazily on first use; its native module is not
|
||||
// linked into every build (matches the FilesScreen upload flow).
|
||||
@@ -57,6 +58,8 @@ export default function ContactImportSheet({
|
||||
const [result, setResult] = React.useState<{ imported: number; failed: number } | null>(null);
|
||||
|
||||
const slideY = React.useRef(new Animated.Value(900)).current;
|
||||
// Centred on a wide window, clear of side insets in landscape.
|
||||
const sheetInsets = useSheetInsets();
|
||||
const overlayOpacity = React.useRef(new Animated.Value(0)).current;
|
||||
|
||||
const resetState = React.useCallback(() => {
|
||||
@@ -165,7 +168,7 @@ export default function ContactImportSheet({
|
||||
<Animated.View style={[styles.overlay, { opacity: overlayOpacity }]}>
|
||||
<Pressable style={{ flex: 1 }} onPress={onClose} />
|
||||
</Animated.View>
|
||||
<Animated.View style={[styles.sheet, { transform: [{ translateY: slideY }] }]}>
|
||||
<Animated.View style={[styles.sheet, { transform: [{ translateY: slideY }], ...sheetInsets }]}>
|
||||
<SafeAreaView edges={['bottom']} style={{ flex: 1 }}>
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.title}>{t('contacts.import.title', 'Import Contacts')}</Text>
|
||||
|
||||
@@ -17,6 +17,7 @@ import { spacing, radius, typography, componentSizes, type ThemePalette } from '
|
||||
import { useColors } from '../../theme/colors';
|
||||
import { useLocaleStore } from '../../stores/locale-store';
|
||||
import { MODAL_ORIENTATIONS } from '../../lib/modal-orientations';
|
||||
import { useSheetInsets } from '../../lib/use-layout';
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
@@ -42,6 +43,8 @@ export default function ContactPickerSheet({
|
||||
const [selected, setSelected] = React.useState<Set<string>>(new Set());
|
||||
|
||||
const slideY = React.useRef(new Animated.Value(Dimensions.get('window').height)).current;
|
||||
// Centred on a wide window, clear of side insets in landscape.
|
||||
const sheetInsets = useSheetInsets();
|
||||
const overlay = React.useRef(new Animated.Value(0)).current;
|
||||
const dragHandlers = useSheetDrag({
|
||||
slideY,
|
||||
@@ -100,7 +103,7 @@ export default function ContactPickerSheet({
|
||||
<Pressable style={styles.overlayPress} onPress={onClose} />
|
||||
</Animated.View>
|
||||
|
||||
<Animated.View style={[styles.sheet, { transform: [{ translateY: slideY }] }]}>
|
||||
<Animated.View style={[styles.sheet, { transform: [{ translateY: slideY }], ...sheetInsets }]}>
|
||||
<SafeAreaView style={{ flex: 1 }} edges={['bottom']}>
|
||||
<View {...dragHandlers}>
|
||||
<View style={styles.handleHit}>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useSheetDrag } from '../../lib/use-sheet-drag';
|
||||
import { useContactsStore, selectKeywordsUsed } from '../../stores/contacts-store';
|
||||
import { useLocaleStore } from '../../stores/locale-store';
|
||||
import { MODAL_ORIENTATIONS } from '../../lib/modal-orientations';
|
||||
import { useSheetInsets } from '../../lib/use-layout';
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
@@ -36,6 +37,8 @@ export default function TagAssignSheet({
|
||||
const [input, setInput] = React.useState('');
|
||||
|
||||
const slideY = React.useRef(new Animated.Value(600)).current;
|
||||
// Centred on a wide window, clear of side insets in landscape.
|
||||
const sheetInsets = useSheetInsets();
|
||||
const overlayOpacity = React.useRef(new Animated.Value(0)).current;
|
||||
const dragHandlers = useSheetDrag({ slideY, closedY: 600, onClose });
|
||||
|
||||
@@ -68,7 +71,7 @@ export default function TagAssignSheet({
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.sheet,
|
||||
{ paddingBottom: Math.max(insets.bottom, spacing.md), transform: [{ translateY: slideY }] },
|
||||
{ paddingBottom: Math.max(insets.bottom, spacing.md), transform: [{ translateY: slideY }], ...sheetInsets },
|
||||
]}
|
||||
>
|
||||
<View {...dragHandlers}>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useColors } from '../../theme/colors';
|
||||
import { useSheetDrag } from '../../lib/use-sheet-drag';
|
||||
import { useLocaleStore } from '../../stores/locale-store';
|
||||
import { MODAL_ORIENTATIONS } from '../../lib/modal-orientations';
|
||||
import { useSheetInsets } from '../../lib/use-layout';
|
||||
|
||||
export interface ActionSheetItem {
|
||||
key: string;
|
||||
@@ -38,6 +39,8 @@ export function ActionSheet({ visible, title, subtitle, items, onClose, children
|
||||
const insets = useSafeAreaInsets();
|
||||
const t = useLocaleStore((s) => s.t);
|
||||
const slideY = React.useRef(new Animated.Value(500)).current;
|
||||
// Centred on a wide window, clear of side insets in landscape.
|
||||
const sheetInsets = useSheetInsets();
|
||||
const overlayOpacity = React.useRef(new Animated.Value(0)).current;
|
||||
const dragHandlers = useSheetDrag({ slideY, closedY: 500, onClose });
|
||||
|
||||
@@ -63,7 +66,7 @@ export function ActionSheet({ visible, title, subtitle, items, onClose, children
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.sheet,
|
||||
{ paddingBottom: Math.max(insets.bottom, spacing.md), transform: [{ translateY: slideY }] },
|
||||
{ paddingBottom: Math.max(insets.bottom, spacing.md), transform: [{ translateY: slideY }], ...sheetInsets },
|
||||
]}
|
||||
>
|
||||
<View {...dragHandlers}>
|
||||
|
||||
@@ -19,6 +19,7 @@ import { spacing, radius, typography, type ThemePalette } from '../../theme/toke
|
||||
import { useColors } from '../../theme/colors';
|
||||
import { useLocaleStore } from '../../stores/locale-store';
|
||||
import { MODAL_ORIENTATIONS } from '../../lib/modal-orientations';
|
||||
import { useSheetInsets } from '../../lib/use-layout';
|
||||
|
||||
type RolePreset = 'read' | 'readWrite' | 'manager';
|
||||
|
||||
@@ -66,6 +67,8 @@ interface ShareSheetProps {
|
||||
}
|
||||
|
||||
export default function ShareSheet({ node, onClose, onChanged }: ShareSheetProps) {
|
||||
// Centred on a wide window, clear of side insets in landscape.
|
||||
const sheetInsets = useSheetInsets();
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const t = useLocaleStore((s) => s.t);
|
||||
@@ -158,7 +161,7 @@ export default function ShareSheet({ node, onClose, onChanged }: ShareSheetProps
|
||||
return (
|
||||
<Modal supportedOrientations={MODAL_ORIENTATIONS} visible transparent animationType="slide" onRequestClose={onClose}>
|
||||
<TouchableWithoutFeedback onPress={onClose}>
|
||||
<View style={styles.backdrop}>
|
||||
<View style={[styles.backdrop, { paddingLeft: sheetInsets.left, paddingRight: sheetInsets.right }]}>
|
||||
<TouchableWithoutFeedback>
|
||||
<View style={styles.sheet}>
|
||||
<View style={styles.titleRow}>
|
||||
|
||||
@@ -26,6 +26,7 @@ import { createMailbox, updateMailbox, deleteMailbox } from '../../api/email';
|
||||
import { jmapClient } from '../../api/jmap-client';
|
||||
import type { Mailbox } from '../../api/types';
|
||||
import { MODAL_ORIENTATIONS } from '../../lib/modal-orientations';
|
||||
import { useSheetInsets } from '../../lib/use-layout';
|
||||
|
||||
const ROLE_ICON: Record<string, any> = {
|
||||
inbox: Inbox, drafts: FileText, sent: Send, trash: Trash,
|
||||
@@ -64,6 +65,8 @@ function pathOf(all: Mailbox[], mb: Mailbox, t: (k: string, f?: string) => strin
|
||||
}
|
||||
|
||||
export function FolderSettings() {
|
||||
// Centred on a wide window, clear of side insets in landscape.
|
||||
const sheetInsets = useSheetInsets();
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const t = useLocaleStore((s) => s.t);
|
||||
@@ -299,7 +302,7 @@ export function FolderSettings() {
|
||||
</SettingsSection>
|
||||
|
||||
<Modal supportedOrientations={MODAL_ORIENTATIONS} visible={!!editor} animationType="slide" transparent onRequestClose={closeEditor}>
|
||||
<View style={styles.modalOverlay}>
|
||||
<View style={[styles.modalOverlay, { paddingLeft: sheetInsets.left, paddingRight: sheetInsets.right }]}>
|
||||
<View style={styles.modalSheet}>
|
||||
<View style={styles.modalHeader}>
|
||||
<Text style={styles.modalTitle}>
|
||||
|
||||
@@ -28,6 +28,7 @@ import { isValidEmail, parseRecipientList, formatRecipient } from '../../lib/rec
|
||||
import { sanitizeDisplayName } from '../../lib/rfc5322-mailbox';
|
||||
import { sanitizeSignatureHtml } from '../../lib/signature-utils';
|
||||
import { MODAL_ORIENTATIONS } from '../../lib/modal-orientations';
|
||||
import { useSheetInsets } from '../../lib/use-layout';
|
||||
|
||||
type DraftIdentity = {
|
||||
id: string;
|
||||
@@ -93,6 +94,8 @@ function parseAddressList(value: string): { list: EmailAddress[]; invalid: strin
|
||||
}
|
||||
|
||||
export function IdentitySettings() {
|
||||
// Centred on a wide window, clear of side insets in landscape.
|
||||
const sheetInsets = useSheetInsets();
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const identities = useSettingsStore((s) => s.identities);
|
||||
@@ -300,7 +303,7 @@ export function IdentitySettings() {
|
||||
))}
|
||||
|
||||
<Modal supportedOrientations={MODAL_ORIENTATIONS} visible={!!editing} animationType="slide" transparent onRequestClose={closeEditor}>
|
||||
<View style={styles.modalOverlay}>
|
||||
<View style={[styles.modalOverlay, { paddingLeft: sheetInsets.left, paddingRight: sheetInsets.right }]}>
|
||||
<View style={styles.modalSheet}>
|
||||
<View style={styles.modalHeader}>
|
||||
<Text style={styles.modalTitle}>
|
||||
|
||||
@@ -18,8 +18,11 @@ import { useColors } from '../../theme/colors';
|
||||
import { useTemplatesStore, type EmailTemplate } from '../../stores/templates-store';
|
||||
import { useLocaleStore } from '../../stores/locale-store';
|
||||
import { MODAL_ORIENTATIONS } from '../../lib/modal-orientations';
|
||||
import { useSheetInsets } from '../../lib/use-layout';
|
||||
|
||||
export function TemplateSettings() {
|
||||
// Centred on a wide window, clear of side insets in landscape.
|
||||
const sheetInsets = useSheetInsets();
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const tr = useLocaleStore((s) => s.t);
|
||||
@@ -198,7 +201,7 @@ export function TemplateSettings() {
|
||||
|
||||
{/* Edit/Create modal */}
|
||||
<Modal supportedOrientations={MODAL_ORIENTATIONS} visible={!!editing} animationType="slide" transparent onRequestClose={closeEditor}>
|
||||
<View style={styles.modalOverlay}>
|
||||
<View style={[styles.modalOverlay, { paddingLeft: sheetInsets.left, paddingRight: sheetInsets.right }]}>
|
||||
<View style={styles.modalSheet}>
|
||||
<View style={styles.modalHeader}>
|
||||
<Text style={styles.modalTitle}>
|
||||
@@ -272,7 +275,7 @@ export function TemplateSettings() {
|
||||
|
||||
{/* Import modal */}
|
||||
<Modal supportedOrientations={MODAL_ORIENTATIONS} visible={importVisible} animationType="slide" transparent onRequestClose={() => setImportVisible(false)}>
|
||||
<View style={styles.modalOverlay}>
|
||||
<View style={[styles.modalOverlay, { paddingLeft: sheetInsets.left, paddingRight: sheetInsets.right }]}>
|
||||
<View style={styles.modalSheet}>
|
||||
<View style={styles.modalHeader}>
|
||||
<Text style={styles.modalTitle}>{tr('settings.templates.import', 'Import')}</Text>
|
||||
|
||||
@@ -25,6 +25,7 @@ import { otherAccountsSyncing, type SyncCollection } from '../../../device-sync/
|
||||
import { CONTACTS_AUTHORITY, type Authority } from '../../../device-sync/types';
|
||||
import { useSyncCollections } from './use-sync-collections';
|
||||
import { MODAL_ORIENTATIONS } from '../../../lib/modal-orientations';
|
||||
import { useSheetInsets } from '../../../lib/use-layout';
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
@@ -49,6 +50,8 @@ export function DeviceSyncCollectionsSheet({ visible, onClose, registryId, autho
|
||||
const changed = React.useRef(false);
|
||||
|
||||
const slideY = React.useRef(new Animated.Value(600)).current;
|
||||
// Centred on a wide window, clear of side insets in landscape.
|
||||
const sheetInsets = useSheetInsets();
|
||||
const overlayOpacity = React.useRef(new Animated.Value(0)).current;
|
||||
const close = React.useCallback(() => {
|
||||
const wasChanged = changed.current;
|
||||
@@ -116,7 +119,7 @@ export function DeviceSyncCollectionsSheet({ visible, onClose, registryId, autho
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.sheet,
|
||||
{ paddingBottom: Math.max(insets.bottom, spacing.md), transform: [{ translateY: slideY }] },
|
||||
{ paddingBottom: Math.max(insets.bottom, spacing.md), transform: [{ translateY: slideY }], ...sheetInsets },
|
||||
]}
|
||||
>
|
||||
<View {...dragHandlers}>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
vi.mock('react-native', () => ({ useWindowDimensions: () => ({ width: 411, height: 914 }) }));
|
||||
vi.mock('react-native-safe-area-context', () => ({ useSafeAreaInsets: () => ({ top: 0, bottom: 0, left: 0, right: 0 }) }));
|
||||
|
||||
import { layoutFor, listPaneWidth } from '../use-layout';
|
||||
import { floatingCardFrame, layoutFor, listPaneWidth, sheetSideInsets } from '../use-layout';
|
||||
|
||||
describe('layoutFor', () => {
|
||||
it('keeps a phone in portrait on the single-pane layout', () => {
|
||||
@@ -50,3 +51,31 @@ describe('listPaneWidth', () => {
|
||||
expect(listPaneWidth(2000)).toBe(420);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sheetSideInsets', () => {
|
||||
it('keeps a phone sheet full width inside the side insets', () => {
|
||||
expect(sheetSideInsets(411, 0, 0)).toEqual({ left: 0, right: 0 });
|
||||
});
|
||||
|
||||
it('centres the sheet at 560 dp on a wide window', () => {
|
||||
expect(sheetSideInsets(1280, 0, 0)).toEqual({ left: 360, right: 360 });
|
||||
expect(sheetSideInsets(914, 0, 48)).toEqual({ left: 177, right: 177 });
|
||||
});
|
||||
|
||||
it('never puts a centred sheet under a side inset', () => {
|
||||
expect(sheetSideInsets(640, 0, 48)).toEqual({ left: 48, right: 48 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('floatingCardFrame', () => {
|
||||
it('spans a phone with 12 dp margins above the tab bar', () => {
|
||||
expect(floatingCardFrame(411, false, { left: 0, right: 0 })).toEqual({ left: 12, right: 12, bottom: 64 });
|
||||
});
|
||||
|
||||
it('sits bottom right at 360 dp on a split layout', () => {
|
||||
const f = floatingCardFrame(1280, true, { left: 0, right: 0 });
|
||||
expect(1280 - f.left - f.right).toBe(360);
|
||||
expect(f.right).toBe(20);
|
||||
expect(f.bottom).toBe(20);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import { useWindowDimensions } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
/**
|
||||
* Window size classes for the adaptive layouts (docs/ux-audit-2026-10.md,
|
||||
@@ -45,3 +47,67 @@ export function listPaneWidth(windowWidth: number): number {
|
||||
|
||||
/** Width for sheets, dialogs and floating cards that would span a wide window. */
|
||||
export const SHEET_MAX_WIDTH = 560;
|
||||
|
||||
/**
|
||||
* Left and right offsets of a bottom sheet: centred at SHEET_MAX_WIDTH on a
|
||||
* window 600 dp or wider (a phone in landscape included), else full width
|
||||
* inside the side safe-area insets (a landscape notch or navigation bar).
|
||||
*/
|
||||
export function sheetSideInsets(
|
||||
width: number,
|
||||
insetLeft: number,
|
||||
insetRight: number,
|
||||
): { left: number; right: number } {
|
||||
if (width >= MEDIUM_MIN_WIDTH) {
|
||||
const side = Math.max(Math.round((width - SHEET_MAX_WIDTH) / 2), insetLeft, insetRight);
|
||||
return { left: side, right: side };
|
||||
}
|
||||
return { left: insetLeft, right: insetRight };
|
||||
}
|
||||
|
||||
/** `sheetSideInsets` for the current window. */
|
||||
export function useSheetInsets(): { left: number; right: number } {
|
||||
const { width } = useWindowDimensions();
|
||||
const insets = useSafeAreaInsets();
|
||||
const { left, right } = sheetSideInsets(width, insets.left, insets.right);
|
||||
return React.useMemo(() => ({ left, right }), [left, right]);
|
||||
}
|
||||
|
||||
/** APP.md toast card: 360 wide, 20 from the edges on a large screen. */
|
||||
const FLOATING_CARD_WIDTH = 360;
|
||||
const FLOATING_CARD_MARGIN = 20;
|
||||
/** Phone: full width with 12 dp margins, above the 64 dp tab bar. */
|
||||
const PHONE_CARD_MARGIN = 12;
|
||||
const TAB_BAR_CLEARANCE = 64;
|
||||
|
||||
/**
|
||||
* Where toasts, the undo bar and the push prompt float. A phone gets the full
|
||||
* width above the tab bar; a split layout (tabs in a side rail) gets the
|
||||
* webmail's desktop placement, bottom right.
|
||||
*/
|
||||
export function floatingCardFrame(
|
||||
width: number,
|
||||
split: boolean,
|
||||
insets: { left: number; right: number },
|
||||
): { left: number; right: number; bottom: number } {
|
||||
if (split) {
|
||||
return {
|
||||
left: Math.max(FLOATING_CARD_MARGIN, width - FLOATING_CARD_WIDTH - FLOATING_CARD_MARGIN - insets.right),
|
||||
right: FLOATING_CARD_MARGIN + insets.right,
|
||||
bottom: FLOATING_CARD_MARGIN,
|
||||
};
|
||||
}
|
||||
return {
|
||||
left: PHONE_CARD_MARGIN + insets.left,
|
||||
right: PHONE_CARD_MARGIN + insets.right,
|
||||
bottom: TAB_BAR_CLEARANCE,
|
||||
};
|
||||
}
|
||||
|
||||
/** `floatingCardFrame` for the current window; `bottom` excludes the safe-area inset. */
|
||||
export function useFloatingCardFrame(): { left: number; right: number; bottom: number } {
|
||||
const { width, height } = useWindowDimensions();
|
||||
const insets = useSafeAreaInsets();
|
||||
const { left, right, bottom } = floatingCardFrame(width, layoutFor(width, height).isSplit, insets);
|
||||
return React.useMemo(() => ({ left, right, bottom }), [left, right, bottom]);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import { spacing, radius, typography, type ThemePalette } from '../theme/tokens'
|
||||
import { useColors } from '../theme/colors';
|
||||
import { useLocaleStore, type TranslateFn } from '../stores/locale-store';
|
||||
import { MODAL_ORIENTATIONS } from '../lib/modal-orientations';
|
||||
import { useSheetInsets } from '../lib/use-layout';
|
||||
|
||||
type Nav = NativeStackNavigationProp<RootStackParamList, 'ContactDetail'>;
|
||||
type Route = RouteProp<RootStackParamList, 'ContactDetail'>;
|
||||
@@ -789,6 +790,8 @@ function MoreActionsSheet({
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const slideY = React.useRef(new Animated.Value(Dimensions.get('window').height)).current;
|
||||
// Centred on a wide window, clear of side insets in landscape.
|
||||
const sheetInsets = useSheetInsets();
|
||||
const overlayOpacity = React.useRef(new Animated.Value(0)).current;
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -810,7 +813,7 @@ function MoreActionsSheet({
|
||||
<Animated.View style={[styles.sheetOverlay, { opacity: overlayOpacity }]}>
|
||||
<Pressable style={{ flex: 1 }} onPress={onClose} />
|
||||
</Animated.View>
|
||||
<Animated.View style={[styles.sheet, { transform: [{ translateY: slideY }] }]}>
|
||||
<Animated.View style={[styles.sheet, { transform: [{ translateY: slideY }], ...sheetInsets }]}>
|
||||
<SafeAreaView edges={['bottom']}>
|
||||
<View style={styles.handleHit}>
|
||||
<View style={styles.handle} />
|
||||
|
||||
@@ -48,6 +48,7 @@ import { accountScopedId } from '../lib/thread-utils';
|
||||
import type { Email, EmailAddress, Identity } from '../api/types';
|
||||
import type { RootStackParamList } from '../navigation/types';
|
||||
import { MODAL_ORIENTATIONS } from '../lib/modal-orientations';
|
||||
import { useSheetInsets } from '../lib/use-layout';
|
||||
|
||||
type Props = NativeStackScreenProps<RootStackParamList, 'EmailThread'> & {
|
||||
/** Shown in a split view's detail pane: no back button. */
|
||||
@@ -1269,6 +1270,8 @@ function MoreMenuSheet({
|
||||
const t = useLocaleStore((s) => s.t);
|
||||
const insets = useSafeAreaInsets();
|
||||
const slideY = React.useRef(new Animated.Value(600)).current;
|
||||
// Centred on a wide window, clear of side insets in landscape.
|
||||
const sheetInsets = useSheetInsets();
|
||||
const overlayOpacity = React.useRef(new Animated.Value(0)).current;
|
||||
const dragHandlers = useSheetDrag({ slideY, closedY: 600, onClose });
|
||||
|
||||
@@ -1294,7 +1297,7 @@ function MoreMenuSheet({
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.sheet,
|
||||
{ paddingBottom: Math.max(insets.bottom, spacing.md), transform: [{ translateY: slideY }] },
|
||||
{ paddingBottom: Math.max(insets.bottom, spacing.md), transform: [{ translateY: slideY }], ...sheetInsets },
|
||||
]}
|
||||
>
|
||||
<View {...dragHandlers}>
|
||||
@@ -1418,6 +1421,8 @@ function TagMenuSheet({ visible, onClose, keywords, activeKeywords, onToggle }:
|
||||
const t = useLocaleStore((s) => s.t);
|
||||
const insets = useSafeAreaInsets();
|
||||
const slideY = React.useRef(new Animated.Value(500)).current;
|
||||
// Centred on a wide window, clear of side insets in landscape.
|
||||
const sheetInsets = useSheetInsets();
|
||||
const overlayOpacity = React.useRef(new Animated.Value(0)).current;
|
||||
const dragHandlers = useSheetDrag({ slideY, closedY: 500, onClose });
|
||||
|
||||
@@ -1443,7 +1448,7 @@ function TagMenuSheet({ visible, onClose, keywords, activeKeywords, onToggle }:
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.sheet,
|
||||
{ paddingBottom: Math.max(insets.bottom, spacing.md), transform: [{ translateY: slideY }] },
|
||||
{ paddingBottom: Math.max(insets.bottom, spacing.md), transform: [{ translateY: slideY }], ...sheetInsets },
|
||||
]}
|
||||
>
|
||||
<View {...dragHandlers}>
|
||||
|
||||
@@ -66,6 +66,7 @@ import Dialog from '../components/Dialog';
|
||||
import ShareSheet from '../components/files/ShareSheet';
|
||||
import { FilePreviewModal, canPreviewInApp } from '../components/files/FilePreviewModal';
|
||||
import { MODAL_ORIENTATIONS } from '../lib/modal-orientations';
|
||||
import { useSheetInsets } from '../lib/use-layout';
|
||||
|
||||
// A file row carries the display name alongside the rest of the node.
|
||||
interface FileRow extends FileNode {
|
||||
@@ -1208,6 +1209,8 @@ interface ActionsSheetProps {
|
||||
function ActionsSheet({
|
||||
target, sharingEnabled, t, onClose, onPreview, onOpenWith, onDownload, onShare, onMove, onDuplicate, onRename, onDelete,
|
||||
}: ActionsSheetProps) {
|
||||
// Centred on a wide window, clear of side insets in landscape.
|
||||
const sheetInsets = useSheetInsets();
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeSheetStyles(c), [c]);
|
||||
if (!target) return null;
|
||||
@@ -1220,7 +1223,7 @@ function ActionsSheet({
|
||||
return (
|
||||
<Modal supportedOrientations={MODAL_ORIENTATIONS} visible transparent animationType="slide" onRequestClose={onClose}>
|
||||
<TouchableWithoutFeedback onPress={onClose}>
|
||||
<View style={styles.backdrop}>
|
||||
<View style={[styles.backdrop, { paddingLeft: sheetInsets.left, paddingRight: sheetInsets.right }]}>
|
||||
<TouchableWithoutFeedback>
|
||||
<View style={styles.sheet}>
|
||||
<Text style={styles.sheetTitle} numberOfLines={1}>
|
||||
@@ -1302,6 +1305,8 @@ interface FolderPickerSheetProps {
|
||||
// its descendants (a folder can't be moved into itself), flattened with
|
||||
// indentation, plus the drive root.
|
||||
function FolderPickerSheet({ target, allNodes, rootLabel, t, onClose, onPick }: FolderPickerSheetProps) {
|
||||
// Centred on a wide window, clear of side insets in landscape.
|
||||
const sheetInsets = useSheetInsets();
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeSheetStyles(c), [c]);
|
||||
const options = useMemo(() => {
|
||||
@@ -1342,7 +1347,7 @@ function FolderPickerSheet({ target, allNodes, rootLabel, t, onClose, onPick }:
|
||||
return (
|
||||
<Modal supportedOrientations={MODAL_ORIENTATIONS} visible transparent animationType="slide" onRequestClose={onClose}>
|
||||
<TouchableWithoutFeedback onPress={onClose}>
|
||||
<View style={styles.backdrop}>
|
||||
<View style={[styles.backdrop, { paddingLeft: sheetInsets.left, paddingRight: sheetInsets.right }]}>
|
||||
<TouchableWithoutFeedback>
|
||||
<View style={styles.sheet}>
|
||||
<Text style={styles.sheetTitle} numberOfLines={1}>
|
||||
@@ -1390,6 +1395,8 @@ interface SortSheetProps {
|
||||
}
|
||||
|
||||
function SortSheet({ visible, sortKey, sortDir, t, onClose, onChangeKey, onChangeDir }: SortSheetProps) {
|
||||
// Centred on a wide window, clear of side insets in landscape.
|
||||
const sheetInsets = useSheetInsets();
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeSheetStyles(c), [c]);
|
||||
if (!visible) return null;
|
||||
@@ -1397,7 +1404,7 @@ function SortSheet({ visible, sortKey, sortDir, t, onClose, onChangeKey, onChang
|
||||
return (
|
||||
<Modal supportedOrientations={MODAL_ORIENTATIONS} visible transparent animationType="slide" onRequestClose={onClose}>
|
||||
<TouchableWithoutFeedback onPress={onClose}>
|
||||
<View style={styles.backdrop}>
|
||||
<View style={[styles.backdrop, { paddingLeft: sheetInsets.left, paddingRight: sheetInsets.right }]}>
|
||||
<TouchableWithoutFeedback>
|
||||
<View style={styles.sheet}>
|
||||
<Text style={styles.sheetTitle}>{t('files.settings_default_sort', 'Sort')}</Text>
|
||||
|
||||
Reference in New Issue
Block a user