# Kotlin Compose patterns — @fn_compose

Pautas autoritativas para apps Android Kotlin Compose del registry. Mirror de `cpp/PATTERNS.md` (C++ ImGui) y `frontend/functions/ui/` (web Mantine).

## 1. Dark-first

`FnTheme` aplica dark por defecto. Mirror del web stack donde `FnMantineProvider` hardcodea `defaultColorScheme="dark"`.

```kotlin
setContent {
    FnTheme {                                  // darkMode = true por default
        Surface(Modifier.fillMaxSize()) { ... }
    }
}

// Override explicito:
FnTheme(darkMode = false) { ... }              // forzar light
FnTheme(darkMode = isSystemInDarkTheme()) { } // seguir sistema
```

NUNCA `MaterialTheme {}` directo. SIEMPRE `FnTheme {}`.

## 2. Tokens — fuente unica

Todo valor visual via `fn.compose.theme.FnTokens` o sus alias directos:

```
FnColors      paleta Mantine 50→950 (gray, brand) + status (red, green, yellow, blue) + pure
FnSpacing     xs=8 sm=12 md=16 lg=24 xl=32 xxl=48 (Dp)
FnRadius      none=0 xs=2 sm=4 md=8 lg=12 xl=16 full=9999 (Dp)
FnTypography  sizes xs..xl + h1..h6 + weights normal/medium/semibold/bold + ready TextStyles
FnShadows     none/xs/sm/md/lg/xl elevation (Dp)
```

**Regla:** si haces `Color(0xFF...)` o `8.dp` literal en codigo app, ESTA MAL. Usar tokens.

Excepcion permitida: literales en helpers privados que SOLO retornan tokens (ej. `categoryColor(c) -> FnBadgeColor`).

## 3. Componentes propios, no Material3 directo

```
Material3 raw    ← solo dentro de @fn_compose
   ↓
@fn_compose      ← apps consumen estos
   ↓
App-specific UI  ← logica del dominio
```

Apps importan `fn.compose.ui.FnButton`, NUNCA `androidx.compose.material3.Button`.

Primitivas Compose foundation OK en apps: `Box`, `Column`, `Row`, `Spacer`, `Modifier`, `LazyColumn`. Preferir `FnStack`/`FnGroup` para layouts agregados.

## 4. Catalogo de componentes (28)

### Theming (8)
- `FnTheme` — provider
- `FnTokens` — agregador
- `FnColors` / `FnSpacing` / `FnRadius` / `FnTypography` / `FnShadows`

### Layout (4)
- `FnStack` — Column con gap
- `FnGroup` — Row con gap
- `FnPaper` — Surface con tokens
- `FnAppShell` — Scaffold

### Display (5)
- `FnText` — body xs..xl
- `FnTitle` — h1..h6
- `FnCard` — variants Default/Borderless/Ghost
- `FnBadge` — pill 6 colores semanticos
- `FnAvatar` — circle con initials, Sm/Md/Lg

### Input (5)
- `FnButton` — variants Filled/Outlined/Secondary/Ghost/Destructive/Link
- `FnTextInput` — OutlinedTextField + label/error
- `FnSelect` — ExposedDropdownMenu
- `FnSwitch` / `FnCheckbox`

### Navigation (1)
- `FnTabs` — TabRow + ScrollableTabRow

### Feedback (4)
- `FnAlert` — Info/Success/Warning/Error theme-aware
- `FnLoader` — CircularProgress Sm/Md/Lg
- `FnSkeleton` — animated shimmer
- `FnDialog` — AlertDialog wrapper + destructive variant

### Data (3)
- `FnDataTable` — Column-based table generico
- `FnKpiCard` — label + value + delta + sparkline
- `FnPageHeader` — title + subtitle + actions slot
- `FnEmptyState` — icon + title + description + action

### Charts (3)
- `FnLineChart` — Canvas-based, con area fill opcional
- `FnBarChart` — vertical bars + labels
- `FnSparkline` — mini inline chart (KPIs, tablas)

## 5. Variants estandar (mirror Mantine)

| Componente | Variants |
|---|---|
| `FnButton` | Filled, Outlined, Secondary, Ghost, Destructive, Link |
| `FnCard` | Default (border+shadow), Borderless (bg only), Ghost (transparent) |
| `FnBadge` | Brand, Gray, Green, Red, Yellow, Blue |
| `FnAlert` | Info, Success, Warning, Error (theme-aware en dark+light) |
| `FnAvatar` | Sm (28dp), Md (40dp), Lg (56dp) |
| `FnLoader` | Sm (16dp), Md (32dp), Lg (56dp) |
| `FnText` | size Xs/Sm/Md/Lg/Xl |
| `FnTitle` | order 1..6 |

## 6. Layout patterns

### App shell estandar

```kotlin
FnAppShell(title = "Mi App") { padding ->
    FnStack(
        modifier = Modifier.padding(padding).fillMaxSize(),
        gap = FnSpacing.md,
    ) {
        FnPageHeader("Seccion", subtitle = "Descripcion")
        // content...
    }
}
```

Body slot recibe `PaddingValues` → aplicar SIEMPRE para no solapar topbar.

