From 8e74c5f3096c0e2a381b885b21c8515810b412fe Mon Sep 17 00:00:00 2001 From: dev Date: Wed, 29 Apr 2026 22:40:13 +0200 Subject: [PATCH] Expand frontend docs and app integration tests --- NEXT_STEPS.md | 5 +- src/arts/aapp/test/active-app.test.ts | 22 ++ .../aapp/test/ecosystem.integration.test.ts | 34 ++- src/arts/fend/README.md | 252 +++++++++++++++--- 4 files changed, 277 insertions(+), 36 deletions(-) diff --git a/NEXT_STEPS.md b/NEXT_STEPS.md index af016dc..ee68b6e 100644 --- a/NEXT_STEPS.md +++ b/NEXT_STEPS.md @@ -2,7 +2,7 @@ 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. - `fmts` verde: `npx vitest run src/arts/fmts` -> 14 archivos, 39 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. - `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`. +- `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/`. Pendiente para manana: diff --git a/src/arts/aapp/test/active-app.test.ts b/src/arts/aapp/test/active-app.test.ts index b7ed878..54a6a8b 100644 --- a/src/arts/aapp/test/active-app.test.ts +++ b/src/arts/aapp/test/active-app.test.ts @@ -77,6 +77,28 @@ describe('createActiveApp — composition', () => { 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', () => { const App = createActiveApp({ lang: { schema, defaultLocale: 'es' }, diff --git a/src/arts/aapp/test/ecosystem.integration.test.ts b/src/arts/aapp/test/ecosystem.integration.test.ts index 2b0bdd5..b0e3dc5 100644 --- a/src/arts/aapp/test/ecosystem.integration.test.ts +++ b/src/arts/aapp/test/ecosystem.integration.test.ts @@ -9,7 +9,7 @@ import { type CacheEvent, type ResolvedScopeValues } 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 type { StandardSchemaV1 } from '$libs/standard-schema'; import { @@ -326,6 +326,38 @@ describe('ActiveApp — total ecosystem integration', () => { 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(); + 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() { diff --git a/src/arts/fend/README.md b/src/arts/fend/README.md index e18cf71..5fe042e 100644 --- a/src/arts/fend/README.md +++ b/src/arts/fend/README.md @@ -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 Frontend = 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. -## Preferencias Persistibles +## Locale Y Direction -`fend/preferences.ts` declara las preferencias que otras capas pueden persistir: -`theme`, `mode`, `density`, `dir`, `reducedMotion` y `reducedSound`. +`dir` es el caso mas sensible porque afecta layout, navegacion, tablas, iconos +direccionales y componentes de texto. Por defecto: -`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`. +```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: + +```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 -`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 ``` -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; +``` + +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.