# Plan: `42-components` — Librería headless de componentes en TypeScript

> Extracción de la lógica de los componentes de maildrill (accordion, modal, tooltip, dropdown,
> gallery, etc.) a una librería **headless** independiente, en TypeScript, con Storybook y
> adaptadores para Blade, React y Vue.

## Decisiones tomadas

| # | Decisión | Valor |
|---|----------|-------|
| 1 | Publicación | **npm público** |
| 2 | Estructura de paquetes core | **Un solo paquete `@42/core` con _subpath exports_** (tree-shaking por componente) |
| 3 | Lenguaje | TypeScript (estricto) |
| 4 | Estilos | Separados en `@42/styles` (CSS plano + custom properties, sin obligar a Tailwind) |
| 5 | Build | Vite library mode (multi-entry) + `vite-plugin-dts` |
| 6 | Storybook | `@storybook/html-vite` (renderer HTML/vanilla) + `addon-a11y` |
| 7 | Tests | Vitest + `@testing-library/dom` (+ `axe-core` para a11y) |
| 8 | Versionado | Changesets (semver) |
| 9 | Adaptadores | **Sí**: Blade, React y Vue (arquitectura reservada desde el inicio) |
| 10 | Gestor de paquetes | pnpm workspaces |

## Principio: ¿qué es "headless" aquí?

Al ser vanilla (no React), un componente headless es un **controlador (clase/factory) que se monta
sobre el DOM que aporta el consumidor** (mejora progresiva). El controlador se encarga de:

- **Estado** (abierto/cerrado, índice activo, selección…).
- **Accesibilidad** (roles ARIA, `aria-expanded`, `aria-controls`, focus management, focus trap).
- **Teclado** (flechas, Home/End, Esc, Tab).
- **Eventos** (`CustomEvent` tipados: `accordion:change`, `modal:open`…).
- **Reflejo de estado en `data-*`** (`data-state="open|closed"`, `data-disabled`) para que el CSS reaccione.

Lo que **NO** hace: imponer estilos. El look vive en `@42/styles`. Esto reemplaza el acoplamiento
actual a Alpine (`Alpine.data`, `Alpine.store`, estado del padre como en `accordion.blade.php`).

Contrato de hooks por DOM (ejemplo accordion):

```html
<div data-acc>
  <button data-acc-trigger aria-controls="p1">Título</button>
  <div data-acc-panel id="p1">contenido</div>
</div>
```

```ts
import { Accordion } from '@42/core/accordion';
const acc = new Accordion(el, { multiple: false });
acc.on('change', (e) => { /* ... */ });
```

## Arquitectura del monorepo

```
42-components/
│
├── packages/
│   ├── core/                      # @42/core — controladores headless (framework-agnostic)
│   │   ├── accordion/
│   │   │   ├── accordion.ts        # clase controladora: estado, ARIA, teclado, eventos
│   │   │   ├── accordion.types.ts  # tipos de opciones y eventos
│   │   │   ├── accordion.css        # CSS funcional mínimo (sin tema)
│   │   │   ├── accordion.test.ts
│   │   │   └── index.ts
│   │   ├── modal/
│   │   ├── tooltip/
│   │   ├── dropdown/
│   │   ├── shared/                 # utilidades: focus-trap, keyboard, dom, event-emitter
│   │   └── index.ts
│   │
│   ├── styles/                     # @42/styles — tema opcional (CSS + custom properties)
│   │
│   ├── react/                      # @42/react — wrappers idiomáticos React
│   │   └── accordion/index.tsx
│   │
│   ├── vue/                        # @42/vue — composables/components Vue
│   │   └── accordion/index.ts
│   │
│   └── blade/                      # @42/blade — bootstrap JS (auto-discovery de data-* y montaje)
│       └── auto.ts
│
│   # nota: blade-php/ (paquete Composer con los componentes <x-...>) también vive en el monorepo
│
├── stories/
│   ├── Accordion.stories.ts
│   ├── Modal.stories.ts
│   └── ...
│
├── .storybook/
├── docs/
├── package.json
├── pnpm-workspace.yaml
├── tsconfig.json
└── vite.config.ts
```

### Paquetes publicables en npm

