--- title: The channels as one system — synthesis type: notes audience: human + agent authority: E3 — the capstone tying the per-channel RFCs to the book's thesis status: current source: migrated from src/uix/eidos/CHANNELS_SYNTHESIS.md (2026-07-02, docs-book F7.3) --- # The channels as one system — synthesis ("Diseñando lo que ocurre") > The **capstone** of the per-channel RFCs. The thesis of the book _Diseñando > lo que ocurre_: an interaction **is not** color, or movement, or sound — it > is **one perceptual occurrence spread across N channels**, under the > **two-moment** model. This ties together [`rfc-color-engine`](../rfcs/rfc-color-engine.md) · > [`rfc-typography`](../rfcs/rfc-typography.md) · [`rfc-depth`](../rfcs/rfc-depth.md) · > [`rfc-shape`](../rfcs/rfc-shape.md) · > [`rfc-structure`](../rfcs/rfc-structure.md). Live demo: **`/temas/orquesta`** (the mixer). ## 1. Two moments, joined by a token: sema emits, eidos reads An occurrence flows through **two moments**, connected by the **token**: - **Sema's moment (emission)** — sema evaluates the occurrence and **emits** it. Sound + haptics it **executes right there** (runtime channels); for the visual, it **stamps it as tokens** `data-event-*` (family · intent · direction · phase). Sema **knows no DOM/CSS**. - **Eidos's moment (materialization)** — eidos **reads** those tokens (+ `data-state`) and **materializes** them in CSS (the visual channel). It is the **sole visual owner**. The **token is the contract**: sema writes, eidos reads — which is why the layers decouple (sema DOM-agnostic, eidos free of semantic logic). Over that producer → consumer axis runs the **temporal** axis (motion F1) — _which_ token: | Token | Nature | Who writes it | Eidos reads it as | | -------------- | ----------------------------------------------- | ------------- | ----------------- | | `data-state` | persistent — what the element **is** | soma / morfo | `presets` | | `data-event-*` | transient — what **occurs** (during the `hold`) | sema (emits) | `signatures` | `sequence` (`pre` / `coincident` / `post`) orders the two. ## 2. Expression channels (the book) vs sema channels (runtime) The book has **8 expression channels** (perceptual dimensions). The framework implements them with sema's **runtime channels**, which are **4** — and here is the key that avoids the classic confusion: | Sema channel (runtime) | Covers (the book's expression) | Who materializes it | | ----------------------------------------- | --------------------------------------------- | -------------------------------------------------- | | **visual** (sema projects `data-event-*`) | movement · presence · depth · shape · color | **eidos** — CSS over `data-event-*` + `data-state` | | **sound** | sound | sema (`chans/sound`) | | **haptic** | haptics | sema (`chans/haptic`) | | **announce** | — (accessible substitute, not a book channel) | sema (`chans/announce`) | `announce` joined on 2026-07-04 as a built-in channel — opt-in on a bare engine, wired by DEFAULT by the composition roots since S-19(ii) (see the backlog entry below and [`architecture/sema.md`](../architecture/sema.md) §Announce channel): it is the accessible counterpart the book demands when a modality is unavailable (`BK-SIGNAL-A11Y`), not a ninth perceptual dimension. `visual`, `sound` and `haptic` are the three that express; `announce` is the one that substitutes. > **Don't get confused**: **motion · depth · shape · color are NOT "eidos > channels"** — they are sema's **`visual` channel**, which sema projects and > **eidos materializes**. Eidos is the visual channel's _materializer_, not > the owner of channels of its own. The eidos RFCs (`COLOR/DEPTH/SHAPE_ENGINE`) > describe _how_ eidos materializes each visual facet — not independent > channels. Plus **space** ([`rfc-structure.md`](../rfcs/rfc-structure.md)) — **structural, not expressive**: it doesn't "occur", it is the stage (state-only). ## 3. The layer chain (each with its role) An occurrence **crosses the layers** — it is not "all sema", and eidos is not a shim: 1. **morfo** — **declares** the event and its semantics (family · intent · verb). The contract / DNA; pure TypeScript, no runtime. 2. **soma** — **fires** it (`runtime.trigger`): the sequence `prewrite → sema.emit → handler →` state attrs. It writes `data-state` (the state-moment). 3. **sema** — **emits** it: dispatches the signal to its channels — **executes** `sound` + `haptic`; **projects** the visual channel by stamping `data-event-*`. It knows no DOM/CSS. 4. **eidos** — **materializes** it: reads `data-state` (`presets`) + `data-event-*` (`signatures`) and renders them with its **token engines** (color · motion · depth · shape · space — the RFCs). It is the **complete visual system** and the sole owner of the visual. > Eidos does **not "make sema visible"**: sema contributes the **what** > (family / semantic intent), eidos contributes the **how** (the visual > vocabulary and its materialization). Co-layers, not one subordinate. The split is load-bearing, and the `shift` repair of 2026-08-11 is the case that shows it. `shift` had a sound (`slide`) and no visual signature at all — the _shift invisible_ antipattern (book ch. 34 §14) inside the framework that names it. The token was never the problem: sema was already stamping `data-event-family='shift'`. What was missing was on eidos's side of the line, and the fix stayed there — two entries in `BUILTIN_SIGNATURES`. Sema's only addition was a new token, `data-event-direction` (`forward` | `backward`), the SENSE of the crossing; eidos is what decides that a sense means the inline axis and flips it under `:dir(rtl)`. Sema still knows nothing about left and right, which is exactly the property that lets it stay DOM-agnostic. What `shift` has now are two signatures keyed on that token — `shift-forward` and `shift-backward` (they name the `shift-cross-*` keyframes): the frame arrives displaced one `--motion-distance-xl` (30 px) along the **inline** axis and settles in 320 ms on the `emphasized` curve. An emission that stamps no direction matches neither and stays visually silent — deliberate, since there is no neutral sense of travel to draw. Geometry, the `--motion-shift-sign` pair and the selector shape: [`motion.md`](./motion.md) §8. And the misreading that delayed the repair, because the structure invites it: `SEMA_MAP.families.shift.activeChannels` is `['sound']`, which looks like a family declared mute everywhere else. It is not. **"Mute by doctrine" is a statement about a CHANNEL, never about a family** — `activeChannels` governs only the two channels sema _executes_. `delegate` is the proof: its `activeChannels` is literally `[]`, and it still carries two visual signatures (`delegate-return-fulfill`, `delegate-return-loss`). No field of `SEMA_MAP` could say otherwise, because eidos is the sole visual owner (§1) — the visual channel is not in the map to be switched off, which is also why motion does not live there. Read an empty `activeChannels` as "silent", and `shift` files itself beside `sustain`, whose continuity is genuinely carried by persistent state and not by a firma. (The full canonical narrative lives in `CLAUDE.md` → "Sema: open channel registry".) ## 4. The composition — one event, N channels The **signature** (`BUILTIN_SIGNATURES` + `BUILTIN_KEYFRAMES`) is **cross-modal**: a single keyframe carries several modalities. A real example (`press-squeeze`, family `contact`): ``` contact · press → scale 0.96 (motion) + box-shadow → flat (depth recede) + --shape-smoothing 2→3 (shape: the corner firms up) + tick (sema sound) + vibration (sema haptic) ``` One `engine.emit(...)` stamps `data-event-*` (the **visual** channel reacts — eidos materializes it as color/motion/depth/shape) **and** fires sound + haptic — from the **same** event. There are no systems coordinating by hand; there is one occurrence expressing itself through sema's channels. ## 5. Runtime builders — the sextet (an open cage) Every axis retunable at runtime, same pattern (seed → managed block), opt-in over the authored scale: `applyColorScheme` · `applyTypeScale` · `applyDepth` · `applyShape` · `applySpacing` · `applyGradients` And the **capstone composing them**: **`applyTheme(seed)`** — a single seed (`{ color?, type?, depth?, shape?, space?, gradient? }`) composes the six axes in **one managed write** (vs six loose `apply*`), **atomically**: the axes you pass are applied, the ones you omit revert to the authored foundation. `clearTheme()` reverts everything. For surgical per-axis tweaks, the individual `apply*` remain. No reference system gathers the six perceptual axes under a single runtime theme builder. ## 5b. How each channel is AUTHORED — and the asymmetry that is left The sextet above retunes the **visual** channel at runtime. Sound and haptic have no equivalent, and after the 2026-08-06 rework that gap is worth naming precisely, because the reason it existed is gone. | Channel | Where a component AUTHORS it | Where a THEME retunes it | | ----------------------------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------- | | visual — color · motion · depth · shape · space | eidos recipes, reacting in CSS to `data-event-*` | `applyTheme(seed)` + the six `apply*` (runtime, atomic) | | **sound** | **names one entry of `SOUNDS`** (`src/uix/sema/sound-names.ts`) — a word, never a parameter | **nothing** — see below | | **haptic** | a categorical `kind` (`tick` / `tap` / `pulse` / …) | **nothing** | **What the sound rework did NOT change.** Nothing in eidos. The visual channel is projected by stamping `data-event-*`, and that path was untouched: no recipe, no token, no selector moved. Eidos does not know sound exists and did not need to. The rework changed WHERE the sound is authored (a catalogue instead of 71 pack files) and WHEN it is applied (before the intent instead of after it) — both entirely inside sema. **What it did change, and it is the useful part.** Until then, "theming the sound" was not a coherent idea: 214 cascade rules authored 33 hand-written signatures across 71 files, and there was no object to retune. Today there is exactly one — sixteen names in one const — so a `sound` axis in `ThemeSeed` would be the same shape as the other six, and a brand could ship its own `tick` the way it ships its own colour ramp. **The gap, and its closing (2026-08-06).** The table above used to have a second failure hiding behind the first: not only was there no `sound` axis in the seed, the two halves had different SHAPES. Eidos retunes live, atomically, revertibly; sema read `overrides.runtime` ONCE in the engine constructor and never again. A single `applyTheme` built over that would have been worse than two honest doors — one call where the colour changes now and the sound changes never, because it had already booted. So the substrate was made symmetric first: - **The catalogue moved INTO `SEMA_MAP`** (`families · intents · sounds`). It is data, and the map is the structure a theme addresses. While it sat outside as a module const, the VOCABULARY was the one perceptual axis a product could not re-voice at any time. - **`applyMap(seed)` / `applySounds(seed)` / `clearMap()`** — live and revertible, rebuilt from the authored map so applying twice is applying once. Same shape as every `applyX` / `clearX` on the visual side. - **A bad path now THROWS** (`SemaConfigError`). It used to create the branch: `families.commmit.…` left the real value untouched and grew a phantom beside it, in silence (audit S-09). A theming API that swallows a typo is worse than no API — the symptom is «the sound did not change», with nothing to point at. What a theme can reach today, measured in `theming.test.ts`: | Target | Path | | --------------------------- | ------------------------------------------------------------- | | a family's base | `families.commit.base.sound.gain` | | an intent's delta | `intents.fulfill.deltas.sound.gain` | | **what a NAME sounds like** | `sounds.tick.gain`, or `applySounds({ tick: { gain: 0.1 } })` | **What stays closed on purpose**: the vocabulary. `SoundName` is `keyof SOUND_CATALOGUE`, so a theme changes what a name sounds like and cannot invent one no component could reference. The voice is open; the words are not. **One door, and it is `applyTheme`.** The seed gained a seventh axis: ```ts eidos.applyTheme({ color: '#3b5bdb', sound: { tick: { gain: 0.1 } } }); eidos.clearTheme(); // reverts all seven, visual and perceptual ``` It lives on `ActiveEidos` and not on the composition root, which corrects the first reading of this section: **the dependency runs eidos → uix**, not the other way. `ActiveEidos` already holds `uix` and drives it (it registers motion presets through `uix.motion`); `ActiveUix` does not hold eidos, so a composed theme could not live there without inverting the direction. An axis you pass is applied, an axis you omit reverts — the same atomic reading the six visual axes already had, now over seven. Without a `uix` (a headless render) the sound axis is skipped and the visual ones still apply: a theme that half-applies beats one that throws. **And the vocabulary is complete**: the haptic `kind → pattern` table moved into the map too (`haptics.success.pattern`), so no perceptual axis is left that a theme cannot reach. Leaving haptic behind would have swapped one asymmetry for another. `masterGain` is applied as well: passing it to a shared engine used to be dropped in silence (audit S-05 / S-32); it now reaches `engine.master.setGain`, and the options that genuinely cannot apply to a shared engine are warned about instead of vanishing. ## 6. Position vs the references No reference system gathers the channels under **one semantic model**. Material has shape morph + motion (ad-hoc, without integrated sound/haptics); Apple, continuity + materials (platform-bound); everyone, color. **UIX gathers them** — the visual channel (sema → eidos) + sound + haptic (sema) — over the two-moment model, fired from one event, with runtime builders and an open registry. That is the framework, not the sum of its parts. ## 7. Demo `/temas/orquesta` — the mixer: press a control and mute each facet to hear its part. The **visual channel's** 4 facets materialize with eidos tokens (toggleable so each can be muted one by one); **sound + haptic** are fired by the real sema engine (`EngineSemantic.emit`, filtered channels). One event, sema's channels. ## Backlog / Evolution decisions ### 2026-08-13 — `announce` is ON by default in both composition roots Supersedes the wording in §2 above («`announce` joined on 2026-07-04 as a built-in **opt-in** channel»). Opt-in described the engine option; it is no longer what an app gets. S-19(ii) (`23b20fff5`) wires the channel in `createActiveUix` AND `attachActiveUix`, default ON. Announce is SUBSTITUTION, not ornament, so the a11y announcer ships ambient — the field norm (Angular CDK `LiveAnnouncer`, React Aria). The root is the only place that holds both ends (the shared sink and the engine), and it registers the channel post-construction with a late-bound closure, because `uix.announce` does not exist yet at engine-construction time; any wiring an app wrote itself fell back to the channel's self-owned regions — a SECOND live `role=status` / `role=alert` pair, the thing this chapter forbids. Opt out with `events: { announce: false }`; an explicitly passed `announce` wins — the root never overwrites an existing channel. Detail: [`architecture/sema.md`](../architecture/sema.md).