From 91ceca2be58948ed27701891a436039f0e2d3c66 Mon Sep 17 00:00:00 2001 From: Azmog <80716388+AzmogEx@users.noreply.github.com> Date: Fri, 19 Jun 2026 15:29:40 +0200 Subject: [PATCH] =?UTF-8?q?polish(mobile):=20audit=20design=20DSFR=20multi?= =?UTF-8?q?-agents=20=E2=80=94=20tokens,=20a11y,=20=C3=A9tats,=20finitions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- mobile/app/(tabs)/_layout.tsx | 10 +-- mobile/app/(tabs)/index.tsx | 26 +++++--- mobile/app/(tabs)/info/[slug].tsx | 11 +++- mobile/app/(tabs)/info/index.tsx | 13 +++- mobile/app/(tabs)/journal/edit.tsx | 25 ++++++-- mobile/app/(tabs)/journal/index.tsx | 36 +++++++---- mobile/app/(tabs)/journal/new.tsx | 30 ++++++--- mobile/app/(tabs)/journal/reports.tsx | 19 ++++-- mobile/app/(tabs)/profile.tsx | 11 +++- mobile/app/_layout.tsx | 3 +- mobile/app/admin/contenus.tsx | 6 +- mobile/app/admin/emotions.tsx | 68 ++++++++++++++++----- mobile/app/admin/index.tsx | 15 +++-- mobile/app/admin/utilisateurs.tsx | 11 +++- mobile/app/auth/forgot-password.tsx | 14 ++--- mobile/app/auth/login.tsx | 8 +-- mobile/app/auth/register.tsx | 10 +-- mobile/components/EmotionPicker.tsx | 34 ++++++++--- mobile/components/EmotionPieChart.tsx | 32 ++++++---- mobile/components/RepubliqueHeader.tsx | 52 ---------------- mobile/components/ui/AppBar.tsx | 12 ++-- mobile/components/ui/Badge.tsx | 19 +++--- mobile/components/ui/Button.tsx | 7 ++- mobile/components/ui/Card.tsx | 32 ++++++++-- mobile/components/ui/EmptyState.tsx | 12 ++-- mobile/components/ui/Loader.tsx | 2 +- mobile/components/ui/SectionTitle.tsx | 10 ++- mobile/components/ui/TextField.tsx | 85 ++++++++++++++++++-------- mobile/lib/theme.ts | 44 ++++++++++++- 29 files changed, 433 insertions(+), 224 deletions(-) delete mode 100644 mobile/components/RepubliqueHeader.tsx diff --git a/mobile/app/(tabs)/_layout.tsx b/mobile/app/(tabs)/_layout.tsx index 3f1c880..62e964b 100644 --- a/mobile/app/(tabs)/_layout.tsx +++ b/mobile/app/(tabs)/_layout.tsx @@ -1,6 +1,7 @@ import { Tabs } from 'expo-router'; import { Ionicons } from '@expo/vector-icons'; import { Colors } from '@/lib/colors'; +import { FontWeight, Spacing } from '@/lib/theme'; export default function TabLayout() { return ( @@ -11,12 +12,13 @@ export default function TabLayout() { tabBarStyle: { backgroundColor: Colors.white, borderTopColor: Colors.gray[200], - paddingBottom: 8, - paddingTop: 8, - height: 65, + paddingBottom: Spacing.sm, + paddingTop: Spacing.sm, + height: Spacing.xxxl + Spacing.xl + 1, }, + tabBarLabelStyle: { fontWeight: FontWeight.medium }, headerStyle: { backgroundColor: Colors.white }, - headerTitleStyle: { fontWeight: '700', color: Colors.black }, + headerTitleStyle: { fontWeight: FontWeight.bold, color: Colors.black }, }} > router.push('/(tabs)/journal/new')} padding={Spacing.lg} style={styles.actionPrimary}> - + Nouvelle saisie router.push('/(tabs)/journal/reports')} padding={Spacing.lg} style={styles.actionCard}> - + Mes rapports @@ -73,7 +79,11 @@ export default function DashboardScreen() { Dernières saisies {recentSaisies.length > 0 && ( - router.push('/(tabs)/journal')}> + router.push('/(tabs)/journal')} + accessibilityRole="link" + > Tout voir )} @@ -97,7 +107,7 @@ export default function DashboardScreen() { padding={Spacing.md} style={styles.saisieCard} > - + {saisie.emotion?.icone || '🔵'} @@ -132,9 +142,9 @@ const styles = StyleSheet.create({ backgroundColor: 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 }, - actionIconSecondary: { backgroundColor: '#E3E3FD' }, + actionIconSecondary: { backgroundColor: Colors.gray[100] }, actionText: { fontSize: FontSize.sm, fontWeight: FontWeight.semibold, color: Colors.black }, actionTextPrimary: { fontSize: FontSize.sm, fontWeight: FontWeight.bold, color: Colors.white }, block: { marginBottom: Spacing.xl }, @@ -153,8 +163,8 @@ const styles = StyleSheet.create({ paddingTop: Spacing.xs, }, saisieCard: { flexDirection: 'row', alignItems: 'center', gap: Spacing.md, marginBottom: Spacing.sm }, - emotionIcon: { width: 44, height: 44, borderRadius: Radius.sm, justifyContent: 'center', alignItems: 'center' }, - emotionEmoji: { fontSize: 22 }, + emotionIcon: { width: EMOTION_ICON, height: EMOTION_ICON, borderRadius: Radius.sm, justifyContent: 'center', alignItems: 'center' }, + emotionEmoji: { fontSize: EMOJI_SIZE }, saisieContent: { flex: 1, gap: Spacing.xs, alignItems: 'flex-start' }, saisieDate: { fontSize: FontSize.xs, color: Colors.gray[500] }, }); diff --git a/mobile/app/(tabs)/info/[slug].tsx b/mobile/app/(tabs)/info/[slug].tsx index ac1ba77..bf4c262 100644 --- a/mobile/app/(tabs)/info/[slug].tsx +++ b/mobile/app/(tabs)/info/[slug].tsx @@ -31,7 +31,16 @@ export default function FeedDetailScreen() { ); } - const texteNettoye = (feed.contenu ?? '').replace(/<[^>]*>/g, ''); + const texteNettoye = (feed.contenu ?? '') + .replace(/<[^>]*>/g, '') + .replace(/ /g, ' ') + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/'|'/g, "'") + .replace(/"/g, '"') + .replace(/\n{3,}/g, '\n\n') + .trim(); return ( diff --git a/mobile/app/(tabs)/info/index.tsx b/mobile/app/(tabs)/info/index.tsx index b41a7d3..ae179f9 100644 --- a/mobile/app/(tabs)/info/index.tsx +++ b/mobile/app/(tabs)/info/index.tsx @@ -3,7 +3,7 @@ import { Ionicons } from '@expo/vector-icons'; import { useRouter } from 'expo-router'; import { useFeeds } from '@/hooks/useFeeds'; 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 type { Feed } from '@/types'; @@ -12,7 +12,14 @@ export default function InfoListScreen() { const router = useRouter(); const renderItem = ({ item }: { item: Feed }) => { - const extrait = (item.contenu ?? '').replace(/<[^>]*>/g, '').trim(); + const extrait = (item.contenu ?? '') + .replace(/<[^>]*>/g, '') + .replace(/ /g, ' ') + .replace(/&/g, '&') + .replace(/'|'/g, "'") + .replace(/"/g, '"') + .replace(/\s+/g, ' ') + .trim(); return ( router.push(`/(tabs)/info/${item.slug}`)} style={styles.card}> @@ -73,7 +80,7 @@ const styles = StyleSheet.create({ width: 48, height: 48, borderRadius: Radius.sm, - backgroundColor: '#E3E3FD', + backgroundColor: Tint.primary, justifyContent: 'center', alignItems: 'center', }, diff --git a/mobile/app/(tabs)/journal/edit.tsx b/mobile/app/(tabs)/journal/edit.tsx index a844729..1251741 100644 --- a/mobile/app/(tabs)/journal/edit.tsx +++ b/mobile/app/(tabs)/journal/edit.tsx @@ -6,12 +6,15 @@ import EmotionPicker from '@/components/EmotionPicker'; import { IntensityPicker } from './new'; import { Colors } from '@/lib/colors'; 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() { const router = useRouter(); const { id } = useLocalSearchParams<{ id: string }>(); - const { data: saisies } = useSaisies(); + const { data: saisies, isLoading } = useSaisies(); const updateSaisie = useUpdateSaisie(); const saisie = saisies?.find((s) => s.id === Number(id)); @@ -46,10 +49,24 @@ export default function EditEntryScreen() { } }; - if (!saisie) { + if (isLoading) { return ; } + if (!saisie) { + return ( + + router.back()} + /> + + ); + } + return ( {/* Émotion */} @@ -106,7 +123,7 @@ const styles = StyleSheet.create({ section: { marginBottom: Spacing.xl }, label: { fontSize: FontSize.body, fontWeight: FontWeight.bold, color: Colors.black, marginBottom: Spacing.md }, 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 }, scaleLabels: { flexDirection: 'row', justifyContent: 'space-between', marginTop: Spacing.md }, scaleLabel: { fontSize: FontSize.xs, color: Colors.gray[500] }, diff --git a/mobile/app/(tabs)/journal/index.tsx b/mobile/app/(tabs)/journal/index.tsx index e775c12..468160c 100644 --- a/mobile/app/(tabs)/journal/index.tsx +++ b/mobile/app/(tabs)/journal/index.tsx @@ -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 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() { const router = useRouter(); const { data: saisies, isLoading, refetch } = useSaisies(); @@ -39,7 +49,7 @@ export default function JournalScreen() { const renderItem = ({ item }: { item: SaisieTracker }) => ( - + {item.emotion?.icone || '🔵'} @@ -79,7 +89,7 @@ export default function JournalScreen() { accessibilityLabel="Modifier la saisie" onPress={() => router.push({ pathname: '/(tabs)/journal/edit', params: { id: String(item.id) } })} > - + handleDelete(item.id)} > - + @@ -120,10 +130,12 @@ export default function JournalScreen() { )} router.push('/(tabs)/journal/new')} > - + ); @@ -131,15 +143,15 @@ export default function JournalScreen() { const styles = StyleSheet.create({ 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 }, - emotionIcon: { width: 48, height: 48, borderRadius: Radius.sm, justifyContent: 'center', alignItems: 'center' }, - emotionEmoji: { fontSize: 24 }, + emotionIcon: { width: EMOTION_ICON, height: EMOTION_ICON, borderRadius: Radius.sm, justifyContent: 'center', alignItems: 'center' }, + emotionEmoji: { fontSize: EMOJI_SIZE }, saisieContent: { flex: 1 }, saisieHeader: { flexDirection: 'row', alignItems: 'center', gap: Spacing.sm, marginBottom: Spacing.sm, flexWrap: 'wrap' }, intensityText: { fontSize: FontSize.xs, color: Colors.gray[500] }, - intensityBar: { height: 6, backgroundColor: Colors.gray[200], borderRadius: Radius.sm, marginBottom: Spacing.sm }, - intensityFill: { height: 6, borderRadius: Radius.sm }, + intensityBar: { height: BAR_HEIGHT, backgroundColor: Colors.gray[200], borderRadius: Radius.sm, marginBottom: Spacing.sm }, + intensityFill: { height: BAR_HEIGHT, borderRadius: Radius.sm }, note: { fontSize: FontSize.xs, color: Colors.gray[600], marginBottom: Spacing.xs, lineHeight: 19 }, date: { fontSize: FontSize.xs, color: Colors.gray[400], textTransform: 'capitalize' }, actions: { gap: Spacing.sm }, @@ -153,9 +165,9 @@ const styles = StyleSheet.create({ fab: { position: 'absolute', right: Spacing.lg, - bottom: Spacing.lg, - width: 56, - height: 56, + bottom: Spacing.xl, + width: FAB_SIZE, + height: FAB_SIZE, borderRadius: Radius.sm, backgroundColor: Colors.primary, justifyContent: 'center', diff --git a/mobile/app/(tabs)/journal/new.tsx b/mobile/app/(tabs)/journal/new.tsx index 1db654d..58f7ae8 100644 --- a/mobile/app/(tabs)/journal/new.tsx +++ b/mobile/app/(tabs)/journal/new.tsx @@ -8,6 +8,14 @@ import { Colors } from '@/lib/colors'; import { Spacing, Radius, FontSize, FontWeight } from '@/lib/theme'; 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() { const router = useRouter(); const createSaisie = useCreateSaisie(); @@ -34,12 +42,16 @@ export default function NewEntryScreen() { return ( {/* Stepper */} - + {[1, 2, 3].map((s) => ( = s && styles.stepActive]}> {step > s ? ( - + ) : ( = s && styles.stepNumberActive]}>{s} )} @@ -123,6 +135,8 @@ export function IntensityPicker({ value, onChange }: { value: number; onChange: key={v} style={[pickerStyles.dot, active && pickerStyles.dotActive]} onPress={() => onChange(v)} + accessibilityRole="button" + accessibilityState={{ selected: active }} accessibilityLabel={`Intensité ${v}`} > {v} @@ -136,8 +150,8 @@ export function IntensityPicker({ value, onChange }: { value: number; onChange: const pickerStyles = StyleSheet.create({ grid: { flexDirection: 'row', flexWrap: 'wrap', gap: Spacing.sm, justifyContent: 'center' }, dot: { - width: 44, - height: 44, + width: DOT_SIZE, + height: DOT_SIZE, borderRadius: Radius.sm, backgroundColor: Colors.gray[100], borderWidth: 1, @@ -156,8 +170,8 @@ const styles = StyleSheet.create({ stepper: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', marginBottom: Spacing.xxl }, stepRow: { flexDirection: 'row', alignItems: 'center' }, stepCircle: { - width: 36, - height: 36, + width: STEP_CIRCLE, + height: STEP_CIRCLE, borderRadius: Radius.pill, backgroundColor: Colors.gray[200], justifyContent: 'center', @@ -166,11 +180,11 @@ const styles = StyleSheet.create({ stepActive: { backgroundColor: Colors.primary }, stepNumber: { fontSize: FontSize.md, fontWeight: FontWeight.bold, color: Colors.gray[500] }, 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 }, stepTitle: { fontSize: FontSize.xl, fontWeight: FontWeight.bold, color: Colors.black, 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 }, scaleLabels: { flexDirection: 'row', justifyContent: 'space-between', marginTop: Spacing.md, marginBottom: Spacing.xxl }, scaleLabel: { fontSize: FontSize.xs, color: Colors.gray[500] }, diff --git a/mobile/app/(tabs)/journal/reports.tsx b/mobile/app/(tabs)/journal/reports.tsx index 0f52fa8..ad9be27 100644 --- a/mobile/app/(tabs)/journal/reports.tsx +++ b/mobile/app/(tabs)/journal/reports.tsx @@ -3,10 +3,15 @@ import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from 'react-nati import { Ionicons } from '@expo/vector-icons'; import { useRapport } from '@/hooks/useRapport'; 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 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'; const PERIODS: { value: Period; label: string }[] = [ @@ -33,6 +38,8 @@ export default function ReportsScreen() { key={p.value} style={[styles.segmentBtn, active && styles.segmentBtnActive]} onPress={() => setPeriod(p.value)} + accessibilityRole="button" + accessibilityState={{ selected: active }} accessibilityLabel={p.label} > {p.label} @@ -49,13 +56,13 @@ export default function ReportsScreen() { - + {rapport.stats.total_saisies} Saisies - + {rapport.stats.intensite_moyenne?.toFixed(1) || '—'} Intensité moy. @@ -134,7 +141,7 @@ const styles = StyleSheet.create({ container: { flex: 1 }, content: { padding: Spacing.lg, paddingBottom: Spacing.xxxl }, 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 }, segmentText: { fontSize: FontSize.xs, fontWeight: FontWeight.semibold, color: Colors.gray[500] }, segmentTextActive: { color: Colors.white }, @@ -152,8 +159,8 @@ const styles = StyleSheet.create({ repItemBorder: { borderTopWidth: 1, borderTopColor: Colors.gray[200] }, repHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: Spacing.sm }, repPct: { fontSize: FontSize.sm, fontWeight: FontWeight.bold, color: Colors.black }, - progressBar: { height: 6, backgroundColor: Colors.gray[200], borderRadius: Radius.sm, marginBottom: Spacing.xs }, - progressFill: { height: 6, borderRadius: Radius.sm }, + progressBar: { height: BAR_HEIGHT, backgroundColor: Colors.gray[200], borderRadius: Radius.sm, marginBottom: Spacing.xs }, + progressFill: { height: BAR_HEIGHT, borderRadius: Radius.sm }, repFooter: { flexDirection: 'row', justifyContent: 'space-between' }, repDetail: { fontSize: FontSize.xs, color: Colors.gray[500] }, }); diff --git a/mobile/app/(tabs)/profile.tsx b/mobile/app/(tabs)/profile.tsx index 09b9337..60c77b5 100644 --- a/mobile/app/(tabs)/profile.tsx +++ b/mobile/app/(tabs)/profile.tsx @@ -3,7 +3,7 @@ import { useRouter } from 'expo-router'; import { Ionicons } from '@expo/vector-icons'; import { useAuthStore } from '@/lib/auth-store'; 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'; export default function ProfileScreen() { @@ -101,7 +101,12 @@ function MenuItem({ last?: boolean; }) { return ( - + {label} @@ -133,7 +138,7 @@ const styles = StyleSheet.create({ width: 48, height: 48, borderRadius: Radius.sm, - backgroundColor: '#E3E3FD', + backgroundColor: Tint.primary, justifyContent: 'center', alignItems: 'center', }, diff --git a/mobile/app/_layout.tsx b/mobile/app/_layout.tsx index d5cc56a..d88d6af 100644 --- a/mobile/app/_layout.tsx +++ b/mobile/app/_layout.tsx @@ -4,6 +4,7 @@ import { StatusBar } from 'expo-status-bar'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { useAuthStore } from '@/lib/auth-store'; import { View, ActivityIndicator } from 'react-native'; +import { Colors } from '@/lib/colors'; const queryClient = new QueryClient(); @@ -33,7 +34,7 @@ function AuthGuard({ children }: { children: React.ReactNode }) { if (isLoading) { return ( - + ); } diff --git a/mobile/app/admin/contenus.tsx b/mobile/app/admin/contenus.tsx index e51522e..4421d69 100644 --- a/mobile/app/admin/contenus.tsx +++ b/mobile/app/admin/contenus.tsx @@ -19,6 +19,9 @@ import { Colors } from '@/lib/colors'; import { FontSize, FontWeight, HIT_SLOP, Radius, Spacing } from '@/lib/theme'; 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 { id?: number; titre: string; @@ -160,6 +163,7 @@ export default function AdminContenusScreen() { setForm((f) => ({ ...f, est_publie: v }))} + accessibilityLabel="Publier le contenu" trackColor={{ true: Colors.primary, false: Colors.gray[300] }} thumbColor={Colors.white} /> @@ -186,7 +190,7 @@ const styles = StyleSheet.create({ badges: { flexDirection: 'row', gap: Spacing.sm, flexWrap: 'wrap' }, extrait: { fontSize: FontSize.sm, color: Colors.gray[500], lineHeight: 20 }, 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: { backgroundColor: Colors.white, borderTopLeftRadius: Radius.md, diff --git a/mobile/app/admin/emotions.tsx b/mobile/app/admin/emotions.tsx index 0bf1cc9..9868420 100644 --- a/mobile/app/admin/emotions.tsx +++ b/mobile/app/admin/emotions.tsx @@ -15,9 +15,20 @@ import { Ionicons } from '@expo/vector-icons'; import { Button, Card, EmptyState, Loader, SectionTitle, TextField } from '@/components/ui'; import { useAdminEmotions, useSaveEmotion, useDeleteEmotion } from '@/hooks/useAdmin'; 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'; +/** 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 { id?: number; nom: string; @@ -28,7 +39,7 @@ interface FormState { 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 { const err = e as { response?: { data?: { message?: string } } }; @@ -83,7 +94,7 @@ export default function AdminEmotionsScreen() { Alert.alert('Champs requis', 'Le nom est obligatoire.'); 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).'); return; } @@ -127,7 +138,7 @@ export default function AdminEmotionsScreen() { const renderEmotion = (e: Emotion, estEnfant: boolean) => ( - + {e.icone ? {e.icone} : null} @@ -195,7 +206,12 @@ export default function AdminEmotionsScreen() { required /> - + Niveau - + setForm((f) => ({ ...f, niveau: 1, parent_id: null }))} > @@ -216,6 +234,8 @@ export default function AdminEmotionsScreen() { setForm((f) => ({ ...f, niveau: 2 }))} > @@ -228,7 +248,7 @@ export default function AdminEmotionsScreen() { {form.niveau === 2 ? ( <> Émotion parente - + {parents.length === 0 ? ( Aucune émotion de niveau 1 disponible. ) : ( @@ -237,6 +257,9 @@ export default function AdminEmotionsScreen() { return ( setForm((f) => ({ ...f, parent_id: p.id }))} > @@ -260,6 +283,7 @@ export default function AdminEmotionsScreen() { setForm((f) => ({ ...f, est_actif: v }))} + accessibilityLabel="Émotion active dans le tracker" trackColor={{ true: Colors.primary, false: Colors.gray[300] }} thumbColor={Colors.white} /> @@ -285,13 +309,21 @@ const styles = StyleSheet.create({ emotionCard: { padding: Spacing.md, gap: Spacing.sm }, emotionEnfant: { marginLeft: Spacing.xl, borderColor: Colors.gray[100] }, emotionRow: { flexDirection: 'row', alignItems: 'center', gap: Spacing.md }, - pastille: { width: 36, height: 36, borderRadius: Radius.pill, alignItems: 'center', justifyContent: 'center' }, - pastilleIcone: { fontSize: 18 }, + pastille: { + width: PASTILLE, + height: PASTILLE, + borderRadius: Radius.pill, + borderWidth: 1, + borderColor: Colors.gray[200], + alignItems: 'center', + justifyContent: 'center', + }, + pastilleIcone: { fontSize: FontSize.lg }, emotionInfo: { flex: 1 }, emotionNom: { fontSize: FontSize.md, fontWeight: FontWeight.semibold, color: Colors.black }, emotionMeta: { fontSize: FontSize.xs, color: Colors.gray[500], marginTop: 2 }, 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: { backgroundColor: Colors.white, borderTopLeftRadius: Radius.md, @@ -312,12 +344,12 @@ const styles = StyleSheet.create({ couleurRow: { flexDirection: 'row', alignItems: 'flex-start', gap: Spacing.md }, couleurField: { flex: 1 }, couleurApercu: { - width: 44, - height: 44, + width: APERCU, + height: APERCU, borderRadius: Radius.sm, borderWidth: 1, borderColor: Colors.gray[200], - marginTop: 24, + marginTop: Spacing.xl, }, segment: { flexDirection: 'row', @@ -344,8 +376,14 @@ const styles = StyleSheet.create({ backgroundColor: Colors.white, minHeight: 44, }, - parentChipActive: { borderColor: Colors.primary, backgroundColor: '#E3E3FD' }, - parentDot: { width: 14, height: 14, borderRadius: Radius.pill }, + parentChipActive: { borderColor: Colors.primary, backgroundColor: Tint.primary }, + parentDot: { + width: PARENT_DOT, + height: PARENT_DOT, + borderRadius: Radius.pill, + borderWidth: 1, + borderColor: Colors.gray[200], + }, parentChipText: { fontSize: FontSize.sm, color: Colors.gray[600] }, parentChipTextActive: { color: Colors.primary, fontWeight: FontWeight.semibold }, switchRow: { diff --git a/mobile/app/admin/index.tsx b/mobile/app/admin/index.tsx index 2378884..b58a2d8 100644 --- a/mobile/app/admin/index.tsx +++ b/mobile/app/admin/index.tsx @@ -4,7 +4,12 @@ import { Ionicons } from '@expo/vector-icons'; import { Card, SectionTitle } from '@/components/ui'; import { useAdminUtilisateurs, useAdminFeeds, useAdminEmotions } from '@/hooks/useAdmin'; 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 { href: '/admin/utilisateurs' | '/admin/contenus' | '/admin/emotions'; @@ -83,10 +88,10 @@ const styles = StyleSheet.create({ card: { padding: Spacing.lg }, row: { flexDirection: 'row', alignItems: 'center', gap: Spacing.md }, iconCircle: { - width: 48, - height: 48, + width: ICON_CIRCLE, + height: ICON_CIRCLE, borderRadius: Radius.md, - backgroundColor: '#E3E3FD', + backgroundColor: Tint.primary, alignItems: 'center', justifyContent: 'center', }, @@ -98,7 +103,7 @@ const styles = StyleSheet.create({ fontSize: FontSize.sm, fontWeight: FontWeight.bold, color: Colors.primary, - minWidth: 22, + minWidth: COUNT_MIN_WIDTH, textAlign: 'right', }, }); diff --git a/mobile/app/admin/utilisateurs.tsx b/mobile/app/admin/utilisateurs.tsx index eec369e..44b152a 100644 --- a/mobile/app/admin/utilisateurs.tsx +++ b/mobile/app/admin/utilisateurs.tsx @@ -26,6 +26,9 @@ import type { Utilisateur } from '@/types'; const ROLE_MEMBRE = 2; 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 { prenom: string; nom: string; @@ -192,8 +195,10 @@ export default function AdminUtilisateursScreen() { /> Rôle - + setForm((f) => ({ ...f, role_id: ROLE_MEMBRE }))} > @@ -202,6 +207,8 @@ export default function AdminUtilisateursScreen() { 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 }, badges: { flexDirection: 'row', gap: Spacing.sm, flexWrap: 'wrap' }, 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: { backgroundColor: Colors.white, borderTopLeftRadius: Radius.md, diff --git a/mobile/app/auth/forgot-password.tsx b/mobile/app/auth/forgot-password.tsx index 42d6ca1..6c5cb4c 100644 --- a/mobile/app/auth/forgot-password.tsx +++ b/mobile/app/auth/forgot-password.tsx @@ -3,7 +3,7 @@ import { View, Text, StyleSheet, KeyboardAvoidingView, Platform, ScrollView } fr import { useRouter } from 'expo-router'; import { Ionicons } from '@expo/vector-icons'; 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'; const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; @@ -33,7 +33,7 @@ export default function ForgotPasswordScreen() { - + Vérifiez votre boîte mail @@ -102,20 +102,20 @@ export default function ForgotPasswordScreen() { const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: Colors.gray[50] }, 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 }, successWrap: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: Spacing.xl }, iconCircle: { - width: 72, - height: 72, + width: ICON_CIRCLE, + height: ICON_CIRCLE, borderRadius: Radius.pill, - backgroundColor: '#E3E3FD', + backgroundColor: Tint.primary, alignItems: 'center', justifyContent: 'center', marginBottom: Spacing.lg, }, 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 }, successAction: { alignSelf: 'stretch', marginTop: Spacing.lg }, }); diff --git a/mobile/app/auth/login.tsx b/mobile/app/auth/login.tsx index 31fe20f..1707d25 100644 --- a/mobile/app/auth/login.tsx +++ b/mobile/app/auth/login.tsx @@ -5,7 +5,7 @@ import { Ionicons } from '@expo/vector-icons'; import { useAuthStore } from '@/lib/auth-store'; import { getApiError } from '@/lib/api'; 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'; export default function LoginScreen() { @@ -46,8 +46,8 @@ export default function LoginScreen() { {error ? ( - - + + {error} ) : null} @@ -118,7 +118,7 @@ const styles = StyleSheet.create({ gap: Spacing.sm, backgroundColor: Colors.errorBg, borderRadius: Radius.sm, - borderLeftWidth: 3, + borderLeftWidth: Spacing.xs - 1, borderLeftColor: Colors.error, padding: Spacing.md, marginBottom: Spacing.lg, diff --git a/mobile/app/auth/register.tsx b/mobile/app/auth/register.tsx index a928ded..49a507f 100644 --- a/mobile/app/auth/register.tsx +++ b/mobile/app/auth/register.tsx @@ -5,7 +5,7 @@ import { Ionicons } from '@expo/vector-icons'; import { useAuthStore } from '@/lib/auth-store'; import { getApiError } from '@/lib/api'; 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'; const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; @@ -132,7 +132,7 @@ export default function RegisterScreen() { > @@ -143,7 +143,7 @@ export default function RegisterScreen() { {consentError ? ( - + {consentError} ) : null} @@ -185,8 +185,8 @@ const styles = StyleSheet.create({ paddingVertical: Spacing.sm, minHeight: MIN_TOUCH, }, - consentIcon: { marginTop: 1 }, - consentText: { flex: 1, fontSize: FontSize.xs, color: Colors.black, lineHeight: 19 }, + consentIcon: { marginTop: Spacing.xs / 4 }, + consentText: { flex: 1, fontSize: FontSize.xs, color: Colors.black, lineHeight: LineHeight.snug }, consentLink: { color: Colors.primary, fontWeight: FontWeight.semibold, textDecorationLine: 'underline' }, consentErrorRow: { flexDirection: 'row', alignItems: 'center', gap: Spacing.xs, marginTop: Spacing.xs }, consentErrorText: { fontSize: FontSize.xs, color: Colors.error }, diff --git a/mobile/components/EmotionPicker.tsx b/mobile/components/EmotionPicker.tsx index 58f5801..2ebaddd 100644 --- a/mobile/components/EmotionPicker.tsx +++ b/mobile/components/EmotionPicker.tsx @@ -7,6 +7,14 @@ import { Spacing, Radius, FontSize, FontWeight, MIN_TOUCH, HIT_SLOP } from '@/li import { Loader } from '@/components/ui'; 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 { value?: number; onChange: (emotionId: number) => void; @@ -23,8 +31,14 @@ export default function EmotionPicker({ value, onChange }: EmotionPickerProps) { if (selectedParent?.enfants && selectedParent.enfants.length > 0) { return ( - setSelectedParent(null)}> - + setSelectedParent(null)} + > + Retour aux émotions @@ -85,16 +99,18 @@ function EmotionTile({ {emotion.icone || '🔵'} {emotion.nom} - {hasChildren && } + {hasChildren && } ); } @@ -107,7 +123,7 @@ const styles = StyleSheet.create({ grid: { flexDirection: 'row', flexWrap: 'wrap', gap: Spacing.md, justifyContent: 'space-between' }, tile: { width: '30%', - minHeight: 92, + minHeight: TILE_MIN_HEIGHT, backgroundColor: Colors.white, borderRadius: Radius.sm, paddingVertical: Spacing.md, @@ -115,10 +131,10 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', gap: Spacing.xs, - borderWidth: 1.5, + borderWidth: BORDER_DEFAULT, borderColor: Colors.gray[200], }, - tileEmoji: { fontSize: 30 }, + tileEmoji: { fontSize: TILE_EMOJI }, tileName: { fontSize: FontSize.xs, fontWeight: FontWeight.semibold, color: Colors.black, textAlign: 'center' }, - chevron: { marginTop: 2 }, + chevron: { marginTop: Spacing.xs / 2 }, }); diff --git a/mobile/components/EmotionPieChart.tsx b/mobile/components/EmotionPieChart.tsx index c98e03b..37bef0e 100644 --- a/mobile/components/EmotionPieChart.tsx +++ b/mobile/components/EmotionPieChart.tsx @@ -1,6 +1,14 @@ import { View, Text, StyleSheet } from 'react-native'; import Svg, { Circle, G, Text as SvgText } from 'react-native-svg'; 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 { nom: string; @@ -84,9 +92,9 @@ export default function EmotionPieChart({ {/* Centre du donut : part dominante */} @@ -94,8 +102,8 @@ export default function EmotionPieChart({ @@ -111,7 +119,9 @@ export default function EmotionPieChart({ {item.nom} - {item.pourcentage}% + + {item.pourcentage}% · {item.count} + ))} @@ -120,10 +130,10 @@ export default function EmotionPieChart({ } const styles = StyleSheet.create({ - container: { alignItems: 'center', marginBottom: 20 }, - legend: { alignSelf: 'stretch', marginTop: 20, gap: 8 }, + container: { alignItems: 'center', marginBottom: Spacing.lg }, + legend: { alignSelf: 'stretch', marginTop: Spacing.lg, gap: Spacing.sm }, legendRow: { flexDirection: 'row', alignItems: 'center' }, - legendDot: { width: 12, height: 12, borderRadius: 6, marginRight: 10 }, - legendName: { flex: 1, fontSize: 14, color: Colors.black }, - legendPct: { fontSize: 14, fontWeight: '700', color: Colors.gray[600] }, + legendDot: { width: LEGEND_DOT, height: LEGEND_DOT, borderRadius: Radius.pill, marginRight: Spacing.sm }, + legendName: { flex: 1, fontSize: FontSize.sm, color: Colors.black }, + legendPct: { fontSize: FontSize.sm, fontWeight: FontWeight.bold, color: Colors.gray[600] }, }); diff --git a/mobile/components/RepubliqueHeader.tsx b/mobile/components/RepubliqueHeader.tsx deleted file mode 100644 index 89075c8..0000000 --- a/mobile/components/RepubliqueHeader.tsx +++ /dev/null @@ -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 ( - - - RÉPUBLIQUE - FRANÇAISE - Liberté{'\n'}Égalité{'\n'}Fraternité - - {showService && ( - - CESIZen - Santé mentale - - )} - - ); -} - -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 }, -}); diff --git a/mobile/components/ui/AppBar.tsx b/mobile/components/ui/AppBar.tsx index 3f36c92..1116718 100644 --- a/mobile/components/ui/AppBar.tsx +++ b/mobile/components/ui/AppBar.tsx @@ -1,6 +1,6 @@ import { StyleSheet, Text, View } from 'react-native'; import { Colors } from '@/lib/colors'; -import { FontSize, FontWeight, Spacing } from '@/lib/theme'; +import { FontSize, FontWeight, LetterSpacing, LineHeight, Spacing } from '@/lib/theme'; interface AppBarProps { /** Titre de la page, affiché sous le bandeau institutionnel. */ @@ -17,7 +17,7 @@ interface AppBarProps { */ export default function AppBar({ title, subtitle, right }: AppBarProps) { return ( - + RÉPUBLIQUE @@ -61,12 +61,12 @@ const styles = StyleSheet.create({ fontSize: FontSize.tiny, fontWeight: FontWeight.bold, color: Colors.black, - letterSpacing: 0.5, - lineHeight: 13, + letterSpacing: LetterSpacing.sm, + lineHeight: LineHeight.tight, }, serviceBlock: { flex: 1 }, 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 }, titleRow: { paddingHorizontal: Spacing.lg, @@ -74,5 +74,5 @@ const styles = StyleSheet.create({ paddingTop: Spacing.xs, }, 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 }, }); diff --git a/mobile/components/ui/Badge.tsx b/mobile/components/ui/Badge.tsx index 0aa1409..240be28 100644 --- a/mobile/components/ui/Badge.tsx +++ b/mobile/components/ui/Badge.tsx @@ -1,6 +1,6 @@ import { StyleSheet, Text, View } from 'react-native'; 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'; @@ -14,7 +14,7 @@ interface BadgeProps { /** Pastille d'état DSFR, taille homogène. */ 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 ( @@ -28,9 +28,9 @@ const TONES: Record = { neutral: { bg: Colors.gray[100], text: Colors.gray[600] }, success: { bg: Colors.successBg, text: Colors.success }, error: { bg: Colors.errorBg, text: Colors.error }, - info: { bg: '#E8EDFF', text: Colors.info }, - warning: { bg: '#FFE9C7', text: Colors.warning }, - primary: { bg: '#E3E3FD', text: Colors.primary }, + info: { bg: Tint.info, text: Colors.info }, + warning: { bg: Tint.warning, text: Colors.warning }, + primary: { bg: Tint.primary, text: Colors.primary }, }; /** Ajoute un canal alpha (0–1) à une couleur hex #RRGGBB. */ @@ -39,18 +39,13 @@ function hexWithAlpha(hex: string, alpha: number): string { 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({ badge: { alignSelf: 'flex-start', paddingHorizontal: Spacing.sm, - paddingVertical: 3, + paddingVertical: Spacing.xs - 1, borderRadius: Radius.sm, }, text: { fontSize: FontSize.tiny, fontWeight: FontWeight.bold }, - uppercase: { textTransform: 'uppercase', letterSpacing: 0.5 }, + uppercase: { textTransform: 'uppercase', letterSpacing: LetterSpacing.sm }, }); diff --git a/mobile/components/ui/Button.tsx b/mobile/components/ui/Button.tsx index ee859b8..fd96555 100644 --- a/mobile/components/ui/Button.tsx +++ b/mobile/components/ui/Button.tsx @@ -1,7 +1,7 @@ import { ActivityIndicator, Pressable, StyleSheet, Text, View, type StyleProp, type ViewStyle } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; 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 Size = 'md' | 'sm'; @@ -36,8 +36,9 @@ export default function Button({ }: ButtonProps) { const isDisabled = disabled || loading; 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 iconSize = fontSize + Spacing.xs - 1; return ( ) : ( - {icon && } + {icon && } {title} )} diff --git a/mobile/components/ui/Card.tsx b/mobile/components/ui/Card.tsx index 4f83a27..f9767d8 100644 --- a/mobile/components/ui/Card.tsx +++ b/mobile/components/ui/Card.tsx @@ -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 { Radius, Shadow, Spacing } from '@/lib/theme'; @@ -9,18 +16,31 @@ interface CardProps { padding?: number; /** Légère ombre portée. */ 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; } /** 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]; if (onPress) { return ( [content, pressed && styles.pressed]} > {children} @@ -28,7 +48,11 @@ export default function Card({ children, onPress, padding = Spacing.lg, elevated ); } - return {children}; + return ( + + {children} + + ); } const styles = StyleSheet.create({ diff --git a/mobile/components/ui/EmptyState.tsx b/mobile/components/ui/EmptyState.tsx index c117d2b..3be26cc 100644 --- a/mobile/components/ui/EmptyState.tsx +++ b/mobile/components/ui/EmptyState.tsx @@ -1,7 +1,7 @@ import { StyleSheet, Text, View } 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, ICON_CIRCLE, IconSize, LineHeight, Radius, Spacing, Tint } from '@/lib/theme'; import Button from './Button'; interface EmptyStateProps { @@ -17,7 +17,7 @@ export default function EmptyState({ icon = 'document-text-outline', title, mess return ( - + {title} {message ? {message} : null} @@ -33,15 +33,15 @@ export default function EmptyState({ icon = 'document-text-outline', title, mess const styles = StyleSheet.create({ container: { alignItems: 'center', justifyContent: 'center', paddingVertical: Spacing.xxxl, paddingHorizontal: Spacing.xl }, iconCircle: { - width: 72, - height: 72, + width: ICON_CIRCLE, + height: ICON_CIRCLE, borderRadius: Radius.pill, - backgroundColor: '#E3E3FD', + backgroundColor: Tint.primary, alignItems: 'center', justifyContent: 'center', marginBottom: Spacing.lg, }, 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 }, }); diff --git a/mobile/components/ui/Loader.tsx b/mobile/components/ui/Loader.tsx index fdc44b3..f4439ce 100644 --- a/mobile/components/ui/Loader.tsx +++ b/mobile/components/ui/Loader.tsx @@ -5,7 +5,7 @@ import { FontSize, Spacing } from '@/lib/theme'; /** Indicateur de chargement centré et homogène, avec libellé optionnel. */ export default function Loader({ label }: { label?: string }) { return ( - + {label ? {label} : null} diff --git a/mobile/components/ui/SectionTitle.tsx b/mobile/components/ui/SectionTitle.tsx index 1c5a883..911fc22 100644 --- a/mobile/components/ui/SectionTitle.tsx +++ b/mobile/components/ui/SectionTitle.tsx @@ -1,10 +1,14 @@ import { StyleSheet, Text } from 'react-native'; 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. */ export default function SectionTitle({ children }: { children: string }) { - return {children}; + return ( + + {children} + + ); } const styles = StyleSheet.create({ @@ -13,7 +17,7 @@ const styles = StyleSheet.create({ fontWeight: FontWeight.bold, color: Colors.gray[500], textTransform: 'uppercase', - letterSpacing: 1, + letterSpacing: LetterSpacing.md, marginBottom: Spacing.sm, }, }); diff --git a/mobile/components/ui/TextField.tsx b/mobile/components/ui/TextField.tsx index 31fc68a..267fc4c 100644 --- a/mobile/components/ui/TextField.tsx +++ b/mobile/components/ui/TextField.tsx @@ -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 ( @@ -61,31 +64,53 @@ export default function TextField({ {label} {required && *} - 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, - ]} - /> + + 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 ? ( + setRevealed((r) => !r)} + hitSlop={HIT_SLOP} + style={styles.toggle} + accessibilityRole="button" + accessibilityLabel={revealed ? 'Masquer le mot de passe' : 'Afficher le mot de passe'} + > + + + ) : null} + {error ? ( - - {error} + + + {error} + ) : helper ? ( {helper} @@ -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 }, diff --git a/mobile/lib/theme.ts b/mobile/lib/theme.ts index d948e36..d8c06fd 100644 --- a/mobile/lib/theme.ts +++ b/mobile/lib/theme.ts @@ -1,6 +1,8 @@ // 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. +import { Colors } from './colors'; + /** Espacements (échelle 4px, comme le DSFR). */ export const Spacing = { xs: 4, @@ -32,6 +34,30 @@ export const FontSize = { display: 32, } 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). */ export const FontWeight = { regular: '400', @@ -41,17 +67,28 @@ export const FontWeight = { heavy: '800', } 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). */ export const Shadow = { card: { - shadowColor: '#161616', + shadowColor: Colors.black, shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.06, shadowRadius: 4, elevation: 1, }, raised: { - shadowColor: '#161616', + shadowColor: Colors.black, shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.12, shadowRadius: 10, @@ -62,5 +99,8 @@ export const Shadow = { /** Cible tactile minimale recommandée (accessibilité). */ 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. */ export const HIT_SLOP = { top: 10, bottom: 10, left: 10, right: 10 } as const;