--- title: UIX — Active Architecture type: reference audience: human + agent authority: E1 architecture — the deep whole-system view: motivations, the four layers, the transcription chain, the hard rules status: current source: migrated from src/uix/active_architecture.md (2026-07-02, docs-book F7.2) --- # UIX — Active Architecture > The living document of UIX's active architecture: motivations, the four > layers, how they articulate, what problem they solve, what they > deliberately leave out. This doc is the whole-system view; the per-layer > chapters are the operational reference. Dated status snapshots live in > [`docs/process/`](../process/) (see §10). --- ## 0. Minimum contracts per module What each module requires, what is optional, how it degrades and when it fails. The ownership and degradation rules are stated, timelessly, in [`architecture/active-uix.md`](./active-uix.md) §"Ownership and degradation rules". > Executable source: `src/uix/contracts.ts`. Boundary test: > `src/uix/contracts.test.ts`. ```text Module Requires Optional When missing Error active-uix langs,prefs,dom* clipboard,format,events,portal standalone disabledDom missing langs/dom in attach morfo none translations registers no translations no soma dom events,langs,format,clipboard disabledDom from uix invalid morfo/event/part; absent optional service sema projector/dom* sound,haptic,visual:false none SemaConfigError without dom/projector eidos dom* langs,format,prefs,mode/density sources applyDom:false missing dom with applyDom active adom ActiveDom surface target/window/breakpoints disabledDom only explicit ADom errors without a real DOM * `dom` means an `ActiveDom` surface, not necessarily a real DOM. It may be `disabledDom` only in standalone when the integrator asks for `dom:false`. In attach it must come from `ActiveApp`. * Outside `ActiveUix`, an `EngineSemantic` with the visual channel active must receive `dom` or `projector`; `visual:false` is the explicit degradation. ``` Future changes must derive from this table, not from constructors invented in lower layers. Applied correction: `ActiveUix` neither imports nor instantiates `Soma`/`Eidos`. `portal` remains a generic UIX setting; `Soma` consumes it as the default for `portalTo`, and `ActiveEidos.create(...)` creates the visual scope when the app needs Eidos. --- ## 0.1 Canonical naming The architecture may keep the historical folder names (`morfo`, `soma`, `sema`, `eidos`), but the public surface must use a consistent grammar. General rule: one name represents one concept; if a term is a historical alias, it must be marked as such with a retirement path. | Concept | Canonical name | Avoid / retire | | -------------------------------- | -------------------------------------------- | --------------------------------------------------- | | Runtime translation service | `langs` | `lang` as a service | | Active language | `prefs.language` | `locale` for the translation language | | Locale / regional formats | `prefs.locale` | `language` for formats | | Declarative text catalogs | `translations` | `langs` inside `morfo`; global per-component tables | | UIX preferences | `prefs` | `settings`, `presentation` as new names | | UIX perceptual events | `events` | `semantic` as a public service | | In-flight occurrence | `signal` | using it for the whole layer | | A morfo event's semantic payload | `semantic` | mixing it with the runtime service | | Declarative TS contract | `morfo` | `contract` as a duplicated TS API | | Exported CSS/data contract | `contract` | `morfo` for external CSS | | Runtime CSS bridge | `ActiveEidos` | a mandatory visual runtime for components | | Independent pure engine | `EngineX` only if it lives outside `ActiveX` | decorative engines | | Eidos visual root | `DrawerProps`, `DialogProps` | `DrawerProviderProps` in the visual API | Decisions already applied: - `ActiveUix.events` is the canonical name of the perceptual engine. In attach mode it reads `app.events`; `defineUixServices(...)` declares the service under the same name. - There is no public `ActiveUix.semantic` service. `semantic` survives only as the payload name in `morfo.events[].semantic`. - `morfo.texts` is the declarative field for component-owned idlangrefs — the `morfo.translations` nomenclature was renamed to `texts` during the 2026-05 migration (see `langs/components/*.ts` for the per-component catalogs). `langs` remains the runtime service. - `prefs` is the only name for preferences. `ActiveUix` exposes the raw `ActivePrefs`; Soma/Eidos consume bounded views. No `settings` is introduced. - `ActiveUix.motion` (`EngineMotion`, `arts/motion`) is the animation engine, consumed by Soma (`soma.motion`) and Eidos (`eidos.motion`). It lives in `arts/`, not in Eidos, so Soma can animate (spring) without a soma→eidos dependency. In attach it reads `app.motion`. Retirement order: 1. Keep `assertContract` as a data-contract validator, not as a parallel registry. `registerContract` remains for tooling/direct tests; Soma registers contracts via `registerMorfo()`. 2. Only afterwards clean up prop names in visual components. --- ## 1. The thesis in one sentence > UIX treats a component as **four layers with explicit contracts**, not as a > monolithic block mixing structure, behavior, semantics and presentation. The four layers are **Morfo · Soma · Sema · Eidos**. Each does one sharp job and communicates with the others only through the DOM and a shared declarative contract. None invades the next. --- ## 2. The problem it solves In most UI frameworks a component accumulates: - the DOM's **public contract** (attributes, parts, ARIA) - the **headless behavior** (state, keyboard, focus, events) - the event's **semantics** (what "opening a dialog" means beyond an attribute change) - the **visual layer** (CSS, animations, theming) - the **modal engines** (sound, haptic and CSS reactions via DOM events) - the **app-service integration** (i18n, dates, theme, etc.) All of that lives mixed together. Renaming a `part` touches six places with no automatic verification. An event's semantics are buried in hardcoded strings only the component knows. CSS couples to incidental DOM structure. Sound engines rewrite per-component mappings. When you want to change a cross-cutting decision — "all triggers must share a common hover dim" — you must enumerate the 30 components that have a trigger. UIX breaks that block into four layers with disjoint responsibilities and a common communication channel: **the DOM with attributes declared by the cross-layer contract**. --- ## 3. The four layers ### Morfo — the cross-layer contract `Morfo` declares the component's genetics: its parts, the `data-*` it emits, the ARIA it contributes, the roles, the states, the semantic events it may fire, and the keys it dispatches. One declaration per component, in TypeScript, validated by sium. Morfo **executes nothing**. It is DNA, not protein. ```ts // src/uix/morfo/components/dialog.ts (excerpt) export const dialogMorfo = { name: 'Dialog', kebab: 'dialog', scope: ['soma', 'sema'], events: [{ name: 'emerge-close-cancel', semantic: { family: 'emerge', verb: 'close', target: v.partRef('content'), sequence: 'pre' }, prewrite: [{ part: v.partRef('content'), attr: 'data-last-action', value: 'cancelled' }], commits: { part: v.partRef('content'), attr: 'data-state', value: 'closed' } }], parts: [ { name: 'Trigger', kebab: 'trigger', archetype: 'trigger', role: 'button', ... }, { name: 'Content', kebab: 'content', archetype: 'content', role: 'dialog', ... }, // ... ] } as const satisfies Morfo ``` Morfo is **the single cross-layer articulation point**. Any data the other layers need to share with each other passes through here. It is the most important structural rule: if two layers need to know the same thing, that "same thing" lives in morfo. ### Soma — the headless behavior `Soma` consumes morfo and transcribes it into executable behavior. It reads `morfo.events`, `morfo.keyboard`, `morfo.parts[].data` and `aria`, and materializes them: dispatches keys, applies attributes to the DOM, manages state, integrates with context (Field, Form, Soma). Soma **decides no visuals**. It knows no colors. No transitions. No sounds. Only states, events, focus, keyboard and how to materialize all of that in the DOM. Soma's central piece is `SomaRuntime`: a morfo interpreter that receives the reactive sources from the provider (states, props, parts, events, actions) and takes care of: - emitting the static attrs (`partProps`) - applying the state-derived attrs via `dom.apply` (effects) - dispatching keys via `keydown(part, event)` - executing events via `trigger(eventName)` with the full perceptual chain The provider contributes **what the morfo cannot infer**: reactive getters over internal state, concrete handlers, and the glue for orthogonal layers (Presence, Dismissal, ScrollLock). ### Sema — vocabulary + perceptual channels `Sema` defines the framework's canonical vocabulary and orchestrates the **dispatch of perceptual signals** to a set of modular channels. Canonical vocabulary (`SEMA_MAP` in `src/uix/sema/sema-map.ts`): - **8 families** — `contact`, `commit`, `signal`, `handle`, `emerge`, `shift`, `sustain`, `delegate`. Each declares a `hold`, a base for the real channels (`sound`, `haptic`) and the set of active channels. - **6 intents** — `neutral`, `affirm`, `fulfill`, `risk`, `threat`, `loss`. Each intent declares per-channel `deltas` applied over the family base when the family is valenced. - **Action verbs** (`SEMA_VERBS` in `src/uix/sema/verbs.ts`) — `present`, `dismiss`, `commit`, `cancel`, `announce`, `warn`, … — the canonical verbs for `morfo.events[].semantic.verb`, and the tail of `morfo.events[].name`. Sema **does not decide which event happened** — the provider decides. The `EngineSemantic` only: - keeps a registry of channels implementing `Channel` - generates each occurrence's `id` - resolves the per-channel `EffectiveSignature` (base × intent deltas) - dispatches each signal to every registered channel - blocks the caller only for as long as the visual channel needs ``` src/uix/sema/ ├── engine.ts registry + channel prepare/dispatch ├── resolver.ts resolveSignature(signal): EffectiveSignature ├── sema-map.ts per-family base + per-intent deltas table (typed) ├── verbs.ts SEMA_VERBS catalog └── chans/ ├── types.ts Channel interface ├── visual.ts VisualChannel (built-in, data-event projection + hold) ├── sound.ts SoundChannel (Web Audio, prepare-time priming) └── haptic.ts HapticChannel ``` The **visual channel** (built-in) is the only one sharing the DOM plane with the subsequent structural commit, and therefore the only one that blocks the caller. `EngineSemantic` runs generic channel hooks; `VisualChannel.prepare()` projects `data-event` + `data-event-id` + `data-event-phase` (and optionally `data-event-family`, `data-event-intent` and `data-event-direction`) onto the target through a `SignalProjector`. In `ActiveUix` that projector receives `uix.dom`, so attr writes enter through the same DOM owner soma uses. `VisualChannel` holds the configurable window and the cleanup removes the projection before resolving the Promise (strict sequential semantics). > **Namespace discipline**: the semantic projection writes **only** > attributes under the `data-event-*` prefix. It never touches `data-state`, > `data-intent`, `data-disabled` or other state attrs — those belong to the > runtime/morfo. Eidos reads `data-event-intent` for reactions to the > transient signal and `data-intent` (when the morfo emits it) for the > persistent state. The visual channel's internal hold defaults come from `SEMA_MAP.families[*].hold` and resolve over the perceptual scale `SEMA_DURATIONS`: `glimpse`, `brief`, `noticed`, etc. The integrator can override per signal (`signal.hold`) or globally via `new EngineSemantic({ dom, visual: { defaultHold } })`. The **SoundChannel** is implemented: it synthesizes short earcons via Web Audio (two oscillators → biquad lowpass → ADSR-lite envelope, parameterized by `effective.sound.{pitch, centroid, gain, contour, roughness, duration}`). It does **prepare-time priming**: it creates + resumes the `AudioContext` in the `prepare()` of an audible signal, synchronously inside the user gesture. Only afterwards does it register the capture-phase listener on `document` for later re-resumes. It is opt-in: `new EngineSemantic({ sound: true })`. Reusable sound signatures live in `src/uix/sema/sounds.ts`. Component packs reference names (`sound('handle.pickup.air')`, `sound('notification.ping')`) or dynamic recipes, never loose constants. A repository entry can be synthetic or an external `.wav` with a synthetic fallback; `SoundChannel` plays `sampleUrl` and falls back to synthesis when fetch/decode fails. The **HapticChannel** is an opt-in channel; any non-visual channel is fire-and-forget: it manages its own timing on its plane without affecting the caller. ### Eidos — the visual layer `Eidos` is the visual layer. Its access to the system is **the DOM**: it reads parts, data-attrs, ARIA, archetypes and event signals the other layers write. It does not import soma internals; it does not ask sema. Eidos **is not just CSS**. It covers what the dead `air/` branch called the "visual runtime" plus the token system — inheriting no code. Its current structure: ``` src/uix/eidos/ ├── active-eidos.svelte.ts ActiveEidos: visual runtime/context created by ActiveEidos.create ├── archetypes.css rules common to [data-archetype=*] ├── events.css global hints for [data-event-*] (sema visual) ├── generated/base.css foundation CSS generated from the base EidosConfig (incl. @font-face) ├── lib/ config support, recipes, CSS contract and shared types └── components/{x}/ recipe + Svelte wrapper + per-component types ├── {x}.css recipe (selectors [data-{x}], variants) ├── {x}.svelte wrapper over soma's headless provider ├── types.ts visual Props + soma's public props └── index.ts default root + attached parts ``` `ActiveEidos` is the source of truth for theming: primitives (color + alpha scales, size map, spaces, control height, radius, border, opacity, z-index, focus ring, layout, typography, shadow, motion, icon), semantic roles and themes. It also resolves the active theme from its visual sources (`theme`, `modeSource`, `densitySource` or defaults) and injects runtime CSS when the app doesn't precompile it. External themes can come from CSS alone if they honor the custom-property contract (`themeSource: 'auto' | 'config' | 'css'`); `getCssContract()` publishes that contract as typed data and `renderContractCss()` materializes it as empty CSS from the config. Per-component recipe aliases (`--toast-*`, `--dialog-*`, etc.) live in `EidosConfig.recipes` and are generated into `generated/base.css`; the CSS recipes remain selectors/states, not a parallel token source. `ActiveEidos.listRecipes()` and `getRecipeTokens(component)` are the query surface for theme editors; they return names and defensive copies, never mutable handles into the internal config. `ActiveEidos` can also write runtime variables into its own style block, validating them against the contract so a theme editor doesn't mutate CSS by hand, variable by variable. Full-configuration persistence uses `EidosConfigDocument` (`kind + version + options`), keeping `EidosConfig` a pure authoring object with versioning at the storage/exchange edge. `ActiveEidos` is also the context the Svelte wrappers consume: `ActiveEidos.require()` exposes only the visual surface (`dom`, `langs`, `format`, `prefs` and helpers like `resolve(...)`, `breakpoint(...)` and `isBelow(...)`). Wrappers do not import `getActiveUix()` directly. The public Svelte wrapper follows disciplined option C: a visual root `` / `` / `` and attached parts ``, ``, etc. There is no public `Provider` and no flat snippet-first API. Selection rules (eidos reads, never writes): ```css /* Style common to all triggers, component-independent */ [data-archetype='trigger'] { cursor: pointer; } /* Tint the exit anim by cause (saved/cancelled/dismissed) */ [data-state='closed'][data-last-action='cancelled'] { animation: ...; } /* React to a perceptual signal during the hold (200–260ms by family) */ [data-event-family='commit'][data-event-phase='active'] { animation: eidos-commit-settle 260ms var(--ease-out); } /* Variant by transient intent (the signal's, not the state's) */ [data-event-family='commit'][data-event-intent='threat'][data-event-phase='active'] { animation: eidos-announce-pulse-threat 400ms var(--ease-spring); } ``` The DOM is the channel between events/sema and eidos. The `VisualChannel` projects the occurrence through `SignalProjector` + `ActiveDom`; Eidos reacts. --- ## 3.bis ActiveUix without `frontend` (closed) `frontend` no longer exists as an active artifact. The cross-cutting source of preferences is `ActivePrefs`, following the same pattern `ActiveApp` uses; DOM projection is explicit and lives outside `ActiveUix`. The current partition: - `uix.langs` — language and translations; syncs from `prefs.language`. - `uix.format` — regional formats; consumes `prefs.locale` as a `LocaleSource`. - `uix.clipboard` — clipboard write capability; in standalone it is created unless `clipboard:false`, in attach it is consumed from `app.clipboard` when a component asks. - `uix.dom` — the single writer of global attrs via `dom.apply`. - `uix.motion` — the animation engine (`EngineMotion`, `arts/motion`): registers + runs `--state`-moment presets (CSS settle / JS spring/waapi/rect drivers). Consumed by Soma (`Presence` via `soma.motion`) and Eidos (`eidos.motion`: generates CSS + registers its presets). In standalone it is created with the available `dom`; in attach it reads `app.motion`. - `uix.prefs` — effective cross-cutting preferences: `language`, `locale`, `direction`, `motion`, `sound`, `haptic`, etc. - `uix.portal` — the generic portal target; layers like Soma adapt it to their API (`portalTo`) without `ActiveUix` knowing those layers. Eidos stays outside `ActiveUix`'s surface: `ActiveEidos.create(...)` creates the visual context and, when runtime CSS is needed, uses `uix.dom`, `uix.langs`, `uix.format` and explicit `mode`/`density` sources when the integrator doesn't want the defaults. `prefs.direction` is the single source of effective direction. If the user sets no intent, it derives from `prefs.language`; calling `prefs.direction.set('rtl')` makes that override rule; calling `prefs.direction.clear()` goes back to deriving. The `html[dir]` attribute is only the DOM projection of that effective value; `html[lang]` is the symmetrical projection of `prefs.language`, and travels with it because the browser reads both from the DOM — font selection, hyphenation, screen-reader announcement. That projection answers the page, not the component. A component knows its own direction because it resolves one — the chain runs the prop, then prefs, and never reads the DOM projection back; a component that inherits from a parent composes that link at the call site rather than adding a step to the resolver. The projection is why the common case needs no per-component assertion at all, and the rest — the chain, which attribute carries the assertion, which selector may read it — is [`canon/direction-contract.md`](../canon/direction-contract.md). DOM projection is split by ownership: ```text ActivePrefsDomProjection -> dir, lang, data-motion, data-sound, data-haptic ActiveEidos -> data-theme, data-mode, data-density ``` In standalone mode, `createActiveUix()` instantiates `ActivePrefs` with the standard UIX preset and creates the configured services. In attach mode, `attachActiveUix(app)` reuses `app.prefs` because `prefs` belongs to `ActiveApp`'s core. `langs` and `dom` are the required services for attach: if they are missing, `attachActiveUix(app)` fails early. `clipboard`, `events` and `format` are optional; if a layer needs them and the app didn't declare them, `ActiveUix`'s getter fails explicitly. `ActiveUix` does not auto-project preferences onto the DOM. The cross-modal projection exists in `arts/prefs` as `createActivePrefsDomProjection(...)`; the composition root that wants those global attributes wires it. This allows `ActiveApp` without UIX, UIX without Eidos, or Eidos with precompiled CSS — without duplicate projectors. When a UIX shell wants runtime visual mode, the canonical flow is: ```ts const prefsProjection = createActivePrefsDomProjection({ prefs: uix.prefs, dom: uix.dom }); const eidos = ActiveEidos.create({ theme: 'base', modeSource, applyDom: true }); ``` `prefsProjection` and `eidos` are disposed with the shell. Light/dark mode is not written into `prefs.theme`; it is passed to `ActiveEidos` as a visual source. --- ## 4. How they articulate — the transcription chain The four layers form a declarative transcription chain where each translates the previous contract into its own language: ``` Morfo declares (TypeScript constant + sium schema) ↓ SomaRuntime transcribes (Soma — reading morfo + sources) ↓ Provider supplies sources/handlers (Soma — TypeScript class) ↓ Render bag re-derives attrs (Soma — partProps; Svelte renders it) ↓ EngineSemantic dispatches signals (Sema — registry + prepare/dispatch) ↓ VisualChannel prepares data-event* (Sema — via SignalProjector/uix.dom) ↓ Eidos reads the DOM and applies CSS (Eidos — selectors + tokens) ``` Plus, in parallel (not in the chain): - **ADom** materializes the one imperative attr write left — `trigger`'s declared prewrite (`data-last-action`, etc.). Derived structural attrs travel in the render bag since P0 fase C (audit 2026-08-26). - **Sema's non-visual channels** (sound, haptic, future) receive the same signal and materialize it in their modality — fire-and-forget. The pieces with disjoint responsibilities: | Piece | Responsibility | Doesn't do | | ------------------- | -------------------------------------------------- | ------------------------------------ | | **Morfo** | Declare the contract | Execute anything | | **SomaRuntime** | Transcribe morfo into behavior | Decide business logic | | **Provider** | Supply reactive sources + handlers | Write mutable attrs to the DOM | | **Render bag** | Resolve every morfo plan for Svelte to render | Decide which attrs (morfo says that) | | **EngineSemantic** | Channel registry + prepare/dispatch | Know DOM, audio, vibration | | **VisualChannel** | Project `data-event*` via projector + awaited hold | Write structural attrs | | **SignalProjector** | Project `data-event*` via `dom.apply` | Decide when to emit | | **ADom** | Imperative DOM mutations for structural attrs | Know the upper layers | `Eidos` stays outside that chain: it reads from the DOM; it does not participate in the transcription. --- ## 5. The causal chain of one interaction A concrete example: the user clicks a Toast's **×** button. ``` 1. Browser fires click → Svelte calls Close.onclick 2. Close.onclick runs: void this.toastItem.runtime.trigger('emerge-dismiss') 3. SomaRuntime.trigger('emerge-dismiss'): 3.1. Looks up event 'emerge-dismiss' in morfo.events ✓ 3.2. Resolves target = the Item DOM element via partRef('item') 3.3. AWAITS events.emit({ target, name: 'emerge-dismiss', family: 'emerge' }) EngineSemantic dispatches the signal to ALL registered channels: - VisualChannel.prepare(): SignalProjector applies data-event* via dom.apply(target, data-event-family=emerge) - VisualChannel.handle(): holds the window (240ms for emerge) - cleanup: dom.apply(target, data-event*=undefined) - SoundChannel, HapticChannel: fire-and-forget (not awaited) The Promise resolves when the VisualChannel finished the cleanup (strict sequential semantics) 4. SomaRuntime invokes the provider's handler: sources.events['emerge-dismiss']() → this.provider.toaster.dismiss(opts.toast.current.id) → toast.dismissing = true (state mutation) 5. The runtime's reactive EFFECTS see that isOpen changed: resolvePartAttrs recomputes the item part's attrs dom.apply(target, { 'data-state': 'closed' }) on the next tick 6. Eidos (CSS) has been reacting throughout the sequence: - during t=0..240ms: [data-event^="emerge-dismiss"] fires an @keyframes fade-out (CSS animation, not transition: it runs full-duration even if the attr disappears afterwards) - at t≈245ms: [data-state="closed"] takes over - the Presence layer applies data-ending-style; CSS finishes the animation ``` State is the single source of truth. The DOM is derivation. The perceptual signal PRECEDES the structural change by the full hold (~240ms for emerge) — the caller waits for the cleanup before mutating state, giving CSS a perceivable window to choreograph the exit. --- ## 6. The primitives that travel between layers ### DOM attributes — the universal channel Everything that travels between layers travels through DOM attributes: | Attribute | Who writes | Who reads | | ------------------------------------------- | -------------------------------------- | ----------------------------- | | `data-{component}` | partProps (static) | Eidos (root selector) | | `data-{component}-{part}` | partProps (static) | Eidos (part selector) | | `data-archetype="trigger"` | partProps (static) | Eidos (transversal selector) | | `id` | partProps | ARIA refs, tests | | `role` | dom.apply (effect) | Screen readers, Eidos | | `aria-*` | dom.apply (effect) | Screen readers, Eidos | | `data-state="open"` | dom.apply (effect) | Eidos (variant selector) | | `data-disabled` | dom.apply (effect) | Eidos (state selector) | | `data-event="emerge-dismiss"` | sema.emit (transient) | Eidos (event selector) | | `data-event-phase="active"` | sema.emit (transient) | Eidos | | `data-event-id="sig-N"` | sema.emit (transient) | Future sound/haptic | | `data-event-family="commit"` | sema.emit (transient) | Eidos (family selector) | | `data-event-intent="risk"` | sema.emit (transient) | Eidos (signal tinting) | | `data-event-direction="forward"` | sema.emit (transient, per-emit only) | Eidos (directional signature) | | `data-color="primary"` | dom.apply (effect) | Eidos (per-token recipe) | | `data-intent="risk"` | dom.apply (effect, optional per morfo) | Eidos (persistent state) | | `dir` | prefs projection (page) / provider | Browser bidi, Eidos `:dir()` | | `data-dir` | provider (resolved, opt-in per recipe) | Eidos (unconditional hook) | | `data-last-action="cancelled"` | trigger prewrite | Eidos (exit tinting) | | `data-starting-style` / `data-ending-style` | Presence layer | Eidos (animation hooks) | **Operational rule**: the render bag (`partProps`) is the single attr pipeline — static identity AND every state-derived plan ship through it, server-rendered included (P0 fase C, audit 2026-08-26). `dom.apply` writes only `trigger`'s declared prewrite. There is no double-write. ### Cross-layer vocabularies > **Canonical:** the semantic vocabulary (families, intents, verbs) lives in > [`CANON.md`](../CANON.md). The summary below is for the cross-layer view; > the canon + code are authoritative. Two stable vocabularies anchor the articulation: **Archetypes** — part categories that appear across multiple components. The canonical inventory is the `ARCHETYPE_VOCABULARY` const (`src/uix/morfo/types.ts`) — not copied here: a copied list drifted (it froze at 24 while the code had 26). A `Trigger` of Dialog, Popover, DropdownMenu and Tooltip is the same category — Eidos can style them transversally with `[data-archetype=trigger]`. **Verbs** (`src/uix/sema/verbs.ts:SEMA_VERBS`), grouped by family: ``` contact: press · tap · activate · focus · trigger · release commit: select · unselect · toggle · save · submit · confirm · complete · fail · cancel · reset · discard · delete · restore · expire · acknowledge · apply · partial · block · move · set · remove · reorder · upload signal: announce · notify · warn · alert · inform · emphasize · remind handle: pick · carry · drop · drag · resize · reorder · rotate · scroll · zoom emerge: present · dismiss · open · close · expand · collapse · reveal · hide shift: enter-mode · exit-mode · navigate · route · step · return · context sustain: start · progress · loading · waiting · syncing · processing · streaming · pending · retrying · upload · end delegate: offer · plan · authorize · act · review · escalate · return ``` Verbs that look like one family but belong to another per the canon: **select / toggle / acknowledge** are `commit` (they fix state; they are not mere contact); **edit** is `shift.enter-mode` (it changes the regime). `morfo.events[].name` **declares its family**: the shape is `{family}-{verb}[-{nuance}]` (`commit-toggle`, `emerge-close-cancel`), and `validateMorfo` rejects a name that does not start with its own family. That lets Sema/Sound/Haptic/Eidos subscribe or style by family or verb without enumerating components. **Intents** (`src/uix/sema/sema-map.ts:SEMA_MAP.intents`), 6 values: ``` neutral — no affective load (default) affirm — low positive ("all is well") fulfill — resolutive positive ("goal accomplished") risk — moderate negative ("check this") threat — active negative ("alarm, immediate attention") loss — consummated consequence (negative + low activation, posterior) ``` Intent is orthogonal to family: a `commit` can be `affirm` (subscribe), `risk` (publish), `threat` (delete), or `neutral` (a plain toggle). The provider declares it in `morfo.events[].semantic.intent` (literal) or exposes it as a prop (`fromProp + supported subset`). --- ## 7. Hard rules The operational invariants that keep the system coherent: 1. **Morfo knows no runtime code.** It is pure declaration. 2. **SomaRuntime depends on Dom and Semantic.** By construction, not by import. The provider injects them. 3. **The provider does not write mutable attrs to the DOM directly.** It supplies them as sources to the runtime. 4. **`Semantic` may use `Dom` (downward).** `Dom` does not know `Semantic`. 5. **`ADom` knows no upper layers.** It only applies the mutations, listeners and cross-cutting DOM actions it receives. The DOM boundary does not require wrapping local reads: a component may call `el.contains(...)`, `el.closest(...)`, `el.getBoundingClientRect()` or read its own element's `scrollTop`. By contrast, `document/window` listeners, global queries, imperative focus and window scrolling go through `ActiveDom`. **Timing, not just ownership.** Layout-forcing reads (`getBoundingClientRect`, `getComputedStyle`, `offset*`, `scroll*`, `client*`) must run POST-LAYOUT, never synchronously right after a DOM/style write — read-after-write forces a mid-turn reflow (the `[Violation] Forced reflow while executing JavaScript` family). Defer them with `dom.measure(read, node?)` (the frame-coalesced read queue, the sanctioned vehicle) or from a `dom.raf` callback; a bare deferred read complies just like `dom.apply` does for writes. To resolve a theme token into a concrete color, use `eidos.resolveToken(token)` (config + the `uix.color` engine) — NOT a `getComputedStyle` probe. The dev-only `uix.perf` detector (opt-in `reflowDetector`) attributes violations at runtime via Long Animation Frames. The framework governs layout READS the same way `dom.apply` governs writes. 6. **`Eidos` consumes DOM and `data-*`, not Soma/Sema internals.** If it needs something, it must be declared in morfo or emitted in a sema signal. 7. **What `dom.apply` writes, Svelte does not render from `partProps`.** One authority per attribute. 8. **State is the single source of truth. The DOM is derivation.** Handlers mutate state; effects derive attrs. 9. **Event handlers in `runtime.trigger` are synchronous.** Async goes before calling `trigger`. 10. **Guards live at the call-site, not inside the handler.** If the guard reaches the handler, the perceptual signal was already emitted. 11. **`morfo.events[].commits` is descriptive, not executable.** It documents the observable; smoke validates it. 12. **The 2-of-3 rule for extending Morfo.** A morfo extension is only justified when **at least two of the three layers** (soma, sema, eidos) consume it. Soma-only conveniences live in the provider via a virtual prop. --- ## 8. The authorship / transcription distinction A useful lens for deciding where each thing lives: - **Authorship** — written once by a human, with intent. A component's `Props`, the morfo, the event handlers. It lives in the author's TypeScript. - **Transcription** — mechanically derived from authorship. The provider's `Opts`, the per-prop `readableActive(() => x)` wrapping, the structural attrs. A helper / runtime / generator derives it. UIX aims for only the authorship to be human. Transcription is code that writes code: | Authorship | Transcription | How | | -------------------------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------- | | `Props` | `Opts` | hand-written `extends WithRefOpts, StateProps<>, ActiveProps<>` — or `OptsFromProps` | | Each wrapper prop | Active/State boxes | `bindProps({ ... })` — target-typed | | A part's whole `{ id, ref }` bag | `WithRefOpts` | `partOpts(() => id, () => ref, setRef)` | | `morfo.events[].commits` | Final DOM after handler | Effects derive | | `morfo.parts[].data` | Attributes on each tick | Resolver + dom.apply | | `morfo.events[].name` + canonical verb | `data-event="..."` | sema.emit | Two details the table cannot carry, both load-bearing: - **`Preserve` is not optional decoration.** `OptsFromProps` strips `undefined` from an optional prop by default; the fourth parameter lists the keys whose absence MEANS something (`dir` above all) and keeps their `T | undefined`. Omitting it silently destroys the distinction — [`canon/direction-contract.md`](../canon/direction-contract.md) §1. - **`bindProps` is target-typed, never inferred.** The declared `Opts` computes the config's expected shape (`ConfigFor`), so keys, getter types AND setter bodies are checked and the return IS the opts — no cast. Designs that infer FROM the bag degrade the setter's parameter to `any`. That last point has a behavioural twin, and it is a **cross-layer contract**, not a build step: > **No silent internal write.** Every internal write to a bindable notifies, by > construction — the change callback lives INSIDE that key's setter, which is > the only write path, so the provider cannot forget it. Exactly one named > exception: a **coalesced** (debounced) notification, which is never lost > because clear / submit / unmount flush it. A path that writes without > notifying is a defect — the `bind:` consumer and the callback consumer would > see different histories of the same component. Rules and the third convention: [`guides/component-guide.md`](../guides/component-guide.md) §Callback conventions; acceptance row `E-3.7` in [`guides/completion-checklist.md`](../guides/completion-checklist.md). This distinction explains why the 2-of-3 rule holds: the morfo is **cross-layer authorship**. If only soma needs something, it is soma-internal transcription — not authorial, and it doesn't belong in morfo. --- ## 9. What this architecture is NOT To avoid mission creep, it helps to fix what UIX **does not want to be**: - **Not a visual collection.** Eidos is visual; UIX as a system is not. - **Not an opinionated wrapper over existing primitives.** The four layers are original; they don't wrap Radix/Headless UI. - **Not a classic design system.** Tokens, themes and recipes belong to Eidos, not to the core. - **Not a monolithic event service that executes every modality.** Sound, Haptic, Motion and future modalities register as **channels** of `EngineSemantic`; each manages its own modality. The engine is only registry + dispatch. - **Not a global EventEmitter dressed up as architecture.** Every event has a specific DOM target and a semantic owner declared in morfo. - **Not a mini-DSL in JSON.** Morfo is descriptive declaration, not a program. Logic lives in the provider's TypeScript; morfo only says which attrs and which semantics. --- ## 10. Project status > Dated status snapshots ("what is implemented as of X") live in > [`docs/process/`](../process/) — e.g. > `active-architecture-snapshot-2026-05.md`. This document describes the > architecture, not the progress. ## 11. Acknowledged risks No design is risk-free. UIX has four, explicitly: ### 11.1 Layer excess If the boundaries don't stay sharp, the system feels more complex than what it solves. The 2-of-3 rule and the "virtual prop" doctrine mitigate this, but they require sustained discipline. ### 11.2 Names without discipline `Morfo`, `Sema`, `Soma`, `Eidos` are names that only work if the contracts are sharp. If Sema starts knowing about the DOM, or Soma decides visuals, the names become decoration. ### 11.3 Responsibility invasion The constant danger is one layer trying to do another's job: - `Sema` becoming a multimodal runtime (a regression). - `Soma` deciding CSS or motion. - `SomaRuntime` interpreting business logic. - `Eidos` reaching into soma internals. UIX only works if each layer accepts its limits. ### 11.4 Lack of precedent There are no UI systems with this exact composition. That means more architectural freedom but also fewer external patterns to copy when an edge case appears. --- ## 12. Why it can be worth it If the boundaries hold, UIX offers something uncommon: - **Architectural explainability.** Every decision falls into a recognizable layer; "where does this live" has a predictable answer. - **Less cross-layer drift.** The morfo is authoritative; the other layers derive. Renaming a part touches one place, not six. - **Automatic contract validation.** Sium schema + smoke + morfo-check catch structural drift before it reaches production. - **More freedom to introduce new engines.** Sound, Haptic, Motion, any future modality registers as an additional `Channel` in `EngineSemantic` without touching morfo or soma. - **Honesty about framework-vs-integrator boundaries.** UIX provides vocabularies, contracts, transport and extension points; it doesn't pretend to decide every modality for every app. The important idea: > **Cross-modal coherence can be treated as the integrator's responsibility, > not as the false promise of a centralized runtime that claims to know > everything.** --- ## 13. The summary sentence > **Morfo declares · SomaRuntime transcribes · Provider supplies · the render > bag derives · Semantic emits · Dom applies the prewrite · Eidos reads.** Seven words describing the whole chain. If an architectural decision contradicts one of those seven, the decision is wrong — or the architecture must evolve consciously. --- ## 14. To go deeper - [`architecture/overview.md`](./overview.md) — the general positioning (more narrative) - [`architecture/morfo.md`](./morfo.md) — declaration, archetypes, the 2-of-3 rule - [`architecture/sema.md`](./sema.md) — the `emit` contract, canonical verbs - [`SOMA_ARCHITECTURE.md`](./soma-architecture.md) — the runtime + components - [`component-guide.md`](../guides/component-guide.md) — the operational guide to create/migrate components - [`architecture/eidos.md`](./eidos.md) — the visual layer: tokens, themes, recipes, wrappers - [`src/arts/adom/README.md`](../../src/arts/adom/README.md) — `dom.apply` + reactive DOM services - [`guia-semantica-historica.md`](../decisions/guia-semantica-historica.md) — the original API conventions (historical seed; [`CANON.md`](../CANON.md) rules) Detailed design decisions and historical trade-offs live in the `active-uix` branch's git log.