neues Admin UI Layout eingeführt. Alle Tests auf den neusten Stand gebracht.

This commit is contained in:
Codex Agent
2025-12-30 10:24:06 +01:00
parent 902e78cae9
commit efe2f25b3e
85 changed files with 95235 additions and 19197 deletions

View File

@@ -3,9 +3,9 @@ import { YStack, XStack } from '@tamagui/stacks';
import { SizableText as Text } from '@tamagui/text';
import { Pressable } from '@tamagui/react-native-web-lite';
import { Home, CheckSquare, Image as ImageIcon, User } from 'lucide-react';
import { useTheme } from '@tamagui/core';
import { useTranslation } from 'react-i18next';
import { withAlpha } from './colors';
import { useAdminTheme } from '../theme';
const ICON_SIZE = 20;
@@ -13,8 +13,8 @@ export type NavKey = 'home' | 'tasks' | 'uploads' | 'profile';
export function BottomNav({ active, onNavigate }: { active: NavKey; onNavigate: (key: NavKey) => void }) {
const { t } = useTranslation('mobile');
const theme = useTheme();
const surfaceColor = String(theme.surface?.val ?? 'white');
const { surface, border, primary, accentSoft, muted, subtle, shadow } = useAdminTheme();
const surfaceColor = surface;
const navSurface = withAlpha(surfaceColor, 0.92);
const [pressedKey, setPressedKey] = React.useState<NavKey | null>(null);
const items: Array<{ key: NavKey; icon: React.ComponentType<{ size?: number; color?: string }>; label: string }> = [
@@ -32,11 +32,11 @@ export function BottomNav({ active, onNavigate }: { active: NavKey; onNavigate:
right={0}
backgroundColor={navSurface}
borderTopWidth={1}
borderColor={String(theme.borderColor?.val ?? '#e5e7eb')}
borderColor={border}
paddingVertical="$2"
paddingHorizontal="$4"
zIndex={50}
shadowColor="#0f172a"
shadowColor={shadow}
shadowOpacity={0.08}
shadowRadius={12}
shadowOffset={{ width: 0, height: -4 }}
@@ -72,7 +72,7 @@ export function BottomNav({ active, onNavigate }: { active: NavKey; onNavigate:
paddingHorizontal="$3"
paddingVertical="$2"
borderRadius={12}
backgroundColor={activeState ? '#e8f1ff' : 'transparent'}
backgroundColor={activeState ? accentSoft : 'transparent'}
gap="$1"
style={{
transform: isPressed ? 'scale(0.96)' : 'scale(1)',
@@ -87,17 +87,17 @@ export function BottomNav({ active, onNavigate }: { active: NavKey; onNavigate:
width={28}
height={3}
borderRadius={999}
backgroundColor={String(theme.primary?.val ?? '#007AFF')}
backgroundColor={primary}
/>
) : null}
<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'} />
<IconCmp size={ICON_SIZE} color={activeState ? primary : subtle} />
</YStack>
<Text
fontSize="$xs"
fontWeight="700"
fontFamily="$body"
color={activeState ? '$primary' : '#6b7280'}
color={activeState ? primary : muted}
textAlign="center"
flexShrink={1}
>

View File

@@ -2,8 +2,8 @@ import React from 'react';
import { ChevronDown } from 'lucide-react';
import { YStack, XStack } from '@tamagui/stacks';
import { SizableText as Text } from '@tamagui/text';
import { useTheme } from '@tamagui/core';
import { withAlpha } from './colors';
import { useAdminTheme } from '../theme';
type FieldProps = {
label: string;
@@ -13,24 +13,21 @@ type FieldProps = {
};
export function MobileField({ label, hint, error, children }: FieldProps) {
const theme = useTheme();
const labelColor = String(theme.color?.val ?? '#111827');
const hintColor = String(theme.gray?.val ?? '#6b7280');
const errorColor = String(theme.red10?.val ?? '#b91c1c');
const { text, muted, danger } = useAdminTheme();
return (
<YStack space="$1.5">
<Text fontSize="$sm" fontWeight="800" color={labelColor}>
<Text fontSize="$sm" fontWeight="800" color={text}>
{label}
</Text>
{children}
{hint ? (
<Text fontSize="$xs" color={hintColor}>
<Text fontSize="$xs" color={muted}>
{hint}
</Text>
) : null}
{error ? (
<Text fontSize="$xs" color={errorColor}>
<Text fontSize="$xs" color={danger}>
{error}
</Text>
) : null}
@@ -45,13 +42,8 @@ type ControlProps = {
export const MobileInput = React.forwardRef<HTMLInputElement, React.ComponentPropsWithoutRef<'input'> & ControlProps>(
function MobileInput({ hasError = false, compact = false, style, ...props }, ref) {
const theme = useTheme();
const { border, surface, text, primary, danger } = useAdminTheme();
const [focused, setFocused] = React.useState(false);
const border = String(theme.borderColor?.val ?? '#e5e7eb');
const surface = String(theme.surface?.val ?? '#ffffff');
const text = String(theme.color?.val ?? '#111827');
const primary = String(theme.primary?.val ?? '#2563eb');
const danger = String(theme.red10?.val ?? '#b91c1c');
const height = compact ? 36 : 44;
const borderColor = hasError ? danger : focused ? primary : border;
@@ -92,13 +84,8 @@ export const MobileTextArea = React.forwardRef<
HTMLTextAreaElement,
React.ComponentPropsWithoutRef<'textarea'> & ControlProps
>(function MobileTextArea({ hasError = false, compact = false, style, ...props }, ref) {
const theme = useTheme();
const { border, surface, text, primary, danger } = useAdminTheme();
const [focused, setFocused] = React.useState(false);
const border = String(theme.borderColor?.val ?? '#e5e7eb');
const surface = String(theme.surface?.val ?? '#ffffff');
const text = String(theme.color?.val ?? '#111827');
const primary = String(theme.primary?.val ?? '#2563eb');
const danger = String(theme.red10?.val ?? '#b91c1c');
const borderColor = hasError ? danger : focused ? primary : border;
const ringColor = hasError ? withAlpha(danger, 0.18) : withAlpha(primary, 0.18);
@@ -141,14 +128,8 @@ export function MobileSelect({
style,
...props
}: React.ComponentPropsWithoutRef<'select'> & ControlProps) {
const theme = useTheme();
const { border, surface, text, primary, danger, subtle } = useAdminTheme();
const [focused, setFocused] = React.useState(false);
const border = String(theme.borderColor?.val ?? '#e5e7eb');
const surface = String(theme.surface?.val ?? '#ffffff');
const text = String(theme.color?.val ?? '#111827');
const primary = String(theme.primary?.val ?? '#2563eb');
const danger = String(theme.red10?.val ?? '#b91c1c');
const muted = String(theme.gray?.val ?? '#94a3b8');
const height = compact ? 36 : 44;
const borderColor = hasError ? danger : focused ? primary : border;
@@ -186,7 +167,7 @@ export function MobileSelect({
{children}
</select>
<XStack position="absolute" right={12} pointerEvents="none">
<ChevronDown size={16} color={muted} />
<ChevronDown size={16} color={subtle} />
</XStack>
</XStack>
);

View File

@@ -1,9 +1,9 @@
import React from 'react';
import { YStack } from '@tamagui/stacks';
import { SizableText as Text } from '@tamagui/text';
import { useTheme } from '@tamagui/core';
import { MobileSheet } from './Sheet';
import { CTAButton } from './Primitives';
import { useAdminTheme } from '../theme';
type Translator = (key: string, defaultValue?: string) => string;
@@ -37,20 +37,17 @@ export function LegalConsentSheet({
copy,
t,
}: LegalConsentSheetProps) {
const theme = useTheme();
const { primary, border, surface, danger, text } = useAdminTheme();
const [acceptedTerms, setAcceptedTerms] = React.useState(false);
const [acceptedWaiver, setAcceptedWaiver] = React.useState(false);
const [error, setError] = React.useState<string | null>(null);
const checkboxAccent = String(theme.primary?.val ?? '#2563eb');
const checkboxBorder = String(theme.borderColor?.val ?? '#e5e7eb');
const checkboxSurface = String(theme.surface?.val ?? '#ffffff');
const checkboxStyle = {
marginTop: 4,
width: 18,
height: 18,
accentColor: checkboxAccent,
backgroundColor: checkboxSurface,
border: `1px solid ${checkboxBorder}`,
accentColor: primary,
backgroundColor: surface,
border: `1px solid ${border}`,
borderRadius: 4,
appearance: 'auto',
WebkitAppearance: 'auto',
@@ -90,7 +87,7 @@ export function LegalConsentSheet({
footer={
<YStack space="$2">
{error ? (
<Text fontSize="$sm" color="#b91c1c">
<Text fontSize="$sm" color={danger}>
{error}
</Text>
) : null}
@@ -110,7 +107,7 @@ export function LegalConsentSheet({
}
>
<YStack space="$2">
<Text fontSize="$sm" color="#111827">
<Text fontSize="$sm" color={text}>
{copy?.description ?? t('events.legalConsent.description', 'Please confirm the legal notes before buying an add-on.')}
</Text>
{requireTerms ? (
@@ -121,7 +118,7 @@ export function LegalConsentSheet({
onChange={(event) => setAcceptedTerms(event.target.checked)}
style={checkboxStyle}
/>
<Text fontSize="$sm" color="#111827">
<Text fontSize="$sm" color={text}>
{copy?.checkboxTerms ?? t(
'events.legalConsent.checkboxTerms',
'I have read and accept the Terms & Conditions, Privacy Policy, and Right of Withdrawal.',
@@ -137,7 +134,7 @@ export function LegalConsentSheet({
onChange={(event) => setAcceptedWaiver(event.target.checked)}
style={checkboxStyle}
/>
<Text fontSize="$sm" color="#111827">
<Text fontSize="$sm" color={text}>
{copy?.checkboxWaiver ?? t(
'events.legalConsent.checkboxWaiver',
'I expressly request immediate provision of the digital service and understand my right of withdrawal expires once fulfilled.',

View File

@@ -9,8 +9,8 @@ vi.mock('@tamagui/core', () => ({
gray11: { val: '#6b7280' },
gray6: { val: '#e5e7eb' },
gray2: { val: '#f8fafc' },
blue3: { val: '#dbeafe' },
primary: { val: '#2563eb' },
blue3: { val: '#FFE5EC' },
primary: { val: '#FF5A5F' },
}),
}));

View File

@@ -2,11 +2,11 @@ import React from 'react';
import { Download, Share2, X } from 'lucide-react';
import { YStack, XStack } from '@tamagui/stacks';
import { SizableText as Text } from '@tamagui/text';
import { useTheme } from '@tamagui/core';
import { Pressable } from '@tamagui/react-native-web-lite';
import { InstallBannerState } from '../lib/installBanner';
import { CTAButton, MobileCard } from './Primitives';
import { useTranslation } from 'react-i18next';
import { useAdminTheme } from '../theme';
type MobileInstallBannerProps = {
state: InstallBannerState | null;
@@ -22,13 +22,7 @@ export function MobileInstallBanner({
density = 'default',
}: MobileInstallBannerProps) {
const { t } = useTranslation('common');
const theme = useTheme();
const text = String(theme.color12?.val ?? theme.color?.val ?? '#0f172a');
const muted = String(theme.gray11?.val ?? theme.gray?.val ?? '#6b7280');
const border = String(theme.gray6?.val ?? theme.borderColor?.val ?? '#e5e7eb');
const accent = String(theme.primary?.val ?? '#2563eb');
const surface = String(theme.gray2?.val ?? '#f8fafc');
const accentSoft = String(theme.blue3?.val ?? '#dbeafe');
const { textStrong, muted, border, primary, surfaceMuted, accentSoft } = useAdminTheme();
if (!state) {
return null;
@@ -41,7 +35,7 @@ export function MobileInstallBanner({
<MobileCard
space={isCompact ? '$1.5' : '$2'}
borderColor={border}
backgroundColor={surface}
backgroundColor={surfaceMuted}
padding={isCompact ? '$2' : '$3'}
>
<XStack alignItems="center" justifyContent="space-between" gap="$2">
@@ -54,10 +48,10 @@ export function MobileInstallBanner({
justifyContent="center"
backgroundColor={accentSoft}
>
{isPrompt ? <Download size={16} color={accent} /> : <Share2 size={16} color={accent} />}
{isPrompt ? <Download size={16} color={primary} /> : <Share2 size={16} color={primary} />}
</XStack>
<YStack flex={1} space="$0.5">
<Text fontSize={isCompact ? '$xs' : '$sm'} fontWeight="800" color={text}>
<Text fontSize={isCompact ? '$xs' : '$sm'} fontWeight="800" color={textStrong}>
{t('installBanner.title', 'Install Fotospiel Admin')}
</Text>
<Text fontSize={isCompact ? 10 : '$xs'} color={muted}>
@@ -70,7 +64,7 @@ export function MobileInstallBanner({
<XStack alignItems="center" space="$2">
{isPrompt && onInstall && isCompact ? (
<Pressable onPress={onInstall}>
<Text fontSize={10} fontWeight="700" color={accent}>
<Text fontSize={10} fontWeight="700" color={primary}>
{t('installBanner.action', 'Install')}
</Text>
</Pressable>

View File

@@ -5,7 +5,6 @@ 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';
@@ -20,6 +19,7 @@ import { withAlpha } from './colors';
import { setTabHistory } from '../lib/tabHistory';
import { loadPhotoQueue } from '../lib/photoModerationQueue';
import { countQueuedPhotoActions } from '../lib/queueStatus';
import { useAdminTheme } from '../theme';
type MobileShellProps = {
title?: string;
@@ -38,14 +38,12 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
const { t, i18n } = useTranslation('mobile');
const { count: notificationCount } = useNotificationsBadge();
const online = useOnlineStatus();
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 warningBg = String(theme.yellow3?.val ?? '#fef3c7');
const warningText = String(theme.yellow11?.val ?? '#92400e');
const { background, surface, border, text, muted, warningBg, warningText, primary, danger, shadow } = useAdminTheme();
const backgroundColor = background;
const surfaceColor = surface;
const borderColor = border;
const textColor = text;
const mutedText = muted;
const headerSurface = withAlpha(surfaceColor, 0.94);
const [pickerOpen, setPickerOpen] = React.useState(false);
const [fallbackEvents, setFallbackEvents] = React.useState<TenantEvent[]>([]);
@@ -129,7 +127,7 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
paddingHorizontal="$4"
paddingTop="$4"
paddingBottom="$3"
shadowColor="#0f172a"
shadowColor={shadow}
shadowOpacity={0.06}
shadowRadius={10}
shadowOffset={{ width: 0, height: 4 }}
@@ -148,7 +146,7 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
{onBack ? (
<HeaderActionButton onPress={onBack} ariaLabel={t('actions.back', 'Back')}>
<XStack alignItems="center" space="$1.5">
<ChevronLeft size={28} color="#007AFF" strokeWidth={2.5} />
<ChevronLeft size={28} color={primary} strokeWidth={2.5} />
</XStack>
</HeaderActionButton>
) : (
@@ -198,7 +196,7 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
height={18}
paddingHorizontal={6}
borderRadius={999}
backgroundColor="#ef4444"
backgroundColor={danger}
alignItems="center"
justifyContent="center"
>
@@ -218,7 +216,7 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
height={34}
paddingHorizontal="$3"
borderRadius={12}
backgroundColor="#0ea5e9"
backgroundColor={primary}
alignItems="center"
justifyContent="center"
space="$1.5"
@@ -302,7 +300,7 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
</Text>
<Pressable onPress={() => navigate(adminPath('/mobile/events/new'))}>
<XStack alignItems="center" space="$2">
<Text fontSize="$sm" color="#007AFF" fontWeight="700">
<Text fontSize="$sm" color={primary} fontWeight="700">
{t('header.createEvent', 'Create event')}
</Text>
</XStack>
@@ -346,7 +344,7 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
setPickerOpen(false);
}}
>
<Text fontSize="$xs" color="#6b7280" textAlign="center">
<Text fontSize="$xs" color={mutedText} textAlign="center">
{t('header.clearSelection', 'Clear selection')}
</Text>
</Pressable>

View File

@@ -3,8 +3,8 @@ import { ChevronLeft } 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 { useTheme } from '@tamagui/core';
import { useTranslation } from 'react-i18next';
import { useAdminTheme } from '../theme';
type OnboardingShellProps = {
eyebrow?: string;
@@ -30,12 +30,7 @@ export function OnboardingShell({
skipLabel,
}: OnboardingShellProps) {
const { t } = useTranslation('onboarding');
const theme = useTheme();
const background = String(theme.background?.val ?? '#f7f8fb');
const surface = String(theme.surface?.val ?? '#ffffff');
const text = String(theme.color?.val ?? '#111827');
const muted = String(theme.gray?.val ?? '#6b7280');
const border = String(theme.borderColor?.val ?? '#e5e7eb');
const { background, surface, text, textStrong, muted, border, shadow } = useAdminTheme();
const resolvedBackLabel = backLabel ?? t('layout.back', 'Back');
const resolvedSkipLabel = skipLabel ?? t('layout.skip', 'Skip');
@@ -55,7 +50,7 @@ export function OnboardingShell({
>
<XStack alignItems="center" justifyContent="space-between">
{onBack ? (
<Pressable onPress={onBack} aria-label="Back">
<Pressable onPress={onBack} aria-label={resolvedBackLabel}>
<XStack alignItems="center" space="$1.5">
<ChevronLeft size={22} color={text} />
<Text fontSize="$sm" fontWeight="700" color={text}>
@@ -68,7 +63,7 @@ export function OnboardingShell({
)}
{onSkip ? (
<Pressable onPress={onSkip} aria-label="Skip">
<Pressable onPress={onSkip} aria-label={resolvedSkipLabel}>
<Text fontSize="$sm" fontWeight="700" color={muted}>
{resolvedSkipLabel}
</Text>
@@ -84,7 +79,7 @@ export function OnboardingShell({
borderWidth={1}
borderColor={border}
backgroundColor={surface}
shadowColor="#0f172a"
shadowColor={shadow}
shadowOpacity={0.06}
shadowRadius={14}
shadowOffset={{ width: 0, height: 8 }}
@@ -95,7 +90,7 @@ export function OnboardingShell({
{eyebrow}
</Text>
) : null}
<Text fontSize="$xl" fontWeight="900" color={text}>
<Text fontSize="$xl" fontWeight="900" color={textStrong}>
{title}
</Text>
{subtitle ? (

View File

@@ -2,20 +2,25 @@ import React from 'react';
import { YStack, XStack } from '@tamagui/stacks';
import { SizableText as Text } from '@tamagui/text';
import { Pressable } from '@tamagui/react-native-web-lite';
import { useTheme } from '@tamagui/core';
import { useAdminTheme } from '../theme';
export function MobileCard({ children, ...rest }: React.ComponentProps<typeof YStack>) {
const theme = useTheme();
export function MobileCard({
children,
className,
...rest
}: React.ComponentProps<typeof YStack>) {
const { surface, border, shadow } = useAdminTheme();
return (
<YStack
backgroundColor={String(theme.surface?.val ?? 'white')}
borderRadius={16}
className={['admin-fade-up', className].filter(Boolean).join(' ')}
backgroundColor={surface}
borderRadius={18}
borderWidth={1}
borderColor={String(theme.borderColor?.val ?? '#e5e7eb')}
shadowColor="#0f172a"
shadowOpacity={0.06}
shadowRadius={12}
shadowOffset={{ width: 0, height: 8 }}
borderColor={border}
shadowColor={shadow}
shadowOpacity={0.08}
shadowRadius={14}
shadowOffset={{ width: 0, height: 10 }}
padding="$3.5"
space="$2"
{...rest}
@@ -32,7 +37,7 @@ export function PillBadge({
tone?: 'success' | 'warning' | 'muted';
children: React.ReactNode;
}) {
const theme = useTheme();
const { theme } = useAdminTheme();
const palette: Record<typeof tone, { bg: string; text: string; border: string }> = {
success: {
bg: String(theme.backgroundStrong?.val ?? '#ecfdf3'),
@@ -83,7 +88,7 @@ export function CTAButton({
disabled?: boolean;
loading?: boolean;
}) {
const theme = useTheme();
const { primary, surface, border, text } = useAdminTheme();
const isPrimary = tone === 'primary';
const isDisabled = disabled || loading;
return (
@@ -97,15 +102,15 @@ export function CTAButton({
}}
>
<XStack
height={56}
borderRadius={14}
height={52}
borderRadius={16}
alignItems="center"
justifyContent="center"
backgroundColor={isPrimary ? String(theme.primary?.val ?? '#007AFF') : String(theme.surface?.val ?? 'white')}
backgroundColor={isPrimary ? primary : surface}
borderWidth={isPrimary ? 0 : 1}
borderColor={isPrimary ? 'transparent' : String(theme.borderColor?.val ?? '#e5e7eb')}
borderColor={isPrimary ? 'transparent' : border}
>
<Text fontSize="$sm" fontWeight="800" color={isPrimary ? 'white' : String(theme.color?.val ?? '#111827')}>
<Text fontSize="$sm" fontWeight="800" color={isPrimary ? 'white' : text}>
{label}
</Text>
</XStack>
@@ -122,7 +127,7 @@ export function KpiTile({
label: string;
value: string | number;
}) {
const theme = useTheme();
const { accentSoft, primary, text } = useAdminTheme();
return (
<MobileCard borderRadius={14} padding="$3" width="32%" minWidth={110} alignItems="flex-start">
<XStack alignItems="center" space="$2">
@@ -130,17 +135,17 @@ export function KpiTile({
width={32}
height={32}
borderRadius={12}
backgroundColor={String(theme.blue3?.val ?? '#e5f0ff')}
backgroundColor={accentSoft}
alignItems="center"
justifyContent="center"
>
<IconCmp size={16} color={String(theme.primary?.val ?? '#2563eb')} />
<IconCmp size={16} color={primary} />
</XStack>
<Text fontSize="$xs" color={String(theme.color?.val ?? '#111827')}>
<Text fontSize="$xs" color={text}>
{label}
</Text>
</XStack>
<Text fontSize="$xl" fontWeight="800" color={String(theme.color?.val ?? '#111827')}>
<Text fontSize="$xl" fontWeight="800" color={text}>
{value}
</Text>
</MobileCard>
@@ -159,15 +164,16 @@ export function ActionTile({
color,
onPress,
disabled = false,
delayMs = 0,
}: {
icon: React.ComponentType<{ size?: number; color?: string }>;
label: string;
color: string;
onPress?: () => void;
disabled?: boolean;
delayMs?: number;
}) {
const theme = useTheme();
const text = String(theme.color12?.val ?? theme.color?.val ?? '#f8fafc');
const { textStrong } = useAdminTheme();
return (
<Pressable
onPress={disabled ? undefined : onPress}
@@ -175,6 +181,8 @@ export function ActionTile({
disabled={disabled}
>
<YStack
className="admin-fade-up"
style={delayMs ? { animationDelay: `${delayMs}ms` } : undefined}
borderRadius={16}
padding="$3"
space="$2.5"
@@ -188,7 +196,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={text} textAlign="center">
<Text fontSize="$sm" fontWeight="700" color={textStrong} textAlign="center">
{label}
</Text>
</YStack>
@@ -205,7 +213,7 @@ export function FloatingActionButton({
label: string;
icon: React.ComponentType<{ size?: number; color?: string }>;
}) {
const theme = useTheme();
const { primary, shadow } = useAdminTheme();
const [pressed, setPressed] = React.useState(false);
return (
<Pressable
@@ -231,8 +239,8 @@ export function FloatingActionButton({
alignItems="center"
justifyContent="center"
space="$2"
backgroundColor={String(theme.primary?.val ?? '#007AFF')}
shadowColor="#0f172a"
backgroundColor={primary}
shadowColor={shadow}
shadowOpacity={0.2}
shadowRadius={16}
shadowOffset={{ width: 0, height: 8 }}

View File

@@ -4,8 +4,8 @@ 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';
import { withAlpha } from './colors';
import { useAdminTheme } from '../theme';
type MobileScaffoldProps = {
title: string;
@@ -17,11 +17,7 @@ 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');
const { background, surface, border, text, primary } = useAdminTheme();
const headerSurface = withAlpha(surface, 0.94);
return (
@@ -48,8 +44,8 @@ export function MobileScaffold({ title, onBack, rightSlot, children, footer }: M
{onBack ? (
<Pressable onPress={onBack}>
<XStack alignItems="center" space="$1.5">
<ChevronLeft size={18} color={String(theme.primary?.val ?? '#007AFF')} />
<Text fontSize="$sm" color={String(theme.primary?.val ?? '#007AFF')} fontWeight="600">
<ChevronLeft size={18} color={primary} />
<Text fontSize="$sm" color={primary} fontWeight="600">
{t('actions.back', 'Back')}
</Text>
</XStack>
@@ -58,7 +54,7 @@ export function MobileScaffold({ title, onBack, rightSlot, children, footer }: M
<Text />
)}
</XStack>
<Text fontSize="$lg" fontWeight="800" color={textColor}>
<Text fontSize="$lg" fontWeight="800" color={text}>
{title}
</Text>
<XStack minWidth={40} justifyContent="flex-end">

View File

@@ -3,7 +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';
import { useAdminTheme } from '../theme';
type SheetProps = {
open: boolean;
@@ -17,11 +17,7 @@ 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)');
const { surface, textStrong, muted, overlay, shadow } = useAdminTheme();
if (!open) return null;
return (
@@ -35,7 +31,7 @@ export function MobileSheet({ open, title, onClose, children, footer, bottomOffs
padding="$4"
paddingBottom="$7"
space="$3"
shadowColor="#0f172a"
shadowColor={shadow}
shadowOpacity={0.12}
shadowRadius={18}
shadowOffset={{ width: 0, height: -8 }}
@@ -45,7 +41,7 @@ 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={text}>
<Text fontSize="$md" fontWeight="800" color={textStrong}>
{title}
</Text>
<Pressable onPress={onClose}>

View File

@@ -1,12 +1,11 @@
import React from 'react';
import { SizableText as Text } from '@tamagui/text';
import { XStack } from '@tamagui/stacks';
import { useTheme } from '@tamagui/core';
import { useAdminTheme } from '../theme';
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');
const { border, text } = useAdminTheme();
const baseColor = color ?? border;
return (
<XStack
@@ -19,7 +18,7 @@ export function Tag({ label, color }: { label: string; color?: string }) {
borderColor={`${baseColor}55`}
alignSelf="flex-start"
>
<Text fontSize={11} fontWeight="600" color={textColor}>
<Text fontSize={11} fontWeight="600" color={text}>
{label}
</Text>
</XStack>

View File

@@ -4,7 +4,7 @@ import { render } from '@testing-library/react';
vi.mock('@tamagui/core', () => ({
useTheme: () => ({
primary: { val: '#2563eb' },
primary: { val: '#FF5A5F' },
borderColor: { val: '#e5e7eb' },
surface: { val: '#ffffff' },
}),