- `@42/core` — controladores headless (con subpath exports por componente).
- `@42/styles` — tema opcional.
- `@42/react` — adaptador React.
- `@42/vue` — adaptador Vue.
- `@42/blade` — bootstrap JS para auto-montaje (la parte PHP/Blade se distribuye vía Composer, ver §8).

## Anatomía estándar de un componente (plantilla repetible)

Cada `packages/core/<name>/` contiene exactamente:

| Archivo | Responsabilidad |
|---|---|
| `<name>.ts` | Clase controladora: estado, ARIA, teclado, eventos. Cero estilos. |
| `<name>.types.ts` | Tipos de opciones y eventos (export público). |
| `<name>.css` | CSS funcional mínimo (p. ej. `[data-state=closed]{display:none}`), sin tema. |
| `<name>.test.ts` | Vitest + testing-library: estado, teclado, ARIA, eventos. |
| `index.ts` | Re-export público del componente. |

Definir esta plantilla **una sola vez** y replicarla es lo que hace el proyecto escalable.

## Estrategia de adaptadores

El core es la **única fuente de verdad**. Los adaptadores son glue fino que monta el controlador
en el ciclo de vida del framework. Para que escale, se usa un **binding genérico** y no glue
manual por componente:

- **React** (`@42/react`): `createReactComponent(Controller)` → componente que crea refs, instancia
  el controlador en `useLayoutEffect`, limpia en unmount, mapea eventos del controlador a callbacks
  (`onChange`) y soporta modo controlado/no controlado.
- **Vue** (`@42/vue`): `useController(Controller)` (composable) → instancia en `onMounted`, limpia en
  `onUnmounted`, sincroniza props con `watch`. Opcionalmente componentes SFC envolventes.
- **Blade**: dos piezas —
  1. **Componentes Blade** (`<x-acc>…`) que renderizan el markup con los `data-*` correctos.
     Se distribuyen como **paquete Composer** (repo o path separado), no por npm.
  2. **Bootstrap JS** (`@42/blade`): auto-descubre elementos `data-*` en el DOM y monta los
     controladores de `@42/core`. Es además el camino de **reintegración en maildrill** (retirar
     Alpine componente a componente).

**Orden de construcción de adaptadores:** React primero (junto al `accordion` de referencia, como
prueba de que la API del core es envolvible), luego Vue, luego Blade.

## Anatomía estándar de un adaptador (plantilla repetible)

Igual que el core, cada adaptador se define **una sola vez** (binding genérico) y luego cada
componente es glue de 3–5 líneas. Lo que hace esto posible es que el core respete un **contrato
mínimo de controlador**.

### Contrato mínimo del controlador (lo que el core debe exponer)

Para que un controlador sea envolvible por cualquier adaptador sin glue manual, su API pública debe
cumplir:

| Miembro | Firma | Uso del adaptador |
|---|---|---|
| `constructor` | `(el: HTMLElement, options?: O)` | montaje sobre el DOM del consumidor |
| `update` | `(options: Partial<O>): void` | sincronizar props reactivas (React rerender / Vue `watch`) |
| `destroy` | `(): void` (idempotente) | cleanup en unmount |
| eventos | `on(name, cb)` / `off(name, cb)` (o `EventTarget`) | mapear a callbacks (`onChange`) |
| `getState` | `(): S` | soporte de modo controlado (opcional) |

Este contrato vive en `@42/core/shared` como `interface Controller<O, S, E>` y todos los
componentes lo implementan. **Si un componente no puede cumplirlo, el problema está en el core, no
en el adaptador.**

### `@42/react` — estructura y binding

```
packages/react/
├── src/
│   ├── create-react-component.tsx   # binding genérico (UNA vez)
│   ├── accordion/index.tsx          # glue de 3-5 líneas
│   └── index.ts
├── package.json                     # peerDependencies: react, react-dom >=19
└── tsconfig.json
```

```tsx
// create-react-component.tsx (binding genérico)
export function createReactComponent<O, S, E extends Record<string, unknown>>(
  Controller: new (el: HTMLElement, options?: O) => Controller<O, S, E>,
  config: { events?: Partial<Record<keyof E, string>>; displayName?: string },
): React.ForwardRefExoticComponent<O & EventProps<E> & { children?: React.ReactNode }> {
  // crea ref, instancia en useLayoutEffect, llama update() al cambiar props,
  // mapea eventos del controlador a callbacks, destroy() en cleanup.
}
```

