16 KiB
| title | type | audience | authority | status | source |
|---|---|---|---|---|---|
| The channels as one system — synthesis | notes | human + agent | E3 — the capstone tying the per-channel RFCs to the book's thesis | current | 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·rfc-typography·rfc-depth·rfc-shape·rfc-structure. 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
§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
visualchannel, 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) — 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:
- morfo — declares the event and its semantics (family · intent · verb). The contract / DNA; pure TypeScript, no runtime.
- soma — fires it (
runtime.trigger): the sequenceprewrite → sema.emit → handler →state attrs. It writesdata-state(the state-moment). - sema — emits it: dispatches the signal to its channels —
executes
sound+haptic; projects the visual channel by stampingdata-event-*. It knows no DOM/CSS. - 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 §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 everyapplyX/clearXon 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:
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.