Expand frontend docs and app integration tests

master
dev 5 months ago
parent 16d4578aa1
commit 8e74c5f309

@ -2,7 +2,7 @@
Estado al cierre: Estado al cierre:
- Suite unitaria verde: `npm test` -> 103 archivos, 1189 tests. - Suite unitaria verde: `npm test` -> 103 archivos, 1191 tests.
- Typecheck verde: `npm run check` -> 0 errores, 0 warnings. - Typecheck verde: `npm run check` -> 0 errores, 0 warnings.
- `fmts` verde: `npx vitest run src/arts/fmts` -> 14 archivos, 39 tests. - `fmts` verde: `npx vitest run src/arts/fmts` -> 14 archivos, 39 tests.
- `conn` verde: `npx vitest run src/arts/conn` -> 5 archivos, 28 tests. - `conn` verde: `npx vitest run src/arts/conn` -> 5 archivos, 28 tests.
@ -64,6 +64,9 @@ Estado al cierre:
- `src/arts/fmts/README.md` ampliado con guia de uso, `LocaleSource`, contrato auto/manual, submodulos, listeners, integracion con `aapp` y tests. - `src/arts/fmts/README.md` ampliado con guia de uso, `LocaleSource`, contrato auto/manual, submodulos, listeners, integracion con `aapp` y tests.
- `fmts` redujo boilerplate activo con helpers `readFrom` / `writeTo` y los engines comparten directamente las funciones de `createFormatsLocaleState`; APIs públicas sin cambios. - `fmts` redujo boilerplate activo con helpers `readFrom` / `writeTo` y los engines comparten directamente las funciones de `createFormatsLocaleState`; APIs públicas sin cambios.
- `src/web/routes/temp/` corregido: scripts tipados, warnings Svelte eliminados y compatible con `npm run check`. - `src/web/routes/temp/` corregido: scripts tipados, warnings Svelte eliminados y compatible con `npm run check`.
- `aapp` integration reforzado: `Connections` creadas antes de `Sess` reciben eventos posteriores de sesion y cierran en revoke.
- `aapp` composition reforzado: `Frontend.dir` reacciona a locale solo mientras esta en `auto`; los overrides manuales no se pisan.
- `src/arts/fend/README.md` ampliado: API, composicion via App, contrato auto/manual, locale/dir, salida DOM, integracion `adom`, persistencia, SSR y tests.
- No commitear `.idea/`, `.claude/` ni `.opencode/`. - No commitear `.idea/`, `.claude/` ni `.opencode/`.
Pendiente para manana: Pendiente para manana:

