# arckode-ui — AIREF (tier 1 mínimo)

Lo mínimo para empezar. Profundizar → sub-skills al final.

## STACK

| Dep | Versión | Razón |
|---|---|---|
| `vite` | `^5.4` (NO 6+) | plugin usa `transformWithEsbuild` removido en v6 |
| `tailwindcss` + `@tailwindcss/vite` | `^4.0` | `@import "tailwindcss"` + `@theme` (NO PostCSS) |
| `typescript` | `^5.0` | `<script lang="ts">` obligatorio |

## BOOTSTRAP — scaffold completo (`ark new frontend`)

```bash
npx arckode-ui new frontend mi-app
cd mi-app && bun install && bun dev
```

### Lo que genera `ark new frontend`

```
mi-app/
├── index.html
├── vite.config.ts               ← arkcodeUi() + tailwindcss()
├── tsconfig.json
├── package.json                 ← arckode-ui + vite + tailwind + typescript
├── AGENTS.md / CLAUDE.md        ← pointers a AIREF.md (para Claude/OpenCode)
├── arckode.json                 ← manifiesto del proyecto
├── UI.ref / STORES.ref / SERVICES.ref / ROUTES.ref   ← stubs auto-generados
│
├── src/
│   ├── main.ts                  ← createRouter() + mount(App, '#app')
│   ├── App.ark                  ← entry point con auth check en onMount
│   ├── guards.ts                ← protectPage() y requireAuth()
│   │
│   ├── styles/app.css           ← @import "tailwindcss" + @theme (tokens)
│   │
│   ├── layouts/MainLayout.ark   ← navbar auth-aware + slot + ToastContainer + ConfirmDialog
│   │
│   ├── pages/
│   │   ├── index.ark            ← Skeleton + EmptyState + SEO meta
│   │   ├── login.ark            ← FormData + AuthStore + SEO + validación
│   │   └── register.ark         ← FormData + AuthStore + SEO + validación
│   │
│   ├── components/ui/           ← 52 primitivos canónicos (Button, Input, Card, etc.)
│   ├── stores/
│   │   ├── auth.store.ts        ← useAuth: user, token, loading, isAuthenticated
│   │   ├── toast.store.ts       ← useToast: success, error, warning, info
│   │   ├── confirm.store.ts     ← useConfirm: open, resolve
│   │   └── i18n.store.ts        ← useI18n: t(), setLocale(), addTranslations()
│   │
│   ├── services/auth.service.ts ← createService: login, register, getMe, logout
│   ├── types/index.ts           ← DTOs: User, ApiEnvelope, PaginatedResponse
│   │
│   ├── composables/
│   │   ├── useFetch.ts          ← useFetch(loader): data, loading, error, refetch
│   │   ├── useConfirm.ts        ← useConfirm(): { confirm(opts) => Promise<bool> }
│   │   └── useRequireAuth.ts    ← useRequireAuth(opts): redirect si no autenticado
│   │
│   └── shared/
│       ├── icons.ts             ← Lucide subset (check, chevron, x, etc.)
│       └── validation.ts        ← createSchema, required, email, minLength, etc.
```

### src/main.ts — entry point

```typescript
import './styles/app.css'
import { mount, createRouter } from 'arckode-ui'
import App from './App.ark'

createRouter([
  { path: '/',         filePath: 'src/pages/index.ark',    params: [], isLayout: false, isError: false },
  { path: '/login',    filePath: 'src/pages/login.ark',    params: [], isLayout: false, isError: false },
  { path: '/register', filePath: 'src/pages/register.ark', params: [], isLayout: false, isError: false },
])

mount(App, '#app')
```

### src/App.ark — componente raíz

```html
<template><MainLayout /></template>

<script lang="ts">
import { defineComponent, onMount } from 'arckode-ui'
import MainLayout from './layouts/MainLayout.ark'
import { useAuth } from './stores/auth.store'

export default defineComponent({
  name: 'App',
  setup() {
    const auth = useAuth()
    onMount(() => { auth.actions.checkAuth?.() })
    return {}
  },
})
</script>
```

### src/layouts/MainLayout.ark — navbar auth-aware

