# SKILL: Components — gotchas + 18 patrones canónicos

Cargar cuando AIREF no alcanza: necesitás un patrón específico (form, async, modal, slots, teleport) o entender por qué un componente se rompe silenciosamente.

## Gotcha #1 — `.value` en template

```
state.X        template → SIN .value    (Proxy auto-unwrap)
state.X.value  script   → CON .value
props.X        template → SIN .value    (props NO son signals)
props.X.value           → NUNCA. undefined.value crashea silencioso si prop no se pasa.
```
Analyzer rechaza `props.X.value` en template con `PROP_VALUE_IN_TEMPLATE`.

```html
<!-- ❌ siempre roto -->
<div v-if="state.open.value">…</div>
<input :value="state.email.value" />
<!-- ✅ -->
<div v-if="state.open">…</div>
<input :value="state.email" />
```

## Gotcha #2 — Snapshot en computed con closure

```typescript
// ❌ dep no se registra (closure cacheada)
const cls = computed(() => (tab) => tab === activeTab.value ? 'on' : 'off')
// ✅ leer dep en el getter
const cls = computed(() => { const a = activeTab.value; return (t) => t === a ? 'on' : 'off' })
```

## Gotcha #3 — `@click` con argumento NO acepta curried

El template-compiler convierte `@click="actions.fn(arg)"` en `onClick: (e) => { actions.fn(arg) }`. **Llama `fn(arg)` y descarta el retorno.** Si `fn` devuelve una función, esa función NUNCA se ejecuta.

```typescript
// ❌ ROTO en @click nativo — el retorno se ignora
function setActive(id: string) { return () => { currentView.value = id } }
// template: @click="actions.setActive('users')"  → llama setActive('users'), retorno descartado, signal NO cambia

// ✅ patrón correcto — muta directo en el cuerpo
function setActive(id: string) { currentView.value = id }
// template: @click="actions.setActive('users')"  → llama setActive('users'), signal cambia ✓
```

**Excepción única**: el patrón curried SÍ funciona como prop a componentes hijos, porque el Child recibe el resultado como prop y lo invoca él mismo:
```html
<Button :onClick="actions.makeHandler(item.id)" />   <!-- Child invoca el retorno -->
```
Pero en `@click` del DOM nativo, el compiler descarta el retorno. Analyzer detecta este patrón con `CURRIED_ACTION_IN_TEMPLATE`.

## Gotcha #4 — Form submit OBLIGATORIO stopPropagation

```typescript
function submit(e: Event) {
  e.preventDefault(); e.stopPropagation()   // burbujea — Component padre puede capturar y disparar 2x
}
```

## Gotcha #5 — Anti-patterns silenciosos (analyzer NO detecta)

```typescript
// ❌ mutar dentro de computed (side effect)
const x = computed(() => { otroSignal.value = 5; return a.value * 2 })
// ❌ hook fuera del sync setup
onMount(async () => { await fetch('/x'); onUnmount(() => {}) })  // hooks usan estado global
// ❌ mount con HTMLElement (toma string)
mount(App, document.getElementById('app')!)   // ✗
mount(App, '#app')                            // ✓
```

## Gotcha #6 — Wrapper invisible del child

`h(ChildComp, props, ...children)` crea `<div style="display:contents">` con `__arkUnmount`, `__arkSlot`, `__arkTag`. Props `onX` se registran 2x: prop del child + listener del wrapper. Inline closures `:onClick="() => x()"` NO re-montan (`diffComponentProps` lo resuelve desde v0.16+).

---

## PATRONES — 18 canónicos

### P1 — `:class` ternario reactivo

```typescript
const btnCls = computed(() => state.active.value ? 'on' : 'off')
// :class="computed.btnCls"
```

### P2 — Input controlado

```html
<input :value="state.email" @input="actions.onEmail" />
```
```typescript
function onEmail(e: Event) { email.value = (e.target as HTMLInputElement).value }
```

### P3 — Action con argumento en v-for

```typescript
// ✅ muta directo, NO usa return ()=>{}
function toggle(id: string) {
  todos.value = todos.value.map(t => t.id === id ? {...t, done: !t.done} : t)
}
// @click="actions.toggle(todo.id)"   ← compiler invoca toggle(id), todos.value se actualiza
```
Ver Gotcha #3 — el patrón curried (`return () => {}`) está prohibido en `@click` nativo porque el compiler descarta el retorno.

### P4 — Comm hijo → padre (emit)

```typescript
// child:  emit('user-saved', payload)
// parent: <UserCard @user-saved="actions.onSave" />
// parent: function onSave(payload) {}             // recibe e.detail directo
```

### P5 — Modal con slot default

```html
<template>
  <div v-show="props.visible" class="fixed inset-0 ...">
    <header>{{ props.title }} <button @click="actions.close">✕</button></header>
    <slot />
  </div>
</template>
```