```tsx
// accordion/index.tsx (glue por componente)
import { Accordion as AccordionController } from '@42/core/accordion';
export const Accordion = createReactComponent(AccordionController, {
  events: { change: 'onChange' },
  displayName: 'Accordion',
});
```

### `@42/vue` — estructura y binding

```
packages/vue/
├── src/
│   ├── use-controller.ts            # composable genérico (UNA vez)
│   ├── accordion/index.ts           # glue por componente
│   └── index.ts
├── package.json                     # peerDependencies: vue >=3
└── tsconfig.json
```

```ts
// use-controller.ts (binding genérico)
export function useController<O, S, E>(
  Controller: new (el: HTMLElement, options?: O) => Controller<O, S, E>,
  elRef: Ref<HTMLElement | null>,
  options: MaybeRefOrGetter<O>,
  handlers?: Partial<Record<keyof E, (payload: unknown) => void>>,
): { instance: Ref<Controller<O, S, E> | null> } {
  // onMounted -> new Controller(el, opts); watch(options) -> instance.update();
  // onUnmounted -> instance.destroy().
}
```

### `@42/blade` — auto-montaje

```
packages/blade/
├── src/
│   ├── registry.ts                  # mapa data-attr -> Controller
│   ├── auto.ts                      # mount(): querySelectorAll + MutationObserver
│   └── index.ts
└── package.json                     # dependency: @42/core
```

```ts
// uso en maildrill / cualquier app server-rendered
import { mount } from '@42/blade';
mount(); // descubre [data-acc], [data-c42-modal]... y monta los controladores de @42/core
```

El `MutationObserver` cubre DOM inyectado por Livewire (montaje/desmontaje en caliente).

### Plantilla de tests por adaptador

Cada adaptador prueba el **binding**, no la lógica del componente (eso ya lo cubre el core):

| Caso | React (`@testing-library/react`) | Vue (`@testing-library/vue`) | Blade (`@testing-library/dom`) |
|---|---|---|---|
| Montaje | instancia creada en mount | `onMounted` instancia | `mount()` monta sobre `data-*` |
| Sincronización | cambiar prop → `update()` | cambiar prop → `watch` → `update()` | n/a |
| Eventos | evento del core → callback (`onChange`) | evento → emit | `CustomEvent` despachado |
| Desmontaje | `destroy()` llamado en unmount | `onUnmounted` → `destroy()` | nodo removido → `destroy()` |

## Inventario y mapeo (maildrill actual → librería)

Origen: `resources/js/components/*`, `resources/js/modules/*`, `resources/views/components/*`.

**Fase A — presentacionales (solo `@42/styles`, lógica nula/trivial):**
`button`, `badge`, `card`, `divider`, `breadcrumb`, `input-label`, `indicator`/`status-indicator`.
Validan el pipeline (build + Storybook + theming) con bajo riesgo.

**Fase B — interactivos núcleo:**
- `accordion` ← `accordion.blade.php` (hoy depende de estado Alpine del padre → la clase lo internaliza).
- `tooltip` ← `components/tooltip.js` (reutiliza `@floating-ui/dom`).
- `modal`/`dialog` ← `modal.blade.php` + `jet-modal` (focus trap, Esc, scroll-lock, ARIA).
- `dropdown` ← `dropdown.blade.php` (+ floating-ui).
- `clipboard` ← `components/copy.js` (hoy `Alpine.store` → utilidad sin Alpine).
- `switch` ← `switch.blade.php` / `toggle-input`.

**Fase C — compuestos:**
- `combobox`/`multiselect` ← `components/multiselect*.js`.
- `tags-input` ← `components/input-tag.js`.
- `tree`/`nested-list` ← `components/nested-list.js`.
- `gallery` ← `gallery-admin` + `modal-gallery-preview` + `modules/gallery.js`.
- `phone-code`, `country-select` (como combobox especializados, a decidir).

**Fuera de alcance (no son UI reutilizable):** `modal-create-campaign`, `modal-add-subscribers`,
`datatable*`, `email-builder`, etc. — acoplados a Livewire/negocio.

## Storybook y docs