```html
<template>
  <div class="min-h-dvh flex flex-col bg-canvas text-ink">
    <nav class="flex items-center justify-between px-6 py-3 border-b border-line bg-surface">
      <Button variant="ghost" size="sm" :onClick="actions.goHome">App</Button>
      <div v-if="computed.isAuthed" class="flex items-center gap-4">
        <span class="text-sm text-ink-secondary">{{ state.user?.nombre ?? state.user?.name }}</span>
        <Button variant="ghost" size="sm" :onClick="actions.logout">Salir</Button>
      </div>
      <div v-else class="flex items-center gap-4">
        <Button variant="ghost" size="sm" :onClick="actions.goLogin">Ingresar</Button>
        <Button variant="primary" size="sm" :onClick="actions.goRegister">Crear cuenta</Button>
      </div>
    </nav>
    <main class="flex-1"><slot /></main>
    <ToastContainer /><ConfirmDialog />
  </div>
</template>

<script lang="ts">
import { defineComponent, computed } from 'arckode-ui'
import { navigateTo } from 'arckode-ui'
import { useAuth } from '../stores/auth.store'

export default defineComponent({
  name: 'MainLayout',
  setup() {
    const auth = useAuth()
    const isAuthed = computed(() => !!auth.state.token.value && !!auth.state.user.value)

    function goHome() { navigateTo('/') }
    function goLogin() { navigateTo('/login') }
    function goRegister() { navigateTo('/register') }
    function logout() { auth.actions.logout(); navigateTo('/') }

    return {
      state: { user: auth.state.user },
      computed: { isAuthed },
      actions: { goHome, goLogin, goRegister, logout },
    }
  },
})
</script>
```

### auth.service.ts — HTTP client

```typescript
import { createService } from 'arckode-ui'

export interface LoginPayload { email: string; password: string }
export interface RegisterPayload { nombre: string; email: string; password: string }
export interface AuthUser { id: string; email: string; nombre: string; rol?: string }
export interface LoginResponse { token: string; user: AuthUser }

export const AuthService = createService({
  baseUrl: '/api/auth', timeout: 5000,
  async login(data: LoginPayload): Promise<LoginResponse> {
    return this.post<LoginResponse>('/login', { body: data })
  },
  async register(data: RegisterPayload): Promise<LoginResponse> {
    return this.post<LoginResponse>('/register', { body: data })
  },
  async getMe(): Promise<AuthUser> { return this.get<AuthUser>('/me') },
  async logout(): Promise<void> { await this.post('/logout', {}) },
})
```

### guards.ts — route protection

```typescript
import { useAuth } from './stores/auth.store'

export function protectPage(requiredRoles?: string[]): boolean {
  const auth = useAuth()
  if (!auth.state.token.value || !auth.state.user.value) return false
  if (requiredRoles && !requiredRoles.includes(auth.state.user.value.rol ?? '')) return false
  return true
}

export function requireAuth(): boolean { return protectPage() }
```

### types/index.ts — DTOs compartidos

```typescript
export interface User { id: string; email: string; nombre: string; rol?: string; telefono?: string }
export interface ApiEnvelope<T> { success: boolean; data: T; message?: string }
export interface PaginationMeta { page: number; limit: number; total: number; totalPages: number; hasNext: boolean }
export interface PaginatedResponse<T> { data: T[]; pagination: PaginationMeta }
```

### login.ark / register.ark — formularios con FormData

