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.
291 lines
16 KiB
291 lines
16 KiB
---
|
|
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 opt-in 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).
|