A Ressaca do Vibe Coding

Você abriu o Cursor, digitou um prompt e viu o Claude Code gerar uma UI inteira em React Native em segundos. Layouts com flex, botões arredondados, temas de cor, tudo funcionando de primeira. Você publicou no TestFlight. Os usuários começaram a baixar. Isso foi na primeira semana.

Na sexta semana, você precisou mudar uma única cor da marca e gastou três horas fazendo find-and-replace em quarenta e sete arquivos.

Esse é o custo escondido do vibe coding sem um contrato de styling. Quando cada componente hardcodeia utility classes do Tailwind, seu mecanismo de styling vira uma dependência que vaza para cada tela, cada hook, cada route guard. Mudar A quebra B, C e D, não porque sua lógica está errada, mas porque seu styling não tem fronteiras.

Por que o NativeWind Vira uma Armadilha

É isso que o Cursor gera quando você pede um botão. Parece limpo. Também é um pesadelo de manutenção:

// 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>

O problema não é o código. É o padrão. Quando todo arquivo contém lógica de styling bruta, você não consegue mudar seu design system sem tocar em todo arquivo. A IA entrega rápido, mas entrega acoplamento. Sua codebase “funcionando” é, na verdade, um castelo de cartas.

A Correção: um Contrato de Tema

Sua app deveria conversar com um contrato de tema, não com um mecanismo de styling. O contrato mora em um arquivo. Os primitivos moram em uma pasta. Todo o resto só usa os primitivos.

// 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

A keyword satisfies é a arma secreta. O TypeScript valida o contrato, mas preserva os tipos literais. Você ganha autocomplete em theme.spacing.md. O Claude Code ganha um schema que consegue seguir. Todo mundo ganha.

Primitivos Semânticos: o Vocabulário da sua UI

Primitivos semânticos são os únicos arquivos que encostam em StyleSheet. Um Button conhece variantes e tamanhos. Um Stack conhece gap e alinhamento. Nenhuma tela escreve 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>
  )
}

Mude um arquivo e todos os botões atualizam. Gere novas telas contra o contrato e a sua arquitetura continua intacta.

O Gate do ESLint

Contratos sem enforcement são só sugestões. Adicione uma regra que rejeita className fora da costura dos primitivos:

// 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.'
    }]
  }
}

O Autotomy Expo Starter Pack já vem com esse guardrail configurado. Você ganha a velocidade do Cursor com uma arquitetura que sobrevive aos seus primeiros mil usuários.