```html
<template>
  <div class="flex min-h-full items-center justify-center px-4 py-12">
    <div class="w-full max-w-sm space-y-6">
      <div class="text-center">
        <h1 class="text-2xl font-display font-bold text-ink">Iniciar Sesión</h1>
        <p class="mt-2 text-sm text-ink-secondary">Ingresá tus credenciales</p>
      </div>
      <form @submit.prevent="actions.submit" class="space-y-4">
        <Input name="email" type="email" placeholder="tu@email.com" label="Email" />
        <Input name="password" type="password" placeholder="••••••••" label="Contraseña" />
        <Button htmlType="submit" :loading="state.loading">Ingresar</Button>
      </form>
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent, signal, useSeoMeta } from 'arckode-ui'
import { navigateTo } from 'arckode-ui'
import { useAuth } from '../stores/auth.store'
import { useToast } from '../stores/toast.store'

export default defineComponent({
  name: 'LoginPage',
  setup() {
    useSeoMeta({ title: 'Iniciar Sesión | App', description: 'Iniciá sesión', lang: 'es' })
    const auth = useAuth(); const toast = useToast()
    const loading = signal(false); const error = signal<string | null>(null)

    async function submit(e: Event) {
      const form = new FormData(e.target as HTMLFormElement)
      const email = form.get('email') as string; const password = form.get('password') as string
      if (!email || !password) { error.value = 'Completá todos los campos'; return }
      loading.value = true; error.value = null
      try {
        await auth.actions.login?.({ email, password })
        toast.actions.success?.('Bienvenido'); navigateTo('/')
      } catch { error.value = 'Credenciales inválidas' }
      finally { loading.value = false }
    }

    return {
      state: { loading, error },
      computed: {},
      actions: { submit, goRegister: () => navigateTo('/register') },
    }
  },
})
</script>
```

### index.ark — página de inicio con Skeleton + EmptyState

```html
<template>
  <div class="max-w-4xl mx-auto p-6">
    <Skeleton v-if="state.loading" variant="text" />
    <Skeleton v-if="state.loading" variant="text" />
    <template v-if="!state.loading">
      <EmptyState v-if="computed.isEmpty" title="Bienvenido"
        description="Arrancá editando src/pages/index.ark" />
      <div v-if="!computed.isEmpty" class="grid gap-4">
        <Card v-for="item in state.items" :key="item.id" padding="md">
          <h3 class="font-medium text-ink">{{ item.nombre }}</h3>
        </Card>
      </div>
    </template>
  </div>
</template>
```

---

## WORKFLOW — cómo la IA debe trabajar

### Protocolo obligatorio (cada vez que generás o modificás código)

```
PASO 1 — LEER contexto del proyecto
  Leer arckode.json          ← reglas exactas del proyecto (naming, layers, forbidden APIs)
  Leer UI.ref                ← API surface de todos los componentes disponibles
  Leer STORES.ref            ← stores existentes con state/getters/actions
  Leer SERVICES.ref          ← servicios existentes con métodos
  Leer ROUTES.ref            ← rutas + params existentes

PASO 2 — USAR primitivos del framework
  NO inventes HTML nativo. Todo tiene un primitivo: <Button>, <Input>, <Card>, etc.
  Si no sabés la API de un primitivo, leé UI.ref.

PASO 3 — GENERAR código con la estructura canónica
  Plantilla exacta: <template> → <script lang="ts"> → export default defineComponent
  NO <script setup> — usar defineComponent() con return { state, computed, actions }

PASO 4 — VERIFICAR antes de declarar "listo"
  npx ark analyze   ← 0 errors o no está listo
  npx ark fix       ← aplicar fixes automáticos si hay violations
  bun run build     ← debe compilar sin errores
```

### Reglas que la IA NO debe romper

| # | Regla | Explicación |
|---|-------|-------------|
| 1 | NUNCA `<script setup>` | Siempre `defineComponent()` con return explícito |
| 2 | NUNCA `ref()` / `reactive()` / `v-model` | Siempre `signal()` / `computed()` / `:value` + `:onChange` |
| 3 | NUNCA `fetch()` en componentes | Siempre `createService()` en `services/` |
| 4 | NUNCA `href="/..."` | Siempre `navigateTo('/path')` en un action |
| 5 | NUNCA HTML nativo si hay primitivo | `<button>` → `<Button>`, `<input>` → `<Input>`, `<table>` → `<DataTable>` |
| 6 | NUNCA `Cargando...` como texto | `<Skeleton />` o `<Spinner />` |
| 7 | NUNCA `No hay datos` como texto | `<EmptyState title="..." />` |
| 8 | NUNCA `@click="fn(arg)"` curried en DOM | Se ignora el retorno. Usar function directa o `:onClick` a Child |
| 9 | NUNCA crear primitivos UI que ya existen | Leer UI.ref primero |
| 10 | NUNCA `state.X` sin `.value` en script | Template: `state.X` (Proxy). Script: `state.X.value` |
| 11 | NUNCA `props.X.value` | Props NO son signals. Usar `(props.X as T) ?? default` |
| 12 | NUNCA `createRouter()` después de `mount()` | `createRouter()` DEBE ejecutarse antes de `mount()` |

