|
|
|
|
@ -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<string, PrefsDimension<TIntent, TEffective>>`.
|
|
|
|
|
`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<string, PrefsDimension<TIntent, TEffective>>;
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
`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<Locale>(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<PrefsIntent>;
|
|
|
|
|
effective(): PrefsEffective;
|
|
|
|
|
|
|
|
|
|
setIntent<K extends keyof PrefsIntent>(
|
|
|
|
|
key: K,
|
|
|
|
|
value: NonNullable<PrefsIntent[K]>
|
|
|
|
|
): PrefsSnapshot;
|
|
|
|
|
|
|
|
|
|
clearIntent<K extends keyof PrefsIntent>(key: K): PrefsSnapshot;
|
|
|
|
|
resetIntent(next?: PrefsIntent): PrefsSnapshot;
|
|
|
|
|
|
|
|
|
|
refreshEnvironment(next: PrefsEnvironment): PrefsSnapshot;
|
|
|
|
|
patchEnvironment(patch: Partial<PrefsEnvironment>): 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<PrefsIntent | null>;
|
|
|
|
|
save(intent: PrefsIntent): void | Promise<void>;
|
|
|
|
|
clear(): void | Promise<void>;
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
`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<PrefsEffective>;
|
|
|
|
|
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`.
|
|
|
|
|
|