La gueule de bois du vibe coding
Vous avez ouvert Cursor, tapé un prompt et regardé Claude Code générer une UI React Native complète en quelques secondes. Layouts flex, boutons arrondis, thèmes de couleur, tout fonctionnait du premier coup. Vous avez livré sur TestFlight. Les utilisateurs ont commencé à télécharger. C’était la première semaine.
À la sixième semaine, vous deviez changer une seule couleur de marque et cela vous a pris trois heures de find-and-replace à travers quarante-sept fichiers.
Voilà le coût caché du vibe coding sans contrat de styling. Quand chaque composant code en dur des classes utilitaires Tailwind, votre moteur de styling devient une dépendance qui fuit dans chaque écran, chaque hook, chaque route guard. Changer A casse B, C et D, non pas parce que votre logique est mauvaise, mais parce que votre styling n’a aucune frontière.
Pourquoi NativeWind devient un piège
Voici ce que Cursor génère quand vous demandez un bouton. Ça a l’air propre. C’est aussi un cauchemar de maintenance :
// This is what Claude Code gives you. It works until it doesn't.
<View className='p-4 flex-row justify-between'>
<Text className='text-lg font-bold text-slate-800'>
Title
</Text>
<Button className='bg-blue-500 px-4 py-2 rounded-lg' />
</View>
Le problème n’est pas ce code. Le problème, c’est le pattern. Quand chaque fichier contient une logique de styling brute, vous ne pouvez plus faire évoluer votre design system sans toucher à tous les fichiers. L’IA livre vite, mais elle livre du couplage. Votre codebase qui “marche” est en réalité un château de cartes.
La solution : un contrat de thème
Votre app doit parler à un contrat de thème, pas à un moteur de styling. Le contrat vit dans un seul fichier. Les primitives vivent dans un seul dossier. Tout le reste ne fait qu’utiliser ces primitives.
// src/constants/theme.ts
// The ONLY file that defines your app's visual identity.
// Claude Code reads this. Cursor references it.
export interface ThemeContract {
colors: {
background: string
foreground: string
surface: string
primary: string
danger: string
}
spacing: { xs: number; sm: number; md: number; lg: number; xl: number }
radius: { sm: number; md: number; lg: number; full: number }
typography: { title: number; body: number; caption: number }
}
export const theme = {
colors: {
background: '#0B0F19',
foreground: '#F8FAFC',
surface: '#111827',
primary: '#2563EB',
danger: '#DC2626',
},
spacing: { xs: 4, sm: 8, md: 16, lg: 24, xl: 32 },
radius: { sm: 8, md: 12, lg: 20, full: 999 },
typography: { title: 28, body: 16, caption: 13 },
} as const satisfies ThemeContract
Le mot-clé satisfies est l’arme secrète. TypeScript vérifie le contrat, mais préserve les types littéraux. Vous avez l’autocomplétion sur theme.spacing.md. Claude Code a un schéma qu’il peut suivre. Tout le monde y gagne.
Des primitives sémantiques : votre vocabulaire UI
Les primitives sémantiques sont les seuls fichiers qui touchent à StyleSheet. Un Button connaît ses variantes et ses tailles. Un Stack connaît le gap et l’alignement. Aucun écran n’écrit className :
// src/components/ui/button.tsx
// Public API: semantic props only.
import { Pressable, Text } from 'react-native'
import { theme } from '@/constants/theme'
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'danger'
size?: 'sm' | 'md' | 'lg'
loading?: boolean
onPress: () => void
children: string
}
export function Button({
variant = 'primary',
size = 'md',
loading,
onPress,
children,
}: ButtonProps) {
const bgMap = {
primary: theme.colors.primary,
secondary: theme.colors.surface,
danger: theme.colors.danger,
}
const heightMap = { sm: 36, md: 44, lg: 52 }
return (
<Pressable
onPress={onPress}
disabled={loading}
accessibilityRole='button'
accessibilityState={{ busy: loading }}
style={{
backgroundColor: bgMap[variant],
height: heightMap[size],
paddingHorizontal: theme.spacing.lg,
borderRadius: theme.radius.md,
justifyContent: 'center',
alignItems: 'center',
opacity: loading ? 0.6 : 1,
}}>
<Text style={{ color: '#FFF', fontSize: theme.typography.body }}>
{children}
</Text>
</Pressable>
)
}
Changez un fichier, tous les boutons sont mis à jour. Générez de nouveaux écrans contre le contrat, et votre architecture reste intacte.
La barrière ESLint
Un contrat sans enforcement n’est qu’une suggestion. Ajoutez une règle qui rejette className en dehors de la couche de primitives :
// eslint.config.js
{
files: ['app/**/*', 'src/**/*'],
ignores: ['src/components/ui/**/*', 'src/components/layout/**/*'],
rules: {
'no-restricted-syntax': ['error', {
selector: 'JSXAttribute[name.name="className"]',
message: 'Use semantic UI primitives instead of className.'
}]
}
}
L’Autotomy Expo Starter Pack livre ce garde-fou préconfiguré. Vous gardez la vitesse de Cursor avec une architecture capable de survivre à vos mille premiers utilisateurs.