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/theming/channels.md

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 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) — 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 §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:

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.

Powered by TurnKey Linux.