---

## BUENAS PRÁCTICAS vs MALAS PRÁCTICAS

### Componentes y Template

| ✅ Buenas prácticas | ❌ Malas prácticas |
|---------------------|-------------------|
| `<Button variant="primary" :onClick="actions.submit">Guardar</Button>` | `<button class="bg-blue-500 text-white px-4 py-2 rounded" @click="submit">Guardar</button>` |
| `<Input :value="state.email" :onChange="actions.setEmail" :error="state.errors.email" />` | `<input v-model="email" class="border p-2 rounded" />` |
| `<Card padding="md"><h3>Título</h3><p>Contenido</p></Card>` | `<div class="border rounded-lg shadow-sm p-4"><h3>Título</h3><p>Contenido</p></div>` |
| `<Select :value="state.rol" :options="roles" :onChange="actions.setRol" />` | `<select v-model="rol"><option v-for="r in roles">{{ r }}</option></select>` |
| `<DataTable :rows="state.users" :columns="columns" />` | `<table><thead><tr><th>Nombre</th></tr></thead><tbody><tr v-for="u in users"><td>{{ u.name }}</td></tr></tbody></table>` |
| `<Skeleton variant="text" />` / `<Spinner />` | `<div>Cargando...</div>` |
| `<EmptyState title="Sin resultados" description="No hay datos que mostrar" />` | `<div>No hay datos</div>` |
| `<Tabs :items="tabs" :activeKey :onChange />` | `v-if="state.tab === 'x'"` con 3+ bloques |
| `<Pagination :currentPage :totalPages />` | `totalPages` + `v-for` manual de páginas |
| `<Tooltip content="Ayuda"><Icon name="info" /></Tooltip>` | `@mouseenter` + `@mouseleave` + div absoluto |
| `<Dialog :open="state.open" :onClose="actions.close" />` | `fixed inset-0 z-50` + fondo + modal manual |
| `<DropdownMenu :items :trigger />` | toggle + `v-if state.menuOpen` + posicionamiento manual |
| `<Stepper :steps :currentStep />` | `currentStep` + `nextStep()` + `v-if` manual |
| `<Badge variant="success">Activo</Badge>` | `<span class="rounded-full text-xs px-2 py-0.5 bg-green-100 text-green-800">Activo</span>` |
| `<Avatar :src :alt :size />` | `<img class="rounded-full w-10 h-10 object-cover" :src="url" />` |
| `<ProgressBar :value="75" :max="100" />` | `<progress :value="75" max="100"></progress>` |
| `<Separator />` | `<hr class="my-4 border-gray-200" />` |
| `<Accordion :items />` | `<details><summary>Título</summary>Contenido</details>` |

### Estado y Reactividad

| ✅ Buenas prácticas | ❌ Malas prácticas |
|---------------------|-------------------|
| `const count = signal(0)` | `const count = ref(0)` |
| `const total = computed(() => items.value.length)` | `const total = computed(() => items.length)` (sin .value) |
| `state.count.value++` (en script) | `state.count++` (en script — el Proxy no es reactivo en escritura) |
| `{{ state.count }}` (en template) | `{{ state.count.value }}` (en template — Proxy ya unwrap) |
| `props.name` / `(props.name as string) ?? default` | `props.name.value` (crashea si no se pasa la prop) |
| `defineForm({ schema, submit })` para forms complejos | 3+ signals + validación manual + try/catch manual |
| `batch(() => { a.value = 1; b.value = 2 })` para mutaciones múltiples | Mutar 3+ signals sin batch (múltiples re-renders) |

### Stores y Servicios

