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

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

View File

@@ -34,14 +34,32 @@ import { LayoutElement, CANVAS_WIDTH, CANVAS_HEIGHT } from './invite-layout/sche
import { buildInitialTextFields } from './qr/utils';
import { adminPath } from '../constants';
import { useBackNavigation } from './hooks/useBackNavigation';
import { ADMIN_COLORS, ADMIN_GRADIENTS, useAdminTheme } from './theme';
type Step = 'background' | 'text' | 'preview';
const BACKGROUND_PRESETS = [
{ id: 'bg-blue-floral', src: '/storage/layouts/backgrounds-portrait/bg-blue-floral.png', label: 'Blue Floral' },
{ id: 'bg-goldframe', src: '/storage/layouts/backgrounds-portrait/bg-goldframe.png', label: 'Gold Frame' },
{ id: 'gr-green-floral', src: '/storage/layouts/backgrounds-portrait/gr-green-floral.png', label: 'Green Floral' },
{
id: 'bg-blue-floral',
src: '/storage/layouts/backgrounds-portrait/bg-blue-floral.png',
labelKey: 'events.qr.backgroundPresets.blueFloral',
label: 'Blue Floral',
},
{
id: 'bg-goldframe',
src: '/storage/layouts/backgrounds-portrait/bg-goldframe.png',
labelKey: 'events.qr.backgroundPresets.goldFrame',
label: 'Gold Frame',
},
{
id: 'gr-green-floral',
src: '/storage/layouts/backgrounds-portrait/gr-green-floral.png',
labelKey: 'events.qr.backgroundPresets.greenFloral',
label: 'Green Floral',
},
];
const DEFAULT_BODY_FONT = 'Manrope';
const DEFAULT_DISPLAY_FONT = 'Fraunces';
export default function MobileQrLayoutCustomizePage() {
const { slug: slugParam, tokenId: tokenParam } = useParams<{ slug?: string; tokenId?: string }>();
@@ -51,6 +69,7 @@ export default function MobileQrLayoutCustomizePage() {
const layoutParam = searchParams.get('layout');
const navigate = useNavigate();
const { t } = useTranslation('management');
const { textStrong, danger } = useAdminTheme();
const [event, setEvent] = React.useState<TenantEvent | null>(null);
const [invite, setInvite] = React.useState<EventQrInvite | null>(null);
@@ -187,13 +206,13 @@ export default function MobileQrLayoutCustomizePage() {
onBack={back}
headerActions={
<HeaderActionButton onPress={() => window.location.reload()} ariaLabel={t('common.refresh', 'Refresh')}>
<RefreshCcw size={18} color="#0f172a" />
<RefreshCcw size={18} color={textStrong} />
</HeaderActionButton>
}
>
{error ? (
<MobileCard>
<Text fontWeight="700" color="#b91c1c">
<Text fontWeight="700" color={danger}>
{error}
</Text>
</MobileCard>
@@ -259,6 +278,7 @@ export default function MobileQrLayoutCustomizePage() {
function Stepper({ current, onStepChange }: { current: Step; onStepChange: (step: Step) => void }) {
const { t } = useTranslation('management');
const { textStrong, muted, border, primary, accentSoft, surface, successText } = useAdminTheme();
const steps: { key: Step; label: string }[] = [
{ key: 'background', label: t('events.qr.background', 'Hintergrund') },
{ key: 'text', label: t('events.qr.text', 'Text') },
@@ -278,16 +298,16 @@ function Stepper({ current, onStepChange }: { current: Step; onStepChange: (step
<YStack
borderRadius={12}
borderWidth={1}
borderColor={active ? '#2563EB' : completed ? '#16a34a' : '#e5e7eb'}
backgroundColor={active ? '#eff6ff' : '#fff'}
borderColor={active ? primary : completed ? successText : border}
backgroundColor={active ? accentSoft : surface}
padding="$2"
alignItems="center"
justifyContent="center"
>
<Text fontSize="$sm" fontWeight="700" color="#111827">
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{step.label}
</Text>
<Text fontSize="$xs" color="#6b7280">
<Text fontSize="$xs" color={muted}>
{t('events.qr.step', 'Schritt')} {idx + 1}
</Text>
</YStack>
@@ -295,8 +315,8 @@ function Stepper({ current, onStepChange }: { current: Step; onStepChange: (step
);
})}
</XStack>
<YStack height={8} borderRadius={999} backgroundColor="#e5e7eb" overflow="hidden">
<YStack height="100%" width={`${progress}%`} backgroundColor="#2563EB" />
<YStack height={8} borderRadius={999} backgroundColor={border} overflow="hidden">
<YStack height="100%" width={`${progress}%`} backgroundColor={primary} />
</YStack>
</YStack>
);
@@ -400,10 +420,10 @@ function renderEventName(name: TenantEvent['name'] | null | undefined): string |
function getDefaultSlots(): Record<string, SlotDefinition> {
return {
headline: { x: 0.08, y: 0.12, w: 0.84, fontSize: 90, fontWeight: 800, fontFamily: 'Playfair Display', align: 'center' },
subtitle: { x: 0.1, y: 0.21, w: 0.8, fontSize: 37, fontWeight: 600, fontFamily: 'Montserrat', align: 'center' },
description: { x: 0.1, y: 0.66, w: 0.8, fontSize: 26, fontFamily: 'Lora', lineHeight: 1.4, align: 'center' },
instructions: { x: 0.1, y: 0.7, w: 0.8, fontSize: 24, fontFamily: 'Lora', lineHeight: 1.5 },
headline: { x: 0.08, y: 0.12, w: 0.84, fontSize: 90, fontWeight: 800, fontFamily: DEFAULT_DISPLAY_FONT, align: 'center' },
subtitle: { x: 0.1, y: 0.21, w: 0.8, fontSize: 37, fontWeight: 600, fontFamily: DEFAULT_BODY_FONT, align: 'center' },
description: { x: 0.1, y: 0.66, w: 0.8, fontSize: 26, fontFamily: DEFAULT_BODY_FONT, lineHeight: 1.4, align: 'center' },
instructions: { x: 0.1, y: 0.7, w: 0.8, fontSize: 24, fontFamily: DEFAULT_BODY_FONT, lineHeight: 1.5 },
qr: { x: 0.39, y: 0.37, w: 0.27 },
};
}
@@ -433,10 +453,10 @@ function resolveSlots(layout: EventQrInviteLayout | null, isFoldable: boolean, o
const baseSlots = isFoldable
? {
headline: { x: 0.08, y: 0.15, w: 0.84, fontSize: 50, fontWeight: 800, fontFamily: 'Playfair Display', align: 'center' },
subtitle: { x: 0.1, y: 0.21, w: 0.8, fontSize: 37, fontWeight: 600, fontFamily: 'Montserrat', align: 'center' },
description: { x: 0.1, y: 0.66, w: 0.8, fontSize: 13, fontFamily: 'Lora', lineHeight: 1.4, align: 'center' },
instructions: { x: 0.12, y: 0.36, w: 0.76, fontSize: 13, fontFamily: 'Lora', lineHeight: 1.3 },
headline: { x: 0.08, y: 0.15, w: 0.84, fontSize: 50, fontWeight: 800, fontFamily: DEFAULT_DISPLAY_FONT, align: 'center' },
subtitle: { x: 0.1, y: 0.21, w: 0.8, fontSize: 37, fontWeight: 600, fontFamily: DEFAULT_BODY_FONT, align: 'center' },
description: { x: 0.1, y: 0.66, w: 0.8, fontSize: 13, fontFamily: DEFAULT_BODY_FONT, lineHeight: 1.4, align: 'center' },
instructions: { x: 0.12, y: 0.36, w: 0.76, fontSize: 13, fontFamily: DEFAULT_BODY_FONT, lineHeight: 1.3 },
qr: { x: 0.3, y: 0.3, w: 0.28 },
}
: getDefaultSlots();
@@ -498,9 +518,9 @@ function buildFabricOptions({
const slots = resolveSlots(layout, isFoldable, slotOverrides);
const elements: LayoutElement[] = [];
const textColor = layout?.preview?.text ?? '#0f172a';
const accentColor = layout?.preview?.accent ?? '#2563EB';
const secondaryColor = layout?.preview?.secondary ?? '#1f2937';
const textColor = layout?.preview?.text ?? ADMIN_COLORS.backdrop;
const accentColor = layout?.preview?.accent ?? ADMIN_COLORS.primary;
const secondaryColor = layout?.preview?.secondary ?? ADMIN_COLORS.text;
const badgeColor = layout?.preview?.badge ?? accentColor;
const rotatePoint = (cx: number, cy: number, x: number, y: number, angleDeg: number) => {
@@ -617,7 +637,7 @@ function buildFabricOptions({
badgeColor,
qrCodeDataUrl: qrPngUrl ?? (qrUrl ? `https://api.qrserver.com/v1/create-qr-code/?size=400x400&data=${encodeURIComponent(qrUrl)}` : null),
logoDataUrl: null,
backgroundColor: backgroundSolid ?? layout?.preview?.background ?? '#ffffff',
backgroundColor: backgroundSolid ?? layout?.preview?.background ?? ADMIN_COLORS.surface,
backgroundGradient,
backgroundImageUrl,
readOnly: true,
@@ -639,7 +659,7 @@ function BackgroundStep({
saving,
}: {
onBack: () => void;
presets: { id: string; src: string; label: string }[];
presets: { id: string; src: string; labelKey: string; label: string }[];
selectedPreset: string | null;
onSelectPreset: (id: string) => void;
selectedLayout: EventQrInviteLayout | null;
@@ -652,6 +672,7 @@ function BackgroundStep({
saving: boolean;
}) {
const { t } = useTranslation('management');
const { textStrong, muted, border, primary, accentSoft, surface, surfaceMuted } = useAdminTheme();
const resolvedLayout =
selectedLayout ??
layouts.find((layout) => {
@@ -664,25 +685,55 @@ function BackgroundStep({
const isFoldable = (resolvedLayout?.panel_mode ?? '').toLowerCase() === 'double-mirror';
const formatPaper = resolvePaper(resolvedLayout).toUpperCase();
const orientation = ((resolvedLayout?.orientation ?? (isFoldable ? 'landscape' : 'portrait')) || '').toLowerCase() === 'landscape' ? 'Landscape' : 'Portrait';
const isLandscape = ((resolvedLayout?.orientation ?? (isFoldable ? 'landscape' : 'portrait')) || '').toLowerCase() === 'landscape';
const orientationLabel = isLandscape
? t('events.qr.orientation.landscape', 'Landscape')
: t('events.qr.orientation.portrait', 'Portrait');
const formatLabel = isFoldable
? `${formatPaper} Landscape (double A5/mirrored)`
: `${formatPaper} ${orientation}`;
? t('events.qr.formatLabel.foldable', '{{paper}} {{orientation}} (double A5/mirrored)', {
paper: formatPaper,
orientation: orientationLabel,
})
: t('events.qr.formatLabel.standard', '{{paper}} {{orientation}}', {
paper: formatPaper,
orientation: orientationLabel,
});
const disablePresets = isFoldable;
const gradientPresets = [
{ angle: 180, stops: ['#F8FAFC', '#EEF2FF', '#F8FAFC'], label: 'Soft Lilac' },
{ angle: 135, stops: ['#FEE2E2', '#EFF6FF', '#ECFDF3'], label: 'Pastell' },
{ angle: 210, stops: ['#0B132B', '#1C2541', '#274690'], label: 'Midnight' },
{
angle: 180,
stops: ['#F8FAFC', '#EEF2FF', '#F8FAFC'],
labelKey: 'events.qr.gradientPresets.softLilac',
label: 'Soft Lilac',
},
{
angle: 135,
stops: ['#FEE2E2', '#EFF6FF', '#ECFDF3'],
labelKey: 'events.qr.gradientPresets.pastel',
label: 'Pastel',
},
{
angle: 210,
stops: ['#0B132B', '#1C2541', '#274690'],
labelKey: 'events.qr.gradientPresets.midnight',
label: 'Midnight',
},
];
const solidPresets = [
ADMIN_COLORS.surface,
ADMIN_COLORS.surfaceMuted,
ADMIN_COLORS.backdrop,
ADMIN_COLORS.primary,
ADMIN_COLORS.warning,
];
const solidPresets = ['#FFFFFF', '#F8FAFC', '#0F172A', '#2563EB', '#F59E0B'];
return (
<YStack space="$3" marginTop="$2">
<XStack alignItems="center" space="$2">
<Pressable onPress={onBack}>
<XStack alignItems="center" space="$1">
<ArrowLeft size={16} color="#111827" />
<Text fontSize="$sm" color="#111827">
<ArrowLeft size={16} color={textStrong} />
<Text fontSize="$sm" color={textStrong}>
{t('common.back', 'Zurück')}
</Text>
</XStack>
@@ -691,10 +742,10 @@ function BackgroundStep({
</XStack>
<YStack space="$2">
<Text fontSize="$sm" fontWeight="700" color="#111827">
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{disablePresets
? t('events.qr.backgroundPicker', 'Hintergrund für A5 (Gradient/Farbe)')
: t('events.qr.backgroundPicker', `Hintergrund auswählen (${formatLabel})`)}
? t('events.qr.backgroundPickerFoldable', 'Hintergrund für A5 (Gradient/Farbe)')
: t('events.qr.backgroundPicker', 'Hintergrund auswählen ({{formatLabel}})', { formatLabel })}
</Text>
{!disablePresets ? (
<>
@@ -709,8 +760,8 @@ function BackgroundStep({
borderRadius={14}
overflow="hidden"
borderWidth={isSelected ? 2 : 1}
borderColor={isSelected ? '#2563EB' : '#e5e7eb'}
backgroundColor="#f8fafc"
borderColor={isSelected ? primary : border}
backgroundColor={surfaceMuted}
>
<YStack
flex={1}
@@ -719,8 +770,8 @@ function BackgroundStep({
backgroundPosition="center"
/>
<XStack padding="$2" justifyContent="space-between" alignItems="center" backgroundColor="rgba(255,255,255,0.92)">
<Text fontSize="$xs" color="#111827">
{preset.label}
<Text fontSize="$xs" color={textStrong}>
{t(preset.labelKey, preset.label)}
</Text>
{isSelected ? <PillBadge tone="muted">{t('common.selected', 'Ausgewählt')}</PillBadge> : null}
</XStack>
@@ -729,20 +780,20 @@ function BackgroundStep({
);
})}
</XStack>
<Text fontSize="$xs" color="#6b7280">
<Text fontSize="$xs" color={muted}>
{t('events.qr.backgroundNote', 'Diese Presets sind für A4 Hochformat. Spiegelung erfolgt automatisch bei Tischkarten.')}
</Text>
</>
) : (
<Text fontSize="$xs" color="#6b7280">
<Text fontSize="$xs" color={muted}>
{t('events.qr.foldableBackgroundNote', 'Für A5 Tischkarten bitte Gradient oder Vollfarbe wählen.')}
</Text>
)}
<YStack space="$2" marginTop="$2">
<Text fontSize="$sm" fontWeight="700" color="#111827">
{t('events.qr.gradients', 'Gradienten')}
</Text>
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{t('events.qr.gradients', 'Gradienten')}
</Text>
<XStack flexWrap="wrap" gap="$2">
{gradientPresets.map((gradient, idx) => {
const isSelected =
@@ -753,13 +804,18 @@ function BackgroundStep({
backgroundImage: `linear-gradient(${gradient.angle}deg, ${gradient.stops.join(',')})`,
} as React.CSSProperties;
return (
<Pressable key={idx} onPress={() => onSelectGradient(gradient)} style={{ width: '30%' }}>
<Pressable
key={idx}
onPress={() => onSelectGradient(gradient)}
style={{ width: '30%' }}
aria-label={t(gradient.labelKey, gradient.label)}
>
<YStack
height={70}
borderRadius={12}
overflow="hidden"
borderWidth={isSelected ? 2 : 1}
borderColor={isSelected ? '#2563EB' : '#e5e7eb'}
borderColor={isSelected ? primary : border}
style={style}
/>
</Pressable>
@@ -769,7 +825,7 @@ function BackgroundStep({
</YStack>
<YStack space="$2" marginTop="$2">
<Text fontSize="$sm" fontWeight="700" color="#111827">
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{t('events.qr.colors', 'Vollfarbe')}
</Text>
<XStack flexWrap="wrap" gap="$2">
@@ -782,7 +838,7 @@ function BackgroundStep({
borderRadius={12}
overflow="hidden"
borderWidth={isSelected ? 2 : 1}
borderColor={isSelected ? '#2563EB' : '#e5e7eb'}
borderColor={isSelected ? primary : border}
backgroundColor={color}
/>
</Pressable>
@@ -840,8 +896,8 @@ function TextStep({
<XStack alignItems="center" space="$2">
<Pressable onPress={onBack}>
<XStack alignItems="center" space="$1">
<ArrowLeft size={16} color="#111827" />
<Text fontSize="$sm" color="#111827">
<ArrowLeft size={16} color={textStrong} />
<Text fontSize="$sm" color={textStrong}>
{t('common.back', 'Zurück')}
</Text>
</XStack>
@@ -850,7 +906,7 @@ function TextStep({
</XStack>
<YStack space="$2">
<Text fontSize="$sm" fontWeight="700" color="#111827">
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{t('events.qr.textFields', 'Texte')}
</Text>
<Input
@@ -875,9 +931,9 @@ function TextStep({
</YStack>
<YStack space="$2">
<Text fontSize="$sm" fontWeight="700" color="#111827">
{t('events.qr.instructions', 'Anleitung')}
</Text>
<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">
<TextArea
@@ -896,10 +952,10 @@ function TextStep({
alignItems="center"
justifyContent="center"
borderWidth={1}
borderColor="#e5e7eb"
backgroundColor="#fff"
borderColor={border}
backgroundColor={surface}
>
<Text fontSize="$md" fontWeight="800" color="#111827">
<Text fontSize="$md" fontWeight="800" color={textStrong}>
</Text>
</XStack>
@@ -913,10 +969,10 @@ function TextStep({
alignItems="center"
justifyContent="center"
borderWidth={1}
borderColor="#e5e7eb"
backgroundColor="#fff"
borderColor={border}
backgroundColor={surface}
>
<Plus size={18} color="#111827" />
<Plus size={18} color={textStrong} />
</XStack>
</Pressable>
) : null}
@@ -953,7 +1009,7 @@ function PreviewStep({
backgroundPreset: string | null;
backgroundGradient: { angle: number; stops: string[] } | null;
backgroundSolid: string | null;
presets: { id: string; src: string; label: string }[];
presets: { id: string; src: string; labelKey: string; label: string }[];
textFields: { headline: string; subtitle: string; description: string; instructions: string[] };
qrUrl: string;
qrPngUrl?: string | null;
@@ -963,6 +1019,7 @@ function PreviewStep({
tenantFonts: TenantFont[];
}) {
const { t } = useTranslation('management');
const { textStrong, muted, border, surface } = useAdminTheme();
const presetSrc = backgroundPreset ? presets.find((p) => p.id === backgroundPreset)?.src ?? null : null;
const resolvedBgGradient =
backgroundGradient ??
@@ -1039,7 +1096,10 @@ function PreviewStep({
const aspectRatio = `${canvasBase.width}/${canvasBase.height}`;
const paper = resolvePaper(layout);
const orientation = ((layout?.orientation ?? (isFoldable ? 'landscape' : 'portrait')) || '').toLowerCase() === 'landscape' ? 'landscape' : 'portrait';
const isLandscape = ((layout?.orientation ?? (isFoldable ? 'landscape' : 'portrait')) || '').toLowerCase() === 'landscape';
const orientationLabel = isLandscape
? t('events.qr.orientation.landscape', 'Landscape')
: t('events.qr.orientation.portrait', 'Portrait');
const qrImageSrc = qrPngUrl ?? qrUrl ?? '';
return (
@@ -1047,19 +1107,19 @@ function PreviewStep({
<XStack alignItems="center" space="$2">
<Pressable onPress={onBack}>
<XStack alignItems="center" space="$1">
<ArrowLeft size={16} color="#111827" />
<Text fontSize="$sm" color="#111827">
<ArrowLeft size={16} color={textStrong} />
<Text fontSize="$sm" color={textStrong}>
{t('common.back', 'Zurück')}
</Text>
</XStack>
</Pressable>
<PillBadge tone="muted">
{paper.toUpperCase()} {orientation === 'landscape' ? 'Landscape' : 'Portrait'}
{paper.toUpperCase()} {orientationLabel}
</PillBadge>
</XStack>
<YStack space="$2">
<Text fontSize="$sm" fontWeight="700" color="#111827">
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{t('events.qr.preview', 'Vorschau')}
</Text>
<YStack
@@ -1068,22 +1128,22 @@ function PreviewStep({
borderRadius={16}
overflow="hidden"
borderWidth={1}
borderColor="#e5e7eb"
backgroundColor="#fff"
borderColor={border}
backgroundColor={surface}
style={{ aspectRatio }}
alignItems="center"
justifyContent="center"
>
{previewLoading ? (
<Text color="#6b7280">{t('common.loading', 'Lädt Vorschau …')}</Text>
<Text color={muted}>{t('common.loading', 'Lädt Vorschau …')}</Text>
) : previewUrl ? (
<img
src={previewUrl}
alt="QR Layout Preview"
alt={t('events.qr.previewAlt', 'QR layout preview')}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
) : (
<Text color="#6b7280">{t('events.qr.missing', 'Kein QR-Link vorhanden')}</Text>
<Text color={muted}>{t('events.qr.missing', 'Kein QR-Link vorhanden')}</Text>
)}
</YStack>
</YStack>
@@ -1140,8 +1200,9 @@ function LayoutControls({
}) {
const { t } = useTranslation('management');
const [openColorSlot, setOpenColorSlot] = React.useState<string | null>(null);
const { border, surface, surfaceMuted, text, textStrong, muted, overlay, shadow, danger } = useAdminTheme();
const fontOptions = React.useMemo(() => {
const preset = ['Playfair Display', 'Lora', 'Montserrat', 'Inter', 'Roboto'];
const preset = ['Fraunces', 'Manrope', 'Inter', 'Roboto', 'Lora'];
const tenant = tenantFonts.map((font) => font.family);
return Array.from(new Set([...tenant, ...preset]));
}, [tenantFonts]);
@@ -1149,20 +1210,24 @@ function LayoutControls({
const numberInputStyle: React.CSSProperties = {
width: 90,
padding: '10px 12px',
border: '1px solid #e5e7eb',
borderRadius: 10,
border: `1px solid ${border}`,
borderRadius: 12,
fontSize: 14,
background: '#fff',
fontFamily: DEFAULT_BODY_FONT,
background: surfaceMuted,
color: text,
appearance: 'textfield',
};
const selectStyle: React.CSSProperties = {
width: '100%',
padding: '10px 12px',
border: '1px solid #e5e7eb',
borderRadius: 10,
border: `1px solid ${border}`,
borderRadius: 12,
fontSize: 14,
background: '#fff',
fontFamily: DEFAULT_BODY_FONT,
background: surfaceMuted,
color: text,
};
const StepperInput = ({
@@ -1185,8 +1250,8 @@ function LayoutControls({
return (
<XStack space="$1" alignItems="center">
<Pressable onPress={dec}>
<XStack width={36} height={36} borderRadius={10} alignItems="center" justifyContent="center" borderWidth={1} borderColor="#e5e7eb" backgroundColor="#fff">
<Text fontSize="$md" fontWeight="800" color="#111827">
<XStack width={36} height={36} borderRadius={10} alignItems="center" justifyContent="center" borderWidth={1} borderColor={border} backgroundColor={surface}>
<Text fontSize="$md" fontWeight="800" color={textStrong}>
</Text>
</XStack>
@@ -1206,8 +1271,8 @@ function LayoutControls({
style={numberInputStyle}
/>
<Pressable onPress={inc}>
<XStack width={36} height={36} borderRadius={10} alignItems="center" justifyContent="center" borderWidth={1} borderColor="#e5e7eb" backgroundColor="#fff">
<Text fontSize="$md" fontWeight="800" color="#111827">
<XStack width={36} height={36} borderRadius={10} alignItems="center" justifyContent="center" borderWidth={1} borderColor={border} backgroundColor={surface}>
<Text fontSize="$md" fontWeight="800" color={textStrong}>
+
</Text>
</XStack>
@@ -1250,20 +1315,20 @@ function LayoutControls({
return (
<Accordion.Item value={slotKey} key={slotKey}>
<Accordion.Trigger padding="$2" borderWidth={1} borderColor="#e5e7eb" borderRadius={12} backgroundColor="#f8fafc">
<Accordion.Trigger padding="$2" borderWidth={1} borderColor={border} borderRadius={12} backgroundColor={surfaceMuted}>
<XStack justifyContent="space-between" alignItems="center" flex={1}>
<Text fontSize="$sm" fontWeight="700" color="#111827">
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{label}
</Text>
<ChevronDown size={16} color="#6b7280" />
<ChevronDown size={16} color={muted} />
</XStack>
</Accordion.Trigger>
<Accordion.Content paddingTop="$2">
<YStack space="$2" padding="$2" borderWidth={1} borderColor="#e5e7eb" borderRadius={12}>
<YStack space="$2" padding="$2" borderWidth={1} borderColor={border} borderRadius={12}>
<XStack space="$3">
<YStack flex={1} space="$1">
<Text fontSize="$xs" color="#6b7280">
X (%)
<Text fontSize="$xs" color={muted}>
{t('events.qr.positionX', 'X (%)')}
</Text>
<XStack space="$2" alignItems="center">
<StepperInput
@@ -1279,10 +1344,10 @@ function LayoutControls({
paddingVertical="$2"
borderRadius={10}
borderWidth={1}
borderColor="#e5e7eb"
backgroundColor="#fff"
borderColor={border}
backgroundColor={surface}
>
<Text fontSize="$xs" color="#111827">
<Text fontSize="$xs" color={textStrong}>
{t('common.center', 'Zentrieren')}
</Text>
</XStack>
@@ -1290,8 +1355,8 @@ function LayoutControls({
</XStack>
</YStack>
<YStack flex={1} space="$1">
<Text fontSize="$xs" color="#6b7280">
Y (%)
<Text fontSize="$xs" color={muted}>
{t('events.qr.positionY', 'Y (%)')}
</Text>
<StepperInput
value={currentY * 100}
@@ -1302,8 +1367,8 @@ function LayoutControls({
/>
</YStack>
<YStack flex={1} space="$1">
<Text fontSize="$xs" color="#6b7280">
Breite (%)
<Text fontSize="$xs" color={muted}>
{t('events.qr.width', 'Breite (%)')}
</Text>
<StepperInput
value={currentW * 100}
@@ -1317,14 +1382,14 @@ function LayoutControls({
<XStack space="$3">
<YStack flex={1} space="$1">
<Text fontSize="$xs" color="#6b7280">
Font Size (px)
<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">
<Text fontSize="$xs" color="#6b7280">
Font Family
<Text fontSize="$xs" color={muted}>
{t('events.qr.fontFamily', 'Font Family')}
</Text>
<select
value={override.fontFamily ?? slot.fontFamily ?? ''}
@@ -1340,7 +1405,7 @@ function LayoutControls({
</select>
</YStack>
<YStack flex={1} space="$1">
<Text fontSize="$xs" color="#6b7280">
<Text fontSize="$xs" color={muted}>
{t('events.qr.fontColor', 'Schriftfarbe')}
</Text>
<YStack space="$2">
@@ -1351,14 +1416,14 @@ function LayoutControls({
height={36}
borderRadius={10}
borderWidth={1}
borderColor="#e5e7eb"
backgroundColor={override.color ?? slot.color ?? '#0f172a'}
borderColor={border}
backgroundColor={override.color ?? slot.color ?? textStrong}
/>
</Pressable>
<input
type="text"
value={override.color ?? slot.color ?? ''}
placeholder="#0f172a"
placeholder={ADMIN_COLORS.text}
onChange={(event) => onUpdateSlot(slotKey, { color: event.target.value })}
style={{ ...numberInputStyle, width: 110 }}
/>
@@ -1373,28 +1438,28 @@ function LayoutControls({
bottom={0}
alignItems="center"
justifyContent="center"
backgroundColor="rgba(0,0,0,0.35)"
backgroundColor={overlay}
zIndex={9999}
onPress={() => setOpenColorSlot(null)}
>
<YStack
padding="$3"
borderRadius={16}
backgroundColor="#fff"
backgroundColor={surface}
borderWidth={1}
borderColor="#e5e7eb"
borderColor={border}
elevation="$4"
shadowColor="rgba(0,0,0,0.08)"
shadowColor={shadow}
shadowOffset={{ width: 0, height: 4 }}
shadowOpacity={0.2}
shadowRadius={12}
gap="$2"
>
<Text fontSize="$sm" fontWeight="700" color="#111827">
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{t('events.qr.fontColor', 'Schriftfarbe')}
</Text>
<HexColorPicker
color={override.color ?? slot.color ?? '#0f172a'}
color={override.color ?? slot.color ?? ADMIN_COLORS.text}
onChange={(val) => onUpdateSlot(slotKey, { color: val })}
style={{ width: 240, height: 200 }}
/>
@@ -1405,10 +1470,10 @@ function LayoutControls({
paddingVertical="$2"
borderRadius={10}
borderWidth={1}
borderColor="#e5e7eb"
backgroundColor="#fff"
borderColor={border}
backgroundColor={surface}
>
<Text fontSize="$xs" color="#111827">
<Text fontSize="$xs" color={textStrong}>
{t('common.close', 'Schließen')}
</Text>
</XStack>
@@ -1424,8 +1489,8 @@ function LayoutControls({
<XStack space="$2">
<YStack flex={1} space="$1">
<Text fontSize="$xs" color="#6b7280">
Align
<Text fontSize="$xs" color={muted}>
{t('events.qr.align', 'Align')}
</Text>
<select
value={override.align ?? slot.align ?? 'left'}
@@ -1438,8 +1503,8 @@ function LayoutControls({
</select>
</YStack>
<YStack flex={1} space="$1">
<Text fontSize="$xs" color="#6b7280">
Line Height
<Text fontSize="$xs" color={muted}>
{t('events.qr.lineHeight', 'Line Height')}
</Text>
<StepperInput
value={override.lineHeight ?? slot.lineHeight ?? 1.35}
@@ -1470,7 +1535,7 @@ function LayoutControls({
return (
<YStack space="$3" marginTop="$2">
<Text fontSize="$sm" fontWeight="700" color="#111827">
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{t('events.qr.layoutControls', 'Layout & Schrift')}
</Text>
<Accordion type="multiple" defaultValue={accordionDefaults}>
@@ -1481,20 +1546,20 @@ function LayoutControls({
{qrSlot ? (
<Accordion.Item value="qr">
<Accordion.Trigger padding="$2" borderWidth={1} borderColor="#e5e7eb" borderRadius={12} backgroundColor="#f8fafc">
<Accordion.Trigger padding="$2" borderWidth={1} borderColor={border} borderRadius={12} backgroundColor={surfaceMuted}>
<XStack justifyContent="space-between" alignItems="center" flex={1}>
<Text fontSize="$sm" fontWeight="700" color="#111827">
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{t('events.qr.qr_code_label', 'QRCode')}
</Text>
<ChevronDown size={16} color="#6b7280" />
<ChevronDown size={16} color={muted} />
</XStack>
</Accordion.Trigger>
<Accordion.Content paddingTop="$2">
<YStack space="$2" padding="$2" borderWidth={1} borderColor="#e5e7eb" borderRadius={12}>
<YStack space="$2" padding="$2" borderWidth={1} borderColor={border} borderRadius={12}>
<XStack space="$2">
<YStack flex={1} space="$1">
<Text fontSize="$xs" color="#6b7280">
X (%)
<Text fontSize="$xs" color={muted}>
{t('events.qr.positionX', 'X (%)')}
</Text>
<StepperInput
value={(qrOverride.x ?? qrSlot.x) * 100}
@@ -1505,8 +1570,8 @@ function LayoutControls({
/>
</YStack>
<YStack flex={1} space="$1">
<Text fontSize="$xs" color="#6b7280">
Y (%)
<Text fontSize="$xs" color={muted}>
{t('events.qr.positionY', 'Y (%)')}
</Text>
<StepperInput
value={(qrOverride.y ?? qrSlot.y) * 100}
@@ -1517,8 +1582,8 @@ function LayoutControls({
/>
</YStack>
<YStack flex={1} space="$1">
<Text fontSize="$xs" color="#6b7280">
Größe (%)
<Text fontSize="$xs" color={muted}>
{t('events.qr.size', 'Größe (%)')}
</Text>
<StepperInput
value={(qrOverride.w ?? qrSlot.w) * 100}
@@ -1530,7 +1595,7 @@ function LayoutControls({
</YStack>
</XStack>
{!qrUrl ? (
<Text fontSize="$xs" color="#b91c1c">
<Text fontSize="$xs" color={danger}>
{t('events.qr.missing', 'Kein QR-Link vorhanden')}
</Text>
) : null}