# air
Arquitectura y diseño de la capa visual para `src/uix/air`.
---
## 1. Propósito y relación con terra
`air` no es `terra` con CSS encima. Es la capa visual del sistema: define tokens, aplica estilos y expone una API declarativa para el consumidor. `terra` resuelve comportamiento y accesibilidad. `air` resuelve presentación.
La relación entre ambas capas es asimétrica y deliberada:
- `air` conoce `terra`: consume su anatomía, sus `data-*` attrs y sus estados.
- `terra` no conoce `air`: no importa nada de `air`, no depende de ningún token visual.
> **Regla central.** Si algo en `air` necesita que `terra` cambie su comportamiento, hay un error de diseño. La dependencia solo fluye en una dirección.
---
## 2. Sistema de tokens en tres capas
El error más común en design systems es tener un solo nivel de tokens donde todo es una variable CSS directa. `air` usa tres capas con responsabilidades distintas.
### 2.1 Primitive tokens
Valores absolutos. Son la escala base del sistema. Ningún componente los usa directamente.
```css
/* tokens/primitive.css */
:root {
--blue-1: #eff6ff;
--blue-9: #6e56cf;
--blue-12: #1d1930;
--gray-1: #fcfcfd;
--gray-6: #d9dce3;
--gray-7: #c9ced6;
--gray-12: #111827;
--space-1: 4px;
--space-2: 8px;
--space-4: 16px;
--radius-1: 4px;
--radius-2: 6px;
--radius-3: 8px;
}
```
### 2.2 Semantic tokens
Apuntan a primitive tokens y cambian según el tema activo. Son la única capa que conoce el concepto de tema. Los componentes pueden usarlos, pero se prefiere que usen component tokens.
```css
/* tokens/semantic.css */
:root,
[data-theme='light'] {
--color-accent: var(--blue-9);
--color-accent-muted: var(--blue-3);
--color-surface: var(--gray-1);
--color-text-primary: var(--gray-12);
--color-border: var(--gray-6);
--radius-md: var(--radius-2);
}
[data-theme='dark'] {
--color-accent: var(--blue-10);
--color-surface: var(--gray-2);
--color-text-primary: var(--gray-1);
--color-border: var(--gray-7);
}
```
### 2.3 Component tokens
Scoped a cada componente. Son el único nivel que el CSS de un componente debe escribir. Permiten override quirúrgico sin tocar la escala global.
```css
/* tokens/component/button.css */
.btn {
height: var(--btn-height);
padding-inline: var(--btn-px);
font-size: var(--btn-font-size);
background: var(--btn-bg);
color: var(--btn-fg);
border: 1px solid var(--btn-border);
border-radius: var(--btn-radius, var(--radius-md));
}
```
> **Regla de componentes.** El CSS de un componente de `air` solo escribe `var(--btn-*)`, `var(--input-*)`, etc. Nunca toca `--blue-9` ni tokens primitivos directamente.
---
## 3. El contrato con terra: data-* como API pública
`terra` genera `data-*` attrs en todos sus estados. Esa es exactamente la superficie que `air` necesita consumir. El CSS de `air` no necesita nada más para responder a estados del componente.
### 3.1 data-* attrs de contrato previstos en terra
| Atributo | Cuándo aparece | Uso típico en air |
|---|---|---|
| `data-disabled` | Componente deshabilitado | `opacity: .5; pointer-events: none` |
| `data-invalid` | Valor no válido | `color: var(--color-error)` |
| `data-open` / `data-closed` | Overlay visible o no | `display: block / none` |
| `data-highlighted` | Item bajo cursor o foco | `background: var(--color-accent-muted)` |
| `data-selected` | Item seleccionado | `font-weight: 500` |
| `data-placement` | `top \| bottom \| left \| right` | `transform` para animar entrada |
| `data-orientation` | `horizontal \| vertical` | `flex-direction` del contenedor |
### 3.2 Cómo air consume esos attrs
```css
/* air responde a estados sin saber cómo terra los produce */
[data-disabled] { opacity: .5; pointer-events: none; }
[data-invalid] { --input-border: var(--color-error); }
[data-open] { display: block; }
[data-highlighted] { background: var(--color-accent-muted); }
[data-selected] { font-weight: 500; }
/* posicionamiento de overlays */
[data-placement='top'] { transform-origin: bottom center; }
[data-placement='bottom'] { transform-origin: top center; }
```
> **Advertencia crítica.** Los `data-*` de `terra` son API pública con el mismo peso que los tipos TypeScript, pero deben formalizarse componente a componente. Si `terra` renombra `data-open` a `data-expanded`, el CSS de `air` rompe silenciosamente sin ningún error de compilación. Tratar ese contrato con el mismo rigor que las props tipadas.
---
## 4. Anatomía pública por componente
`air` no solo depende de `data-*` attrs: también depende de la anatomía de `terra`. Qué partes existen, cómo se llaman, cuáles son raíz y cuáles hijas.
Para cada componente que `air` estiliza, debe estar documentado:
- qué partes existen (`Root`, `Trigger`, `Content`, `Item`, etc.)
- qué `data-*` garantiza cada parte
- qué parte es el root visual (la que recibe los component tokens)
- qué partes pueden recibir snippets custom del consumidor
### 4.1 Ejemplo: anatomía de Input / Field
```
Field.Root ← crea contexto, recibe props globales
Field.Label ← [data-invalid], [data-disabled], [data-required]
Field.Input ← [data-invalid], [data-disabled], [data-readonly]
Field.Description ← ayuda contextual
Field.Error ← visible cuando [data-invalid] en el Root
```
Esta anatomía es contrato estable solo si `terra` la declara como pública. `air` asume que esas partes existen con esos nombres cuando están documentadas como soportadas. Si `terra` renombra `Field.Error` a `Field.Message`, `air` necesita actualizarse.
---
## 5. Variantes: desacoplando dimensiones
El problema de las variantes no es cuántas hay, sino cómo se combinan. Con múltiples dimensiones ortogonales (`variant × size × color × state`), el número de combinaciones explota si el CSS las trata como casos individuales.
La solución es diseñar los component tokens de forma que cada dimensión sea independiente:
- `size` define tokens de dimensión: `height`, `padding`, `font-size`
- `variant` define tokens de apariencia: `background`, `color`, `border`
- `state` aplica overrides transversales: `disabled`, `invalid`, `loading`
```css
/* size solo toca dimensiones */
[data-size='1'] { --btn-height: 28px; --btn-px: 10px; --btn-font-size: 12px; }
[data-size='2'] { --btn-height: 36px; --btn-px: 14px; --btn-font-size: 14px; }
[data-size='3'] { --btn-height: 44px; --btn-px: 18px; --btn-font-size: 16px; }
/* variant solo toca apariencia */
[data-variant='solid'] { --btn-bg: var(--color-accent); --btn-fg: white; --btn-border: transparent; }
[data-variant='outline'] { --btn-bg: transparent; --btn-fg: var(--color-accent); --btn-border: var(--color-accent); }
[data-variant='soft'] { --btn-bg: var(--color-accent-muted); --btn-fg: var(--color-accent-text); --btn-border: transparent; }
/* state no necesita conocer variant ni size */
[data-disabled] { opacity: .5; pointer-events: none; }
[data-loading] { --btn-fg: transparent; }
```
Esto escala a N variantes × M tamaños × K estados sin escribir combinaciones. Cada dimensión toca solo su propio conjunto de tokens.
---
## 6. Theming: el tema es un atributo DOM, no estado JS
El store de Svelte es solo el mecanismo para cambiar `data-theme` en el elemento raíz. El theming real vive en CSS. El store no contiene colores ni tokens: solo el nombre del tema activo.
```ts
// theme/index.ts
import { writable } from 'svelte/store';
export const theme = writable<'light' | 'dark'>('light');
export function setTheme(next: 'light' | 'dark') {
theme.set(next);
document.documentElement.setAttribute('data-theme', next);
localStorage.setItem('theme', next);
}
```
La ventaja de este enfoque es que el tema puede ser local a cualquier contenedor, no solo global. Poner `data-theme='dark'` en cualquier `div` crea un subtema sin JS adicional.
### 6.1 accentColor dinámico
Para paletas de acento personalizadas, el sistema puede derivar la escala desde un color base usando `color-mix()`:
```css
[data-accent='custom'] {
--accent-base: #e54d2e;
--color-accent: var(--accent-base);
--color-accent-muted: color-mix(in oklch, var(--accent-base) 15%, white);
--color-accent-text: color-mix(in oklch, var(--accent-base) 80%, black);
}
```
> **Nota de compatibilidad.** `color-mix()` con `oklch` tiene soporte amplio en navegadores modernos (Chrome 111+, Firefox 113+, Safari 16.2+). Para proyectos que requieran soporte más amplio, evaluar generación de escalas en build time como alternativa o complemento. No tomar este enfoque como dogma: es la dirección correcta para sistemas nuevos, pero necesita validación de requisitos antes de adoptarlo como base única.
---
## 7. CSS scoped de Svelte: la trampa más común
Los `
```
---
## 7.1 Contrato formal entre terra y air
Antes de estilizar un componente en `air`, debe quedar explícito qué parte del componente es pública para consumo visual y qué parte es incidental.
### Pública
- anatomía documentada (`Root`, `Trigger`, `Content`, `Item`, etc.)
- `data-*` attrs documentados para cada parte
- atributos de variante y tamaño que `air` puede añadir (`data-variant`, `data-size`, etc.)
### No pública
- clases generadas por Svelte
- orden incidental del DOM que no esté documentado
- wrappers internos añadidos por comodidad de implementación
- nombres de variables locales o detalles del State de `terra`
> **Regla operativa.** Si un selector de `air` necesita una clase interna o un wrapper no documentado de `terra`, el contrato está mal definido.
## 7.2 Qué no debe hacer air
Para mantener la separación entre capas, `air` no debe:
- cambiar comportamiento que pertenece a `terra`
- depender de clases internas o estructura incidental del DOM
- leer estado interno de clases `State`
- definir lógica de accesibilidad, foco o keyboard navigation
- introducir tokens visuales dentro de `terra`
`air` sí puede:
- consumir `data-*` attrs públicos
- añadir `data-variant`, `data-size` o attrs equivalentes de presentación
- mapear tokens semánticos a component tokens
- ofrecer wrappers finos y declarativos sobre componentes de `terra`
## 8. Estructura de carpetas
```
src/uix/air/
tokens/
primitive.css ← escala absoluta de colores, espacio, radio
semantic.css ← surface, accent, text, border por tema
component/
button.css ← define --btn-* tokens
input.css ← define --input-* tokens
field.css ← define --field-* tokens
select.css ← define --select-* tokens
index.css ← barrel: importa todos los tokens
theme/
index.ts ← store + setTheme() + setAccent()
components/
button/
button.svelte ← wrapper fino sobre terra.Button
button.css ← aplica tokens a la anatomía real de terra
input/
input.svelte
input.css ← aplica --input-* sobre data-* attrs públicos
field/
field.svelte ← wrapper de Field.Root
field.css ← aplica --field-* sobre data-* attrs públicos
exports.ts
index.ts
```
> **Diferencia importante.** `tokens/component/*.css` define qué tokens existen por componente. `components/*/*.css` aplica esos tokens a la anatomía concreta de `terra`. No deben mezclarse ambas responsabilidades en un mismo archivo por comodidad.
---
## 9. Estrategia de implementación incremental
No tokenizar todos los componentes desde el principio. Los component tokens son trabajo de mantenimiento real. Crear solo los que tienen evidencia de que el consumidor va a necesitar override.
### Fase 1: piloto con Input / Field
- definir `primitive.css` y `semantic.css` completos
- component tokens solo para `Field` y `Input`
- validar que el contrato con `terra` funciona via `data-*`
- validar theming anidado con `data-theme` en contenedor
### Fase 2: expandir con evidencia
- añadir component tokens por componente según necesidad real
- no adelantar tokens que nadie ha pedido sobrescribir
- documentar qué tokens son API pública en cada componente
---
## 10. Diferencias respecto a Radix Themes
| Aspecto | Radix Themes | air |
|---|---|---|
| Capas de tokens | 2 niveles (primitives + semánticos) | 3 niveles + component tokens scoped |
| Customización de componente | Sobrescribir clases internas | Override de `--component-*` tokens |
| Temas anidados | `className='dark'` en contenedor | `data-theme` en cualquier contenedor |
| Dependencia de JS | Context + estado para theming | Solo para cambiar atributo DOM |
| AccentColor personalizado | Redefinir escala entera (9+ vars) | Una variable base, escala derivada |
| Contrato con headless | Implícito en clases internas | Explícito via `data-*` como API pública |
| CSS scoped | N/A (React) | Global para tokens y `data-*` rules |
---
## 11. Checklist: componente Input / Field
Antes de considerar terminado cualquier componente de `air`, verificar cada punto.
### 11.1 Tokens
- [ ] `primitive.css` tiene la escala completa de colores, espacio y radio
- [ ] `semantic.css` mapea correctamente para light y dark
- [ ] `component/field.css` define `--field-gap`, `--field-label-size`, `--field-error-color`
- [ ] `component/input.css` define `--input-height`, `--input-px`, `--input-border`, `--input-bg`, `--input-radius`
- [ ] Ningún selector en `field.css` o `input.css` usa `--blue-9` ni tokens primitivos directamente
### 11.2 Contrato con terra
- [ ] El CSS de `air` responde a `[data-disabled]` en `Field.Root` y en `Field.Input`
- [ ] El CSS de `air` responde a `[data-invalid]` en `Field.Root` (cambia border e icono de error)
- [ ] `Field.Error` puede estilizarse o mostrarse a partir de `[data-invalid]` del root, sin asumir clases internas
- [ ] `Field.Label` refleja `[data-required]` si `terra` lo expone
- [ ] No hay ningún selector que dependa de clases generadas por Svelte scoped
### 11.3 Variantes y tamaños
- [ ] Los tokens de `size` solo tocan dimensiones: `height`, `padding`, `font-size`
- [ ] Los tokens de `variant` solo tocan apariencia: `bg`, `color`, `border`
- [ ] El estado `[data-disabled]` funciona igual en todas las variantes sin CSS específico por combinación
- [ ] El estado `[data-invalid]` funciona igual en todos los tamaños
### 11.4 Theming
- [ ] El componente cambia correctamente con `data-theme='dark'` en un contenedor padre
- [ ] No hay colores hardcodeados en el CSS del componente
- [ ] El tema anidado (dark dentro de light) funciona sin JS adicional
### 11.5 CSS global vs scoped
- [ ] Los archivos `.css` de tokens y variantes son importados como módulos globales
- [ ] Los selectores `data-*` no están dentro de `