| ✅ Buenas prácticas | ❌ Malas prácticas |
|---------------------|-------------------|
| `useToast()` + `toast.actions.success('OK')` | `signal()` para toasts + `<div>` flotante manual |
| `useConfirm()` + `confirm({ title, message })` | `confirm()` nativo del browser |
| `useAuth()` + `auth.computed.isAuthenticated` | signal manual para sesión + localStorage directo |
| `createService()` en `services/` | `fetch()` directo en el componente |
| `defineStore()` con state/getters/actions | signals sueltas + imports circulares |
| `navigateTo('/path')` | `window.location.href = '/path'` o `href="#/path"` |
| `protectPage(['admin'])` en guard | `v-if="user.rol === 'admin'"` en cada componente |
| `createService({ baseUrl })` con timeout | fetch sin timeout ni error handling |

### SEO y Meta

| ✅ Buenas prácticas | ❌ Malas prácticas |
|---------------------|-------------------|
| `useSeoMeta({ title, description, lang })` en cada página | Página sin `<title>` ni `<meta description>` |
| `updateSeo(path)` en navegación SPA | Solo meta tags en index.html (no cambian por ruta) |
| SSG + backend catch-all para SEO real | Solo SPA + confiar en que Google ejecute JS |

### Routing y Navegación

| ✅ Buenas prácticas | ❌ Malas prácticas |
|---------------------|-------------------|
| `navigateTo('/dashboard')` | `href="#/dashboard"` (no funciona con History API) |
| `createRouter(routes)` ANTES de `mount(App)` | `createRouter()` después de `mount()` |
| Rutas en `main.ts` con filePath para analyzer | Rutas sin registrar en createRouter |
| `<a @click="actions.go" class="cursor-pointer">` | `<a href="/page">` (recarga la página) |

### Estructura de Archivos

| ✅ Buenas prácticas | ❌ Malas prácticas |
|---------------------|-------------------|
| `components/ui/Button.ark` — PascalCase | `components/ui/button.ark` — kebab-case para UI |
| `stores/auth.store.ts` — camelCase.store | `stores/AuthStore.ts` — sin suffijo .store |
| `services/user.service.ts` — PascalCase.service | `services/userService.ts` — sin suffijo .service |
| `composables/useFetch.ts` — useXxx | `composables/fetch.ts` — sin prefijo use |
| `pages/product-list.ark` — kebab-case | `pages/ProductList.ark` — PascalCase para pages |
| `components/features/` — componentes de dominio | Todo mezclado en components/ sin subdirectorios |

### Async y Error Handling

| ✅ Buenas prácticas | ❌ Malas prácticas |
|---------------------|-------------------|
| `try { await service.call() } catch { toast.error() }` | `await service.call()` sin try/catch |
| `<Button :loading="state.loading" :onClick="actions.submit">` | Loading manual + disabled manual en submit |
| `useFetch(loader)` con `refetch()` en onMount | Fetch en setup() sin estado de carga |
| `onError(err => { log(err); return true })` | Errores silenciosos ignorados |

---

## ARCKODE.JSON — manifiesto del proyecto

Cada proyecto arckode-ui tiene `arckode.json` en la raíz. Es la **fuente única de verdad** que AI, analyzer y CI leen.

```json
{
  "version": "1",
  "project": { "name": "mi-app", "description": "" },
  "layers": {
    "ui": "src/components/ui/*.ark",
    "features": "src/components/features/*.ark",
    "pages": "src/pages/**/*.ark",
    "stores": "src/stores/*.store.ts",
    "services": "src/services/*.service.ts",
    "composables": "src/composables/*.ts"
  },
  "conventions": {
    "naming": {
      "components": "PascalCase.ark",
      "stores": "camelCase.store.ts",
      "services": "PascalCase.service.ts",
      "composables": "useXxx.ts",
      "pages": "kebab-case.ark"
    },
    "maxComponentLines": 250,
    "maxProps": 10,
    "maxNesting": 8,
    "maxCyclomatic": 10
  },
  "forbidden": ["v-model", "ref()", "reactive()", "provide()", "inject()"],
  "required": {
    "asyncButtons": true,
    "errorHandling": true,
    "imgAlt": true,
    "emitDeclarations": true
  }
}
```

**Para la AI**: leer `arckode.json` ANTES de generar código. Las reglas ahí son obligatorias.

