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.

86 lines
3.2 KiB

Prefs as schema-based core + lowercase App.* surface Two structural changes that were overdue and got bundled because they touched the same set of files. ## Prefs is now a schema, not a fixed shape Previously every preference had to be declared in a closed `PrefsCapabilities` interface (`languages`, `locales`, `currencies`, `themes`, `densities`, `motions`, `timezones`, `unitSystems`). Adding a new pref required forking `$libs/prefs` — bad framework design. The redesign replaces the fixed shape with a schema: PrefsSchema = Record<string, PrefsDimension<TIntent, TEffective>> Each dimension owns its own validator (`validate`), environment-fed resolver (`resolve`) and optional sibling-derived value (`derive`). The engine is generic over the schema and iterates it; it knows nothing about "locale" or "theme" specifically. Built-in dimensions live in `arts/prefs/dimensions/*` (locale, language, theme, density, motion, timezone, currency, unit-system, direction, plus boolean / enum / string / number primitives). The `standardPrefsDimensions(catalog)` preset composes the canonical set; apps spread it and add their own: const schema = { ...standardPrefsDimensions({ languages, locales, currencies }), sidebarCollapsed: booleanDimension({ default: false }), notificationLevel: enumDimension( ['all', 'mentions', 'none'] as const, { default: 'mentions' } ) }; Active surface exposes one slot per schema key with uniform verbs: App.prefs.locale.get() App.prefs.locale.set('es-ES') App.prefs.locale.clear() App.prefs.locale.onChange((v) => …) App.prefs.sidebarCollapsed.set(true) `setIntent('locale', value)` stays available as a low-level pass- through (storage bridge consumes it generically) but UI code uses the dimension surface. ## Lowercase core surface `App.Logger`, `App.Bus`, `App.Timers`, `App.Orca`, `App.Prefs` are gone. The "PascalCase for core, lowercase for services" rule was visual signalling against JS convention, no technical benefit, and created an asymmetry on the same object. All core members are now lowercase, matching services: App.logger App.bus App.timers App.orca App.prefs `createPrefsStorageBridge` keeps its old responsibilities; sources helpers (`prefsLocaleSource`, …) are gone — the dimension API replaces them. ## What changed - `$libs/prefs`: fully generic schema-based types + resolver. Old fixed `PrefsCapabilities` / `PrefsIntent` / `PrefsEffective` removed; replaced by `PrefsDimension`, `PrefsSchema`, `PrefsEffectiveOf<S>`, `PrefsIntentOf<S>`. - `arts/prefs`: engine + active wrapper rewritten to schema. Per- dimension active surface auto-built from schema keys. Sources file deleted (replaced by dimension surface). New `arts/prefs/dimensions/*` and `arts/prefs/standard.ts`. Storage bridge made schema-generic. - `arts/active-app`: lowercase `CoreServices` / `ActiveAppCore`, `prefs?: ActiveAppPrefsOptions<S>` root option carrying the schema. `defineActivePrefs` deleted (prefs is core, not service). `lang` / `format` / `frontend` factories migrated to read `core.prefs.<dim>` directly via defensive `readSlot()` helpers (each dimension is optional from the factory's POV; if the app's schema omits one, the integration degrades gracefully). - Presets, demos, web routes, README docstrings, `check-aliases.mjs` guards, marketing snippets all migrated. - Tests: `engine-prefs`, `active-prefs`, `storage-bridge`, `resolve-prefs`, `validate-intent`, `prefs-consumer-wiring`, `service-factories` rewritten for the schema-based API. `sources.test.ts` deleted (sources file is gone). ## Verification - `npm run check`: 0 errors, 0 warnings (1527 files). - `npm test`: 1645 tests across 139 files, all green. - `node scripts/check-aliases.mjs`: clean (lowercase enforced for every member of `App.*`, including `Logger`/`Bus`/`Timers`/`Orca`/ `Prefs` which now flag as forbidden capitals). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
import type { Currency } from '$libs/currency';
import type { Locale } from '$libs/locale';
import type { Timezone } from '$libs/timezone';
import { currencyDimension } from './dimensions/currency.ts';
import { densityDimension } from './dimensions/density.ts';
import { directionDimension } from './dimensions/direction.ts';
import { languageDimension } from './dimensions/language.ts';
import { localeDimension } from './dimensions/locale.ts';
import { motionDimension } from './dimensions/motion.ts';
import { themeDimension } from './dimensions/theme.ts';
import { timezoneDimension } from './dimensions/timezone.ts';
import { unitSystemDimension } from './dimensions/unit-system.ts';
/**
* Catalog input for `standardPrefsDimensions`. Apps declare which
* languages, locales and currencies they support; the preset wires the
* canonical built-ins around them.
*/
export interface StandardPrefsCatalog {
readonly languages: readonly Locale[];
readonly locales: readonly Locale[];
readonly currencies: readonly Currency[];
/** Optional IANA zone allowlist; omit for "any zone Intl can canonicalize". */
readonly timezones?: readonly Timezone[];
/**
* Optional per-dimension defaults. When omitted each dimension picks
* its own — usually the first entry in its catalog.
*/
readonly defaults?: {
readonly language?: Locale;
readonly locale?: Locale;
readonly currency?: Currency;
readonly timezone?: Timezone;
};
}
/**
* Compose the canonical built-in dimensions around the application's
* catalogs. Returns the schema fragment so apps can spread it into
* their full schema:
*
* ```ts
* const schema = {
* ...standardPrefsDimensions({ languages, locales, currencies }),
* // App-specific dimensions
* sidebarCollapsed: booleanDimension({ default: false }),
* notificationLevel: enumDimension(['all','mentions','none'] as const, { default: 'mentions' })
* };
*
* createActiveApp({ prefs: { schema } });
* ```
*
* Apps that don't want a particular built-in (say, a single-currency
* app skipping `currency`) drop the entry after spreading or compose a
* subset by hand instead of using this preset.
*/
export function standardPrefsDimensions(catalog: StandardPrefsCatalog) {
const defaults = catalog.defaults ?? {};
return {
language: languageDimension({ catalog: catalog.languages, default: defaults.language }),
locale: localeDimension({ catalog: catalog.locales, default: defaults.locale }),
currency: currencyDimension({ catalog: catalog.currencies, default: defaults.currency }),
timezone: timezoneDimension({ catalog: catalog.timezones, default: defaults.timezone }),
unitSystem: unitSystemDimension(),
theme: themeDimension(),
density: densityDimension(),
motion: motionDimension(),
direction: directionDimension()
} as const;
}
/**
* Neutral baseline used by `createActiveApp` when the caller omits
* `options.prefs`. Keeps the core surface populated even for apps that
* don't think about preferences. Apps that DO care override
* `options.prefs.schema`.
*/
export const NEUTRAL_PREFS_SCHEMA = standardPrefsDimensions({
languages: ['en'],
locales: ['en-US'],
currencies: ['USD']
});
export type StandardPrefsSchema = ReturnType<typeof standardPrefsDimensions>;

Powered by TurnKey Linux.