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/docs/architecture/overview.md

21 KiB

title type audience authority status source
UIX — the thesis reference human + agent E1 architecture — the narrative introduction to the four layers and their boundaries current migrated from src/uix/README.md (2026-07-02, docs-book F7.2)

UIX

A short architectural positioning document for src/uix.

Whole-system view: to understand the four layers (morfo, soma, sema, eidos) in a single read — motivations and articulation included — go to active-architecture. This document keeps the more narrative introduction.

Semantic vocabulary (families, intents, verbs, channels): the single source of truth is CANON.md, anchored to the book and the code. (The original Spanish implementation guide survives as a historical seed at decisions/guia-semantica-historica.md.)

UIX does not try to be "another component library". The bet is more ambitious and structural: separate layers that almost every current framework keeps mixed together.

In most UI systems these things live glued to each other:

  • the component's public DOM contract
  • headless behavior
  • accessibility
  • event semantics
  • the visual layer
  • modal engines (sound, haptic, motion)
  • app-service integration

UIX splits that block into pieces with strong boundaries.


1. The central idea

UIX models the interface as several cooperating layers, not as one giant component that does everything at once.

App
├─ cross-cutting services (active-app)
│  └─ adom, langs, format, ...
└─ components
   └─ structural / semantic / behavioral / visual layer

The intuition:

  • a component's public structure is not the same as its behavior
  • an event's semantics are not the same as its materialization
  • the active DOM is not the same as pure DOM utilities
  • the app should not couple modal engines to each other

UIX gives those separations explicit names and contracts.


2. The UIX layers

Morfo (src/uix/morfo/)

The component's cross-layer structural contract. It declares parts, data-*, ARIA, focus, keyboard, events and — when text belongs to the contract — the component's texts slots (idlangrefs). It is neither prose nor runtime: it is the canonical public shape.

See: architecture/morfo.md

Sema / Events (src/uix/sema/)

The semantic vocabulary and the orchestration of perceptual events. sema remains the historical folder name; ActiveUix's public surface calls the service events.

  • canonical families (8): contact, commit, signal, handle, emerge, shift, sustain, delegate
  • canonical intents: neutral, affirm, fulfill, risk, threat, loss
  • uix.events.emit(signal) for providers to publish occurrences
  • a channel registry (VisualChannel, SoundChannel, HapticChannel)
  • src/uix/sema/channels.ts as the single source of channel ids, signatures and overrides
  • src/uix/sema/sounds.ts as the single nominal repository of synthetic sounds, external .wavs and dynamic recipes
  • the VisualChannel's window: it prepares data-event-* through a DOM projector, holds them for the hold window, waits for the expression and cleans up BEFORE resolving EmitHandle.settled — a window that gates nobody, since emit returns its handle synchronously (D-full, 2026-09-15)

Sema does not decide what happened (the provider decides that). It only orchestrates the occurrence and dispatches it to the registered channels.

See: architecture/sema.md

Soma (src/uix/soma/)

The headless behavior layer: state, context, a11y, keyboard / pointer / focus, event emission.

Soma does not know the concrete implementation of the modal engines. Its job is to emit the component's facts, not to materialize them.

See: architecture/soma.md, SOMA_ARCHITECTURE.md

Eidos (src/uix/eidos/)

The visual layer: tokens, themes, per-component CSS recipes, archetype rules, event reactions and Svelte wrappers over soma's headless providers with the visual props (variant, size, block, iconOnly, icon, checkMark).

The general module is flattened into ActiveEidos:

  • ActiveEidos — the active runtime / visual context created by ActiveEidos.create(...). It manages visual configuration, primitives, canonical roles, themes, validation, CSS generation and persistence. With ActiveUix services it receives dom, langs, prefs and format; mode and density arrive through explicit sources or its own defaults. It injects <style data-uix-eidos> only when the app wants runtime CSS instead of precompiled CSS.

The app can provide a full EidosConfig or a partial themeBase patch via ActiveEidos.create({ config/themeBase }), or delegate theme values to external CSS with themeSource: 'css'. getCssContract() exposes the typed list of custom properties and renderContractCss() publishes it as empty CSS for external themes. For editors or live preferences, ActiveEidos can inject a runtime variable map into its own <style> validated against that contract. When the app wants to persist a full configuration, it stores a versioned EidosConfigDocument (kind + version + options) and can pass it back as ActiveEidos.create({ config: document }).

