- getAir() is now the single entry point — returns behavior, dom, translator, logger - behavior.play() encapsulates soundEnabled check + preset resolution - dom.resolve() resolves ResponsiveProp using current viewport/breakpoints - internal/ has no knowledge of config/ (correct dependency direction) - internal/ types stripped of Air prefix; public barrel re-exports with prefix - context.ts defines TranslatorService locally (structurally compatible with TerraTranslator) - button/text components updated to use only getAir(), no direct terra/config imports - AudioContext fix: withRunningContext() resumes on first user gesture before playing - AIR_IMPLEMENTATION_GUIDE updated: 12 semantics, correct APIs, current patterns Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>glm-5
parent
9a9c4d4f14
commit
b30092dd06
@ -1,416 +0,0 @@
|
||||
# Air Motion
|
||||
|
||||
Especificacion del sistema de motion de `air`.
|
||||
|
||||
Este documento define como se mueve la interfaz, que presets existen, que tokens necesita el tema base y que reglas deben respetar los componentes. La motion de `air` no es un adorno: es parte del lenguaje visual del sistema.
|
||||
|
||||
---
|
||||
|
||||
## 1. Proposito
|
||||
|
||||
La motion en `air` existe para resolver cuatro cosas:
|
||||
|
||||
- reforzar jerarquia
|
||||
- comunicar causa y efecto
|
||||
- hacer mas legibles transiciones de estado
|
||||
- aportar tactilidad sin teatralidad
|
||||
|
||||
La motion no existe para:
|
||||
|
||||
- llamar la atencion por si misma
|
||||
- compensar una jerarquia visual debil
|
||||
- introducir rebotes o desplazamientos gratuitos
|
||||
- convertir cada componente en una demo de animacion
|
||||
|
||||
---
|
||||
|
||||
## 2. Principios
|
||||
|
||||
### Sobria
|
||||
|
||||
Toda animacion debe sentirse contenida. Si un usuario la nota mas que el cambio de estado que acompana, probablemente es excesiva.
|
||||
|
||||
### Estructural
|
||||
|
||||
La motion debe reforzar estructura:
|
||||
|
||||
- un overlay entra como overlay
|
||||
- un dropdown se despliega desde su origen
|
||||
- un button responde al gesto sin saltar por la pantalla
|
||||
|
||||
### Rapida
|
||||
|
||||
La interfaz de `air` no debe sentirse pesada. La salida suele ser igual o mas rapida que la entrada.
|
||||
|
||||
### Consistente
|
||||
|
||||
Los mismos tipos de transicion deben compartir:
|
||||
|
||||
- duracion
|
||||
- easing
|
||||
- distancia
|
||||
- escala
|
||||
|
||||
### Accesible
|
||||
|
||||
Todo preset debe poder degradarse con `prefers-reduced-motion`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Familias de motion
|
||||
|
||||
### Estado
|
||||
|
||||
Para cambios cortos de UI:
|
||||
|
||||
- hover
|
||||
- focus-visible
|
||||
- pressed
|
||||
- loading
|
||||
- invalid
|
||||
- selected
|
||||
|
||||
### Overlay
|
||||
|
||||
Para elementos flotantes o superficies elevadas:
|
||||
|
||||
- tooltip
|
||||
- popover
|
||||
- select content
|
||||
- dialog
|
||||
- drawer
|
||||
|
||||
### Estructural
|
||||
|
||||
Para cambios de layout o revelado de contenido:
|
||||
|
||||
- accordion
|
||||
- tabs indicator
|
||||
- collapsible regions
|
||||
- helper/error text
|
||||
|
||||
### Continua
|
||||
|
||||
Solo para feedback donde tenga sentido:
|
||||
|
||||
- spinner
|
||||
- progress indeterminate
|
||||
|
||||
Regla:
|
||||
|
||||
- la motion continua debe ser excepcional
|
||||
- si algo gira o pulsa en loop, debe comunicar trabajo en curso o actividad real
|
||||
|
||||
---
|
||||
|
||||
## 4. Tokens
|
||||
|
||||
El contrato de `air` ya define duraciones y easings base. El sistema de motion necesita ademas una pequena capa de distancia y escala.
|
||||
|
||||
### Duraciones
|
||||
|
||||
- `--air-duration-instant`
|
||||
- `--air-duration-fast`
|
||||
- `--air-duration-normal`
|
||||
- `--air-duration-moderate`
|
||||
- `--air-duration-slow`
|
||||
|
||||
### Easings
|
||||
|
||||
- `--air-ease-default`
|
||||
- `--air-ease-out`
|
||||
- `--air-ease-in`
|
||||
- `--air-ease-spring`
|
||||
|
||||
### Distancias
|
||||
|
||||
- `--air-motion-distance-xs`
|
||||
- `--air-motion-distance-sm`
|
||||
- `--air-motion-distance-md`
|
||||
|
||||
### Escalas
|
||||
|
||||
- `--air-motion-scale-enter`
|
||||
- `--air-motion-scale-press`
|
||||
|
||||
Regla:
|
||||
|
||||
- la motion no debe inventar distancias o escalas locales por componente si ya existe un token adecuado
|
||||
|
||||
---
|
||||
|
||||
## 5. Valores base
|
||||
|
||||
Direccion recomendada para `base-light` y `base-dark`:
|
||||
|
||||
```css
|
||||
--air-duration-instant: 0ms;
|
||||
--air-duration-fast: 120ms;
|
||||
--air-duration-normal: 180ms;
|
||||
--air-duration-moderate: 240ms;
|
||||
--air-duration-slow: 320ms;
|
||||
|
||||
--air-ease-default: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--air-ease-out: cubic-bezier(0, 0, 0.2, 1);
|
||||
--air-ease-in: cubic-bezier(0.4, 0, 1, 1);
|
||||
--air-ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
|
||||
--air-motion-distance-xs: 2px;
|
||||
--air-motion-distance-sm: 4px;
|
||||
--air-motion-distance-md: 8px;
|
||||
|
||||
--air-motion-scale-enter: 0.985;
|
||||
--air-motion-scale-press: 0.985;
|
||||
```
|
||||
|
||||
Notas:
|
||||
|
||||
- `fast` es la duracion de referencia para microinteraccion
|
||||
- `normal` sirve para error/helper/loading transitions y feedback visibles pero discretos
|
||||
- `moderate` y `slow` son para overlays, no para botones
|
||||
|
||||
---
|
||||
|
||||
## 6. Presets
|
||||
|
||||
## 6.1 Hover
|
||||
|
||||
Uso:
|
||||
|
||||
- color
|
||||
- border-color
|
||||
- shadow
|
||||
- background
|
||||
|
||||
Regla:
|
||||
|
||||
- no mover botones en `hover`
|
||||
- no desplazar componentes por pasar el cursor
|
||||
|
||||
Preset:
|
||||
|
||||
```css
|
||||
transition:
|
||||
background var(--air-duration-fast) var(--air-ease-default),
|
||||
border-color var(--air-duration-fast) var(--air-ease-default),
|
||||
color var(--air-duration-fast) var(--air-ease-default),
|
||||
box-shadow var(--air-duration-fast) var(--air-ease-default);
|
||||
```
|
||||
|
||||
## 6.2 Press
|
||||
|
||||
Uso:
|
||||
|
||||
- buttons
|
||||
- icon buttons
|
||||
- checkables tactiles
|
||||
|
||||
Regla:
|
||||
|
||||
- la respuesta debe sentirse tactil, no elástica
|
||||
- el gesto debe ser corto y controlado
|
||||
|
||||
Preset:
|
||||
|
||||
```css
|
||||
transform: scale(var(--air-motion-scale-press));
|
||||
transition-duration: var(--air-duration-fast);
|
||||
```
|
||||
|
||||
## 6.3 Focus
|
||||
|
||||
Uso:
|
||||
|
||||
- focus ring
|
||||
- border de foco
|
||||
|
||||
Regla:
|
||||
|
||||
- el focus no debe aparecer como destello brusco
|
||||
- tampoco debe ser tan lento que se sienta retrasado
|
||||
|
||||
Preset:
|
||||
|
||||
- `fast`
|
||||
- `ease-default`
|
||||
|
||||
## 6.4 Spinner
|
||||
|
||||
Uso:
|
||||
|
||||
- loading de button
|
||||
- progress indeterminate
|
||||
|
||||
Regla:
|
||||
|
||||
- movimiento continuo, uniforme y legible
|
||||
- evitar easings complejos
|
||||
|
||||
Preset:
|
||||
|
||||
```css
|
||||
animation: air-spin 0.8s linear infinite;
|
||||
```
|
||||
|
||||
## 6.5 Fade
|
||||
|
||||
Uso:
|
||||
|
||||
- helper text
|
||||
- error text
|
||||
- estados suaves
|
||||
|
||||
Preset:
|
||||
|
||||
```css
|
||||
opacity: 0 -> 1;
|
||||
transition: opacity var(--air-duration-normal) var(--air-ease-default);
|
||||
```
|
||||
|
||||
## 6.6 Slide-fade
|
||||
|
||||
Uso:
|
||||
|
||||
- dropdowns
|
||||
- popovers
|
||||
- small overlays
|
||||
|
||||
Preset:
|
||||
|
||||
```css
|
||||
opacity: 0;
|
||||
transform: translateY(var(--air-motion-distance-sm));
|
||||
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
```
|
||||
|
||||
Duracion recomendada:
|
||||
|
||||
- entrada: `moderate` + `ease-out`
|
||||
- salida: `normal` + `ease-in`
|
||||
|
||||
## 6.7 Scale-fade
|
||||
|
||||
Uso:
|
||||
|
||||
- menus compactos
|
||||
- dialogs pequenos
|
||||
- surfaces elevadas que deban sentirse contenidas
|
||||
|
||||
Preset:
|
||||
|
||||
```css
|
||||
opacity: 0;
|
||||
transform: scale(var(--air-motion-scale-enter));
|
||||
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
```
|
||||
|
||||
Regla:
|
||||
|
||||
- la escala debe ser casi imperceptible
|
||||
- `air` no debe usar zooms teatrales
|
||||
|
||||
---
|
||||
|
||||
## 7. Mapeo por componente
|
||||
|
||||
### Button
|
||||
|
||||
Permitido:
|
||||
|
||||
- transicion de color, borde y sombra en hover
|
||||
- press scale corto
|
||||
- spinner continuo en loading
|
||||
|
||||
No permitido:
|
||||
|
||||
- lift en hover
|
||||
- bounce
|
||||
- desplazamiento vertical
|
||||
|
||||
### Field / Input
|
||||
|
||||
Permitido:
|
||||
|
||||
- transicion de border-color
|
||||
- transicion de focus ring
|
||||
- fade o slide-fade corto para helper/error
|
||||
|
||||
### Tabs
|
||||
n
|
||||
Permitido:
|
||||
|
||||
- indicador activo
|
||||
- color del trigger
|
||||
- reveal corto del panel si aporta claridad
|
||||
|
||||
### Popover / Select / Dropdown
|
||||
|
||||
Permitido:
|
||||
|
||||
- slide-fade o scale-fade
|
||||
- origen ligado a placement
|
||||
|
||||
### Dialog / Drawer
|
||||
|
||||
Permitido:
|
||||
|
||||
- backdrop fade
|
||||
- panel slide o scale-fade contenido
|
||||
|
||||
Regla:
|
||||
|
||||
- backdrop y panel no deben competir entre si
|
||||
|
||||
---
|
||||
|
||||
## 8. Accesibilidad
|
||||
|
||||
Todo preset debe degradarse con:
|
||||
|
||||
```css
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Regla:
|
||||
|
||||
- reducir motion no debe romper feedback esencial
|
||||
- el estado final debe seguir siendo claro sin depender del movimiento
|
||||
|
||||
---
|
||||
|
||||
## 9. Anti-patrones
|
||||
|
||||
- mover botones en `hover`
|
||||
- usar rebotes como comportamiento base
|
||||
- animar `width` o `height` cuando un fade/transform lo resuelve mejor
|
||||
- meter motion distinta por componente para el mismo tipo de transicion
|
||||
- usar duraciones lentas en microinteracciones
|
||||
- usar motion como compensacion de una UI visualmente floja
|
||||
|
||||
---
|
||||
|
||||
## 10. Regla final
|
||||
|
||||
La motion de `air` debe sentirse como una extension natural del sistema visual:
|
||||
|
||||
- precisa
|
||||
- contenida
|
||||
- consistente
|
||||
- tactil
|
||||
- nunca dramatica
|
||||
|
||||
Si la animacion se vuelve protagonista, deja de ser `air`.
|
||||
|
||||
|
||||
|
||||
|
||||
@ -1,293 +0,0 @@
|
||||
# CORREGIR_DUPLICIDAD_TOKENS.md
|
||||
|
||||
> Estado: propuesta pendiente de implementacion
|
||||
> Fecha: 2026-04-06
|
||||
> Afecta: `themes/base/`, tema por defecto, mecanismo de activacion
|
||||
|
||||
---
|
||||
|
||||
## 1. Problema
|
||||
|
||||
Los archivos `themes/base/light.css` y `themes/base/dark.css` duplican ~110 lineas de tokens identicos: spacing, radius, tipografia, iconos, shadows y motion. Solo los tokens de color difieren entre ambos.
|
||||
|
||||
La duplicacion existe por una limitacion arquitectural, no por decision de diseno:
|
||||
|
||||
- ambos archivos usan `:root` como selector para funcionar como fallback cuando no hay tema explicito
|
||||
- `dark.css` no puede tener `:root` porque su orden de carga posterior pisaria los valores de `light.css`
|
||||
- sin `:root` en dark, este depende de que light cargue primero para heredar los tokens comunes
|
||||
- con `:root` en ambos, el ultimo en orden de fuente gana siempre, rompiendo light
|
||||
|
||||
### Estado actual
|
||||
|
||||
```
|
||||
light.css → [data-theme='base-light'], :root { ... colores + comunes ... }
|
||||
dark.css → [data-theme='base-dark'] { ... colores + comunes ... } ← depende de light
|
||||
```
|
||||
|
||||
Dark no es autosuficiente. Si se importara solo `dark.css`, faltarian spacing, tipografia, radius, motion, iconos y shadows.
|
||||
|
||||
---
|
||||
|
||||
## 2. Por que los demas no lo resuelven mejor
|
||||
|
||||
El problema no es de CSS. Es de arquitectura de entrega. El navegador no puede saber la preferencia del usuario antes de pintar sin que alguien se lo diga.
|
||||
|
||||
| Sistema | Estrategia | Limitacion |
|
||||
| --------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
|
||||
| Radix Themes | Un solo CSS compilado con ambos temas. `:root` es light, `.dark` override. | Funciona pero acopla compilacion |
|
||||
| shadcn/ui | Delega a Next.js + next-themes. SSR inyecta la clase antes del hydration. | Resuelve el delivery, no el CSS |
|
||||
| Mantine | Script inline obligatorio en `<head>`. Documentado como requisito. | Funciona pero es intrusivo |
|
||||
| Primer (GitHub) | Tokens compilados para cada tema. Ninguno usa `:root`. El servidor decide. | El enfoque mas maduro pero requiere build tool dedicado |
|
||||
| Chakra UI v3 | Panda CSS genera `_light` como base incondicional, `_dark` como `.dark` override. Tokens estaticos una sola vez. | El mas elegante pero requiere runtime de build |
|
||||
|
||||
Ninguno ha inventado algo mejor porque no pueden: es una limitacion del modelo de carga del navegador.
|
||||
|
||||
---
|
||||
|
||||
## 3. Solucion adoptada
|
||||
|
||||
Separar tokens en dos capas fisicas y activar el tema por clase en `<html>`.
|
||||
|
||||
### 3.1 Estructura de archivos propuesta
|
||||
|
||||
```
|
||||
themes/base/
|
||||
_static.css ← tokens no-color, una sola vez, en :root
|
||||
light.css ← solo colores light, sin :root
|
||||
dark.css ← solo colores dark, sin :root
|
||||
index.css ← barrel: importa _static, luego light, luego dark
|
||||
```
|
||||
|
||||
### 3.2 Contenido de cada archivo
|
||||
|
||||
#### `_static.css` — tokens no-color (singleton)
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Spacing scale */
|
||||
--air-space-0: 0px;
|
||||
--air-space-1: 4px;
|
||||
/* ... */
|
||||
|
||||
/* Radius scale */
|
||||
--air-radius-none: 0px;
|
||||
--air-radius-sm: 4px;
|
||||
/* ... */
|
||||
|
||||
/* Typography: familias, escala de 12 pasos, pesos, leading, tracking */
|
||||
--air-font-sans: 'Instrument Sans', system-ui, sans-serif;
|
||||
/* ... */
|
||||
|
||||
/* Icons */
|
||||
--air-icon-size-sm: 16px;
|
||||
/* ... */
|
||||
|
||||
/* Shadows */
|
||||
--air-shadow-0: none;
|
||||
/* ... */
|
||||
|
||||
/* Motion: duraciones, easings, distancias, escalas */
|
||||
--air-duration-fast: 120ms;
|
||||
/* ... */
|
||||
|
||||
/* Compatibility aliases (deprecated) */
|
||||
--air-color-bg: var(--air-color-surface-default);
|
||||
/* ... */
|
||||
}
|
||||
```
|
||||
|
||||
#### `light.css` — solo colores light
|
||||
|
||||
```css
|
||||
[data-theme='base-light'] {
|
||||
/* Primitives: neutral */
|
||||
--air-primitive-neutral-1: #fcfbf9;
|
||||
/* ... */
|
||||
|
||||
/* Primitives: primary, info, success, warning, danger */
|
||||
/* ... */
|
||||
|
||||
/* Semantic surfaces, content, borders */
|
||||
/* ... */
|
||||
|
||||
/* Semantic palettes: primary, neutral, success, warning, danger, info */
|
||||
/* ... */
|
||||
}
|
||||
```
|
||||
|
||||
#### `dark.css` — solo colores dark
|
||||
|
||||
```css
|
||||
[data-theme='base-dark'] {
|
||||
/* Primitives: neutral */
|
||||
--air-primitive-neutral-1: #111110;
|
||||
/* ... */
|
||||
|
||||
/* Primitives: primary, info, success, warning, danger */
|
||||
/* ... */
|
||||
|
||||
/* Semantic surfaces, content, borders */
|
||||
/* ... */
|
||||
|
||||
/* Semantic palettes: primary, neutral, success, warning, danger, info */
|
||||
/* ... */
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 Activacion del tema
|
||||
|
||||
El tema se activa mediante `data-theme` en `<html>`:
|
||||
|
||||
```html
|
||||
<html data-theme="base-light"></html>
|
||||
```
|
||||
|
||||
```html
|
||||
<html data-theme="base-dark"></html>
|
||||
```
|
||||
|
||||
Ningun archivo de tema usa `:root`. Los tokens estaticos siempre estan disponibles. El tema solo aporta colores.
|
||||
|
||||
### 3.4 Tema por defecto
|
||||
|
||||
El tema por defecto se decide fuera del CSS. Dos mecanismos posibles:
|
||||
|
||||
#### Opcion A: SSR / cookie
|
||||
|
||||
```javascript
|
||||
// hooks.server.js
|
||||
event.locals.theme = event.cookies.get('theme') ?? 'base-light';
|
||||
```
|
||||
|
||||
```html
|
||||
<!-- app.html -->
|
||||
<html data-theme="%sveltekit.theme%"></html>
|
||||
```
|
||||
|
||||
#### Opcion B: Script inline en `<head>`
|
||||
|
||||
```html
|
||||
<!-- app.html -->
|
||||
<script>
|
||||
document.documentElement.setAttribute(
|
||||
'data-theme',
|
||||
localStorage.getItem('theme') || 'base-light'
|
||||
);
|
||||
</script>
|
||||
```
|
||||
|
||||
El script se ejecuta antes del primer paint porque es sincrono y esta antes que los estilos. Evita flash de tema incorrecto.
|
||||
|
||||
### 3.5 Cambio de tema en runtime
|
||||
|
||||
```javascript
|
||||
document.documentElement.setAttribute('data-theme', 'base-dark');
|
||||
localStorage.setItem('theme', 'base-dark');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Por que funciona
|
||||
|
||||
| Problema anterior | Como se resuelve |
|
||||
| ------------------------------------------------------- | ------------------------------------------------------------------------------ |
|
||||
| Duplicacion de ~110 lineas comunes | Tokens estaticos en `_static.css`, una sola vez |
|
||||
| Dark depende de light para tokens comunes | Cada tema es autosuficiente en colores; los estaticos siempre estan en `:root` |
|
||||
| `:root` en ambos temas causa conflicto de especificidad | Ningun tema usa `:root` |
|
||||
| Ultimo import gana cuando no hay atributo | No hay fallback CSS; el atributo siempre esta presente |
|
||||
| Flash de tema incorrecto | La clase viene del SSR o de un script inline sincrono |
|
||||
| No se puede cambiar el default sin reordenar imports | El default lo decide el servidor o el script, no el CSS |
|
||||
|
||||
---
|
||||
|
||||
## 5. Resultado esperado
|
||||
|
||||
### Antes (actual)
|
||||
|
||||
| Archivo | Lineas | Contiene |
|
||||
| --------- | ------ | ----------------------------------------------- |
|
||||
| light.css | ~300 | colores light + todos los estaticos |
|
||||
| dark.css | ~300 | colores dark + todos los estaticos (duplicados) |
|
||||
|
||||
### Despues (propuesto)
|
||||
|
||||
| Archivo | Lineas | Contiene |
|
||||
| ------------ | ------ | ------------------------------------------------------------- |
|
||||
| \_static.css | ~110 | spacing, radius, tipografia, iconos, shadows, motion, aliases |
|
||||
| light.css | ~170 | solo primitivos y semanticos de color light |
|
||||
| dark.css | ~170 | solo primitivos y semanticos de color dark |
|
||||
|
||||
Ahorro neto: ~110 lineas de duplicacion eliminadas. Cada tema es mas legible porque solo contiene lo que cambia.
|
||||
|
||||
---
|
||||
|
||||
## 6. Cambios necesarios
|
||||
|
||||
### 6.1 CSS
|
||||
|
||||
1. Crear `themes/base/_static.css` con todos los tokens no-color en `:root`
|
||||
2. Eliminar de `light.css` todos los tokens no-color, dejar solo bloques de color
|
||||
3. Eliminar de `dark.css` todos los tokens no-color, dejar solo bloques de color
|
||||
4. Quitar `:root` del selector de ambos archivos de tema
|
||||
5. Actualizar `themes/base/index.css` para importar `_static.css` primero
|
||||
|
||||
### 6.2 HTML / SSR
|
||||
|
||||
1. Asegurar que `<html>` siempre tenga `data-theme` antes del primer paint
|
||||
2. Eliminar el `$effect` que setea el tema en `+layout.svelte` como unico mecanismo
|
||||
3. Reemplazar por script inline en `app.html` o por inyeccion SSR via hooks
|
||||
|
||||
### 6.3 JS (si aplica)
|
||||
|
||||
1. Actualizar la logica de `setAirTheme` para escribir en `<html>` en vez de un `<div>` intermedio
|
||||
2. Verificar que el cambio de tema en runtime sigue funcionando
|
||||
|
||||
---
|
||||
|
||||
## 7. Riesgos y consideraciones
|
||||
|
||||
### Riesgo: flash si el atributo no esta antes del CSS
|
||||
|
||||
Si `<html>` llega sin `data-theme`, los tokens de color no estaran definidos y la UI se vera sin color hasta que JS corrija. La solucion es que el atributo siempre este presente antes del paint.
|
||||
|
||||
### Riesgo: temas de terceros
|
||||
|
||||
Un tema de terceros solo necesita implementar colores. Los tokens estaticos ya estan garantizados por `_static.css`. Eso simplifica el contrato para autores de tema.
|
||||
|
||||
### Consideracion: `prefers-color-scheme`
|
||||
|
||||
Se puede anadir un fallback automatico:
|
||||
|
||||
```css
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme]) {
|
||||
/* tokens de dark como fallback del sistema operativo */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Esto cubre el caso extremo donde el script SSR falla y el usuario prefiere dark. No es necesario en la primera iteracion.
|
||||
|
||||
---
|
||||
|
||||
## 8. Orden de implementacion
|
||||
|
||||
1. Crear `_static.css` con los tokens no-color extraidos de `light.css`
|
||||
2. Limpiar `light.css`: quitar tokens no-color, quitar `:root` del selector
|
||||
3. Limpiar `dark.css`: quitar tokens no-color ya presentes via `_static.css`
|
||||
4. Actualizar `index.css` con el nuevo orden de imports
|
||||
5. Añadir script inline en `app.html` para setear `data-theme` antes del paint
|
||||
6. Actualizar la logica de tema en JS para operar sobre `<html>`
|
||||
7. Verificar build y tests
|
||||
8. Verificar visualmente light y dark en la demo
|
||||
|
||||
---
|
||||
|
||||
## 9. Criterio de exito
|
||||
|
||||
- [ ] No hay tokens duplicados entre `light.css` y `dark.css`
|
||||
- [ ] Ambos temas son visualmente correctos
|
||||
- [ ] Cambio de tema en runtime funciona sin flash
|
||||
- [ ] Un tema de terceros solo necesita implementar colores
|
||||
- [ ] Build pasa sin errores nuevos
|
||||
- [ ] La demo `/test/air` valida ambos temas correctamente
|
||||
@ -1,3 +1,4 @@
|
||||
export { default as Air } from './component/air.svelte';
|
||||
export { getAir, createAir, useAir, AirState } from './air.svelte.ts';
|
||||
export { createAir, useAir } from './air.svelte.ts';
|
||||
export { getContext as getAir } from '$uix/air/internal/context.js';
|
||||
export type { AirProps, AirPropsWithoutChildren, AirOwnProps } from './types';
|
||||
|
||||
@ -0,0 +1,59 @@
|
||||
import { readableActive, type Active } from '$terra/utils';
|
||||
import type { SoundPresets, SoundSemantic, SoundPhase } from '../sound/types';
|
||||
import type { MotionPresets } from '../motion/types';
|
||||
import { playSemantic, type PlaySemanticOptions } from '../sound/presets';
|
||||
|
||||
export type BehaviorProps = {
|
||||
soundEnabled: Active<boolean | undefined>;
|
||||
motionEnabled: Active<boolean | undefined>;
|
||||
soundVolume: Active<number | undefined>;
|
||||
soundPresets: Active<SoundPresets | undefined>;
|
||||
motionPresets: Active<MotionPresets | undefined>;
|
||||
};
|
||||
|
||||
export type Behavior = {
|
||||
soundEnabled: Active<boolean>;
|
||||
motionEnabled: Active<boolean>;
|
||||
soundVolume: Active<number>;
|
||||
/** Merged: parent ← child (el hijo solo declara lo que sobreescribe). */
|
||||
soundPresets: Active<SoundPresets | undefined>;
|
||||
/** Merged: parent ← child (el hijo solo declara lo que sobreescribe). */
|
||||
motionPresets: Active<MotionPresets | undefined>;
|
||||
/**
|
||||
* Reproduce el sonido de una semántica si el sonido está habilitado.
|
||||
* Aplica automáticamente los presets configurados en el árbol de Air.
|
||||
*/
|
||||
play(semantic: SoundSemantic, phase: SoundPhase, opts?: Omit<PlaySemanticOptions, 'presets'>): void;
|
||||
};
|
||||
|
||||
export function createBehavior(props: BehaviorProps, parent?: Behavior): Behavior {
|
||||
const soundEnabled = readableActive(
|
||||
() => props.soundEnabled?.current ?? parent?.soundEnabled.current ?? true
|
||||
);
|
||||
const soundPresets = readableActive(() => {
|
||||
const own = props.soundPresets?.current;
|
||||
const inherited = parent?.soundPresets.current;
|
||||
if (!own && !inherited) return undefined;
|
||||
return { ...inherited, ...own };
|
||||
});
|
||||
return {
|
||||
soundEnabled,
|
||||
motionEnabled: readableActive(
|
||||
() => props.motionEnabled?.current ?? parent?.motionEnabled.current ?? true
|
||||
),
|
||||
soundVolume: readableActive(
|
||||
() => props.soundVolume?.current ?? parent?.soundVolume.current ?? 1
|
||||
),
|
||||
soundPresets,
|
||||
motionPresets: readableActive(() => {
|
||||
const own = props.motionPresets?.current;
|
||||
const inherited = parent?.motionPresets.current;
|
||||
if (!own && !inherited) return undefined;
|
||||
return { ...inherited, ...own };
|
||||
}),
|
||||
play(semantic, phase, opts = {}) {
|
||||
if (!soundEnabled.current) return;
|
||||
playSemantic(semantic, phase, { ...opts, presets: soundPresets.current });
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -1,2 +1 @@
|
||||
export * from './behaviors.svelte.ts';
|
||||
|
||||
export { feedback, attention, emphasis, persistence, selection, completion } from './behaviors.svelte.ts';
|
||||
|
||||
@ -0,0 +1,43 @@
|
||||
import { createTerraContext } from '$terra/utils/context';
|
||||
import { readableActive, type Active } from '$terra/utils';
|
||||
import { createBehavior, type Behavior } from './behavior/behavior.svelte.js';
|
||||
import { createDom, type Dom } from './dom/dom.svelte.js';
|
||||
|
||||
// Tipos mínimos — estructuralmente compatibles con terra sin importarlo
|
||||
export interface Logger {
|
||||
warn(msg: string, ...args: unknown[]): void;
|
||||
error(msg: string, ...args: unknown[]): void;
|
||||
info?(msg: string, ...args: unknown[]): void;
|
||||
}
|
||||
|
||||
export interface TranslatorService {
|
||||
translate(value: unknown): string;
|
||||
translatePath(path: string, ...args: unknown[]): string;
|
||||
getLocale(): string;
|
||||
}
|
||||
|
||||
export type ContextValue = {
|
||||
behavior: Behavior;
|
||||
dom: Dom;
|
||||
translator: Active<TranslatorService | undefined>;
|
||||
logger: Active<Logger | undefined>;
|
||||
};
|
||||
|
||||
export const ctx = createTerraContext<ContextValue>('Air');
|
||||
|
||||
const FALLBACK: ContextValue = {
|
||||
behavior: createBehavior({
|
||||
soundEnabled: readableActive(() => undefined),
|
||||
motionEnabled: readableActive(() => undefined),
|
||||
soundVolume: readableActive(() => undefined),
|
||||
soundPresets: readableActive(() => undefined),
|
||||
motionPresets: readableActive(() => undefined),
|
||||
}),
|
||||
dom: createDom({ breakpoints: readableActive(() => undefined) }),
|
||||
translator: readableActive(() => undefined),
|
||||
logger: readableActive(() => undefined),
|
||||
};
|
||||
|
||||
export function getContext(): ContextValue {
|
||||
return ctx.getOr(FALLBACK);
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
import { readableActive, type Active } from '$terra/utils';
|
||||
import { BREAKPOINTS_DEFAULT, viewport, resolveResponsiveProp, type Breakpoints, type ResponsiveProp } from './responsive.svelte.js';
|
||||
|
||||
export type DomProps = {
|
||||
breakpoints: Active<Partial<Breakpoints> | undefined>;
|
||||
};
|
||||
|
||||
export type Dom = {
|
||||
/**
|
||||
* Breakpoints resueltos: defaults ← parent ← own.
|
||||
* Siempre devuelve un objeto completo con los 6 breakpoints.
|
||||
*/
|
||||
breakpoints: Active<Breakpoints>;
|
||||
/**
|
||||
* Ancho actual del viewport. Fuente reactiva única — los componentes
|
||||
* leen airConfig.dom.viewport.width sin importar nada de responsive.
|
||||
*/
|
||||
viewport: typeof viewport;
|
||||
/**
|
||||
* Resuelve un ResponsiveProp usando el viewport y breakpoints actuales.
|
||||
* Los componentes llaman `airConfig.dom.resolve(prop)` sin importar nada de responsive.
|
||||
*/
|
||||
resolve<T>(value: ResponsiveProp<T> | undefined): T | undefined;
|
||||
};
|
||||
|
||||
export function createDom(props: DomProps, parent?: Dom): Dom {
|
||||
const breakpoints = readableActive(() => ({
|
||||
...BREAKPOINTS_DEFAULT,
|
||||
...parent?.breakpoints.current,
|
||||
...props.breakpoints?.current,
|
||||
}));
|
||||
return {
|
||||
breakpoints,
|
||||
viewport,
|
||||
resolve<T>(value: ResponsiveProp<T> | undefined): T | undefined {
|
||||
return resolveResponsiveProp(value, viewport.width, breakpoints.current);
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -1 +1,2 @@
|
||||
export * from './responsive.svelte.ts';
|
||||
export * from './dom.svelte.js'
|
||||
@ -1,29 +0,0 @@
|
||||
import { tick } from 'svelte';
|
||||
|
||||
type MotionPreset = 'feedback' | 'revelation' | 'context' | 'none';
|
||||
|
||||
interface AirMotionOptions {
|
||||
type?: MotionPreset;
|
||||
index?: number; // Para el staggering (escalonamiento)
|
||||
origin?: 'top' | 'bottom' | 'left' | 'right' | 'center';
|
||||
}
|
||||
|
||||
export function airMotion(node: HTMLElement, options: AirMotionOptions = {}) {
|
||||
const { type = 'feedback', index = 0, origin = 'top' } = options;
|
||||
|
||||
// 1. Aplicar variables de delay para Staggering
|
||||
if (index > 0) {
|
||||
node.style.setProperty('--air-delay', `${index * 20}ms`);
|
||||
node.style.transitionDelay = 'var(--air-delay)';
|
||||
}
|
||||
|
||||
// 2. Determinar la clase de entrada según el tipo
|
||||
node.classList.add(`air-motion-${type}`);
|
||||
node.classList.add(`air-origin-${origin}`);
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
node.classList.remove(`air-motion-${type}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
import type { MotionPresets } from './types';
|
||||
|
||||
/**
|
||||
* Presets de motion por defecto del sistema.
|
||||
* Fallback garantizado cuando ningún tema activo declara un preset.
|
||||
*
|
||||
* Viven en internal/ — los temas importan desde aquí, nunca al revés.
|
||||
* themes/base/motion.ts re-exporta esto como BASE_MOTION_PRESETS.
|
||||
*/
|
||||
export const DEFAULT_MOTION_PRESETS: MotionPresets = {
|
||||
// ── Micro-interacción ─────────────────────────────────────────────────────
|
||||
|
||||
feedback: {
|
||||
stateClass: 'air-motion-press'
|
||||
},
|
||||
selection: {
|
||||
stateClass: 'air-motion-selection'
|
||||
},
|
||||
|
||||
// ── Aparición de contenido ────────────────────────────────────────────────
|
||||
|
||||
revelation: {
|
||||
enter: { duration: 180, easing: 'cubicOut', opacity: true, distance: 4, stagger: 20 },
|
||||
exit: { duration: 120, easing: 'cubicIn', opacity: true, scale: 0.92 }
|
||||
},
|
||||
context: {
|
||||
enter: { duration: 240, easing: 'cubicOut', opacity: true, scale: 0.985, distance: 16 },
|
||||
exit: { duration: 160, easing: 'cubicIn', opacity: true, scale: 0.96 }
|
||||
},
|
||||
expansion: {
|
||||
enter: { duration: 200, easing: 'cubicOut', opacity: true },
|
||||
exit: { duration: 150, easing: 'cubicIn', opacity: true }
|
||||
},
|
||||
|
||||
// ── Movimiento espacial ───────────────────────────────────────────────────
|
||||
|
||||
navigation: {
|
||||
stateClass: 'air-motion-navigation'
|
||||
},
|
||||
|
||||
// ── Mensajes y estados ────────────────────────────────────────────────────
|
||||
|
||||
notification: {
|
||||
stateClass: 'air-motion-notification'
|
||||
},
|
||||
attention: {
|
||||
stateClass: 'air-motion-attention'
|
||||
},
|
||||
emphasis: {
|
||||
stateClass: 'air-motion-emphasis'
|
||||
},
|
||||
|
||||
// ── Cierre y transformación ───────────────────────────────────────────────
|
||||
|
||||
completion: {
|
||||
stateClass: 'air-motion-completion'
|
||||
},
|
||||
destruction: {
|
||||
exit: { duration: 120, easing: 'cubicIn', opacity: true, scale: 0.88 },
|
||||
stateClass: 'air-motion-destruction'
|
||||
},
|
||||
persistence: {
|
||||
stateClass: 'air-motion-spin'
|
||||
}
|
||||
};
|
||||
@ -1,5 +1,7 @@
|
||||
export { airReveal, airDismiss, airContext } from './transitions';
|
||||
export { reveal, dismiss, contextIn, contextOut, expand, expandOut } from './transitions';
|
||||
export type { MotionOrigin, RevealOptions, ContextOptions } from './transitions';
|
||||
|
||||
export { airMotion } from './motion.svelte.js';
|
||||
export type { MotionType, AirMotionOptions } from './motion.svelte.js';
|
||||
export { motion } from './motion.svelte.js';
|
||||
export type { MotionType, MotionOptions } from './motion.svelte.js';
|
||||
|
||||
export type { EasingName, MotionPresetData, MotionTransitionFn, MotionPreset, MotionPresets, Semantic } from './types';
|
||||
|
||||
@ -0,0 +1,100 @@
|
||||
import type { TransitionConfig } from 'svelte/transition';
|
||||
import type { Semantic } from '$uix/air/internal/semantic';
|
||||
|
||||
export type { Semantic };
|
||||
|
||||
/**
|
||||
* Nombres de easing disponibles para presets declarativos.
|
||||
* Corresponden directamente a las funciones exportadas por 'svelte/easing'.
|
||||
*/
|
||||
export type EasingName =
|
||||
| 'linear'
|
||||
| 'cubicIn'
|
||||
| 'cubicOut'
|
||||
| 'cubicInOut'
|
||||
| 'sineIn'
|
||||
| 'sineOut'
|
||||
| 'sineInOut'
|
||||
| 'backOut'
|
||||
| 'elasticOut';
|
||||
|
||||
/**
|
||||
* Definición declarativa de una transición JS — JSON puro, sin código.
|
||||
* El engine de transitions.ts la interpreta igual que playSound
|
||||
* interpreta las SyntheticLayer del sistema de sonido.
|
||||
*/
|
||||
export interface MotionPresetData {
|
||||
/** Duración en ms. */
|
||||
duration?: number;
|
||||
/** Función de easing por nombre. */
|
||||
easing?: EasingName;
|
||||
/** Incluir fade de opacidad. Por defecto true. */
|
||||
opacity?: boolean;
|
||||
/** Escala de partida (enter) o de llegada (exit). */
|
||||
scale?: number;
|
||||
/** Distancia de desplazamiento en px. Usada en revelation y context con side. */
|
||||
distance?: number;
|
||||
/** Delay por unidad de índice stagger en ms. Por defecto 20. */
|
||||
stagger?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Función imperativa — el equivalente a FileLayer en sonido.
|
||||
* Úsala para animaciones avanzadas (spring, librería externa como GSAP…).
|
||||
*/
|
||||
export type MotionTransitionFn = (node: Element, options?: unknown) => TransitionConfig;
|
||||
|
||||
/**
|
||||
* Un preset de transición JS es datos declarativos O una función imperativa.
|
||||
* Análogo a SyntheticLayer | FileLayer en el sistema de sonido.
|
||||
*/
|
||||
export type MotionPreset = MotionPresetData | MotionTransitionFn;
|
||||
|
||||
/**
|
||||
* Entrada de motion para una semántica — cubre las dos capas del sistema:
|
||||
*
|
||||
* - `enter` / `exit`: transiciones JS para elementos que entran/salen del DOM
|
||||
* (Svelte `transition:` / `in:` / `out:`). Se usan en overlays, modales,
|
||||
* elementos destruidos, etc.
|
||||
*
|
||||
* - `stateClass`: clase CSS aplicada por la action `airMotion` para animaciones
|
||||
* de estado (el elemento permanece en el DOM pero cambia visualmente).
|
||||
* El tema define la clase; el CSS del tema implementa la animación.
|
||||
* Esto permite al diseñador sobreescribir completamente la animación de
|
||||
* feedback, emphasis, attention, etc. sin tocar código JS.
|
||||
*/
|
||||
export interface MotionEntry {
|
||||
/** Transición JS al aparecer en el DOM. */
|
||||
enter?: MotionPreset;
|
||||
/** Transición JS al salir del DOM. */
|
||||
exit?: MotionPreset;
|
||||
/**
|
||||
* Clase CSS para animaciones de estado (action `airMotion`).
|
||||
* Si no se define, `airMotion` usa los defaults del tema base.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // En el tema earth:
|
||||
* feedback: { stateClass: 'earth-motion-press' }
|
||||
* // En earth/motion.css:
|
||||
* .earth-motion-press { animation: earth-press 120ms ease-out; }
|
||||
* ```
|
||||
*/
|
||||
stateClass?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override parcial de motion por semántica.
|
||||
* Cubre las 12 semánticas del framework — cada una con su entrada de motion.
|
||||
* Estructura simétrica a SoundPresets: mismo eje semántico, mismo patrón.
|
||||
*
|
||||
* @example — datos declarativos (JSON puro)
|
||||
* ```svelte
|
||||
* <Air motionPresets={{
|
||||
* revelation: { enter: { duration: 300, easing: 'backOut', distance: 8 } },
|
||||
* context: { enter: { duration: 350, easing: 'sineOut' }, exit: { duration: 200 } },
|
||||
* feedback: { stateClass: 'my-theme-press' },
|
||||
* }}>
|
||||
* ```
|
||||
*/
|
||||
export type MotionPresets = Partial<Record<Semantic, MotionEntry>>;
|
||||
@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Semánticas del framework Air — eje vertebrador de todos los subsistemas.
|
||||
*
|
||||
* Cualquier sistema expresivo (sonido, motion, haptics, color dinámico…)
|
||||
* referencia este tipo como clave. Un tema define su personalidad una vez,
|
||||
* por semántica, y todos los subsistemas la leen de forma coherente.
|
||||
*
|
||||
* Las 12 semánticas cubren el espectro completo de interacciones UI:
|
||||
*
|
||||
* ── Micro-interacción ──────────────────────────────────────────────────────
|
||||
* feedback Respuesta táctil inmediata: click, press, key
|
||||
* selection Cambio de estado persistente: check, toggle, select item
|
||||
*
|
||||
* ── Aparición de contenido ─────────────────────────────────────────────────
|
||||
* revelation Overlay emergente: dropdown, popover, tooltip
|
||||
* context Superficie modal: dialog, drawer (bloquea el foco)
|
||||
* expansion Contenido inline que crece: accordion, collapsible
|
||||
*
|
||||
* ── Movimiento espacial ────────────────────────────────────────────────────
|
||||
* navigation Transición de vista o ruta
|
||||
*
|
||||
* ── Mensajes y estados ─────────────────────────────────────────────────────
|
||||
* notification Mensaje efímero no bloqueante: toast, snackbar
|
||||
* attention Alerta urgente: error crítico, warning bloqueante
|
||||
* emphasis Guía visual suave: highlight, pulse, nudge
|
||||
*
|
||||
* ── Cierre y transformación ────────────────────────────────────────────────
|
||||
* completion Cierre positivo: éxito, tarea terminada, confirmación
|
||||
* destruction Eliminación: delete, discard, borrado irreversible
|
||||
* persistence Espera activa indeterminada: spinner, skeleton
|
||||
*/
|
||||
export type Semantic =
|
||||
| 'feedback'
|
||||
| 'selection'
|
||||
| 'revelation'
|
||||
| 'context'
|
||||
| 'expansion'
|
||||
| 'navigation'
|
||||
| 'notification'
|
||||
| 'attention'
|
||||
| 'emphasis'
|
||||
| 'completion'
|
||||
| 'destruction'
|
||||
| 'persistence';
|
||||
|
||||
/** Fase de una interacción con semántica (entrada o salida del estado/elemento). */
|
||||
export type SemanticPhase = 'enter' | 'exit';
|
||||
@ -0,0 +1,155 @@
|
||||
import type { SoundPresetMap } from './types';
|
||||
|
||||
/**
|
||||
* Presets de sonido por defecto del sistema — cubre las 12 semánticas.
|
||||
* Fallback garantizado cuando ningún tema activo declara un preset.
|
||||
*
|
||||
* Viven en internal/ — los temas importan desde aquí, nunca al revés.
|
||||
* themes/base/sound.ts re-exporta esto como BASE_SOUND_PRESETS.
|
||||
*/
|
||||
export const DEFAULT_SOUND_PRESETS: SoundPresetMap = {
|
||||
// ── Micro-interacción ──────────────────────────────────────────────────
|
||||
|
||||
feedback: {
|
||||
enter: {
|
||||
layers: [{ frequency: 2600, durationMs: 65, oscillator: 'sine', volumeDb: -12, attack: 0.001, release: 0.03 }]
|
||||
},
|
||||
exit: {
|
||||
layers: [{ frequency: 2210, durationMs: 40, oscillator: 'sine', volumeDb: -16, attack: 0.001, release: 0.02 }]
|
||||
}
|
||||
},
|
||||
|
||||
selection: {
|
||||
enter: {
|
||||
// Clic confirmatorio — más suave que feedback, indica estado persistente
|
||||
layers: [{ frequency: 2400, durationMs: 50, oscillator: 'sine', volumeDb: -14, attack: 0.001, release: 0.02 }]
|
||||
},
|
||||
exit: {
|
||||
// Deselección — tono descendente leve
|
||||
layers: [{ frequency: 2000, durationMs: 35, oscillator: 'sine', volumeDb: -17, attack: 0.001, release: 0.015 }]
|
||||
}
|
||||
},
|
||||
|
||||
// ── Aparición de contenido ─────────────────────────────────────────────
|
||||
|
||||
revelation: {
|
||||
enter: {
|
||||
layers: [{ frequency: 800, endFrequency: 1200, durationMs: 160, oscillator: 'sine', volumeDb: -15, attack: 0.01, release: 0.04 }]
|
||||
},
|
||||
exit: {
|
||||
layers: [{ frequency: 1000, durationMs: 55, oscillator: 'sine', volumeDb: -18, attack: 0.001, release: 0.03 }]
|
||||
}
|
||||
},
|
||||
|
||||
context: {
|
||||
enter: {
|
||||
layers: [{ frequency: 520, durationMs: 240, oscillator: 'sawtooth', volumeDb: -18, attack: 0.02, release: 0.12 }]
|
||||
},
|
||||
exit: {
|
||||
layers: [{ frequency: 520, endFrequency: 320, durationMs: 150, oscillator: 'sawtooth', volumeDb: -21, attack: 0.001, release: 0.08 }]
|
||||
}
|
||||
},
|
||||
|
||||
expansion: {
|
||||
enter: {
|
||||
// Apertura suave — frecuencia ascendente, más corta que revelation
|
||||
layers: [{ frequency: 720, endFrequency: 1100, durationMs: 140, oscillator: 'sine', volumeDb: -17, attack: 0.01, release: 0.07 }]
|
||||
},
|
||||
exit: {
|
||||
// Cierre — frecuencia descendente
|
||||
layers: [{ frequency: 1000, endFrequency: 680, durationMs: 100, oscillator: 'sine', volumeDb: -19, attack: 0.001, release: 0.05 }]
|
||||
}
|
||||
},
|
||||
|
||||
// ── Movimiento espacial ────────────────────────────────────────────────
|
||||
|
||||
navigation: {
|
||||
enter: {
|
||||
// Llegada a nueva vista — sweep ascendente, espacial
|
||||
layers: [{ frequency: 700, endFrequency: 1000, durationMs: 240, oscillator: 'sine', volumeDb: -18, attack: 0.02, release: 0.10 }]
|
||||
},
|
||||
exit: {
|
||||
// Salida de vista — sweep descendente
|
||||
layers: [{ frequency: 900, endFrequency: 650, durationMs: 140, oscillator: 'sine', volumeDb: -20, attack: 0.001, release: 0.07 }]
|
||||
}
|
||||
},
|
||||
|
||||
// ── Mensajes y estados ─────────────────────────────────────────────────
|
||||
|
||||
notification: {
|
||||
enter: {
|
||||
// Dos tonos cálidos escalonados — no urgente, amigable
|
||||
layers: [
|
||||
{ frequency: 1400, durationMs: 120, oscillator: 'sine', volumeDb: -16, attack: 0.01, release: 0.06 },
|
||||
{ frequency: 1760, durationMs: 80, oscillator: 'sine', volumeDb: -19, attack: 0.02, release: 0.04 }
|
||||
],
|
||||
layerDelayMs: 50
|
||||
},
|
||||
exit: {
|
||||
// Las notificaciones desaparecen en silencio
|
||||
layers: []
|
||||
}
|
||||
},
|
||||
|
||||
attention: {
|
||||
enter: {
|
||||
layers: [{ frequency: 1850, endFrequency: 1480, durationMs: 110, oscillator: 'sine', volumeDb: -10, attack: 0.001, release: 0.04 }]
|
||||
},
|
||||
exit: {
|
||||
layers: [{ frequency: 1388, durationMs: 55, oscillator: 'square', volumeDb: -13, attack: 0.001, release: 0.02 }]
|
||||
}
|
||||
},
|
||||
|
||||
emphasis: {
|
||||
enter: {
|
||||
layers: [
|
||||
{ frequency: 1600, durationMs: 120, oscillator: 'sine', volumeDb: -16, attack: 0.01, release: 0.08 },
|
||||
{ frequency: 1760, durationMs: 100, oscillator: 'sine', volumeDb: -18, attack: 0.01, release: 0.08 }
|
||||
],
|
||||
layerDelayMs: 40
|
||||
},
|
||||
exit: {
|
||||
layers: [{ frequency: 1600, durationMs: 88, oscillator: 'sine', volumeDb: -20, attack: 0.001, release: 0.03 }]
|
||||
}
|
||||
},
|
||||
|
||||
// ── Cierre y transformación ────────────────────────────────────────────
|
||||
|
||||
completion: {
|
||||
enter: {
|
||||
// Dos tonos ascendentes — positivo, luminoso, celebratorio pero discreto
|
||||
layers: [
|
||||
{ frequency: 1600, durationMs: 120, oscillator: 'sine', volumeDb: -13, attack: 0.005, release: 0.06 },
|
||||
{ frequency: 2000, durationMs: 100, oscillator: 'sine', volumeDb: -15, attack: 0.01, release: 0.05 }
|
||||
],
|
||||
layerDelayMs: 60
|
||||
},
|
||||
exit: {
|
||||
layers: []
|
||||
}
|
||||
},
|
||||
|
||||
destruction: {
|
||||
enter: {
|
||||
layers: [
|
||||
{ frequency: 1250, durationMs: 95, oscillator: 'triangle', volumeDb: -14, attack: 0.001, release: 0.08 },
|
||||
{ frequency: 913, durationMs: 57, oscillator: 'triangle', volumeDb: -17, attack: 0.001, release: 0.10 }
|
||||
],
|
||||
layerDelayMs: 25
|
||||
},
|
||||
exit: {
|
||||
layers: [{ frequency: 750, endFrequency: 375, durationMs: 50, oscillator: 'triangle', volumeDb: -20, attack: 0.001, release: 0.05 }]
|
||||
}
|
||||
},
|
||||
|
||||
persistence: {
|
||||
enter: {
|
||||
layers: [{ frequency: 900, durationMs: 400, oscillator: 'sine', volumeDb: -20, filterFreq: 1200 }],
|
||||
loop: true,
|
||||
loopFilterFreq: 1200
|
||||
},
|
||||
exit: {
|
||||
layers: []
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,13 @@
|
||||
import type { AirTheme } from '../types.js';
|
||||
|
||||
/**
|
||||
* Tema base light — personalidad por defecto del sistema.
|
||||
* No declara presets: sound y motion heredan de BASE_SOUND_PRESETS y BASE_MOTION_PRESETS.
|
||||
*/
|
||||
export const BASE_LIGHT_THEME: AirTheme = { id: 'base-light' };
|
||||
|
||||
/**
|
||||
* Tema base dark — misma personalidad sonora y cinética que light.
|
||||
* Solo cambia la dimensión visual (CSS tokens de color).
|
||||
*/
|
||||
export const BASE_DARK_THEME: AirTheme = { id: 'base-dark' };
|
||||
@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Re-exporta los defaults de motion del sistema como BASE_MOTION_PRESETS.
|
||||
* Los defaults viven en internal/motion/defaults.ts — esta es solo la
|
||||
* interfaz pública del tema base para que los consumidores puedan importarlos.
|
||||
*/
|
||||
export { DEFAULT_MOTION_PRESETS as BASE_MOTION_PRESETS } from '$uix/air/internal/motion/defaults';
|
||||
@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Re-exporta los defaults de sonido del sistema como BASE_SOUND_PRESETS.
|
||||
* Los defaults viven en internal/sound/defaults.ts — esta es solo la
|
||||
* interfaz pública del tema base para que los consumidores puedan importarlos.
|
||||
*/
|
||||
export { DEFAULT_SOUND_PRESETS as BASE_SOUND_PRESETS } from '$uix/air/internal/sound/defaults';
|
||||
@ -0,0 +1,19 @@
|
||||
/* Earth: override de tokens estáticos de motion.
|
||||
* El resto de tokens estáticos (spacing, tipografía…) heredan de themes/base/_static.css.
|
||||
* Solo se sobreescriben los que tienen valor distinto en el lenguaje cinético earth.
|
||||
*/
|
||||
[data-theme='earth-light'],
|
||||
[data-theme='earth-dark'] {
|
||||
/* Distancias — más generosas, más presencia física */
|
||||
--air-motion-distance-xs: 3px;
|
||||
--air-motion-distance-sm: 6px;
|
||||
--air-motion-distance-md: 10px;
|
||||
--air-motion-distance-lg: 20px;
|
||||
|
||||
/* Escalas — más sutiles, más orgánicas */
|
||||
--air-motion-scale-enter: 0.982;
|
||||
--air-motion-scale-press: 0.95;
|
||||
|
||||
/* Stagger — escalonamiento más perceptible */
|
||||
--air-motion-stagger: 28ms;
|
||||
}
|
||||
@ -0,0 +1,178 @@
|
||||
/* Earth-dark theme — paleta oscura tierra: suelos profundos, terracota brillante.
|
||||
* Tokens estáticos (spacing, tipografía, motion, etc.) viven en _static.css.
|
||||
*/
|
||||
[data-theme='earth-dark'] {
|
||||
/* Private primitives: neutral — negros suelo a blancos cálidos */
|
||||
--air-primitive-neutral-1: #130e09;
|
||||
--air-primitive-neutral-2: #1c160f;
|
||||
--air-primitive-neutral-3: #262016;
|
||||
--air-primitive-neutral-4: #31281b;
|
||||
--air-primitive-neutral-5: #3b3021;
|
||||
--air-primitive-neutral-6: #4a3d2a;
|
||||
--air-primitive-neutral-7: #5e4e38;
|
||||
--air-primitive-neutral-8: #786250;
|
||||
--air-primitive-neutral-9: #8a7460;
|
||||
--air-primitive-neutral-10: #9c876f;
|
||||
--air-primitive-neutral-11: #c4ae94;
|
||||
--air-primitive-neutral-12: #f2e6d4;
|
||||
|
||||
/* Private primitives: primary — terracota luminosa en oscuro */
|
||||
--air-primitive-primary-1: #1e1108;
|
||||
--air-primitive-primary-2: #28170b;
|
||||
--air-primitive-primary-3: #3e2214;
|
||||
--air-primitive-primary-4: #572e1a;
|
||||
--air-primitive-primary-5: #6e3a20;
|
||||
--air-primitive-primary-6: #8a4928;
|
||||
--air-primitive-primary-7: #ab5c34;
|
||||
--air-primitive-primary-8: #cc7044;
|
||||
--air-primitive-primary-9: #e07a4a;
|
||||
--air-primitive-primary-10: #e8916a;
|
||||
--air-primitive-primary-11: #f4bfa0;
|
||||
--air-primitive-primary-12: #fde8d8;
|
||||
|
||||
/* Private primitives: info — pizarra teal oscuro */
|
||||
--air-primitive-info-1: #091514;
|
||||
--air-primitive-info-2: #0f1e1d;
|
||||
--air-primitive-info-3: #142e2c;
|
||||
--air-primitive-info-4: #193f3c;
|
||||
--air-primitive-info-5: #204f4b;
|
||||
--air-primitive-info-6: #296260;
|
||||
--air-primitive-info-7: #347874;
|
||||
--air-primitive-info-8: #408f8a;
|
||||
--air-primitive-info-9: #2a7a72;
|
||||
--air-primitive-info-10: #4da49d;
|
||||
--air-primitive-info-11: #88cec8;
|
||||
--air-primitive-info-12: #c8eeec;
|
||||
|
||||
/* Private primitives: success — oliva oscuro */
|
||||
--air-primitive-success-1: #0d140a;
|
||||
--air-primitive-success-2: #141d0e;
|
||||
--air-primitive-success-3: #1c2d14;
|
||||
--air-primitive-success-4: #243c1a;
|
||||
--air-primitive-success-5: #2d4c21;
|
||||
--air-primitive-success-6: #395e2a;
|
||||
--air-primitive-success-7: #487535;
|
||||
--air-primitive-success-8: #598d42;
|
||||
--air-primitive-success-9: #3a781e;
|
||||
--air-primitive-success-10: #70a850;
|
||||
--air-primitive-success-11: #a4d080;
|
||||
--air-primitive-success-12: #d4eebc;
|
||||
|
||||
/* Private primitives: warning — ámbar oscuro */
|
||||
--air-primitive-warning-1: #191008;
|
||||
--air-primitive-warning-2: #21160c;
|
||||
--air-primitive-warning-3: #322212;
|
||||
--air-primitive-warning-4: #432e18;
|
||||
--air-primitive-warning-5: #553b1f;
|
||||
--air-primitive-warning-6: #6b4a27;
|
||||
--air-primitive-warning-7: #8f6230;
|
||||
--air-primitive-warning-8: #b47a38;
|
||||
--air-primitive-warning-9: #df9830;
|
||||
--air-primitive-warning-10: #f0b050;
|
||||
--air-primitive-warning-11: #fad4a0;
|
||||
--air-primitive-warning-12: #feecd4;
|
||||
|
||||
/* Private primitives: danger — ladrillo oscuro */
|
||||
--air-primitive-danger-1: #190c0a;
|
||||
--air-primitive-danger-2: #221210;
|
||||
--air-primitive-danger-3: #381c18;
|
||||
--air-primitive-danger-4: #4e251f;
|
||||
--air-primitive-danger-5: #613028;
|
||||
--air-primitive-danger-6: #773d33;
|
||||
--air-primitive-danger-7: #964e42;
|
||||
--air-primitive-danger-8: #ba6456;
|
||||
--air-primitive-danger-9: #d97060;
|
||||
--air-primitive-danger-10: #e48474;
|
||||
--air-primitive-danger-11: #f4b0a4;
|
||||
--air-primitive-danger-12: #fce0da;
|
||||
|
||||
/* Semantic surfaces */
|
||||
--air-color-surface-default: var(--air-primitive-neutral-1);
|
||||
--air-color-surface-raised: var(--air-primitive-neutral-3);
|
||||
--air-color-surface-overlay: var(--air-primitive-neutral-4);
|
||||
--air-color-overlay: rgb(0 0 0 / 0.70);
|
||||
|
||||
/* Semantic content */
|
||||
--air-color-content-primary: var(--air-primitive-neutral-12);
|
||||
--air-color-content-secondary: var(--air-primitive-neutral-11);
|
||||
--air-color-content-disabled: var(--air-primitive-neutral-7);
|
||||
--air-color-content-on-solid: #ffffff;
|
||||
|
||||
/* Semantic borders */
|
||||
--air-color-border-subtle: var(--air-primitive-neutral-4);
|
||||
--air-color-border-default: var(--air-primitive-neutral-6);
|
||||
--air-color-border-strong: var(--air-primitive-neutral-8);
|
||||
--air-color-focus-ring: var(--air-primitive-primary-10);
|
||||
|
||||
/* Primary palette */
|
||||
--air-color-primary-track: var(--air-primitive-primary-1);
|
||||
--air-color-primary-element: var(--air-primitive-primary-3);
|
||||
--air-color-primary-hover: var(--air-primitive-primary-4);
|
||||
--air-color-primary-active: var(--air-primitive-primary-5);
|
||||
--air-color-primary-border: var(--air-primitive-primary-6);
|
||||
--air-color-primary-solid: var(--air-primitive-primary-9);
|
||||
--air-color-primary-solid-hover: var(--air-primitive-primary-10);
|
||||
--air-color-primary-text: var(--air-primitive-primary-11);
|
||||
--air-color-primary-contrast: var(--air-primitive-primary-12);
|
||||
|
||||
/* Neutral palette */
|
||||
--air-color-neutral-track: var(--air-primitive-neutral-1);
|
||||
--air-color-neutral-element: var(--air-primitive-neutral-3);
|
||||
--air-color-neutral-hover: var(--air-primitive-neutral-4);
|
||||
--air-color-neutral-active: var(--air-primitive-neutral-5);
|
||||
--air-color-neutral-border: var(--air-primitive-neutral-6);
|
||||
--air-color-neutral-solid: var(--air-primitive-neutral-9);
|
||||
--air-color-neutral-solid-hover: var(--air-primitive-neutral-10);
|
||||
--air-color-neutral-text: var(--air-primitive-neutral-11);
|
||||
--air-color-neutral-contrast: var(--air-primitive-neutral-12);
|
||||
|
||||
/* Success palette */
|
||||
--air-color-success-track: var(--air-primitive-success-1);
|
||||
--air-color-success-element: var(--air-primitive-success-3);
|
||||
--air-color-success-hover: var(--air-primitive-success-4);
|
||||
--air-color-success-active: var(--air-primitive-success-5);
|
||||
--air-color-success-border: var(--air-primitive-success-6);
|
||||
--air-color-success-solid: var(--air-primitive-success-9);
|
||||
--air-color-success-solid-hover: var(--air-primitive-success-10);
|
||||
--air-color-success-text: var(--air-primitive-success-11);
|
||||
--air-color-success-contrast: var(--air-primitive-success-12);
|
||||
|
||||
/* Warning palette */
|
||||
--air-color-warning-track: var(--air-primitive-warning-1);
|
||||
--air-color-warning-element: var(--air-primitive-warning-3);
|
||||
--air-color-warning-hover: var(--air-primitive-warning-4);
|
||||
--air-color-warning-active: var(--air-primitive-warning-5);
|
||||
--air-color-warning-border: var(--air-primitive-warning-6);
|
||||
--air-color-warning-solid: var(--air-primitive-warning-9);
|
||||
--air-color-warning-solid-hover: var(--air-primitive-warning-10);
|
||||
--air-color-warning-text: var(--air-primitive-warning-11);
|
||||
--air-color-warning-contrast: var(--air-primitive-warning-12);
|
||||
|
||||
/* Danger palette */
|
||||
--air-color-danger-track: var(--air-primitive-danger-1);
|
||||
--air-color-danger-element: var(--air-primitive-danger-3);
|
||||
--air-color-danger-hover: var(--air-primitive-danger-4);
|
||||
--air-color-danger-active: var(--air-primitive-danger-5);
|
||||
--air-color-danger-border: var(--air-primitive-danger-6);
|
||||
--air-color-danger-solid: var(--air-primitive-danger-9);
|
||||
--air-color-danger-solid-hover: var(--air-primitive-danger-10);
|
||||
--air-color-danger-text: var(--air-primitive-danger-11);
|
||||
--air-color-danger-contrast: var(--air-primitive-danger-12);
|
||||
|
||||
/* Info palette */
|
||||
--air-color-info-track: var(--air-primitive-info-1);
|
||||
--air-color-info-element: var(--air-primitive-info-3);
|
||||
--air-color-info-hover: var(--air-primitive-info-4);
|
||||
--air-color-info-active: var(--air-primitive-info-5);
|
||||
--air-color-info-border: var(--air-primitive-info-6);
|
||||
--air-color-info-solid: var(--air-primitive-info-9);
|
||||
--air-color-info-solid-hover: var(--air-primitive-info-10);
|
||||
--air-color-info-text: var(--air-primitive-info-11);
|
||||
--air-color-info-contrast: var(--air-primitive-info-12);
|
||||
|
||||
/* Shadows — más cálidas que el base-dark */
|
||||
--air-shadow-0: none;
|
||||
--air-shadow-1: 0 1px 2px rgb(0 0 0 / 0.30), 0 0 0 1px rgb(255 220 180 / 0.04);
|
||||
--air-shadow-2: 0 4px 12px rgb(0 0 0 / 0.40), 0 1px 2px rgb(0 0 0 / 0.20);
|
||||
--air-shadow-3: 0 12px 32px rgb(0 0 0 / 0.55), 0 4px 8px rgb(0 0 0 / 0.28);
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
@import './_static.css';
|
||||
@import './light.css';
|
||||
@import './dark.css';
|
||||
@ -0,0 +1,15 @@
|
||||
import type { AirTheme } from '../types.js';
|
||||
import { earthMotionPresets } from './motion.js';
|
||||
import { earthSoundPresets } from './sound.js';
|
||||
|
||||
export const EARTH_LIGHT_THEME = {
|
||||
id: 'earth-light',
|
||||
motionPresets: earthMotionPresets,
|
||||
soundPresets: earthSoundPresets
|
||||
} satisfies AirTheme;
|
||||
|
||||
export const EARTH_DARK_THEME = {
|
||||
id: 'earth-dark',
|
||||
motionPresets: earthMotionPresets,
|
||||
soundPresets: earthSoundPresets
|
||||
} satisfies AirTheme;
|
||||
@ -0,0 +1,178 @@
|
||||
/* Earth-light theme — paleta de colores tierra: terracota, ocre, salvia, pizarra.
|
||||
* Tokens estáticos (spacing, tipografía, motion, etc.) viven en _static.css.
|
||||
*/
|
||||
[data-theme='earth-light'] {
|
||||
/* Private primitives: neutral — blancos cremosos a marrones cálidos */
|
||||
--air-primitive-neutral-1: #fdfcf8;
|
||||
--air-primitive-neutral-2: #f9f5ee;
|
||||
--air-primitive-neutral-3: #f0ebe0;
|
||||
--air-primitive-neutral-4: #e6ddd0;
|
||||
--air-primitive-neutral-5: #d9d0bf;
|
||||
--air-primitive-neutral-6: #c9bfab;
|
||||
--air-primitive-neutral-7: #b4a890;
|
||||
--air-primitive-neutral-8: #978b72;
|
||||
--air-primitive-neutral-9: #726654;
|
||||
--air-primitive-neutral-10: #5e5342;
|
||||
--air-primitive-neutral-11: #42392c;
|
||||
--air-primitive-neutral-12: #201c12;
|
||||
|
||||
/* Private primitives: primary — terracota / siena */
|
||||
--air-primitive-primary-1: #fdf8f5;
|
||||
--air-primitive-primary-2: #faede6;
|
||||
--air-primitive-primary-3: #f5d6c5;
|
||||
--air-primitive-primary-4: #ecb89a;
|
||||
--air-primitive-primary-5: #e0956f;
|
||||
--air-primitive-primary-6: #d07248;
|
||||
--air-primitive-primary-7: #bc5528;
|
||||
--air-primitive-primary-8: #a44219;
|
||||
--air-primitive-primary-9: #c44b18;
|
||||
--air-primitive-primary-10: #ab3f14;
|
||||
--air-primitive-primary-11: #8c3210;
|
||||
--air-primitive-primary-12: #431608;
|
||||
|
||||
/* Private primitives: info — pizarra azul verdosa */
|
||||
--air-primitive-info-1: #f5f9f9;
|
||||
--air-primitive-info-2: #e8f3f2;
|
||||
--air-primitive-info-3: #cfe8e5;
|
||||
--air-primitive-info-4: #b2dad6;
|
||||
--air-primitive-info-5: #91c9c4;
|
||||
--air-primitive-info-6: #6eb5ae;
|
||||
--air-primitive-info-7: #4d9f97;
|
||||
--air-primitive-info-8: #368881;
|
||||
--air-primitive-info-9: #2a7a72;
|
||||
--air-primitive-info-10: #226962;
|
||||
--air-primitive-info-11: #1a504b;
|
||||
--air-primitive-info-12: #0c2825;
|
||||
|
||||
/* Private primitives: success — verde salvia / oliva */
|
||||
--air-primitive-success-1: #f7faf3;
|
||||
--air-primitive-success-2: #edf4e3;
|
||||
--air-primitive-success-3: #d8eac6;
|
||||
--air-primitive-success-4: #bedba5;
|
||||
--air-primitive-success-5: #a1ca83;
|
||||
--air-primitive-success-6: #82b760;
|
||||
--air-primitive-success-7: #62a240;
|
||||
--air-primitive-success-8: #4a8b2a;
|
||||
--air-primitive-success-9: #3a781e;
|
||||
--air-primitive-success-10: #316818;
|
||||
--air-primitive-success-11: #255013;
|
||||
--air-primitive-success-12: #12280a;
|
||||
|
||||
/* Private primitives: warning — ocre / ámbar dorado */
|
||||
--air-primitive-warning-1: #fffaf2;
|
||||
--air-primitive-warning-2: #fff3de;
|
||||
--air-primitive-warning-3: #ffe5b8;
|
||||
--air-primitive-warning-4: #fed28c;
|
||||
--air-primitive-warning-5: #f8ba60;
|
||||
--air-primitive-warning-6: #eca03a;
|
||||
--air-primitive-warning-7: #d88620;
|
||||
--air-primitive-warning-8: #c26e12;
|
||||
--air-primitive-warning-9: #d6821e;
|
||||
--air-primitive-warning-10: #be7318;
|
||||
--air-primitive-warning-11: #9a5d12;
|
||||
--air-primitive-warning-12: #4e2e08;
|
||||
|
||||
/* Private primitives: danger — rojo ladrillo / arcilla */
|
||||
--air-primitive-danger-1: #fefcfb;
|
||||
--air-primitive-danger-2: #fff4f1;
|
||||
--air-primitive-danger-3: #fde0d8;
|
||||
--air-primitive-danger-4: #fbc8bb;
|
||||
--air-primitive-danger-5: #f8ac9b;
|
||||
--air-primitive-danger-6: #f48e79;
|
||||
--air-primitive-danger-7: #ea6f57;
|
||||
--air-primitive-danger-8: #db5340;
|
||||
--air-primitive-danger-9: #cf4430;
|
||||
--air-primitive-danger-10: #b83a27;
|
||||
--air-primitive-danger-11: #9a2e1f;
|
||||
--air-primitive-danger-12: #4d160e;
|
||||
|
||||
/* Semantic surfaces */
|
||||
--air-color-surface-default: var(--air-primitive-neutral-1);
|
||||
--air-color-surface-raised: var(--air-primitive-neutral-3);
|
||||
--air-color-surface-overlay: var(--air-primitive-neutral-4);
|
||||
--air-color-overlay: rgb(32 28 18 / 0.42);
|
||||
|
||||
/* Semantic content */
|
||||
--air-color-content-primary: var(--air-primitive-neutral-12);
|
||||
--air-color-content-secondary: var(--air-primitive-neutral-10);
|
||||
--air-color-content-disabled: var(--air-primitive-neutral-7);
|
||||
--air-color-content-on-solid: #ffffff;
|
||||
|
||||
/* Semantic borders */
|
||||
--air-color-border-subtle: var(--air-primitive-neutral-4);
|
||||
--air-color-border-default: var(--air-primitive-neutral-6);
|
||||
--air-color-border-strong: var(--air-primitive-neutral-8);
|
||||
--air-color-focus-ring: var(--air-primitive-primary-8);
|
||||
|
||||
/* Primary palette */
|
||||
--air-color-primary-track: var(--air-primitive-primary-1);
|
||||
--air-color-primary-element: var(--air-primitive-primary-3);
|
||||
--air-color-primary-hover: var(--air-primitive-primary-4);
|
||||
--air-color-primary-active: var(--air-primitive-primary-5);
|
||||
--air-color-primary-border: var(--air-primitive-primary-6);
|
||||
--air-color-primary-solid: var(--air-primitive-primary-9);
|
||||
--air-color-primary-solid-hover: var(--air-primitive-primary-10);
|
||||
--air-color-primary-text: var(--air-primitive-primary-11);
|
||||
--air-color-primary-contrast: var(--air-primitive-primary-12);
|
||||
|
||||
/* Neutral palette */
|
||||
--air-color-neutral-track: var(--air-primitive-neutral-1);
|
||||
--air-color-neutral-element: var(--air-primitive-neutral-3);
|
||||
--air-color-neutral-hover: var(--air-primitive-neutral-4);
|
||||
--air-color-neutral-active: var(--air-primitive-neutral-5);
|
||||
--air-color-neutral-border: var(--air-primitive-neutral-6);
|
||||
--air-color-neutral-solid: var(--air-primitive-neutral-9);
|
||||
--air-color-neutral-solid-hover: var(--air-primitive-neutral-10);
|
||||
--air-color-neutral-text: var(--air-primitive-neutral-11);
|
||||
--air-color-neutral-contrast: var(--air-primitive-neutral-12);
|
||||
|
||||
/* Success palette */
|
||||
--air-color-success-track: var(--air-primitive-success-1);
|
||||
--air-color-success-element: var(--air-primitive-success-3);
|
||||
--air-color-success-hover: var(--air-primitive-success-4);
|
||||
--air-color-success-active: var(--air-primitive-success-5);
|
||||
--air-color-success-border: var(--air-primitive-success-6);
|
||||
--air-color-success-solid: var(--air-primitive-success-9);
|
||||
--air-color-success-solid-hover: var(--air-primitive-success-10);
|
||||
--air-color-success-text: var(--air-primitive-success-11);
|
||||
--air-color-success-contrast: var(--air-primitive-success-12);
|
||||
|
||||
/* Warning palette */
|
||||
--air-color-warning-track: var(--air-primitive-warning-1);
|
||||
--air-color-warning-element: var(--air-primitive-warning-3);
|
||||
--air-color-warning-hover: var(--air-primitive-warning-4);
|
||||
--air-color-warning-active: var(--air-primitive-warning-5);
|
||||
--air-color-warning-border: var(--air-primitive-warning-6);
|
||||
--air-color-warning-solid: var(--air-primitive-warning-9);
|
||||
--air-color-warning-solid-hover: var(--air-primitive-warning-10);
|
||||
--air-color-warning-text: var(--air-primitive-warning-11);
|
||||
--air-color-warning-contrast: var(--air-primitive-warning-12);
|
||||
|
||||
/* Danger palette */
|
||||
--air-color-danger-track: var(--air-primitive-danger-1);
|
||||
--air-color-danger-element: var(--air-primitive-danger-3);
|
||||
--air-color-danger-hover: var(--air-primitive-danger-4);
|
||||
--air-color-danger-active: var(--air-primitive-danger-5);
|
||||
--air-color-danger-border: var(--air-primitive-danger-6);
|
||||
--air-color-danger-solid: var(--air-primitive-danger-9);
|
||||
--air-color-danger-solid-hover: var(--air-primitive-danger-10);
|
||||
--air-color-danger-text: var(--air-primitive-danger-11);
|
||||
--air-color-danger-contrast: var(--air-primitive-danger-12);
|
||||
|
||||
/* Info palette */
|
||||
--air-color-info-track: var(--air-primitive-info-1);
|
||||
--air-color-info-element: var(--air-primitive-info-3);
|
||||
--air-color-info-hover: var(--air-primitive-info-4);
|
||||
--air-color-info-active: var(--air-primitive-info-5);
|
||||
--air-color-info-border: var(--air-primitive-info-6);
|
||||
--air-color-info-solid: var(--air-primitive-info-9);
|
||||
--air-color-info-solid-hover: var(--air-primitive-info-10);
|
||||
--air-color-info-text: var(--air-primitive-info-11);
|
||||
--air-color-info-contrast: var(--air-primitive-info-12);
|
||||
|
||||
/* Shadows — calidez marrón en lugar de negro neutro */
|
||||
--air-shadow-0: none;
|
||||
--air-shadow-1: 0 1px 2px rgb(32 20 10 / 0.08), 0 0 0 1px rgb(32 20 10 / 0.03);
|
||||
--air-shadow-2: 0 4px 12px rgb(32 20 10 / 0.10), 0 1px 2px rgb(32 20 10 / 0.04);
|
||||
--air-shadow-3: 0 12px 32px rgb(32 20 10 / 0.14), 0 4px 8px rgb(32 20 10 / 0.06);
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
import type { AirTheme } from '../types.js';
|
||||
|
||||
// Earth solo sobreescribe los valores de transición JS que quiere personalizar.
|
||||
// Las clases CSS de estado (stateClass) son siempre las mismas — el tema las
|
||||
// personaliza únicamente via CSS bajo [data-theme='earth-light/dark'].
|
||||
|
||||
export const earthMotionPresets = {
|
||||
revelation: {
|
||||
enter: { duration: 220, easing: 'sineOut', opacity: true, distance: 6, stagger: 28 },
|
||||
exit: { duration: 160, easing: 'sineIn', opacity: true, scale: 0.94 }
|
||||
},
|
||||
context: {
|
||||
enter: { duration: 300, easing: 'sineOut', opacity: true, scale: 0.982, distance: 20 },
|
||||
exit: { duration: 200, easing: 'sineIn', opacity: true, scale: 0.97 }
|
||||
},
|
||||
expansion: {
|
||||
enter: { duration: 260, easing: 'sineOut', opacity: true },
|
||||
exit: { duration: 200, easing: 'sineIn', opacity: true }
|
||||
},
|
||||
destruction: {
|
||||
exit: { duration: 160, easing: 'sineIn', opacity: true, scale: 0.90 }
|
||||
}
|
||||
} satisfies AirTheme['motionPresets'];
|
||||
@ -0,0 +1,14 @@
|
||||
import type { AirTheme } from '../types.js';
|
||||
|
||||
export const earthSoundPresets = {
|
||||
completion: {
|
||||
enter: { layers: [{ src: '/sounds/ding.wav', volumeDb: -10 }] }
|
||||
},
|
||||
notification: {
|
||||
enter: { layers: [{ src: '/sounds/ding.wav', volumeDb: -16, playbackRate: 0.85 }] }
|
||||
},
|
||||
attention: {
|
||||
enter: { layers: [{ src: '/sounds/error.wav', volumeDb: -8 }] },
|
||||
exit: { layers: [{ src: '/sounds/error.wav', volumeDb: -14, playbackRate: 0.7 }] }
|
||||
}
|
||||
} satisfies AirTheme['soundPresets'];
|
||||
@ -1,5 +1,6 @@
|
||||
@import './motion.css';
|
||||
@import './fonts.css';
|
||||
@import './base/index.css';
|
||||
@import './earth/index.css';
|
||||
@import '../tokens/index.css';
|
||||
|
||||
|
||||
@ -0,0 +1,17 @@
|
||||
import type { SoundPresets } from '$uix/air/internal/sound/types';
|
||||
import type { MotionPresets } from '$uix/air/internal/motion/types';
|
||||
|
||||
/**
|
||||
* Definición completa de un tema — agrupa las tres dimensiones de personalidad:
|
||||
* - id: selector CSS (data-theme="...") — activa los tokens de color/sombra
|
||||
* - soundPresets: personalidad sonora (opcional, hereda del tema base si no se define)
|
||||
* - motionPresets: personalidad cinética (opcional, hereda del tema base si no se define)
|
||||
*
|
||||
* El cambio de tema en caliente es atómico: al actualizar el objeto AirTheme,
|
||||
* las tres dimensiones cambian de forma sincronizada.
|
||||
*/
|
||||
export interface AirTheme {
|
||||
id: string;
|
||||
soundPresets?: SoundPresets;
|
||||
motionPresets?: MotionPresets;
|
||||
}
|
||||
Binary file not shown.
Loading…
Reference in new issue