- Storybook con `@storybook/html-vite` (renderer HTML/vanilla).
- `stories/<Comp>.stories.ts`: renderiza el HTML del contrato + instancia el controlador en
  `play`/decorator, con `argTypes` para las opciones.
- Addons: `@storybook/addon-a11y` (crítico en headless), controls, interactions.
- `docs/`: guía de uso, contrato `data-*`, eventos, y guía de theming con `@42/styles`.

## Pruebas

- Por componente: estado, transiciones, atributos ARIA, navegación por teclado, emisión de eventos.
- `addon-a11y` + opcional `axe-core` en tests para regresiones de accesibilidad.
- Un componente no se da por terminado hasta que `<name>.test.ts` pasa.
- Adaptadores: tests de montaje/desmontaje y sincronización de props/eventos.

## Versionado, publicación y CI

- **Changesets** para semver de todos los paquetes publicables.
- **npm público** bajo el scope `@42` (verificar disponibilidad del scope/org en npm).
- CI (GitHub Actions): lint + test + build + publish (en tag) + deploy de Storybook estático.

## Reintegración en maildrill (fase final, opcional)

Los `*.blade.php` siguen aportando markup con `data-*`; `@42/blade` (bootstrap) monta los
controladores de `@42/core`, retirando Alpine componente a componente. Se hace **después** de tener
la librería estable.

## Fases ejecutables

1. **Scaffold**: monorepo pnpm, `core` + `styles`, Vite lib mode + dts, tsconfig estricto, ESLint/Prettier.
2. **Storybook + Vitest** operativos con un componente "hola mundo".
3. **Plantilla de componente** + `accordion` de referencia completo (ts + types + css + test + story).
4. **Adaptador React de referencia** sobre `accordion` (valida API agnóstica del core) + binding genérico.
5. **Fase A** (presentacionales) → valida pipeline end-to-end.
6. **Fase B** (interactivos núcleo).
7. **Fase C** (compuestos).
8. **Adaptadores Vue y Blade** (replicando el binding genérico).
9. **Changesets + CI + publicación npm + deploy de docs/Storybook**.
10. (Opcional) **Reintegración en maildrill**.

## Independencia de paquetes y dependencias (sin acoplamiento)

El monorepo es solo organización de desarrollo; lo publicado en npm son **paquetes independientes**.
El usuario nunca arrastra librerías que no necesita. Reglas que lo garantizan:

1. **Paquetes separados, no un mega-paquete.** Quien solo quiere vanilla instala `@42/core` y no
   trae nada de React ni Vue. Los adaptadores son instalaciones aparte y opcionales.
2. **`peerDependencies`, no `dependencies`, para los frameworks.** `@42/react` declara `react` como
   _peerDependency_ (usa el React de la app, no lo empaqueta ni duplica); `@42/vue` igual con `vue`.
   El único `dependency` real de un adaptador es `@42/core`.
3. **`@42/core` con cero dependencias de framework.** Sus dependencias son utilidades puntuales y
   solo donde se usan (p. ej. `@floating-ui/dom` en `tooltip`/`dropdown`). Con _subpath exports_ +
   tree-shaking, importar `@42/core/accordion` no trae `tooltip` ni floating-ui.
4. **Anti-patrón a evitar:** NO incluir adaptadores dentro de `@42/core`. Se mantienen como paquetes
   separados (`@42/react`, `@42/vue`, `@42/blade`).

Matriz de instalación:

| Usuario | Instala | Arrastra |
|---|---|---|
| Vanilla / Blade | `@42/core` (+ `@42/styles` opcional) | nada de React/Vue |
| React | `@42/react` + `@42/core` | usa el React del usuario (peer) |
| Vue | `@42/vue` + `@42/core` | usa el Vue del usuario (peer) |

## Preguntas abiertas / pendientes de confirmar

- ~~Disponibilidad del scope `@42` en npm.~~ **Confirmado: `@42` está disponible.**
- ~~¿El paquete Composer de Blade vive en este mismo monorepo o en repo separado?~~
  **Resuelto: dentro del monorepo** (p. ej. `packages/blade-php/`, paquete Composer).
- ~~Versiones objetivo de React y Vue.~~ **Resuelto: React 19 (`peerDependencies: react >=19`), Vue 3.**

_Todas las decisiones cerradas. Listo para ejecutar las Fases 1–4._
