admin mobile: improve small-screen readability across checklist, tabs, badges, and headers
Some checks are pending
linter / quality (push) Waiting to run
tests / ci (push) Waiting to run
tests / ui (push) Waiting to run

This commit is contained in:
Codex Agent
2026-02-08 22:13:01 +01:00
parent 83cf863548
commit e3bb1642db
11 changed files with 131 additions and 82 deletions

View File

@@ -980,8 +980,8 @@ function PackageCard({
backgroundColor={isActive ? accentSoft : undefined}
gap="$2"
>
<XStack alignItems="center" justifyContent="space-between">
<Text fontSize="$md" fontWeight="800" color={textStrong}>
<XStack alignItems="flex-start" justifyContent="space-between" gap="$2" flexWrap="wrap">
<Text fontSize="$md" fontWeight="800" color={textStrong} flex={1} minWidth={0}>
{pkg.package_name ?? t('mobileBilling.packageFallback', 'Package')}
</Text>
{label ? <PillBadge tone="success">{label}</PillBadge> : null}
@@ -1262,8 +1262,8 @@ function AddonRow({
return (
<MobileCard borderColor={border} padding="$3" gap="$1.5">
<XStack alignItems="center" justifyContent="space-between">
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
<XStack alignItems="flex-start" justifyContent="space-between" gap="$2" flexWrap="wrap">
<Text fontSize="$sm" fontWeight="700" color={textStrong} flex={1} minWidth={0}>
{addon.label ?? addon.addon_key}
</Text>
<PillBadge tone={status.tone}>{status.text}</PillBadge>
@@ -1271,11 +1271,11 @@ function AddonRow({
{!hideEventLink && eventName ? (
eventPath ? (
<Pressable onPress={() => navigate(eventPath)}>
<XStack alignItems="center" justifyContent="space-between">
<Text fontSize="$xs" color={textStrong} fontWeight="600">
<XStack alignItems="flex-start" justifyContent="space-between" gap="$2" flexWrap="wrap">
<Text fontSize="$xs" color={textStrong} fontWeight="600" flex={1} minWidth={0}>
{eventName}
</Text>
<Text fontSize="$xs" color={primary} fontWeight="700">
<Text fontSize="$xs" color={primary} fontWeight="700" flexShrink={0}>
{t('mobileBilling.openEvent', 'Open event')}
</Text>
</XStack>
@@ -1316,8 +1316,8 @@ function EventAddonRow({ addon }: { addon: EventAddonSummary }) {
return (
<MobileCard borderColor={border} padding="$3" gap="$1.5">
<XStack alignItems="center" justifyContent="space-between">
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
<XStack alignItems="flex-start" justifyContent="space-between" gap="$2" flexWrap="wrap">
<Text fontSize="$sm" fontWeight="700" color={textStrong} flex={1} minWidth={0}>
{addon.label ?? addon.key}
</Text>
<PillBadge tone={status.tone}>{status.text}</PillBadge>

View File

@@ -629,9 +629,9 @@ function LifecycleHero({
paddingHorizontal="$3"
onPress={() => navigate(adminPath(nextStep.targetPath))}
title={
<XStack alignItems="center" gap="$2">
<XStack alignItems="flex-start" gap="$2" flex={1} minWidth={0}>
<Circle size={18} color={theme.primary} strokeWidth={2.5} />
<Text fontSize="$sm" fontWeight="700" color={theme.textStrong}>
<Text fontSize="$sm" fontWeight="700" color={theme.textStrong} flexShrink={1}>
{nextStep.label}
</Text>
</XStack>
@@ -644,8 +644,17 @@ function LifecycleHero({
) : undefined
}
iconAfter={
<XStack alignItems="center" gap="$1">
<PillBadge tone="success">{nextStep.ctaLabel}</PillBadge>
<XStack alignItems="center" gap="$1" maxWidth="52%" flexShrink={1}>
<Text
fontSize="$xs"
fontWeight="700"
color={theme.primary}
textAlign="right"
numberOfLines={2}
flexShrink={1}
>
{nextStep.ctaLabel}
</Text>
<ChevronRight size={16} color={theme.muted} />
</XStack>
}

View File

@@ -335,11 +335,11 @@ export default function MobileEventGuestNotificationsPage() {
<YStack gap="$2">
{history.map((item) => (
<MobileCard key={item.id} gap="$2" borderColor={border}>
<XStack alignItems="center" justifyContent="space-between">
<Text fontSize="$sm" fontWeight="800" color={textStrong}>
<XStack alignItems="flex-start" justifyContent="space-between" gap="$2" flexWrap="wrap">
<Text fontSize="$sm" fontWeight="800" color={textStrong} flex={1} minWidth={0}>
{item.title || t('guestMessages.history.untitled', 'Untitled')}
</Text>
<XStack gap="$1.5" alignItems="center">
<XStack gap="$1.5" alignItems="center" flexWrap="wrap" justifyContent="flex-end">
<PillBadge tone={item.status === 'active' ? 'success' : 'muted'}>
{t(`guestMessages.status.${item.status}`, item.status)}
</PillBadge>
@@ -353,8 +353,8 @@ export default function MobileEventGuestNotificationsPage() {
<Text fontSize="$sm" color={text}>
{item.body ?? t('guestMessages.history.noBody', 'No body provided.')}
</Text>
<XStack alignItems="center" justifyContent="space-between">
<XStack gap="$1.5" alignItems="center">
<XStack alignItems="flex-start" justifyContent="space-between" gap="$2" flexWrap="wrap">
<XStack gap="$1.5" alignItems="center" flexWrap="wrap" flex={1} minWidth={0}>
<PillBadge tone="muted">{t(`guestMessages.type.${item.type}`, item.type)}</PillBadge>
{item.target_identifier ? (
<PillBadge tone="muted">
@@ -376,7 +376,7 @@ export default function MobileEventGuestNotificationsPage() {
</PillBadge>
)}
</XStack>
<Text fontSize="$xs" color={mutedText}>
<Text fontSize="$xs" color={mutedText} flexShrink={0}>
{formatGuestMessageDate(item.created_at, locale)}
</Text>
</XStack>

View File

@@ -358,15 +358,15 @@ export default function MobileEventMembersPage() {
const statusInfo = resolveStatus(member.status);
return (
<MobileCard key={member.id} padding="$3" borderColor={border}>
<XStack alignItems="center" justifyContent="space-between">
<YStack gap="$1">
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
<XStack alignItems="flex-start" justifyContent="space-between" gap="$2">
<YStack gap="$1" flex={1} minWidth={0}>
<Text fontSize="$sm" fontWeight="700" color={textStrong} numberOfLines={2}>
{member.name || member.email || t('events.members.fallbackName', 'Guest')}
</Text>
<Text fontSize="$xs" color={muted}>
<Text fontSize="$xs" color={muted} numberOfLines={2}>
{member.email ?? ''}
</Text>
<XStack gap="$1.5" alignItems="center">
<XStack gap="$1.5" alignItems="center" flexWrap="wrap">
<PillBadge tone={statusInfo.tone}>
{statusInfo.label}
</PillBadge>

View File

@@ -200,60 +200,72 @@ export default function MobileEventRecapPage() {
orientation="horizontal"
flexDirection="column"
>
<Tabs.List gap="$2">
<Tabs.List gap="$1" flexWrap="wrap">
<Tabs.Tab
value="overview"
flex={1}
paddingVertical="$2.5"
paddingHorizontal="$3"
flexGrow={1}
flexShrink={1}
flexBasis="calc(33.333% - 4px)"
minWidth={108}
paddingVertical="$2"
paddingHorizontal="$2"
borderRadius="$4"
hoverStyle={{ backgroundColor: '$backgroundHover' }}
pressStyle={{ backgroundColor: '$backgroundPress' }}
activeStyle={{ backgroundColor: '$backgroundPress' }}
>
<Text
fontSize="$sm"
fontSize="$xs"
fontWeight={activeTab === 'overview' ? '700' : '600'}
color={activeTab === 'overview' ? text : muted}
textAlign="center"
numberOfLines={2}
>
{t('events.recap.tabs.overview', 'Overview')}
</Text>
</Tabs.Tab>
<Tabs.Tab
value="engagement"
flex={1}
paddingVertical="$2.5"
paddingHorizontal="$3"
flexGrow={1}
flexShrink={1}
flexBasis="calc(33.333% - 4px)"
minWidth={108}
paddingVertical="$2"
paddingHorizontal="$2"
borderRadius="$4"
hoverStyle={{ backgroundColor: '$backgroundHover' }}
pressStyle={{ backgroundColor: '$backgroundPress' }}
activeStyle={{ backgroundColor: '$backgroundPress' }}
>
<Text
fontSize="$sm"
fontSize="$xs"
fontWeight={activeTab === 'engagement' ? '700' : '600'}
color={activeTab === 'engagement' ? text : muted}
textAlign="center"
numberOfLines={2}
>
{t('events.recap.tabs.engagement', 'Engagement')}
</Text>
</Tabs.Tab>
<Tabs.Tab
value="compliance"
flex={1}
paddingVertical="$2.5"
paddingHorizontal="$3"
flexGrow={1}
flexShrink={1}
flexBasis="calc(33.333% - 4px)"
minWidth={108}
paddingVertical="$2"
paddingHorizontal="$2"
borderRadius="$4"
hoverStyle={{ backgroundColor: '$backgroundHover' }}
pressStyle={{ backgroundColor: '$backgroundPress' }}
activeStyle={{ backgroundColor: '$backgroundPress' }}
>
<Text
fontSize="$sm"
fontSize="$xs"
fontWeight={activeTab === 'compliance' ? '700' : '600'}
color={activeTab === 'compliance' ? text : muted}
textAlign="center"
numberOfLines={2}
>
{t('events.recap.tabs.compliance', 'Compliance')}
</Text>
@@ -263,8 +275,8 @@ export default function MobileEventRecapPage() {
<Tabs.Content value="overview" paddingTop="$2">
<YStack gap="$4">
<MobileCard gap="$3">
<XStack alignItems="center" justifyContent="space-between">
<YStack gap="$1">
<XStack alignItems="flex-start" justifyContent="space-between" gap="$2" flexWrap="wrap">
<YStack gap="$1" flex={1} minWidth={0}>
<Text fontSize="$xl" fontWeight="800" color={textStrong}>
{t('events.recap.completedTitle', 'Event abgeschlossen')}
</Text>

View File

@@ -1176,31 +1176,45 @@ export default function MobileEventTasksPage() {
borderRadius={16}
borderWidth={1}
borderColor={border}
overflow="hidden"
gap="$0"
overflow="visible"
gap="$1"
flexWrap="wrap"
padding="$1"
>
{[
{ value: 'assigned', label: t('events.tasks.tabs.tasks', 'Tasks') },
{ value: 'library', label: t('events.tasks.tabs.library', 'Task Library') },
{ value: 'emotions', label: t('events.tasks.tabs.emotions', 'Emotions') },
{ value: 'collections', label: t('events.tasks.tabs.collections', 'Collections') },
].map((tab, index, arr) => {
].map((tab) => {
const isActive = activeTab === tab.value;
return (
<Tabs.Tab
key={tab.value}
value={tab.value}
flex={1}
paddingVertical="$2.5"
flexGrow={1}
flexShrink={1}
flexBasis="calc(50% - 4px)"
minWidth={132}
paddingVertical="$2"
paddingHorizontal="$2"
alignItems="center"
justifyContent="center"
borderRightWidth={index === arr.length - 1 ? 0 : 1}
borderRightColor={border}
borderWidth={1}
borderColor={isActive ? withAlpha(primary, 0.45) : border}
borderRadius={12}
backgroundColor={isActive ? withAlpha(primary, 0.12) : surface}
hoverStyle={{ backgroundColor: '$backgroundHover' }}
pressStyle={{ backgroundColor: '$backgroundPress' }}
activeStyle={{ backgroundColor: '$backgroundPress' }}
>
<Text fontSize="$sm" fontWeight={isActive ? '700' : '600'} color={isActive ? text : muted}>
<Text
fontSize="$xs"
fontWeight={isActive ? '700' : '600'}
color={isActive ? text : muted}
numberOfLines={2}
textAlign="center"
>
{tab.label}
</Text>
</Tabs.Tab>
@@ -1232,6 +1246,7 @@ export default function MobileEventTasksPage() {
<XStack
alignItems="center"
gap="$1.5"
flexShrink={1}
paddingVertical="$2"
paddingHorizontal="$3"
borderRadius={14}
@@ -1239,14 +1254,16 @@ export default function MobileEventTasksPage() {
borderColor={border}
backgroundColor={surface}
>
<Text fontSize={11} fontWeight="700" color={text}>
{t('events.tasks.emotionFilterShort', 'Emotion')}
</Text>
<Text fontSize={11} color={muted}>
{emotionFilter
? emotions.find((e) => String(e.id) === emotionFilter)?.name ?? t('events.tasks.customEmotion', 'Custom emotion')
: t('events.tasks.allEmotions', 'All')}
</Text>
<YStack flexShrink={1} minWidth={0} maxWidth={128}>
<Text fontSize={11} fontWeight="700" color={text} numberOfLines={1}>
{t('events.tasks.emotionFilterShort', 'Emotion')}
</Text>
<Text fontSize={11} color={muted} numberOfLines={1}>
{emotionFilter
? emotions.find((e) => String(e.id) === emotionFilter)?.name ?? t('events.tasks.customEmotion', 'Custom emotion')
: t('events.tasks.allEmotions', 'All')}
</Text>
</YStack>
<ChevronDown size={14} color={muted} />
</XStack>
</Pressable>

View File

@@ -449,11 +449,11 @@ function EventListItem({
const stats = buildEventListStats(event);
return (
<YStack gap="$1.5">
<XStack alignItems="center" justifyContent="space-between" gap="$2">
<Text fontSize="$md" fontWeight="800" color={text}>
<XStack alignItems="flex-start" justifyContent="space-between" gap="$2" flexWrap="wrap">
<Text fontSize="$md" fontWeight="800" color={text} flex={1} minWidth={0}>
{renderName(event.name, t)}
</Text>
<XStack alignItems="center" gap="$1.5">
<XStack alignItems="center" gap="$1.5" flexWrap="wrap" justifyContent="flex-end">
<PillBadge tone={statusTone}>{statusLabel}</PillBadge>
{onEdit ? (
<Pressable onPress={() => onEdit(event.slug)}>

View File

@@ -239,10 +239,10 @@ function PackageShopCard({
backgroundColor={isActive ? '$green1' : undefined}
style={{ opacity: isSubdued ? 0.6 : 1 }}
>
<XStack justifyContent="space-between" alignItems="flex-start">
<YStack gap="$1">
<XStack gap="$2" alignItems="center">
<Text fontSize="$lg" fontWeight="800" color={textStrong}>
<XStack justifyContent="space-between" alignItems="flex-start" gap="$2" flexWrap="wrap">
<YStack gap="$1" flex={1} minWidth={0}>
<XStack gap="$2" alignItems="center" flexWrap="wrap">
<Text fontSize="$lg" fontWeight="800" color={textStrong} flexShrink={1}>
{pkg.name}
</Text>
{isRecommended && <PillBadge tone="warning">{t('shop.badges.recommended', 'Recommended')}</PillBadge>}
@@ -255,12 +255,12 @@ function PackageShopCard({
{!isResellerCatalog && isActive ? <PillBadge tone="success">{t('shop.badges.active', 'Active')}</PillBadge> : null}
</XStack>
<XStack gap="$2" alignItems="center">
<XStack gap="$2" alignItems="center" flexWrap="wrap">
<Text fontSize="$md" color={primary} fontWeight="700">
{new Intl.NumberFormat(undefined, { style: 'currency', currency: 'EUR' }).format(pkg.price)}
</Text>
{statusLabel && (
<Text fontSize="$xs" color={muted} fontWeight="600">
<Text fontSize="$xs" color={muted} fontWeight="600" flexShrink={1}>
{statusLabel}
</Text>
)}

View File

@@ -583,41 +583,49 @@ export default function MobileProfileAccountPage() {
orientation="horizontal"
flexDirection="column"
>
<Tabs.List gap="$2">
<Tabs.List gap="$1" flexWrap="wrap">
<Tabs.Tab
value="account"
flex={1}
paddingVertical="$2.5"
paddingHorizontal="$3"
flexGrow={1}
flexShrink={1}
flexBasis="calc(50% - 4px)"
minWidth={132}
paddingVertical="$2"
paddingHorizontal="$2"
borderRadius="$4"
hoverStyle={{ backgroundColor: '$backgroundHover' }}
pressStyle={{ backgroundColor: '$backgroundPress' }}
activeStyle={{ backgroundColor: '$backgroundPress' }}
>
<Text
fontSize="$sm"
fontSize="$xs"
fontWeight={activeTab === 'account' ? '700' : '600'}
color={activeTab === 'account' ? text : muted}
textAlign="center"
numberOfLines={2}
>
{t('profile.tabs.account', 'Account')}
</Text>
</Tabs.Tab>
<Tabs.Tab
value="branding"
flex={1}
paddingVertical="$2.5"
paddingHorizontal="$3"
flexGrow={1}
flexShrink={1}
flexBasis="calc(50% - 4px)"
minWidth={132}
paddingVertical="$2"
paddingHorizontal="$2"
borderRadius="$4"
hoverStyle={{ backgroundColor: '$backgroundHover' }}
pressStyle={{ backgroundColor: '$backgroundPress' }}
activeStyle={{ backgroundColor: '$backgroundPress' }}
>
<Text
fontSize="$sm"
fontSize="$xs"
fontWeight={activeTab === 'branding' ? '700' : '600'}
color={activeTab === 'branding' ? text : muted}
textAlign="center"
numberOfLines={2}
>
{t('profile.tabs.branding', 'Standard-Branding')}
</Text>

View File

@@ -338,7 +338,7 @@ export default function MobileSettingsPage() {
</Text>
) : (
<YStack gap="$2">
<XStack alignItems="center" justifyContent="space-between" gap="$2">
<XStack alignItems="flex-start" justifyContent="space-between" gap="$2" flexWrap="wrap">
<YStack flex={1} gap="$1">
<Text fontSize="$sm" fontWeight="700" color={text}>
{t('mobileSettings.deviceStatus.notifications.label', 'Notifications')}
@@ -351,7 +351,7 @@ export default function MobileSettingsPage() {
{permissionLabel(devicePermissions.notifications)}
</PillBadge>
</XStack>
<XStack alignItems="center" justifyContent="space-between" gap="$2">
<XStack alignItems="flex-start" justifyContent="space-between" gap="$2" flexWrap="wrap">
<YStack flex={1} gap="$1">
<Text fontSize="$sm" fontWeight="700" color={text}>
{t('mobileSettings.deviceStatus.camera.label', 'Camera')}
@@ -364,7 +364,7 @@ export default function MobileSettingsPage() {
{permissionLabel(devicePermissions.camera)}
</PillBadge>
</XStack>
<XStack alignItems="center" justifyContent="space-between" gap="$2">
<XStack alignItems="flex-start" justifyContent="space-between" gap="$2" flexWrap="wrap">
<YStack flex={1} gap="$1">
<Text fontSize="$sm" fontWeight="700" color={text}>
{t('mobileSettings.deviceStatus.storage.label', 'Offline storage')}
@@ -377,7 +377,7 @@ export default function MobileSettingsPage() {
{storageLabel(devicePermissions.storage)}
</PillBadge>
</XStack>
<XStack alignItems="center" justifyContent="space-between" gap="$2">
<XStack alignItems="flex-start" justifyContent="space-between" gap="$2" flexWrap="wrap">
<YStack flex={1} gap="$1">
<Text fontSize="$sm" fontWeight="700" color={text}>
{t('mobileSettings.deviceStatus.connection.label', 'Connection')}

View File

@@ -39,19 +39,21 @@ export function SetupChecklist({
<YStack>
<Pressable onPress={() => setCollapsed(!collapsed)}>
<YStack padding="$3" paddingVertical="$2.5" gap="$2">
<XStack alignItems="center" justifyContent="space-between">
<XStack alignItems="center" gap="$2">
<XStack alignItems="flex-start" justifyContent="space-between" gap="$2" flexWrap="wrap">
<XStack alignItems="center" gap="$2" flex={1} minWidth={0} flexWrap="wrap">
<Text fontSize="$xs" fontWeight="700" color={theme.muted} textTransform="uppercase" letterSpacing={1}>
{title}
</Text>
{isAllComplete ? (
<CheckCircle2 size={14} color={theme.successText} />
) : (
<PillBadge tone="warning">{t('readiness.pending', 'Noch offen')}</PillBadge>
<XStack flexShrink={0}>
<PillBadge tone="warning">{t('readiness.pending', 'Noch offen')}</PillBadge>
</XStack>
)}
</XStack>
<XStack alignItems="center" gap="$2">
<XStack alignItems="center" justifyContent="flex-end" gap="$2" flexShrink={0}>
<Text fontSize="$xs" color={theme.muted} fontWeight="600">
{completedCount}/{steps.length}
</Text>
@@ -89,7 +91,7 @@ export function SetupChecklist({
backgroundColor={isNext ? theme.surfaceMuted : 'transparent'}
onPress={() => navigate(adminPath(step.targetPath))}
title={
<XStack alignItems="center" gap="$2.5">
<XStack alignItems="flex-start" gap="$2.5" flex={1} minWidth={0}>
{step.isComplete ? (
<CheckCircle2 size={18} color={theme.successText} />
) : isNext ? (
@@ -102,6 +104,7 @@ export function SetupChecklist({
fontWeight={isNext ? '700' : '500'}
color={step.isComplete ? theme.muted : theme.textStrong}
textDecorationLine={step.isComplete ? 'line-through' : 'none'}
flexShrink={1}
>
{step.label}
</Text>