8.2 KiB
| title | type | audience | authority | status | source |
|---|---|---|---|---|---|
| ActiveUix — the composition root | reference | human + agent | E1 architecture — how UIX boots, which services it owns, and how it degrades | current | migrated from src/uix/active-uix/README.md (2026-07-02, docs-book F7.1) |
ActiveUix
active-uix is UIX's composition root. Its responsibility is not to be another
behavior layer, but to hand morfo, soma, sema and eidos the minimal
services they need — without components ever knowing ActiveApp directly.
Whole-system architecture:
architecture/active-architecture.md. Executable contract:src/uix/contracts.ts, validated bycontracts.test.ts.
Two boot modes
createActiveUix(options) — standalone. Composes its own runtime:
- creates
logger,timers,busandprefs— and the prefs schema ALWAYS carries the four visual axes:uixVisualPrefsDimensions()is merged under whateverprefs.schemathe app passes, so the app may redefinemode/theme/density/scalingbut never omit them; - the
bususescreateSvelteEngineBus({ logger, clock: timers.clock }), same asActiveApp, so listeners run underuntrackand never create accidental reactive dependencies; - creates
langs,dom(ordisabledDomwhendom:false),clipboard,formatandeventsaccording to the options; - keeps
portalas a generic portal target, so each layer adapts it to its own API (portalTo) without couplingactive-uixto that layer; - registers the common translations and the per-component catalogs from
src/uix/langs/components/*.
attachActiveUix(app, options) — attach to an external ActiveApp:
- reuses
app.prefs,app.langs,app.dom,app.clipboardandapp.formatwhen they exist; - does not merge the visual axes. The prefs engine already exists and belongs
to the app, so nothing can be composed into it after the fact: an attaching app
spreads
uixVisualPrefsDimensions()(from$active-uix) into its own prefs schema. Otherwise eidos warns once per absent slot (eidos.prefs) and falls back to its own default —mode: 'light'on a dark-mode machine included; - requires
app.langsandapp.dom; if either is missing it throws a configuration error; - does not re-subscribe
langstoprefs.language(that connection belongs todefineActiveLangsinsideActiveApp); - registers the common translations and the per-component catalogs;
- does not own the
app's lifecycle.
Minimum contracts per module
Executable source:
src/uix/contracts.ts.
| Module | Minimum required | Optional | Fallback when missing | Error when missing |
|---|---|---|---|---|
ActiveUix standalone |
langs config |
clipboard:false, format, events, portal, dom:false |
creates prefs, core services and, with dom:false, a local disabledDom |
missing langs config |
ActiveUix attach |
ActiveApp core + langs, dom services |
app.clipboard, app.format, event engine, portal |
none for required services | missing langs or dom on the app; the getter of an absent optional service fails explicitly |
SomaRuntime |
dom from ActiveUix |
event engine, langs, format |
none of its own | nonexistent morfo/event/part |
Sema direct |
dom or projector when visual is active |
sound, haptic, visual:false |
none of its own for UIX services | SemaConfigError without dom/projector while visual is active |
Eidos |
dom when applyDom |
langs, format, prefs, mode/density sources |
applyDom:false allows render/serialize without DOM |
missing dom with applyDom active |
ADom direct |
caller's target/window/document | breakpoints/window | disabledDom only when the caller asks for it |
ADom's own errors without a real DOM |
Ownership and degradation rules
- Only composition roots create shared services.
morfo,soma,sema,eidosand components never createdom,langs,prefs,format,clipboardor equivalents: they receive them fromActiveUix. dom:falseonly degrades in standalone.ActiveUixexposes a localdisabledDomand passes it toEngineSemantictoo; Sema/events never fall back to direct DOM writes. In attach there is no compensating creation: if the app has nodom,attachActiveUix(app)fails.disabledDomis deliberately MIXED (AUX-2, 2026-07-11): write-shaped calls (apply,listen, …) no-op silently, but reads that MUST return a value —measure,raf,getDocument/getWindow— throwActiveUixDomDisabledError; a component that measures underdom:falsedies loudly instead of computing from a phantom layout.langsis notlocale.prefs.languagefeeds translations;prefs.localefeeds formats. They never mix.clipboardis a capability service, not visual DOM. Standalone creates it unlessclipboard:false; attach consumes it fromapp.clipboardwhen a layer asks for it, and fails with an explicit error if it was not declared.prefs.directionis the effective direction preference;html[dir]is only its DOM projection.- Standalone projects the cross-modal preferences by default.
createActiveUixbuildscreateActivePrefsDomProjectionunlessprojectPrefs: false(types.ts→projectPrefs,@default true), and it ownsdir,lang,data-motion,data-soundanddata-haptic; components stamp only ASSERTED directions, so the page must reflect the ambient preference or nothing does. In attach it is the opposite: opt-in withprojectPrefs: true, because the host app may own<html>. The visual axes (data-theme,data-mode,data-density,data-scaling) areActiveEidos' job, never this one. Thefrontendartifact was retired and must not reappear as alocalesource. What UIX writes into<html dir>carries an ownership mark —canon/direction-contract.md§6. ActiveUixneither creates nor knowsSomaorEidos.Soma.create(...)creates its own scope andSoma.runtime(...);ActiveEidos.create(...)creates the visual scope when the app needs runtime CSS.
Booting a UIX shell
A shell that uses visual components wires three pieces explicitly:
const uix = createActiveUix({ langs, prefs: { schema } });
setActiveUix(uix);
Soma.create();
const eidos = ActiveEidos.create({ applyDom: true });
The cross-modal projection comes with the root (rule 6): uix.prefsProjection
owns dir, lang, data-motion, data-sound and data-haptic. Building
createActivePrefsDomProjection by hand here would be a SECOND projection over
the same attributes.
ActiveEidos owns data-theme, data-mode and data-density. Light/dark
mode is a prefs dimension like the rest (2026-09-14): the toggle writes
uix.prefs.setIntent('mode', 'dark') and eidos, which reads the slot,
re-applies the attributes.