@ -1,47 +1,79 @@
# Frontend
`Frontend` es la API publica del artefacto `fend` . Su capa actual es
`createActiveFrontend()` , porque mantiene preferencias reactivas y escribe el
resultado sobre el DOM mediante `adom.apply()` .
`Frontend` es la API publica del artefacto `fend` . Su responsabilidad es
mantener las preferencias globales de presentacion de la aplicacion y aplicar
su resultado al DOM mediante `adom.apply()` .
No es un sistema de componentes, no genera CSS y no decide la identidad visual
de la app. Es la capa que hace que el resto del framework pueda preguntar de
forma uniforme:
- que direccion debe usar la UI (`ltr` / `rtl` ),
- que tema esta activo,
- que modo visual esta resuelto (`light` / `dark` ),
- que densidad usa la interfaz,
- si debe reducir movimiento o sonido.
## API Principal
```ts
import { createActiveFrontend } from '$fend';
const frontend = createActiveFrontend({
const F rontend = createActiveFrontend({
locale: 'es-ES',
target: () => document.documentElement
target: () => document.documentElement,
theme: 'base',
mode: 'auto',
dir: 'auto',
density: 'normal'
});
frontend.setLocale('ar-EG');
frontend.getDir(); // rtl, si dir esta en auto
Frontend.setLocale('ar-EG');
Frontend.getDir(); // rtl, mientras dir siga en auto
Frontend.setDir('ltr');
Frontend.getDir(); // ltr, aunque el locale sea arabe
Frontend.clearDir();
Frontend.getDir(); // rtl, vuelve a derivar del locale
```
## Composicion via aapp
La capa es `Active` porque mantiene estado reactivo con Svelte 5 runes y puede
notificar cambios mediante `onPreferenceChange(...)` .
En la mayoria de apps no se construye `ActiveFrontend` directamente — lo
provee `createActiveApp(...)` con su `localeSource` ya enchufado a `App.Lang`
y reutilizando un `ActiveDom` compartido. Ver `$aapp/README.md` .
## Composicion Via App
En una aplicacion normal no se crea `ActiveFrontend` a mano. `createActiveApp`
lo construye, le pasa el `LocaleSource` de `App.Lang` y reutiliza el `ActiveDom`
compartido de la app:
```ts
const App = createActiveApp({ frontend: { theme: 'base', mode: 'auto' } });
App.Frontend.setTheme('forest');
```
import { createActiveApp } from '$aapp';
## Locale source
const App = createActiveApp({
lang: { schema, defaultLocale: 'es' },
frontend: {
theme: 'base',
mode: 'auto',
dir: 'auto',
density: 'normal'
}
});
`ActiveFrontend` consume el alias compartido `LocaleSource` de `$locale` :
App.setLocale('ar');
App.Frontend.getDir(); // rtl
```ts
import type { LocaleSource } from '$locale';
App.Frontend.setTheme('forest');
```
Asi un mismo objeto puede alimentar `Formats` y `Frontend` sin duplicar
contratos. El tipo local `FrontendLocaleSource` se mantiene como alias por
compatibilidad .
La regla importante es que `Lang` sigue siendo la fuente unica de locale. Si
`App.setLocale(...)` cambia el idioma, `Formats` y `Frontend` reaccionan desde
la misma fuente, sin duplicar estado .
## Valores Auto
## Contrato Auto / Manual
La dinamica es la misma que en `Formats` :
`fend` sigue la misma dinamica conceptual que `fmts` : un valor en `auto` se
deriva de otra fuente; un valor fijado por el usuario no se vuelve a pisar.
```ts
setX('auto'); // vuelve a derivar
@ -55,22 +87,40 @@ isXAuto(); // true si el usuario no fijo un valor explicito
| `mode` | `prefers-color-scheme` | `setMode('dark')` | `setMode('auto')` / `clearMode()` |
| `reducedMotion` | `prefers-reduced-motion` | `setReducedMotion(true)` | `setReducedMotion('auto')` / `clearReducedMotion()` |
Los valores `theme` , `density` y `reducedSound` son preferencias explicitas: no
dependen del locale ni del sistema operativo.
`theme` , `density` y `reducedSound` son preferencias explicitas. No cambian al
cambiar locale ni al cambiar preferencias del sistema.
## Locale Y Direction
`dir` es el caso mas sensible porque afecta layout, navegacion, tablas, iconos
direccionales y componentes de texto. Por defecto:
```ts
const Frontend = createActiveFrontend({ locale: 'es' });
Frontend.getDir(); // ltr
Frontend.setLocale('ar');
Frontend.getDir(); // rtl
```
Si el usuario fija manualmente una direccion, el locale deja de modificarla:
## Preferencias Persistibles
```ts
Frontend.setDir('ltr');
Frontend.setLocale('ar-EG');
Frontend.getDir(); // ltr
`fend/preferences.ts` declara las preferencias que otras capas pueden persistir:
`theme` , `mode` , `density` , `dir` , `reducedMotion` y `reducedSound` .
Frontend.clearDir();
Frontend.getDir(); // rtl
```
`fend` es dueño de cómo leer la intención del usuario (por ejemplo, persistir
`mode: 'auto'` cuando el modo está derivado). `aapp` solo conecta esas
preferencias con `Storage` .
Este patron evita una clase de bugs habitual: el sistema cambia de locale y
rompe una decision explicita del usuario.
## Salida DOM
`ActiveFrontend` no decide estilos por si mismo. Solo resuelve preferencias y
las aplica como atributos:
`ActiveFrontend` no aplica clases arbitrarias. Escribe atributos estables sobre
el target configurado, normalmente `document.documentElement` .
```html
< html
@ -83,4 +133,138 @@ las aplica como atributos:
>< / html >
```
La capa de CSS o componentes consume esos atributos.
La capa de CSS, design tokens o componentes consume esos atributos:
```css
:root[data-theme='forest'] {
--surface: #102018 ;
}
:root[data-density='compact'] {
--control-height: 2rem;
}
:root[dir='rtl'] .icon-next {
transform: scaleX(-1);
}
```
## Integracion Con Adom
`fend` usa un subconjunto de `ActiveDom` :
```ts
type FrontendDom = Pick< ActiveDom , ' apply ' > ;
```
Eso significa que `fend` no necesita conocer todos los helpers DOM, solo la
operacion declarativa de aplicar atributos. Si no se pasa `dom` , crea un
`ActiveDom` propio salvo que `applyDom: false` este activado.
```ts
const Frontend = createActiveFrontend({
applyDom: false
});
Frontend.setTheme('forest'); // actualiza estado, no toca el DOM
```
`createActiveApp(...)` siempre reutiliza `App.Dom` , por lo que la aplicacion
tiene una sola capa DOM global.
## Persistencia
`fend/preferences.ts` declara las preferencias persistibles:
- `theme`
- `mode`
- `density`
- `dir`
- `reducedMotion`
- `reducedSound`
`fend` es duenio de como leer la intencion del usuario. Por ejemplo, si `mode`
esta en auto, `readFrontendPreference(frontend, 'mode')` devuelve `'auto'` , no
el modo resuelto del sistema.
`aapp` conecta esas preferencias con `Storage` :
```ts
const App = createActiveApp({
storage: { adapter: localAdapter, namespace: 'app' },
frontend: {
theme: 'base',
persist: true
}
});
App.Frontend.setTheme('forest');
```
Tambien se puede persistir solo parte de la configuracion:
```ts
const App = createActiveApp({
storage: { adapter: localAdapter, namespace: 'app' },
frontend: {
persist: {
keys: ['theme', 'mode', 'density']
}
}
});
```
Y se puede enviar una preferencia a otro adapter, por ejemplo una cookie que el
servidor necesita leer:
```ts
const App = createActiveApp({
storage: { adapter: localAdapter, namespace: 'app' },
frontend: {
persist: {
overrides: {
theme: { adapter: cookieAdapter({ path: '/' }), namespace: false, raw: true }
}
}
}
});
```
## Eventos
`onPreferenceChange(...)` se dispara cuando cambia cualquier preferencia
relevante. `aapp` lo usa para persistencia, pero tambien sirve para paneles de
debug o integraciones de analytics.
```ts
const off = App.Frontend.onPreferenceChange(() => {
console.log({
theme: App.Frontend.getTheme(),
dir: App.Frontend.getDir(),
mode: App.Frontend.getMode()
});
});
off();
```
## SSR
En SSR no existe `document` , por lo que el target por defecto es `null` y no se
escribe nada en el DOM. El estado sigue siendo util para construir un snapshot
inicial o para hidratar preferencias desde cookies/storage server-side.
Para evitar flash visual, la recomendacion es resolver en el servidor los
valores que afectan first paint (`theme`, `mode` , `dir` ) y pasarlos a
`createActiveApp(...)` como opciones iniciales.
## Testing
Las garantias principales estan cubiertas por:
- `src/arts/fend/test/active-frontend.test.ts` : auto/manual, DOM attrs y OS
preferences.
- `src/arts/aapp/test/storage-integration.test.ts` : persistencia desde
`createActiveApp` .
- `src/arts/aapp/test/active-app.test.ts` : locale unico y `dir` reactivo solo
cuando el valor esta en auto.