diff --git a/continue.md b/continue.md index 6e9609179..fbb929cc1 100644 --- a/continue.md +++ b/continue.md @@ -38,6 +38,9 @@ Actualizacion 2026-05-14: - Contrato directo fuera de `ActiveUix` aclarado: `EngineSemantic` con visual activo necesita `dom` o `projector` y falla con `SemaConfigError` si faltan; `ADom` directo solo usa `disabledDom` cuando el caller lo pide. +- `src/arts/prefs/README.md` reescrito al modelo actual schema-based. Se + elimina la arquitectura historica de capabilities como guia principal y se + marca `themeDimension`/`densityDimension` como legacy/custom fuera de UIX. - Commits nuevos empujados: - `694eb5c5` — `Clarify prefs projection contract` - `223cdf9e` — `Align sema docs with channel ownership` diff --git a/src/arts/prefs/README.md b/src/arts/prefs/README.md index 9c3355f2a..d2934ebd2 100644 --- a/src/arts/prefs/README.md +++ b/src/arts/prefs/README.md @@ -1,791 +1,279 @@ # Prefs -`prefs` is the Active preference resolution module. It is part of the -core (`App.prefs`) and is generic over a user-defined `PrefsSchema = -Record>`. +`prefs` es el artefacto activo de preferencias. Su trabajo es resolver, de +forma generica, la relacion entre intencion de usuario, entorno detectado y +valor efectivo para un esquema declarado por la app. -## Handoff 2026-05-13 +No traduce, no formatea, no persiste por si mismo y no escribe el DOM salvo +cuando el composition root cablea explicitamente `createActivePrefsDomProjection(...)`. -`prefs` es la pieza que debe resolver las preferencias compartidas entre -`ActiveApp` y las capas consumidoras, no una bolsa generica duplicada por cada -capa. Queda decidido: +## Estado 2026-05-14 -- `prefs.language` alimenta `langs`; -- `prefs.locale` alimenta `format`; -- `prefs.direction` resuelve la direccion efectiva; -- `prefs.motion`, `prefs.sound` y `prefs.haptic` son preferencias - transversales de percepcion/interaccion; -- `createActivePrefsDomProjection(...)` proyecta esas preferencias al DOM - cuando el composition root le pasa un `ActiveDom`; -- `theme`, `mode` y `density` son visuales y pertenecen a Eidos, no al preset - core de `prefs` ni a `ActiveUix`. - -> **Heads-up:** the sections below describe the original four-layer -> design (capabilities + environment + intent → effective). The current -> implementation is **schema-based**: each preference is a -> `PrefsDimension` that owns its own validator, environment-fed -> resolver and (optionally) sibling-derived value. Built-in dimensions -> (`localeDimension`, `motionDimension`, …) live in -> `arts/prefs/dimensions/*` and the `standardPrefsDimensions(catalog)` -> preset composes the canonical set. The active surface exposes one -> slot per schema key with `.get()` / `.set()` / `.clear()` / -> `.onChange()` verbs: -> -> ```ts -> createActiveApp({ -> prefs: { -> schema: { -> ...standardPrefsDimensions({ languages, locales, currencies }), -> sidebarCollapsed: booleanDimension({ default: false }) -> } -> } -> }); -> -> App.prefs.locale.get(); -> App.prefs.locale.set('es-ES'); -> App.prefs.sidebarCollapsed.set(true); -> ``` -> -> The historical text below is kept for archival reference until this -> README is rewritten in full. - -## Core Rule - -`prefs` is not a Svelte store and must not depend on `svelte/store`. - -The core is a pure preference resolver: - -```txt -capabilities + environment + intent -> effective -``` - -The Svelte layer is only an adapter around that core: - -```txt -libs/prefs = pure contracts, validation and resolution -arts/prefs = engine + active rune adapter -active-app later = composition and wiring -``` - -No resolver may read `window`, `navigator`, cookies, headers, `localStorage`, -IndexedDB, Svelte stores, DOM APIs or framework services directly. Those are -adapters/ports. - -## Purpose - -`prefs` owns application preferences that influence translation, formatting and -UI presentation. - -It answers: - -- Which locale does the user explicitly want? -- Which locale did the server or browser detect? -- Which values does this application support? -- Which values are effective after validation and fallback? -- Which subset should be persisted as user intent? - -It does not translate, format, persist by itself, apply DOM changes, own -security policy, or become a generic settings bag. - -Boundary: - -```txt -Prefs = preference resolution -Langs = translation/catalog lookup -Format = number/date/currency/unit formatting -Prefs DOM projector = optional cross-modal DOM attrs -Eidos = visual theme/mode/density projection -Storage = persistence adapter -Bus = optional event publication bridge -Orca = optional orchestration bridge -``` - -## Conceptual Model - -There are four state layers plus defaults. - -```txt -capabilities = what the application can offer -environment = what the current request/browser/system suggests -intent = what the user explicitly selected -effective = what the system actually uses -defaults = final fallback inside capabilities -``` +Decisiones vigentes: -Rules: - -- `capabilities` is declared by application composition. -- `environment` can contain unsupported values. -- `intent` is sparse and stores only explicit user choices. -- `effective` is total, derived and always valid against capabilities. -- `defaults` are part of capabilities and must be valid. -- Only `intent` is persisted. -- `environment` is recalculated. -- `effective` is never persisted. +- `prefs.language` alimenta `langs`. +- `prefs.locale` alimenta `format`. +- `prefs.direction` resuelve direccion efectiva. +- `prefs.motion`, `prefs.sound` y `prefs.haptic` son preferencias + transversales de percepcion/interaccion. +- `createActivePrefsDomProjection(...)` proyecta solo `dir`, + `data-motion`, `data-sound` y `data-haptic`. +- `theme`, `mode` y `density` visuales pertenecen a `ActiveEidos`, no al + preset core de `prefs`, `ActiveApp` ni `ActiveUix`. +- `themeDimension(...)` y `densityDimension(...)` quedan como factories + legacy/custom para apps ajenas a UIX; no usarlas en shells UIX nuevas. -Resolution: +## Composition Rule -```txt -effective[key] = - valid(intent[key], capabilities) - ?? resolveFrom(environment, capabilities, key) - ?? capabilities.defaults[key] -``` +Solo los composition roots crean `ActivePrefs`: -If an old persisted intent becomes unsupported after an app update, it is not -deleted automatically. It is ignored for `effective` until capabilities allow it -again or the app explicitly migrates/clears it. +- `ActiveApp` crea o recibe `prefs`. +- `createActiveUix(...)` crea `prefs` cuando UIX arranca standalone. +- `attachActiveUix(app)` reutiliza `app.prefs`. -## Naming +Las capas consumidoras leen slots concretos o reciben vistas acotadas. No +deben crear otra instancia compensatoria de preferencias. -Use: - -```txt -capabilities -environment -intent -effective -defaults +```text +ActiveApp/createActiveUix -> ActivePrefs +langs -> prefs.language +format -> prefs.locale, currency, timezone, unitSystem +ActivePrefsDomProjection -> direction, motion, sound, haptic +ActiveEidos -> theme/mode/density visuales propios ``` -Avoid: +## Schema Model -```txt -allowed // sounds like permissions/security -settings // too broad; becomes a junk drawer -current // ambiguous -active // already overloaded in Active -chosen // ambiguous between intent and effective -formatLocale // duplicate source of truth -``` - -## Domain Types +La implementacion actual es schema-based: ```ts -export type PrefsLocale = string; -export type PrefsCurrency = string; -export type PrefsTimezone = string; - -export type PrefsUnitSystem = 'metric' | 'imperial'; -export type PrefsThemeIntent = 'light' | 'dark' | 'system'; -export type PrefsThemeEffective = 'light' | 'dark'; -export type PrefsDensity = 'compact' | 'comfortable' | 'spacious'; -export type PrefsMotion = 'allow' | 'reduce' | 'system'; -export type PrefsMotionEffective = 'allow' | 'reduce'; -export type PrefsSoundEffective = 'allow' | 'reduce'; -export type PrefsDirection = 'ltr' | 'rtl'; +type PrefsSchema = Record>; ``` -`system` can be a real intent for theme/motion. It means "the user explicitly -wants to follow the environment". It must not appear in `effective`. +Cada dimension declara: -## Capabilities +- `defaultValue`: valor de fallback. +- `validate(value)`: valida intencion de usuario. +- `resolve(intent, env)`: opcional; convierte intencion + entorno en valor + efectivo. +- `catalog()`: opcional; lista de valores seleccionables. -`capabilities` is the legal universe of user-selectable values. +El motor mantiene tres planos: -```ts -export interface PrefsCapabilities { - readonly languages: readonly PrefsLocale[]; - readonly locales: readonly PrefsLocale[]; - readonly currencies: readonly PrefsCurrency[]; - readonly unitSystems: readonly PrefsUnitSystem[]; - readonly themes: readonly PrefsThemeIntent[]; - readonly densities: readonly PrefsDensity[]; - readonly motions: readonly PrefsMotion[]; - readonly sounds: readonly PrefsSoundEffective[]; - readonly timezones?: readonly PrefsTimezone[]; - readonly defaults: PrefsEffective; -} +```text +intent = lo que el usuario eligio explicitamente +environment = lo que servidor/browser/sistema sugieren +effective = valor total que leen los consumidores ``` -The app composes capabilities. `prefs` does not discover them by importing other -modules. +Solo `intent` se persiste. `environment` se recalcula y `effective` se deriva. -Example future composition: +## Standard Preset + +`standardPrefsDimensions(catalog)` compone el preset transversal: ```ts -const capabilities: PrefsCapabilities = { - languages: intersect(appConfig.languages, Langs.availableLocales), - locales: intersect(appConfig.locales, Langs.availableLocales), - currencies: appConfig.currencies, - unitSystems: appConfig.unitSystems, - themes: ['light', 'dark', 'system'], - densities: ['compact', 'comfortable', 'spacious'], - motions: ['allow', 'reduce', 'system'], - sounds: ['allow', 'reduce'], - defaults: { - locale: 'es-ES', - language: 'es-ES', - currency: 'EUR', - timezone: 'Europe/Madrid', - unitSystem: 'metric', - theme: 'light', - density: 'comfortable', - motion: 'allow', - sound: 'allow', - direction: 'ltr' - } +const schema = { + ...standardPrefsDimensions({ + languages: ['es', 'en'], + locales: ['es-ES', 'en-US'], + currencies: ['EUR', 'USD'], + defaults: { + language: 'es', + locale: 'es-ES', + currency: 'EUR' + } + }), + sidebarCollapsed: booleanDimension({ default: false }) }; ``` -If `Langs` does not provide `es-MX` and the application requires user-selectable -locales to exist in `Langs`, then `es-MX` must not be in `capabilities.locales`. +Incluye: -## Environment - -`environment` is detected context. It is not user intent. - -```ts -export interface PrefsEnvironment { - readonly locales?: readonly PrefsLocale[]; - readonly timezone?: PrefsTimezone; - readonly currency?: PrefsCurrency; - readonly region?: string; - readonly unitSystem?: PrefsUnitSystem; - readonly colorScheme?: 'light' | 'dark'; - readonly reducedMotion?: boolean; - readonly source?: 'server' | 'browser' | 'mixed' | 'test'; -} +```text +language +locale +currency +timezone +unitSystem +motion +sound +haptic +direction ``` -Examples: +No incluye: -```txt -Accept-Language: es-MX,es;q=0.9,en;q=0.8 -navigator.languages: ['es-MX', 'es', 'en-US'] -Intl timezone: America/Mexico_City -matchMedia prefers-color-scheme: dark +```text +theme +mode +density ``` -Environment may contain values outside capabilities. That is useful diagnostic -information and should not be confused with selectable values. - -## Intent +Esos valores son visuales en UIX. Una shell debe pasarlos a `ActiveEidos` +mediante `theme`, `modeSource` y `densitySource`. -`intent` is what the user explicitly selected. +## Active Surface -It is sparse. Missing means "derive it". +`createActivePrefs({ schema })` devuelve una superficie reactiva con un slot +por dimension: ```ts -export interface PrefsIntent { - readonly language?: PrefsLocale; - readonly locale?: PrefsLocale; - readonly currency?: PrefsCurrency; - readonly timezone?: PrefsTimezone; - readonly unitSystem?: PrefsUnitSystem; - readonly theme?: PrefsThemeIntent; - readonly density?: PrefsDensity; - readonly motion?: PrefsMotion; - readonly sound?: PrefsSoundEffective; - readonly direction?: PrefsDirection; -} -``` - -Intent writes must validate against capabilities. +const prefs = createActivePrefs({ schema }); -```txt -setIntent('locale', 'es-ES') -> ok if capabilities.locales includes es-ES -setIntent('locale', 'es-MX') -> rejected if capabilities.locales excludes es-MX -setIntent('currency', 'MXN') -> rejected if capabilities.currencies excludes MXN +prefs.locale.get(); +prefs.locale.set('en-US'); +prefs.locale.clear(); +prefs.locale.onChange((locale) => {}); +prefs.locale.catalog(); ``` -Do not use `undefined` as a write command. Clearing intent must be explicit: +Tambien expone metodos genericos para adaptadores: ```ts -Prefs.setIntent('locale', 'es-ES'); -Prefs.clearIntent('locale'); -Prefs.resetIntent(); +prefs.setIntent('locale', 'en-US'); +prefs.clearIntent('locale'); +prefs.resetIntent(); +prefs.patchEnvironment({ reducedMotion: true }); +prefs.refreshEnvironment(nextEnvironment); +prefs.subscribe((event) => {}); +prefs.dispose(); ``` -## Effective - -`effective` is the only layer consumers should read. +Los servicios que reciben un `ActivePrefs` abierto y no conocen su schema en +tiempo de compilacion deben leer defensivamente: ```ts -export interface PrefsEffective { - readonly language: PrefsLocale; - readonly locale: PrefsLocale; - readonly currency: PrefsCurrency; - readonly timezone: PrefsTimezone; - readonly unitSystem: PrefsUnitSystem; - readonly theme: PrefsThemeEffective; - readonly density: PrefsDensity; - readonly motion: PrefsMotionEffective; - readonly sound: PrefsSoundEffective; - readonly direction: PrefsDirection; -} +const slot = readActivePrefsSlot(prefs, 'locale'); +const locale = slot?.get(); ``` -`effective` is total, readonly and always valid. +Si el slot no existe, el consumidor decide si puede degradar o debe lanzar su +propio error de configuracion. -```txt -Langs reads effective.language -Format reads effective.locale, currency, timezone, unitSystem -PrefsDomProjection reads effective.direction, motion, sound, haptic -Eidos owns visual theme/mode/density -``` - -`language` and `locale` are intentionally distinct. `language` drives -translations and writing direction; `locale` drives regional formatting. There -is no `formatLocale` alias: `Format` uses the effective locale that the app -allowed. - -## Snapshot - -```ts -export interface PrefsSnapshot { - readonly capabilities: PrefsCapabilities; - readonly environment: PrefsEnvironment; - readonly intent: PrefsIntent; - readonly effective: PrefsEffective; - readonly version: number; -} -``` - -Snapshots are serializable and immutable. Consumers must not mutate returned -objects. - -## Resolver - -The resolver is pure. - -```ts -export interface PrefsResolveInput { - readonly capabilities: PrefsCapabilities; - readonly environment: PrefsEnvironment; - readonly intent: PrefsIntent; -} - -export function resolvePrefs(input: PrefsResolveInput): PrefsEffective; -``` +## Environment -The resolver does not persist, emit events, read browser APIs or touch Svelte -state. It only returns data. +El entorno entra por adaptadores. Ninguna dimension lee `window`, cookies, +headers, `localStorage` o DOM directamente. -## Locale Matching +Adaptadores disponibles: -Locale matching should use canonical locale data, not string splitting. +- `detectServerEnvironment(input)` +- `detectBrowserEnvironment(overrides?)` +- `applyBrowserEnvironment(prefs, overrides?)` +- `watchBrowserEnvironment(prefs, overrides?)` -Recommended order: +Ejemplos de entorno: -```txt -1. exact match -2. language + script match -3. language + region match -4. language match -5. default locale +```text +Accept-Language -> language/locale candidates +Intl timezone -> timezone +matchMedia -> reducedMotion/colorScheme +navigator -> languages, reduced sound/haptics when available ``` -Example: +`colorScheme` puede existir en el entorno porque el browser lo expone, pero +UIX no lo convierte en `prefs.theme`; `ActiveEidos` puede leer el sistema por +su propia `modeSource`. -```txt -environment.locales = ['es-MX', 'en-US'] -capabilities.locales = ['es-ES', 'en-US'] -effective.locale = 'es-ES' -``` - -`environment.locales` keeps `es-MX`; `effective.locale` uses `es-ES`. +## DOM Projection -Pseudo-helper: +`ActivePrefs` no escribe el DOM por si mismo. Si la app quiere atributos +globales, cablea el proyector: ```ts -function safeLocale(tag: string): Intl.Locale | undefined { - try { - return new Intl.Locale(Intl.getCanonicalLocales(tag)[0]); - } catch { - return undefined; - } -} +const prefsProjection = createActivePrefsDomProjection({ + prefs: App.prefs, + dom: App.dom +}); ``` -The exact implementation belongs in `libs/prefs/match-locale.ts`. - -## Currency +El proyector es idempotente, se suscribe a los slots disponibles y limpia los +atributos que gestiono en `dispose()`. -Currency is strict against capabilities. +Contrato de atributos: -```txt -intent.currency -> accepted only if in capabilities.currencies -environment.currency -> used only if in capabilities.currencies -default.currency -> must be in capabilities.currencies +```text +prefs.direction -> dir +prefs.motion -> data-motion +prefs.sound -> data-sound +prefs.haptic -> data-haptic ``` -Locale does not imply currency. A user can use `en-US` with `EUR` if the app -allows that currency. +No proyecta `data-theme`, `data-mode` ni `data-density`. -## Timezone +## Eidos Boundary -Timezone may be configured as either: - -```txt -open IANA validation -closed capabilities.timezones allowlist -``` - -Validation should canonicalize when possible: +Para una shell visual: ```ts -function normalizeTimezone(timezone: string): string | undefined { - try { - return new Intl.DateTimeFormat('en-US', { timeZone: timezone }) - .resolvedOptions() - .timeZone; - } catch { - return undefined; - } -} -``` - -If `capabilities.timezones` exists, the normalized timezone must be present in -that list. - -## Engine Contract - -`EnginePrefs` is the non-Svelte runtime. +const uix = createActiveUix({ langs, prefs: { schema } }); +const prefsProjection = createActivePrefsDomProjection({ + prefs: uix.prefs, + dom: uix.dom +}); -```ts -export interface EnginePrefs { - readonly kind: 'prefs'; - - snapshot(): PrefsSnapshot; - capabilities(): PrefsCapabilities; - environment(): PrefsEnvironment; - intent(): Readonly; - effective(): PrefsEffective; - - setIntent( - key: K, - value: NonNullable - ): PrefsSnapshot; - - clearIntent(key: K): PrefsSnapshot; - resetIntent(next?: PrefsIntent): PrefsSnapshot; - - refreshEnvironment(next: PrefsEnvironment): PrefsSnapshot; - patchEnvironment(patch: Partial): PrefsSnapshot; - setCapabilities(next: PrefsCapabilities): PrefsSnapshot; - - subscribe(handler: PrefsChangeHandler): PrefsUnsubscribe; - dispose(): void; -} +const eidos = ActiveEidos.create({ + theme: 'base', + modeSource, + densitySource, + applyDom: true +}); ``` -Writes recompute `effective` and notify subscribers only when the effective view -or relevant snapshot layer changes. - -`dispose()` must be idempotent. +Regla practica: -## Active Contract - -`ActivePrefs` is a Svelte rune adapter over `EnginePrefs`. - -It may use `$state`/`$derived` inside `.svelte.ts`, but the public contract is -still the Active interface. It must not expose `svelte/store` as the module -contract. - -```ts -export interface ActivePrefs extends EnginePrefs { - readonly state: { - readonly snapshot: PrefsSnapshot; - readonly effective: PrefsEffective; - readonly pending: boolean; - readonly lastError: unknown; - }; -} +```text +NO: uix.prefs.setIntent('theme', 'dark') +SI: modeSource notifica 'dark' a ActiveEidos ``` -The active layer must not invent different rules. All validation and resolution -comes from the core. +Si una app no UIX decide declarar una dimension visual propia en `prefs`, es +un contrato local de esa app. No debe filtrarse a `ActiveUix`, Soma, Sema ni +Morfo. -## Adapters And Ports +## Storage -External IO lives behind ports. +`createPrefsStorageBridge(...)` persiste intenciones, no valores efectivos: ```ts -export interface PrefsEnvironmentDetector { - detect(): PrefsEnvironment; -} - -export interface PrefsIntentStorage { - load(): PrefsIntent | null | Promise; - save(intent: PrefsIntent): void | Promise; - clear(): void | Promise; -} -``` - -`prefs` should not import `storage`; a bridge connects them later: - -```ts -createPrefsStorageBridge({ +const bridge = createPrefsStorageBridge({ prefs, storage, key: 'active:prefs' }); ``` -Storage bridge rules: - -- Persist only `intent`. -- Do not persist `environment`. -- Do not persist `effective`. -- Do not persist secrets. -- Do not write during initial hydrate unless explicitly configured. -- Storage failures must not corrupt in-memory preferences. +Reglas: -## Consumer Ports +- Persistir solo `intent`. +- No persistir `environment`. +- No persistir `effective`. +- No escribir durante hydrate salvo configuracion explicita. +- Un fallo de storage no debe corromper preferencias en memoria. -Consumers should define ports in their own modules. `prefs` can satisfy those -ports, but it should not know them. - -Example future ports: - -```ts -// langs -export interface LangsPrefsPort { - readonly locale: PrefsLocale; - subscribe(handler: (locale: PrefsLocale) => void): PrefsUnsubscribe; -} - -// format -export interface FormatPrefsPort { - readonly locale: PrefsLocale; - readonly currency: PrefsCurrency; - readonly timezone: PrefsTimezone; - readonly unitSystem: PrefsUnitSystem; - subscribe(handler: (view: FormatPrefsView) => void): PrefsUnsubscribe; -} - -// active-uix / eidos -export interface UixPrefsPort { - readonly theme: PrefsThemeEffective; - readonly density: PrefsDensity; - readonly motion: PrefsMotionEffective; - readonly sound: PrefsSoundEffective; - readonly direction: PrefsDirection; - subscribe(handler: (view: UixPrefsView) => void): PrefsUnsubscribe; -} -``` +## Errors -Consumers build filtered views/proxies from `App.prefs` or `uix.prefs`. -`prefs` itself remains unaware of `langs`, `format`, `uix`, `eidos` or DOM. +Los errores publicos usan la familia `prefs::*`: -## Events +- `prefs::unknown_dimension` +- `prefs::intent_invalid` +- `prefs::reserved_key` +- `prefs::disposed` -The engine exposes local subscriptions. It does not import `buss`. +Ejemplo conocido: -```ts -export interface PrefsChangeEvent { - readonly previous: PrefsSnapshot; - readonly next: PrefsSnapshot; - readonly effectiveDiff: Partial; - readonly cause: - | 'intent:set' - | 'intent:clear' - | 'intent:reset' - | 'environment:refresh' - | 'capabilities:set' - | 'hydrate'; -} +```text +prefs::unknown_dimension: [prefs] no such dimension in schema: theme ``` -Later, `active-app` can bridge this to `Bus`: - -```ts -Prefs.subscribe((event) => { - App.bus.publish(PREFS_EVENT_CHANGED, event); -}); -``` +En UIX ese error normalmente significa que una shell intento escribir +`prefs.theme`. La correccion es pasar el modo visual a `ActiveEidos`. -All event names must be constants. +## Tests -## Server And Browser Initialization - -Server flow: - -```txt -1. app builds capabilities -2. server detector reads request/session/profile/cookies/Accept-Language -3. optional server storage loads persisted intent -4. engine resolves snapshot synchronously -5. snapshot is serialized into SSR payload +```bash +npx vitest run src/arts/prefs/test/engine-prefs.test.ts +npx vitest run src/arts/prefs/test/active-prefs.svelte.test.ts +npx vitest run src/arts/prefs/test/dom-projection.test.ts ``` - -Browser flow: - -```txt -1. hydrate from server snapshot -2. browser detector reads navigator/matchMedia/Intl APIs -3. optional client storage loads persisted intent -4. engine re-resolves -5. emit change only if effective values changed -``` - -Hydration rule: - -```txt -server snapshot wins first paint -browser detection may refine after hydration -browser detection must not override explicit intent -``` - -Avoid first-paint theme flashes at the UI shell boundary, not inside `prefs`: -an inline head script may apply early `data-theme`/`data-mode` before Svelte -mounts, then `ActiveEidos` adopts the same visual state during hydration. - -## Validation - -Validation is field-specific: - -```txt -locale -> capability membership, with environment matching fallback -currency -> strict capability membership -timezone -> IANA validation, optionally capability membership -unitSystem -> strict capability membership -motion -> strict capability membership; system allowed as intent -sound -> strict capability membership -haptic -> strict capability membership -``` - -`theme` and `density` factories still exist for apps that declare their own -custom preference schema, but they are no longer part of the standard core -preset. Eidos owns visual `theme` / `mode` / `density`. - -## DOM projection - -`ActivePrefs` is pure state and never writes the DOM by itself. When an app -wants global preference attrs it wires the explicit projector: - -```ts -import { createActivePrefsDomProjection } from '$prefs'; - -const prefsProjection = createActivePrefsDomProjection({ - prefs: App.prefs, - dom: App.dom -}); -``` - -`ActiveUix` follows the same rule: it exposes `uix.prefs` and `uix.dom`, but -does not create this projector automatically. A UI shell that wants global attrs -must wire it explicitly: - -```ts -const uix = createActiveUix({ langs, prefs: { schema } }); -const prefsProjection = createActivePrefsDomProjection({ - prefs: uix.prefs, - dom: uix.dom -}); -``` - -The projector only owns cross-modal attrs: - -```txt -prefs.direction -> dir -prefs.motion -> data-motion -prefs.sound -> data-sound -prefs.haptic -> data-haptic -``` - -Visual attrs belong to `ActiveEidos`: - -```txt -eidos.theme -> data-theme -eidos.mode -> data-mode -eidos.density -> data-density -``` - -Do not declare or write `prefs.theme` for UIX. A docs shell or app theme toggle -should pass visual mode/theme/density to `ActiveEidos` (`modeSource`, -`densitySource`, `theme`) and leave `prefs` for cross-modal preferences. - -Invalid user writes should return/throw structured `prefs` errors when the -`errs` module is available. Until then, tests should assert stable error names. - -## Stability - -The engine should avoid unnecessary reactions: - -- Emit only when snapshot/effective data actually changes. -- Provide field-level diffs. -- Allow consumer ports to subscribe to focused views. -- Return readonly snapshots. - -The resolver may create new objects internally, but subscribers should not be -notified if values are shallow-equal. - -## Future File Layout - -```txt -src/ - libs/ - prefs/ - index.ts - consts.ts - types.ts - ports.ts - guards.ts - match-locale.ts - validate-intent.ts - resolve-prefs.ts - - arts/ - prefs/ - index.ts - README.md - engine-prefs.ts - active-prefs.svelte.ts - adapters/ - browser-environment.ts - server-environment.ts - storage-bridge.ts - test/ -``` - -## Implementation Agenda - -1. Implement `libs/prefs` domain types and constants. -2. Implement `match-locale.ts` with exact/script/region/language/default order. -3. Implement field validation and intent sanitization. -4. Implement pure `resolvePrefs()`. -5. Implement `createEnginePrefs()` with local subscriptions and diffs. -6. Implement `active-prefs.svelte.ts` as a rune adapter over the engine. -7. Implement environment detector adapters. -8. Implement optional storage bridge that persists only `intent`. -9. Add tests for locale fallback, currency strictness, timezone normalization, - explicit clear, theme `system`, hydration and dispose. -10. Only after that, integrate through `active-app` as `App.prefs`. - -## Test Agenda - -Required tests before integration: - -- Default snapshot is valid against capabilities. -- Intent writes reject unsupported values. -- `clearIntent()` restores derived behavior without storing `undefined`. -- Only `intent` is persisted. -- `environment` may contain unsupported values. -- `effective` never contains unsupported values. -- Locale matching handles exact, language/script, language/region and language. -- Currency never falls back by locale unless the app explicitly implements that - policy in the resolver. -- Timezone is canonicalized or rejected according to policy. -- `theme: 'system'` persists as intent but resolves to `light` or `dark`. -- Browser detection cannot override explicit intent. -- `setCapabilities()` revalidates without deleting old intent. -- Subscribers receive diffs and can unsubscribe. -- `dispose()` is idempotent. - -## IA Agents - -When working on `prefs`: - -- Do not wire it into `active-app` unless explicitly requested. -- Do not import `svelte/store` in the core. -- Do not import `langs`, `format`, `frontend`, `storage`, `buss`, `orca` or - `active-app` from `libs/prefs`. -- Do not persist `environment` or `effective`. -- Do not create `formatLocale`. -- Do not let `Langs` own preferences. -- Do not let `Format` keep parallel preference state. -- Do not add arbitrary settings. -- Keep IO behind ports/adapters. -- Add tests before another artifact consumes `prefs`. diff --git a/src/arts/prefs/dimensions/density.ts b/src/arts/prefs/dimensions/density.ts index 1f9a7aed8..c31f1da93 100644 --- a/src/arts/prefs/dimensions/density.ts +++ b/src/arts/prefs/dimensions/density.ts @@ -6,6 +6,11 @@ export interface DensityDimensionOptions { readonly default?: Density; } +/** + * @deprecated UIX visual density belongs to `ActiveEidos`, not to the + * standard prefs preset. Keep this only for legacy/custom app schemas + * outside UIX. + */ export function densityDimension( options: DensityDimensionOptions = {} ): PrefsDimension { diff --git a/src/arts/prefs/dimensions/theme.ts b/src/arts/prefs/dimensions/theme.ts index 30c3ac2af..ee7160ab9 100644 --- a/src/arts/prefs/dimensions/theme.ts +++ b/src/arts/prefs/dimensions/theme.ts @@ -21,10 +21,11 @@ export interface ThemeDimensionOptions { } /** - * Built-in theme dimension. `TIntent` includes `'system'`, `TEffective` - * does not — the dimension's `resolve` folds `'system'` (and the no- - * intent case) into a concrete `'light' | 'dark'` using - * `env.colorScheme`. + * @deprecated UIX visual mode belongs to `ActiveEidos`, not to the standard + * prefs preset. Keep this only for legacy/custom app schemas outside UIX. + * + * `TIntent` includes `'system'`, `TEffective` does not: `resolve` folds + * `'system'` into a concrete `'light' | 'dark'` using `env.colorScheme`. */ export function themeDimension( options: ThemeDimensionOptions = {}