diff --git a/mobile/app/_layout.tsx b/mobile/app/_layout.tsx
index 862f1f7..d5cc56a 100644
--- a/mobile/app/_layout.tsx
+++ b/mobile/app/_layout.tsx
@@ -48,6 +48,7 @@ export default function RootLayout() {
+
diff --git a/mobile/components/ui/AppBar.tsx b/mobile/components/ui/AppBar.tsx
new file mode 100644
index 0000000..3f36c92
--- /dev/null
+++ b/mobile/components/ui/AppBar.tsx
@@ -0,0 +1,78 @@
+import { StyleSheet, Text, View } from 'react-native';
+import { Colors } from '@/lib/colors';
+import { FontSize, FontWeight, Spacing } from '@/lib/theme';
+
+interface AppBarProps {
+ /** Titre de la page, affiché sous le bandeau institutionnel. */
+ title?: string;
+ subtitle?: string;
+ /** Élément optionnel aligné à droite du bandeau (ex. bouton). */
+ right?: React.ReactNode;
+}
+
+/**
+ * Bandeau institutionnel République Française réutilisable, à placer en haut
+ * de chaque écran principal. Reprend le bloc-marque de l'État (DSFR) + le nom
+ * du service, avec un titre/sous-titre de page optionnel.
+ */
+export default function AppBar({ title, subtitle, right }: AppBarProps) {
+ return (
+
+
+
+ RÉPUBLIQUE
+ FRANÇAISE
+
+
+ CESIZen
+ Santé mentale
+
+ {right ? {right} : null}
+
+ {title ? (
+
+ {title}
+ {subtitle ? {subtitle} : null}
+
+ ) : null}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ wrapper: {
+ backgroundColor: Colors.white,
+ borderBottomWidth: 1,
+ borderBottomColor: Colors.gray[200],
+ },
+ brandRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: Spacing.lg,
+ paddingVertical: Spacing.md,
+ },
+ marianne: {
+ paddingRight: Spacing.md,
+ marginRight: Spacing.md,
+ borderRightWidth: 1,
+ borderRightColor: Colors.gray[200],
+ },
+ republique: {
+ fontSize: FontSize.tiny,
+ fontWeight: FontWeight.bold,
+ color: Colors.black,
+ letterSpacing: 0.5,
+ lineHeight: 13,
+ },
+ serviceBlock: { flex: 1 },
+ serviceName: { fontSize: FontSize.lg, fontWeight: FontWeight.heavy, color: Colors.primary },
+ serviceTagline: { fontSize: FontSize.tiny, color: Colors.gray[500], marginTop: 1 },
+ right: { marginLeft: Spacing.sm },
+ titleRow: {
+ paddingHorizontal: Spacing.lg,
+ paddingBottom: Spacing.md,
+ paddingTop: Spacing.xs,
+ },
+ title: { fontSize: FontSize.xxl, fontWeight: FontWeight.bold, color: Colors.black },
+ subtitle: { fontSize: FontSize.sm, color: Colors.gray[500], marginTop: 2 },
+});
diff --git a/mobile/components/ui/Badge.tsx b/mobile/components/ui/Badge.tsx
new file mode 100644
index 0000000..0aa1409
--- /dev/null
+++ b/mobile/components/ui/Badge.tsx
@@ -0,0 +1,56 @@
+import { StyleSheet, Text, View } from 'react-native';
+import { Colors } from '@/lib/colors';
+import { FontSize, FontWeight, Radius, Spacing } from '@/lib/theme';
+
+type Tone = 'neutral' | 'success' | 'error' | 'info' | 'warning' | 'primary';
+
+interface BadgeProps {
+ label: string;
+ tone?: Tone;
+ /** Couleur personnalisée (ex. couleur d'une émotion) — prioritaire sur `tone`. */
+ color?: string;
+ uppercase?: boolean;
+}
+
+/** 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];
+ return (
+
+
+ {label}
+
+
+ );
+}
+
+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 },
+};
+
+/** Ajoute un canal alpha (0–1) à une couleur hex #RRGGBB. */
+function hexWithAlpha(hex: string, alpha: number): string {
+ const a = Math.round(alpha * 255).toString(16).padStart(2, '0');
+ 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,
+ borderRadius: Radius.sm,
+ },
+ text: { fontSize: FontSize.tiny, fontWeight: FontWeight.bold },
+ uppercase: { textTransform: 'uppercase', letterSpacing: 0.5 },
+});
diff --git a/mobile/components/ui/Button.tsx b/mobile/components/ui/Button.tsx
new file mode 100644
index 0000000..ee859b8
--- /dev/null
+++ b/mobile/components/ui/Button.tsx
@@ -0,0 +1,93 @@
+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';
+
+type Variant = 'primary' | 'secondary' | 'danger' | 'tertiary';
+type Size = 'md' | 'sm';
+
+interface ButtonProps {
+ title: string;
+ onPress?: () => void;
+ variant?: Variant;
+ size?: Size;
+ loading?: boolean;
+ disabled?: boolean;
+ /** Nom d'icône Ionicons affichée à gauche du libellé. */
+ icon?: keyof typeof Ionicons.glyphMap;
+ fullWidth?: boolean;
+ style?: StyleProp;
+}
+
+/**
+ * Bouton DSFR. 4 variantes : primary (bleu plein), secondary (contour),
+ * danger (rouge plein), tertiary (texte seul). Hauteur ≥ 44px (accessibilité).
+ */
+export default function Button({
+ title,
+ onPress,
+ variant = 'primary',
+ size = 'md',
+ loading = false,
+ disabled = false,
+ icon,
+ fullWidth = false,
+ style,
+}: ButtonProps) {
+ const isDisabled = disabled || loading;
+ const palette = VARIANTS[variant];
+ const minHeight = size === 'sm' ? 40 : MIN_TOUCH;
+ const fontSize = size === 'sm' ? FontSize.sm : FontSize.md;
+
+ return (
+ [
+ styles.base,
+ {
+ minHeight,
+ paddingVertical: size === 'sm' ? Spacing.sm : Spacing.md,
+ backgroundColor: palette.bg,
+ borderColor: palette.border,
+ borderWidth: palette.border === 'transparent' ? 0 : 1,
+ },
+ fullWidth && styles.fullWidth,
+ pressed && !isDisabled && { opacity: 0.85 },
+ isDisabled && styles.disabled,
+ style,
+ ]}
+ >
+ {loading ? (
+
+ ) : (
+
+ {icon && }
+ {title}
+
+ )}
+
+ );
+}
+
+const VARIANTS: Record = {
+ primary: { bg: Colors.primary, text: Colors.white, border: 'transparent' },
+ secondary: { bg: Colors.white, text: Colors.primary, border: Colors.primary },
+ danger: { bg: Colors.error, text: Colors.white, border: 'transparent' },
+ tertiary: { bg: 'transparent', text: Colors.primary, border: 'transparent' },
+};
+
+const styles = StyleSheet.create({
+ base: {
+ borderRadius: Radius.sm,
+ paddingHorizontal: Spacing.xl,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ fullWidth: { alignSelf: 'stretch', width: '100%' },
+ disabled: { opacity: 0.45 },
+ content: { flexDirection: 'row', alignItems: 'center', gap: Spacing.sm },
+ label: { fontWeight: FontWeight.semibold },
+});
diff --git a/mobile/components/ui/Card.tsx b/mobile/components/ui/Card.tsx
new file mode 100644
index 0000000..4f83a27
--- /dev/null
+++ b/mobile/components/ui/Card.tsx
@@ -0,0 +1,42 @@
+import { Pressable, StyleSheet, View, type StyleProp, type ViewStyle } from 'react-native';
+import { Colors } from '@/lib/colors';
+import { Radius, Shadow, Spacing } from '@/lib/theme';
+
+interface CardProps {
+ children: React.ReactNode;
+ onPress?: () => void;
+ /** Padding interne (défaut lg = 16). Mettre 0 pour gérer soi-même. */
+ padding?: number;
+ /** Légère ombre portée. */
+ elevated?: boolean;
+ 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) {
+ const content = [styles.card, { padding }, elevated && Shadow.card, style];
+
+ if (onPress) {
+ return (
+ [content, pressed && styles.pressed]}
+ >
+ {children}
+
+ );
+ }
+
+ return {children};
+}
+
+const styles = StyleSheet.create({
+ card: {
+ backgroundColor: Colors.white,
+ borderWidth: 1,
+ borderColor: Colors.gray[200],
+ borderRadius: Radius.sm,
+ },
+ pressed: { backgroundColor: Colors.gray[50] },
+});
diff --git a/mobile/components/ui/EmptyState.tsx b/mobile/components/ui/EmptyState.tsx
new file mode 100644
index 0000000..c117d2b
--- /dev/null
+++ b/mobile/components/ui/EmptyState.tsx
@@ -0,0 +1,47 @@
+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 Button from './Button';
+
+interface EmptyStateProps {
+ icon?: keyof typeof Ionicons.glyphMap;
+ title: string;
+ message?: string;
+ actionLabel?: string;
+ onAction?: () => void;
+}
+
+/** État vide homogène : icône, titre, message et action optionnelle. */
+export default function EmptyState({ icon = 'document-text-outline', title, message, actionLabel, onAction }: EmptyStateProps) {
+ return (
+
+
+
+
+ {title}
+ {message ? {message} : null}
+ {actionLabel && onAction ? (
+
+
+
+ ) : null}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: { alignItems: 'center', justifyContent: 'center', paddingVertical: Spacing.xxxl, paddingHorizontal: Spacing.xl },
+ iconCircle: {
+ width: 72,
+ height: 72,
+ borderRadius: Radius.pill,
+ backgroundColor: '#E3E3FD',
+ 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 },
+ action: { marginTop: Spacing.xl },
+});
diff --git a/mobile/components/ui/Loader.tsx b/mobile/components/ui/Loader.tsx
new file mode 100644
index 0000000..fdc44b3
--- /dev/null
+++ b/mobile/components/ui/Loader.tsx
@@ -0,0 +1,18 @@
+import { ActivityIndicator, StyleSheet, Text, View } from 'react-native';
+import { Colors } from '@/lib/colors';
+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}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: Spacing.xl, backgroundColor: Colors.gray[50] },
+ label: { marginTop: Spacing.md, fontSize: FontSize.sm, color: Colors.gray[500] },
+});
diff --git a/mobile/components/ui/SectionTitle.tsx b/mobile/components/ui/SectionTitle.tsx
new file mode 100644
index 0000000..1c5a883
--- /dev/null
+++ b/mobile/components/ui/SectionTitle.tsx
@@ -0,0 +1,19 @@
+import { StyleSheet, Text } from 'react-native';
+import { Colors } from '@/lib/colors';
+import { FontSize, FontWeight, 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};
+}
+
+const styles = StyleSheet.create({
+ title: {
+ fontSize: FontSize.xs,
+ fontWeight: FontWeight.bold,
+ color: Colors.gray[500],
+ textTransform: 'uppercase',
+ letterSpacing: 1,
+ marginBottom: Spacing.sm,
+ },
+});
diff --git a/mobile/components/ui/TextField.tsx b/mobile/components/ui/TextField.tsx
new file mode 100644
index 0000000..31fc68a
--- /dev/null
+++ b/mobile/components/ui/TextField.tsx
@@ -0,0 +1,122 @@
+import { useState } from 'react';
+import {
+ StyleSheet,
+ Text,
+ TextInput,
+ View,
+ type KeyboardTypeOptions,
+ type StyleProp,
+ type TextInputProps,
+ type ViewStyle,
+} from 'react-native';
+import { Ionicons } from '@expo/vector-icons';
+import { Colors } from '@/lib/colors';
+import { FontSize, FontWeight, Radius, Spacing } from '@/lib/theme';
+
+interface TextFieldProps {
+ label: string;
+ value: string;
+ onChangeText: (text: string) => void;
+ placeholder?: string;
+ error?: string;
+ helper?: string;
+ required?: boolean;
+ secureTextEntry?: boolean;
+ keyboardType?: KeyboardTypeOptions;
+ autoCapitalize?: TextInputProps['autoCapitalize'];
+ autoComplete?: TextInputProps['autoComplete'];
+ multiline?: boolean;
+ numberOfLines?: number;
+ editable?: boolean;
+ style?: StyleProp;
+}
+
+/**
+ * Champ de saisie DSFR : libellé au-dessus, fond gris clair, bordure basse
+ * accentuée (bleu au focus, rouge en erreur), message d'erreur sous le champ.
+ */
+export default function TextField({
+ label,
+ value,
+ onChangeText,
+ placeholder,
+ error,
+ helper,
+ required = false,
+ secureTextEntry = false,
+ keyboardType,
+ autoCapitalize = 'none',
+ autoComplete,
+ multiline = false,
+ numberOfLines,
+ editable = true,
+ style,
+}: TextFieldProps) {
+ const [focused, setFocused] = useState(false);
+ const borderColor = error ? Colors.error : focused ? Colors.primary : Colors.gray[600];
+
+ return (
+
+
+ {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,
+ ]}
+ />
+ {error ? (
+
+
+ {error}
+
+ ) : helper ? (
+ {helper}
+ ) : null}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: { marginBottom: Spacing.lg },
+ label: {
+ fontSize: FontSize.sm,
+ fontWeight: FontWeight.medium,
+ color: Colors.black,
+ marginBottom: Spacing.xs,
+ },
+ required: { color: Colors.error },
+ input: {
+ backgroundColor: Colors.gray[50],
+ borderTopLeftRadius: Radius.sm,
+ borderTopRightRadius: Radius.sm,
+ borderBottomWidth: 2,
+ paddingHorizontal: Spacing.md,
+ paddingVertical: Spacing.md,
+ fontSize: FontSize.body,
+ color: Colors.black,
+ minHeight: 44,
+ },
+ multiline: { minHeight: 110, 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 },
+ helperText: { fontSize: FontSize.xs, color: Colors.gray[500], marginTop: Spacing.xs },
+});
diff --git a/mobile/components/ui/index.ts b/mobile/components/ui/index.ts
new file mode 100644
index 0000000..1ce238e
--- /dev/null
+++ b/mobile/components/ui/index.ts
@@ -0,0 +1,9 @@
+// Barrel des composants UI DSFR réutilisables.
+export { default as AppBar } from './AppBar';
+export { default as Badge } from './Badge';
+export { default as Button } from './Button';
+export { default as Card } from './Card';
+export { default as EmptyState } from './EmptyState';
+export { default as Loader } from './Loader';
+export { default as SectionTitle } from './SectionTitle';
+export { default as TextField } from './TextField';
diff --git a/mobile/hooks/useAdmin.ts b/mobile/hooks/useAdmin.ts
new file mode 100644
index 0000000..b77bd4a
--- /dev/null
+++ b/mobile/hooks/useAdmin.ts
@@ -0,0 +1,123 @@
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import api from '@/lib/api';
+import type { Emotion, Feed, Utilisateur } from '@/types';
+
+// Toutes les requêtes ci-dessous tapent les routes /admin/* du backend,
+// protégées par le middleware role:administrateur.
+
+/** Normalise une réponse paginée Laravel ({data:[...]}) ou un tableau brut. */
+function unwrap(payload: { data?: T } | T): T {
+ const body = payload as { data?: T };
+ return (body?.data ?? payload) as T;
+}
+
+// ----------------------------- Utilisateurs --------------------------------
+
+export function useAdminUtilisateurs() {
+ return useQuery({
+ queryKey: ['admin', 'utilisateurs'],
+ queryFn: async () => unwrap((await api.get('/admin/utilisateurs')).data),
+ });
+}
+
+export interface NouvelUtilisateur {
+ nom: string;
+ prenom: string;
+ email: string;
+ password: string;
+ password_confirmation: string;
+ role_id: number;
+}
+
+export function useCreateUtilisateur() {
+ const qc = useQueryClient();
+ return useMutation({
+ mutationFn: async (data: NouvelUtilisateur) => (await api.post('/admin/utilisateurs', data)).data,
+ onSuccess: () => qc.invalidateQueries({ queryKey: ['admin', 'utilisateurs'] }),
+ });
+}
+
+export function useToggleUtilisateurActif() {
+ const qc = useQueryClient();
+ return useMutation({
+ mutationFn: async (id: number) => (await api.patch(`/admin/utilisateurs/${id}/toggle-active`)).data,
+ onSuccess: () => qc.invalidateQueries({ queryKey: ['admin', 'utilisateurs'] }),
+ });
+}
+
+export function useDeleteUtilisateur() {
+ const qc = useQueryClient();
+ return useMutation({
+ mutationFn: async (id: number) => (await api.delete(`/admin/utilisateurs/${id}`)).data,
+ onSuccess: () => qc.invalidateQueries({ queryKey: ['admin', 'utilisateurs'] }),
+ });
+}
+
+// -------------------------------- Feeds ------------------------------------
+
+export function useAdminFeeds() {
+ return useQuery({
+ queryKey: ['admin', 'feeds'],
+ queryFn: async () => unwrap((await api.get('/admin/feeds')).data),
+ });
+}
+
+export interface FeedPayload {
+ titre: string;
+ contenu: string;
+ image_url?: string | null;
+ est_publie: boolean;
+ ordre: number;
+}
+
+export function useSaveFeed() {
+ const qc = useQueryClient();
+ return useMutation({
+ mutationFn: async ({ id, ...data }: FeedPayload & { id?: number }) =>
+ id ? (await api.put(`/admin/feeds/${id}`, data)).data : (await api.post('/admin/feeds', data)).data,
+ onSuccess: () => qc.invalidateQueries({ queryKey: ['admin', 'feeds'] }),
+ });
+}
+
+export function useDeleteFeed() {
+ const qc = useQueryClient();
+ return useMutation({
+ mutationFn: async (id: number) => (await api.delete(`/admin/feeds/${id}`)).data,
+ onSuccess: () => qc.invalidateQueries({ queryKey: ['admin', 'feeds'] }),
+ });
+}
+
+// ------------------------------- Émotions ----------------------------------
+
+export function useAdminEmotions() {
+ return useQuery({
+ queryKey: ['admin', 'emotions'],
+ queryFn: async () => unwrap((await api.get('/admin/emotions')).data),
+ });
+}
+
+export interface EmotionPayload {
+ nom: string;
+ couleur: string;
+ icone?: string | null;
+ niveau: number;
+ parent_id?: number | null;
+ est_actif: boolean;
+}
+
+export function useSaveEmotion() {
+ const qc = useQueryClient();
+ return useMutation({
+ mutationFn: async ({ id, ...data }: EmotionPayload & { id?: number }) =>
+ id ? (await api.put(`/admin/emotions/${id}`, data)).data : (await api.post('/admin/emotions', data)).data,
+ onSuccess: () => qc.invalidateQueries({ queryKey: ['admin', 'emotions'] }),
+ });
+}
+
+export function useDeleteEmotion() {
+ const qc = useQueryClient();
+ return useMutation({
+ mutationFn: async (id: number) => (await api.delete(`/admin/emotions/${id}`)).data,
+ onSuccess: () => qc.invalidateQueries({ queryKey: ['admin', 'emotions'] }),
+ });
+}
diff --git a/mobile/lib/theme.ts b/mobile/lib/theme.ts
new file mode 100644
index 0000000..d948e36
--- /dev/null
+++ b/mobile/lib/theme.ts
@@ -0,0 +1,66 @@
+// 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.
+
+/** Espacements (échelle 4px, comme le DSFR). */
+export const Spacing = {
+ xs: 4,
+ sm: 8,
+ md: 12,
+ lg: 16,
+ xl: 24,
+ xxl: 32,
+ xxxl: 40,
+} as const;
+
+/** Rayons de bordure. Le DSFR est volontairement « carré » (rayons faibles). */
+export const Radius = {
+ sm: 4,
+ md: 8,
+ pill: 999,
+} as const;
+
+/** Tailles de police. */
+export const FontSize = {
+ tiny: 11,
+ xs: 13,
+ sm: 14,
+ body: 15,
+ md: 16,
+ lg: 18,
+ xl: 20,
+ xxl: 24,
+ display: 32,
+} as const;
+
+/** Graisses (typées pour React Native). */
+export const FontWeight = {
+ regular: '400',
+ medium: '500',
+ semibold: '600',
+ bold: '700',
+ heavy: '800',
+} as const;
+
+/** Ombres douces, discrètes (le DSFR reste sobre). */
+export const Shadow = {
+ card: {
+ shadowColor: '#161616',
+ shadowOffset: { width: 0, height: 1 },
+ shadowOpacity: 0.06,
+ shadowRadius: 4,
+ elevation: 1,
+ },
+ raised: {
+ shadowColor: '#161616',
+ shadowOffset: { width: 0, height: 4 },
+ shadowOpacity: 0.12,
+ shadowRadius: 10,
+ elevation: 4,
+ },
+} as const;
+
+/** Cible tactile minimale recommandée (accessibilité). */
+export const MIN_TOUCH = 44;
+
+/** 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;