You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/src/uix/air/air-arquitectura.md

16 KiB

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.

/* 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.

/* 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.

/* 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

/* 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
/* 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.

// 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():

[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 <style> scoped de Svelte no atraviesan el boundary de componente. Si air usa <style scoped>, los selectores basados en data-* no llegan a los elementos que renderiza terra.

La regla es clara:

  • tokens (primitive, semantic, component): CSS global
  • reglas basadas en data-*: CSS global
  • estilos verdaderamente locales al nodo del wrapper de air: scoped
<!-- components/input/input.svelte -->
<script>
  import './input.css';  // global: contiene data-* rules y component tokens
  let { variant = 'outline', size = '2', ...rest } = $props();
</script>

<!-- terra.Field.Input ya aplica los data-* attrs desde su State -->
<Field.Input data-variant={variant} data-size={size} {...rest} />

<style>
  /* solo estilos que no dependen de data-* ni de terra */
</style>

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 <style scoped> en ningún .svelte
  • Si hay estilos scoped, son solo para propiedades que no dependen de data-* ni de terra

11.6 API pública

  • exports.ts expone el componente con su nombre correcto
  • Los component tokens de Input y Field están documentados (qué tokens son override permitido)
  • Existe una demo en src/routes/test/field/ con: disabled, invalid, readonly, dark theme, tamaños

11.7 Limitaciones conocidas

  • Si se usa color-mix(), está documentado el requisito de navegador mínimo
  • Si se generan escalas en build time, el pipeline está documentado
  • El comportamiento cuando terra renombra un data-* está contemplado en la demo

Si actualizamos la arquitectura de air, este documento también debe actualizarse. El contrato con terra es tan estable como queramos que sea.

Powered by TurnKey Linux.