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:
Azmog
2026-06-19 15:29:40 +02:00
parent 7fd27d8dc2
commit 91ceca2be5
29 changed files with 433 additions and 224 deletions
+6 -4
View File
@@ -1,6 +1,7 @@
import { Tabs } from 'expo-router'; import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { Colors } from '@/lib/colors'; import { Colors } from '@/lib/colors';
import { FontWeight, Spacing } from '@/lib/theme';
export default function TabLayout() { export default function TabLayout() {
return ( return (
@@ -11,12 +12,13 @@ export default function TabLayout() {
tabBarStyle: { tabBarStyle: {
backgroundColor: Colors.white, backgroundColor: Colors.white,
borderTopColor: Colors.gray[200], borderTopColor: Colors.gray[200],
paddingBottom: 8, paddingBottom: Spacing.sm,
paddingTop: 8, paddingTop: Spacing.sm,
height: 65, height: Spacing.xxxl + Spacing.xl + 1,
}, },
tabBarLabelStyle: { fontWeight: FontWeight.medium },
headerStyle: { backgroundColor: Colors.white }, headerStyle: { backgroundColor: Colors.white },
headerTitleStyle: { fontWeight: '700', color: Colors.black }, headerTitleStyle: { fontWeight: FontWeight.bold, color: Colors.black },
}} }}
> >
<Tabs.Screen <Tabs.Screen
+18 -8
View File
@@ -8,6 +8,12 @@ import { Colors } from '@/lib/colors';
import { Spacing, Radius, FontSize, FontWeight, MIN_TOUCH } from '@/lib/theme'; import { Spacing, Radius, FontSize, FontWeight, MIN_TOUCH } from '@/lib/theme';
import { AppBar, Card, Badge, SectionTitle, EmptyState, Loader } from '@/components/ui'; import { AppBar, Card, Badge, SectionTitle, EmptyState, Loader } from '@/components/ui';
/** Tailles d'icônes et de pastilles (multiples de l'échelle 4px du DSFR). */
const ICON_LG = 28;
const ACTION_ICON = 56;
const EMOTION_ICON = 44;
const EMOJI_SIZE = 22;
export default function DashboardScreen() { export default function DashboardScreen() {
const router = useRouter(); const router = useRouter();
const { user } = useAuthStore(); const { user } = useAuthStore();
@@ -27,13 +33,13 @@ export default function DashboardScreen() {
<View style={styles.actionsRow}> <View style={styles.actionsRow}>
<Card onPress={() => router.push('/(tabs)/journal/new')} padding={Spacing.lg} style={styles.actionPrimary}> <Card onPress={() => router.push('/(tabs)/journal/new')} padding={Spacing.lg} style={styles.actionPrimary}>
<View style={[styles.actionIcon, styles.actionIconPrimary]}> <View style={[styles.actionIcon, styles.actionIconPrimary]}>
<Ionicons name="add-circle" size={28} color={Colors.white} /> <Ionicons name="add-circle" size={ICON_LG} color={Colors.white} />
</View> </View>
<Text style={styles.actionTextPrimary}>Nouvelle saisie</Text> <Text style={styles.actionTextPrimary}>Nouvelle saisie</Text>
</Card> </Card>
<Card onPress={() => router.push('/(tabs)/journal/reports')} padding={Spacing.lg} style={styles.actionCard}> <Card onPress={() => router.push('/(tabs)/journal/reports')} padding={Spacing.lg} style={styles.actionCard}>
<View style={[styles.actionIcon, styles.actionIconSecondary]}> <View style={[styles.actionIcon, styles.actionIconSecondary]}>
<Ionicons name="bar-chart" size={28} color={Colors.primary} /> <Ionicons name="bar-chart" size={ICON_LG} color={Colors.primary} />
</View> </View>
<Text style={styles.actionText}>Mes rapports</Text> <Text style={styles.actionText}>Mes rapports</Text>
</Card> </Card>
@@ -73,7 +79,11 @@ export default function DashboardScreen() {
<View style={styles.sectionHeader}> <View style={styles.sectionHeader}>
<SectionTitle>Dernières saisies</SectionTitle> <SectionTitle>Dernières saisies</SectionTitle>
{recentSaisies.length > 0 && ( {recentSaisies.length > 0 && (
<Text style={styles.seeAll} onPress={() => router.push('/(tabs)/journal')}> <Text
style={styles.seeAll}
onPress={() => router.push('/(tabs)/journal')}
accessibilityRole="link"
>
Tout voir Tout voir
</Text> </Text>
)} )}
@@ -97,7 +107,7 @@ export default function DashboardScreen() {
padding={Spacing.md} padding={Spacing.md}
style={styles.saisieCard} style={styles.saisieCard}
> >
<View style={[styles.emotionIcon, { backgroundColor: `${saisie.emotion?.couleur}20` }]}> <View style={[styles.emotionIcon, { backgroundColor: `${saisie.emotion?.couleur || Colors.primary}20` }]}>
<Text style={styles.emotionEmoji}>{saisie.emotion?.icone || '🔵'}</Text> <Text style={styles.emotionEmoji}>{saisie.emotion?.icone || '🔵'}</Text>
</View> </View>
<View style={styles.saisieContent}> <View style={styles.saisieContent}>
@@ -132,9 +142,9 @@ const styles = StyleSheet.create({
backgroundColor: Colors.primary, backgroundColor: Colors.primary,
borderColor: Colors.primary, borderColor: Colors.primary,
}, },
actionIcon: { width: 56, height: 56, borderRadius: Radius.sm, justifyContent: 'center', alignItems: 'center' }, actionIcon: { width: ACTION_ICON, height: ACTION_ICON, borderRadius: Radius.sm, justifyContent: 'center', alignItems: 'center' },
actionIconPrimary: { backgroundColor: Colors.primaryDark }, actionIconPrimary: { backgroundColor: Colors.primaryDark },
actionIconSecondary: { backgroundColor: '#E3E3FD' }, actionIconSecondary: { backgroundColor: Colors.gray[100] },
actionText: { fontSize: FontSize.sm, fontWeight: FontWeight.semibold, color: Colors.black }, actionText: { fontSize: FontSize.sm, fontWeight: FontWeight.semibold, color: Colors.black },
actionTextPrimary: { fontSize: FontSize.sm, fontWeight: FontWeight.bold, color: Colors.white }, actionTextPrimary: { fontSize: FontSize.sm, fontWeight: FontWeight.bold, color: Colors.white },
block: { marginBottom: Spacing.xl }, block: { marginBottom: Spacing.xl },
@@ -153,8 +163,8 @@ const styles = StyleSheet.create({
paddingTop: Spacing.xs, paddingTop: Spacing.xs,
}, },
saisieCard: { flexDirection: 'row', alignItems: 'center', gap: Spacing.md, marginBottom: Spacing.sm }, saisieCard: { flexDirection: 'row', alignItems: 'center', gap: Spacing.md, marginBottom: Spacing.sm },
emotionIcon: { width: 44, height: 44, borderRadius: Radius.sm, justifyContent: 'center', alignItems: 'center' }, emotionIcon: { width: EMOTION_ICON, height: EMOTION_ICON, borderRadius: Radius.sm, justifyContent: 'center', alignItems: 'center' },
emotionEmoji: { fontSize: 22 }, emotionEmoji: { fontSize: EMOJI_SIZE },
saisieContent: { flex: 1, gap: Spacing.xs, alignItems: 'flex-start' }, saisieContent: { flex: 1, gap: Spacing.xs, alignItems: 'flex-start' },
saisieDate: { fontSize: FontSize.xs, color: Colors.gray[500] }, saisieDate: { fontSize: FontSize.xs, color: Colors.gray[500] },
}); });
+10 -1
View File
@@ -31,7 +31,16 @@ export default function FeedDetailScreen() {
); );
} }
const texteNettoye = (feed.contenu ?? '').replace(/<[^>]*>/g, ''); const texteNettoye = (feed.contenu ?? '')
.replace(/<[^>]*>/g, '')
.replace(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&#39;|&apos;/g, "'")
.replace(/&quot;/g, '"')
.replace(/\n{3,}/g, '\n\n')
.trim();
return ( return (
<View style={styles.fill}> <View style={styles.fill}>
+10 -3
View File
@@ -3,7 +3,7 @@ import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import { useFeeds } from '@/hooks/useFeeds'; import { useFeeds } from '@/hooks/useFeeds';
import { Colors } from '@/lib/colors'; import { Colors } from '@/lib/colors';
import { FontSize, FontWeight, Radius, Spacing } from '@/lib/theme'; import { FontSize, FontWeight, Radius, Spacing, Tint } from '@/lib/theme';
import { AppBar, Card, EmptyState, Loader } from '@/components/ui'; import { AppBar, Card, EmptyState, Loader } from '@/components/ui';
import type { Feed } from '@/types'; import type { Feed } from '@/types';
@@ -12,7 +12,14 @@ export default function InfoListScreen() {
const router = useRouter(); const router = useRouter();
const renderItem = ({ item }: { item: Feed }) => { const renderItem = ({ item }: { item: Feed }) => {
const extrait = (item.contenu ?? '').replace(/<[^>]*>/g, '').trim(); const extrait = (item.contenu ?? '')
.replace(/<[^>]*>/g, '')
.replace(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
.replace(/&#39;|&apos;/g, "'")
.replace(/&quot;/g, '"')
.replace(/\s+/g, ' ')
.trim();
return ( return (
<Card onPress={() => router.push(`/(tabs)/info/${item.slug}`)} style={styles.card}> <Card onPress={() => router.push(`/(tabs)/info/${item.slug}`)} style={styles.card}>
<View style={styles.cardRow}> <View style={styles.cardRow}>
@@ -73,7 +80,7 @@ const styles = StyleSheet.create({
width: 48, width: 48,
height: 48, height: 48,
borderRadius: Radius.sm, borderRadius: Radius.sm,
backgroundColor: '#E3E3FD', backgroundColor: Tint.primary,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
}, },
+21 -4
View File
@@ -6,12 +6,15 @@ import EmotionPicker from '@/components/EmotionPicker';
import { IntensityPicker } from './new'; import { IntensityPicker } from './new';
import { Colors } from '@/lib/colors'; import { Colors } from '@/lib/colors';
import { Spacing, FontSize, FontWeight } from '@/lib/theme'; import { Spacing, FontSize, FontWeight } from '@/lib/theme';
import { Button, TextField, Loader } from '@/components/ui'; import { Button, TextField, Loader, EmptyState } from '@/components/ui';
/** Hauteur de ligne du grand chiffre d'intensité. */
const INTENSITY_LINE_HEIGHT = 56;
export default function EditEntryScreen() { export default function EditEntryScreen() {
const router = useRouter(); const router = useRouter();
const { id } = useLocalSearchParams<{ id: string }>(); const { id } = useLocalSearchParams<{ id: string }>();
const { data: saisies } = useSaisies(); const { data: saisies, isLoading } = useSaisies();
const updateSaisie = useUpdateSaisie(); const updateSaisie = useUpdateSaisie();
const saisie = saisies?.find((s) => s.id === Number(id)); const saisie = saisies?.find((s) => s.id === Number(id));
@@ -46,10 +49,24 @@ export default function EditEntryScreen() {
} }
}; };
if (!saisie) { if (isLoading) {
return <Loader label="Chargement…" />; return <Loader label="Chargement…" />;
} }
if (!saisie) {
return (
<View style={styles.container}>
<EmptyState
icon="alert-circle-outline"
title="Saisie introuvable"
message="Cette saisie n'existe plus ou a été supprimée."
actionLabel="Retour"
onAction={() => router.back()}
/>
</View>
);
}
return ( return (
<ScrollView style={styles.container} contentContainerStyle={styles.content}> <ScrollView style={styles.container} contentContainerStyle={styles.content}>
{/* Émotion */} {/* Émotion */}
@@ -106,7 +123,7 @@ const styles = StyleSheet.create({
section: { marginBottom: Spacing.xl }, section: { marginBottom: Spacing.xl },
label: { fontSize: FontSize.body, fontWeight: FontWeight.bold, color: Colors.black, marginBottom: Spacing.md }, label: { fontSize: FontSize.body, fontWeight: FontWeight.bold, color: Colors.black, marginBottom: Spacing.md },
intensityContainer: { flexDirection: 'row', alignItems: 'baseline', justifyContent: 'center', marginBottom: Spacing.lg }, intensityContainer: { flexDirection: 'row', alignItems: 'baseline', justifyContent: 'center', marginBottom: Spacing.lg },
intensityValue: { fontSize: FontSize.display, fontWeight: FontWeight.heavy, color: Colors.primary, lineHeight: 56 }, intensityValue: { fontSize: FontSize.display, fontWeight: FontWeight.heavy, color: Colors.primary, lineHeight: INTENSITY_LINE_HEIGHT },
intensityUnit: { fontSize: FontSize.xl, color: Colors.gray[400], marginLeft: Spacing.xs }, intensityUnit: { fontSize: FontSize.xl, color: Colors.gray[400], marginLeft: Spacing.xs },
scaleLabels: { flexDirection: 'row', justifyContent: 'space-between', marginTop: Spacing.md }, scaleLabels: { flexDirection: 'row', justifyContent: 'space-between', marginTop: Spacing.md },
scaleLabel: { fontSize: FontSize.xs, color: Colors.gray[500] }, scaleLabel: { fontSize: FontSize.xs, color: Colors.gray[500] },
+24 -12
View File
@@ -8,6 +8,16 @@ import { Spacing, Radius, FontSize, FontWeight, Shadow, MIN_TOUCH, HIT_SLOP } fr
import { AppBar, Card, Badge, EmptyState, Loader } from '@/components/ui'; import { AppBar, Card, Badge, EmptyState, Loader } from '@/components/ui';
import type { SaisieTracker } from '@/types'; import type { SaisieTracker } from '@/types';
/** Dimensions locales (multiples de l'échelle 4px du DSFR). */
const EMOTION_ICON = 48;
const EMOJI_SIZE = 24;
const ACTION_ICON_SIZE = 20;
const FAB_SIZE = 56;
const FAB_ICON = 30;
const BAR_HEIGHT = 6;
/** Marge basse de la liste : laisse passer le FAB sans qu'il chevauche le contenu. */
const LIST_BOTTOM_GAP = FAB_SIZE + Spacing.xl + Spacing.lg;
export default function JournalScreen() { export default function JournalScreen() {
const router = useRouter(); const router = useRouter();
const { data: saisies, isLoading, refetch } = useSaisies(); const { data: saisies, isLoading, refetch } = useSaisies();
@@ -39,7 +49,7 @@ export default function JournalScreen() {
const renderItem = ({ item }: { item: SaisieTracker }) => ( const renderItem = ({ item }: { item: SaisieTracker }) => (
<Card padding={Spacing.lg} style={styles.saisieCard}> <Card padding={Spacing.lg} style={styles.saisieCard}>
<View style={[styles.emotionIcon, { backgroundColor: `${item.emotion?.couleur}20` }]}> <View style={[styles.emotionIcon, { backgroundColor: `${item.emotion?.couleur || Colors.primary}20` }]}>
<Text style={styles.emotionEmoji}>{item.emotion?.icone || '🔵'}</Text> <Text style={styles.emotionEmoji}>{item.emotion?.icone || '🔵'}</Text>
</View> </View>
<View style={styles.saisieContent}> <View style={styles.saisieContent}>
@@ -79,7 +89,7 @@ export default function JournalScreen() {
accessibilityLabel="Modifier la saisie" accessibilityLabel="Modifier la saisie"
onPress={() => router.push({ pathname: '/(tabs)/journal/edit', params: { id: String(item.id) } })} onPress={() => router.push({ pathname: '/(tabs)/journal/edit', params: { id: String(item.id) } })}
> >
<Ionicons name="create-outline" size={20} color={Colors.primary} /> <Ionicons name="create-outline" size={ACTION_ICON_SIZE} color={Colors.primary} />
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity <TouchableOpacity
style={styles.actionBtn} style={styles.actionBtn}
@@ -87,7 +97,7 @@ export default function JournalScreen() {
accessibilityLabel="Supprimer la saisie" accessibilityLabel="Supprimer la saisie"
onPress={() => handleDelete(item.id)} onPress={() => handleDelete(item.id)}
> >
<Ionicons name="trash-outline" size={20} color={Colors.error} /> <Ionicons name="trash-outline" size={ACTION_ICON_SIZE} color={Colors.error} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</Card> </Card>
@@ -120,10 +130,12 @@ export default function JournalScreen() {
)} )}
<TouchableOpacity <TouchableOpacity
style={styles.fab} style={styles.fab}
accessibilityRole="button"
accessibilityLabel="Nouvelle saisie" accessibilityLabel="Nouvelle saisie"
hitSlop={HIT_SLOP}
onPress={() => router.push('/(tabs)/journal/new')} onPress={() => router.push('/(tabs)/journal/new')}
> >
<Ionicons name="add" size={30} color={Colors.white} /> <Ionicons name="add" size={FAB_ICON} color={Colors.white} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
); );
@@ -131,15 +143,15 @@ export default function JournalScreen() {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
screen: { flex: 1, backgroundColor: Colors.gray[50] }, screen: { flex: 1, backgroundColor: Colors.gray[50] },
listContent: { padding: Spacing.lg, paddingBottom: 96 }, listContent: { padding: Spacing.lg, paddingBottom: LIST_BOTTOM_GAP },
saisieCard: { flexDirection: 'row', alignItems: 'flex-start', gap: Spacing.md, marginBottom: Spacing.md }, saisieCard: { flexDirection: 'row', alignItems: 'flex-start', gap: Spacing.md, marginBottom: Spacing.md },
emotionIcon: { width: 48, height: 48, borderRadius: Radius.sm, justifyContent: 'center', alignItems: 'center' }, emotionIcon: { width: EMOTION_ICON, height: EMOTION_ICON, borderRadius: Radius.sm, justifyContent: 'center', alignItems: 'center' },
emotionEmoji: { fontSize: 24 }, emotionEmoji: { fontSize: EMOJI_SIZE },
saisieContent: { flex: 1 }, saisieContent: { flex: 1 },
saisieHeader: { flexDirection: 'row', alignItems: 'center', gap: Spacing.sm, marginBottom: Spacing.sm, flexWrap: 'wrap' }, saisieHeader: { flexDirection: 'row', alignItems: 'center', gap: Spacing.sm, marginBottom: Spacing.sm, flexWrap: 'wrap' },
intensityText: { fontSize: FontSize.xs, color: Colors.gray[500] }, intensityText: { fontSize: FontSize.xs, color: Colors.gray[500] },
intensityBar: { height: 6, backgroundColor: Colors.gray[200], borderRadius: Radius.sm, marginBottom: Spacing.sm }, intensityBar: { height: BAR_HEIGHT, backgroundColor: Colors.gray[200], borderRadius: Radius.sm, marginBottom: Spacing.sm },
intensityFill: { height: 6, borderRadius: Radius.sm }, intensityFill: { height: BAR_HEIGHT, borderRadius: Radius.sm },
note: { fontSize: FontSize.xs, color: Colors.gray[600], marginBottom: Spacing.xs, lineHeight: 19 }, note: { fontSize: FontSize.xs, color: Colors.gray[600], marginBottom: Spacing.xs, lineHeight: 19 },
date: { fontSize: FontSize.xs, color: Colors.gray[400], textTransform: 'capitalize' }, date: { fontSize: FontSize.xs, color: Colors.gray[400], textTransform: 'capitalize' },
actions: { gap: Spacing.sm }, actions: { gap: Spacing.sm },
@@ -153,9 +165,9 @@ const styles = StyleSheet.create({
fab: { fab: {
position: 'absolute', position: 'absolute',
right: Spacing.lg, right: Spacing.lg,
bottom: Spacing.lg, bottom: Spacing.xl,
width: 56, width: FAB_SIZE,
height: 56, height: FAB_SIZE,
borderRadius: Radius.sm, borderRadius: Radius.sm,
backgroundColor: Colors.primary, backgroundColor: Colors.primary,
justifyContent: 'center', justifyContent: 'center',
+22 -8
View File
@@ -8,6 +8,14 @@ import { Colors } from '@/lib/colors';
import { Spacing, Radius, FontSize, FontWeight } from '@/lib/theme'; import { Spacing, Radius, FontSize, FontWeight } from '@/lib/theme';
import { Button, TextField } from '@/components/ui'; import { Button, TextField } from '@/components/ui';
/** Dimensions locales (multiples de l'échelle 4px du DSFR). */
const STEP_CIRCLE = 36;
const STEP_LINE_WIDTH = 40;
const STEP_LINE_HEIGHT = 3;
const STEP_CHECK_ICON = 18;
const INTENSITY_LINE_HEIGHT = 60;
const DOT_SIZE = 44;
export default function NewEntryScreen() { export default function NewEntryScreen() {
const router = useRouter(); const router = useRouter();
const createSaisie = useCreateSaisie(); const createSaisie = useCreateSaisie();
@@ -34,12 +42,16 @@ export default function NewEntryScreen() {
return ( return (
<ScrollView style={styles.container} contentContainerStyle={styles.content}> <ScrollView style={styles.container} contentContainerStyle={styles.content}>
{/* Stepper */} {/* Stepper */}
<View style={styles.stepper}> <View
style={styles.stepper}
accessibilityRole="progressbar"
accessibilityLabel={`Étape ${step} sur 3`}
>
{[1, 2, 3].map((s) => ( {[1, 2, 3].map((s) => (
<View key={s} style={styles.stepRow}> <View key={s} style={styles.stepRow}>
<View style={[styles.stepCircle, step >= s && styles.stepActive]}> <View style={[styles.stepCircle, step >= s && styles.stepActive]}>
{step > s ? ( {step > s ? (
<Ionicons name="checkmark" size={18} color={Colors.white} /> <Ionicons name="checkmark" size={STEP_CHECK_ICON} color={Colors.white} />
) : ( ) : (
<Text style={[styles.stepNumber, step >= s && styles.stepNumberActive]}>{s}</Text> <Text style={[styles.stepNumber, step >= s && styles.stepNumberActive]}>{s}</Text>
)} )}
@@ -123,6 +135,8 @@ export function IntensityPicker({ value, onChange }: { value: number; onChange:
key={v} key={v}
style={[pickerStyles.dot, active && pickerStyles.dotActive]} style={[pickerStyles.dot, active && pickerStyles.dotActive]}
onPress={() => onChange(v)} onPress={() => onChange(v)}
accessibilityRole="button"
accessibilityState={{ selected: active }}
accessibilityLabel={`Intensité ${v}`} accessibilityLabel={`Intensité ${v}`}
> >
<Text style={[pickerStyles.dotText, active && pickerStyles.dotTextActive]}>{v}</Text> <Text style={[pickerStyles.dotText, active && pickerStyles.dotTextActive]}>{v}</Text>
@@ -136,8 +150,8 @@ export function IntensityPicker({ value, onChange }: { value: number; onChange:
const pickerStyles = StyleSheet.create({ const pickerStyles = StyleSheet.create({
grid: { flexDirection: 'row', flexWrap: 'wrap', gap: Spacing.sm, justifyContent: 'center' }, grid: { flexDirection: 'row', flexWrap: 'wrap', gap: Spacing.sm, justifyContent: 'center' },
dot: { dot: {
width: 44, width: DOT_SIZE,
height: 44, height: DOT_SIZE,
borderRadius: Radius.sm, borderRadius: Radius.sm,
backgroundColor: Colors.gray[100], backgroundColor: Colors.gray[100],
borderWidth: 1, borderWidth: 1,
@@ -156,8 +170,8 @@ const styles = StyleSheet.create({
stepper: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', marginBottom: Spacing.xxl }, stepper: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', marginBottom: Spacing.xxl },
stepRow: { flexDirection: 'row', alignItems: 'center' }, stepRow: { flexDirection: 'row', alignItems: 'center' },
stepCircle: { stepCircle: {
width: 36, width: STEP_CIRCLE,
height: 36, height: STEP_CIRCLE,
borderRadius: Radius.pill, borderRadius: Radius.pill,
backgroundColor: Colors.gray[200], backgroundColor: Colors.gray[200],
justifyContent: 'center', justifyContent: 'center',
@@ -166,11 +180,11 @@ const styles = StyleSheet.create({
stepActive: { backgroundColor: Colors.primary }, stepActive: { backgroundColor: Colors.primary },
stepNumber: { fontSize: FontSize.md, fontWeight: FontWeight.bold, color: Colors.gray[500] }, stepNumber: { fontSize: FontSize.md, fontWeight: FontWeight.bold, color: Colors.gray[500] },
stepNumberActive: { color: Colors.white }, stepNumberActive: { color: Colors.white },
stepLine: { width: 40, height: 3, backgroundColor: Colors.gray[200], marginHorizontal: Spacing.xs }, stepLine: { width: STEP_LINE_WIDTH, height: STEP_LINE_HEIGHT, backgroundColor: Colors.gray[200], marginHorizontal: Spacing.xs },
stepLineActive: { backgroundColor: Colors.primary }, stepLineActive: { backgroundColor: Colors.primary },
stepTitle: { fontSize: FontSize.xl, fontWeight: FontWeight.bold, color: Colors.black, marginBottom: Spacing.xl }, stepTitle: { fontSize: FontSize.xl, fontWeight: FontWeight.bold, color: Colors.black, marginBottom: Spacing.xl },
intensityContainer: { flexDirection: 'row', alignItems: 'baseline', justifyContent: 'center', marginBottom: Spacing.xl }, intensityContainer: { flexDirection: 'row', alignItems: 'baseline', justifyContent: 'center', marginBottom: Spacing.xl },
intensityValue: { fontSize: FontSize.display, fontWeight: FontWeight.heavy, color: Colors.primary, lineHeight: 60 }, intensityValue: { fontSize: FontSize.display, fontWeight: FontWeight.heavy, color: Colors.primary, lineHeight: INTENSITY_LINE_HEIGHT },
intensityLabel: { fontSize: FontSize.xxl, color: Colors.gray[400], marginLeft: Spacing.xs }, intensityLabel: { fontSize: FontSize.xxl, color: Colors.gray[400], marginLeft: Spacing.xs },
scaleLabels: { flexDirection: 'row', justifyContent: 'space-between', marginTop: Spacing.md, marginBottom: Spacing.xxl }, scaleLabels: { flexDirection: 'row', justifyContent: 'space-between', marginTop: Spacing.md, marginBottom: Spacing.xxl },
scaleLabel: { fontSize: FontSize.xs, color: Colors.gray[500] }, scaleLabel: { fontSize: FontSize.xs, color: Colors.gray[500] },
+13 -6
View File
@@ -3,10 +3,15 @@ import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from 'react-nati
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { useRapport } from '@/hooks/useRapport'; import { useRapport } from '@/hooks/useRapport';
import { Colors } from '@/lib/colors'; import { Colors } from '@/lib/colors';
import { Spacing, Radius, FontSize, FontWeight } from '@/lib/theme'; import { Spacing, Radius, FontSize, FontWeight, MIN_TOUCH } from '@/lib/theme';
import { AppBar, Card, Badge, SectionTitle, EmptyState, Loader } from '@/components/ui'; import { AppBar, Card, Badge, SectionTitle, EmptyState, Loader } from '@/components/ui';
import EmotionPieChart from '@/components/EmotionPieChart'; import EmotionPieChart from '@/components/EmotionPieChart';
/** Dimensions locales (multiples de l'échelle 4px du DSFR). */
const STAT_ICON = 20;
const SEGMENT_MIN_HEIGHT = MIN_TOUCH;
const BAR_HEIGHT = 6;
type Period = 'week' | 'month' | 'quarter' | 'year'; type Period = 'week' | 'month' | 'quarter' | 'year';
const PERIODS: { value: Period; label: string }[] = [ const PERIODS: { value: Period; label: string }[] = [
@@ -33,6 +38,8 @@ export default function ReportsScreen() {
key={p.value} key={p.value}
style={[styles.segmentBtn, active && styles.segmentBtnActive]} style={[styles.segmentBtn, active && styles.segmentBtnActive]}
onPress={() => setPeriod(p.value)} onPress={() => setPeriod(p.value)}
accessibilityRole="button"
accessibilityState={{ selected: active }}
accessibilityLabel={p.label} accessibilityLabel={p.label}
> >
<Text style={[styles.segmentText, active && styles.segmentTextActive]}>{p.label}</Text> <Text style={[styles.segmentText, active && styles.segmentTextActive]}>{p.label}</Text>
@@ -49,13 +56,13 @@ export default function ReportsScreen() {
<Card padding={Spacing.lg} style={styles.statsCard}> <Card padding={Spacing.lg} style={styles.statsCard}>
<View style={styles.statsRow}> <View style={styles.statsRow}>
<View style={styles.statCell}> <View style={styles.statCell}>
<Ionicons name="bar-chart" size={20} color={Colors.primary} /> <Ionicons name="bar-chart" size={STAT_ICON} color={Colors.primary} />
<Text style={styles.statValue}>{rapport.stats.total_saisies}</Text> <Text style={styles.statValue}>{rapport.stats.total_saisies}</Text>
<Text style={styles.statLabel}>Saisies</Text> <Text style={styles.statLabel}>Saisies</Text>
</View> </View>
<View style={styles.statDivider} /> <View style={styles.statDivider} />
<View style={styles.statCell}> <View style={styles.statCell}>
<Ionicons name="trending-up" size={20} color={Colors.primary} /> <Ionicons name="trending-up" size={STAT_ICON} color={Colors.primary} />
<Text style={styles.statValue}>{rapport.stats.intensite_moyenne?.toFixed(1) || '—'}</Text> <Text style={styles.statValue}>{rapport.stats.intensite_moyenne?.toFixed(1) || '—'}</Text>
<Text style={styles.statLabel}>Intensité moy.</Text> <Text style={styles.statLabel}>Intensité moy.</Text>
</View> </View>
@@ -134,7 +141,7 @@ const styles = StyleSheet.create({
container: { flex: 1 }, container: { flex: 1 },
content: { padding: Spacing.lg, paddingBottom: Spacing.xxxl }, content: { padding: Spacing.lg, paddingBottom: Spacing.xxxl },
segment: { flexDirection: 'row', backgroundColor: Colors.gray[100], borderRadius: Radius.sm, padding: Spacing.xs, marginBottom: Spacing.lg }, segment: { flexDirection: 'row', backgroundColor: Colors.gray[100], borderRadius: Radius.sm, padding: Spacing.xs, marginBottom: Spacing.lg },
segmentBtn: { flex: 1, paddingVertical: Spacing.sm, alignItems: 'center', borderRadius: Radius.sm, minHeight: 40, justifyContent: 'center' }, segmentBtn: { flex: 1, paddingVertical: Spacing.sm, alignItems: 'center', borderRadius: Radius.sm, minHeight: SEGMENT_MIN_HEIGHT, justifyContent: 'center' },
segmentBtnActive: { backgroundColor: Colors.primary }, segmentBtnActive: { backgroundColor: Colors.primary },
segmentText: { fontSize: FontSize.xs, fontWeight: FontWeight.semibold, color: Colors.gray[500] }, segmentText: { fontSize: FontSize.xs, fontWeight: FontWeight.semibold, color: Colors.gray[500] },
segmentTextActive: { color: Colors.white }, segmentTextActive: { color: Colors.white },
@@ -152,8 +159,8 @@ const styles = StyleSheet.create({
repItemBorder: { borderTopWidth: 1, borderTopColor: Colors.gray[200] }, repItemBorder: { borderTopWidth: 1, borderTopColor: Colors.gray[200] },
repHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: Spacing.sm }, repHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: Spacing.sm },
repPct: { fontSize: FontSize.sm, fontWeight: FontWeight.bold, color: Colors.black }, repPct: { fontSize: FontSize.sm, fontWeight: FontWeight.bold, color: Colors.black },
progressBar: { height: 6, backgroundColor: Colors.gray[200], borderRadius: Radius.sm, marginBottom: Spacing.xs }, progressBar: { height: BAR_HEIGHT, backgroundColor: Colors.gray[200], borderRadius: Radius.sm, marginBottom: Spacing.xs },
progressFill: { height: 6, borderRadius: Radius.sm }, progressFill: { height: BAR_HEIGHT, borderRadius: Radius.sm },
repFooter: { flexDirection: 'row', justifyContent: 'space-between' }, repFooter: { flexDirection: 'row', justifyContent: 'space-between' },
repDetail: { fontSize: FontSize.xs, color: Colors.gray[500] }, repDetail: { fontSize: FontSize.xs, color: Colors.gray[500] },
}); });
+8 -3
View File
@@ -3,7 +3,7 @@ import { useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { useAuthStore } from '@/lib/auth-store'; import { useAuthStore } from '@/lib/auth-store';
import { Colors } from '@/lib/colors'; import { Colors } from '@/lib/colors';
import { FontSize, FontWeight, MIN_TOUCH, Radius, Spacing } from '@/lib/theme'; import { FontSize, FontWeight, HIT_SLOP, MIN_TOUCH, Radius, Spacing, Tint } from '@/lib/theme';
import { AppBar, Badge, Button, Card, SectionTitle } from '@/components/ui'; import { AppBar, Badge, Button, Card, SectionTitle } from '@/components/ui';
export default function ProfileScreen() { export default function ProfileScreen() {
@@ -101,7 +101,12 @@ function MenuItem({
last?: boolean; last?: boolean;
}) { }) {
return ( return (
<TouchableOpacity style={[styles.menuItem, last && styles.menuItemLast]} accessibilityRole="button"> <TouchableOpacity
style={[styles.menuItem, last && styles.menuItemLast]}
accessibilityRole="button"
accessibilityLabel={label}
hitSlop={HIT_SLOP}
>
<Ionicons name={icon} size={20} color={Colors.gray[600]} /> <Ionicons name={icon} size={20} color={Colors.gray[600]} />
<Text style={styles.menuLabel}>{label}</Text> <Text style={styles.menuLabel}>{label}</Text>
<Ionicons name="chevron-forward" size={18} color={Colors.gray[400]} /> <Ionicons name="chevron-forward" size={18} color={Colors.gray[400]} />
@@ -133,7 +138,7 @@ const styles = StyleSheet.create({
width: 48, width: 48,
height: 48, height: 48,
borderRadius: Radius.sm, borderRadius: Radius.sm,
backgroundColor: '#E3E3FD', backgroundColor: Tint.primary,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
}, },
+2 -1
View File
@@ -4,6 +4,7 @@ import { StatusBar } from 'expo-status-bar';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useAuthStore } from '@/lib/auth-store'; import { useAuthStore } from '@/lib/auth-store';
import { View, ActivityIndicator } from 'react-native'; import { View, ActivityIndicator } from 'react-native';
import { Colors } from '@/lib/colors';
const queryClient = new QueryClient(); const queryClient = new QueryClient();
@@ -33,7 +34,7 @@ function AuthGuard({ children }: { children: React.ReactNode }) {
if (isLoading) { if (isLoading) {
return ( return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#fff' }}> <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#fff' }}>
<ActivityIndicator size="large" color="#000091" /> <ActivityIndicator size="large" color={Colors.primary} />
</View> </View>
); );
} }
+5 -1
View File
@@ -19,6 +19,9 @@ import { Colors } from '@/lib/colors';
import { FontSize, FontWeight, HIT_SLOP, Radius, Spacing } from '@/lib/theme'; import { FontSize, FontWeight, HIT_SLOP, Radius, Spacing } from '@/lib/theme';
import type { Feed } from '@/types'; import type { Feed } from '@/types';
/** Voile sombre derrière la modale (noir DSFR à 40 % d'opacité). */
const OVERLAY = 'rgba(22, 22, 22, 0.4)';
interface FormState { interface FormState {
id?: number; id?: number;
titre: string; titre: string;
@@ -160,6 +163,7 @@ export default function AdminContenusScreen() {
<Switch <Switch
value={form.est_publie} value={form.est_publie}
onValueChange={(v) => setForm((f) => ({ ...f, est_publie: v }))} onValueChange={(v) => setForm((f) => ({ ...f, est_publie: v }))}
accessibilityLabel="Publier le contenu"
trackColor={{ true: Colors.primary, false: Colors.gray[300] }} trackColor={{ true: Colors.primary, false: Colors.gray[300] }}
thumbColor={Colors.white} thumbColor={Colors.white}
/> />
@@ -186,7 +190,7 @@ const styles = StyleSheet.create({
badges: { flexDirection: 'row', gap: Spacing.sm, flexWrap: 'wrap' }, badges: { flexDirection: 'row', gap: Spacing.sm, flexWrap: 'wrap' },
extrait: { fontSize: FontSize.sm, color: Colors.gray[500], lineHeight: 20 }, extrait: { fontSize: FontSize.sm, color: Colors.gray[500], lineHeight: 20 },
actions: { flexDirection: 'row', gap: Spacing.sm, marginTop: Spacing.xs }, actions: { flexDirection: 'row', gap: Spacing.sm, marginTop: Spacing.xs },
modalOverlay: { flex: 1, justifyContent: 'flex-end', backgroundColor: 'rgba(22,22,22,0.4)' }, modalOverlay: { flex: 1, justifyContent: 'flex-end', backgroundColor: OVERLAY },
modalSheet: { modalSheet: {
backgroundColor: Colors.white, backgroundColor: Colors.white,
borderTopLeftRadius: Radius.md, borderTopLeftRadius: Radius.md,
+53 -15
View File
@@ -15,9 +15,20 @@ import { Ionicons } from '@expo/vector-icons';
import { Button, Card, EmptyState, Loader, SectionTitle, TextField } from '@/components/ui'; import { Button, Card, EmptyState, Loader, SectionTitle, TextField } from '@/components/ui';
import { useAdminEmotions, useSaveEmotion, useDeleteEmotion } from '@/hooks/useAdmin'; import { useAdminEmotions, useSaveEmotion, useDeleteEmotion } from '@/hooks/useAdmin';
import { Colors } from '@/lib/colors'; import { Colors } from '@/lib/colors';
import { FontSize, FontWeight, HIT_SLOP, Radius, Spacing } from '@/lib/theme'; import { FontSize, FontWeight, HIT_SLOP, MIN_TOUCH, Radius, Spacing, Tint } from '@/lib/theme';
import type { Emotion } from '@/types'; import type { Emotion } from '@/types';
/** Voile sombre derrière la modale (noir DSFR à 40 % d'opacité). */
const OVERLAY = 'rgba(22, 22, 22, 0.4)';
/** Format hexadécimal #RRGGBB attendu pour une couleur d'émotion. */
const HEX_RE = /^#([0-9A-Fa-f]{6})$/;
/** Diamètre de la pastille de couleur d'une émotion en liste. */
const PASTILLE = 36;
/** Côté de l'aperçu de couleur dans le formulaire (cible tactile). */
const APERCU = MIN_TOUCH;
/** Diamètre de la pastille de couleur d'une émotion parente (chip). */
const PARENT_DOT = 14;
interface FormState { interface FormState {
id?: number; id?: number;
nom: string; nom: string;
@@ -28,7 +39,7 @@ interface FormState {
est_actif: boolean; est_actif: boolean;
} }
const FORM_VIDE: FormState = { nom: '', couleur: '#000091', icone: '', niveau: 1, parent_id: null, est_actif: true }; const FORM_VIDE: FormState = { nom: '', couleur: Colors.primary, icone: '', niveau: 1, parent_id: null, est_actif: true };
function messageErreur(e: unknown, defaut: string): string { function messageErreur(e: unknown, defaut: string): string {
const err = e as { response?: { data?: { message?: string } } }; const err = e as { response?: { data?: { message?: string } } };
@@ -83,7 +94,7 @@ export default function AdminEmotionsScreen() {
Alert.alert('Champs requis', 'Le nom est obligatoire.'); Alert.alert('Champs requis', 'Le nom est obligatoire.');
return; return;
} }
if (!/^#([0-9A-Fa-f]{6})$/.test(form.couleur.trim())) { if (!HEX_RE.test(form.couleur.trim())) {
Alert.alert('Couleur', 'Saisissez une couleur hexadécimale (ex. #000091).'); Alert.alert('Couleur', 'Saisissez une couleur hexadécimale (ex. #000091).');
return; return;
} }
@@ -127,7 +138,7 @@ export default function AdminEmotionsScreen() {
const renderEmotion = (e: Emotion, estEnfant: boolean) => ( const renderEmotion = (e: Emotion, estEnfant: boolean) => (
<Card key={e.id} style={[styles.emotionCard, estEnfant && styles.emotionEnfant]}> <Card key={e.id} style={[styles.emotionCard, estEnfant && styles.emotionEnfant]}>
<View style={styles.emotionRow}> <View style={styles.emotionRow}>
<View style={[styles.pastille, { backgroundColor: e.couleur }]}> <View style={[styles.pastille, { backgroundColor: e.couleur }]} accessibilityElementsHidden>
{e.icone ? <Text style={styles.pastilleIcone}>{e.icone}</Text> : null} {e.icone ? <Text style={styles.pastilleIcone}>{e.icone}</Text> : null}
</View> </View>
<View style={styles.emotionInfo}> <View style={styles.emotionInfo}>
@@ -195,7 +206,12 @@ export default function AdminEmotionsScreen() {
required required
/> />
</View> </View>
<View style={[styles.couleurApercu, { backgroundColor: /^#([0-9A-Fa-f]{6})$/.test(form.couleur) ? form.couleur : Colors.gray[200] }]} /> <View
style={[
styles.couleurApercu,
{ backgroundColor: HEX_RE.test(form.couleur) ? form.couleur : Colors.gray[200] },
]}
/>
</View> </View>
<TextField <TextField
label="Icône (emoji, optionnel)" label="Icône (emoji, optionnel)"
@@ -206,8 +222,10 @@ export default function AdminEmotionsScreen() {
/> />
<Text style={styles.fieldLabel}>Niveau</Text> <Text style={styles.fieldLabel}>Niveau</Text>
<View style={styles.segment}> <View style={styles.segment} accessibilityRole="radiogroup">
<Pressable <Pressable
accessibilityRole="radio"
accessibilityState={{ selected: form.niveau === 1 }}
style={[styles.segmentItem, form.niveau === 1 && styles.segmentItemActive]} style={[styles.segmentItem, form.niveau === 1 && styles.segmentItemActive]}
onPress={() => setForm((f) => ({ ...f, niveau: 1, parent_id: null }))} onPress={() => setForm((f) => ({ ...f, niveau: 1, parent_id: null }))}
> >
@@ -216,6 +234,8 @@ export default function AdminEmotionsScreen() {
</Text> </Text>
</Pressable> </Pressable>
<Pressable <Pressable
accessibilityRole="radio"
accessibilityState={{ selected: form.niveau === 2 }}
style={[styles.segmentItem, form.niveau === 2 && styles.segmentItemActive]} style={[styles.segmentItem, form.niveau === 2 && styles.segmentItemActive]}
onPress={() => setForm((f) => ({ ...f, niveau: 2 }))} onPress={() => setForm((f) => ({ ...f, niveau: 2 }))}
> >
@@ -228,7 +248,7 @@ export default function AdminEmotionsScreen() {
{form.niveau === 2 ? ( {form.niveau === 2 ? (
<> <>
<Text style={styles.fieldLabel}>Émotion parente</Text> <Text style={styles.fieldLabel}>Émotion parente</Text>
<View style={styles.parentList}> <View style={styles.parentList} accessibilityRole="radiogroup">
{parents.length === 0 ? ( {parents.length === 0 ? (
<Text style={styles.switchHelper}>Aucune émotion de niveau 1 disponible.</Text> <Text style={styles.switchHelper}>Aucune émotion de niveau 1 disponible.</Text>
) : ( ) : (
@@ -237,6 +257,9 @@ export default function AdminEmotionsScreen() {
return ( return (
<Pressable <Pressable
key={p.id} key={p.id}
accessibilityRole="radio"
accessibilityState={{ selected: actif }}
accessibilityLabel={p.nom}
style={[styles.parentChip, actif && styles.parentChipActive]} style={[styles.parentChip, actif && styles.parentChipActive]}
onPress={() => setForm((f) => ({ ...f, parent_id: p.id }))} onPress={() => setForm((f) => ({ ...f, parent_id: p.id }))}
> >
@@ -260,6 +283,7 @@ export default function AdminEmotionsScreen() {
<Switch <Switch
value={form.est_actif} value={form.est_actif}
onValueChange={(v) => setForm((f) => ({ ...f, est_actif: v }))} onValueChange={(v) => setForm((f) => ({ ...f, est_actif: v }))}
accessibilityLabel="Émotion active dans le tracker"
trackColor={{ true: Colors.primary, false: Colors.gray[300] }} trackColor={{ true: Colors.primary, false: Colors.gray[300] }}
thumbColor={Colors.white} thumbColor={Colors.white}
/> />
@@ -285,13 +309,21 @@ const styles = StyleSheet.create({
emotionCard: { padding: Spacing.md, gap: Spacing.sm }, emotionCard: { padding: Spacing.md, gap: Spacing.sm },
emotionEnfant: { marginLeft: Spacing.xl, borderColor: Colors.gray[100] }, emotionEnfant: { marginLeft: Spacing.xl, borderColor: Colors.gray[100] },
emotionRow: { flexDirection: 'row', alignItems: 'center', gap: Spacing.md }, emotionRow: { flexDirection: 'row', alignItems: 'center', gap: Spacing.md },
pastille: { width: 36, height: 36, borderRadius: Radius.pill, alignItems: 'center', justifyContent: 'center' }, pastille: {
pastilleIcone: { fontSize: 18 }, width: PASTILLE,
height: PASTILLE,
borderRadius: Radius.pill,
borderWidth: 1,
borderColor: Colors.gray[200],
alignItems: 'center',
justifyContent: 'center',
},
pastilleIcone: { fontSize: FontSize.lg },
emotionInfo: { flex: 1 }, emotionInfo: { flex: 1 },
emotionNom: { fontSize: FontSize.md, fontWeight: FontWeight.semibold, color: Colors.black }, emotionNom: { fontSize: FontSize.md, fontWeight: FontWeight.semibold, color: Colors.black },
emotionMeta: { fontSize: FontSize.xs, color: Colors.gray[500], marginTop: 2 }, emotionMeta: { fontSize: FontSize.xs, color: Colors.gray[500], marginTop: 2 },
actions: { flexDirection: 'row', gap: Spacing.sm }, actions: { flexDirection: 'row', gap: Spacing.sm },
modalOverlay: { flex: 1, justifyContent: 'flex-end', backgroundColor: 'rgba(22,22,22,0.4)' }, modalOverlay: { flex: 1, justifyContent: 'flex-end', backgroundColor: OVERLAY },
modalSheet: { modalSheet: {
backgroundColor: Colors.white, backgroundColor: Colors.white,
borderTopLeftRadius: Radius.md, borderTopLeftRadius: Radius.md,
@@ -312,12 +344,12 @@ const styles = StyleSheet.create({
couleurRow: { flexDirection: 'row', alignItems: 'flex-start', gap: Spacing.md }, couleurRow: { flexDirection: 'row', alignItems: 'flex-start', gap: Spacing.md },
couleurField: { flex: 1 }, couleurField: { flex: 1 },
couleurApercu: { couleurApercu: {
width: 44, width: APERCU,
height: 44, height: APERCU,
borderRadius: Radius.sm, borderRadius: Radius.sm,
borderWidth: 1, borderWidth: 1,
borderColor: Colors.gray[200], borderColor: Colors.gray[200],
marginTop: 24, marginTop: Spacing.xl,
}, },
segment: { segment: {
flexDirection: 'row', flexDirection: 'row',
@@ -344,8 +376,14 @@ const styles = StyleSheet.create({
backgroundColor: Colors.white, backgroundColor: Colors.white,
minHeight: 44, minHeight: 44,
}, },
parentChipActive: { borderColor: Colors.primary, backgroundColor: '#E3E3FD' }, parentChipActive: { borderColor: Colors.primary, backgroundColor: Tint.primary },
parentDot: { width: 14, height: 14, borderRadius: Radius.pill }, parentDot: {
width: PARENT_DOT,
height: PARENT_DOT,
borderRadius: Radius.pill,
borderWidth: 1,
borderColor: Colors.gray[200],
},
parentChipText: { fontSize: FontSize.sm, color: Colors.gray[600] }, parentChipText: { fontSize: FontSize.sm, color: Colors.gray[600] },
parentChipTextActive: { color: Colors.primary, fontWeight: FontWeight.semibold }, parentChipTextActive: { color: Colors.primary, fontWeight: FontWeight.semibold },
switchRow: { switchRow: {
+10 -5
View File
@@ -4,7 +4,12 @@ import { Ionicons } from '@expo/vector-icons';
import { Card, SectionTitle } from '@/components/ui'; import { Card, SectionTitle } from '@/components/ui';
import { useAdminUtilisateurs, useAdminFeeds, useAdminEmotions } from '@/hooks/useAdmin'; import { useAdminUtilisateurs, useAdminFeeds, useAdminEmotions } from '@/hooks/useAdmin';
import { Colors } from '@/lib/colors'; import { Colors } from '@/lib/colors';
import { FontSize, FontWeight, Radius, Spacing } from '@/lib/theme'; import { FontSize, FontWeight, MIN_TOUCH, Radius, Spacing, Tint } from '@/lib/theme';
/** Diamètre du rond d'icône de section (cible tactile confortable). */
const ICON_CIRCLE = 48;
/** Largeur minimale de la pastille compteur (alignement des chiffres). */
const COUNT_MIN_WIDTH = MIN_TOUCH / 2;
interface HubItem { interface HubItem {
href: '/admin/utilisateurs' | '/admin/contenus' | '/admin/emotions'; href: '/admin/utilisateurs' | '/admin/contenus' | '/admin/emotions';
@@ -83,10 +88,10 @@ const styles = StyleSheet.create({
card: { padding: Spacing.lg }, card: { padding: Spacing.lg },
row: { flexDirection: 'row', alignItems: 'center', gap: Spacing.md }, row: { flexDirection: 'row', alignItems: 'center', gap: Spacing.md },
iconCircle: { iconCircle: {
width: 48, width: ICON_CIRCLE,
height: 48, height: ICON_CIRCLE,
borderRadius: Radius.md, borderRadius: Radius.md,
backgroundColor: '#E3E3FD', backgroundColor: Tint.primary,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
}, },
@@ -98,7 +103,7 @@ const styles = StyleSheet.create({
fontSize: FontSize.sm, fontSize: FontSize.sm,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.primary, color: Colors.primary,
minWidth: 22, minWidth: COUNT_MIN_WIDTH,
textAlign: 'right', textAlign: 'right',
}, },
}); });
+9 -2
View File
@@ -26,6 +26,9 @@ import type { Utilisateur } from '@/types';
const ROLE_MEMBRE = 2; const ROLE_MEMBRE = 2;
const ROLE_ADMIN = 3; const ROLE_ADMIN = 3;
/** Voile sombre derrière la modale (noir DSFR à 40 % d'opacité). */
const OVERLAY = 'rgba(22, 22, 22, 0.4)';
interface FormState { interface FormState {
prenom: string; prenom: string;
nom: string; nom: string;
@@ -192,8 +195,10 @@ export default function AdminUtilisateursScreen() {
/> />
<Text style={styles.fieldLabel}>Rôle</Text> <Text style={styles.fieldLabel}>Rôle</Text>
<View style={styles.segment}> <View style={styles.segment} accessibilityRole="radiogroup">
<Pressable <Pressable
accessibilityRole="radio"
accessibilityState={{ selected: form.role_id === ROLE_MEMBRE }}
style={[styles.segmentItem, form.role_id === ROLE_MEMBRE && styles.segmentItemActive]} style={[styles.segmentItem, form.role_id === ROLE_MEMBRE && styles.segmentItemActive]}
onPress={() => setForm((f) => ({ ...f, role_id: ROLE_MEMBRE }))} onPress={() => setForm((f) => ({ ...f, role_id: ROLE_MEMBRE }))}
> >
@@ -202,6 +207,8 @@ export default function AdminUtilisateursScreen() {
</Text> </Text>
</Pressable> </Pressable>
<Pressable <Pressable
accessibilityRole="radio"
accessibilityState={{ selected: form.role_id === ROLE_ADMIN }}
style={[styles.segmentItem, form.role_id === ROLE_ADMIN && styles.segmentItemActive]} style={[styles.segmentItem, form.role_id === ROLE_ADMIN && styles.segmentItemActive]}
onPress={() => setForm((f) => ({ ...f, role_id: ROLE_ADMIN }))} onPress={() => setForm((f) => ({ ...f, role_id: ROLE_ADMIN }))}
> >
@@ -234,7 +241,7 @@ const styles = StyleSheet.create({
email: { fontSize: FontSize.sm, color: Colors.gray[500], marginTop: 2 }, email: { fontSize: FontSize.sm, color: Colors.gray[500], marginTop: 2 },
badges: { flexDirection: 'row', gap: Spacing.sm, flexWrap: 'wrap' }, badges: { flexDirection: 'row', gap: Spacing.sm, flexWrap: 'wrap' },
actions: { flexDirection: 'row', gap: Spacing.sm, marginTop: Spacing.xs }, actions: { flexDirection: 'row', gap: Spacing.sm, marginTop: Spacing.xs },
modalOverlay: { flex: 1, justifyContent: 'flex-end', backgroundColor: 'rgba(22,22,22,0.4)' }, modalOverlay: { flex: 1, justifyContent: 'flex-end', backgroundColor: OVERLAY },
modalSheet: { modalSheet: {
backgroundColor: Colors.white, backgroundColor: Colors.white,
borderTopLeftRadius: Radius.md, borderTopLeftRadius: Radius.md,
+7 -7
View File
@@ -3,7 +3,7 @@ import { View, Text, StyleSheet, KeyboardAvoidingView, Platform, ScrollView } fr
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { Colors } from '@/lib/colors'; import { Colors } from '@/lib/colors';
import { FontSize, FontWeight, Radius, Spacing } from '@/lib/theme'; import { FontSize, FontWeight, ICON_CIRCLE, IconSize, LineHeight, Radius, Spacing, Tint } from '@/lib/theme';
import { AppBar, Button, Card, TextField } from '@/components/ui'; import { AppBar, Button, Card, TextField } from '@/components/ui';
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
@@ -33,7 +33,7 @@ export default function ForgotPasswordScreen() {
<AppBar title="Demande enregistrée" /> <AppBar title="Demande enregistrée" />
<View style={styles.successWrap}> <View style={styles.successWrap}>
<View style={styles.iconCircle}> <View style={styles.iconCircle}>
<Ionicons name="mail-open" size={32} color={Colors.primary} /> <Ionicons name="mail-open" size={IconSize.xl} color={Colors.primary} />
</View> </View>
<Text style={styles.successTitle}>Vérifiez votre boîte mail</Text> <Text style={styles.successTitle}>Vérifiez votre boîte mail</Text>
<Text style={styles.successText}> <Text style={styles.successText}>
@@ -102,20 +102,20 @@ export default function ForgotPasswordScreen() {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: Colors.gray[50] }, container: { flex: 1, backgroundColor: Colors.gray[50] },
scrollContent: { flexGrow: 1, justifyContent: 'center', padding: Spacing.xl }, scrollContent: { flexGrow: 1, justifyContent: 'center', padding: Spacing.xl },
intro: { fontSize: FontSize.sm, color: Colors.gray[600], lineHeight: 20, marginBottom: Spacing.xl, textAlign: 'center' }, intro: { fontSize: FontSize.sm, color: Colors.gray[600], lineHeight: LineHeight.normal, marginBottom: Spacing.xl, textAlign: 'center' },
backRow: { alignItems: 'center', marginTop: Spacing.sm }, backRow: { alignItems: 'center', marginTop: Spacing.sm },
successWrap: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: Spacing.xl }, successWrap: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: Spacing.xl },
iconCircle: { iconCircle: {
width: 72, width: ICON_CIRCLE,
height: 72, height: ICON_CIRCLE,
borderRadius: Radius.pill, borderRadius: Radius.pill,
backgroundColor: '#E3E3FD', backgroundColor: Tint.primary,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
marginBottom: Spacing.lg, marginBottom: Spacing.lg,
}, },
successTitle: { fontSize: FontSize.xl, fontWeight: FontWeight.heavy, color: Colors.black, marginBottom: Spacing.md, textAlign: 'center' }, successTitle: { fontSize: FontSize.xl, fontWeight: FontWeight.heavy, color: Colors.black, marginBottom: Spacing.md, textAlign: 'center' },
successText: { fontSize: FontSize.sm, color: Colors.gray[600], textAlign: 'center', lineHeight: 20, marginBottom: Spacing.md }, successText: { fontSize: FontSize.sm, color: Colors.gray[600], textAlign: 'center', lineHeight: LineHeight.normal, marginBottom: Spacing.md },
bold: { fontWeight: FontWeight.bold, color: Colors.black }, bold: { fontWeight: FontWeight.bold, color: Colors.black },
successAction: { alignSelf: 'stretch', marginTop: Spacing.lg }, successAction: { alignSelf: 'stretch', marginTop: Spacing.lg },
}); });
+4 -4
View File
@@ -5,7 +5,7 @@ import { Ionicons } from '@expo/vector-icons';
import { useAuthStore } from '@/lib/auth-store'; import { useAuthStore } from '@/lib/auth-store';
import { getApiError } from '@/lib/api'; import { getApiError } from '@/lib/api';
import { Colors } from '@/lib/colors'; import { Colors } from '@/lib/colors';
import { FontSize, FontWeight, Radius, Spacing } from '@/lib/theme'; import { FontSize, FontWeight, IconSize, Radius, Spacing } from '@/lib/theme';
import { AppBar, Button, Card, TextField } from '@/components/ui'; import { AppBar, Button, Card, TextField } from '@/components/ui';
export default function LoginScreen() { export default function LoginScreen() {
@@ -46,8 +46,8 @@ export default function LoginScreen() {
<Card padding={Spacing.xl} elevated> <Card padding={Spacing.xl} elevated>
{error ? ( {error ? (
<View style={styles.errorBanner}> <View style={styles.errorBanner} accessibilityLiveRegion="polite">
<Ionicons name="alert-circle" size={18} color={Colors.error} /> <Ionicons name="alert-circle" size={IconSize.md} color={Colors.error} />
<Text style={styles.errorBannerText}>{error}</Text> <Text style={styles.errorBannerText}>{error}</Text>
</View> </View>
) : null} ) : null}
@@ -118,7 +118,7 @@ const styles = StyleSheet.create({
gap: Spacing.sm, gap: Spacing.sm,
backgroundColor: Colors.errorBg, backgroundColor: Colors.errorBg,
borderRadius: Radius.sm, borderRadius: Radius.sm,
borderLeftWidth: 3, borderLeftWidth: Spacing.xs - 1,
borderLeftColor: Colors.error, borderLeftColor: Colors.error,
padding: Spacing.md, padding: Spacing.md,
marginBottom: Spacing.lg, marginBottom: Spacing.lg,
+5 -5
View File
@@ -5,7 +5,7 @@ import { Ionicons } from '@expo/vector-icons';
import { useAuthStore } from '@/lib/auth-store'; import { useAuthStore } from '@/lib/auth-store';
import { getApiError } from '@/lib/api'; import { getApiError } from '@/lib/api';
import { Colors } from '@/lib/colors'; import { Colors } from '@/lib/colors';
import { FontSize, FontWeight, MIN_TOUCH, Radius, Spacing } from '@/lib/theme'; import { FontSize, FontWeight, IconSize, LineHeight, MIN_TOUCH, Radius, Spacing } from '@/lib/theme';
import { AppBar, Button, Card, TextField } from '@/components/ui'; import { AppBar, Button, Card, TextField } from '@/components/ui';
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
@@ -132,7 +132,7 @@ export default function RegisterScreen() {
> >
<Ionicons <Ionicons
name={consentement ? 'checkbox' : 'square-outline'} name={consentement ? 'checkbox' : 'square-outline'}
size={24} size={IconSize.lg}
color={consentement ? Colors.primary : Colors.gray[500]} color={consentement ? Colors.primary : Colors.gray[500]}
style={styles.consentIcon} style={styles.consentIcon}
/> />
@@ -143,7 +143,7 @@ export default function RegisterScreen() {
</Pressable> </Pressable>
{consentError ? ( {consentError ? (
<View style={styles.consentErrorRow}> <View style={styles.consentErrorRow}>
<Ionicons name="alert-circle" size={14} color={Colors.error} /> <Ionicons name="alert-circle" size={IconSize.sm} color={Colors.error} />
<Text style={styles.consentErrorText}>{consentError}</Text> <Text style={styles.consentErrorText}>{consentError}</Text>
</View> </View>
) : null} ) : null}
@@ -185,8 +185,8 @@ const styles = StyleSheet.create({
paddingVertical: Spacing.sm, paddingVertical: Spacing.sm,
minHeight: MIN_TOUCH, minHeight: MIN_TOUCH,
}, },
consentIcon: { marginTop: 1 }, consentIcon: { marginTop: Spacing.xs / 4 },
consentText: { flex: 1, fontSize: FontSize.xs, color: Colors.black, lineHeight: 19 }, consentText: { flex: 1, fontSize: FontSize.xs, color: Colors.black, lineHeight: LineHeight.snug },
consentLink: { color: Colors.primary, fontWeight: FontWeight.semibold, textDecorationLine: 'underline' }, consentLink: { color: Colors.primary, fontWeight: FontWeight.semibold, textDecorationLine: 'underline' },
consentErrorRow: { flexDirection: 'row', alignItems: 'center', gap: Spacing.xs, marginTop: Spacing.xs }, consentErrorRow: { flexDirection: 'row', alignItems: 'center', gap: Spacing.xs, marginTop: Spacing.xs },
consentErrorText: { fontSize: FontSize.xs, color: Colors.error }, consentErrorText: { fontSize: FontSize.xs, color: Colors.error },
+25 -9
View File
@@ -7,6 +7,14 @@ import { Spacing, Radius, FontSize, FontWeight, MIN_TOUCH, HIT_SLOP } from '@/li
import { Loader } from '@/components/ui'; import { Loader } from '@/components/ui';
import type { Emotion } from '@/types'; import type { Emotion } from '@/types';
/** Dimensions locales (multiples de l'échelle 4px du DSFR). */
const TILE_MIN_HEIGHT = 92;
const TILE_EMOJI = 30;
const BACK_ICON = 16;
const CHEVRON_ICON = 12;
const BORDER_DEFAULT = 1.5;
const BORDER_SELECTED = 2.5;
interface EmotionPickerProps { interface EmotionPickerProps {
value?: number; value?: number;
onChange: (emotionId: number) => void; onChange: (emotionId: number) => void;
@@ -23,8 +31,14 @@ export default function EmotionPicker({ value, onChange }: EmotionPickerProps) {
if (selectedParent?.enfants && selectedParent.enfants.length > 0) { if (selectedParent?.enfants && selectedParent.enfants.length > 0) {
return ( return (
<View> <View>
<TouchableOpacity style={styles.backBtn} hitSlop={HIT_SLOP} onPress={() => setSelectedParent(null)}> <TouchableOpacity
<Ionicons name="arrow-back" size={16} color={Colors.primary} /> style={styles.backBtn}
hitSlop={HIT_SLOP}
accessibilityRole="button"
accessibilityLabel="Retour aux émotions"
onPress={() => setSelectedParent(null)}
>
<Ionicons name="arrow-back" size={BACK_ICON} color={Colors.primary} />
<Text style={styles.backText}>Retour aux émotions</Text> <Text style={styles.backText}>Retour aux émotions</Text>
</TouchableOpacity> </TouchableOpacity>
<Text style={styles.subTitle}> <Text style={styles.subTitle}>
@@ -85,16 +99,18 @@ function EmotionTile({
<TouchableOpacity <TouchableOpacity
style={[ style={[
styles.tile, styles.tile,
selected && { borderColor: emotion.couleur, borderWidth: 2.5, backgroundColor: `${emotion.couleur}12` }, selected && { borderColor: emotion.couleur, borderWidth: BORDER_SELECTED, backgroundColor: `${emotion.couleur}12` },
]} ]}
onPress={onPress} onPress={onPress}
accessibilityLabel={emotion.nom} accessibilityRole="button"
accessibilityState={{ selected }}
accessibilityLabel={hasChildren ? `${emotion.nom}, ouvrir les sous-émotions` : emotion.nom}
> >
<Text style={styles.tileEmoji}>{emotion.icone || '🔵'}</Text> <Text style={styles.tileEmoji}>{emotion.icone || '🔵'}</Text>
<Text style={styles.tileName} numberOfLines={2}> <Text style={styles.tileName} numberOfLines={2}>
{emotion.nom} {emotion.nom}
</Text> </Text>
{hasChildren && <Ionicons name="chevron-down" size={12} color={Colors.gray[400]} style={styles.chevron} />} {hasChildren && <Ionicons name="chevron-down" size={CHEVRON_ICON} color={Colors.gray[400]} style={styles.chevron} />}
</TouchableOpacity> </TouchableOpacity>
); );
} }
@@ -107,7 +123,7 @@ const styles = StyleSheet.create({
grid: { flexDirection: 'row', flexWrap: 'wrap', gap: Spacing.md, justifyContent: 'space-between' }, grid: { flexDirection: 'row', flexWrap: 'wrap', gap: Spacing.md, justifyContent: 'space-between' },
tile: { tile: {
width: '30%', width: '30%',
minHeight: 92, minHeight: TILE_MIN_HEIGHT,
backgroundColor: Colors.white, backgroundColor: Colors.white,
borderRadius: Radius.sm, borderRadius: Radius.sm,
paddingVertical: Spacing.md, paddingVertical: Spacing.md,
@@ -115,10 +131,10 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
gap: Spacing.xs, gap: Spacing.xs,
borderWidth: 1.5, borderWidth: BORDER_DEFAULT,
borderColor: Colors.gray[200], borderColor: Colors.gray[200],
}, },
tileEmoji: { fontSize: 30 }, tileEmoji: { fontSize: TILE_EMOJI },
tileName: { fontSize: FontSize.xs, fontWeight: FontWeight.semibold, color: Colors.black, textAlign: 'center' }, tileName: { fontSize: FontSize.xs, fontWeight: FontWeight.semibold, color: Colors.black, textAlign: 'center' },
chevron: { marginTop: 2 }, chevron: { marginTop: Spacing.xs / 2 },
}); });
+21 -11
View File
@@ -1,6 +1,14 @@
import { View, Text, StyleSheet } from 'react-native'; import { View, Text, StyleSheet } from 'react-native';
import Svg, { Circle, G, Text as SvgText } from 'react-native-svg'; import Svg, { Circle, G, Text as SvgText } from 'react-native-svg';
import { Colors } from '@/lib/colors'; import { Colors } from '@/lib/colors';
import { Spacing, Radius, FontSize, FontWeight } from '@/lib/theme';
/** Dimensions du donut et de sa légende. */
const CENTER_PCT_SIZE = 30;
const CENTER_LABEL_SIZE = FontSize.tiny;
const CENTER_PCT_DY = 6;
const CENTER_LABEL_DY = 16;
const LEGEND_DOT = 12;
export interface EmotionPieSlice { export interface EmotionPieSlice {
nom: string; nom: string;
@@ -84,9 +92,9 @@ export default function EmotionPieChart({
{/* Centre du donut : part dominante */} {/* Centre du donut : part dominante */}
<SvgText <SvgText
x={center} x={center}
y={center - 6} y={center - CENTER_PCT_DY}
fontSize={30} fontSize={CENTER_PCT_SIZE}
fontWeight="800" fontWeight={FontWeight.heavy}
fill={Colors.black} fill={Colors.black}
textAnchor="middle" textAnchor="middle"
> >
@@ -94,8 +102,8 @@ export default function EmotionPieChart({
</SvgText> </SvgText>
<SvgText <SvgText
x={center} x={center}
y={center + 16} y={center + CENTER_LABEL_DY}
fontSize={12} fontSize={CENTER_LABEL_SIZE}
fill={Colors.gray[500]} fill={Colors.gray[500]}
textAnchor="middle" textAnchor="middle"
> >
@@ -111,7 +119,9 @@ export default function EmotionPieChart({
<Text style={styles.legendName} numberOfLines={1}> <Text style={styles.legendName} numberOfLines={1}>
{item.nom} {item.nom}
</Text> </Text>
<Text style={styles.legendPct}>{item.pourcentage}%</Text> <Text style={styles.legendPct}>
{item.pourcentage}% · {item.count}
</Text>
</View> </View>
))} ))}
</View> </View>
@@ -120,10 +130,10 @@ export default function EmotionPieChart({
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { alignItems: 'center', marginBottom: 20 }, container: { alignItems: 'center', marginBottom: Spacing.lg },
legend: { alignSelf: 'stretch', marginTop: 20, gap: 8 }, legend: { alignSelf: 'stretch', marginTop: Spacing.lg, gap: Spacing.sm },
legendRow: { flexDirection: 'row', alignItems: 'center' }, legendRow: { flexDirection: 'row', alignItems: 'center' },
legendDot: { width: 12, height: 12, borderRadius: 6, marginRight: 10 }, legendDot: { width: LEGEND_DOT, height: LEGEND_DOT, borderRadius: Radius.pill, marginRight: Spacing.sm },
legendName: { flex: 1, fontSize: 14, color: Colors.black }, legendName: { flex: 1, fontSize: FontSize.sm, color: Colors.black },
legendPct: { fontSize: 14, fontWeight: '700', color: Colors.gray[600] }, legendPct: { fontSize: FontSize.sm, fontWeight: FontWeight.bold, color: Colors.gray[600] },
}); });
-52
View File
@@ -1,52 +0,0 @@
import { View, Text, StyleSheet } from 'react-native';
import { Colors } from '@/lib/colors';
interface RepubliqueHeaderProps {
/** Affiche le nom du service (CESIZen) à côté du bloc Marianne. */
showService?: boolean;
}
/**
* En-tête institutionnel reprenant l'identité visuelle de l'État (DSFR) :
* bloc « RÉPUBLIQUE / FRANÇAISE » accompagné du nom du service.
*/
export default function RepubliqueHeader({ showService = true }: RepubliqueHeaderProps) {
return (
<View style={styles.container}>
<View style={styles.marianneBlock}>
<Text style={styles.republique}>RÉPUBLIQUE</Text>
<Text style={styles.francaise}>FRANÇAISE</Text>
<Text style={styles.devise}>Liberté{'\n'}Égalité{'\n'}Fraternité</Text>
</View>
{showService && (
<View style={styles.serviceBlock}>
<Text style={styles.serviceName}>CESIZen</Text>
<Text style={styles.serviceTagline}>Santé mentale</Text>
</View>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 12,
backgroundColor: Colors.white,
borderBottomWidth: 1,
borderBottomColor: Colors.gray[200],
},
marianneBlock: {
paddingRight: 16,
borderRightWidth: 1,
borderRightColor: Colors.gray[200],
},
republique: { fontSize: 12, fontWeight: '700', color: Colors.black, letterSpacing: 0.5, lineHeight: 14 },
francaise: { fontSize: 12, fontWeight: '700', color: Colors.black, letterSpacing: 0.5, lineHeight: 14 },
devise: { fontSize: 8, color: Colors.gray[500], marginTop: 4, lineHeight: 10, fontStyle: 'italic' },
serviceBlock: { paddingLeft: 16 },
serviceName: { fontSize: 18, fontWeight: '800', color: Colors.primary },
serviceTagline: { fontSize: 11, color: Colors.gray[500], marginTop: 1 },
});
+6 -6
View File
@@ -1,6 +1,6 @@
import { StyleSheet, Text, View } from 'react-native'; import { StyleSheet, Text, View } from 'react-native';
import { Colors } from '@/lib/colors'; import { Colors } from '@/lib/colors';
import { FontSize, FontWeight, Spacing } from '@/lib/theme'; import { FontSize, FontWeight, LetterSpacing, LineHeight, Spacing } from '@/lib/theme';
interface AppBarProps { interface AppBarProps {
/** Titre de la page, affiché sous le bandeau institutionnel. */ /** Titre de la page, affiché sous le bandeau institutionnel. */
@@ -17,7 +17,7 @@ interface AppBarProps {
*/ */
export default function AppBar({ title, subtitle, right }: AppBarProps) { export default function AppBar({ title, subtitle, right }: AppBarProps) {
return ( return (
<View style={styles.wrapper}> <View style={styles.wrapper} accessibilityRole="header">
<View style={styles.brandRow}> <View style={styles.brandRow}>
<View style={styles.marianne}> <View style={styles.marianne}>
<Text style={styles.republique}>RÉPUBLIQUE</Text> <Text style={styles.republique}>RÉPUBLIQUE</Text>
@@ -61,12 +61,12 @@ const styles = StyleSheet.create({
fontSize: FontSize.tiny, fontSize: FontSize.tiny,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.black, color: Colors.black,
letterSpacing: 0.5, letterSpacing: LetterSpacing.sm,
lineHeight: 13, lineHeight: LineHeight.tight,
}, },
serviceBlock: { flex: 1 }, serviceBlock: { flex: 1 },
serviceName: { fontSize: FontSize.lg, fontWeight: FontWeight.heavy, color: Colors.primary }, serviceName: { fontSize: FontSize.lg, fontWeight: FontWeight.heavy, color: Colors.primary },
serviceTagline: { fontSize: FontSize.tiny, color: Colors.gray[500], marginTop: 1 }, serviceTagline: { fontSize: FontSize.tiny, color: Colors.gray[500], marginTop: Spacing.xs / 4 },
right: { marginLeft: Spacing.sm }, right: { marginLeft: Spacing.sm },
titleRow: { titleRow: {
paddingHorizontal: Spacing.lg, paddingHorizontal: Spacing.lg,
@@ -74,5 +74,5 @@ const styles = StyleSheet.create({
paddingTop: Spacing.xs, paddingTop: Spacing.xs,
}, },
title: { fontSize: FontSize.xxl, fontWeight: FontWeight.bold, color: Colors.black }, title: { fontSize: FontSize.xxl, fontWeight: FontWeight.bold, color: Colors.black },
subtitle: { fontSize: FontSize.sm, color: Colors.gray[500], marginTop: 2 }, subtitle: { fontSize: FontSize.sm, color: Colors.gray[500], marginTop: Spacing.xs / 2 },
}); });
+7 -12
View File
@@ -1,6 +1,6 @@
import { StyleSheet, Text, View } from 'react-native'; import { StyleSheet, Text, View } from 'react-native';
import { Colors } from '@/lib/colors'; import { Colors } from '@/lib/colors';
import { FontSize, FontWeight, Radius, Spacing } from '@/lib/theme'; import { FontSize, FontWeight, LetterSpacing, Radius, Spacing, Tint } from '@/lib/theme';
type Tone = 'neutral' | 'success' | 'error' | 'info' | 'warning' | 'primary'; type Tone = 'neutral' | 'success' | 'error' | 'info' | 'warning' | 'primary';
@@ -14,7 +14,7 @@ interface BadgeProps {
/** Pastille d'état DSFR, taille homogène. */ /** Pastille d'état DSFR, taille homogène. */
export default function Badge({ label, tone = 'neutral', color, uppercase = false }: BadgeProps) { export default function Badge({ label, tone = 'neutral', color, uppercase = false }: BadgeProps) {
const palette = color ? { bg: hexWithAlpha(color, 0.14), text: darken(color) } : TONES[tone]; const palette = color ? { bg: hexWithAlpha(color, 0.14), text: color } : TONES[tone];
return ( return (
<View style={[styles.badge, { backgroundColor: palette.bg }]}> <View style={[styles.badge, { backgroundColor: palette.bg }]}>
<Text style={[styles.text, { color: palette.text }, uppercase && styles.uppercase]} numberOfLines={1}> <Text style={[styles.text, { color: palette.text }, uppercase && styles.uppercase]} numberOfLines={1}>
@@ -28,9 +28,9 @@ const TONES: Record<Tone, { bg: string; text: string }> = {
neutral: { bg: Colors.gray[100], text: Colors.gray[600] }, neutral: { bg: Colors.gray[100], text: Colors.gray[600] },
success: { bg: Colors.successBg, text: Colors.success }, success: { bg: Colors.successBg, text: Colors.success },
error: { bg: Colors.errorBg, text: Colors.error }, error: { bg: Colors.errorBg, text: Colors.error },
info: { bg: '#E8EDFF', text: Colors.info }, info: { bg: Tint.info, text: Colors.info },
warning: { bg: '#FFE9C7', text: Colors.warning }, warning: { bg: Tint.warning, text: Colors.warning },
primary: { bg: '#E3E3FD', text: Colors.primary }, primary: { bg: Tint.primary, text: Colors.primary },
}; };
/** Ajoute un canal alpha (0–1) à une couleur hex #RRGGBB. */ /** Ajoute un canal alpha (0–1) à une couleur hex #RRGGBB. */
@@ -39,18 +39,13 @@ function hexWithAlpha(hex: string, alpha: number): string {
return `${hex}${a}`; return `${hex}${a}`;
} }
/** Pour le texte sur fond teinté, on garde la couleur d'origine (lisible sur 14% d'alpha). */
function darken(hex: string): string {
return hex;
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
badge: { badge: {
alignSelf: 'flex-start', alignSelf: 'flex-start',
paddingHorizontal: Spacing.sm, paddingHorizontal: Spacing.sm,
paddingVertical: 3, paddingVertical: Spacing.xs - 1,
borderRadius: Radius.sm, borderRadius: Radius.sm,
}, },
text: { fontSize: FontSize.tiny, fontWeight: FontWeight.bold }, text: { fontSize: FontSize.tiny, fontWeight: FontWeight.bold },
uppercase: { textTransform: 'uppercase', letterSpacing: 0.5 }, uppercase: { textTransform: 'uppercase', letterSpacing: LetterSpacing.sm },
}); });
+4 -3
View File
@@ -1,7 +1,7 @@
import { ActivityIndicator, Pressable, StyleSheet, Text, View, type StyleProp, type ViewStyle } from 'react-native'; import { ActivityIndicator, Pressable, StyleSheet, Text, View, type StyleProp, type ViewStyle } from 'react-native';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { Colors } from '@/lib/colors'; import { Colors } from '@/lib/colors';
import { FontSize, FontWeight, MIN_TOUCH, Radius, Spacing } from '@/lib/theme'; import { FontSize, FontWeight, MIN_TOUCH, MIN_TOUCH_SM, Radius, Spacing } from '@/lib/theme';
type Variant = 'primary' | 'secondary' | 'danger' | 'tertiary'; type Variant = 'primary' | 'secondary' | 'danger' | 'tertiary';
type Size = 'md' | 'sm'; type Size = 'md' | 'sm';
@@ -36,8 +36,9 @@ export default function Button({
}: ButtonProps) { }: ButtonProps) {
const isDisabled = disabled || loading; const isDisabled = disabled || loading;
const palette = VARIANTS[variant]; const palette = VARIANTS[variant];
const minHeight = size === 'sm' ? 40 : MIN_TOUCH; const minHeight = size === 'sm' ? MIN_TOUCH_SM : MIN_TOUCH;
const fontSize = size === 'sm' ? FontSize.sm : FontSize.md; const fontSize = size === 'sm' ? FontSize.sm : FontSize.md;
const iconSize = fontSize + Spacing.xs - 1;
return ( return (
<Pressable <Pressable
@@ -64,7 +65,7 @@ export default function Button({
<ActivityIndicator color={palette.text} size="small" /> <ActivityIndicator color={palette.text} size="small" />
) : ( ) : (
<View style={styles.content}> <View style={styles.content}>
{icon && <Ionicons name={icon} size={fontSize + 3} color={palette.text} />} {icon && <Ionicons name={icon} size={iconSize} color={palette.text} />}
<Text style={[styles.label, { color: palette.text, fontSize }]}>{title}</Text> <Text style={[styles.label, { color: palette.text, fontSize }]}>{title}</Text>
</View> </View>
)} )}
+28 -4
View File
@@ -1,4 +1,11 @@
import { Pressable, StyleSheet, View, type StyleProp, type ViewStyle } from 'react-native'; import {
Pressable,
StyleSheet,
View,
type AccessibilityRole,
type StyleProp,
type ViewStyle,
} from 'react-native';
import { Colors } from '@/lib/colors'; import { Colors } from '@/lib/colors';
import { Radius, Shadow, Spacing } from '@/lib/theme'; import { Radius, Shadow, Spacing } from '@/lib/theme';
@@ -9,18 +16,31 @@ interface CardProps {
padding?: number; padding?: number;
/** Légère ombre portée. */ /** Légère ombre portée. */
elevated?: boolean; elevated?: boolean;
/** Libellé d'accessibilité (lecteurs d'écran), utile sur une carte cliquable. */
accessibilityLabel?: string;
/** Rôle d'accessibilité (défaut « button » si cliquable). */
accessibilityRole?: AccessibilityRole;
style?: StyleProp<ViewStyle>; style?: StyleProp<ViewStyle>;
} }
/** Conteneur DSFR : fond blanc, bordure grise fine, coins peu arrondis. */ /** Conteneur DSFR : fond blanc, bordure grise fine, coins peu arrondis. */
export default function Card({ children, onPress, padding = Spacing.lg, elevated = false, style }: CardProps) { export default function Card({
children,
onPress,
padding = Spacing.lg,
elevated = false,
accessibilityLabel,
accessibilityRole,
style,
}: CardProps) {
const content = [styles.card, { padding }, elevated && Shadow.card, style]; const content = [styles.card, { padding }, elevated && Shadow.card, style];
if (onPress) { if (onPress) {
return ( return (
<Pressable <Pressable
onPress={onPress} onPress={onPress}
accessibilityRole="button" accessibilityRole={accessibilityRole ?? 'button'}
accessibilityLabel={accessibilityLabel}
style={({ pressed }) => [content, pressed && styles.pressed]} style={({ pressed }) => [content, pressed && styles.pressed]}
> >
{children} {children}
@@ -28,7 +48,11 @@ export default function Card({ children, onPress, padding = Spacing.lg, elevated
); );
} }
return <View style={content}>{children}</View>; return (
<View style={content} accessibilityRole={accessibilityRole} accessibilityLabel={accessibilityLabel}>
{children}
</View>
);
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
+6 -6
View File
@@ -1,7 +1,7 @@
import { StyleSheet, Text, View } from 'react-native'; import { StyleSheet, Text, View } from 'react-native';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { Colors } from '@/lib/colors'; import { Colors } from '@/lib/colors';
import { FontSize, FontWeight, Radius, Spacing } from '@/lib/theme'; import { FontSize, FontWeight, ICON_CIRCLE, IconSize, LineHeight, Radius, Spacing, Tint } from '@/lib/theme';
import Button from './Button'; import Button from './Button';
interface EmptyStateProps { interface EmptyStateProps {
@@ -17,7 +17,7 @@ export default function EmptyState({ icon = 'document-text-outline', title, mess
return ( return (
<View style={styles.container}> <View style={styles.container}>
<View style={styles.iconCircle}> <View style={styles.iconCircle}>
<Ionicons name={icon} size={32} color={Colors.primary} /> <Ionicons name={icon} size={IconSize.xl} color={Colors.primary} />
</View> </View>
<Text style={styles.title}>{title}</Text> <Text style={styles.title}>{title}</Text>
{message ? <Text style={styles.message}>{message}</Text> : null} {message ? <Text style={styles.message}>{message}</Text> : null}
@@ -33,15 +33,15 @@ export default function EmptyState({ icon = 'document-text-outline', title, mess
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { alignItems: 'center', justifyContent: 'center', paddingVertical: Spacing.xxxl, paddingHorizontal: Spacing.xl }, container: { alignItems: 'center', justifyContent: 'center', paddingVertical: Spacing.xxxl, paddingHorizontal: Spacing.xl },
iconCircle: { iconCircle: {
width: 72, width: ICON_CIRCLE,
height: 72, height: ICON_CIRCLE,
borderRadius: Radius.pill, borderRadius: Radius.pill,
backgroundColor: '#E3E3FD', backgroundColor: Tint.primary,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
marginBottom: Spacing.lg, marginBottom: Spacing.lg,
}, },
title: { fontSize: FontSize.lg, fontWeight: FontWeight.bold, color: Colors.black, textAlign: 'center' }, title: { fontSize: FontSize.lg, fontWeight: FontWeight.bold, color: Colors.black, textAlign: 'center' },
message: { fontSize: FontSize.sm, color: Colors.gray[500], textAlign: 'center', marginTop: Spacing.sm, lineHeight: 20 }, message: { fontSize: FontSize.sm, color: Colors.gray[500], textAlign: 'center', marginTop: Spacing.sm, lineHeight: LineHeight.normal },
action: { marginTop: Spacing.xl }, action: { marginTop: Spacing.xl },
}); });
+1 -1
View File
@@ -5,7 +5,7 @@ import { FontSize, Spacing } from '@/lib/theme';
/** Indicateur de chargement centré et homogène, avec libellé optionnel. */ /** Indicateur de chargement centré et homogène, avec libellé optionnel. */
export default function Loader({ label }: { label?: string }) { export default function Loader({ label }: { label?: string }) {
return ( return (
<View style={styles.container}> <View style={styles.container} accessibilityRole="progressbar" accessibilityLabel={label ?? 'Chargement en cours'}>
<ActivityIndicator size="large" color={Colors.primary} /> <ActivityIndicator size="large" color={Colors.primary} />
{label ? <Text style={styles.label}>{label}</Text> : null} {label ? <Text style={styles.label}>{label}</Text> : null}
</View> </View>
+7 -3
View File
@@ -1,10 +1,14 @@
import { StyleSheet, Text } from 'react-native'; import { StyleSheet, Text } from 'react-native';
import { Colors } from '@/lib/colors'; import { Colors } from '@/lib/colors';
import { FontSize, FontWeight, Spacing } from '@/lib/theme'; import { FontSize, FontWeight, LetterSpacing, Spacing } from '@/lib/theme';
/** Intitulé de section (petites capitales DSFR) au-dessus d'une liste/groupe. */ /** Intitulé de section (petites capitales DSFR) au-dessus d'une liste/groupe. */
export default function SectionTitle({ children }: { children: string }) { export default function SectionTitle({ children }: { children: string }) {
return <Text style={styles.title}>{children}</Text>; return (
<Text style={styles.title} accessibilityRole="header">
{children}
</Text>
);
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@@ -13,7 +17,7 @@ const styles = StyleSheet.create({
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.gray[500], color: Colors.gray[500],
textTransform: 'uppercase', textTransform: 'uppercase',
letterSpacing: 1, letterSpacing: LetterSpacing.md,
marginBottom: Spacing.sm, marginBottom: Spacing.sm,
}, },
}); });
+59 -26
View File
@@ -1,5 +1,6 @@
import { useState } from 'react'; import { useState } from 'react';
import { import {
Pressable,
StyleSheet, StyleSheet,
Text, Text,
TextInput, TextInput,
@@ -11,7 +12,7 @@ import {
} from 'react-native'; } from 'react-native';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { Colors } from '@/lib/colors'; 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 { interface TextFieldProps {
label: string; label: string;
@@ -53,7 +54,9 @@ export default function TextField({
style, style,
}: TextFieldProps) { }: TextFieldProps) {
const [focused, setFocused] = useState(false); const [focused, setFocused] = useState(false);
const [revealed, setRevealed] = useState(false);
const borderColor = error ? Colors.error : focused ? Colors.primary : Colors.gray[600]; const borderColor = error ? Colors.error : focused ? Colors.primary : Colors.gray[600];
const isSecure = secureTextEntry && !revealed;
return ( return (
<View style={[styles.container, style]}> <View style={[styles.container, style]}>
@@ -61,31 +64,53 @@ export default function TextField({
{label} {label}
{required && <Text style={styles.required}> *</Text>} {required && <Text style={styles.required}> *</Text>}
</Text> </Text>
<TextInput <View style={styles.inputRow}>
value={value} <TextInput
onChangeText={onChangeText} value={value}
onFocus={() => setFocused(true)} onChangeText={onChangeText}
onBlur={() => setFocused(false)} onFocus={() => setFocused(true)}
placeholder={placeholder} onBlur={() => setFocused(false)}
placeholderTextColor={Colors.gray[400]} placeholder={placeholder}
secureTextEntry={secureTextEntry} placeholderTextColor={Colors.gray[400]}
keyboardType={keyboardType} secureTextEntry={isSecure}
autoCapitalize={autoCapitalize} keyboardType={keyboardType}
autoComplete={autoComplete} autoCapitalize={autoCapitalize}
multiline={multiline} autoComplete={autoComplete}
numberOfLines={numberOfLines} multiline={multiline}
editable={editable} numberOfLines={numberOfLines}
style={[ editable={editable}
styles.input, accessibilityLabel={label}
{ borderBottomColor: borderColor }, accessibilityState={{ disabled: !editable }}
multiline && styles.multiline, style={[
!editable && styles.readonly, 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 ? ( {error ? (
<View style={styles.messageRow}> <View style={styles.messageRow}>
<Ionicons name="alert-circle" size={14} color={Colors.error} /> <Ionicons name="alert-circle" size={IconSize.sm} color={Colors.error} />
<Text style={styles.errorText}>{error}</Text> <Text style={styles.errorText} accessibilityLiveRegion="polite">
{error}
</Text>
</View> </View>
) : helper ? ( ) : helper ? (
<Text style={styles.helperText}>{helper}</Text> <Text style={styles.helperText}>{helper}</Text>
@@ -103,6 +128,7 @@ const styles = StyleSheet.create({
marginBottom: Spacing.xs, marginBottom: Spacing.xs,
}, },
required: { color: Colors.error }, required: { color: Colors.error },
inputRow: { position: 'relative', justifyContent: 'center' },
input: { input: {
backgroundColor: Colors.gray[50], backgroundColor: Colors.gray[50],
borderTopLeftRadius: Radius.sm, borderTopLeftRadius: Radius.sm,
@@ -112,9 +138,16 @@ const styles = StyleSheet.create({
paddingVertical: Spacing.md, paddingVertical: Spacing.md,
fontSize: FontSize.body, fontSize: FontSize.body,
color: Colors.black, 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] }, readonly: { color: Colors.gray[500], backgroundColor: Colors.gray[100] },
messageRow: { flexDirection: 'row', alignItems: 'center', gap: Spacing.xs, marginTop: Spacing.xs }, messageRow: { flexDirection: 'row', alignItems: 'center', gap: Spacing.xs, marginTop: Spacing.xs },
errorText: { fontSize: FontSize.xs, color: Colors.error }, errorText: { fontSize: FontSize.xs, color: Colors.error },
+42 -2
View File
@@ -1,6 +1,8 @@
// Design tokens centralisés — identité du Système de Design de l'État (DSFR). // Design tokens centralisés — identité du Système de Design de l'État (DSFR).
// À utiliser partout au lieu de valeurs « magiques » inline, pour une UI cohérente. // À utiliser partout au lieu de valeurs « magiques » inline, pour une UI cohérente.
import { Colors } from './colors';
/** Espacements (échelle 4px, comme le DSFR). */ /** Espacements (échelle 4px, comme le DSFR). */
export const Spacing = { export const Spacing = {
xs: 4, xs: 4,
@@ -32,6 +34,30 @@ export const FontSize = {
display: 32, display: 32,
} as const; } as const;
/** Hauteurs de ligne (couplées aux tailles de police pour un rythme régulier). */
export const LineHeight = {
tight: 13,
snug: 19,
normal: 20,
} as const;
/** Interlettrages (DSFR : capitales légèrement espacées). */
export const LetterSpacing = {
sm: 0.5,
md: 1,
} as const;
/** Tailles d'icône homogènes (Ionicons). */
export const IconSize = {
sm: 14,
md: 18,
lg: 24,
xl: 32,
} as const;
/** Diamètre du rond d'icône (états vides, succès, en-têtes illustrés). */
export const ICON_CIRCLE = 72;
/** Graisses (typées pour React Native). */ /** Graisses (typées pour React Native). */
export const FontWeight = { export const FontWeight = {
regular: '400', regular: '400',
@@ -41,17 +67,28 @@ export const FontWeight = {
heavy: '800', heavy: '800',
} as const; } as const;
/**
* Fonds teintés DSFR (teintes claires des couleurs d'état / Bleu France),
* utilisés pour les pastilles, bandeaux et ronds d'icône. Centralisés ici pour
* éviter les codes hexadécimaux en dur dans les écrans et composants.
*/
export const Tint = {
primary: '#E3E3FD', // Bleu France 925
info: '#E8EDFF',
warning: '#FFE9C7',
} as const;
/** Ombres douces, discrètes (le DSFR reste sobre). */ /** Ombres douces, discrètes (le DSFR reste sobre). */
export const Shadow = { export const Shadow = {
card: { card: {
shadowColor: '#161616', shadowColor: Colors.black,
shadowOffset: { width: 0, height: 1 }, shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.06, shadowOpacity: 0.06,
shadowRadius: 4, shadowRadius: 4,
elevation: 1, elevation: 1,
}, },
raised: { raised: {
shadowColor: '#161616', shadowColor: Colors.black,
shadowOffset: { width: 0, height: 4 }, shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.12, shadowOpacity: 0.12,
shadowRadius: 10, shadowRadius: 10,
@@ -62,5 +99,8 @@ export const Shadow = {
/** Cible tactile minimale recommandée (accessibilité). */ /** Cible tactile minimale recommandée (accessibilité). */
export const MIN_TOUCH = 44; export const MIN_TOUCH = 44;
/** Cible tactile réduite pour les contrôles « petits » (toujours confortable). */
export const MIN_TOUCH_SM = 40;
/** hitSlop standard pour agrandir la zone cliquable des petites icônes. */ /** hitSlop standard pour agrandir la zone cliquable des petites icônes. */
export const HIT_SLOP = { top: 10, bottom: 10, left: 10, right: 10 } as const; export const HIT_SLOP = { top: 10, bottom: 10, left: 10, right: 10 } as const;