chahinebrini a3f892ddac fix(native/mail): duplicate add-button in empty state + intro hints in ConnectMailSheet
- mail.tsx: hide section-header "+" button when accounts.length === 0 — MailEmptyState's CTA is the sole add trigger; also replaces Pressable with TouchableOpacity
- MailEmptyState: Pressable → TouchableOpacity (no-Pressable rule)
- SheetFieldStack: add optional `intro?: ReactNode` prop rendered in a flexShrink:1 ScrollView above chips/active-input so it compresses gracefully when the keyboard is up
- ConnectMailSheet: move app-password guide + green AES block into `intro` prop so they're visible from the start, before the user types anything

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 23:39:22 +02:00

248 lines
8.0 KiB
TypeScript

import { ReactNode, useState } from 'react';
import {
Keyboard,
ScrollView,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { useColors } from '../lib/theme';
export type SheetField = {
key: string;
label: string;
placeholder?: string;
value: string;
onChangeText: (v: string) => void;
validate?: (v: string) => string | undefined;
normalize?: (v: string) => string;
keyboardType?: TextInput['props']['keyboardType'];
secureTextEntry?: boolean;
autoCapitalize?: TextInput['props']['autoCapitalize'];
autoCorrect?: boolean;
suffix?: ReactNode;
};
type Props = {
fields: SheetField[];
/**
* Immer sichtbarer Bereich über Chips + aktivem Input — für Hinweise, die der User
* sehen soll BEVOR er tippt. Wird in einer eigenen ScrollView mit `flexShrink:1`
* gerendert, sodass er bei kleinem verfügbaren Platz (Tastatur offen) schrumpft,
* der Eingabebereich aber nie weggedrückt wird.
*/
intro?: ReactNode;
/** Rendert sich nach dem letzten Feld — sichtbar sobald alle Felder ausgefüllt sind. */
children?: ReactNode;
onComplete?: () => void;
};
/**
* Progressives Multi-Input-Pattern für FormSheet-Inhalte.
*
* Jeweils ein Feld ist aktiv (TextInput + →/✓-Button). Bereits ausgefüllte
* Felder wandern als antippbare Chips nach oben. Tap auf Chip → zurück zum
* Editieren. Nach dem letzten Feld: Keyboard.dismiss() + children werden sichtbar.
*
* Wird als `children` von `<FormSheet>` benutzt.
*/
export function SheetFieldStack({ fields, intro, children, onComplete }: Props) {
const colors = useColors();
const [activeIndex, setActiveIndex] = useState(0);
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
const allDone = activeIndex >= fields.length;
function advanceOrFinish() {
const field = fields[activeIndex];
if (!field) return;
const error = field.validate?.(field.value);
if (error) {
setFieldErrors((prev) => ({ ...prev, [field.key]: error }));
return;
}
const normalized = field.normalize ? field.normalize(field.value) : field.value;
if (normalized !== field.value) {
field.onChangeText(normalized);
}
setFieldErrors((prev) => {
const next = { ...prev };
delete next[field.key];
return next;
});
if (activeIndex === fields.length - 1) {
Keyboard.dismiss();
setActiveIndex(fields.length);
onComplete?.();
} else {
setActiveIndex((i) => i + 1);
}
}
function goToField(index: number) {
setActiveIndex(index);
}
const isLast = activeIndex === fields.length - 1;
return (
<View style={{ flex: 1 }}>
{/* Intro: immer sichtbar, schrumpft bei wenig Platz (Tastatur offen) */}
{intro != null && (
<ScrollView
style={{ flexShrink: 1 }}
contentContainerStyle={{ padding: 16, paddingBottom: 0 }}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
{intro}
</ScrollView>
)}
{/* Chips + aktives Feld + Post-Completion-Inhalt */}
<ScrollView
style={{ flex: 1 }}
contentContainerStyle={{ padding: 16, paddingTop: intro != null ? 10 : 16, gap: 10 }}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
{/* Abgeschlossene Felder als Chips */}
{fields.slice(0, activeIndex).map((field, index) => (
<TouchableOpacity
key={field.key}
activeOpacity={0.7}
onPress={() => goToField(index)}
style={{
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.surface,
borderWidth: 1,
borderColor: colors.border,
borderRadius: 12,
paddingHorizontal: 14,
paddingVertical: 10,
gap: 10,
}}
>
<View style={{ flex: 1 }}>
<Text
style={{ fontSize: 11, fontFamily: 'Nunito_600SemiBold', color: colors.textMuted }}
>
{field.label}
</Text>
<Text
style={{ fontSize: 14, fontFamily: 'Nunito_400Regular', color: colors.text, marginTop: 1 }}
numberOfLines={1}
>
{field.secureTextEntry ? '••••••••' : field.value}
</Text>
</View>
<Ionicons name="pencil-outline" size={14} color={colors.textMuted} />
</TouchableOpacity>
))}
{/* Aktives Feld */}
{!allDone && (
<View>
<Text
style={{
fontSize: 12,
fontFamily: 'Nunito_600SemiBold',
color: colors.textMuted,
marginBottom: 6,
}}
>
{fields[activeIndex].label}
</Text>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
<View
style={{
flex: 1,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.surfaceElevated,
borderRadius: 12,
paddingHorizontal: 14,
borderWidth: fieldErrors[fields[activeIndex].key] ? 1 : 0,
borderColor: '#dc2626',
}}
>
<TextInput
autoFocus
value={fields[activeIndex].value}
onChangeText={(v) => {
fields[activeIndex].onChangeText(v);
if (fieldErrors[fields[activeIndex].key]) {
setFieldErrors((prev) => {
const next = { ...prev };
delete next[fields[activeIndex].key];
return next;
});
}
}}
placeholder={fields[activeIndex].placeholder}
placeholderTextColor={colors.textMuted}
keyboardType={fields[activeIndex].keyboardType ?? 'default'}
secureTextEntry={fields[activeIndex].secureTextEntry}
autoCapitalize={fields[activeIndex].autoCapitalize ?? 'sentences'}
autoCorrect={fields[activeIndex].autoCorrect ?? true}
returnKeyType={isLast ? 'done' : 'next'}
onSubmitEditing={advanceOrFinish}
blurOnSubmit={false}
style={{
flex: 1,
paddingVertical: 12,
fontSize: 15,
fontFamily: 'Nunito_400Regular',
color: colors.text,
}}
/>
{fields[activeIndex].suffix}
</View>
<TouchableOpacity
activeOpacity={0.8}
onPress={advanceOrFinish}
style={{
width: 44,
height: 44,
borderRadius: 22,
backgroundColor: colors.brandOrange,
alignItems: 'center',
justifyContent: 'center',
}}
>
<Ionicons
name={isLast ? 'checkmark' : 'arrow-forward'}
size={20}
color="#fff"
/>
</TouchableOpacity>
</View>
{fieldErrors[fields[activeIndex].key] && (
<Text
style={{
fontSize: 12,
fontFamily: 'Nunito_400Regular',
color: '#dc2626',
marginTop: 4,
}}
>
{fieldErrors[fields[activeIndex].key]}
</Text>
)}
</View>
)}
{/* Rest des Formulars — sichtbar wenn alle Felder durch */}
{allDone && children}
</ScrollView>
</View>
);
}