### Lista de items
```kotlin
LazyColumn(verticalArrangement = Arrangement.spacedBy(FnSpacing.sm)) {
    items(data) { item -> ItemRow(item) }
}
```

### Tabs navegacion entre categorias
```kotlin
var tab by remember { mutableStateOf(0) }
FnTabs(tabs = categorias, selectedIndex = tab, onTabSelected = { tab = it }, scrollable = true)
when (tab) { 0 -> ScreenA(); 1 -> ScreenB() ... }
```

### Dashboard grid KPIs
```kotlin
FnGroup(gap = FnSpacing.sm) {
    FnKpiCard(label = "Revenue", value = "€42k", sparklineData = ..., modifier = Modifier.weight(1f))
    FnKpiCard(label = "Churn",   value = "3.1%", sparklineData = ..., modifier = Modifier.weight(1f))
}
```

### Tabla
```kotlin
FnDataTable(
    rows = items,
    modifier = Modifier.fillMaxWidth(),
    columns = listOf(
        FnTableColumn(header = "Nombre", weight = 2f, cell = { FnText(it.name) }),
        FnTableColumn(header = "Estado", weight = 1f, cell = { FnBadge(it.status, color = ...) }),
    ),
)
```

NO anidar `FnDataTable` dentro de `Modifier.verticalScroll()` con muchas rows — usa `LazyColumn` arriba o limita altura. FnDataTable internamente usa Column estatica.

## 7. Programacion funcional

- **Datos inmutables** — `data class` con `val`
- **Pure helpers** — Composables stateless. Transformaciones `state → state` puras fuera de Composables
- **State via mutableStateOf** — Compose lo requiere para reactividad
- **Transformaciones** — `state = puraTransform(state, evento)`. NUNCA `state.add(...)`
- **Side effects bordes** — solo en callbacks (`onClick`, `onValueChange`)
- **Tests JVM puros** — `PureLogicTest.kt` valida helpers sin Compose

Ejemplo canonico:
```kotlin
internal fun appendMyMessage(messages: List<Message>, text: String): List<Message> =
    if (text.isBlank()) messages
    else messages + Message("Yo", text, "ahora", true)

@Composable
fun ChatScreen() {
    var msgs by remember { mutableStateOf(MOCK_MESSAGES) }
    var draft by remember { mutableStateOf("") }
    ChatContent(
        messages = msgs,
        draft = draft,
        onDraftChange = { draft = it },
        onSend = {
            msgs = appendMyMessage(msgs, draft)   // pure
            draft = ""
        },
    )
}
```

## 8. Testing — 3 niveles

| Nivel | Donde | Que valida | Cuando |
|---|---|---|---|
| `PureLogicTest` | `src/test/.../` | Funciones puras JVM | Cada PR |
| `ExampleScreenshotTest` (Roborazzi) | `src/test/.../` | Snapshots Composables light+dark+edge | Cada PR |
| `MainActivityTest` (instrumented) | `src/androidTest/.../` | Real device/emulator clicks | E2E gate |

Pattern Roborazzi:
```kotlin
@RunWith(RobolectricTestRunner::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE)
@Config(qualifiers = "w360dp-h800dp-xhdpi")
class ScreenshotTest {
    @get:Rule val composeTestRule = createComposeRule()

    @Test fun snapshotLight() {
        composeTestRule.setContent { FnTheme(darkMode = false) { Surface { ScreenContent(...) } } }
        composeTestRule.onRoot().captureRoboImage("src/test/snapshots/images/screen_light.png")
    }
}
```

Comando record goldens: `./gradlew :app:recordRoborazziDebug`. Verify: `:app:verifyRoborazziDebug`. Goldens (.png) se commitean.

## 9. testTag para tests instrumented

Componentes interactivos llevan `Modifier.testTag("nombre_corto")`:

```kotlin
Button(modifier = Modifier.testTag("btn_submit"), ...)
```

Test:
```kotlin
composeTestRule.onNodeWithTag("btn_submit").performClick()
```

NUNCA `onNodeWithText("Submit")` — rompe al traducir strings o si texto duplicado.

Texto duplicado en tree → `onAllNodesWithText("X")[0]` en lugar de `onNodeWithText`. Detectado real: tests fallan con `Expected at most 1 node but found 2` si dos componentes muestran mismo string.

## 10. Visibilidad vs existencia

- `assertIsDisplayed()` — visible en viewport. Falla si below-the-fold.
- `assertExists()` — solo presente en tree. Para items en scroll containers.

Regla: items below viewport en scroll → `assertExists()`.

## 11. e2e_checks declarado en app.md

Toda app nueva (via scaffolder) genera bloque:

```yaml
e2e_checks:
  - id: unit         # gradle :app:test
  - id: screenshot   # gradle :app:verifyRoborazziDebug
  - id: build        # gradle :app:assembleDebug
  - id: emu_start    # arrancar AVD
  - id: instrumented # gradle :app:connectedAndroidTest
  - id: emu_stop     # cleanup
```

`fn-analizador` consume esto en fase 4 del bucle reactivo.

## 12. Composite build local

