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

504 lines
20 KiB

---
title: UIX — the thesis
type: reference
audience: human + agent
authority: E1 architecture — the narrative introduction to the four layers and their boundaries
status: current
source: 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`](../../src/uix/active_architecture.md). This document
> keeps the more narrative introduction.
>
> **Semantic vocabulary** (families, intents, verbs, channels): the single
> source of truth is [`CANON.md`](../CANON.md), anchored to the book and the
> code. (The original Spanish implementation guide survives as a historical
> seed at [`src/docs/GUIA_IMPLEMENTACION_SEMAUIX.md`](../../src/docs/GUIA_IMPLEMENTACION_SEMAUIX.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.
```text
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`](./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 `.wav`s and dynamic recipes
- the `VisualChannel`'s strict sequential semantics: it prepares
`data-event-*` through a DOM projector, holds them for the `hold` window,
and cleans up BEFORE resolving the Promise
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`](./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.md),
[`SOMA_ARCHITECTURE.md`](../../src/uix/soma/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: [`src/uix/eidos/README.md`](../../src/uix/eidos/README.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`](./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:
```text
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):
```ts
const uix = createActiveUix({ langs, prefs: { schema } });
setActiveUix(uix);
Soma.create();
const prefsProjection = createActivePrefsDomProjection({ prefs: uix.prefs, dom: uix.dom });
const eidos = ActiveEidos.create({
theme: 'base',
modeSource,
applyDom: true
});
```
The light/dark toggle feeds `ActiveEidos.modeSource`, not `uix.prefs.theme`.
`prefs.theme` is not part of UIX's core preset.
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`](../../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
Effects sync derived attrs
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 generates
the id, runs the channels' `prepare` hooks and dispatches the signal to ALL
registered channels. The built-in `VisualChannel` projects `data-event-*`
through `SignalProjector`, holds the attrs for the configured `hold`, cleans
them up and only then resolves the Promise (strict sequential semantics).
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. await events.emit(event)
3. the provider's synchronous handler mutates state
4. effects derive and apply structural attrs (data-state, aria-*)
```
The handler mutates state. The effects see the change and rewrite the DOM.
ADom is the only writer of mutable attrs.
### Three Soma scenarios
```ts
// Structural change without a signal
provider.commitState(change);
// internally: dom.apply(change)
// Structural change with a signal
provider.commitState(change, event);
// internally: await events.emit(event); dom.apply(change)
// 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
```text
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`. Convention for composite names:
`{verb}-{variant}` (e.g. `commit-save`, `dismiss-outside`).
---
## 6. The difference in one sentence
> Morfo declares, SomaRuntime transcribes, Provider supplies, Effects sync,
> Sema emits, ADom applies, Eidos reads.
Seven pieces, seven responsibilities, none invades the next.
---
## 7. Suggested reading order
0. [`CANON.md`](../CANON.md) — the **semantic canon**: the vocabulary
(families, intents, verbs, composition, channels) anchored to the book +
code. The source of truth everything else links.
1. [`architecture/morfo.md`](./morfo.md) — declaration, archetypes, the
2-of-3 rule
2. [`architecture/sema.md`](./sema.md) — the `emit`
contract, canonical verbs, channels
3. [`src/uix/eidos/README.md`](../../src/uix/eidos/README.md) — what eidos
consumes from the DOM + wrappers
4. [`SOMA_ARCHITECTURE.md`](../../src/uix/soma/SOMA_ARCHITECTURE.md) — the
runtime that transcribes morfo
5. [`COMPONENT_GUIDE.md`](../../src/uix/soma/COMPONENT_GUIDE.md) — the
operational guide to create/migrate components
6. [`src/arts/adom/README.md`](../../src/arts/adom/README.md) — `dom.apply`
7. [`architecture/active-uix.md`](./active-uix.md) — the composition root

Powered by TurnKey Linux.