PostCommentsSheet: - Fix Resize-Bug: PanResponder nur auf Grabber+Header, kein onStartShouldSetPanResponderCapture (das stahl Touch-Events von der FlatList und brach Drag-Resize) - Height-Limits (MAX/MIN/INITIAL) als Refs in PanResponder-Closure, damit sie nicht auf den ersten-Render-Stand eingefroren werden - Keyboard-Show/-Hide animiert currentHeight korrekt ohne den Resize-Referenzpunkt zu verlieren - Avatar in CommentRow: resolveAvatar() wenn authorAvatar vorhanden, Initialen-Fallback sonst. Bereit sobald Backend authorAvatar in Comments-Response mitliefert. - Alle Pressable durch TouchableOpacity ersetzt SheetFieldStack (neu): - Progressives Multi-Input-Pattern als FormSheet-Inhalt - Ausgefüllte Felder werden als antippbare Chips (mit Stift-Icon) nach oben verschoben - Aktives Feld: TextInput + →/✓-Button (letztes Feld = Checkmark) - Validate + Normalize pro Feld, Fehleranzeige unter dem Input - suffix-Slot für Eye-Toggle etc. - Nach letztem Feld: Keyboard.dismiss() + children (Rest des Formulars) erscheint Migriert auf FormSheet + SheetFieldStack: - ConnectMailSheet: Grid-View unveraendert; Form-View (email+password) via SheetFieldStack; Zurück/Abbrechen-Header-Buttons entfernt (Schliessen = Swipe/Backdrop) - EditMailAccountSheet: single-password-field via SheetFieldStack; Cancel-Header-Button weg - AddDomainSheet: domain-field via SheetFieldStack; Favicon-Preview+Warning+Checkbox+Button als children; Cancel-Header-Button weg - CreateRoomSheet: name+description via SheetFieldStack; Public-Toggle+JoinMode+Buttons als children; Abbrechen-Button bleibt (kein Header-Button, design-OK) useSheetKeyboardLift: geloescht (keine Aufrufer mehr nach Migration) KeyboardAwareSheet bleibt (AddMacSheet + AddWindowsSheet nutzen es noch) tsc --noEmit: gruen Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
226 lines
6.9 KiB
TypeScript
226 lines
6.9 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[];
|
|
/** 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, 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 (
|
|
<ScrollView
|
|
style={{ flex: 1 }}
|
|
contentContainerStyle={{ padding: 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>
|
|
);
|
|
}
|