## COMPONENTE — estructura canónica

```html
<template>
  {{ state.count }}  {{ computed.label }}  {{ props.name }}
  <button @click="actions.increment">+</button>
  <Input :value="state.text" :onChange="actions.onInput" />
  <div v-if="state.visible">…</div>
  <li v-for="item in state.items" :key="item.id">…</li>
  <Child :prop="state.x" :onClick="actions.handle" />
</template>

<script lang="ts">
import { defineComponent, signal, computed, onMount } from 'arckode-ui'
export default defineComponent({
  name: 'MyComponent',                        // PascalCase
  props: { name: { type: String, required: true }, count: { type: Number, default: 0 } },
  emits: ['item-saved'],                      // kebab-case
  setup(props, { emit }) {
    const count = signal(0)
    const label = computed(() => `Total: ${count.value}`)
    onMount(() => {})
    function increment() { count.value++ }    // function decl, NO arrow en return
    return { state: { count }, computed: { label }, actions: { increment } }
  },
})
</script>
```

## VALIDACIÓN OBLIGATORIA

```bash
ark analyze          # violations → error. Ejecutar siempre antes de "listo".
ark fix              # aplicar fixes automáticos
ark guard            # pre-commit: solo archivos staged
ark ci               # CI gate completo
```

**Para AI**: después de generar/modificar código, correr `ark analyze`. Si hay violations, leer `fix:` del output y aplicarlo literal.

## STORE / SERVICE / ROUTER

```typescript
// store — defineStore
export const useCartStore = defineStore('cart', {
  state:   () => ({ items: signal<Item[]>([]) }),
  getters: { total: computed(() => items.value.reduce((s,x)=>s+x.price,0)) },
  actions: { add(i: Item) { items.value = [...items.value, i] } },
  persist: { key: 'cart', storage: 'localStorage' },
})
```
```typescript
// service — createService (NUNCA fetch directo)
export const ProductService = createService(
  { baseUrl: '/api/products', timeout: 5000 },
  { async getAll() { return this.get<Product[]>('/') } },
)
```
```
// router
src/pages/index.ark             → /
src/pages/users/[id].ark        → /users/:id
src/pages/_layout.ark           → layout con <slot/>
navigateTo('/dashboard')         // 99% de los casos
```

## CLI REFERENCE

```bash
ark new <name>              # proyecto nuevo
ark g c|p|s|sv|l <name>     # generate: component|page|store|service|layout
ark analyze [--json]        # corre analyzer
ark fix [file] [--all]      # aplica fixes automáticos
ark routes / schema / theme init / ssg / stubs
ark agent list              # lista recipes (login, crud, ...)
ark agent run <recipe>      # ejecuta recipe completa
```

## SUB-SKILLS (cargar SOLO si necesitás)

```
node_modules/arckode-ui/skills/
  components/SKILL.md   ← 18 patrones canónicos (modal, form, async, slots)
  analyzer/SKILL.md     ← 27 reglas + troubleshooting + agregar regla
  runtime/SKILL.md      ← signals, lifecycle, renderer, reconciliation
  compiler/SKILL.md     ← directivas, scoped CSS, vite plugin
  testing/SKILL.md      ← vitest + happy-dom
  cli/SKILL.md          ← extender CLI
  ai-doc-style/SKILL.md ← replicar AIREF en otro proyecto
```

| Tarea | Cargar |
|---|---|
| Escribir componente / store / service básico | solo este AIREF |
| Patrón específico (form, async, modal) | + `components/SKILL.md` |
| Error del analyzer / troubleshooting | + `analyzer/SKILL.md` |
| Optimizar signals / lifecycle profundo | + `runtime/SKILL.md` |
| Agregar directiva / debuggear scoped CSS | + `compiler/SKILL.md` |
| Escribir tests | + `testing/SKILL.md` |
| Agregar comando al CLI | + `cli/SKILL.md` |

## Para más detalle

- API completa: `node_modules/arckode-ui/dist/index.d.ts`
- Catálogo UI dinámico: `UI.ref` (auto-generado por `ark stubs`)
- Lista de reglas con fixes: `npx ark analyze`
