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.
504 lines
20 KiB
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`](./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 [`decisions/guia-semantica-historica.md`](../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.
|
|
|
|
```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`](./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`](./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`](./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. [`architecture/eidos.md`](./eidos.md) — what eidos
|
|
consumes from the DOM + wrappers
|
|
4. [`SOMA_ARCHITECTURE.md`](./soma-architecture.md) — the
|
|
runtime that transcribes morfo
|
|
5. [`component-guide.md`](../guides/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
|