Apps importan `@fn_compose` via `includeBuild("../../kotlin/functions/ui")` en `settings.gradle.kts`. NO maven artifact. Cambios en library → apps recompilan auto.

## 13. Scaffolder canonico (OBLIGATORIO)

Apps Kotlin nuevas SIEMPRE via:

```bash
./fn run init_kotlin_app <name> [--project <p>] [--package com.x.y]
```

NUNCA crear MainActivity + build.gradle.kts + app.md a mano. Si scaffolder no cubre un caso, modifica el scaffolder. Plantilla = codigo, no decoracion.

## 14. Equivalencias cross-stack

```
Concepto         Web Mantine             C++ ImGui            Android Compose
─────────────    ───────────────         ──────────────       ───────────────
Provider         <MantineProvider>       fn::run_app cfg      FnTheme {}
Default theme    dark                    ThemeMode::FnDark    darkMode=true
Tokens           theme.colors            fn_tokens::colors    FnColors / FnTokens
Container        <Paper>                 fn_ui::Card          FnPaper / FnCard
Button variants  default/outline/...     n/a                  Filled/Outlined/...
Card variants    default/borderless/...  n/a                  Default/Borderless/Ghost
Layout col       <Stack>                 ImGui::BeginGroup    FnStack
Layout row       <Group>                 SameLine             FnGroup
Heading          <Title order={N}>       PushFont(big)        FnTitle order=N
Body text        <Text size="md">        Text(...)            FnText size=Md
Badge            <Badge>                 colored span         FnBadge
Alert            <Alert>                 message box          FnAlert
Loader           <Loader>                spinner widget       FnLoader
Skeleton         <Skeleton>              n/a                  FnSkeleton
App scaffold     <AppShell>              dockspace + menu     FnAppShell
Tabs             <Tabs>                  ImGui::BeginTabBar   FnTabs
Modal            <Dialog>                modal_dialog         FnDialog
Avatar           <Avatar>                n/a                  FnAvatar
Select           <Select>                ImGui::Combo         FnSelect
Toggle           <Switch>                ImGui::Checkbox      FnSwitch
Table            <DataTable>             fn_ui::table         FnDataTable
Line chart       <LineChart>             fn_viz::line_plot    FnLineChart
Bar chart        <BarChart>              fn_viz::bar_chart    FnBarChart
Sparkline        <Sparkline>             n/a                  FnSparkline
KPI card         <KPICard>               n/a                  FnKpiCard
Empty state      <EmptyState>            n/a                  FnEmptyState
Page header      <PageHeader>            n/a                  FnPageHeader
```

Cambios paleta brand en un sitio → propagan cross-stack manualmente. Tracker drift en backlog.

## 15. Gallery showcase

App `apps/gallery_kt/` muestra los 28 componentes organizados por categoria con FnTabs. Sirve como:
- Regression visual gate via Roborazzi snapshots
- Onboarding rapido para devs nuevos
- Documentacion ejecutable

Rebuilds y screenshots: `./fn run run_kotlin_app_tests apps/gallery_kt`.

## 16. Anti-patterns

| Anti-patron | Por que mal | Sustituir por |
|---|---|---|
| `Color(0xFFAABBCC)` en app code | Bypass tokens | `FnColors.gray700` |
| `8.dp` literal en app code | Bypass spacing | `FnSpacing.xs` |
| `MaterialTheme {}` en app | No usa tokens design system | `FnTheme {}` |
| `androidx.compose.material3.Button` directo | Sin variants ni shape consistente | `FnButton` |
| `var msgs = mutableListOf<X>()` | Mutable state mutable | `var msgs by remember { mutableStateOf(emptyList<X>()) }` |
| `state.add(x)` | Mutacion in-place | `state = state + x` |
| Helpers @Composable solo para colores | Side-effect implicito | Funcion pura que retorna `FnBadgeColor` enum |
| `LazyColumn` dentro de `verticalScroll` | Infinity constraint crash | `Column` o limitar altura |
| `onNodeWithText` para tests | Romper en traducciones / duplicados | `testTag` + `onNodeWithTag` |
| Logica de negocio inline en MainActivity | No testeable | Extraer helpers puros + `internal` para tests |
| Scaffolder a mano | Drift entre apps | `fn run init_kotlin_app` |

## 17. Reglas de incorporacion de componentes nuevos

1. Buscar primero en registry — `mcp__registry__fn_search` por nombre/desc/code
2. Mirar el equivalente Mantine (`frontend/functions/ui/`) para inspirar API
3. Crear via `fn-constructor` con prompt tight (no inline en apps)
4. `lang: kt`, `domain: ui`, `kind: component`, `framework: compose`, `error_type: error_go_core`
5. Theme-aware: usar `MaterialTheme.colorScheme.*` para colores semanticos
6. Tokens primero: `FnSpacing.*`, `FnRadius.*`, `FnTypography.*` antes que valores raw
7. Stateless preferido — caller mantiene state via `remember`
8. Variants via `enum class` — no booleanos sueltos
9. `Modifier.testTag` en cualquier elemento interactivo
10. Update gallery_kt con seccion nueva tras crearlo