### P6 — Form submit + validación (stopPropagation)

```typescript
function submit(e: Event) {
  e.preventDefault(); e.stopPropagation()
  if (computed.formInvalid.value) return
  /* dispatch a service */
}
```

### P7 — Async action ⚡ (try/catch + toast)

**Desde v0.24.3**: `<Button>` detecta automáticamente onClick async, se deshabilita durante la promesa, muestra spinner, y previene doble submit. Solo escribís el try/catch:

```typescript
async function save() {
  try { await userService.update(form); toast.actions.success('Guardado') }
  catch { toast.actions.error('No se pudo guardar') }
}
// <Button :onClick="actions.save">Guardar</Button>   ← Button maneja loading SOLO
```

Si querés forzar loading manual (ej. desde un signal global), pasás `:loading="..."` explícito. Analyzer aún dispara `ASYNC_ACTION_NO_ERROR_HANDLING` si falta try/catch.

### P8 — Lista con `:key`

```html
<li v-for="todo in state.todos" :key="todo.id">…</li>
<TaskCard v-for="t in state.tasks" :key="t.id" :task="t" />
```

### P9 — Error boundary (onError)

```typescript
import { onError } from 'arckode-ui'
onError((err) => { toast.actions.error('Error'); return true })   // true=suprime, undef=propaga
```

### P10 — `<style scoped>`

```html
<style scoped>
  .card { padding: 1rem }                  /* → .card[data-ark-XXX] */
  :host { display: block }                 /* → [data-ark-XXX] */
  :global(body.dark) .x { color: white }   /* escapa scope */
  @keyframes fadeIn { 0% {} 100% {} }      /* opaco — no scopea */
</style>
```
Default = Tailwind utilities. Scoped solo para pseudo-clases complejas, keyframes, selectores imposibles con utilities.

### P11 — Lazy route

```typescript
const routes = [
  { path: '/', component: HomePage },
  { path: '/dashboard', component: () => import('./pages/dashboard.ark') },
  { path: '/reports', component: () => import('./pages/reports.ark'),
    loading: () => h('div', null, 'Cargando…'),
    error: (err) => h('div', null, `Error: ${err}`) },
]
```

### P12 — Named slots

```html
<!-- def -->
<article>
  <header v-if="props.__slot_header"><slot name="header"/></header>
  <main><slot/></main>
  <footer v-if="props.__slot_footer"><slot name="footer"/></footer>
</article>

<!-- uso -->
<Card>
  <template #header><h2>Título</h2></template>
  <template #footer><Button>OK</Button></template>
  <p>Body</p>
</Card>
```

### P13 — Scoped slot (render custom por item)

```html
<!-- def --> <li v-for="item in props.items" :key="item.id"><slot :item="item"/></li>
<!-- uso -->
<DataList :items="state.users">
  <template #default="{ item }"><Avatar :name="item.name"/></template>
</DataList>
```

### P14 — Teleport (modal/tooltip fuera del DOM tree)

```html
<script lang="ts">import { Teleport } from 'arckode-ui'</script>      <!-- IMPORT OBLIGATORIO -->
<template>
  <Teleport to="#modals">
    <div v-if="state.open" class="fixed inset-0 z-50" @click="actions.close">
      <div @click.stop>contenido</div>
    </div>
  </Teleport>
</template>
```
Prerequisito: `<div id="modals"></div>` en `index.html` o `_layout.ark`.

### P15 — Dynamic component

```html
<component :is="computed.activeTab" :data="state.data" />
```
```typescript
const TABS = { overview: OverviewTab, stats: StatsTab } as const
const activeTab = computed(() => TABS[state.activeId.value])
```

### P16 — `batch()` (3+ signals juntos)

```typescript
import { batch } from 'arckode-ui'
function reset() { batch(() => { filterText.value=''; filterTag.value=null; page.value=1 }) }
```

### P17 — `useHead()` (SEO)

```typescript
import { useHead } from 'arckode-ui'
useHead({ title: 'Sobre · Mi App', description: '...',
  meta: [{ property: 'og:title', content: '...' }, { property: 'og:image', content: '...' }],
  link: [{ rel: 'canonical', href: '...' }] })
useHead(() => ({ title: user.value ? `${user.value.name} · Profile` : 'Loading…' }))   // reactivo
```
Share previews (WhatsApp/Twitter/Discord) NO ejecutan JS → requieren SSG.

### P18 — `arckode-disable`

```html
<!-- arckode-disable PRIMITIVE_INLINE -->
```
```typescript
// arckode-disable HIGH_CYCLOMATIC_COMPLEXITY
```
Primeras 5 líneas del archivo. Solo cuando hay razón documentada.
