upgrade to tamagui v2 and guest pwa overhaul

This commit is contained in:
Codex Agent
2026-02-02 13:01:20 +01:00
parent 2e78f3ab8d
commit 7c6e14ffe2
168 changed files with 47462 additions and 8914 deletions

View File

@@ -93,7 +93,7 @@ export function DevTenantSwitcher({ bottomOffset = 16, variant = 'floating' }: D
borderColor="rgba(234,179,8,0.5)"
backgroundColor="rgba(255,255,255,0.95)"
padding="$3"
space="$2"
gap="$2"
borderRadius="$4"
shadowColor="#f59e0b"
shadowOpacity={0.25}
@@ -102,7 +102,7 @@ export function DevTenantSwitcher({ bottomOffset = 16, variant = 'floating' }: D
maxWidth={320}
>
<XStack alignItems="center" justifyContent="space-between">
<XStack alignItems="center" space="$2">
<XStack alignItems="center" gap="$2">
<Text fontSize={13} fontWeight="800" color="#92400e">
Demo tenants
</Text>
@@ -119,7 +119,7 @@ export function DevTenantSwitcher({ bottomOffset = 16, variant = 'floating' }: D
aria-label="Switcher minimieren"
/>
</XStack>
<YStack space="$1">
<YStack gap="$1">
{DEV_TENANT_KEYS.map(({ key, label }) => (
<Button
key={key}
@@ -162,7 +162,7 @@ export function DevTenantSwitcher({ bottomOffset = 16, variant = 'floating' }: D
right="$4"
zIndex={1000}
maxWidth={320}
space="$2"
gap="$2"
borderWidth={1}
borderColor="rgba(234,179,8,0.5)"
backgroundColor="rgba(255,255,255,0.95)"
@@ -176,7 +176,7 @@ export function DevTenantSwitcher({ bottomOffset = 16, variant = 'floating' }: D
style={{ bottom: bottomOffset + 70 }}
>
<XStack alignItems="center" justifyContent="space-between">
<XStack alignItems="center" space="$2">
<XStack alignItems="center" gap="$2">
<Text fontSize={13} fontWeight="800" color="#92400e">
Demo tenants
</Text>
@@ -196,7 +196,7 @@ export function DevTenantSwitcher({ bottomOffset = 16, variant = 'floating' }: D
<Text fontSize={11} color="#a16207">
Select a seeded tenant to mint Sanctum PATs and jump straight into their admin space. Available only in development builds.
</Text>
<YStack space="$1">
<YStack gap="$1">
{DEV_TENANT_KEYS.map(({ key, label }) => (
<Button
key={key}

View File

@@ -67,7 +67,7 @@ export default function AuthCallbackPage(): React.ReactElement {
shadowRadius={16}
shadowOffset={{ width: 0, height: 10 }}
>
<YStack alignItems="center" space="$2">
<YStack alignItems="center" gap="$2">
<Spinner size="small" color={textStrong} />
<Text fontSize="$md" fontWeight="800" color={textStrong}>
{t('processing.title', 'Signing you in …')}

View File

@@ -274,9 +274,9 @@ export default function MobileBillingPage() {
<ContextHelpLink slug="billing-packages-exports" />
</XStack>
{pendingCheckout && (checkoutStatus === 'failed' || checkoutStatus === 'cancelled') ? (
<MobileCard borderColor={danger} backgroundColor="$red1" space="$2">
<MobileCard borderColor={danger} backgroundColor="$red1" gap="$2">
<XStack alignItems="center" justifyContent="space-between">
<YStack space="$0.5" flex={1}>
<YStack gap="$0.5" flex={1}>
<Text fontSize="$sm" fontWeight="800" color={danger}>
{t('billing.checkoutFailedTitle', 'Checkout failed')}
</Text>
@@ -296,7 +296,7 @@ export default function MobileBillingPage() {
{t('billing.checkoutFailedBadge', 'Failed')}
</PillBadge>
</XStack>
<XStack space="$2">
<XStack gap="$2">
<CTAButton
label={t('billing.checkoutFailedRetry', 'Try again')}
onPress={() => navigate(shopLink)}
@@ -312,9 +312,9 @@ export default function MobileBillingPage() {
</MobileCard>
) : null}
{pendingCheckout && checkoutStatus === 'requires_customer_action' ? (
<MobileCard borderColor={accentSoft} backgroundColor={accentSoft} space="$2">
<MobileCard borderColor={accentSoft} backgroundColor={accentSoft} gap="$2">
<XStack alignItems="center" justifyContent="space-between">
<YStack space="$0.5" flex={1}>
<YStack gap="$0.5" flex={1}>
<Text fontSize="$sm" fontWeight="800" color={textStrong}>
{t('billing.checkoutActionTitle', 'Action required')}
</Text>
@@ -326,7 +326,7 @@ export default function MobileBillingPage() {
{t('billing.checkoutActionBadge', 'Action needed')}
</PillBadge>
</XStack>
<XStack space="$2">
<XStack gap="$2">
<CTAButton
label={t('billing.checkoutActionButton', 'Continue checkout')}
onPress={() => {
@@ -348,9 +348,9 @@ export default function MobileBillingPage() {
</MobileCard>
) : null}
{pendingCheckout && checkoutStatus !== 'failed' && checkoutStatus !== 'cancelled' && checkoutStatus !== 'requires_customer_action' ? (
<MobileCard borderColor={accentSoft} backgroundColor={accentSoft} space="$2">
<MobileCard borderColor={accentSoft} backgroundColor={accentSoft} gap="$2">
<XStack alignItems="center" justifyContent="space-between">
<YStack space="$0.5" flex={1}>
<YStack gap="$0.5" flex={1}>
<Text fontSize="$sm" fontWeight="800" color={textStrong}>
{t('billing.checkoutPendingTitle', 'Activating your package')}
</Text>
@@ -365,7 +365,7 @@ export default function MobileBillingPage() {
{t('billing.checkoutPendingBadge', 'Pending')}
</PillBadge>
</XStack>
<XStack space="$2">
<XStack gap="$2">
<CTAButton label={t('billing.checkoutPendingRefresh', 'Refresh')} onPress={load} fullWidth={false} />
<CTAButton
label={t('billing.checkoutPendingDismiss', 'Dismiss')}
@@ -377,8 +377,8 @@ export default function MobileBillingPage() {
</MobileCard>
) : null}
<MobileCard space="$2" ref={packagesRef as any}>
<XStack alignItems="center" space="$2">
<MobileCard gap="$2" ref={packagesRef as any}>
<XStack alignItems="center" gap="$2">
<Package size={18} color={textStrong} />
<Text fontSize="$md" fontWeight="800" color={textStrong}>
{t('billing.sections.packages.title', 'Packages')}
@@ -397,7 +397,7 @@ export default function MobileBillingPage() {
{t('common.loading', 'Lädt...')}
</Text>
) : (
<YStack space="$2">
<YStack gap="$2">
{activePackage ? (
<PackageCard
pkg={activePackage}
@@ -415,8 +415,8 @@ export default function MobileBillingPage() {
)}
</MobileCard>
<MobileCard space="$2" ref={invoicesRef as any}>
<XStack alignItems="center" space="$2">
<MobileCard gap="$2" ref={invoicesRef as any}>
<XStack alignItems="center" gap="$2">
<Receipt size={18} color={textStrong} />
<Text fontSize="$md" fontWeight="800" color={textStrong}>
{t('billing.sections.invoices.title', 'Invoices & Payments')}
@@ -430,7 +430,7 @@ export default function MobileBillingPage() {
{t('common.loading', 'Lädt...')}
</Text>
) : transactions.length === 0 ? (
<YStack space="$2">
<YStack gap="$2">
<Text fontSize="$sm" color={text}>
{t('billing.sections.invoices.empty', 'Keine Zahlungen gefunden.')}
</Text>
@@ -438,7 +438,7 @@ export default function MobileBillingPage() {
<CTAButton label={t('billing.actions.contactSupport', 'Contact support')} tone="ghost" onPress={openSupport} />
</YStack>
) : (
<YStack space="$1.5">
<YStack gap="$1.5">
{transactions.slice(0, 8).map((trx) => (
<XStack key={trx.id} alignItems="center" justifyContent="space-between" borderBottomWidth={1} borderColor={border} paddingVertical="$1.5">
<YStack>
@@ -475,8 +475,8 @@ export default function MobileBillingPage() {
)}
</MobileCard>
<MobileCard space="$2">
<XStack alignItems="center" space="$2">
<MobileCard gap="$2">
<XStack alignItems="center" gap="$2">
<Sparkles size={18} color={textStrong} />
<Text fontSize="$md" fontWeight="800" color={textStrong}>
{t('billing.sections.addOns.title', 'Add-ons')}
@@ -494,7 +494,7 @@ export default function MobileBillingPage() {
{t('billing.sections.addOns.empty', 'Keine Add-ons gebucht.')}
</Text>
) : (
<YStack space="$1.5">
<YStack gap="$1.5">
{addons.slice(0, 8).map((addon) => (
<AddonRow key={addon.id} addon={addon} />
))}
@@ -550,7 +550,7 @@ function PackageCard({
borderColor={isActive ? primary : border}
borderWidth={isActive ? 2 : 1}
backgroundColor={isActive ? accentSoft : undefined}
space="$2"
gap="$2"
>
<XStack alignItems="center" justifyContent="space-between">
<Text fontSize="$md" fontWeight="800" color={textStrong}>
@@ -563,7 +563,7 @@ function PackageCard({
{expires}
</Text>
) : null}
<XStack space="$2" marginTop="$2" flexWrap="wrap">
<XStack gap="$2" marginTop="$2" flexWrap="wrap">
<PillBadge tone="muted">{remainingText}</PillBadge>
{pkg.price !== null && pkg.price !== undefined ? (
<PillBadge tone="muted">{formatAmount(pkg.price, pkg.currency ?? 'EUR')}</PillBadge>
@@ -578,7 +578,7 @@ function PackageCard({
</Text>
) : null}
{limitEntries.length ? (
<YStack space="$1.5" marginTop="$2">
<YStack gap="$1.5" marginTop="$2">
<Text fontSize="$xs" color={muted}>
{t('mobileBilling.details.limitsTitle', 'Limits')}
</Text>
@@ -595,12 +595,12 @@ function PackageCard({
</YStack>
) : null}
{featureKeys.length ? (
<YStack space="$1.5" marginTop="$2">
<YStack gap="$1.5" marginTop="$2">
<Text fontSize="$xs" color={muted}>
{t('mobileBilling.details.featuresTitle', 'Features')}
</Text>
{featureKeys.map((feature) => (
<XStack key={feature} alignItems="center" space="$2">
<XStack key={feature} alignItems="center" gap="$2">
<Sparkles size={14} color={primary} />
<Text fontSize="$xs" color={textStrong}>
{getPackageFeatureLabel(feature, t)}
@@ -610,7 +610,7 @@ function PackageCard({
</YStack>
) : null}
{usageMetrics.length ? (
<YStack space="$2" marginTop="$2">
<YStack gap="$2" marginTop="$2">
{usageMetrics.map((metric) => (
<UsageBar key={metric.key} metric={metric} />
))}
@@ -684,12 +684,12 @@ function UsageBar({ metric }: { metric: PackageUsageMetric }) {
const fillColor = status === 'danger' ? danger : status === 'warning' ? warningText : primary;
return (
<YStack space="$1.5">
<YStack gap="$1.5">
<XStack alignItems="center" justifyContent="space-between">
<Text fontSize="$xs" color={muted}>
{labelMap[metric.key]}
</Text>
<XStack alignItems="center" space="$1.5">
<XStack alignItems="center" gap="$1.5">
{statusLabel ? <PillBadge tone={status === 'danger' ? 'danger' : 'warning'}>{statusLabel}</PillBadge> : null}
<Text fontSize="$xs" color={textStrong} fontWeight="700">
{valueText}
@@ -737,7 +737,7 @@ function AddonRow({ addon }: { addon: TenantAddonHistoryEntry }) {
const eventPath = addon.event?.slug ? ADMIN_EVENT_VIEW_PATH(addon.event.slug) : null;
const hasImpact = Boolean(addon.extra_photos || addon.extra_guests || addon.extra_gallery_days);
const impactBadges = hasImpact ? (
<XStack space="$2" marginTop="$1.5" flexWrap="wrap">
<XStack gap="$2" marginTop="$1.5" flexWrap="wrap">
{addon.extra_photos ? (
<PillBadge tone="muted">{t('mobileBilling.extra.photos', '+{{count}} photos', { count: addon.extra_photos })}</PillBadge>
) : null}
@@ -751,7 +751,7 @@ function AddonRow({ addon }: { addon: TenantAddonHistoryEntry }) {
) : null;
return (
<MobileCard borderColor={border} padding="$3" space="$1.5">
<MobileCard borderColor={border} padding="$3" gap="$1.5">
<XStack alignItems="center" justifyContent="space-between">
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{addon.label ?? addon.addon_key}

View File

@@ -389,7 +389,7 @@ export default function MobileBrandingPage() {
return (
<>
<MobileCard space="$3">
<MobileCard gap="$3">
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{t('events.watermark.previewTitle', 'Watermark Preview')}
</Text>
@@ -421,7 +421,7 @@ export default function MobileBrandingPage() {
/>
) : null}
<MobileCard space="$3">
<MobileCard gap="$3">
<Text fontSize="$md" fontWeight="800" color={textStrong}>
{t('events.watermark.title', 'Wasserzeichen')}
</Text>
@@ -449,14 +449,14 @@ export default function MobileBrandingPage() {
</MobileField>
{resolvedMode === 'custom' && !controlsLocked ? (
<YStack space="$2">
<YStack gap="$2">
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{t('events.watermark.upload', 'Wasserzeichen hochladen')}
</Text>
<Pressable onPress={() => document.getElementById('watermark-upload-input')?.click()}>
<XStack
alignItems="center"
space="$2"
gap="$2"
paddingHorizontal="$3.5"
paddingVertical="$2.5"
borderRadius={12}
@@ -520,7 +520,7 @@ export default function MobileBrandingPage() {
) : null}
</MobileCard>
<MobileCard space="$3">
<MobileCard gap="$3">
<Text fontSize="$md" fontWeight="800" color={textStrong}>
{t('events.watermark.placement', 'Position & Größe')}
</Text>
@@ -603,8 +603,8 @@ export default function MobileBrandingPage() {
<ContextHelpLink slug="event-branding-assets" />
</XStack>
<MobileCard space="$2">
<XStack space="$2">
<MobileCard gap="$2">
<XStack gap="$2">
<TabButton label={t('events.branding.titleShort', 'Branding')} active={activeTab === 'branding'} onPress={() => setActiveTab('branding')} />
<TabButton label={t('events.watermark.tab', 'Wasserzeichen')} active={activeTab === 'watermark'} onPress={() => setActiveTab('watermark')} />
</XStack>
@@ -612,20 +612,20 @@ export default function MobileBrandingPage() {
{activeTab === 'branding' ? (
<>
<MobileCard space="$3">
<MobileCard gap="$3">
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{t('events.branding.previewTitle', 'Guest App Preview')}
</Text>
<YStack borderRadius={16} borderWidth={1} borderColor={previewBorder} backgroundColor={previewBackground} padding="$3" space="$2" alignItems="center">
<YStack borderRadius={16} borderWidth={1} borderColor={previewBorder} backgroundColor={previewBackground} padding="$3" gap="$2" alignItems="center">
<YStack width="100%" borderRadius={12} backgroundColor={previewSurface} borderWidth={1} borderColor={previewBorder} overflow="hidden">
<YStack
height={64}
style={{ background: `linear-gradient(135deg, ${previewForm.primary}, ${previewForm.accent})` }}
/>
<YStack padding="$3" space="$2">
<YStack padding="$3" gap="$2">
<XStack
alignItems="center"
space="$2"
gap="$2"
flexDirection={previewForm.logoPosition === 'center' ? 'column' : previewForm.logoPosition === 'right' ? 'row-reverse' : 'row'}
justifyContent={previewForm.logoPosition === 'center' ? 'center' : 'flex-start'}
>
@@ -665,7 +665,7 @@ export default function MobileBrandingPage() {
</Text>
</YStack>
</XStack>
<XStack space="$2" marginTop="$1">
<XStack gap="$2" marginTop="$1">
<ColorSwatch color={previewForm.primary} label={t('events.branding.primary', 'Primary')} borderColor={previewBorder} />
<ColorSwatch color={previewForm.accent} label={t('events.branding.accent', 'Accent')} borderColor={previewBorder} />
<ColorSwatch color={previewBackground} label={t('events.branding.background', 'Background')} borderColor={previewBorder} />
@@ -701,11 +701,11 @@ export default function MobileBrandingPage() {
) : null}
<>
<MobileCard space="$3">
<MobileCard gap="$3">
<Text fontSize="$md" fontWeight="800" color={textStrong}>
{t('events.branding.mode', 'Theme')}
</Text>
<XStack space="$2">
<XStack gap="$2">
<ModeButton
label={t('events.branding.modeLight', 'Light')}
active={form.mode === 'light'}
@@ -727,7 +727,7 @@ export default function MobileBrandingPage() {
</XStack>
</MobileCard>
<MobileCard space="$3">
<MobileCard gap="$3">
<Text fontSize="$md" fontWeight="800" color={textStrong}>
{t('events.branding.colors', 'Colors')}
</Text>
@@ -757,7 +757,7 @@ export default function MobileBrandingPage() {
/>
</MobileCard>
<MobileCard space="$3">
<MobileCard gap="$3">
<Text fontSize="$md" fontWeight="800" color={textStrong}>
{t('events.branding.fonts', 'Fonts')}
</Text>
@@ -786,7 +786,7 @@ export default function MobileBrandingPage() {
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{t('events.branding.fontSize', 'Font Size')}
</Text>
<XStack space="$2">
<XStack gap="$2">
<ModeButton
label={t('events.branding.fontSizeSmall', 'S')}
active={form.fontSize === 's'}
@@ -808,14 +808,14 @@ export default function MobileBrandingPage() {
</XStack>
</MobileCard>
<MobileCard space="$3">
<MobileCard gap="$3">
<Text fontSize="$md" fontWeight="800" color={textStrong}>
{t('events.branding.logo', 'Logo')}
</Text>
<Text fontSize="$sm" color={muted}>
{t('events.branding.logoHint', 'Upload a logo or use an emoji for the guest header.')}
</Text>
<XStack space="$2">
<XStack gap="$2">
<ModeButton
label={t('events.branding.logoModeUpload', 'Upload')}
active={form.logoMode === 'upload'}
@@ -847,7 +847,7 @@ export default function MobileBrandingPage() {
padding="$3"
alignItems="center"
justifyContent="center"
space="$2"
gap="$2"
>
{form.logoDataUrl ? (
<>
@@ -856,7 +856,7 @@ export default function MobileBrandingPage() {
alt={t('events.branding.logoAlt', 'Logo')}
style={{ maxHeight: 80, maxWidth: '100%', objectFit: 'contain' }}
/>
<XStack space="$2">
<XStack gap="$2">
<CTAButton
label={t('events.branding.replaceLogo', 'Replace logo')}
onPress={() => document.getElementById('branding-logo-input')?.click()}
@@ -868,7 +868,7 @@ export default function MobileBrandingPage() {
>
<XStack
alignItems="center"
space="$1.5"
gap="$1.5"
paddingHorizontal="$3"
paddingVertical="$2"
borderRadius={12}
@@ -892,7 +892,7 @@ export default function MobileBrandingPage() {
>
<XStack
alignItems="center"
space="$2"
gap="$2"
paddingHorizontal="$3.5"
paddingVertical="$2.5"
borderRadius={12}
@@ -939,7 +939,7 @@ export default function MobileBrandingPage() {
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{t('events.branding.logoPosition', 'Position')}
</Text>
<XStack space="$2">
<XStack gap="$2">
<ModeButton
label={t('events.branding.positionLeft', 'Left')}
active={form.logoPosition === 'left'}
@@ -962,7 +962,7 @@ export default function MobileBrandingPage() {
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{t('events.branding.logoSize', 'Size')}
</Text>
<XStack space="$2">
<XStack gap="$2">
<ModeButton
label={t('events.branding.logoSizeSmall', 'S')}
active={form.logoSize === 's'}
@@ -984,14 +984,14 @@ export default function MobileBrandingPage() {
</XStack>
</MobileCard>
<MobileCard space="$3">
<MobileCard gap="$3">
<Text fontSize="$md" fontWeight="800" color={textStrong}>
{t('events.branding.buttons', 'Buttons & Links')}
</Text>
<Text fontSize="$sm" color={muted}>
{t('events.branding.buttonsHint', 'Style, radius, and link color for CTA buttons.')}
</Text>
<XStack space="$2">
<XStack gap="$2">
<ModeButton
label={t('events.branding.buttonFilled', 'Filled')}
active={form.buttonStyle === 'filled'}
@@ -1039,7 +1039,7 @@ export default function MobileBrandingPage() {
renderWatermarkTab()
)}
<YStack space="$2">
<YStack gap="$2">
<CTAButton label={saving ? t('events.branding.saving', 'Saving...') : t('events.branding.save', 'Save Branding')} onPress={() => handleSave()} />
<Pressable disabled={loading || saving} onPress={handleReset}>
<XStack
@@ -1050,7 +1050,7 @@ export default function MobileBrandingPage() {
backgroundColor={surface}
borderWidth={1}
borderColor={border}
space="$2"
gap="$2"
>
<RefreshCcw size={16} color={textStrong} />
<Text fontSize="$sm" color={textStrong} fontWeight="700">
@@ -1067,7 +1067,7 @@ export default function MobileBrandingPage() {
footer={null}
bottomOffsetPx={120}
>
<YStack space="$2">
<YStack gap="$2">
{fontsLoading ? (
Array.from({ length: 4 }).map((_, idx) => <SkeletonCard key={`font-sk-${idx}`} height={48} />)
) : fonts.length === 0 ? (
@@ -1228,11 +1228,11 @@ function ColorField({
}) {
const { textStrong, muted } = useAdminTheme();
return (
<YStack space="$2" opacity={disabled ? 0.6 : 1}>
<YStack gap="$2" opacity={disabled ? 0.6 : 1}>
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{label}
</Text>
<XStack alignItems="center" space="$2">
<XStack alignItems="center" gap="$2">
<MobileColorInput
value={value}
onChange={(event) => onChange(event.target.value)}
@@ -1249,7 +1249,7 @@ function ColorField({
function ColorSwatch({ color, label, borderColor }: { color: string; label: string; borderColor?: string }) {
const { border, muted } = useAdminTheme();
return (
<YStack alignItems="center" space="$1">
<YStack alignItems="center" gap="$1">
<YStack width={40} height={40} borderRadius={12} borderWidth={1} borderColor={borderColor ?? border} backgroundColor={color} />
<Text fontSize="$xs" color={muted}>
{label}
@@ -1276,7 +1276,7 @@ function InputField({
const { primary } = useAdminTheme();
return (
<MobileField label={label}>
<XStack alignItems="center" space="$2">
<XStack alignItems="center" gap="$2">
<MobileInput
value={value}
placeholder={placeholder}
@@ -1316,7 +1316,7 @@ function LabeledSlider({
}) {
const { textStrong, muted, primary, border, surface } = useAdminTheme();
return (
<YStack space="$1.5">
<YStack gap="$1.5">
<XStack alignItems="center" justifyContent="space-between">
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{label}
@@ -1367,7 +1367,7 @@ function PositionGrid({
];
return (
<YStack space="$2">
<YStack gap="$2">
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
Position
</Text>
@@ -1503,8 +1503,8 @@ function InfoBadge({ icon, text, tone = 'info' }: { icon?: React.ReactNode; text
const color = tone === 'danger' ? dangerText : textStrong;
return (
<MobileCard space="$2" backgroundColor={background} borderColor={border}>
<XStack space="$2" alignItems="center">
<MobileCard gap="$2" backgroundColor={background} borderColor={border}>
<XStack gap="$2" alignItems="center">
{icon}
<Text fontSize="$sm" color={color}>
{text}
@@ -1529,7 +1529,7 @@ function UpgradeCard({
return (
<MobileCard
space="$4"
gap="$4"
padding="$6"
alignItems="center"
justifyContent="center"
@@ -1547,7 +1547,7 @@ function UpgradeCard({
>
<Lock size={32} color={primary} />
</YStack>
<YStack space="$2" alignItems="center">
<YStack gap="$2" alignItems="center">
<Text fontSize="$xl" fontWeight="900" color={textStrong} textAlign="center">
{title}
</Text>

View File

@@ -89,7 +89,7 @@ function SectionHeader({
const subtitleSize = compact ? '$xs' : '$sm';
const spacing = compact ? '$1' : '$1.5';
return (
<YStack space={spacing}>
<YStack gap={spacing}>
<XStack alignItems="center" justifyContent="space-between">
<Text fontSize={titleSize} fontWeight="800" color={theme.textStrong}>
{title}
@@ -222,7 +222,7 @@ export default function MobileDashboardPage() {
return (
<MobileShell activeTab="home" title={t('mobileDashboard.title', 'Dashboard')}>
<DashboardCard padding="$0">
<YStack padding="$3" space="$2">
<YStack padding="$3" gap="$2">
<SectionHeader
title={t('dashboard:overview.title', 'At a glance')}
showSeparator={false}
@@ -231,7 +231,7 @@ export default function MobileDashboardPage() {
/>
</YStack>
<Separator backgroundColor={theme.border} opacity={0.6} />
<YStack padding="$3" space="$2.5">
<YStack padding="$3" gap="$2.5">
{/* 1. LIFECYCLE HERO */}
<LifecycleHero
event={activeEvent}
@@ -326,7 +326,7 @@ function LifecycleHero({
if (phase === 'live') {
return (
<YStack space="$2">
<YStack gap="$2">
<Header />
<DashboardCard
variant={cardVariant}
@@ -335,10 +335,10 @@ function LifecycleHero({
borderColor="transparent"
style={{ backgroundImage: 'linear-gradient(135deg, #4F46E5 0%, #4338CA 100%)' }}
>
<YStack space={isEmbedded ? '$2.5' : '$3'}>
<YStack gap={isEmbedded ? '$2.5' : '$3'}>
<XStack alignItems="center" justifyContent="space-between">
<YStack space="$1">
<XStack alignItems="center" space="$2">
<YStack gap="$1">
<XStack alignItems="center" gap="$2">
<YStack width={8} height={8} borderRadius={4} backgroundColor="#22C55E" />
<Text color="white" fontWeight="700" fontSize="$xs" textTransform="uppercase" letterSpacing={1}>
{t('dashboard:liveNow.status', 'Happening Now')}
@@ -404,11 +404,11 @@ function LifecycleHero({
if (phase === 'post') {
return (
<YStack space="$2">
<YStack gap="$2">
<Header />
<DashboardCard variant={cardVariant} padding={cardPadding}>
<YStack space={isEmbedded ? '$2.5' : '$3'}>
<XStack alignItems="center" space="$2.5">
<YStack gap={isEmbedded ? '$2.5' : '$3'}>
<XStack alignItems="center" gap="$2.5">
<YStack width={40} height={40} borderRadius={20} backgroundColor={theme.successText} alignItems="center" justifyContent="center">
<CheckCircle2 size={20} color="white" />
</YStack>
@@ -427,7 +427,7 @@ function LifecycleHero({
height={48}
borderRadius={16}
>
<XStack alignItems="center" space="$2">
<XStack alignItems="center" gap="$2">
<Download size={16} color="white" />
<Text fontSize="$sm" fontWeight="800" color="white">
{t('events.recap.downloadAll', 'Download photos')}
@@ -443,7 +443,7 @@ function LifecycleHero({
height={48}
borderRadius={16}
>
<XStack alignItems="center" space="$2">
<XStack alignItems="center" gap="$2">
<Text fontSize="$sm" fontWeight="800" color={theme.textStrong}>
{t('events.recap.openRecap', 'Open recap')}
</Text>
@@ -458,7 +458,7 @@ function LifecycleHero({
// SETUP
return (
<YStack space="$2">
<YStack gap="$2">
<Header />
{showQuickControls ? (
<XStack
@@ -472,7 +472,7 @@ function LifecycleHero({
paddingVertical="$2"
>
<Pressable onPress={() => navigate(adminPath(`/mobile/events/${event.slug}/edit`))}>
<XStack alignItems="center" space="$1.5">
<XStack alignItems="center" gap="$1.5">
<Settings size={16} color={theme.primary} />
<Text fontSize="$sm" fontWeight="700" color={theme.textStrong}>
{t('dashboard:readiness.quickSettings', 'Event settings')}
@@ -480,8 +480,8 @@ function LifecycleHero({
</XStack>
</Pressable>
<XStack alignItems="center" space="$3">
<YStack alignItems="center" space="$1">
<XStack alignItems="center" gap="$3">
<YStack alignItems="center" gap="$1">
<Text fontSize="$xs" color={theme.muted} textTransform="uppercase" letterSpacing={0.8}>
{t('dashboard:readiness.publishToggle', 'Live')}
</Text>
@@ -499,13 +499,13 @@ function LifecycleHero({
</XStack>
) : null}
<DashboardCard variant={cardVariant} padding={cardPadding}>
<YStack space={isEmbedded ? '$2.5' : '$3'}>
<YStack gap={isEmbedded ? '$2.5' : '$3'}>
<XStack alignItems="center" justifyContent="space-between">
<YStack>
<Text fontSize="$xs" color={theme.muted} fontWeight="700" textTransform="uppercase">
{t('dashboard:upcoming.status.planning', 'Countdown')}
</Text>
<Text fontSize="$2xl" fontWeight="900" color={theme.primary}>
<Text fontSize="$xxl" fontWeight="900" color={theme.primary}>
{daysToGo}{' '}
<Text fontSize="$sm" color={theme.muted} fontWeight="500">
{t('management:galleryStatus.daysLabel', 'days')}
@@ -518,7 +518,7 @@ function LifecycleHero({
</XStack>
{showNextStep && nextStep ? (
<YStack space="$2">
<YStack gap="$2">
<Text fontSize="$xs" fontWeight="700" color={theme.muted} textTransform="uppercase" letterSpacing={1}>
{t('dashboard:readiness.nextStepTitle', 'Next step')}
</Text>
@@ -531,7 +531,7 @@ function LifecycleHero({
paddingHorizontal="$3"
onPress={() => navigate(adminPath(nextStep.targetPath))}
title={
<XStack alignItems="center" space="$2">
<XStack alignItems="center" gap="$2">
<Circle size={18} color={theme.primary} strokeWidth={2.5} />
<Text fontSize="$sm" fontWeight="700" color={theme.textStrong}>
{nextStep.label}
@@ -546,7 +546,7 @@ function LifecycleHero({
) : undefined
}
iconAfter={
<XStack alignItems="center" space="$1">
<XStack alignItems="center" gap="$1">
<PillBadge tone="success">{nextStep.ctaLabel}</PillBadge>
<ChevronRight size={16} color={theme.muted} />
</XStack>
@@ -665,7 +665,7 @@ function UnifiedToolGrid({ event, navigate, permissions, isMember, isCompleted }
return (
<DashboardCard padding="$0">
<YStack padding="$3.5" space="$2">
<YStack padding="$3.5" gap="$2">
<SectionHeader
title={t('dashboard:quickActions.title', 'Quick actions')}
subtitle={t('dashboard:quickActions.description', 'Jump straight to the most important actions.')}
@@ -674,9 +674,9 @@ function UnifiedToolGrid({ event, navigate, permissions, isMember, isCompleted }
/>
</YStack>
<Separator backgroundColor={theme.border} opacity={0.6} />
<YStack padding="$3.5" space="$3">
<YStack padding="$3.5" gap="$3">
{sections.map((section) => (
<YStack key={section.title} space="$2">
<YStack key={section.title} gap="$2">
<Text fontSize="$xs" fontWeight="700" color={theme.muted} textTransform="uppercase" letterSpacing={1}>
{section.title}
</Text>
@@ -692,7 +692,7 @@ function UnifiedToolGrid({ event, navigate, permissions, isMember, isCompleted }
paddingHorizontal="$3"
onPress={() => navigate(adminPath(item.path))}
title={
<XStack alignItems="center" space="$2.5">
<XStack alignItems="center" gap="$2.5">
<XStack
width={32}
height={32}
@@ -729,7 +729,7 @@ function RecentPhotosSection({ photos, navigate, slug }: { photos: TenantPhoto[]
return (
<DashboardCard>
<YStack space="$2">
<YStack gap="$2">
<XStack alignItems="center" justifyContent="space-between">
<Text fontSize="$xs" fontWeight="700" color={theme.muted} textTransform="uppercase" letterSpacing={1}>
{t('photos.recentTitle', 'Latest Uploads')}
@@ -749,7 +749,7 @@ function RecentPhotosSection({ photos, navigate, slug }: { photos: TenantPhoto[]
<Separator backgroundColor={theme.border} opacity={0.6} />
<XStack space="$2" overflow="scroll" paddingVertical="$1">
<XStack gap="$2" overflow="scroll" paddingVertical="$1">
{photos.map((photo) => (
<Pressable key={photo.id} onPress={() => navigate(adminPath(`/mobile/events/${slug}/control-room`))}>
<YStack
@@ -762,7 +762,7 @@ function RecentPhotosSection({ photos, navigate, slug }: { photos: TenantPhoto[]
borderColor={theme.border}
>
{photo.thumbnail_url ? (
<Image source={{ uri: photo.thumbnail_url }} width={80} height={80} resizeMode="cover" />
<Image src={photo.thumbnail_url} width={80} height={80} objectFit="cover" />
) : (
<YStack flex={1} alignItems="center" justifyContent="center">
<ImageIcon size={20} color={theme.muted} />
@@ -785,12 +785,12 @@ function AlertsSection({ event, stats, t }: any) {
return (
<DashboardCard>
<YStack space="$2">
<YStack gap="$2">
<Text fontSize="$xs" fontWeight="700" color={theme.muted} textTransform="uppercase" letterSpacing={1}>
{t('management:alertsTitle', 'Alerts')}
</Text>
<Separator backgroundColor={theme.border} opacity={0.6} />
<YStack space="$2">
<YStack gap="$2">
{limitWarnings.map((w: any, idx: number) => {
const isDanger = w.tone === 'danger';
const bg = isDanger ? theme.dangerBg : theme.warningBg;
@@ -807,7 +807,7 @@ function AlertsSection({ event, stats, t }: any) {
borderWidth={1}
borderColor={border}
alignItems="center"
space="$2"
gap="$2"
>
<Icon size={16} color={text} />
<Text fontSize="$sm" color={text} fontWeight="600">
@@ -826,7 +826,7 @@ function EmptyState({ canManage, onCreate }: any) {
const theme = useAdminTheme();
const { t } = useTranslation(['management', 'mobile']);
return (
<YStack flex={1} alignItems="center" justifyContent="center" space="$4">
<YStack flex={1} alignItems="center" justifyContent="center" gap="$4">
<Sparkles size={48} color={theme.primary} />
<Text fontSize="$lg" fontWeight="800" color={theme.textStrong} textAlign="center">
{t('mobile:header.appName', 'Event Admin')}

View File

@@ -192,14 +192,14 @@ export function DataExportsPanel({
</MobileCard>
) : null}
<MobileCard space="$2">
<MobileCard gap="$2">
<Text fontSize="$md" fontWeight="800" color={textStrong}>
{t('dataExports.request.title', 'Export request')}
</Text>
<Text fontSize="$xs" color={muted}>
{t('dataExports.request.hint', 'Export account data or a specific event archive.')}
</Text>
<YStack space="$2">
<YStack gap="$2">
{!isRecap ? (
<XStack alignItems="center" justifyContent="space-between">
<Text fontSize="$sm" color={text}>
@@ -276,7 +276,7 @@ export function DataExportsPanel({
/>
</MobileCard>
<MobileCard space="$2">
<MobileCard gap="$2">
<Text fontSize="$md" fontWeight="800" color={textStrong}>
{t('dataExports.history.title', 'Recent exports')}
</Text>
@@ -284,7 +284,7 @@ export function DataExportsPanel({
{t('dataExports.history.hint', 'Latest 10 exports for your account and events.')}
</Text>
{loading ? (
<YStack space="$2">
<YStack gap="$2">
<SkeletonCard height={72} />
<SkeletonCard height={72} />
</YStack>
@@ -293,9 +293,9 @@ export function DataExportsPanel({
{t('dataExports.history.empty', 'No exports yet.')}
</Text>
) : (
<YStack space="$2">
<YStack gap="$2">
{visibleExports.map((entry) => (
<MobileCard key={entry.id} space="$2">
<MobileCard key={entry.id} gap="$2">
<XStack alignItems="center" justifyContent="space-between">
<YStack>
<Text fontSize="$sm" fontWeight="700" color={textStrong}>

View File

@@ -37,7 +37,7 @@ export default function MobileEventAnalyticsPage() {
return (
<MobileShell title={t('analytics.title', 'Analytics')} activeTab="home">
<MobileCard
space="$4"
gap="$4"
padding="$6"
alignItems="center"
justifyContent="center"
@@ -55,7 +55,7 @@ export default function MobileEventAnalyticsPage() {
>
<Lock size={32} color={primary} />
</YStack>
<YStack space="$2" alignItems="center">
<YStack gap="$2" alignItems="center">
<Text fontSize="$xl" fontWeight="900" color={textStrong} textAlign="center">
{t('analytics.lockedTitle', 'Unlock Analytics')}
</Text>
@@ -75,7 +75,7 @@ export default function MobileEventAnalyticsPage() {
if (isLoading) {
return (
<MobileShell title={t('analytics.title', 'Analytics')} activeTab="home">
<YStack space="$3">
<YStack gap="$3">
<SkeletonCard height={200} />
<SkeletonCard height={150} />
<SkeletonCard height={150} />
@@ -116,12 +116,12 @@ export default function MobileEventAnalyticsPage() {
activeTab="home"
onBack={() => navigate(-1)}
>
<YStack space="$4">
<YStack space="$2">
<YStack gap="$4">
<YStack gap="$2">
<Text fontSize="$sm" fontWeight="800" color={textStrong}>
{t('analytics.kpiTitle', 'Event snapshot')}
</Text>
<XStack space="$2" flexWrap="wrap">
<XStack gap="$2" flexWrap="wrap">
<KpiTile
icon={TrendingUp}
label={t('analytics.kpiUploads', 'Uploads')}
@@ -140,14 +140,14 @@ export default function MobileEventAnalyticsPage() {
</XStack>
</YStack>
{/* Activity Timeline */}
<MobileCard space="$3" borderColor={border} backgroundColor={surface}>
<XStack alignItems="center" space="$2">
<MobileCard gap="$3" borderColor={border} backgroundColor={surface}>
<XStack alignItems="center" gap="$2">
<TrendingUp size={18} color={primary} />
<Text fontSize="$md" fontWeight="800" color={textStrong}>
{t('analytics.activityTitle', 'Activity Timeline')}
</Text>
</XStack>
<YStack space="$0.5">
<YStack gap="$0.5">
<Text fontSize="$xs" color={muted}>
{t('analytics.timeframe', 'Last {{hours}} hours', { hours: timeframeHours })}
</Text>
@@ -159,7 +159,7 @@ export default function MobileEventAnalyticsPage() {
</YStack>
{hasTimeline ? (
<YStack height={180} justifyContent="flex-end" space="$2">
<YStack height={180} justifyContent="flex-end" gap="$2">
<XStack alignItems="flex-end" justifyContent="space-between" height={150} gap="$1">
{timeline.map((point, index) => {
const heightPercent = (point.count / maxTimelineCount) * 100;
@@ -168,7 +168,7 @@ export default function MobileEventAnalyticsPage() {
const showLabel = timeline.length < 8 || index % 3 === 0;
return (
<YStack key={point.timestamp} flex={1} alignItems="center" space="$1">
<YStack key={point.timestamp} flex={1} alignItems="center" gap="$1">
<YStack
width="100%"
height={`${Math.max(heightPercent, 4)}%`}
@@ -200,8 +200,8 @@ export default function MobileEventAnalyticsPage() {
</MobileCard>
{/* Top Contributors */}
<MobileCard space="$3" borderColor={border} backgroundColor={surface}>
<XStack alignItems="center" space="$2">
<MobileCard gap="$3" borderColor={border} backgroundColor={surface}>
<XStack alignItems="center" gap="$2">
<Trophy size={18} color={primary} />
<Text fontSize="$md" fontWeight="800" color={textStrong}>
{t('analytics.contributorsTitle', 'Top Contributors')}
@@ -209,10 +209,10 @@ export default function MobileEventAnalyticsPage() {
</XStack>
{hasContributors ? (
<YStack space="$3">
<YStack gap="$3">
{contributors.map((contributor, idx) => (
<XStack key={idx} alignItems="center" justifyContent="space-between" paddingVertical="$1">
<XStack alignItems="center" space="$3">
<XStack alignItems="center" gap="$3">
<YStack
width={28}
height={28}
@@ -250,8 +250,8 @@ export default function MobileEventAnalyticsPage() {
</MobileCard>
{/* Task Stats */}
<MobileCard space="$3" borderColor={border} backgroundColor={surface}>
<XStack alignItems="center" space="$2">
<MobileCard gap="$3" borderColor={border} backgroundColor={surface}>
<XStack alignItems="center" gap="$2">
<ListTodo size={18} color={primary} />
<Text fontSize="$md" fontWeight="800" color={textStrong}>
{t('analytics.tasksTitle', 'Popular photo tasks')}
@@ -259,11 +259,11 @@ export default function MobileEventAnalyticsPage() {
</XStack>
{hasTasks ? (
<YStack space="$3">
<YStack gap="$3">
{tasks.map((task) => {
const percent = (task.count / maxTaskCount) * 100;
return (
<YStack key={task.task_id} space="$1">
<YStack key={task.task_id} gap="$1">
<XStack justifyContent="space-between">
<Text fontSize="$sm" color={textStrong} numberOfLines={1} flex={1}>
{task.task_name}
@@ -308,7 +308,7 @@ function EmptyState({
}) {
const { muted } = useAdminTheme();
return (
<YStack padding="$4" alignItems="center" justifyContent="center" space="$2">
<YStack padding="$4" alignItems="center" justifyContent="center" gap="$2">
<Text fontSize="$sm" color={muted}>
{message}
</Text>

View File

@@ -179,7 +179,7 @@ function PhotoGridTile({
)}
{badges.length ? (
<XStack position="absolute" top={6} left={6} space="$1.5">
<XStack position="absolute" top={6} left={6} gap="$1.5">
{badges.map((label) => (
<PhotoStatusTag key={`${photo.id}-${label}`} label={label} />
))}
@@ -194,7 +194,7 @@ function PhotoGridTile({
padding="$1"
borderRadius={12}
backgroundColor={overlayBg}
space="$2"
gap="$2"
justifyContent="space-between"
>
{actions.map((action) => (
@@ -1034,7 +1034,7 @@ export default function MobileEventControlRoomPage() {
}, [queuedActions, slug]);
const headerActions = (
<XStack space="$2">
<XStack gap="$2">
<HeaderActionButton
onPress={() => {
if (activeTab === 'moderation') {
@@ -1070,7 +1070,7 @@ export default function MobileEventControlRoomPage() {
value={activeTab}
onValueChange={(val) => setActiveTab(val as 'moderation' | 'live')}
header={(
<YStack space="$2">
<YStack gap="$2">
<XStack justifyContent="flex-end">
<ContextHelpLink slug="control-room-moderation" />
</XStack>
@@ -1094,8 +1094,8 @@ export default function MobileEventControlRoomPage() {
</XStack>
</Accordion.Trigger>
<Accordion.Content {...({ paddingTop: '$2' } as any)}>
<YStack space="$3">
<YStack space="$2">
<YStack gap="$3">
<YStack gap="$2">
<Text fontSize="$sm" fontWeight="800" color={text}>
{t('controlRoom.automation.title', 'Automation')}
</Text>
@@ -1177,7 +1177,7 @@ export default function MobileEventControlRoomPage() {
</MobileField>
</YStack>
<YStack space="$2">
<YStack gap="$2">
<Text fontSize="$sm" fontWeight="800" color={text}>
{t('controlRoom.automation.uploaders.title', 'Uploader overrides')}
</Text>
@@ -1195,7 +1195,7 @@ export default function MobileEventControlRoomPage() {
'Uploads from these devices skip the approval queue.',
)}
>
<YStack space="$2">
<YStack gap="$2">
<MobileSelect
value={trustedUploaderSelection}
onChange={(event) => setTrustedUploaderSelection(event.target.value)}
@@ -1215,7 +1215,7 @@ export default function MobileEventControlRoomPage() {
/>
</YStack>
{trustedUploaders.length ? (
<YStack space="$2">
<YStack gap="$2">
{trustedUploaders.map((rule) => (
<XStack
key={`trusted-${rule.device_id}`}
@@ -1227,7 +1227,7 @@ export default function MobileEventControlRoomPage() {
borderColor={border}
backgroundColor={surfaceMuted}
>
<YStack space="$0.5">
<YStack gap="$0.5">
<Text fontSize="$sm" fontWeight="700" color={text}>
{rule.label ?? t('common.anonymous', 'Anonymous')}
</Text>
@@ -1267,7 +1267,7 @@ export default function MobileEventControlRoomPage() {
'Uploads from these devices always need approval.',
)}
>
<YStack space="$2">
<YStack gap="$2">
<MobileSelect
value={forceReviewSelection}
onChange={(event) => setForceReviewSelection(event.target.value)}
@@ -1287,7 +1287,7 @@ export default function MobileEventControlRoomPage() {
/>
</YStack>
{forceReviewUploaders.length ? (
<YStack space="$2">
<YStack gap="$2">
{forceReviewUploaders.map((rule) => (
<XStack
key={`force-${rule.device_id}`}
@@ -1299,7 +1299,7 @@ export default function MobileEventControlRoomPage() {
borderColor={border}
backgroundColor={surfaceMuted}
>
<YStack space="$0.5">
<YStack gap="$0.5">
<Text fontSize="$sm" fontWeight="700" color={text}>
{rule.label ?? t('common.anonymous', 'Anonymous')}
</Text>
@@ -1344,11 +1344,11 @@ export default function MobileEventControlRoomPage() {
value: 'moderation',
label: t('controlRoom.tabs.moderation', 'Moderation'),
content: (
<YStack space="$2">
<YStack gap="$2">
{queuedEventCount > 0 ? (
<MobileCard>
<XStack alignItems="center" justifyContent="space-between" space="$2">
<YStack space="$1" flex={1}>
<XStack alignItems="center" justifyContent="space-between" gap="$2">
<YStack gap="$1" flex={1}>
<Text fontSize="$sm" fontWeight="700" color={text}>
{t('mobilePhotos.queueTitle', 'Changes waiting to sync')}
</Text>
@@ -1371,7 +1371,7 @@ export default function MobileEventControlRoomPage() {
) : null}
<MobileCard>
<YStack space="$2">
<YStack gap="$2">
<Text fontSize="$xs" fontWeight="800" color={text}>
{t('mobilePhotos.filtersTitle', 'Filter')}
</Text>
@@ -1390,7 +1390,7 @@ export default function MobileEventControlRoomPage() {
value={moderationFilter}
onValueChange={(value: string) => value && setModerationFilter(value as ModerationFilter)}
>
<XStack space="$1.5">
<XStack gap="$1.5">
{MODERATION_FILTERS.map((option) => {
const active = option.value === moderationFilter;
const count = moderationCounts[option.value] ?? 0;
@@ -1407,7 +1407,7 @@ export default function MobileEventControlRoomPage() {
paddingHorizontal="$3"
pressStyle={{ opacity: 0.85 }}
>
<XStack alignItems="center" space="$1.5">
<XStack alignItems="center" gap="$1.5">
<Text fontSize="$xs" fontWeight="700" color={active ? '#fff' : muted}>
{t(option.labelKey, option.fallback)}
</Text>
@@ -1455,13 +1455,13 @@ export default function MobileEventControlRoomPage() {
) : null}
{moderationLoading && moderationPage === 1 ? (
<YStack space="$2">
<YStack gap="$2">
{Array.from({ length: 3 }).map((_, idx) => (
<SkeletonCard key={`moderation-skeleton-${idx}`} height={120} />
))}
</YStack>
) : moderationPhotos.length === 0 ? (
<MobileCard alignItems="center" justifyContent="center" space="$2">
<MobileCard alignItems="center" justifyContent="center" gap="$2">
<ImageIcon size={28} color={muted} />
<Text fontSize="$sm" fontWeight="700" color={text}>
{t('controlRoom.emptyModeration', 'No uploads match this filter.')}
@@ -1543,7 +1543,7 @@ export default function MobileEventControlRoomPage() {
value: 'live',
label: t('controlRoom.tabs.live', 'Live Show'),
content: (
<YStack space="$2">
<YStack gap="$2">
<MobileCard borderColor={border} backgroundColor="transparent">
<Text fontSize="$sm" color={muted}>
{t(
@@ -1559,7 +1559,7 @@ export default function MobileEventControlRoomPage() {
</MobileCard>
<MobileCard>
<YStack space="$2">
<YStack gap="$2">
<Text fontSize="$xs" fontWeight="800" color={text}>
{t('liveShowQueue.filterLabel', 'Live status')}
</Text>
@@ -1578,7 +1578,7 @@ export default function MobileEventControlRoomPage() {
value={liveStatusFilter}
onValueChange={(value: string) => value && setLiveStatusFilter(value as LiveShowQueueStatus)}
>
<XStack space="$1.5">
<XStack gap="$1.5">
{LIVE_STATUS_OPTIONS.map((option) => {
const active = option.value === liveStatusFilter;
const count = liveCounts[option.value] ?? 0;
@@ -1595,7 +1595,7 @@ export default function MobileEventControlRoomPage() {
paddingHorizontal="$3"
pressStyle={{ opacity: 0.85 }}
>
<XStack alignItems="center" space="$1.5">
<XStack alignItems="center" gap="$1.5">
<Text fontSize="$xs" fontWeight="700" color={active ? '#fff' : muted}>
{t(option.labelKey, option.fallback)}
</Text>
@@ -1631,13 +1631,13 @@ export default function MobileEventControlRoomPage() {
) : null}
{liveLoading && livePage === 1 ? (
<YStack space="$2">
<YStack gap="$2">
{Array.from({ length: 3 }).map((_, idx) => (
<SkeletonCard key={`live-skeleton-${idx}`} height={120} />
))}
</YStack>
) : livePhotos.length === 0 ? (
<MobileCard alignItems="center" justifyContent="center" space="$2">
<MobileCard alignItems="center" justifyContent="center" gap="$2">
<Text fontSize="$sm" fontWeight="700" color={text}>
{t('controlRoom.emptyLive', 'No photos waiting for Live Show.')}
</Text>

View File

@@ -377,7 +377,7 @@ export default function MobileEventFormPage() {
const requiredLabel = React.useCallback(
(label: string) => (
<XStack alignItems="center" space="$1">
<XStack alignItems="center" gap="$1">
<Text fontSize="$sm" fontWeight="800" color={text}>
{label}
</Text>
@@ -407,7 +407,7 @@ export default function MobileEventFormPage() {
<ContextHelpLink slug="event-settings" />
</XStack>
<MobileCard space="$3">
<MobileCard gap="$3">
<MobileField label={requiredLabel(t('eventForm.fields.name.label', 'Event name'))}>
<MobileInput
type="text"
@@ -471,7 +471,7 @@ export default function MobileEventFormPage() {
) : null}
<MobileField label={requiredLabel(t('eventForm.fields.date.label', 'Date & time'))}>
<XStack alignItems="center" space="$2">
<XStack alignItems="center" gap="$2">
<MobileDateInput
value={extractDateValue(form.date)}
onChange={handleDateChange}
@@ -511,7 +511,7 @@ export default function MobileEventFormPage() {
</MobileField>
<MobileField label={t('eventForm.fields.location.label', 'Location')}>
<XStack alignItems="center" space="$2">
<XStack alignItems="center" gap="$2">
<MobileInput
type="text"
value={form.location}
@@ -524,7 +524,7 @@ export default function MobileEventFormPage() {
</MobileField>
<MobileField label={t('eventForm.fields.publish.label', 'Publish immediately')}>
<XStack alignItems="center" space="$2">
<XStack alignItems="center" gap="$2">
<Switch
checked={form.published}
onCheckedChange={(checked: boolean) =>
@@ -543,7 +543,7 @@ export default function MobileEventFormPage() {
</MobileField>
<MobileField label={t('eventForm.fields.tasksMode.label', 'Photo tasks & challenges')}>
<XStack alignItems="center" space="$2">
<XStack alignItems="center" gap="$2">
<Switch
checked={form.tasksEnabled}
onCheckedChange={(checked: boolean) =>
@@ -574,7 +574,7 @@ export default function MobileEventFormPage() {
</MobileField>
<MobileField label={t('eventForm.fields.uploadVisibility.label', 'Uploads visible immediately')}>
<XStack alignItems="center" space="$2">
<XStack alignItems="center" gap="$2">
<Switch
checked={form.autoApproveUploads}
onCheckedChange={(checked: boolean) =>
@@ -605,7 +605,7 @@ export default function MobileEventFormPage() {
</MobileField>
</MobileCard>
<YStack space="$2" paddingBottom="$10">
<YStack gap="$2" paddingBottom="$10">
{!isEdit ? (
<CTAButton
label={t('eventForm.actions.create', 'Create event')}

View File

@@ -204,11 +204,11 @@ export default function MobileEventGuestNotificationsPage() {
) : null}
<YStack ref={formRef}>
<MobileCard space="$3">
<MobileCard gap="$3">
<Text fontSize="$md" fontWeight="800" color={textStrong}>
{t('guestMessages.composeTitle', 'Send a message')}
</Text>
<YStack space="$2">
<YStack gap="$2">
<MobileField label={t('guestMessages.form.title', 'Title')}>
<MobileInput
type="text"
@@ -244,7 +244,7 @@ export default function MobileEventGuestNotificationsPage() {
</MobileField>
) : null}
<MobileField label={t('guestMessages.form.cta', 'CTA (optional)')}>
<YStack space="$1.5">
<YStack gap="$1.5">
<MobileInput
type="text"
value={form.cta_label}
@@ -262,7 +262,7 @@ export default function MobileEventGuestNotificationsPage() {
</Text>
</YStack>
</MobileField>
<XStack space="$2">
<XStack gap="$2">
<MobileField label={t('guestMessages.form.expiresIn', 'Expires in (minutes)')}>
<MobileInput
type="number"
@@ -301,7 +301,7 @@ export default function MobileEventGuestNotificationsPage() {
</MobileCard>
</YStack>
<MobileCard space="$2">
<MobileCard gap="$2">
<XStack alignItems="center" justifyContent="space-between">
<Text fontSize="$md" fontWeight="800" color={textStrong}>
{t('guestMessages.historyTitle', 'Recent messages')}
@@ -312,13 +312,13 @@ export default function MobileEventGuestNotificationsPage() {
</XStack>
{loading ? (
<YStack space="$2">
<YStack gap="$2">
{Array.from({ length: 3 }).map((_, idx) => (
<SkeletonCard key={`s-${idx}`} height={72} />
))}
</YStack>
) : history.length === 0 ? (
<YStack space="$2">
<YStack gap="$2">
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{t('guestMessages.emptyTitle', 'Send your first guest message')}
</Text>
@@ -332,14 +332,14 @@ export default function MobileEventGuestNotificationsPage() {
/>
</YStack>
) : (
<YStack space="$2">
<YStack gap="$2">
{history.map((item) => (
<MobileCard key={item.id} space="$2" borderColor={border}>
<MobileCard key={item.id} gap="$2" borderColor={border}>
<XStack alignItems="center" justifyContent="space-between">
<Text fontSize="$sm" fontWeight="800" color={textStrong}>
{item.title || t('guestMessages.history.untitled', 'Untitled')}
</Text>
<XStack space="$1.5" alignItems="center">
<XStack gap="$1.5" alignItems="center">
<PillBadge tone={item.status === 'active' ? 'success' : 'muted'}>
{t(`guestMessages.status.${item.status}`, item.status)}
</PillBadge>
@@ -354,11 +354,11 @@ export default function MobileEventGuestNotificationsPage() {
{item.body ?? t('guestMessages.history.noBody', 'No body provided.')}
</Text>
<XStack alignItems="center" justifyContent="space-between">
<XStack space="$1.5" alignItems="center">
<XStack gap="$1.5" alignItems="center">
<PillBadge tone="muted">{t(`guestMessages.type.${item.type}`, item.type)}</PillBadge>
{item.target_identifier ? (
<PillBadge tone="muted">
<XStack alignItems="center" space="$1.5">
<XStack alignItems="center" gap="$1.5">
<User size={12} color={muted} />
<Text fontSize="$xs" fontWeight="700" color={muted}>
{item.target_identifier}
@@ -367,7 +367,7 @@ export default function MobileEventGuestNotificationsPage() {
</PillBadge>
) : (
<PillBadge tone="muted">
<XStack alignItems="center" space="$1.5">
<XStack alignItems="center" gap="$1.5">
<Users size={12} color={muted} />
<Text fontSize="$xs" fontWeight="700" color={muted}>
{t('guestMessages.audience.all', 'All guests')}

View File

@@ -285,15 +285,15 @@ export default function MobileEventLiveShowSettingsPage() {
</XStack>
{loading ? (
<YStack space="$2">
<YStack gap="$2">
{Array.from({ length: 3 }).map((_, idx) => (
<SkeletonCard key={`ls-skel-${idx}`} height={110} />
))}
</YStack>
) : (
<YStack space="$2">
<MobileCard space="$2">
<XStack alignItems="center" space="$2">
<YStack gap="$2">
<MobileCard gap="$2">
<XStack alignItems="center" gap="$2">
<Link2 size={18} color={text} />
<Text fontSize="$md" fontWeight="800" color={text}>
{t('liveShowSettings.link.title', 'Live Show link')}
@@ -313,7 +313,7 @@ export default function MobileEventLiveShowSettingsPage() {
{t('liveShowSettings.link.empty', 'No Live Show link available.')}
</Text>
)}
<XStack space="$2" marginTop="$2" alignItems="center" flexWrap="nowrap">
<XStack gap="$2" marginTop="$2" alignItems="center" flexWrap="nowrap">
<IconAction
label={t('liveShowSettings.link.copy', 'Copy')}
disabled={!liveShowLink?.url}
@@ -344,7 +344,7 @@ export default function MobileEventLiveShowSettingsPage() {
</IconAction>
</XStack>
{liveShowLink?.qr_code_data_url ? (
<XStack space="$2" alignItems="center" marginTop="$2" flexWrap="wrap">
<XStack gap="$2" alignItems="center" marginTop="$2" flexWrap="wrap">
<Pressable
onPress={() => downloadQr(liveShowLink.qr_code_data_url!, 'live-show-qr.png')}
title={t('liveShowSettings.link.downloadQr', 'Download QR')}
@@ -373,8 +373,8 @@ export default function MobileEventLiveShowSettingsPage() {
) : null}
</MobileCard>
<MobileCard space="$2">
<XStack alignItems="center" space="$2">
<MobileCard gap="$2">
<XStack alignItems="center" gap="$2">
<Settings size={18} color={text} />
<Text fontSize="$md" fontWeight="800" color={text}>
{t('liveShowSettings.title', 'Live Show settings')}
@@ -385,7 +385,7 @@ export default function MobileEventLiveShowSettingsPage() {
</Text>
</MobileCard>
<MobileCard space="$3">
<MobileCard gap="$3">
<Text fontSize="$sm" fontWeight="800" color={text}>
{t('liveShowSettings.sections.moderation', 'Moderation')}
</Text>
@@ -423,7 +423,7 @@ export default function MobileEventLiveShowSettingsPage() {
</MobileField>
</MobileCard>
<MobileCard space="$3">
<MobileCard gap="$3">
<Text fontSize="$sm" fontWeight="800" color={text}>
{t('liveShowSettings.sections.playback', 'Playback')}
</Text>
@@ -478,7 +478,7 @@ export default function MobileEventLiveShowSettingsPage() {
) : null}
</MobileCard>
<MobileCard space="$3">
<MobileCard gap="$3">
<Text fontSize="$sm" fontWeight="800" color={text}>
{t('liveShowSettings.sections.effects', 'Effects & layout')}
</Text>
@@ -661,7 +661,7 @@ function EffectSlider({
const { text, muted, primary, border, surface } = useAdminTheme();
return (
<YStack space="$1.5">
<YStack gap="$1.5">
<XStack alignItems="center" justifyContent="space-between">
<Text fontSize="$sm" fontWeight="700" color={text}>
{label}

View File

@@ -163,11 +163,11 @@ export default function MobileEventMembersPage() {
<ContextHelpLink slug="event-team-invites" />
</XStack>
<MobileCard space="$3">
<MobileCard gap="$3">
<Text fontSize="$md" fontWeight="800" color={textStrong}>
{t('events.members.inviteTitle', 'Invite Member')}
</Text>
<YStack space="$2">
<YStack gap="$2">
<MobileField label={t('events.members.name', 'Name')}>
<MobileInput
type="text"
@@ -223,11 +223,11 @@ export default function MobileEventMembersPage() {
) : null}
{members.length > 0 ? (
<YStack space="$2">
<YStack gap="$2">
<Text fontSize="$xs" fontWeight="700" color={muted}>
{t('events.members.filters.statusLabel', 'Status')}
</Text>
<XStack space="$2" flexWrap="wrap">
<XStack gap="$2" flexWrap="wrap">
{statusOptions.map((option) => {
const isActive = statusFilter === option.key;
return (
@@ -251,7 +251,7 @@ export default function MobileEventMembersPage() {
<Text fontSize="$xs" fontWeight="700" color={muted}>
{t('events.members.filters.roleLabel', 'Role')}
</Text>
<XStack space="$2" flexWrap="wrap">
<XStack gap="$2" flexWrap="wrap">
{roleOptions.map((option) => {
const isActive = roleFilter === option.key;
return (
@@ -275,18 +275,18 @@ export default function MobileEventMembersPage() {
</YStack>
) : null}
<MobileCard space="$3">
<MobileCard gap="$3">
<Text fontSize="$md" fontWeight="800" color={textStrong}>
{t('events.members.listTitle', 'Team & Guests')}
</Text>
{loading ? (
<YStack space="$2">
<YStack gap="$2">
{Array.from({ length: 4 }).map((_, idx) => (
<SkeletonCard key={`m-${idx}`} height={70} />
))}
</YStack>
) : members.length === 0 ? (
<YStack space="$2">
<YStack gap="$2">
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{t('events.members.emptyTitle', 'Invite your team')}
</Text>
@@ -295,9 +295,9 @@ export default function MobileEventMembersPage() {
</Text>
</YStack>
) : (
<YStack space="$2">
<YStack gap="$2">
{filteredMembers.length === 0 ? (
<YStack space="$1.5" padding="$2">
<YStack gap="$1.5" padding="$2">
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{t('events.members.emptyFilteredTitle', 'No matching members')}
</Text>
@@ -322,14 +322,14 @@ export default function MobileEventMembersPage() {
return (
<MobileCard key={member.id} padding="$3" borderColor={border}>
<XStack alignItems="center" justifyContent="space-between">
<YStack space="$1">
<YStack gap="$1">
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{member.name || member.email || t('events.members.fallbackName', 'Guest')}
</Text>
<Text fontSize="$xs" color={muted}>
{member.email ?? ''}
</Text>
<XStack space="$1.5" alignItems="center">
<XStack gap="$1.5" alignItems="center">
<PillBadge tone={statusInfo.tone}>
{statusInfo.label}
</PillBadge>
@@ -340,7 +340,7 @@ export default function MobileEventMembersPage() {
</PillBadge>
</XStack>
</YStack>
<XStack space="$2">
<XStack gap="$2">
<Pressable
aria-label={t('events.members.copyEmailLabel', 'Copy email')}
onPress={async () => {

View File

@@ -191,15 +191,15 @@ export default function MobileEventPhotoboothPage() {
) : null}
{loading ? (
<YStack space="$2">
<YStack gap="$2">
{Array.from({ length: 3 }).map((_, idx) => (
<SkeletonCard key={`ph-skel-${idx}`} height={110} />
))}
</YStack>
) : (
<YStack space="$2">
<MobileCard space="$3">
<YStack space="$1">
<YStack gap="$2">
<MobileCard gap="$3">
<YStack gap="$1">
<Text fontSize="$sm" fontWeight="800" color={text}>
{t('photobooth.steps.activate.title', '1. Photobooth aktivieren')}
</Text>
@@ -207,7 +207,7 @@ export default function MobileEventPhotoboothPage() {
{t('photobooth.steps.activate.description', 'Schalte den Upload-Zugang fuer dieses Event frei.')}
</Text>
</YStack>
<XStack alignItems="center" justifyContent="space-between" space="$3" flexWrap="wrap">
<XStack alignItems="center" justifyContent="space-between" gap="$3" flexWrap="wrap">
<PillBadge tone={isActive ? 'success' : 'warning'}>
{isActive ? t('photobooth.status.badgeActive', 'ACTIVE') : t('photobooth.status.badgeInactive', 'INACTIVE')}
</PillBadge>
@@ -215,7 +215,7 @@ export default function MobileEventPhotoboothPage() {
{t('photobooth.mode.active', 'Current: {{mode}}', { mode: modeLabel })}
</Text>
</XStack>
<XStack space="$2" marginTop="$2">
<XStack gap="$2" marginTop="$2">
<CTAButton
label={isActive ? t('photobooth.actions.disable', 'Disable uploads') : t('photobooth.actions.enable', 'Enable uploads')}
onPress={() => (isActive ? handleDisable() : handleEnable())}
@@ -237,8 +237,8 @@ export default function MobileEventPhotoboothPage() {
</XStack>
</MobileCard>
<MobileCard space="$3">
<YStack space="$1">
<MobileCard gap="$3">
<YStack gap="$1">
<Text fontSize="$sm" fontWeight="800" color={text}>
{t('photobooth.steps.download.title', '2. Uploader App herunterladen')}
</Text>
@@ -249,7 +249,7 @@ export default function MobileEventPhotoboothPage() {
)}
</Text>
</YStack>
<XStack space="$2" marginTop="$2" flexWrap="wrap">
<XStack gap="$2" marginTop="$2" flexWrap="wrap">
<CTAButton
label={t('photobooth.uploaderDownload.actionWindows', 'Uploader herunterladen (Windows)')}
onPress={() => {
@@ -278,7 +278,7 @@ export default function MobileEventPhotoboothPage() {
fullWidth={false}
/>
</XStack>
<XStack space="$2" marginTop="$2">
<XStack gap="$2" marginTop="$2">
<CTAButton
label={
sendingEmail
@@ -294,8 +294,8 @@ export default function MobileEventPhotoboothPage() {
</XStack>
</MobileCard>
<MobileCard space="$3">
<YStack space="$1">
<MobileCard gap="$3">
<YStack gap="$1">
<Text fontSize="$sm" fontWeight="800" color={text}>
{t('photobooth.steps.access.title', '3. Verbindungscode erstellen')}
</Text>
@@ -303,7 +303,7 @@ export default function MobileEventPhotoboothPage() {
{t('photobooth.steps.access.description', 'Der Code verbindet die App sicher mit deinem Event.')}
</Text>
</YStack>
<XStack space="$2" marginTop="$2">
<XStack gap="$2" marginTop="$2">
<CTAButton
label={
connectLoading
@@ -326,7 +326,7 @@ export default function MobileEventPhotoboothPage() {
fullWidth={false}
/>
</XStack>
<YStack space="$2" marginTop="$2">
<YStack gap="$2" marginTop="$2">
{connectCode ? (
<CredentialRow label={t('photobooth.connectCode.label', 'Connect code')} value={connectCode} border={border} />
) : null}
@@ -338,7 +338,7 @@ export default function MobileEventPhotoboothPage() {
</Text>
) : null}
{showCredentials ? (
<YStack space="$1">
<YStack gap="$1">
<CredentialRow label={t('photobooth.credentials.postUrl', 'Upload URL')} value={uploadUrl ?? '—'} border={border} />
<CredentialRow label={t('photobooth.credentials.username', 'Username')} value={username ?? '—'} border={border} />
<CredentialRow label={t('photobooth.credentials.password', 'Password')} value={password ?? '—'} border={border} masked />
@@ -354,11 +354,11 @@ export default function MobileEventPhotoboothPage() {
</YStack>
</MobileCard>
<MobileCard space="$2">
<MobileCard gap="$2">
<Text fontSize="$sm" fontWeight="700" color={text}>
{t('photobooth.status.heading', 'Status')}
</Text>
<YStack space="$1">
<YStack gap="$1">
<StatusRow icon={<ShieldCheck size={16} color={text} />} label={t('photobooth.status.mode', 'Mode')} value={modeLabel} />
<StatusRow
icon={<PlugZap size={16} color={text} />}
@@ -437,7 +437,7 @@ function StatusRow({ icon, label, value }: { icon: React.ReactNode; label: strin
const { text } = useAdminTheme();
return (
<XStack alignItems="center" justifyContent="space-between">
<XStack alignItems="center" space="$2">
<XStack alignItems="center" gap="$2">
{icon}
<Text fontSize="$sm" color={text}>
{label}

View File

@@ -170,7 +170,7 @@ export default function MobileEventRecapPage() {
if (loading) {
return (
<MobileShell activeTab="home" title={t('events.recap.title', 'Event Recap')} onBack={back}>
<YStack space="$3">
<YStack gap="$3">
<SkeletonCard height={120} />
<SkeletonCard height={200} />
<SkeletonCard height={150} />
@@ -207,8 +207,8 @@ export default function MobileEventRecapPage() {
title={t('events.recap.title', 'Event Recap')}
onBack={back}
>
<YStack space="$4">
<XStack space="$2">
<YStack gap="$4">
<XStack gap="$2">
<TabButton
label={t('events.recap.tabs.overview', 'Overview')}
active={activeTab === 'overview'}
@@ -227,10 +227,10 @@ export default function MobileEventRecapPage() {
</XStack>
{activeTab === 'overview' ? (
<YStack space="$4">
<MobileCard space="$3">
<YStack gap="$4">
<MobileCard gap="$3">
<XStack alignItems="center" justifyContent="space-between">
<YStack space="$1">
<YStack gap="$1">
<Text fontSize="$xl" fontWeight="800" color={textStrong}>
{t('events.recap.completedTitle', 'Event abgeschlossen')}
</Text>
@@ -248,8 +248,8 @@ export default function MobileEventRecapPage() {
</XStack>
</MobileCard>
<MobileCard space="$3">
<XStack alignItems="center" space="$2">
<MobileCard gap="$3">
<XStack alignItems="center" gap="$2">
<Share2 size={18} color={primary} />
<Text fontSize="$md" fontWeight="800" color={textStrong}>
{t('events.recap.shareGuests', 'Gäste-Galerie teilen')}
@@ -260,7 +260,7 @@ export default function MobileEventRecapPage() {
</Text>
{guestLink ? (
<YStack space="$2" marginTop="$1">
<YStack gap="$2" marginTop="$1">
<XStack
backgroundColor={border}
padding="$3"
@@ -292,7 +292,7 @@ export default function MobileEventRecapPage() {
)}
{guestLink && activeInvite?.qr_code_data_url ? (
<YStack alignItems="center" space="$2" marginTop="$2">
<YStack alignItems="center" gap="$2" marginTop="$2">
<YStack
padding="$2"
backgroundColor="white"
@@ -315,15 +315,15 @@ export default function MobileEventRecapPage() {
) : null}
</MobileCard>
<MobileCard space="$3">
<XStack alignItems="center" space="$2">
<MobileCard gap="$3">
<XStack alignItems="center" gap="$2">
<Users size={18} color={primary} />
<Text fontSize="$md" fontWeight="800" color={textStrong}>
{t('events.recap.settings', 'Nachlauf-Optionen')}
</Text>
</XStack>
<YStack space="$1.5">
<YStack gap="$1.5">
<ToggleOption
label={t('events.recap.allowDownloads', 'Gäste dürfen Fotos laden')}
value={Boolean(event.settings?.guest_downloads_enabled)}
@@ -337,8 +337,8 @@ export default function MobileEventRecapPage() {
</YStack>
</MobileCard>
<MobileCard space="$3">
<XStack alignItems="center" space="$2">
<MobileCard gap="$3">
<XStack alignItems="center" gap="$2">
<Sparkles size={18} color={primary} />
<Text fontSize="$md" fontWeight="800" color={textStrong}>
{t('events.recap.addons', 'Galerie verlängern')}
@@ -348,7 +348,7 @@ export default function MobileEventRecapPage() {
{t('events.recap.addonBody', 'Die Online-Zeit deiner Galerie neigt sich dem Ende? Hier kannst du sie verlängern.')}
</Text>
<YStack space="$2">
<YStack gap="$2">
{addons
.filter((a) => a.key === 'gallery_extension')
.map((addon) => (
@@ -365,9 +365,9 @@ export default function MobileEventRecapPage() {
) : null}
{activeTab === 'engagement' ? (
<YStack space="$4">
<YStack gap="$4">
{engagementLoading ? (
<YStack space="$2">
<YStack gap="$2">
<SkeletonCard height={140} />
<SkeletonCard height={180} />
<SkeletonCard height={180} />
@@ -387,9 +387,9 @@ export default function MobileEventRecapPage() {
</Text>
</MobileCard>
) : (
<YStack space="$4">
<MobileCard space="$3">
<XStack alignItems="center" space="$2">
<YStack gap="$4">
<MobileCard gap="$3">
<XStack alignItems="center" gap="$2">
<TrendingUp size={18} color={primary} />
<Text fontSize="$md" fontWeight="800" color={textStrong}>
{t('events.recap.engagement.title', 'Guest engagement')}
@@ -418,8 +418,8 @@ export default function MobileEventRecapPage() {
</XStack>
</MobileCard>
<MobileCard space="$3">
<XStack alignItems="center" space="$2">
<MobileCard gap="$3">
<XStack alignItems="center" gap="$2">
<Trophy size={18} color={primary} />
<Text fontSize="$md" fontWeight="800" color={textStrong}>
{t('events.recap.engagement.leaderboards.uploadsTitle', 'Top contributors')}
@@ -430,7 +430,7 @@ export default function MobileEventRecapPage() {
{t('events.recap.engagement.leaderboards.uploadsEmpty', 'No uploads yet.')}
</Text>
) : (
<YStack space="$1.5" marginTop="$1">
<YStack gap="$1.5" marginTop="$1">
{engagement.leaderboards.uploads.slice(0, 5).map((entry, index) => (
<LeaderboardRow
key={`${entry.guest}-${entry.photos}-${index}`}
@@ -443,8 +443,8 @@ export default function MobileEventRecapPage() {
)}
</MobileCard>
<MobileCard space="$3">
<XStack alignItems="center" space="$2">
<MobileCard gap="$3">
<XStack alignItems="center" gap="$2">
<Heart size={18} color={primary} />
<Text fontSize="$md" fontWeight="800" color={textStrong}>
{t('events.recap.engagement.leaderboards.likesTitle', 'Most liked')}
@@ -455,7 +455,7 @@ export default function MobileEventRecapPage() {
{t('events.recap.engagement.leaderboards.likesEmpty', 'No likes yet.')}
</Text>
) : (
<YStack space="$1.5" marginTop="$1">
<YStack gap="$1.5" marginTop="$1">
{engagement.leaderboards.likes.slice(0, 5).map((entry, index) => (
<LeaderboardRow
key={`${entry.guest}-${entry.likes}-${index}`}
@@ -468,15 +468,15 @@ export default function MobileEventRecapPage() {
)}
</MobileCard>
<MobileCard space="$3">
<XStack alignItems="center" space="$2">
<MobileCard gap="$3">
<XStack alignItems="center" gap="$2">
<Sparkles size={18} color={primary} />
<Text fontSize="$md" fontWeight="800" color={textStrong}>
{t('events.recap.engagement.highlightsTitle', 'Highlights')}
</Text>
</XStack>
<YStack space="$2" marginTop="$1">
<XStack space="$2" alignItems="center">
<YStack gap="$2" marginTop="$1">
<XStack gap="$2" alignItems="center">
<YStack
width={72}
height={72}
@@ -519,7 +519,7 @@ export default function MobileEventRecapPage() {
</Text>
</XStack>
<YStack space="$1">
<YStack gap="$1">
<Text fontSize="$sm" fontWeight="600" color={textStrong}>
{t('events.recap.engagement.timeline', 'Uploads over time')}
</Text>
@@ -528,7 +528,7 @@ export default function MobileEventRecapPage() {
{t('events.recap.engagement.timelineEmpty', 'No timeline data yet.')}
</Text>
) : (
<YStack space="$1">
<YStack gap="$1">
{engagement.highlights.timeline.slice(-5).map((point) => (
<XStack key={point.date} alignItems="center" justifyContent="space-between">
<Text fontSize="$xs" color={muted}>
@@ -553,7 +553,7 @@ export default function MobileEventRecapPage() {
) : null}
{activeTab === 'compliance' ? (
<YStack space="$4">
<YStack gap="$4">
<DataExportsPanel variant="recap" event={event} />
</YStack>
) : null}
@@ -643,7 +643,7 @@ function LeaderboardRow({ rank, name, value }: { rank: number; name: string; val
borderColor={border}
backgroundColor={surfaceMuted}
>
<XStack alignItems="center" space="$2">
<XStack alignItems="center" gap="$2">
<Text fontSize="$xs" color={muted} fontWeight="700">
#{rank}
</Text>

View File

@@ -588,8 +588,8 @@ export default function MobileEventTasksPage() {
}
const taskPanel = assignedTasks.length === 0 ? (
<YStack space="$2">
<MobileCard space="$2">
<YStack gap="$2">
<MobileCard gap="$2">
<Text fontSize={13} fontWeight="700" color={text}>
{t('events.tasks.emptyTitle', 'No photo tasks yet')}
</Text>
@@ -602,7 +602,7 @@ export default function MobileEventTasksPage() {
{limitReachedHint ? ` ${limitReachedHint}` : ''}
</Text>
) : null}
<XStack space="$2">
<XStack gap="$2">
<CTAButton
label={t('events.tasks.emptyActionTask', 'Add photo task')}
onPress={() => setShowTaskSheet(true)}
@@ -631,7 +631,7 @@ export default function MobileEventTasksPage() {
setShowTaskSheet(true);
}}
title={
<XStack alignItems="center" space="$2">
<XStack alignItems="center" gap="$2">
<YStack
width={28}
height={28}
@@ -669,7 +669,7 @@ export default function MobileEventTasksPage() {
setActiveTab('collections');
}}
title={
<XStack alignItems="center" space="$2">
<XStack alignItems="center" gap="$2">
<YStack
width={28}
height={28}
@@ -698,9 +698,9 @@ export default function MobileEventTasksPage() {
</YGroup>
</YStack>
) : (
<YStack space="$2">
<XStack alignItems="baseline" justifyContent="space-between" flexWrap="wrap" space="$2">
<XStack alignItems="baseline" space="$2" flexWrap="wrap">
<YStack gap="$2">
<XStack alignItems="baseline" justifyContent="space-between" flexWrap="wrap" gap="$2">
<XStack alignItems="baseline" gap="$2" flexWrap="wrap">
<Text fontSize="$sm" fontWeight="800" color={text}>
{t('events.tasks.assignedTitle', 'Task list')}
</Text>
@@ -718,11 +718,11 @@ export default function MobileEventTasksPage() {
) : null}
</XStack>
{selectionMode ? (
<MobileCard padding="$2.5" space="$2">
<MobileCard padding="$2.5" gap="$2">
<Text fontSize={12} fontWeight="700" color={text}>
{t('events.tasks.selectionCount', '{{count}} ausgewählt', { count: selectedTaskIds.size })}
</Text>
<XStack space="$2">
<XStack gap="$2">
<CTAButton
label={t('events.tasks.bulkRemove', 'Auswahl löschen')}
tone="danger"
@@ -751,7 +751,7 @@ export default function MobileEventTasksPage() {
onPointerLeave={cancelLongPress}
onPointerCancel={cancelLongPress}
title={
<XStack alignItems="center" space="$2">
<XStack alignItems="center" gap="$2">
{selectionMode ? (
<Checkbox
size="$3"
@@ -779,7 +779,7 @@ export default function MobileEventTasksPage() {
}
iconAfter={
selectionMode ? null : (
<XStack space="$2" alignItems="center">
<XStack gap="$2" alignItems="center">
{task.emotion ? (
<Tag label={task.emotion.name ?? ''} color={task.emotion.color ?? text} />
) : null}
@@ -811,7 +811,7 @@ export default function MobileEventTasksPage() {
);
const libraryPanel = (
<YStack space="$2">
<YStack gap="$2">
<XStack alignItems="center" justifyContent="space-between" flexWrap="wrap">
<Text fontSize="$sm" fontWeight="700" color={text}>
{t('events.tasks.tabs.library', 'Task Library')}
@@ -865,7 +865,7 @@ export default function MobileEventTasksPage() {
) : null
}
iconAfter={
<XStack space="$1.5" alignItems="center">
<XStack gap="$1.5" alignItems="center">
<Pressable
onPress={() => {
if (!canAddTasks) {
@@ -875,7 +875,7 @@ export default function MobileEventTasksPage() {
quickAssign(task.id);
}}
>
<XStack alignItems="center" space="$1">
<XStack alignItems="center" gap="$1">
<Plus size={14} color={canAddTasks ? primary : muted} />
<Text fontSize={12} fontWeight="600" color={canAddTasks ? primary : muted}>
{assigningId === task.id ? t('common.processing', '...') : t('events.tasks.add', 'Add')}
@@ -896,7 +896,7 @@ export default function MobileEventTasksPage() {
);
const collectionsPanel = (
<YStack space="$2">
<YStack gap="$2">
<Text fontSize="$xs" color={muted}>
{t('events.tasks.importHint', 'Use predefined packs for your event type.')}
</Text>
@@ -937,7 +937,7 @@ export default function MobileEventTasksPage() {
) : null
}
iconAfter={
<XStack space="$1.5" alignItems="center">
<XStack gap="$1.5" alignItems="center">
<Button
size="$2"
backgroundColor={withAlpha(primary, 0.12)}
@@ -969,7 +969,7 @@ export default function MobileEventTasksPage() {
);
const emotionsPanel = (
<YStack space="$2">
<YStack gap="$2">
<XStack alignItems="center" justifyContent="space-between" flexWrap="wrap">
<Text fontSize="$sm" fontWeight="700" color={text}>
{t('events.tasks.tabs.emotions', 'Emotions')}
@@ -1002,12 +1002,12 @@ export default function MobileEventTasksPage() {
hoverTheme
pressTheme
title={
<XStack alignItems="center" space="$2">
<XStack alignItems="center" gap="$2">
<Tag label={emotion.name ?? ''} color={emotion.color ?? border} />
</XStack>
}
iconAfter={
<XStack space="$2">
<XStack gap="$2">
<Pressable
onPress={() => {
setEditingEmotion(emotion);
@@ -1039,7 +1039,7 @@ export default function MobileEventTasksPage() {
title={t('events.tasks.title', 'Photo tasks for guests')}
onBack={back}
headerActions={
<XStack space="$2">
<XStack gap="$2">
<HeaderActionButton onPress={() => load()} ariaLabel={t('common.refresh', 'Refresh')}>
<RefreshCcw size={18} color={text} />
</HeaderActionButton>
@@ -1061,7 +1061,7 @@ export default function MobileEventTasksPage() {
) : null}
{loading ? (
<YStack space="$2">
<YStack gap="$2">
{Array.from({ length: 4 }).map((_, idx) => (
<SkeletonCard key={`tsk-${idx}`} height={70} />
))}
@@ -1074,7 +1074,7 @@ export default function MobileEventTasksPage() {
backgroundColor={surface}
padding="$3"
>
<YStack space="$3">
<YStack gap="$3">
<Card
borderRadius={18}
borderWidth={1}
@@ -1082,8 +1082,8 @@ export default function MobileEventTasksPage() {
backgroundColor={surfaceMuted}
padding="$3"
>
<YStack space="$2">
<XStack alignItems="center" justifyContent="space-between" space="$2">
<YStack gap="$2">
<XStack alignItems="center" justifyContent="space-between" gap="$2">
<Text fontSize="$xs" fontWeight="700" color={text}>
{t('events.tasks.toggle.title', 'Photo tasks for guests')}
</Text>
@@ -1121,7 +1121,7 @@ export default function MobileEventTasksPage() {
<Text fontSize="$xs" fontWeight="700" color={text}>
{t('events.tasks.toggle.switchLabel', 'Photo task mode')}
</Text>
<XStack alignItems="center" space="$1.5">
<XStack alignItems="center" gap="$1.5">
<Switch
size="$3"
checked={tasksEnabled}
@@ -1205,7 +1205,7 @@ export default function MobileEventTasksPage() {
<Tabs.Content value="assigned" paddingTop="$2">
<Card borderRadius={18} borderWidth={1} borderColor={border} backgroundColor={surface} padding="$3">
<YStack space="$2">
<YStack gap="$2">
<Card
borderRadius={16}
borderWidth={1}
@@ -1213,7 +1213,7 @@ export default function MobileEventTasksPage() {
backgroundColor={surfaceMuted}
padding="$2.5"
>
<XStack alignItems="center" space="$2">
<XStack alignItems="center" gap="$2">
<XStack flex={1}>
<MobileInput
type="search"
@@ -1226,7 +1226,7 @@ export default function MobileEventTasksPage() {
<Pressable onPress={() => setShowEmotionFilterSheet(true)}>
<XStack
alignItems="center"
space="$1.5"
gap="$1.5"
paddingVertical="$2"
paddingHorizontal="$3"
borderRadius={14}
@@ -1286,7 +1286,7 @@ export default function MobileEventTasksPage() {
/>
}
>
<YStack space="$2">
<YStack gap="$2">
{!canAddTasks ? (
<Text fontSize={12} fontWeight="600" color={dangerText}>
{limitReachedMessage}
@@ -1338,7 +1338,7 @@ export default function MobileEventTasksPage() {
/>
}
>
<YStack space="$2">
<YStack gap="$2">
{!canAddTasks ? (
<Text fontSize={12} fontWeight="600" color={dangerText}>
{limitReachedMessage}
@@ -1381,7 +1381,7 @@ export default function MobileEventTasksPage() {
/>
}
>
<YStack space="$2">
<YStack gap="$2">
<MobileField label={t('events.tasks.emotionName', 'Name')}>
<MobileInput
type="text"
@@ -1416,8 +1416,8 @@ export default function MobileEventTasksPage() {
setShowEmotionFilterSheet(false);
}}
>
<YStack space="$2">
<XStack alignItems="center" space="$2">
<YStack gap="$2">
<XStack alignItems="center" gap="$2">
<RadioGroup.Item value="">
<RadioGroup.Indicator />
</RadioGroup.Item>
@@ -1426,7 +1426,7 @@ export default function MobileEventTasksPage() {
</Text>
</XStack>
{emotions.map((emotion) => (
<XStack key={`emo-filter-${emotion.id}`} alignItems="center" space="$2">
<XStack key={`emo-filter-${emotion.id}`} alignItems="center" gap="$2">
<RadioGroup.Item value={String(emotion.id)}>
<RadioGroup.Indicator />
</RadioGroup.Item>
@@ -1458,7 +1458,7 @@ export default function MobileEventTasksPage() {
maxWidth={420}
width="90%"
>
<YStack space="$3">
<YStack gap="$3">
<AlertDialog.Title>
<Text fontSize="$md" fontWeight="800" color={text}>
{t('events.tasks.removeTitle', 'Remove photo task?')}
@@ -1471,7 +1471,7 @@ export default function MobileEventTasksPage() {
: t('events.tasks.removeBodyFallback', 'This will remove the photo task from the event.')}
</Text>
</AlertDialog.Description>
<XStack space="$2" justifyContent="flex-end">
<XStack gap="$2" justifyContent="flex-end">
<AlertDialog.Cancel asChild>
<CTAButton
label={t('common.cancel', 'Cancel')}
@@ -1513,7 +1513,7 @@ export default function MobileEventTasksPage() {
maxWidth={420}
width="90%"
>
<YStack space="$3">
<YStack gap="$3">
<AlertDialog.Title>
<Text fontSize="$md" fontWeight="800" color={text}>
{t('events.tasks.bulkRemoveTitle', 'Auswahl löschen')}
@@ -1524,7 +1524,7 @@ export default function MobileEventTasksPage() {
{t('events.tasks.bulkRemoveBody', 'This will remove the selected photo tasks from the event.')}
</Text>
</AlertDialog.Description>
<XStack space="$2" justifyContent="flex-end">
<XStack gap="$2" justifyContent="flex-end">
<AlertDialog.Cancel asChild>
<CTAButton
label={t('common.cancel', 'Cancel')}

View File

@@ -136,7 +136,7 @@ export default function MobileEventsPage() {
shadowRadius={16}
shadowOffset={{ width: 0, height: 10 }}
>
<YStack space="$2.5">
<YStack gap="$2.5">
<XStack
alignItems="center"
paddingHorizontal="$3"
@@ -165,7 +165,7 @@ export default function MobileEventsPage() {
</Card>
{loading ? (
<YStack space="$2">
<YStack gap="$2">
{Array.from({ length: 3 }).map((_, idx) => (
<SkeletonCard key={`sk-${idx}`} height={90} />
))}
@@ -182,7 +182,7 @@ export default function MobileEventsPage() {
shadowRadius={16}
shadowOffset={{ width: 0, height: 10 }}
>
<YStack space="$2" alignItems="center">
<YStack gap="$2" alignItems="center">
<Text fontSize="$md" fontWeight="700">
{t('events.list.title')}
</Text>
@@ -263,7 +263,7 @@ function EventsList({
];
return (
<YStack space="$3">
<YStack gap="$3">
{filteredEvents.length === 0 ? (
<Card
borderRadius={22}
@@ -276,7 +276,7 @@ function EventsList({
shadowRadius={14}
shadowOffset={{ width: 0, height: 8 }}
>
<YStack space="$2" alignItems="center">
<YStack gap="$2" alignItems="center">
<Text fontSize="$sm" fontWeight="700" color={text}>
{t('events.list.empty.filtered')}
</Text>
@@ -303,7 +303,7 @@ function EventsList({
shadowRadius={14}
shadowOffset={{ width: 0, height: 8 }}
>
<YStack space="$2.5">
<YStack gap="$2.5">
<XStack alignItems="center" justifyContent="space-between">
<Text fontSize="$xs" fontWeight="800" color={text}>
{t('events.workspace.fields.status')}
@@ -326,7 +326,7 @@ function EventsList({
value={statusFilter}
onValueChange={(value: string) => value && onStatusChange(value as EventStatusKey)}
>
<XStack space="$1.5">
<XStack gap="$1.5">
{filters.map((filter) => {
const active = filter.key === statusFilter;
return (
@@ -340,7 +340,7 @@ function EventsList({
paddingVertical="$1.5"
paddingHorizontal="$3"
>
<XStack alignItems="center" space="$1.5">
<XStack alignItems="center" gap="$1.5">
<Text fontSize="$xs" fontWeight="700" color={active ? primary : muted}>
{filter.label}
</Text>
@@ -407,7 +407,7 @@ function EventsList({
/>
}
iconAfter={
<XStack alignItems="center" space="$1.5">
<XStack alignItems="center" gap="$1.5">
<Text fontSize="$xs" color={primary} fontWeight="700">
{t('events.list.actions.open')}
</Text>
@@ -448,12 +448,12 @@ function EventListItem({
const locale = i18n.language;
const stats = buildEventListStats(event);
return (
<YStack space="$1.5">
<XStack alignItems="center" justifyContent="space-between" space="$2">
<YStack gap="$1.5">
<XStack alignItems="center" justifyContent="space-between" gap="$2">
<Text fontSize="$md" fontWeight="800" color={text}>
{renderName(event.name, t)}
</Text>
<XStack alignItems="center" space="$1.5">
<XStack alignItems="center" gap="$1.5">
<PillBadge tone={statusTone}>{statusLabel}</PillBadge>
{onEdit ? (
<Pressable onPress={() => onEdit(event.slug)}>
@@ -464,21 +464,21 @@ function EventListItem({
) : null}
</XStack>
</XStack>
<XStack alignItems="center" space="$2" flexWrap="wrap">
<XStack alignItems="center" space="$1.5">
<XStack alignItems="center" gap="$2" flexWrap="wrap">
<XStack alignItems="center" gap="$1.5">
<CalendarDays size={12} color={subtle} />
<Text fontSize="$xs" color={muted}>
{formatDate(event.event_date, t, locale)}
</Text>
</XStack>
<XStack alignItems="center" space="$1.5">
<XStack alignItems="center" gap="$1.5">
<MapPin size={12} color={subtle} />
<Text fontSize="$xs" color={muted}>
{resolveLocation(event, t)}
</Text>
</XStack>
</XStack>
<XStack alignItems="center" space="$2" flexWrap="wrap">
<XStack alignItems="center" gap="$2" flexWrap="wrap">
<EventStatChip icon={Camera} label={t('events.list.stats.photos')} value={stats.photos} muted={subtle} />
<EventStatChip icon={Users} label={t('events.list.stats.guests')} value={stats.guests} muted={subtle} />
<EventStatChip icon={Sparkles} label={t('events.list.stats.tasks')} value={stats.tasks} muted={subtle} />
@@ -499,7 +499,7 @@ function EventStatChip({
muted: string;
}) {
return (
<XStack alignItems="center" space="$1">
<XStack alignItems="center" gap="$1">
<Icon size={12} color={muted} />
<Text fontSize="$xs" color={muted}>
{value} {label}

View File

@@ -76,10 +76,10 @@ export default function ForgotPasswordPage() {
paddingVertical="$5"
style={{ ...safeAreaStyle, background: ADMIN_GRADIENTS.loginBackground }}
>
<YStack width="100%" maxWidth={520} space="$4">
<YStack width="100%" maxWidth={520} gap="$4">
<MobileCard backgroundColor="rgba(15, 23, 42, 0.6)" borderColor="rgba(255,255,255,0.12)">
<YStack space="$2">
<XStack alignItems="center" space="$2">
<YStack gap="$2">
<XStack alignItems="center" gap="$2">
<XStack
width={42}
height={42}
@@ -103,7 +103,7 @@ export default function ForgotPasswordPage() {
</MobileCard>
<MobileCard backgroundColor={surface} borderColor={border}>
<YStack space="$3">
<YStack gap="$3">
<MobileField label={t('login.email', 'Email address')}>
<MobileInput
type="email"

View File

@@ -34,7 +34,7 @@ export default function MobileHelpArticlePage() {
return (
<MobileShell activeTab="profile" title={article?.title ?? t('common.help', 'Help')} onBack={back}>
{isLoading ? (
<YStack space="$2">
<YStack gap="$2">
<SkeletonCard height={120} />
<SkeletonCard height={160} />
</YStack>
@@ -42,7 +42,7 @@ export default function MobileHelpArticlePage() {
{isError ? (
<MobileCard>
<YStack space="$2">
<YStack gap="$2">
<Text fontSize="$sm" fontWeight="700" color={theme.textStrong}>
{t('dashboard:help.error', 'Help could not be loaded.')}
</Text>
@@ -56,9 +56,9 @@ export default function MobileHelpArticlePage() {
) : null}
{!isLoading && article ? (
<YStack space="$3">
<YStack gap="$3">
<MobileCard>
<YStack space="$2">
<YStack gap="$2">
<Text fontSize="$lg" fontWeight="800" color={theme.textStrong}>
{article.title}
</Text>
@@ -81,7 +81,7 @@ export default function MobileHelpArticlePage() {
{article.related && article.related.length > 0 ? (
<MobileCard>
<YStack space="$2">
<YStack gap="$2">
<Text fontSize="$sm" fontWeight="700" color={theme.textStrong}>
{t('help.article.relatedTitle', 'Weitere Artikel')}
</Text>

View File

@@ -41,7 +41,7 @@ export default function MobileHelpCenterPage() {
return (
<MobileShell activeTab="profile" title={t('common.help', 'Help')} onBack={back}>
{isLoading ? (
<YStack space="$2">
<YStack gap="$2">
<SkeletonCard height={120} />
<SkeletonCard height={120} />
</YStack>
@@ -49,7 +49,7 @@ export default function MobileHelpCenterPage() {
{isError ? (
<MobileCard>
<YStack space="$2">
<YStack gap="$2">
<Text fontSize="$sm" fontWeight="700" color={theme.textStrong}>
{t('dashboard:help.error', 'Help could not be loaded.')}
</Text>
@@ -63,7 +63,7 @@ export default function MobileHelpCenterPage() {
) : null}
{!isLoading && !isError ? (
<YStack space="$3">
<YStack gap="$3">
<HelpSection
title={t('dashboard:help.title', 'FAQ')}
icon={HelpCircle}
@@ -100,8 +100,8 @@ function HelpSection({
return (
<MobileCard padding="$0">
<YStack padding="$3" space="$2">
<XStack alignItems="center" space="$2">
<YStack padding="$3" gap="$2">
<XStack alignItems="center" gap="$2">
{IconCmp ? (
<XStack
width={28}

View File

@@ -202,9 +202,9 @@ export default function MobileLoginPage() {
paddingVertical="$5"
style={{ ...safeAreaStyle, background: ADMIN_GRADIENTS.loginBackground }}
>
<YStack width="100%" maxWidth={520} space="$4">
<YStack width="100%" maxWidth={520} gap="$4">
<MobileCard backgroundColor="rgba(15, 23, 42, 0.6)" borderColor="rgba(255,255,255,0.12)">
<YStack alignItems="center" space="$3">
<YStack alignItems="center" gap="$3">
<XStack
width={56}
height={56}
@@ -217,7 +217,7 @@ export default function MobileLoginPage() {
>
<img src="/logo-transparent-md.png" alt={t('auth.logoAlt', 'Fotospiel')} width={40} height={40} />
</XStack>
<YStack alignItems="center" space="$1">
<YStack alignItems="center" gap="$1">
<Text fontSize="$xl" fontWeight="800" color="white" textAlign="center">
{t('login.panel_title', 'Fotospiel.App Event Login')}
</Text>
@@ -230,7 +230,7 @@ export default function MobileLoginPage() {
<MobileCard backgroundColor={surface} borderColor={border}>
<form onSubmit={handleSubmit}>
<YStack space="$3">
<YStack gap="$3">
{oauthMessage ? (
<YStack
borderRadius={12}
@@ -313,7 +313,7 @@ export default function MobileLoginPage() {
pressStyle={{ opacity: 0.9 }}
style={{ boxShadow: '0 12px 24px rgba(255, 90, 95, 0.25)' }}
>
<XStack alignItems="center" space="$2">
<XStack alignItems="center" gap="$2">
<Loader2 size={16} className={isSubmitting ? 'animate-spin' : ''} />
<Text fontSize="$sm" color="white" fontWeight="800">
{isSubmitting ? t('login.loading', 'Anmeldung …') : t('login.submit', 'Anmelden')}
@@ -332,7 +332,7 @@ export default function MobileLoginPage() {
borderWidth={1}
color={text}
>
<XStack alignItems="center" space="$2">
<XStack alignItems="center" gap="$2">
{isRedirectingToGoogle ? (
<Loader2 size={16} className="animate-spin" />
) : (
@@ -355,7 +355,7 @@ export default function MobileLoginPage() {
borderWidth={1}
color={text}
>
<XStack alignItems="center" space="$2">
<XStack alignItems="center" gap="$2">
{isRedirectingToFacebook ? (
<Loader2 size={16} className="animate-spin" />
) : (
@@ -380,7 +380,7 @@ export default function MobileLoginPage() {
}}
/>
<YStack alignItems="center" space="$2">
<YStack alignItems="center" gap="$2">
<Text fontSize="$xs" color={muted} textAlign="center">
{t('login.support', 'Fragen? Schreib uns an support@fotospiel.de oder antworte direkt auf deine Einladung.')}
</Text>

View File

@@ -52,7 +52,7 @@ export default function LoginStartPage(): React.ReactElement {
shadowRadius={16}
shadowOffset={{ width: 0, height: 10 }}
>
<YStack alignItems="center" space="$2">
<YStack alignItems="center" gap="$2">
<Spinner size="small" color={textStrong} />
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{t('redirecting', 'Redirecting to login …')}

View File

@@ -43,7 +43,7 @@ export default function LogoutPage() {
shadowRadius={16}
shadowOffset={{ width: 0, height: 10 }}
>
<YStack alignItems="center" space="$2">
<YStack alignItems="center" gap="$2">
<Spinner size="small" color={textStrong} />
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
Abmeldung wird vorbereitet ...

View File

@@ -86,13 +86,13 @@ function NotificationSwipeRow({ item, onOpen, onMarkRead, children }: Notificati
pointerEvents="none"
style={{ position: 'absolute', top: 0, right: 0, bottom: 0, left: 0 }}
>
<XStack alignItems="center" space="$2">
<XStack alignItems="center" gap="$2">
<Check size={16} color={markText} />
<Text fontSize="$xs" fontWeight="700" color={markText}>
{item.is_read ? t('notificationLogs.read', 'Read') : t('notificationLogs.markRead', 'Mark read')}
</Text>
</XStack>
<XStack alignItems="center" space="$2">
<XStack alignItems="center" gap="$2">
<Text fontSize="$xs" fontWeight="700" color={detailText}>
Details
</Text>
@@ -508,7 +508,7 @@ export default function MobileNotificationsPage() {
) : null}
{showFilterNotice ? (
<MobileCard space="$2">
<MobileCard gap="$2">
<Text fontSize="$sm" fontWeight="700" color={text}>
{t('notificationLogs.filterEmpty', 'No notifications for this event.')}
</Text>
@@ -524,7 +524,7 @@ export default function MobileNotificationsPage() {
</MobileCard>
) : null}
<XStack space="$2" marginBottom="$2">
<XStack gap="$2" marginBottom="$2">
<MobileSelect
value={statusParam}
onChange={(e) => updateFilters({ status: e.target.value })}
@@ -552,7 +552,7 @@ export default function MobileNotificationsPage() {
) : null}
</XStack>
<XStack space="$2" flexWrap="wrap" marginBottom="$2">
<XStack gap="$2" flexWrap="wrap" marginBottom="$2">
{([
{ key: 'all', label: t('notificationLogs.scope.all', 'All scopes') },
{ key: 'photos', label: t('notificationLogs.scope.photos', 'Photos') },
@@ -585,13 +585,13 @@ export default function MobileNotificationsPage() {
</XStack>
{loading ? (
<YStack space="$2">
<YStack gap="$2">
{Array.from({ length: 4 }).map((_, idx) => (
<SkeletonCard key={`al-${idx}`} height={70} />
))}
</YStack>
) : statusFiltered.length === 0 ? (
<MobileCard alignItems="center" justifyContent="center" space="$2">
<MobileCard alignItems="center" justifyContent="center" gap="$2">
<Bell size={24} color={subtle} />
<Text fontSize="$sm" fontWeight="700" color={text}>
{t('mobileNotifications.emptyTitle', 'All caught up')}
@@ -607,7 +607,7 @@ export default function MobileNotificationsPage() {
/>
</MobileCard>
) : (
<YStack space="$2">
<YStack gap="$2">
{events.length ? (
<Pressable onPress={() => setShowEventPicker(true)}>
<Text fontSize="$sm" color={primary} fontWeight="700">
@@ -616,12 +616,12 @@ export default function MobileNotificationsPage() {
</Pressable>
) : null}
{grouped.map((group) => (
<YStack key={group.scope} space="$2">
<YStack key={group.scope} gap="$2">
<XStack alignItems="center" justifyContent="space-between">
<Text fontSize="$xs" fontWeight="700" color={muted}>
{t(`notificationLogs.scope.${group.scope}`, group.scope)}
</Text>
<XStack space="$2" alignItems="center">
<XStack gap="$2" alignItems="center">
{group.unread > 0 ? (
<Pressable onPress={() => markGroupRead(group)}>
<Text fontSize="$xs" color={primary} fontWeight="700">
@@ -646,8 +646,8 @@ export default function MobileNotificationsPage() {
onOpen={openNotification}
onMarkRead={markNotificationRead}
>
<MobileCard space="$2" borderColor={item.is_read ? border : primary}>
<XStack alignItems="center" space="$2">
<MobileCard gap="$2" borderColor={item.is_read ? border : primary}>
<XStack alignItems="center" gap="$2">
<XStack
width={36}
height={36}
@@ -658,7 +658,7 @@ export default function MobileNotificationsPage() {
>
<Bell size={18} color={item.tone === 'warning' ? warningIcon : infoIcon} />
</XStack>
<YStack space="$0.5" flex={1}>
<YStack gap="$0.5" flex={1}>
<Text fontSize="$sm" fontWeight="700" color={text}>
{item.title}
</Text>
@@ -699,14 +699,14 @@ export default function MobileNotificationsPage() {
}
>
{selectedNotification ? (
<YStack space="$2">
<YStack gap="$2">
<Text fontSize="$sm" color={text} fontWeight="700">
{selectedNotification.title}
</Text>
<Text fontSize="$sm" color={muted}>
{selectedNotification.body}
</Text>
<XStack space="$2" flexWrap="wrap" style={{ rowGap: 8 }}>
<XStack gap="$2" flexWrap="wrap" style={{ rowGap: 8 }}>
<PillBadge tone={selectedNotification.tone === 'warning' ? 'warning' : 'muted'}>
{selectedNotification.scope}
</PillBadge>
@@ -725,7 +725,7 @@ export default function MobileNotificationsPage() {
title={t('mobileNotifications.filterByEvent', 'Nach Event filtern')}
footer={null}
>
<YStack space="$2">
<YStack gap="$2">
{events.length === 0 ? (
<Text fontSize="$sm" color={muted}>
{t('events.list.empty.description', 'Starte jetzt mit deinem ersten Event.')}

View File

@@ -65,7 +65,7 @@ export default function MobilePackageShopPage() {
onBack={() => navigate(-1)}
activeTab="profile"
>
<YStack space="$3">
<YStack gap="$3">
<SkeletonCard height={150} />
<SkeletonCard height={150} />
</YStack>
@@ -125,10 +125,10 @@ export default function MobilePackageShopPage() {
onBack={() => navigate(-1)}
activeTab="profile"
>
<YStack space="$4">
<YStack gap="$4">
{catalogType !== 'reseller' && recommendedFeature && (
<MobileCard borderColor={primary} backgroundColor={accentSoft} space="$2" padding="$3">
<XStack space="$2" alignItems="center">
<MobileCard borderColor={primary} backgroundColor={accentSoft} gap="$2" padding="$3">
<XStack gap="$2" alignItems="center">
<Sparkles size={16} color={primary} />
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{t('shop.recommendationTitle', 'Recommended for you')}
@@ -149,7 +149,7 @@ export default function MobilePackageShopPage() {
</YStack>
{packageEntries.length > 1 ? (
<XStack space="$2" paddingHorizontal="$2">
<XStack gap="$2" paddingHorizontal="$2">
<CTAButton
label={t('shop.compare.toggleCards', 'Cards')}
tone={viewMode === 'cards' ? 'primary' : 'ghost'}
@@ -167,7 +167,7 @@ export default function MobilePackageShopPage() {
</XStack>
) : null}
<YStack space="$3">
<YStack gap="$3">
{viewMode === 'compare' ? (
<PackageShopCompareView
entries={packageEntries}
@@ -234,14 +234,14 @@ function PackageShopCard({
onPress={handlePress}
borderColor={isRecommended ? primary : (isActive ? '$green8' : border)}
borderWidth={isRecommended || isActive ? 2 : 1}
space="$3"
gap="$3"
pressStyle={handlePress ? { backgroundColor: accentSoft } : undefined}
backgroundColor={isActive ? '$green1' : undefined}
style={{ opacity: isSubdued ? 0.6 : 1 }}
>
<XStack justifyContent="space-between" alignItems="flex-start">
<YStack space="$1">
<XStack space="$2" alignItems="center">
<YStack gap="$1">
<XStack gap="$2" alignItems="center">
<Text fontSize="$lg" fontWeight="800" color={textStrong}>
{pkg.name}
</Text>
@@ -255,7 +255,7 @@ function PackageShopCard({
{!isResellerCatalog && isActive ? <PillBadge tone="success">{t('shop.badges.active', 'Active')}</PillBadge> : null}
</XStack>
<XStack space="$2" alignItems="center">
<XStack gap="$2" alignItems="center">
<Text fontSize="$md" color={primary} fontWeight="700">
{new Intl.NumberFormat(undefined, { style: 'currency', currency: 'EUR' }).format(pkg.price)}
</Text>
@@ -271,7 +271,7 @@ function PackageShopCard({
</YStack>
</XStack>
<YStack space="$1.5">
<YStack gap="$1.5">
{isResellerCatalog ? (
<>
{includedTierLabel ? (
@@ -333,7 +333,7 @@ function PackageShopCard({
function FeatureRow({ label }: { label: string }) {
const { textStrong, primary } = useAdminTheme();
return (
<XStack alignItems="center" space="$2">
<XStack alignItems="center" gap="$2">
<Check size={14} color={primary} />
<Text fontSize="$sm" color={textStrong}>{label}</Text>
</XStack>
@@ -411,8 +411,8 @@ function PackageShopCompareView({
};
return (
<MobileCard space="$3" borderColor={border}>
<YStack space="$1">
<MobileCard gap="$3" borderColor={border}>
<YStack gap="$1">
<Text fontSize="$md" fontWeight="700" color={textStrong}>
{t('shop.compare.title', 'Compare plans')}
</Text>
@@ -422,7 +422,7 @@ function PackageShopCompareView({
</YStack>
<XStack style={{ overflowX: 'auto' }}>
<YStack space="$1.5" minWidth={labelWidth + columnWidth * entries.length}>
<YStack gap="$1.5" minWidth={labelWidth + columnWidth * entries.length}>
{rows.map((row) => (
<XStack key={row.id} borderBottomWidth={1} borderColor={border}>
<YStack
@@ -443,11 +443,11 @@ function PackageShopCompareView({
if (row.id === 'meta.plan') {
const statusLabel = getPackageStatusLabel({ t, isActive: entry.isActive, owned: entry.owned });
content = (
<YStack space="$1">
<YStack gap="$1">
<Text fontSize="$sm" fontWeight="800" color={textStrong}>
{entry.pkg.name}
</Text>
<XStack space="$1.5" flexWrap="wrap">
<XStack gap="$1.5" flexWrap="wrap">
{entry.isRecommended ? (
<PillBadge tone="warning">{t('shop.badges.recommended', 'Recommended')}</PillBadge>
) : null}
@@ -492,7 +492,7 @@ function PackageShopCompareView({
} else if (row.type === 'feature') {
const enabled = getEnabledPackageFeatures(entry.pkg).includes(row.featureKey);
content = (
<XStack alignItems="center" space="$1.5">
<XStack alignItems="center" gap="$1.5">
{enabled ? (
<Check size={16} color={primary} />
) : (
@@ -607,8 +607,8 @@ function CheckoutConfirmation({ pkg, onCancel }: { pkg: Package; onCancel: () =>
return (
<MobileShell title={t('shop.confirmTitle', 'Confirm Purchase')} onBack={onCancel} activeTab="profile">
<YStack space="$4">
<MobileCard space="$2" borderColor={border}>
<YStack gap="$4">
<MobileCard gap="$2" borderColor={border}>
<Text fontSize="$sm" color={muted}>{subtitle}</Text>
<Text fontSize="$xl" fontWeight="800" color={textStrong}>{pkg.name}</Text>
<Text fontSize="$lg" color={primary} fontWeight="700">
@@ -616,13 +616,13 @@ function CheckoutConfirmation({ pkg, onCancel }: { pkg: Package; onCancel: () =>
</Text>
</MobileCard>
<MobileCard space="$3" borderColor={border}>
<XStack alignItems="center" space="$2">
<MobileCard gap="$3" borderColor={border}>
<XStack alignItems="center" gap="$2">
<ShieldCheck size={18} color={textStrong} />
<Text fontSize="$md" fontWeight="700" color={textStrong}>{t('shop.legalTitle', 'Terms & Conditions')}</Text>
</XStack>
<XStack space="$3" alignItems="flex-start">
<XStack gap="$3" alignItems="flex-start">
<Checkbox
id="agb"
size="$4"
@@ -638,7 +638,7 @@ function CheckoutConfirmation({ pkg, onCancel }: { pkg: Package; onCancel: () =>
</Text>
</XStack>
<XStack space="$3" alignItems="flex-start">
<XStack gap="$3" alignItems="flex-start">
<Checkbox
id="withdrawal"
size="$4"
@@ -655,7 +655,7 @@ function CheckoutConfirmation({ pkg, onCancel }: { pkg: Package; onCancel: () =>
</XStack>
</MobileCard>
<YStack space="$2">
<YStack gap="$2">
<CTAButton
label={busy ? t('shop.processing', 'Processing...') : t('shop.payNow', 'Pay Now')}
onPress={handleCheckout}

View File

@@ -306,7 +306,7 @@ export default function MobileProfileAccountPage() {
onBack={back}
>
{brandingTabEnabled ? (
<XStack space="$2">
<XStack gap="$2">
<TabButton
label={t('profile.tabs.account', 'Account')}
active={activeTab === 'account'}
@@ -330,7 +330,7 @@ export default function MobileProfileAccountPage() {
</MobileCard>
) : null}
<MobileCard space="$3">
<MobileCard gap="$3">
<Text fontSize="$md" fontWeight="800" color={text}>
{t('profile.branding.title', 'Standard-Branding')}
</Text>
@@ -339,11 +339,11 @@ export default function MobileProfileAccountPage() {
</Text>
</MobileCard>
<MobileCard space="$3">
<MobileCard gap="$3">
<Text fontSize="$md" fontWeight="800" color={text}>
{t('profile.branding.theme', 'Theme')}
</Text>
<YStack space="$3">
<YStack gap="$3">
<MobileField label={t('events.branding.mode', 'Theme')}>
<MobileSelect
value={brandingForm.mode}
@@ -369,11 +369,11 @@ export default function MobileProfileAccountPage() {
</YStack>
</MobileCard>
<MobileCard space="$3">
<MobileCard gap="$3">
<Text fontSize="$md" fontWeight="800" color={text}>
{t('events.branding.colors', 'Colors')}
</Text>
<YStack space="$3">
<YStack gap="$3">
<ColorField
label={t('events.branding.primary', 'Primary Color')}
value={brandingForm.primary}
@@ -401,11 +401,11 @@ export default function MobileProfileAccountPage() {
</YStack>
</MobileCard>
<MobileCard space="$3">
<MobileCard gap="$3">
<Text fontSize="$md" fontWeight="800" color={text}>
{t('events.branding.fonts', 'Fonts')}
</Text>
<YStack space="$3">
<YStack gap="$3">
<MobileField label={t('events.branding.headingFont', 'Headline Font')}>
<MobileInput
value={brandingForm.headingFont}
@@ -444,8 +444,8 @@ export default function MobileProfileAccountPage() {
</MobileCard>
) : null}
<MobileCard space="$3">
<XStack alignItems="center" space="$3">
<MobileCard gap="$3">
<XStack alignItems="center" gap="$3">
<XStack
width={48}
height={48}
@@ -456,7 +456,7 @@ export default function MobileProfileAccountPage() {
>
<User size={20} color={primary} />
</XStack>
<YStack space="$1">
<YStack gap="$1">
<Text fontSize="$md" fontWeight="800" color={text}>
{form.name || profile?.email || t('profile.title', 'Profil')}
</Text>
@@ -465,7 +465,7 @@ export default function MobileProfileAccountPage() {
</Text>
</YStack>
</XStack>
<XStack alignItems="center" space="$2" flexWrap="wrap">
<XStack alignItems="center" gap="$2" flexWrap="wrap">
{profile?.email_verified ? (
<CheckCircle2 size={14} color={subtle} />
) : (
@@ -480,8 +480,8 @@ export default function MobileProfileAccountPage() {
</XStack>
</MobileCard>
<MobileCard space="$3">
<XStack alignItems="center" space="$2">
<MobileCard gap="$3">
<XStack alignItems="center" gap="$2">
<User size={16} color={text} />
<Text fontSize="$md" fontWeight="800" color={text}>
{t('profile.sections.account.heading', 'Account-Informationen')}
@@ -495,7 +495,7 @@ export default function MobileProfileAccountPage() {
{t('profile.loading', 'Lädt ...')}
</Text>
) : (
<YStack space="$3">
<YStack gap="$3">
<MobileField label={t('profile.fields.name', 'Anzeigename')}>
<MobileInput
value={form.name}
@@ -535,8 +535,8 @@ export default function MobileProfileAccountPage() {
)}
</MobileCard>
<MobileCard space="$3">
<XStack alignItems="center" space="$2">
<MobileCard gap="$3">
<XStack alignItems="center" gap="$2">
<Lock size={16} color={text} />
<Text fontSize="$md" fontWeight="800" color={text}>
{t('profile.sections.password.heading', 'Passwort ändern')}
@@ -545,7 +545,7 @@ export default function MobileProfileAccountPage() {
<Text fontSize="$sm" color={muted}>
{t('profile.sections.password.description', 'Wähle ein sicheres Passwort, um deinen Admin-Zugang zu schützen.')}
</Text>
<YStack space="$3">
<YStack gap="$3">
<MobileField label={t('profile.fields.currentPassword', 'Aktuelles Passwort')}>
<MobileInput
value={form.currentPassword}
@@ -625,11 +625,11 @@ function ColorField({
}) {
const { text, muted } = useAdminTheme();
return (
<YStack space="$2" opacity={disabled ? 0.6 : 1}>
<YStack gap="$2" opacity={disabled ? 0.6 : 1}>
<Text fontSize="$sm" fontWeight="700" color={text}>
{label}
</Text>
<XStack alignItems="center" space="$2">
<XStack alignItems="center" gap="$2">
<MobileColorInput
value={value}
onChange={(event) => onChange(event.target.value)}

View File

@@ -68,13 +68,13 @@ export default function MobileProfilePage() {
shadowRadius={16}
shadowOffset={{ width: 0, height: 10 }}
>
<YStack space="$2.5" alignItems="center">
<YStack gap="$2.5" alignItems="center">
<Avatar size="$7" borderRadius={20} backgroundColor={avatarBg}>
<Avatar.Fallback>
<User size={28} color={primary} />
</Avatar.Fallback>
</Avatar>
<YStack space="$0.5" alignItems="center">
<YStack gap="$0.5" alignItems="center">
<Text fontSize="$md" fontWeight="800" color={textColor}>
{name}
</Text>
@@ -101,7 +101,7 @@ export default function MobileProfilePage() {
shadowRadius={16}
shadowOffset={{ width: 0, height: 10 }}
>
<YStack space="$2.5">
<YStack gap="$2.5">
<XStack
alignItems="center"
paddingHorizontal="$3"
@@ -210,7 +210,7 @@ export default function MobileProfilePage() {
shadowRadius={16}
shadowOffset={{ width: 0, height: 10 }}
>
<YStack space="$2.5">
<YStack gap="$2.5">
<XStack
alignItems="center"
paddingHorizontal="$3"
@@ -230,7 +230,7 @@ export default function MobileProfilePage() {
paddingVertical="$2"
paddingHorizontal="$3"
title={
<XStack space="$2" alignItems="center">
<XStack gap="$2" alignItems="center">
<Globe size={16} color={muted} />
<Text fontSize="$sm" color={textColor}>
{t('mobileProfile.language', 'Language')}
@@ -259,7 +259,7 @@ export default function MobileProfilePage() {
paddingVertical="$2"
paddingHorizontal="$3"
title={
<XStack space="$2" alignItems="center">
<XStack gap="$2" alignItems="center">
<Moon size={16} color={muted} />
<Text fontSize="$sm" color={textColor}>
{t('mobileProfile.theme', 'Theme')}
@@ -295,8 +295,8 @@ export default function MobileProfilePage() {
shadowRadius={12}
shadowOffset={{ width: 0, height: 6 }}
>
<YStack space="$2">
<XStack alignItems="center" space="$2">
<YStack gap="$2">
<XStack alignItems="center" gap="$2">
<XStack
width={36}
height={36}

View File

@@ -54,10 +54,10 @@ export default function PublicHelpPage() {
paddingVertical="$4"
style={{ ...safeAreaStyle, background: ADMIN_GRADIENTS.loginBackground }}
>
<YStack width="100%" maxWidth={680} alignSelf="center" space="$4">
<YStack width="100%" maxWidth={680} alignSelf="center" gap="$4">
<MobileCard backgroundColor="rgba(15, 23, 42, 0.55)" borderColor="rgba(255,255,255,0.08)">
<YStack space="$3">
<XStack alignItems="center" space="$2">
<YStack gap="$3">
<XStack alignItems="center" gap="$2">
<XStack
width={42}
height={42}
@@ -82,8 +82,8 @@ export default function PublicHelpPage() {
</MobileCard>
<MobileCard backgroundColor={surface} borderColor={border}>
<YStack space="$3">
<XStack alignItems="center" space="$2">
<YStack gap="$3">
<XStack alignItems="center" gap="$2">
<XStack
width={36}
height={36}
@@ -98,7 +98,7 @@ export default function PublicHelpPage() {
{t('login.help_faq_title', 'Haeufige Fragen vor dem Login')}
</Text>
</XStack>
<YStack space="$2">
<YStack gap="$2">
{faqItems.map((item, index) => (
<YStack
key={`${item.question}-${index}`}
@@ -107,7 +107,7 @@ export default function PublicHelpPage() {
borderWidth={1}
borderColor={border}
backgroundColor="rgba(255,255,255,0.6)"
space="$1"
gap="$1"
>
<Text fontSize="$sm" fontWeight="700" color={text}>
{item.question}
@@ -122,8 +122,8 @@ export default function PublicHelpPage() {
</MobileCard>
<MobileCard backgroundColor={surface} borderColor={border}>
<YStack space="$3">
<XStack alignItems="center" space="$2">
<YStack gap="$3">
<XStack alignItems="center" gap="$2">
<XStack
width={36}
height={36}

View File

@@ -262,7 +262,7 @@ export default function MobileQrLayoutCustomizePage() {
<Stepper current={step} onStepChange={setStep} />
<MobileCard space="$2" marginTop="$2">
<MobileCard gap="$2" marginTop="$2">
{step === 'background' && (
<BackgroundStep
onBack={back}
@@ -330,8 +330,8 @@ function Stepper({ current, onStepChange }: { current: Step; onStepChange: (step
const progress = ((currentIndex + 1) / steps.length) * 100;
return (
<YStack space="$2" marginTop="$2">
<XStack space="$2" alignItems="center">
<YStack gap="$2" marginTop="$2">
<XStack gap="$2" alignItems="center">
{steps.map((step, idx) => {
const active = step.key === current;
const completed = idx < currentIndex;
@@ -800,10 +800,10 @@ function BackgroundStep({
];
return (
<YStack space="$3" marginTop="$2">
<XStack alignItems="center" space="$2">
<YStack gap="$3" marginTop="$2">
<XStack alignItems="center" gap="$2">
<Pressable onPress={onBack}>
<XStack alignItems="center" space="$1">
<XStack alignItems="center" gap="$1">
<ArrowLeft size={16} color={textStrong} />
<Text fontSize="$sm" color={textStrong}>
{t('common.back', 'Zurück')}
@@ -813,7 +813,7 @@ function BackgroundStep({
<PillBadge tone="muted">{formatLabel}</PillBadge>
</XStack>
<YStack space="$2">
<YStack gap="$2">
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{disablePresets
? t('events.qr.backgroundPickerFoldable', 'Hintergrund für A5 (Gradient/Farbe)')
@@ -859,7 +859,7 @@ function BackgroundStep({
</>
) : null}
<YStack space="$2" marginTop="$2">
<YStack gap="$2" marginTop="$2">
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{t('events.qr.gradients', 'Gradienten')}
</Text>
@@ -894,7 +894,7 @@ function BackgroundStep({
</XStack>
</YStack>
<YStack space="$2" marginTop="$2">
<YStack gap="$2" marginTop="$2">
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{t('events.qr.colors', 'Vollfarbe')}
</Text>
@@ -966,10 +966,10 @@ function TextStep({
};
return (
<YStack space="$3" marginTop="$2">
<XStack alignItems="center" space="$2">
<YStack gap="$3" marginTop="$2">
<XStack alignItems="center" gap="$2">
<Pressable onPress={onBack}>
<XStack alignItems="center" space="$1">
<XStack alignItems="center" gap="$1">
<ArrowLeft size={16} color={textStrong} />
<Text fontSize="$sm" color={textStrong}>
{t('common.back', 'Zurück')}
@@ -979,7 +979,7 @@ function TextStep({
<PillBadge tone="muted">{t('events.qr.textStep', 'Texte & Hinweise')}</PillBadge>
</XStack>
<YStack space="$2">
<YStack gap="$2">
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{t('events.qr.textFields', 'Texte')}
</Text>
@@ -1000,12 +1000,12 @@ function TextStep({
/>
</YStack>
<YStack space="$2">
<YStack gap="$2">
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{t('events.qr.instructions', 'Anleitung')}
</Text>
{textFields.instructions.map((item, idx) => (
<XStack key={idx} alignItems="center" space="$2">
<XStack key={idx} alignItems="center" gap="$2">
<MobileTextArea
value={item}
onChange={(event) => updateInstruction(idx, event.target.value)}
@@ -1196,10 +1196,10 @@ function PreviewStep({
const qrImageSrc = qrPngUrl ?? qrUrl ?? '';
return (
<YStack space="$3" marginTop="$2">
<XStack alignItems="center" space="$2">
<YStack gap="$3" marginTop="$2">
<XStack alignItems="center" gap="$2">
<Pressable onPress={onBack}>
<XStack alignItems="center" space="$1">
<XStack alignItems="center" gap="$1">
<ArrowLeft size={16} color={textStrong} />
<Text fontSize="$sm" color={textStrong}>
{t('common.back', 'Zurück')}
@@ -1211,7 +1211,7 @@ function PreviewStep({
</PillBadge>
</XStack>
<YStack space="$2">
<YStack gap="$2">
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{t('events.qr.preview', 'Vorschau')}
</Text>
@@ -1243,7 +1243,7 @@ function PreviewStep({
<LayoutControls slots={resolvedSlots} slotOverrides={slotOverrides} onUpdateSlot={onUpdateSlot} tenantFonts={tenantFonts} qrUrl={qrImageSrc} />
<XStack space="$2" width="100%" flexWrap="wrap">
<XStack gap="$2" width="100%" flexWrap="wrap">
<CTAButton
label={t('events.qr.exportPdf', 'Export PDF')}
onPress={async () => {
@@ -1318,7 +1318,7 @@ function LayoutControls({
const decimals = step % 1 === 0 ? 0 : Math.min(4, `${step}`.split('.')[1]?.length ?? 1);
const formatValue = (val: number) => val.toFixed(decimals);
return (
<XStack space="$1" alignItems="center">
<XStack gap="$1" alignItems="center">
<Pressable onPress={dec}>
<XStack width={36} height={36} borderRadius={10} alignItems="center" justifyContent="center" borderWidth={1} borderColor={border} backgroundColor={surface}>
<Text fontSize="$md" fontWeight="800" color={textStrong}>
@@ -1398,13 +1398,13 @@ function LayoutControls({
</XStack>
</Accordion.Trigger>
<Accordion.Content {...({ paddingTop: "$2" } as any)}>
<YStack space="$2" padding="$2" borderWidth={1} borderColor={border} borderRadius={12}>
<XStack space="$3">
<YStack flex={1} space="$1">
<YStack gap="$2" padding="$2" borderWidth={1} borderColor={border} borderRadius={12}>
<XStack gap="$3">
<YStack flex={1} gap="$1">
<Text fontSize="$xs" color={muted}>
{t('events.qr.positionX', 'X (%)')}
</Text>
<XStack space="$2" alignItems="center">
<XStack gap="$2" alignItems="center">
<StepperInput
value={currentX * 100}
min={0}
@@ -1428,7 +1428,7 @@ function LayoutControls({
</Pressable>
</XStack>
</YStack>
<YStack flex={1} space="$1">
<YStack flex={1} gap="$1">
<Text fontSize="$xs" color={muted}>
{t('events.qr.positionY', 'Y (%)')}
</Text>
@@ -1440,7 +1440,7 @@ function LayoutControls({
onChange={(val) => onPercentChange('y')(val)}
/>
</YStack>
<YStack flex={1} space="$1">
<YStack flex={1} gap="$1">
<Text fontSize="$xs" color={muted}>
{t('events.qr.width', 'Breite (%)')}
</Text>
@@ -1454,14 +1454,14 @@ function LayoutControls({
</YStack>
</XStack>
<XStack space="$3">
<YStack flex={1} space="$1">
<XStack gap="$3">
<YStack flex={1} gap="$1">
<Text fontSize="$xs" color={muted}>
{t('events.qr.fontSize', 'Font Size (px)')}
</Text>
<StepperInput value={override.fontSize ?? slot.fontSize ?? 16} min={8} max={200} step={1} onChange={(val) => onFontSizeChange(val)} />
</YStack>
<YStack flex={1} space="$1">
<YStack flex={1} gap="$1">
<Text fontSize="$xs" color={muted}>
{t('events.qr.fontFamily', 'Font Family')}
</Text>
@@ -1477,12 +1477,12 @@ function LayoutControls({
))}
</MobileSelect>
</YStack>
<YStack flex={1} space="$1">
<YStack flex={1} gap="$1">
<Text fontSize="$xs" color={muted}>
{t('events.qr.fontColor', 'Schriftfarbe')}
</Text>
<YStack space="$2">
<XStack space="$2" alignItems="center">
<YStack gap="$2">
<XStack gap="$2" alignItems="center">
<Pressable onPress={() => setOpenColorSlot(openColorSlot === slotKey ? null : slotKey)}>
<XStack
width={48}
@@ -1536,7 +1536,7 @@ function LayoutControls({
onChange={(val) => onUpdateSlot(slotKey, { color: val })}
style={{ width: 240, height: 200 }}
/>
<XStack space="$2" justifyContent="flex-end">
<XStack gap="$2" justifyContent="flex-end">
<Pressable onPress={() => setOpenColorSlot(null)}>
<XStack
paddingHorizontal="$3"
@@ -1560,8 +1560,8 @@ function LayoutControls({
</YStack>
</XStack>
<XStack space="$2">
<YStack flex={1} space="$1">
<XStack gap="$2">
<YStack flex={1} gap="$1">
<Text fontSize="$xs" color={muted}>
{t('events.qr.align', 'Align')}
</Text>
@@ -1574,7 +1574,7 @@ function LayoutControls({
<option value="right">{t('common.right', 'Rechts')}</option>
</MobileSelect>
</YStack>
<YStack flex={1} space="$1">
<YStack flex={1} gap="$1">
<Text fontSize="$xs" color={muted}>
{t('events.qr.lineHeight', 'Line Height')}
</Text>
@@ -1606,7 +1606,7 @@ function LayoutControls({
const accordionDefaults = ['headline'];
return (
<YStack space="$3" marginTop="$2">
<YStack gap="$3" marginTop="$2">
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{t('events.qr.layoutControls', 'Layout & Schrift')}
</Text>
@@ -1627,9 +1627,9 @@ function LayoutControls({
</XStack>
</Accordion.Trigger>
<Accordion.Content {...({ paddingTop: "$2" } as any)}>
<YStack space="$2" padding="$2" borderWidth={1} borderColor={border} borderRadius={12}>
<XStack space="$2">
<YStack flex={1} space="$1">
<YStack gap="$2" padding="$2" borderWidth={1} borderColor={border} borderRadius={12}>
<XStack gap="$2">
<YStack flex={1} gap="$1">
<Text fontSize="$xs" color={muted}>
{t('events.qr.positionX', 'X (%)')}
</Text>
@@ -1641,7 +1641,7 @@ function LayoutControls({
onChange={(val) => onQrPercentChange('x')(val)}
/>
</YStack>
<YStack flex={1} space="$1">
<YStack flex={1} gap="$1">
<Text fontSize="$xs" color={muted}>
{t('events.qr.positionY', 'Y (%)')}
</Text>
@@ -1653,7 +1653,7 @@ function LayoutControls({
onChange={(val) => onQrPercentChange('y')(val)}
/>
</YStack>
<YStack flex={1} space="$1">
<YStack flex={1} gap="$1">
<Text fontSize="$xs" color={muted}>
{t('events.qr.size', 'Größe (%)')}
</Text>

View File

@@ -106,7 +106,7 @@ export default function MobileQrPrintPage() {
<ContextHelpLink slug="guest-access-qr" />
</XStack>
<MobileCard space="$3" alignItems="center">
<MobileCard gap="$3" alignItems="center">
<Text fontSize="$md" fontWeight="800" color={textStrong}>
{t('events.qr.heroTitle', 'Entrance QR Code')}
</Text>
@@ -148,7 +148,7 @@ export default function MobileQrPrintPage() {
<Text fontSize="$xs" color={muted}>
{t('events.qr.description', 'Scan to access the event guest app.')}
</Text>
<XStack space="$2" width="100%" marginTop="$2">
<XStack gap="$2" width="100%" marginTop="$2">
<CTAButton
label={t('events.qr.download', 'Download')}
fullWidth={false}
@@ -191,7 +191,7 @@ export default function MobileQrPrintPage() {
</XStack>
</MobileCard>
<MobileCard space="$2">
<MobileCard gap="$2">
<Text fontSize="$xs" color={muted}>
{t('events.qr.step1', 'Schritt 1: Format wählen')}
</Text>
@@ -226,7 +226,7 @@ export default function MobileQrPrintPage() {
/>
</MobileCard>
<MobileCard space="$2">
<MobileCard gap="$2">
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{t('events.qr.createLink', 'Neuen QR-Link erstellen')}
</Text>
@@ -299,7 +299,7 @@ function FormatSelection({
];
return (
<YStack space="$2" marginTop="$2">
<YStack gap="$2" marginTop="$2">
{cards.map((card) => {
const isSelected = selectedFormat === card.key;
return (
@@ -314,15 +314,15 @@ function FormatSelection({
borderWidth={isSelected ? 2 : 1}
backgroundColor={isSelected ? accentSoft : surface}
>
<XStack alignItems="center" justifyContent="space-between" space="$3">
<YStack space="$1" flex={1}>
<XStack alignItems="center" justifyContent="space-between" gap="$3">
<YStack gap="$1" flex={1}>
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{card.title}
</Text>
<Text fontSize="$xs" color={muted}>
{card.subtitle}
</Text>
<XStack space="$2" alignItems="center" flexWrap="wrap">
<XStack gap="$2" alignItems="center" flexWrap="wrap">
{card.badges.map((badge) => (
<PillBadge tone="muted" key={badge}>
{badge}
@@ -377,10 +377,10 @@ function BackgroundStep({
const formatLabel = isFoldable ? 'A4 Landscape (Double/Mirror)' : 'A4 Portrait';
return (
<YStack space="$3" marginTop="$2">
<XStack alignItems="center" space="$2">
<YStack gap="$3" marginTop="$2">
<XStack alignItems="center" gap="$2">
<Pressable onPress={onBack}>
<XStack alignItems="center" space="$1">
<XStack alignItems="center" gap="$1">
<ArrowLeft size={16} color={textStrong} />
<Text fontSize="$sm" color={textStrong}>
{t('common.back', 'Zurück')}
@@ -392,7 +392,7 @@ function BackgroundStep({
</PillBadge>
</XStack>
<YStack space="$2">
<YStack gap="$2">
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{t(
'events.qr.backgroundPicker',
@@ -488,10 +488,10 @@ function TextStep({
};
return (
<YStack space="$3" marginTop="$2">
<XStack alignItems="center" space="$2">
<YStack gap="$3" marginTop="$2">
<XStack alignItems="center" gap="$2">
<Pressable onPress={onBack}>
<XStack alignItems="center" space="$1">
<XStack alignItems="center" gap="$1">
<ArrowLeft size={16} color={textStrong} />
<Text fontSize="$sm" color={textStrong}>
{t('common.back', 'Zurück')}
@@ -501,7 +501,7 @@ function TextStep({
<PillBadge tone="muted">{t('events.qr.textStep', 'Texte & Hinweise')}</PillBadge>
</XStack>
<YStack space="$2">
<YStack gap="$2">
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{t('events.qr.textFields', 'Texte')}
</Text>
@@ -522,12 +522,12 @@ function TextStep({
/>
</YStack>
<YStack space="$2">
<YStack gap="$2">
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{t('events.qr.instructions', 'Anleitung')}
</Text>
{textFields.instructions.map((item, idx) => (
<XStack key={idx} alignItems="center" space="$2">
<XStack key={idx} alignItems="center" gap="$2">
<MobileInput
style={{ flex: 1 }}
placeholder={t('events.qr.instructionsPlaceholder', 'Schritt hinzufügen')}
@@ -580,10 +580,10 @@ function PreviewStep({
const previewBody = layout?.preview?.text ?? text;
return (
<YStack space="$3" marginTop="$2">
<XStack alignItems="center" space="$2">
<YStack gap="$3" marginTop="$2">
<XStack alignItems="center" gap="$2">
<Pressable onPress={onBack}>
<XStack alignItems="center" space="$1">
<XStack alignItems="center" gap="$1">
<ArrowLeft size={16} color={textStrong} />
<Text fontSize="$sm" color={textStrong}>
{t('common.back', 'Zurück')}
@@ -597,7 +597,7 @@ function PreviewStep({
) : null}
</XStack>
<YStack space="$2">
<YStack gap="$2">
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{t('events.qr.preview', 'Vorschau')}
</Text>
@@ -632,7 +632,7 @@ function PreviewStep({
{textFields.description}
</Text>
) : null}
<YStack space="$1">
<YStack gap="$1">
{textFields.instructions.filter((i) => i.trim().length > 0).map((item, idx) => (
<Text key={idx} fontSize="$xs" color={previewBody}>
{item}
@@ -660,7 +660,7 @@ function PreviewStep({
</YStack>
</YStack>
<XStack space="$2">
<XStack gap="$2">
<CTAButton label={t('events.qr.exportPdf', 'Export PDF')} onPress={() => onExport('pdf')} />
<CTAButton label={t('events.qr.exportPng', 'Export PNG')} onPress={() => onExport('png')} />
</XStack>

View File

@@ -110,10 +110,10 @@ export default function ResetPasswordPage() {
paddingVertical="$5"
style={{ ...safeAreaStyle, background: ADMIN_GRADIENTS.loginBackground }}
>
<YStack width="100%" maxWidth={520} space="$4">
<YStack width="100%" maxWidth={520} gap="$4">
<MobileCard backgroundColor="rgba(15, 23, 42, 0.6)" borderColor="rgba(255,255,255,0.12)">
<YStack space="$2">
<XStack alignItems="center" space="$2">
<YStack gap="$2">
<XStack alignItems="center" gap="$2">
<XStack
width={42}
height={42}
@@ -137,7 +137,7 @@ export default function ResetPasswordPage() {
</MobileCard>
<MobileCard backgroundColor={surface} borderColor={border}>
<YStack space="$3">
<YStack gap="$3">
<MobileField label={t('login.email', 'Email address')} error={fieldErrors.email?.[0]}>
<MobileInput
type="email"

View File

@@ -210,8 +210,8 @@ export default function MobileSettingsPage() {
}}
/>
<MobileCard space="$3">
<XStack alignItems="center" space="$2">
<MobileCard gap="$3">
<XStack alignItems="center" gap="$2">
<Shield size={18} color={text} />
<Text fontSize="$md" fontWeight="800" color={text}>
{t('mobileSettings.accountTitle', 'Account')}
@@ -223,14 +223,14 @@ export default function MobileSettingsPage() {
{user?.tenant_id ? (
<PillBadge tone="muted">{t('mobileSettings.tenantBadge', 'Account #{{id}}', { id: user.tenant_id })}</PillBadge>
) : null}
<XStack space="$2">
<XStack gap="$2">
<CTAButton label={t('settings.profile.actions.openProfile', 'Profil bearbeiten')} onPress={() => navigate(ADMIN_PROFILE_ACCOUNT_PATH)} />
<CTAButton label={t('settings.session.logout', 'Abmelden')} tone="ghost" onPress={() => logout({ redirect: adminPath('/logout') })} />
</XStack>
</MobileCard>
<MobileCard space="$3">
<XStack alignItems="center" space="$2">
<MobileCard gap="$3">
<XStack alignItems="center" gap="$2">
<Bell size={18} color={text} />
<Text fontSize="$md" fontWeight="800" color={text}>
{t('mobileSettings.notificationsTitle', 'Notifications')}
@@ -316,14 +316,14 @@ export default function MobileSettingsPage() {
{pushState.error}
</Text>
) : null}
<XStack space="$2">
<XStack gap="$2">
<CTAButton label={saving ? t('common.processing', '...') : t('settings.notifications.actions.save', 'Speichern')} onPress={() => handleSave()} />
<CTAButton label={t('common.reset', 'Reset')} tone="ghost" onPress={() => handleReset()} />
</XStack>
</MobileCard>
<MobileCard space="$3">
<XStack alignItems="center" space="$2">
<MobileCard gap="$3">
<XStack alignItems="center" gap="$2">
<Smartphone size={18} color={text} />
<Text fontSize="$md" fontWeight="800" color={text}>
{t('mobileSettings.deviceTitle', 'Device & permissions')}
@@ -337,9 +337,9 @@ export default function MobileSettingsPage() {
{t('mobileSettings.deviceLoading', 'Checking device status ...')}
</Text>
) : (
<YStack space="$2">
<YStack gap="$2">
<XStack alignItems="center" justifyContent="space-between" gap="$2">
<YStack flex={1} space="$1">
<YStack flex={1} gap="$1">
<Text fontSize="$sm" fontWeight="700" color={text}>
{t('mobileSettings.deviceStatus.notifications.label', 'Notifications')}
</Text>
@@ -352,7 +352,7 @@ export default function MobileSettingsPage() {
</PillBadge>
</XStack>
<XStack alignItems="center" justifyContent="space-between" gap="$2">
<YStack flex={1} space="$1">
<YStack flex={1} gap="$1">
<Text fontSize="$sm" fontWeight="700" color={text}>
{t('mobileSettings.deviceStatus.camera.label', 'Camera')}
</Text>
@@ -365,7 +365,7 @@ export default function MobileSettingsPage() {
</PillBadge>
</XStack>
<XStack alignItems="center" justifyContent="space-between" gap="$2">
<YStack flex={1} space="$1">
<YStack flex={1} gap="$1">
<Text fontSize="$sm" fontWeight="700" color={text}>
{t('mobileSettings.deviceStatus.storage.label', 'Offline storage')}
</Text>
@@ -378,7 +378,7 @@ export default function MobileSettingsPage() {
</PillBadge>
</XStack>
<XStack alignItems="center" justifyContent="space-between" gap="$2">
<YStack flex={1} space="$1">
<YStack flex={1} gap="$1">
<Text fontSize="$sm" fontWeight="700" color={text}>
{t('mobileSettings.deviceStatus.connection.label', 'Connection')}
</Text>
@@ -404,8 +404,8 @@ export default function MobileSettingsPage() {
) : null}
</MobileCard>
<MobileCard space="$3">
<XStack alignItems="center" space="$2">
<MobileCard gap="$3">
<XStack alignItems="center" gap="$2">
<Sparkles size={18} color={text} />
<Text fontSize="$md" fontWeight="800" color={text}>
{t('mobileSettings.experienceTitle', 'Experience')}
@@ -414,7 +414,7 @@ export default function MobileSettingsPage() {
<Text fontSize="$sm" color={muted}>
{t('mobileSettings.experienceBody', 'Replay the quick tour or re-enable the install banner.')}
</Text>
<XStack space="$2">
<XStack gap="$2">
<CTAButton
label={t('mobileSettings.experienceReplay', 'Replay quick tour')}
onPress={handleReplayTour}
@@ -430,8 +430,8 @@ export default function MobileSettingsPage() {
</XStack>
</MobileCard>
<MobileCard space="$3">
<XStack alignItems="center" space="$2">
<MobileCard gap="$3">
<XStack alignItems="center" gap="$2">
<User size={18} color={text} />
<Text fontSize="$md" fontWeight="800" color={text}>
{t('settings.appearance.title', 'Darstellung')}

View File

@@ -25,7 +25,7 @@ export default function MobileTasksTabPage() {
if (activeEvent?.slug && !tasksEnabled) {
return (
<MobileShell activeTab="tasks" title={t('events.tasks.title', 'Photo tasks')}>
<MobileCard alignItems="flex-start" space="$3">
<MobileCard alignItems="flex-start" gap="$3">
<Text fontSize="$lg" fontWeight="800" color={text}>
{t('events.tasks.disabledTitle', 'Photo task mode is off for this event')}
</Text>
@@ -44,7 +44,7 @@ export default function MobileTasksTabPage() {
if (!hasEvents) {
return (
<MobileShell activeTab="tasks" title={t('events.tasks.title', 'Photo tasks')}>
<MobileCard alignItems="flex-start" space="$3">
<MobileCard alignItems="flex-start" gap="$3">
<Text fontSize="$lg" fontWeight="800" color={text}>
{t('events.tasks.emptyTitle', 'Create an event first')}
</Text>
@@ -64,7 +64,7 @@ export default function MobileTasksTabPage() {
return (
<MobileShell activeTab="tasks" title={t('events.tasks.title', 'Photo tasks')}>
<YStack space="$2">
<YStack gap="$2">
<Text fontSize="$sm" color={text} fontWeight="700">
{t('events.tasks.pickEvent', 'Pick an event to manage photo tasks')}
</Text>
@@ -78,9 +78,9 @@ export default function MobileTasksTabPage() {
}
}}
>
<MobileCard borderColor={border} space="$2">
<MobileCard borderColor={border} gap="$2">
<XStack alignItems="center" justifyContent="space-between">
<YStack space="$1">
<YStack gap="$1">
<Text fontSize="$md" fontWeight="800" color={text}>
{resolveEventDisplayName(event)}
</Text>

View File

@@ -37,7 +37,7 @@ export default function MobileUploadsTabPage() {
shadowRadius={16}
shadowOffset={{ width: 0, height: 10 }}
>
<YStack space="$2.5">
<YStack gap="$2.5">
<XStack
alignItems="center"
paddingHorizontal="$3"
@@ -82,7 +82,7 @@ export default function MobileUploadsTabPage() {
shadowRadius={16}
shadowOffset={{ width: 0, height: 10 }}
>
<YStack space="$2.5">
<YStack gap="$2.5">
<XStack
alignItems="center"
paddingHorizontal="$3"
@@ -105,7 +105,7 @@ export default function MobileUploadsTabPage() {
paddingVertical="$2"
paddingHorizontal="$3"
title={
<YStack space="$1">
<YStack gap="$1">
<Text fontSize="$md" fontWeight="800" color={text}>
{resolveEventDisplayName(event)}
</Text>

View File

@@ -77,7 +77,7 @@ export function BottomNav({ active, onNavigate }: { active: NavKey; onNavigate:
<YStack
alignItems="center"
justifyContent="center"
space="$1"
gap="$1"
minHeight={50}
style={{
transform: isPressed ? 'scale(0.92)' : (activeState ? 'scale(1.05)' : 'scale(1)'),

View File

@@ -27,7 +27,7 @@ export function ContextHelpLink({ slug, label }: ContextHelpLinkProps) {
>
<XStack
alignItems="center"
space="$1.5"
gap="$1.5"
paddingHorizontal="$2.5"
paddingVertical="$1.5"
borderRadius={999}

View File

@@ -41,14 +41,14 @@ export function EventSwitcherSheet({
onClose={onClose}
snapPoints={[65]}
>
<YStack space="$2">
<YStack gap="$2">
{events.map((event) => {
const isActive = event.slug === activeSlug;
return (
<Pressable key={event.slug} onPress={() => handleSelect(event.slug)}>
<XStack
alignItems="center"
space="$3"
gap="$3"
padding="$3"
borderRadius={14}
backgroundColor={isActive ? theme.surfaceMuted : 'transparent'}

View File

@@ -18,7 +18,7 @@ export function MobileField({ label, hint, error, children }: FieldProps) {
const { text, muted, danger } = useAdminTheme();
return (
<YStack space="$1.5">
<YStack gap="$1.5">
{typeof label === 'string' || typeof label === 'number' ? (
<Text fontSize="$sm" fontWeight="800" color={text}>
{label}

View File

@@ -76,7 +76,7 @@ export function LegalConsentSheet({
onClose={onClose}
title={copy?.title ?? t('events.legalConsent.title', 'Before purchase')}
footer={
<YStack space="$2">
<YStack gap="$2">
{error ? (
<Text fontSize="$sm" color={danger}>
{error}
@@ -97,12 +97,12 @@ export function LegalConsentSheet({
</YStack>
}
>
<YStack space="$2">
<YStack gap="$2">
<Text fontSize="$sm" color={text}>
{copy?.description ?? t('events.legalConsent.description', 'Please confirm the legal notes before buying an add-on.')}
</Text>
{requireTerms ? (
<XStack space="$3" alignItems="flex-start">
<XStack gap="$3" alignItems="flex-start">
<Checkbox
id="legal-terms"
size="$4"
@@ -130,7 +130,7 @@ export function LegalConsentSheet({
</XStack>
) : null}
{requireWaiver ? (
<XStack space="$3" alignItems="flex-start">
<XStack gap="$3" alignItems="flex-start">
<Checkbox
id="legal-waiver"
size="$4"

View File

@@ -33,9 +33,9 @@ export function LimitWarnings({
}
return (
<YStack space="$2">
<YStack gap="$2">
{warnings.map((warning) => (
<MobileCard key={warning.id} borderColor={borderColor} space="$2">
<MobileCard key={warning.id} borderColor={borderColor} gap="$2">
<Text fontSize="$sm" color={textColor} fontWeight="700">
{warning.message}
</Text>
@@ -100,7 +100,7 @@ function MobileAddonsPicker({
}
return (
<XStack space="$2" alignItems="center">
<XStack gap="$2" alignItems="center">
<MobileSelect
value={selected}
onChange={(event) => setSelected(event.target.value)}

View File

@@ -33,13 +33,13 @@ export function MobileInstallBanner({
return (
<MobileCard
space={isCompact ? '$1.5' : '$2'}
gap={isCompact ? '$1.5' : '$2'}
borderColor={border}
backgroundColor={surfaceMuted}
padding={isCompact ? '$2' : '$3'}
>
<XStack alignItems="center" justifyContent="space-between" gap="$2">
<XStack alignItems="center" space="$2" flex={1}>
<XStack alignItems="center" gap="$2" flex={1}>
<XStack
width={isCompact ? 32 : 36}
height={isCompact ? 32 : 36}
@@ -50,7 +50,7 @@ export function MobileInstallBanner({
>
{isPrompt ? <Download size={16} color={primary} /> : <Share2 size={16} color={primary} />}
</XStack>
<YStack flex={1} space="$0.5">
<YStack flex={1} gap="$0.5">
<Text fontSize={isCompact ? '$xs' : '$sm'} fontWeight="800" color={textStrong}>
{t('installBanner.title', 'Install Fotospiel Admin')}
</Text>
@@ -61,7 +61,7 @@ export function MobileInstallBanner({
</Text>
</YStack>
</XStack>
<XStack alignItems="center" space="$2">
<XStack alignItems="center" gap="$2">
{isPrompt && onInstall && isCompact ? (
<Pressable onPress={onInstall}>
<Text fontSize={10} fontWeight="700" color={primary}>

View File

@@ -177,7 +177,7 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
paddingVertical="$1.5"
borderRadius={999}
alignItems="center"
space="$1.5"
gap="$1.5"
maxWidth={220}
borderWidth={1}
borderColor="rgba(255, 255, 255, 0.08)"
@@ -201,7 +201,7 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
const headerBackButton = onBack ? (
<HeaderActionButton onPress={onBack} ariaLabel={t('actions.back', 'Back')}>
<XStack alignItems="center" space="$1.5">
<XStack alignItems="center" gap="$1.5">
<ChevronLeft size={28} color="white" strokeWidth={2.5} />
</XStack>
</HeaderActionButton>
@@ -214,7 +214,7 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
);
const headerActionsRow = (
<XStack alignItems="center" space="$2.5">
<XStack alignItems="center" gap="$2.5">
{showQr ? (
<HeaderActionButton
onPress={() => navigate(adminPath(`/mobile/events/${effectiveActive?.slug}/qr`))}
@@ -282,7 +282,7 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
borderWidth={1} borderColor={actionBorder}
>
{user?.avatar_url ? (
<Image source={{ uri: user.avatar_url }} width={36} height={36} resizeMode="cover" />
<Image src={user.avatar_url} width={36} height={36} objectFit="cover" />
) : (
<Text fontSize="$xs" fontWeight="700" color="white">
{user?.name?.charAt(0).toUpperCase() ?? 'U'}
@@ -315,7 +315,7 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
paddingTop: 'calc(env(safe-area-inset-top, 0px) + 16px)',
}}
>
<XStack alignItems="center" justifyContent="space-between" minHeight={48} space="$2">
<XStack alignItems="center" justifyContent="space-between" minHeight={48} gap="$2">
{headerBackButton}
<XStack flex={1} justifyContent="center" alignItems="center">
@@ -332,7 +332,7 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
flex={1}
padding="$4"
paddingBottom="$10"
space="$3"
gap="$3"
width="100%"
maxWidth={800}
style={{ paddingBottom: 'calc(env(safe-area-inset-bottom, 0px) + 96px)' }}
@@ -345,7 +345,7 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
</XStack>
) : null}
{queuedPhotoCount > 0 ? (
<MobileCard space="$2">
<MobileCard gap="$2">
<Text fontSize="$sm" fontWeight="700" color={theme.textStrong}>
{t('status.queueTitle', 'Photo actions pending')}
</Text>
@@ -365,7 +365,7 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
</MobileCard>
) : null}
{subtitle ? (
<YStack space="$1">
<YStack gap="$1">
{title ? (
<Text fontSize="$lg" fontWeight="800" color={theme.textStrong}>
{title}

View File

@@ -45,7 +45,7 @@ export function OnboardingShell({
paddingHorizontal="$5"
paddingTop="$5"
paddingBottom="$6"
space="$4"
gap="$4"
style={{
paddingTop: 'calc(env(safe-area-inset-top, 0px) + 20px)',
paddingBottom: 'calc(env(safe-area-inset-bottom, 0px) + 24px)',
@@ -54,7 +54,7 @@ export function OnboardingShell({
<XStack alignItems="center" justifyContent="space-between">
{onBack ? (
<Pressable onPress={onBack} aria-label={resolvedBackLabel}>
<XStack alignItems="center" space="$1.5">
<XStack alignItems="center" gap="$1.5">
<ChevronLeft size={22} color={text} />
<Text fontSize="$sm" fontWeight="700" color={text}>
{resolvedBackLabel}
@@ -86,7 +86,7 @@ export function OnboardingShell({
shadowOpacity={0.06}
shadowRadius={14}
shadowOffset={{ width: 0, height: 8 }}
space="$2"
gap="$2"
>
{eyebrow ? (
<Text fontSize="$xs" fontWeight="700" color={muted} textTransform="uppercase" letterSpacing={0.6}>
@@ -103,7 +103,7 @@ export function OnboardingShell({
) : null}
</YStack>
<YStack space="$4">{children}</YStack>
<YStack gap="$4">{children}</YStack>
{footer ? <YStack marginTop="$2">{footer}</YStack> : null}
</YStack>
</YStack>

View File

@@ -26,7 +26,7 @@ export function MobileCard({
shadowRadius={16}
shadowOffset={{ width: 0, height: 10 }}
padding="$3.5"
space="$2"
gap="$2"
style={{
...style,
}}
@@ -138,7 +138,7 @@ export function CTAButton({
backgroundColor={backgroundColor}
borderWidth={isPrimary || isDanger ? 0 : 2}
borderColor={borderColor}
space="$2"
gap="$2"
style={primaryStyle}
>
{iconLeft}
@@ -169,7 +169,7 @@ export function KpiTile({
const iconColor = color || primary;
return (
<MobileCard borderRadius={14} padding="$2.5" width="31%" minWidth={100} space="$1.5">
<MobileCard borderRadius={14} padding="$2.5" width="31%" minWidth={100} gap="$1.5">
<XStack
width={28}
height={28}
@@ -181,7 +181,7 @@ export function KpiTile({
<IconCmp size={14} color={iconColor} />
</XStack>
<YStack space="$0">
<YStack gap="$0">
<Text fontSize="$xl" fontWeight="900" color={textStrong} letterSpacing={-0.5} lineHeight="$xl">
{value}
</Text>
@@ -237,7 +237,7 @@ export function KpiStrip({
minWidth={150}
maxWidth={220}
>
<XStack alignItems="center" space="$2">
<XStack alignItems="center" gap="$2">
<Text
fontSize={32}
fontWeight="900"
@@ -248,7 +248,7 @@ export function KpiStrip({
{item.value}
</Text>
<Separator vertical backgroundColor={separatorColor} height={32} marginHorizontal="$1.5" />
<YStack alignItems="center" space="$0.5" paddingLeft="$0.5">
<YStack alignItems="center" gap="$0.5" paddingLeft="$0.5">
<XStack
width={28}
height={28}
@@ -339,7 +339,7 @@ export function ActionTile({
style={tileStyle}
borderRadius={isCluster ? 14 : 16}
padding="$3"
space="$2.5"
gap="$2.5"
backgroundColor={glassSurface ?? backgroundColor}
borderWidth={2}
borderColor={borderColor}
@@ -405,7 +405,7 @@ export function FloatingActionButton({
borderRadius={999}
alignItems="center"
justifyContent="center"
space="$2"
gap="$2"
backgroundColor={primary}
shadowColor={shadow}
shadowOpacity={0.2}

View File

@@ -41,10 +41,10 @@ export function MobileScaffold({ title, onBack, rightSlot, children, footer }: M
WebkitBackdropFilter: 'blur(12px)',
}}
>
<XStack alignItems="center" space="$2">
<XStack alignItems="center" gap="$2">
{onBack ? (
<Pressable onPress={onBack}>
<XStack alignItems="center" space="$1.5">
<XStack alignItems="center" gap="$1.5">
<ChevronLeft size={18} color={primary} />
<Text fontSize="$sm" color={primary} fontWeight="600">
{t('actions.back', 'Back')}
@@ -63,7 +63,7 @@ export function MobileScaffold({ title, onBack, rightSlot, children, footer }: M
</XStack>
</XStack>
<YStack flex={1} padding="$4" space="$3" paddingBottom={footer ? '$14' : '$5'}>
<YStack flex={1} padding="$4" gap="$3" paddingBottom={footer ? '$14' : '$5'}>
{children}
</YStack>

View File

@@ -38,9 +38,9 @@ export function SetupChecklist({
const content = (
<YStack>
<Pressable onPress={() => setCollapsed(!collapsed)}>
<YStack padding="$3" paddingVertical="$2.5" space="$2">
<YStack padding="$3" paddingVertical="$2.5" gap="$2">
<XStack alignItems="center" justifyContent="space-between">
<XStack alignItems="center" space="$2">
<XStack alignItems="center" gap="$2">
<Text fontSize="$xs" fontWeight="700" color={theme.muted} textTransform="uppercase" letterSpacing={1}>
{title}
</Text>
@@ -51,7 +51,7 @@ export function SetupChecklist({
)}
</XStack>
<XStack alignItems="center" space="$2">
<XStack alignItems="center" gap="$2">
<Text fontSize="$xs" color={theme.muted} fontWeight="600">
{completedCount}/{steps.length}
</Text>
@@ -89,7 +89,7 @@ export function SetupChecklist({
backgroundColor={isNext ? theme.surfaceMuted : 'transparent'}
onPress={() => navigate(adminPath(step.targetPath))}
title={
<XStack alignItems="center" space="$2.5">
<XStack alignItems="center" gap="$2.5">
{step.isComplete ? (
<CheckCircle2 size={18} color={theme.successText} />
) : isNext ? (

View File

@@ -77,7 +77,7 @@ export function MobileSheet({
showsVerticalScrollIndicator={false}
{...({ contentContainerStyle: { paddingBottom: 6 } } as any)}
>
<YStack space={contentSpacing}>
<YStack gap={contentSpacing}>
<XStack alignItems="center" justifyContent="space-between">
<Text fontSize="$md" fontWeight="800" color={textStrong}>
{title}

View File

@@ -106,7 +106,7 @@ export function UserMenuSheet({ open, onClose, user, isMember, navigate }: UserM
borderLeftWidth={1}
borderColor={theme.border}
padding="$4"
space="$3"
gap="$3"
style={{
transform: open ? 'translateX(0)' : 'translateX(100%)',
transition: 'transform 220ms ease',
@@ -133,7 +133,7 @@ export function UserMenuSheet({ open, onClose, user, isMember, navigate }: UserM
</Pressable>
</XStack>
<XStack alignItems="center" space="$3">
<XStack alignItems="center" gap="$3">
<XStack
width={48}
height={48}
@@ -158,7 +158,7 @@ export function UserMenuSheet({ open, onClose, user, isMember, navigate }: UserM
<Separator backgroundColor={theme.border} opacity={0.6} />
<YStack space="$2">
<YStack gap="$2">
<Text fontSize="$xs" fontWeight="700" color={theme.muted} textTransform="uppercase" letterSpacing={1}>
{t('mobileProfile.settings', 'Einstellungen')}
</Text>
@@ -172,7 +172,7 @@ export function UserMenuSheet({ open, onClose, user, isMember, navigate }: UserM
paddingHorizontal="$3"
onPress={() => handleNavigate(item.path)}
title={
<XStack alignItems="center" space="$2">
<XStack alignItems="center" gap="$2">
<XStack
width={28}
height={28}
@@ -197,7 +197,7 @@ export function UserMenuSheet({ open, onClose, user, isMember, navigate }: UserM
</YGroup>
</YStack>
<YStack space="$2">
<YStack gap="$2">
<Text fontSize="$xs" fontWeight="700" color={theme.muted} textTransform="uppercase" letterSpacing={1}>
{t('settings.appearance.title', 'Darstellung')}
</Text>
@@ -207,7 +207,7 @@ export function UserMenuSheet({ open, onClose, user, isMember, navigate }: UserM
paddingVertical="$2"
paddingHorizontal="$3"
title={
<XStack space="$2" alignItems="center">
<XStack gap="$2" alignItems="center">
<Text fontSize="$sm" color={theme.textStrong}>
{t('mobileProfile.language', 'Sprache')}
</Text>
@@ -235,7 +235,7 @@ export function UserMenuSheet({ open, onClose, user, isMember, navigate }: UserM
paddingVertical="$2"
paddingHorizontal="$3"
title={
<XStack space="$2" alignItems="center">
<XStack gap="$2" alignItems="center">
<Text fontSize="$sm" color={theme.textStrong}>
{t('mobileProfile.theme', 'Dark Mode')}
</Text>

View File

@@ -50,7 +50,7 @@ export default function WelcomeEventPage() {
onSkip={handleSkip}
skipLabel={t('layout.jumpToDashboard', 'Jump to dashboard')}
>
<MobileCard space="$3">
<MobileCard gap="$3">
<Text fontSize="$sm" fontWeight="800">
{t('eventSetup.step.title', 'Event setup in minutes')}
</Text>
@@ -60,7 +60,7 @@ export default function WelcomeEventPage() {
'We guide you through name, date, mood, and tasks. Afterwards you can moderate photos and support guests live.',
)}
</Text>
<YStack space="$2">
<YStack gap="$2">
<FeatureRow
icon={Sparkles}
title={t('eventSetup.tiles.story.title', 'Story & mood')}
@@ -79,7 +79,7 @@ export default function WelcomeEventPage() {
</YStack>
</MobileCard>
<MobileCard space="$2">
<MobileCard gap="$2">
<Text fontSize="$sm" fontWeight="800">
{t('eventSetup.cta.heading', 'Ready for your first event?')}
</Text>
@@ -95,7 +95,7 @@ export default function WelcomeEventPage() {
/>
</MobileCard>
<YStack space="$2">
<YStack gap="$2">
<CTAButton
label={t('eventSetup.actions.dashboard.button', 'Open dashboard')}
tone="ghost"
@@ -121,7 +121,7 @@ function FeatureRow({
body: string;
}) {
return (
<XStack alignItems="center" space="$2">
<XStack alignItems="center" gap="$2">
<XStack
width={34}
height={34}

View File

@@ -48,7 +48,7 @@ export default function WelcomeLandingPage() {
onSkip={handleSkip}
skipLabel={t('layout.jumpToDashboard', 'Jump to dashboard')}
>
<MobileCard space="$3">
<MobileCard gap="$3">
<PillBadge tone="muted">{t('hero.eyebrow', 'Your event, your stage')}</PillBadge>
<Text fontSize="$lg" fontWeight="900">
{t('hero.title', 'Design the next Fotospiel experience')}
@@ -59,7 +59,7 @@ export default function WelcomeLandingPage() {
'In just a few steps you guide guests through a magical photo journey complete with storytelling, tasks, and a moderated gallery.',
)}
</Text>
<XStack space="$2" flexWrap="wrap">
<XStack gap="$2" flexWrap="wrap">
<CTAButton
label={
shouldGoBilling
@@ -80,7 +80,7 @@ export default function WelcomeLandingPage() {
</XStack>
</MobileCard>
<YStack space="$3">
<YStack gap="$3">
<FeatureCard
icon={ImageIcon}
title={t('highlights.gallery.title', 'Premium guest gallery')}
@@ -117,9 +117,9 @@ function FeatureCard({
badge?: string;
}) {
return (
<MobileCard space="$2">
<MobileCard gap="$2">
<XStack alignItems="center" justifyContent="space-between">
<XStack alignItems="center" space="$2">
<XStack alignItems="center" gap="$2">
<XStack
width={36}
height={36}

View File

@@ -85,7 +85,7 @@ export default function WelcomePackagesPage() {
</Text>
</MobileCard>
) : (
<YStack space="$3">
<YStack gap="$3">
{packages?.map((pkg) => (
<PackageCard
key={pkg.id}
@@ -97,7 +97,7 @@ export default function WelcomePackagesPage() {
</YStack>
)}
<MobileCard space="$2">
<MobileCard gap="$2">
<Text fontSize="$sm" fontWeight="800">
{t('packages.step.title', 'Activate the right plan')}
</Text>
@@ -106,7 +106,7 @@ export default function WelcomePackagesPage() {
</Text>
</MobileCard>
<XStack space="$2">
<XStack gap="$2">
<CTAButton
label={t('packages.cta.summary.button', 'Continue to summary')}
onPress={() => navigate(ADMIN_WELCOME_SUMMARY_PATH)}
@@ -143,9 +143,9 @@ function PackageCard({
return (
<Pressable onPress={onSelect}>
<MobileCard borderColor={selected ? primary : border} space="$2">
<MobileCard borderColor={selected ? primary : border} gap="$2">
<XStack alignItems="center" justifyContent="space-between">
<XStack alignItems="center" space="$2">
<XStack alignItems="center" gap="$2">
<XStack width={36} height={36} borderRadius={12} backgroundColor={accentSoft} alignItems="center" justifyContent="center">
<PackageIcon size={18} color={primary} />
</XStack>
@@ -162,7 +162,7 @@ function PackageCard({
{selected ? t('packages.card.selected', 'Selected') : t('packages.card.select', 'Select package')}
</PillBadge>
</XStack>
<XStack flexWrap="wrap" space="$2">
<XStack flexWrap="wrap" gap="$2">
{badges.map((badge) => (
<PillBadge key={badge as any} tone="muted">
{badge as any}
@@ -170,7 +170,7 @@ function PackageCard({
))}
</XStack>
{selected ? (
<XStack alignItems="center" space="$1">
<XStack alignItems="center" gap="$1">
<Check size={14} color={primary} />
<Text fontSize="$xs" color={primary} fontWeight="700">
{t('packages.card.selected', 'Selected')}

View File

@@ -94,9 +94,9 @@ export default function WelcomeSummaryPage() {
<CTAButton label={t('summary.footer.back', 'Back to package selection')} onPress={() => navigate(ADMIN_WELCOME_PACKAGES_PATH)} />
</MobileCard>
) : (
<MobileCard space="$3">
<MobileCard gap="$3">
<XStack alignItems="center" justifyContent="space-between">
<XStack alignItems="center" space="$2">
<XStack alignItems="center" gap="$2">
<XStack
width={36}
height={36}
@@ -123,7 +123,7 @@ export default function WelcomeSummaryPage() {
</PillBadge>
</XStack>
<YStack space="$2">
<YStack gap="$2">
<SummaryRow
label={t('summary.details.section.photosTitle', 'Photos & gallery')}
value={t('summary.details.section.photosValue', {
@@ -148,7 +148,7 @@ export default function WelcomeSummaryPage() {
</YStack>
{resolvedPackage.active ? (
<XStack alignItems="center" space="$2">
<XStack alignItems="center" gap="$2">
<CheckCircle2 size={18} color={ADMIN_COLORS.success} />
<Text fontSize="$sm" color={ADMIN_COLORS.success} fontWeight="700">
{t('summary.details.section.statusActive', 'Already purchased')}
@@ -158,11 +158,11 @@ export default function WelcomeSummaryPage() {
</MobileCard>
)}
<MobileCard space="$2">
<MobileCard gap="$2">
<Text fontSize="$sm" fontWeight="800">
{t('summary.nextStepsTitle', 'Next steps')}
</Text>
<YStack space="$1">
<YStack gap="$1">
{(t('summary.nextSteps', {
returnObjects: true,
defaultValue: [
@@ -171,7 +171,7 @@ export default function WelcomeSummaryPage() {
'Check your event slots before go-live and share your guest link.',
],
}) as string[]).map((item) => (
<XStack key={item} space="$2">
<XStack key={item} gap="$2">
<Text fontSize="$xs" color={ADMIN_COLORS.textMuted}>
</Text>
@@ -183,7 +183,7 @@ export default function WelcomeSummaryPage() {
</YStack>
</MobileCard>
<XStack space="$2">
<XStack gap="$2">
<CTAButton
label={t('summary.cta.billing.button', 'Go to billing')}
tone="ghost"