Eidos's size is canonical and discrete: xxs..xxl/full. ActiveEidos generates coordinated physical tokens for xxs..xxl; full stays reserved for responsive layout.

The generated contract also includes alpha color scales (--scale-blue-a1..a12 and --primitive-primary-a1..a12), border (width/style + aliases), layout (containerWidth, contentWidth, aspectRatio), density scalars wired to data-density, opacity, z-index and shadows with a physical 1..6 scale plus per-theme semantic aliases. The active recipe aliases (--toast-*, --dialog-*, etc.) live in EidosConfig.recipes; the generated static artifact lives in src/uix/eidos/generated/base.css and src/uix/eidos/index.css imports it as the foundation for SSR/docs with ActiveEidos.create({ applyDom: false }). The old contracts/ and themes/base/ CSS were retired from the active tree; the contract is obtained from ActiveEidos.getCssContract() / renderContractCss(). The tokens/ directory is no longer part of the runtime. ActiveEidos.listRecipes() and getRecipeTokens(component) expose those aliases for theme editors without reading CSS.

Every soma component ships an eidos wrapper (the Soma → Eidos migration completed in 2026-05): each wraps soma's public parts and adds only visual surface. The live inventory is the eidos/components/ tree plus npm run component:audit.

Eidos reads from the DOM what the other layers write (parts, data-attrs, ARIA, event signals) — it never imports soma or sema internals.

See: architecture/eidos.md

active-uix (src/uix/active-uix/)

The composition root. Only ActiveApp and ActiveUix may create shared services:

  • createActiveUix(options) — standalone; instantiates services and prefs internally.
  • attachActiveUix(activeApp) — attaches to an ActiveApp already composed by the application; validates the required services and creates no substitutes.

Active layers consume ActiveUix via getActiveUix() and expose the minimal surface their components need. Soma components read Soma; eidos components read ActiveEidos. That way they neither know which boot path was used nor depend on the full ActiveUix surface.

Full chapter: architecture/active-uix.md

ActiveUix and prefs

ActiveUix neither requires nor knows arts/frontend; that artifact was retired. The pattern follows ActiveApp: prefs is core and consumer layers sync from its dimensions, but each layer projects only what it owns.

  • prefs.language syncs the active language of uix.langs.
  • prefs.locale feeds uix.format.
  • prefs.direction is the single source of effective direction; it derives from language when there is no override.
  • prefs.motion, prefs.sound and prefs.haptic are cross-modal perception/interaction preferences.
  • theme, mode and density belong to Eidos: theme names the visual family, mode resolves light | dark, and density tunes visual ergonomics.

Global writes go through whichever ActiveDom is injected, never direct mutations:

ActivePrefsDomProjection -> dir, data-motion, data-sound, data-haptic
ActiveEidos              -> data-theme, data-mode, data-density

ActiveUix does not auto-create ActivePrefsDomProjection. That projector belongs to arts/prefs and the composition root wires it when it wants those global attrs. Eidos is created separately through ActiveEidos when runtime visuals are needed.

