neues Admin UI Layout eingeführt. Alle Tests auf den neusten Stand gebracht.
This commit is contained in:
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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' },
|
||||
}),
|
||||
}));
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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' },
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user