Refresh prefs architecture docs

active-uix
dev 5 months ago
parent 607dfdbb48
commit 475ac77505

@ -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`

@ -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`.

@ -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<Density> {

@ -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 = {}

Loading…
Cancel
Save

Powered by TurnKey Linux.