Example of a standalone UIX shell (like the docs' /uix route):

const uix = createActiveUix({ langs, prefs: { schema } });

setActiveUix(uix);
Soma.create();

const prefsProjection = createActivePrefsDomProjection({ prefs: uix.prefs, dom: uix.dom });
const eidos = ActiveEidos.create({ applyDom: true });

The light/dark toggle feeds uix.prefs.setIntent('mode', …); eidos reads the slot and re-applies data-mode + data-theme (2026-09-14: mode, theme, density and scaling are prefs dimensions).

In standalone mode, createActiveUix() instantiates ActivePrefs with the standard UIX preset and creates the configured services. In attach mode, attachActiveUix(app) reuses app.prefs, app.langs, app.dom, app.clipboard, app.format and the perceptual engine when they exist. UIX exposes it as uix.events; in attach mode ActiveApp's service is called events. langs and dom are required for attach; if missing, an early error is thrown. clipboard, format and events are optional services: if a layer asks for them and ActiveUix does not expose them, they fail with an explicit error instead of creating substitutes.

soma.prefs is the preferences view the headless providers need, backed by uix.prefs, not by frontend.

adom (src/arts/adom/, alias $adom)

The application's active DOM runtime. It is not a semantic engine. Its only job is to coordinate and synchronize DOM mutations via app.dom.apply(change) and app.dom.remove(target, names).

ADom receives already-resolved instructions. It does not interpret them.

See: src/arts/adom/README.md

libs/dom (src/libs/dom/)

Pure or nearly-pure DOM utilities (contains, getDocument, getWindow, focus, traversal, base observer wrappers). It contains no active runtime — that role belongs to adom.


2.bis How a component executes

The closed architecture (post-2026-04-25) defines six pieces with disjoint responsibilities. None invades the next.

Morfo            declares
SomaRuntime      transcribes
Provider         supplies sources, targets and handlers
Render bag       re-derives attrs from state (Svelte renders them)
EngineSemantic   orchestrates prepare + dispatch to perceptual channels
VisualChannel    prepares data-event* via SignalProjector + holds
SignalProjector  projects data-event* via uix.dom
ADom             applies DOM mutations (the structural commit)

The operational split

Morfo is DNA: one file per component declaring parts, data-*, aria-*, role, keyboard, focus, events. It executes nothing.

SomaRuntime (in soma/) interprets the morfo. One instance per component receives from the provider the state sources, the DOM targets and the event handlers. It exposes partProps(part), attachPart(part, target), keydown(part, event), trigger(eventName).

Provider supplies what the morfo cannot infer: reactive getters for states and props, reactive getters for parts (dynamic ids), synchronous handlers for the events, and the glue for orthogonal layers (Presence, Dismissal, ScrollLock — those are not morfo).

Effects (registered by the runtime on mount) listen for source changes and apply the derived attrs via dom.apply.

EngineSemantic receives the occurrence from runtime.trigger. It mints the id, runs the channels' prepare hooks and dispatches the signal to ALL registered channels — all of it synchronously, before emit returns its { id, settled } handle. The built-in VisualChannel projects data-event-* through SignalProjector, holds the attrs for the configured hold, lets the expression finish, cleans them up and only then resolves settled. Nobody waits for that window: the signal precedes the structural commit, it does not gate it. Non-visual channels (sound, haptic) are fire-and-forget.

ADom only applies the subsequent structural commit. It does not interpret. Sema and ADom are sibling layers: the engine no longer depends on ADom.

The runtime.trigger(eventName) sequence

1. imperative prewrite (transient markers like data-last-action)
2. events.emit(event)  → { id, settled }, dispatched, NOT awaited
3. the provider's synchronous handler mutates state — same tick
4. the render bag re-derives structural attrs (data-state, aria-*) and
   Svelte renders them

The signal precedes the commit; it does not gate it (D-full, 2026-09-15). Step 2 used to be awaited, which meant the state changed only after the ~240 ms hold.

The handler mutates state. The effects see the change and rewrite the DOM. ADom is the only writer of mutable attrs.

Three Soma scenarios

// Structural change without a signal
provider.commitState(change);
// internally: dom.apply(change)

// Structural change with a signal
provider.commitState(change, event);
// internally: events.emit(event); dom.apply(change) — same tick, in that order

// Signal without structural change
provider.emitEvent(event);
// internally: void events.emit(event)

Operational rules

  • What dom.apply writes, Svelte does not render. partProps only emits static identity (id, marker, ref).
  • events handlers are synchronous. Async goes outside the trigger.
  • Guards (if (disabled) return) live at the call-site, not inside the handler — if they reach the handler, a perceptual signal was already emitted.
  • Sema and ADom are sibling layers: neither depends on the other.
  • morfo.events.commits is descriptive: it documents the observable, it does not execute it. The real causal chain is handler → state → effect.

3. What makes UIX different

3.1 The structural contract is its own layer

In most libraries the component's public structure is scattered:

  • attributes in the provider
  • roles in the render
  • parts in CSS
  • selector names in docs
  • contracts in tests

UIX concentrates that in Morfo. That enables:

  • docs derived from the contract
  • cross-layer validation (the eidos linter)
  • less drift between headless and visual
  • more reliable tooling

3.2 Semantics never mix with execution

UIX separates the name of the occurrence from its modal materialization. Sound, vibration, CSS and future engines use the same vocabulary without being glued to each other.

3.3 Headless behavior doesn't carry every modality

Soma is not a mega-engine that knows everything. It doesn't play WAVs or haptic feedback, and it doesn't decide each channel's perceptual physics. Soma emits. Channels execute.

3.4 The active DOM is app infrastructure

UIX recognizes there are cross-cutting DOM facts several consumers want to listen to, which is why it introduces adom as an app service.

The rule is not "all DOM access goes through ADom". The rule is more precise: UIX-managed writes, document/window listeners, global queries, observers (ResizeObserver, MutationObserver, IntersectionObserver) and cross-cutting imperative actions go through ActiveDom. That includes focus and imperative scroll (focus, scrollTo, scrollIntoView, scrollWindowTo). Local reads of an element the component already owns (contains, closest, getBoundingClientRect, scrollTop) stay in the component.

3.5 The app composes services, not "super components"

active-app lets the application compose langs, dom, clipboard, format, events, and ActiveUix delivers them to layer scopes (Soma, Eidos) instead of forcing every component to know the full active root.


4. What UIX is not

UIX is not:

  • a flat collection of visual components
  • a mere opinionated wrapper over existing primitives
  • a classic design system where visuals, behavior and contracts live together
  • a monolithic event service that executes every modality
  • a global EventEmitter dressed up as architecture

UIX's originality is not in inventing exotic names, but in separating real problems that other systems usually accept as a single block.


5. Dependency rules

Morfo            → declares contracts
SomaRuntime      → interprets morfo inside Soma
Provider         → supplies sources, targets, handlers
Effects          → sync state → attrs
EngineSemantic   → registry + prepare/dispatch of signals to channels
VisualChannel    → prepares data-event* via SignalProjector + holds
SignalProjector  → projects data-event* via uix.dom
ADom             → applies DOM mutations (the structural commit)
Eidos            → materializes visually by reading the DOM
App              → composes services

Hard rules:

  • Morfo knows neither Soma nor any runtime code
  • SomaRuntime reads Morfo and depends on Sema
  • Provider does not write mutable attrs to the DOM directly; it supplies them as sources to the runtime
  • EngineSemantic does not mutate the DOM directly; it only orchestrates channel hooks
  • VisualChannel.prepare() delegates DOM projection to a SignalProjector
  • DomSignalProjector writes through the ActiveDom received from ActiveUix
  • Sema and ADom are sibling layers
  • ADom knows neither Morfo, nor Sema, nor Soma, nor Eidos; it only executes the mutations, listeners and cross-cutting DOM actions it receives
  • ActiveEidos adapts ActiveUix for visual components; wrappers do not import getActiveUix() directly
  • Eidos consumes DOM and data-*, not Soma/Sema internals
  • What dom.apply writes, Svelte does not render

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. If only soma benefits, the pattern is a virtual prop on the provider — do not extend the contract.

Extension Soma Sema Eidos In morfo
parts[].archetype ✅ emit ✅ verbs by role ✅ transversal selectors ✅
events[].semantic ✅ payload ✅ vocabulary ✅ tinting ✅
events[].prewrite ✅ executes ✅ sequence ✅ exit tinting ✅
firstOf value source ✅ only — — ❌
prop-not-nullish cond ✅ only — — ❌
Field-context OR — virtual prop — — ❌

Canonical cross-layer vocabularies

  • Archetypes: the cross-component part classification. Defined in src/uix/morfo/types.ts:ARCHETYPE_VOCABULARY. Emitted as data-archetype="..." by runtime.partProps.
  • Verbs: the canonical action verbs for morfo.events[].name. Defined in src/uix/sema/verbs.ts:SEMA_VERBS. Name convention: {family}-{verb}[-{nuance}] (e.g. commit-save, emerge-dismiss-escape). The family prefix is mandatory — validateMorfo throws without it.

6. The difference in one sentence

Morfo declares, SomaRuntime transcribes, Provider supplies, the render bag derives, Sema emits, ADom applies the prewrite, Eidos reads.

Seven pieces, seven responsibilities, none invades the next.


7. Suggested reading order

  1. CANON.md — the semantic canon: the vocabulary (families, intents, verbs, composition, channels) anchored to the book + code. The source of truth everything else links.
  2. architecture/morfo.md — declaration, archetypes, the 2-of-3 rule
  3. architecture/sema.md — the emit contract, canonical verbs, channels
  4. architecture/eidos.md — what eidos consumes from the DOM + wrappers
  5. SOMA_ARCHITECTURE.md — the runtime that transcribes morfo
  6. component-guide.md — the operational guide to create/migrate components
  7. src/arts/adom/README.md — dom.apply
  8. architecture/active-uix.md — the composition root

Powered by TurnKey Linux.