polish(mobile): audit design DSFR multi-agents — tokens, a11y, états, finitions
- tokens centralisés (Tint, IconSize, LineHeight, LetterSpacing), zéro hex en dur - accessibilité (rôles, labels, live regions, afficher/masquer mot de passe) - états vides/erreur complétés (ex. saisie introuvable), cibles tactiles >=44px - suppression du composant mort RepubliqueHeader Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Pressable,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
@@ -11,7 +12,7 @@ import {
|
||||
} from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { Colors } from '@/lib/colors';
|
||||
import { FontSize, FontWeight, Radius, Spacing } from '@/lib/theme';
|
||||
import { FontSize, FontWeight, HIT_SLOP, IconSize, MIN_TOUCH, Radius, Spacing } from '@/lib/theme';
|
||||
|
||||
interface TextFieldProps {
|
||||
label: string;
|
||||
@@ -53,7 +54,9 @@ export default function TextField({
|
||||
style,
|
||||
}: TextFieldProps) {
|
||||
const [focused, setFocused] = useState(false);
|
||||
const [revealed, setRevealed] = useState(false);
|
||||
const borderColor = error ? Colors.error : focused ? Colors.primary : Colors.gray[600];
|
||||
const isSecure = secureTextEntry && !revealed;
|
||||
|
||||
return (
|
||||
<View style={[styles.container, style]}>
|
||||
@@ -61,31 +64,53 @@ export default function TextField({
|
||||
{label}
|
||||
{required && <Text style={styles.required}> *</Text>}
|
||||
</Text>
|
||||
<TextInput
|
||||
value={value}
|
||||
onChangeText={onChangeText}
|
||||
onFocus={() => setFocused(true)}
|
||||
onBlur={() => setFocused(false)}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={Colors.gray[400]}
|
||||
secureTextEntry={secureTextEntry}
|
||||
keyboardType={keyboardType}
|
||||
autoCapitalize={autoCapitalize}
|
||||
autoComplete={autoComplete}
|
||||
multiline={multiline}
|
||||
numberOfLines={numberOfLines}
|
||||
editable={editable}
|
||||
style={[
|
||||
styles.input,
|
||||
{ borderBottomColor: borderColor },
|
||||
multiline && styles.multiline,
|
||||
!editable && styles.readonly,
|
||||
]}
|
||||
/>
|
||||
<View style={styles.inputRow}>
|
||||
<TextInput
|
||||
value={value}
|
||||
onChangeText={onChangeText}
|
||||
onFocus={() => setFocused(true)}
|
||||
onBlur={() => setFocused(false)}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={Colors.gray[400]}
|
||||
secureTextEntry={isSecure}
|
||||
keyboardType={keyboardType}
|
||||
autoCapitalize={autoCapitalize}
|
||||
autoComplete={autoComplete}
|
||||
multiline={multiline}
|
||||
numberOfLines={numberOfLines}
|
||||
editable={editable}
|
||||
accessibilityLabel={label}
|
||||
accessibilityState={{ disabled: !editable }}
|
||||
style={[
|
||||
styles.input,
|
||||
{ borderBottomColor: borderColor },
|
||||
secureTextEntry && styles.inputWithIcon,
|
||||
multiline && styles.multiline,
|
||||
!editable && styles.readonly,
|
||||
]}
|
||||
/>
|
||||
{secureTextEntry ? (
|
||||
<Pressable
|
||||
onPress={() => setRevealed((r) => !r)}
|
||||
hitSlop={HIT_SLOP}
|
||||
style={styles.toggle}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={revealed ? 'Masquer le mot de passe' : 'Afficher le mot de passe'}
|
||||
>
|
||||
<Ionicons
|
||||
name={revealed ? 'eye-off-outline' : 'eye-outline'}
|
||||
size={IconSize.lg}
|
||||
color={Colors.gray[500]}
|
||||
/>
|
||||
</Pressable>
|
||||
) : null}
|
||||
</View>
|
||||
{error ? (
|
||||
<View style={styles.messageRow}>
|
||||
<Ionicons name="alert-circle" size={14} color={Colors.error} />
|
||||
<Text style={styles.errorText}>{error}</Text>
|
||||
<Ionicons name="alert-circle" size={IconSize.sm} color={Colors.error} />
|
||||
<Text style={styles.errorText} accessibilityLiveRegion="polite">
|
||||
{error}
|
||||
</Text>
|
||||
</View>
|
||||
) : helper ? (
|
||||
<Text style={styles.helperText}>{helper}</Text>
|
||||
@@ -103,6 +128,7 @@ const styles = StyleSheet.create({
|
||||
marginBottom: Spacing.xs,
|
||||
},
|
||||
required: { color: Colors.error },
|
||||
inputRow: { position: 'relative', justifyContent: 'center' },
|
||||
input: {
|
||||
backgroundColor: Colors.gray[50],
|
||||
borderTopLeftRadius: Radius.sm,
|
||||
@@ -112,9 +138,16 @@ const styles = StyleSheet.create({
|
||||
paddingVertical: Spacing.md,
|
||||
fontSize: FontSize.body,
|
||||
color: Colors.black,
|
||||
minHeight: 44,
|
||||
minHeight: MIN_TOUCH,
|
||||
},
|
||||
multiline: { minHeight: 110, textAlignVertical: 'top' },
|
||||
inputWithIcon: { paddingRight: Spacing.xxl + Spacing.md },
|
||||
toggle: {
|
||||
position: 'absolute',
|
||||
right: Spacing.md,
|
||||
height: MIN_TOUCH,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
multiline: { minHeight: MIN_TOUCH * 2.5, textAlignVertical: 'top' },
|
||||
readonly: { color: Colors.gray[500], backgroundColor: Colors.gray[100] },
|
||||
messageRow: { flexDirection: 'row', alignItems: 'center', gap: Spacing.xs, marginTop: Spacing.xs },
|
||||
errorText: { fontSize: FontSize.xs, color: Colors.error },
|
||||
|
||||
Reference in New Issue
Block a user