@ -77,6 +77,28 @@ describe('createActiveApp — composition', () => {
App.dispose(); App.dispose();
}); });
it('keeps Frontend dir reactive only while the preference is auto', () => {
const App = createActiveApp({
lang: { schema, defaultLocale: 'es' },
logger: { level: LogLevel.NONE, transports: [] }
});
expect(App.Frontend.isDirAuto()).toBe(true);
App.setLocale('ar');
expect(App.Frontend.getDir()).toBe('rtl');
App.Frontend.setDir('ltr');
App.setLocale('ar-EG');
expect(App.Frontend.isDirAuto()).toBe(false);
expect(App.Frontend.getDir()).toBe('ltr');
App.Frontend.clearDir();
expect(App.Frontend.isDirAuto()).toBe(true);
expect(App.Frontend.getDir()).toBe('rtl');
App.dispose();
});
it('honors BCP 47 resolution end-to-end', () => { it('honors BCP 47 resolution end-to-end', () => {
const App = createActiveApp({ const App = createActiveApp({
lang: { schema, defaultLocale: 'es' }, lang: { schema, defaultLocale: 'es' },

@ -9,7 +9,7 @@ import {
type CacheEvent, type CacheEvent,
type ResolvedScopeValues type ResolvedScopeValues
} from '$cach'; } from '$cach';
import { createMockTransport } from '$conn'; import { CONNECTION_STATE_CLOSED, CONNECTION_STATE_OPEN, createMockTransport } from '$conn';
import { HTTP_CONTENT_TYPE_JSON, HTTP_HEADER_CONTENT_TYPE } from '$libs/http'; import { HTTP_CONTENT_TYPE_JSON, HTTP_HEADER_CONTENT_TYPE } from '$libs/http';
import type { StandardSchemaV1 } from '$libs/standard-schema'; import type { StandardSchemaV1 } from '$libs/standard-schema';
import { import {
@ -326,6 +326,38 @@ describe('ActiveApp — total ecosystem integration', () => {
permissionEngine.dispose(); permissionEngine.dispose();
} }
}); });
it('bridges late-created sessions to existing App connection registries', async () => {
const App = createActiveApp({
logger: { level: LogLevel.NONE, transports: [] }
});
try {
const Connections = App.createActiveConnections();
const transport = createMockTransport();
const Updates = Connections.createConnection(CONNECTION_NAME, {
transport,
heartbeat: false,
reconnect: false,
session: { enabled: true }
});
await Updates.connect();
expect(Updates.state).toBe(CONNECTION_STATE_OPEN);
const Sess = App.createActiveSession<DemoUser>();
Sess.adoptServer({
user: { id: ACTOR_ID, email: 'ada@acme.test' },
issuedAt: 1,
expiresAt: Date.now() + 60_000
});
await Sess.revoke();
expect(Updates.state).toBe(CONNECTION_STATE_CLOSED);
} finally {
App.dispose();
}
});
}); });
function createPermissionEngine() { function createPermissionEngine() {

@ -1,47 +1,79 @@
# Frontend # Frontend
`Frontend` es la API publica del artefacto `fend`. Su capa actual es `Frontend` es la API publica del artefacto `fend`. Su responsabilidad es
`createActiveFrontend()`, porque mantiene preferencias reactivas y escribe el mantener las preferencias globales de presentacion de la aplicacion y aplicar
resultado sobre el DOM mediante `adom.apply()`. 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 ```ts
import { createActiveFrontend } from '$fend'; import { createActiveFrontend } from '$fend';
const frontend = createActiveFrontend({ const Frontend = createActiveFrontend({
locale: 'es-ES', locale: 'es-ES',
target: () => document.documentElement target: () => document.documentElement,
theme: 'base',
mode: 'auto',
dir: 'auto',
density: 'normal'
}); });
frontend.setLocale('ar-EG'); Frontend.setLocale('ar-EG');
frontend.getDir(); // rtl, si dir esta en auto 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 ## Composicion Via App
provee `createActiveApp(...)` con su `localeSource` ya enchufado a `App.Lang`
y reutilizando un `ActiveDom` compartido. Ver `$aapp/README.md`. 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 ```ts
const App = createActiveApp({ frontend: { theme: 'base', mode: 'auto' } }); import { createActiveApp } from '$aapp';
App.Frontend.setTheme('forest');
```
## 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 App.Frontend.setTheme('forest');
import type { LocaleSource } from '$locale';
``` ```
Asi un mismo objeto puede alimentar `Formats` y `Frontend` sin duplicar La regla importante es que `Lang` sigue siendo la fuente unica de locale. Si
contratos. El tipo local `FrontendLocaleSource` se mantiene como alias por `App.setLocale(...)` cambia el idioma, `Formats` y `Frontend` reaccionan desde
compatibilidad. 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 ```ts
setX('auto'); // vuelve a derivar 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()` | | `mode` | `prefers-color-scheme` | `setMode('dark')` | `setMode('auto')` / `clearMode()` |
| `reducedMotion` | `prefers-reduced-motion` | `setReducedMotion(true)` | `setReducedMotion('auto')` / `clearReducedMotion()` | | `reducedMotion` | `prefers-reduced-motion` | `setReducedMotion(true)` | `setReducedMotion('auto')` / `clearReducedMotion()` |
Los valores `theme`, `density` y `reducedSound` son preferencias explicitas: no `theme`, `density` y `reducedSound` son preferencias explicitas. No cambian al
dependen del locale ni del sistema operativo. cambiar locale ni al cambiar preferencias del sistema.
## Preferencias Persistibles ## Locale Y Direction
`fend/preferences.ts` declara las preferencias que otras capas pueden persistir: `dir` es el caso mas sensible porque afecta layout, navegacion, tablas, iconos
`theme`, `mode`, `density`, `dir`, `reducedMotion` y `reducedSound`. direccionales y componentes de texto. Por defecto:
`fend` es dueño de cómo leer la intención del usuario (por ejemplo, persistir ```ts
`mode: 'auto'` cuando el modo está derivado). `aapp` solo conecta esas const Frontend = createActiveFrontend({ locale: 'es' });
preferencias con `Storage`.
Frontend.getDir(); // ltr
Frontend.setLocale('ar');
Frontend.getDir(); // rtl
```
Si el usuario fija manualmente una direccion, el locale deja de modificarla:
```ts
Frontend.setDir('ltr');
Frontend.setLocale('ar-EG');
Frontend.getDir(); // ltr
Frontend.clearDir();
Frontend.getDir(); // rtl
```
Este patron evita una clase de bugs habitual: el sistema cambia de locale y
rompe una decision explicita del usuario.
## Salida DOM ## Salida DOM
`ActiveFrontend` no decide estilos por si mismo. Solo resuelve preferencias y `ActiveFrontend` no aplica clases arbitrarias. Escribe atributos estables sobre
las aplica como atributos: el target configurado, normalmente `document.documentElement`.
```html ```html
<html <html
@ -83,4 +133,138 @@ las aplica como atributos:
></html> ></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.

Loading…
Cancel
Save

Powered by TurnKey Linux.