You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/src/arts/prefs/README.md

7.7 KiB

Prefs

prefs is the active preferences artifact. Its job is to resolve, generically, the relationship between user intent, detected environment and effective value for a schema declared by the app.

It does not translate, does not format, does not persist by itself, and does not write the DOM except when the composition root explicitly wires createActivePrefsDomProjection(...).

Status 2026-05-14

Current decisions:

  • prefs.language feeds langs.
  • prefs.locale feeds format.
  • prefs.direction resolves the effective direction.
  • prefs.motion, prefs.sound and prefs.haptic are cross-cutting perception/interaction preferences.
  • createActivePrefsDomProjection(...) projects only dir, lang, data-motion, data-sound and data-haptic. lang travels with dir because they answer the same question about the document and the browser reads BOTH from the DOM — fonts, hyphenation, quote glyphs and every screen reader take the language from there. A schema without the language dimension yields no slot, so nothing is projected and an app that owns lang server-side (i18n by routing) is untouched.
  • Visual theme, mode and density belong to ActiveEidos, not to the core preset of prefs, ActiveApp or ActiveUix.
  • There is no themeDimension(...) or densityDimension(...) in the public prefs catalog: if an app needs custom dimensions, it uses the generic primitives (enumDimension, stringDimension, etc.) or its own PrefsDimension.

Composition Rule

Only composition roots create ActivePrefs:

  • ActiveApp creates or receives prefs.
  • createActiveUix(...) creates prefs when UIX boots standalone.
  • attachActiveUix(app) reuses app.prefs.

Consuming layers read specific slots or receive scoped views. They must not create another compensatory preferences instance.

ActiveApp/createActiveUix -> ActivePrefs
langs                     -> prefs.language
format                    -> prefs.locale, currency, timezone, unitSystem
ActivePrefsDomProjection  -> direction, language, motion, sound, haptic
ActiveEidos               -> its own visual theme/mode/density

Schema Model

The current implementation is schema-based:

type PrefsSchema = Record<string, PrefsDimension<TIntent, TEffective>>;

Each dimension declares:

  • defaultValue: fallback value.
  • validate(value): validates user intent.
  • resolve(intent, env): optional; turns intent + environment into the effective value.
  • catalog(): optional; list of selectable values.

The engine keeps three planes:

intent      = what the user explicitly chose
environment = what the server/browser/system suggest
effective   = the final value consumers read

Only intent is persisted. environment is recomputed and effective is derived.

Standard Preset

standardPrefsDimensions(catalog) composes the cross-cutting preset:

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 })
};

Includes:

language
locale
currency
timezone
unitSystem
motion
sound
haptic
direction

Does not include:

theme
mode
density

Those values are visual in UIX. A shell must pass them to ActiveEidos via theme, modeSource and densitySource.

Active Surface

createActivePrefs({ schema }) returns a reactive surface with one slot per dimension:

const prefs = createActivePrefs({ schema });

prefs.locale.get();
prefs.locale.set('en-US');
prefs.locale.clear();
prefs.locale.onChange((locale) => {});
prefs.locale.catalog();

It also exposes generic methods for adapters:

prefs.setIntent('locale', 'en-US');
prefs.clearIntent('locale');
prefs.resetIntent();
prefs.patchEnvironment({ reducedMotion: true });
prefs.refreshEnvironment(nextEnvironment);
prefs.subscribe((event) => {});
prefs.dispose();

Services that receive an open ActivePrefs and do not know its schema at compile time should read defensively:

const slot = readActivePrefsSlot<Locale>(prefs, 'locale');
const locale = slot?.get();

If the slot does not exist, the consumer decides whether it can degrade or must throw its own configuration error.

Environment

The environment comes in through adapters. No dimension reads window, cookies, headers, localStorage or the DOM directly.

Available adapters:

  • detectServerEnvironment(input)
  • detectBrowserEnvironment(overrides?)
  • applyBrowserEnvironment(engine, overrides?)
  • watchBrowserEnvironment(apply, overrides?) — apply receives the environment patch ((patch) => void); overrides is only { matchMedia }

Environment examples:

Accept-Language -> language/locale candidates
Intl timezone   -> timezone
matchMedia      -> reducedMotion/colorScheme
navigator       -> languages, reduced sound/haptics when available

colorScheme can exist in the environment because the browser exposes it, but UIX does not turn it into prefs.theme; ActiveEidos can read the system through its own modeSource.

DOM Projection

ActivePrefs does not write the DOM by itself. If the app wants global attributes, it wires the projector:

const prefsProjection = createActivePrefsDomProjection({
	prefs: App.prefs,
	dom: App.dom
});

The projector is idempotent, subscribes to the available slots and clears the attributes it manages on dispose().

Attribute contract:

prefs.direction -> dir
prefs.language  -> lang
prefs.motion    -> data-motion
prefs.sound     -> data-sound
prefs.haptic    -> data-haptic

It does not project data-theme, data-mode or data-density.

The projected dir is the app-global half of the direction contract: the page declares its direction once at the root and every component inherits it. How an individual component obtains its own direction, and when it must assert one on its own element, is the other half — docs/canon/direction-contract.md.

Eidos Boundary

For a visual shell:

const uix = createActiveUix({ langs, prefs: { schema } });
const prefsProjection = createActivePrefsDomProjection({
	prefs: uix.prefs,
	dom: uix.dom
});

const eidos = ActiveEidos.create({
	theme: 'base',
	modeSource,
	densitySource,
	applyDom: true
});

Practical rule:

NO: uix.prefs.setIntent('theme', 'dark')
YES: modeSource notifies 'dark' to ActiveEidos

If a non-UIX app decides to declare its own visual dimension in prefs, that is a local contract of that app. It must not leak into ActiveUix, Soma, Sema or Morfo.

Storage

createPrefsStorageBridge(...) persists intents, not effective values:

const bridge = createPrefsStorageBridge({
	engine, // EnginePrefs
	storage, // PrefsIntentStorage
	onError: (error, op) => report(error, op), // optional
	skipHydrate: false // optional (default false)
});

Rules:

  • Persist only intent.
  • Do not persist environment.
  • Do not persist effective.
  • Do not write during hydrate unless explicitly configured.
  • A storage failure must not corrupt in-memory preferences.

Errors

Public errors use the prefs::* family:

  • prefs::unknown_dimension
  • prefs::intent_invalid
  • prefs::reserved_key
  • prefs::disposed

Known example:

prefs::unknown_dimension: [prefs] no such dimension in schema: theme

In UIX that error usually means a shell tried to write prefs.theme. The fix is to pass the visual mode to ActiveEidos.

Tests

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

Powered by TurnKey Linux.