weitere perfektionierung der neuen mobile app
This commit is contained in:
@@ -6,6 +6,8 @@ import { Home, CheckSquare, Image as ImageIcon, User } from 'lucide-react';
|
||||
import { useTheme } from '@tamagui/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const ICON_SIZE = 18;
|
||||
|
||||
export type NavKey = 'home' | 'tasks' | 'uploads' | 'profile';
|
||||
|
||||
export function BottomNav({ active, onNavigate }: { active: NavKey; onNavigate: (key: NavKey) => void }) {
|
||||
@@ -19,17 +21,17 @@ export function BottomNav({ active, onNavigate }: { active: NavKey; onNavigate:
|
||||
];
|
||||
|
||||
return (
|
||||
<YStack
|
||||
position="fixed"
|
||||
bottom={0}
|
||||
left={0}
|
||||
right={0}
|
||||
backgroundColor="white"
|
||||
borderTopWidth={1}
|
||||
borderColor="#e5e7eb"
|
||||
paddingVertical="$2"
|
||||
paddingHorizontal="$4"
|
||||
zIndex={50}
|
||||
<YStack
|
||||
position="fixed"
|
||||
bottom={0}
|
||||
left={0}
|
||||
right={0}
|
||||
backgroundColor={String(theme.surface?.val ?? 'white')}
|
||||
borderTopWidth={1}
|
||||
borderColor={String(theme.borderColor?.val ?? '#e5e7eb')}
|
||||
paddingVertical="$2"
|
||||
paddingHorizontal="$4"
|
||||
zIndex={50}
|
||||
shadowColor="#0f172a"
|
||||
shadowOpacity={0.08}
|
||||
shadowRadius={12}
|
||||
@@ -44,15 +46,31 @@ export function BottomNav({ active, onNavigate }: { active: NavKey; onNavigate:
|
||||
return (
|
||||
<Pressable key={item.key} onPress={() => onNavigate(item.key)}>
|
||||
<YStack
|
||||
flexGrow={1}
|
||||
flexBasis="0%"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
space="$1"
|
||||
position="relative"
|
||||
padding="$2"
|
||||
minWidth={88}
|
||||
minHeight={64}
|
||||
paddingHorizontal="$3"
|
||||
paddingVertical="$2"
|
||||
borderRadius={12}
|
||||
backgroundColor={activeState ? '#e8f1ff' : 'transparent'}
|
||||
gap="$1"
|
||||
>
|
||||
<IconCmp size={20} color={activeState ? String(theme.primary?.val ?? '#007AFF') : '#94a3b8'} />
|
||||
<Text fontSize="$xs" color={activeState ? '$primary' : '#6b7280'}>
|
||||
<YStack width={ICON_SIZE} height={ICON_SIZE} alignItems="center" justifyContent="center" shrink={0}>
|
||||
<IconCmp size={ICON_SIZE} color={activeState ? String(theme.primary?.val ?? '#007AFF') : '#94a3b8'} />
|
||||
</YStack>
|
||||
<Text
|
||||
fontSize="$xs"
|
||||
fontWeight="700"
|
||||
fontFamily="$body"
|
||||
color={activeState ? '$primary' : '#6b7280'}
|
||||
textAlign="center"
|
||||
flexShrink={1}
|
||||
>
|
||||
{item.label}
|
||||
</Text>
|
||||
</YStack>
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import React from 'react';
|
||||
import React, { Suspense } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { ChevronDown, ChevronLeft, Bell, QrCode } from 'lucide-react';
|
||||
import { YStack, XStack } from '@tamagui/stacks';
|
||||
import { SizableText as Text } from '@tamagui/text';
|
||||
import { Pressable } from '@tamagui/react-native-web-lite';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useTheme } from '@tamagui/core';
|
||||
import { useEventContext } from '../../context/EventContext';
|
||||
import { BottomNav, NavKey } from './BottomNav';
|
||||
import { useMobileNav } from '../hooks/useMobileNav';
|
||||
import { adminPath } from '../../constants';
|
||||
import { MobileSheet } from './Sheet';
|
||||
import { MobileCard, PillBadge } from './Primitives';
|
||||
import { useAlertsBadge } from '../hooks/useAlertsBadge';
|
||||
import { useNotificationsBadge } from '../hooks/useNotificationsBadge';
|
||||
import { formatEventDate, resolveEventDisplayName } from '../../lib/events';
|
||||
import { TenantEvent, getEvents } from '../../api';
|
||||
const DevTenantSwitcher = React.lazy(() => import('../../components/DevTenantSwitcher'));
|
||||
|
||||
type MobileShellProps = {
|
||||
title?: string;
|
||||
@@ -29,11 +31,18 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
|
||||
const { go } = useMobileNav(activeEvent?.slug);
|
||||
const navigate = useNavigate();
|
||||
const { t, i18n } = useTranslation('mobile');
|
||||
const { count: alertCount } = useAlertsBadge();
|
||||
const { count: notificationCount } = useNotificationsBadge();
|
||||
const theme = useTheme();
|
||||
const backgroundColor = String(theme.background?.val ?? '#f7f8fb');
|
||||
const surfaceColor = String(theme.surface?.val ?? '#ffffff');
|
||||
const borderColor = String(theme.borderColor?.val ?? '#e5e7eb');
|
||||
const textColor = String(theme.color?.val ?? '#111827');
|
||||
const mutedText = String(theme.gray?.val ?? '#6b7280');
|
||||
const [pickerOpen, setPickerOpen] = React.useState(false);
|
||||
const [fallbackEvents, setFallbackEvents] = React.useState<TenantEvent[]>([]);
|
||||
const [loadingEvents, setLoadingEvents] = React.useState(false);
|
||||
const [attemptedFetch, setAttemptedFetch] = React.useState(false);
|
||||
const showDevTenantSwitcher = import.meta.env.DEV && import.meta.env.VITE_ENABLE_TENANT_SWITCHER === 'true';
|
||||
|
||||
const locale = i18n.language?.startsWith('en') ? 'en-GB' : 'de-DE';
|
||||
const effectiveEvents = events.length ? events : fallbackEvents;
|
||||
@@ -81,11 +90,11 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
|
||||
const showQr = Boolean(effectiveActive?.slug);
|
||||
|
||||
return (
|
||||
<YStack backgroundColor="#f7f8fb" minHeight="100vh">
|
||||
<YStack backgroundColor={backgroundColor} minHeight="100vh">
|
||||
<YStack
|
||||
backgroundColor="white"
|
||||
backgroundColor={surfaceColor}
|
||||
borderBottomWidth={1}
|
||||
borderColor="#e5e7eb"
|
||||
borderColor={borderColor}
|
||||
paddingHorizontal="$4"
|
||||
paddingTop="$4"
|
||||
paddingBottom="$3"
|
||||
@@ -94,82 +103,88 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
|
||||
shadowRadius={10}
|
||||
shadowOffset={{ width: 0, height: 4 }}
|
||||
>
|
||||
<XStack alignItems="center" justifyContent="space-between" space="$3">
|
||||
<XStack alignItems="center" space="$2">
|
||||
{onBack ? (
|
||||
<Pressable onPress={onBack}>
|
||||
<XStack alignItems="center" space="$1.5">
|
||||
<ChevronLeft size={18} color="#007AFF" />
|
||||
</XStack>
|
||||
</Pressable>
|
||||
) : null}
|
||||
<Pressable
|
||||
disabled={!showEventSwitcher}
|
||||
onPress={() => setPickerOpen(true)}
|
||||
style={{ alignItems: 'flex-start' }}
|
||||
>
|
||||
<Text fontSize="$lg" fontWeight="800" color="#111827">
|
||||
{eventTitle}
|
||||
</Text>
|
||||
{subtitleText ? (
|
||||
<Text fontSize="$xs" color="#6b7280">
|
||||
{subtitleText}
|
||||
</Text>
|
||||
) : null}
|
||||
</Pressable>
|
||||
{showEventSwitcher ? <ChevronDown size={14} color="#111827" /> : null}
|
||||
</XStack>
|
||||
<XStack alignItems="center" space="$2.5">
|
||||
<Pressable onPress={() => navigate(adminPath('/mobile/alerts'))}>
|
||||
<XStack
|
||||
width={34}
|
||||
height={34}
|
||||
borderRadius={12}
|
||||
backgroundColor="#f4f5f7"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
position="relative"
|
||||
>
|
||||
<Bell size={16} color="#111827" />
|
||||
{alertCount > 0 ? (
|
||||
<YStack
|
||||
position="absolute"
|
||||
top={-4}
|
||||
right={-4}
|
||||
minWidth={18}
|
||||
height={18}
|
||||
paddingHorizontal={6}
|
||||
borderRadius={999}
|
||||
backgroundColor="#ef4444"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Text fontSize={10} color="white" fontWeight="700">
|
||||
{alertCount > 9 ? '9+' : alertCount}
|
||||
</Text>
|
||||
</YStack>
|
||||
) : null}
|
||||
<XStack alignItems="center" justifyContent="space-between" minHeight={48} space="$3">
|
||||
{onBack ? (
|
||||
<Pressable onPress={onBack}>
|
||||
<XStack alignItems="center" space="$1.5">
|
||||
<ChevronLeft size={18} color="#007AFF" />
|
||||
</XStack>
|
||||
</Pressable>
|
||||
{showQr ? (
|
||||
<Pressable onPress={() => navigate(adminPath(`/mobile/events/${effectiveActive?.slug}/qr`))}>
|
||||
) : (
|
||||
<XStack width={18} />
|
||||
)}
|
||||
|
||||
<XStack alignItems="center" space="$2.5" flex={1} justifyContent="flex-end">
|
||||
<XStack alignItems="center" space="$1" maxWidth="55%">
|
||||
<Pressable
|
||||
disabled={!showEventSwitcher}
|
||||
onPress={() => setPickerOpen(true)}
|
||||
style={{ alignItems: 'flex-end' }}
|
||||
>
|
||||
<Text fontSize="$lg" fontWeight="800" fontFamily="$display" color={textColor} textAlign="right" numberOfLines={1}>
|
||||
{eventTitle}
|
||||
</Text>
|
||||
{subtitleText ? (
|
||||
<Text fontSize="$xs" color={mutedText} textAlign="right" numberOfLines={1} fontFamily="$body">
|
||||
{subtitleText}
|
||||
</Text>
|
||||
) : null}
|
||||
</Pressable>
|
||||
{showEventSwitcher ? <ChevronDown size={14} color={textColor} /> : null}
|
||||
</XStack>
|
||||
|
||||
<XStack alignItems="center" space="$2">
|
||||
<Pressable onPress={() => navigate(adminPath('/mobile/notifications'))}>
|
||||
<XStack
|
||||
width={34}
|
||||
height={34}
|
||||
paddingHorizontal="$3"
|
||||
borderRadius={12}
|
||||
backgroundColor="#0ea5e9"
|
||||
backgroundColor={surfaceColor}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
space="$1.5"
|
||||
position="relative"
|
||||
>
|
||||
<QrCode size={16} color="white" />
|
||||
<Text fontSize="$xs" fontWeight="800" color="white">
|
||||
{t('header.quickQr', 'Quick QR')}
|
||||
</Text>
|
||||
<Bell size={16} color={textColor} />
|
||||
{notificationCount > 0 ? (
|
||||
<YStack
|
||||
position="absolute"
|
||||
top={-4}
|
||||
right={-4}
|
||||
minWidth={18}
|
||||
height={18}
|
||||
paddingHorizontal={6}
|
||||
borderRadius={999}
|
||||
backgroundColor="#ef4444"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Text fontSize={10} color="white" fontWeight="700">
|
||||
{notificationCount > 9 ? '9+' : notificationCount}
|
||||
</Text>
|
||||
</YStack>
|
||||
) : null}
|
||||
</XStack>
|
||||
</Pressable>
|
||||
) : null}
|
||||
{headerActions ?? null}
|
||||
{showQr ? (
|
||||
<Pressable onPress={() => navigate(adminPath(`/mobile/events/${effectiveActive?.slug}/qr`))}>
|
||||
<XStack
|
||||
height={34}
|
||||
paddingHorizontal="$3"
|
||||
borderRadius={12}
|
||||
backgroundColor="#0ea5e9"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
space="$1.5"
|
||||
>
|
||||
<QrCode size={16} color="white" />
|
||||
<Text fontSize="$xs" fontWeight="800" color="white">
|
||||
{t('header.quickQr', 'Quick QR')}
|
||||
</Text>
|
||||
</XStack>
|
||||
</Pressable>
|
||||
) : null}
|
||||
{headerActions ?? null}
|
||||
</XStack>
|
||||
</XStack>
|
||||
</XStack>
|
||||
</YStack>
|
||||
@@ -178,6 +193,12 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
|
||||
{children}
|
||||
</YStack>
|
||||
|
||||
{showDevTenantSwitcher ? (
|
||||
<Suspense fallback={null}>
|
||||
<DevTenantSwitcher bottomOffset={96} />
|
||||
</Suspense>
|
||||
) : null}
|
||||
|
||||
<BottomNav active={activeTab} onNavigate={go} />
|
||||
|
||||
<MobileSheet
|
||||
@@ -190,10 +211,10 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
|
||||
<YStack space="$2">
|
||||
{effectiveEvents.length === 0 ? (
|
||||
<MobileCard alignItems="flex-start" space="$2">
|
||||
<Text fontSize="$sm" color="#111827" fontWeight="700">
|
||||
<Text fontSize="$sm" color={textColor} fontWeight="700">
|
||||
{t('header.noEventsTitle', 'Create your first event')}
|
||||
</Text>
|
||||
<Text fontSize="$xs" color="#4b5563">
|
||||
<Text fontSize="$xs" color={mutedText}>
|
||||
{t('header.noEventsBody', 'Start an event to access tasks, uploads, QR posters and more.')}
|
||||
</Text>
|
||||
<Pressable onPress={() => navigate(adminPath('/mobile/events/new'))}>
|
||||
@@ -219,10 +240,10 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
|
||||
>
|
||||
<XStack alignItems="center" justifyContent="space-between" paddingVertical="$2">
|
||||
<YStack space="$0.5">
|
||||
<Text fontSize="$sm" fontWeight="700" color="#111827">
|
||||
<Text fontSize="$sm" fontWeight="700" color={textColor}>
|
||||
{resolveEventDisplayName(event)}
|
||||
</Text>
|
||||
<Text fontSize="$xs" color="#6b7280">
|
||||
<Text fontSize="$xs" color={mutedText}>
|
||||
{formatEventDate(event.event_date, locale) ?? t('header.noDate', 'Date tbd')}
|
||||
</Text>
|
||||
</YStack>
|
||||
|
||||
@@ -5,12 +5,13 @@ import { Pressable } from '@tamagui/react-native-web-lite';
|
||||
import { useTheme } from '@tamagui/core';
|
||||
|
||||
export function MobileCard({ children, ...rest }: React.ComponentProps<typeof YStack>) {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<YStack
|
||||
backgroundColor="white"
|
||||
backgroundColor={String(theme.surface?.val ?? 'white')}
|
||||
borderRadius={16}
|
||||
borderWidth={1}
|
||||
borderColor="#e5e7eb"
|
||||
borderColor={String(theme.borderColor?.val ?? '#e5e7eb')}
|
||||
shadowColor="#0f172a"
|
||||
shadowOpacity={0.06}
|
||||
shadowRadius={12}
|
||||
@@ -31,10 +32,23 @@ export function PillBadge({
|
||||
tone?: 'success' | 'warning' | 'muted';
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const theme = useTheme();
|
||||
const palette: Record<typeof tone, { bg: string; text: string; border: string }> = {
|
||||
success: { bg: '#ecfdf3', text: '#047857', border: '#bbf7d0' },
|
||||
warning: { bg: '#fffbeb', text: '#92400e', border: '#fef3c7' },
|
||||
muted: { bg: '#f3f4f6', text: '#374151', border: '#e5e7eb' },
|
||||
success: {
|
||||
bg: String(theme.backgroundStrong?.val ?? '#ecfdf3'),
|
||||
text: String(theme.green10?.val ?? '#047857'),
|
||||
border: String(theme.green6?.val ?? '#bbf7d0'),
|
||||
},
|
||||
warning: {
|
||||
bg: String(theme.yellow3?.val ?? '#fffbeb'),
|
||||
text: String(theme.yellow11?.val ?? '#92400e'),
|
||||
border: String(theme.yellow6?.val ?? '#fef3c7'),
|
||||
},
|
||||
muted: {
|
||||
bg: String(theme.gray3?.val ?? '#f3f4f6'),
|
||||
text: String(theme.gray11?.val ?? '#374151'),
|
||||
border: String(theme.gray6?.val ?? '#e5e7eb'),
|
||||
},
|
||||
};
|
||||
const colors = palette[tone] ?? palette.muted;
|
||||
return (
|
||||
@@ -72,11 +86,11 @@ export function CTAButton({
|
||||
borderRadius={14}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
backgroundColor={isPrimary ? String(theme.primary?.val ?? '#007AFF') : 'white'}
|
||||
backgroundColor={isPrimary ? String(theme.primary?.val ?? '#007AFF') : String(theme.surface?.val ?? 'white')}
|
||||
borderWidth={isPrimary ? 0 : 1}
|
||||
borderColor={isPrimary ? 'transparent' : '#e5e7eb'}
|
||||
borderColor={isPrimary ? 'transparent' : String(theme.borderColor?.val ?? '#e5e7eb')}
|
||||
>
|
||||
<Text fontSize="$sm" fontWeight="800" color={isPrimary ? 'white' : '#111827'}>
|
||||
<Text fontSize="$sm" fontWeight="800" color={isPrimary ? 'white' : String(theme.color?.val ?? '#111827')}>
|
||||
{label}
|
||||
</Text>
|
||||
</XStack>
|
||||
@@ -93,17 +107,25 @@ export function KpiTile({
|
||||
label: string;
|
||||
value: string | number;
|
||||
}) {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<MobileCard borderRadius={14} padding="$3" width="32%" minWidth={110} alignItems="flex-start">
|
||||
<XStack alignItems="center" space="$2">
|
||||
<XStack width={32} height={32} borderRadius={12} backgroundColor="#e5f0ff" alignItems="center" justifyContent="center">
|
||||
<IconCmp size={16} color="#2563eb" />
|
||||
<XStack
|
||||
width={32}
|
||||
height={32}
|
||||
borderRadius={12}
|
||||
backgroundColor={String(theme.blue3?.val ?? '#e5f0ff')}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<IconCmp size={16} color={String(theme.primary?.val ?? '#2563eb')} />
|
||||
</XStack>
|
||||
<Text fontSize="$xs" color="#111827">
|
||||
<Text fontSize="$xs" color={String(theme.color?.val ?? '#111827')}>
|
||||
{label}
|
||||
</Text>
|
||||
</XStack>
|
||||
<Text fontSize="$xl" fontWeight="800" color="#111827">
|
||||
<Text fontSize="$xl" fontWeight="800" color={String(theme.color?.val ?? '#111827')}>
|
||||
{value}
|
||||
</Text>
|
||||
</MobileCard>
|
||||
@@ -121,6 +143,8 @@ export function ActionTile({
|
||||
color: string;
|
||||
onPress: () => void;
|
||||
}) {
|
||||
const theme = useTheme();
|
||||
const text = String(theme.color12?.val ?? theme.color?.val ?? '#f8fafc');
|
||||
return (
|
||||
<Pressable onPress={onPress} style={{ width: '48%', marginBottom: 12 }}>
|
||||
<YStack
|
||||
@@ -137,7 +161,7 @@ export function ActionTile({
|
||||
<XStack width={34} height={34} borderRadius={12} backgroundColor={color} alignItems="center" justifyContent="center">
|
||||
<IconCmp size={16} color="white" />
|
||||
</XStack>
|
||||
<Text fontSize="$sm" fontWeight="700" color="#111827" textAlign="center">
|
||||
<Text fontSize="$sm" fontWeight="700" color={text} textAlign="center">
|
||||
{label}
|
||||
</Text>
|
||||
</YStack>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { SizableText as Text } from '@tamagui/text';
|
||||
import { Pressable } from '@tamagui/react-native-web-lite';
|
||||
import { ChevronLeft } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useTheme } from '@tamagui/core';
|
||||
|
||||
type MobileScaffoldProps = {
|
||||
title: string;
|
||||
@@ -15,24 +16,30 @@ type MobileScaffoldProps = {
|
||||
|
||||
export function MobileScaffold({ title, onBack, rightSlot, children, footer }: MobileScaffoldProps) {
|
||||
const { t } = useTranslation('mobile');
|
||||
const theme = useTheme();
|
||||
const background = String(theme.background?.val ?? '#f7f8fb');
|
||||
const surface = String(theme.surface?.val ?? '#ffffff');
|
||||
const border = String(theme.borderColor?.val ?? '#e5e7eb');
|
||||
const textColor = String(theme.color?.val ?? '#111827');
|
||||
|
||||
return (
|
||||
<YStack backgroundColor="#f7f8fb" minHeight="100vh">
|
||||
<YStack backgroundColor={background} minHeight="100vh">
|
||||
<XStack
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
paddingHorizontal="$4"
|
||||
paddingTop="$4"
|
||||
paddingBottom="$3"
|
||||
backgroundColor="white"
|
||||
backgroundColor={surface}
|
||||
borderBottomWidth={1}
|
||||
borderColor="#e5e7eb"
|
||||
borderColor={border}
|
||||
>
|
||||
<XStack alignItems="center" space="$2">
|
||||
{onBack ? (
|
||||
<Pressable onPress={onBack}>
|
||||
<XStack alignItems="center" space="$1.5">
|
||||
<ChevronLeft size={18} color="#007AFF" />
|
||||
<Text fontSize="$sm" color="#007AFF" fontWeight="600">
|
||||
<ChevronLeft size={18} color={String(theme.primary?.val ?? '#007AFF')} />
|
||||
<Text fontSize="$sm" color={String(theme.primary?.val ?? '#007AFF')} fontWeight="600">
|
||||
{t('actions.back', 'Back')}
|
||||
</Text>
|
||||
</XStack>
|
||||
@@ -41,7 +48,7 @@ export function MobileScaffold({ title, onBack, rightSlot, children, footer }: M
|
||||
<Text />
|
||||
)}
|
||||
</XStack>
|
||||
<Text fontSize="$lg" fontWeight="800" color="#111827">
|
||||
<Text fontSize="$lg" fontWeight="800" color={textColor}>
|
||||
{title}
|
||||
</Text>
|
||||
<XStack minWidth={40} justifyContent="flex-end">
|
||||
|
||||
@@ -3,6 +3,7 @@ import { YStack, XStack } from '@tamagui/stacks';
|
||||
import { SizableText as Text } from '@tamagui/text';
|
||||
import { Pressable } from '@tamagui/react-native-web-lite';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useTheme } from '@tamagui/core';
|
||||
|
||||
type SheetProps = {
|
||||
open: boolean;
|
||||
@@ -16,15 +17,21 @@ type SheetProps = {
|
||||
|
||||
export function MobileSheet({ open, title, onClose, children, footer, bottomOffsetPx = 88 }: SheetProps) {
|
||||
const { t } = useTranslation('mobile');
|
||||
const theme = useTheme();
|
||||
const surface = String(theme.surface?.val ?? '#111827');
|
||||
const text = String(theme.color12?.val ?? theme.color?.val ?? '#f8fafc');
|
||||
const muted = String(theme.gray11?.val ?? theme.gray?.val ?? '#cbd5e1');
|
||||
const overlay = String(theme.gray12?.val ?? 'rgba(0,0,0,0.6)');
|
||||
|
||||
if (!open) return null;
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-end justify-center bg-black/40 backdrop-blur-sm">
|
||||
<div className="fixed inset-0 z-50 flex items-end justify-center backdrop-blur-sm" style={{ backgroundColor: `${overlay}66` }}>
|
||||
<YStack
|
||||
width="100%"
|
||||
maxWidth={520}
|
||||
borderTopLeftRadius={24}
|
||||
borderTopRightRadius={24}
|
||||
backgroundColor="white"
|
||||
backgroundColor={surface}
|
||||
padding="$4"
|
||||
paddingBottom="$7"
|
||||
space="$3"
|
||||
@@ -38,11 +45,11 @@ export function MobileSheet({ open, title, onClose, children, footer, bottomOffs
|
||||
style={{ marginBottom: `max(env(safe-area-inset-bottom, 0px), ${bottomOffsetPx}px)` }}
|
||||
>
|
||||
<XStack alignItems="center" justifyContent="space-between">
|
||||
<Text fontSize="$md" fontWeight="800" color="#111827">
|
||||
<Text fontSize="$md" fontWeight="800" color={text}>
|
||||
{title}
|
||||
</Text>
|
||||
<Pressable onPress={onClose}>
|
||||
<Text fontSize="$md" color="#6b7280">
|
||||
<Text fontSize="$md" color={muted}>
|
||||
{t('actions.close', 'Close')}
|
||||
</Text>
|
||||
</Pressable>
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
import React from 'react';
|
||||
import { SizableText as Text } from '@tamagui/text';
|
||||
import { XStack } from '@tamagui/stacks';
|
||||
import { useTheme } from '@tamagui/core';
|
||||
|
||||
export function Tag({ label, color }: { label: string; color?: string }) {
|
||||
const theme = useTheme();
|
||||
const baseColor = color ?? String(theme.borderColor?.val ?? '#e5e7eb');
|
||||
const textColor = String(theme.color?.val ?? '#111827');
|
||||
|
||||
export function Tag({ label, color = '#e5e7eb' }: { label: string; color?: string }) {
|
||||
return (
|
||||
<XStack
|
||||
alignItems="center"
|
||||
paddingHorizontal="$2"
|
||||
paddingVertical={2}
|
||||
borderRadius={999}
|
||||
backgroundColor={`${color}22`}
|
||||
backgroundColor={`${baseColor}22`}
|
||||
borderWidth={1}
|
||||
borderColor={`${color}55`}
|
||||
borderColor={`${baseColor}55`}
|
||||
alignSelf="flex-start"
|
||||
>
|
||||
<Text fontSize={11} fontWeight="600" color="#111827">
|
||||
<Text fontSize={11} fontWeight="600" color={textColor}>
|
||||
{label}
|
||||
</Text>
|
||||
</XStack>
|
||||
|
||||
Reference in New Issue
Block a user