/** * SEMA_MAP — typed perceptual map. * * For each canonical family declares a `base` set of real runtime-channel * signatures (`sound`, `haptic`) plus the `activeChannels` list stating which * channels participate. Visual concerns (motion, color, presence) live in * Eidos recipes and react to `data-event-*` attrs projected by VisualChannel. * * Channel registry — `SemaChannelSignatures` — is OPEN via TypeScript * declaration merging. Apps can extend it with their own channels (e.g. * `a11y`, `voice`) and the override / cascade types automatically include * the new entries. The canonical built-ins declared here are the non-visual * runtime channels the framework ships with. * * Numeric deltas without an `op` are added to the base; strings / booleans / * arrays replace the base; nested records merge recursively. Use `{ op: * 'replace', value }` when a number must overwrite (e.g. hue), `{ op: * 'multiply', factor }` for proportional scaling, or `{ op: 'add', value }` * for explicit add semantics. */ import type { Intent } from '../intent'; import type { SemaFamily } from './types'; import type { SemaDurationSpec } from './durations'; import type { DeltaValue, SemaChannelId, SemaChannelSignatures, SemaSignatureOverride } from './channels'; // ── Map shape ────────────────────────────────────────────────────────────── export interface FamilyMapEntry { /** Per-channel base signatures. Channels not present default to undefined. */ base: Partial; /** Channels participating in the family's signal. */ activeChannels: readonly SemaChannelId[]; /** * Perceptual hold — how long `data-event-*` tokens live in the DOM * before the engine cleans them up. SEMANTIC timing (the duration * of the perceived signal), independent of visual animation timing. * * Resolution chain (highest priority first): * 1. `signal.hold` — explicit per-call override. * 2. `family.hold` — this field. * 3. VisualChannel default (`brief` = 240 ms). * * Phase 4 of the codex refactor (`refactorizacion_codex.md`) lifted * hold out of visual motion so those concerns can move to Eidos without * losing the semantic timing budget. */ hold?: SemaDurationSpec; } export interface IntentMapEntry { deltas: Partial>>; } /** * Per-component sema declaration — sibling to `Morfo`. Lives in * `src/uix/sema/components/{name}.ts` alongside the component's morfo, * soma and eidos counterparts. * * Naming: `dialogSema: Sema` matches the framework convention * (`dialogMorfo: Morfo`). * * The engine flattens all `Sema` declarations into the cascade BEFORE * app-level rules — so the framework ships sensible defaults and the * app overrides them via `engineOpts.overrides.cascade`. * * `preloadSamples` are passed to `SoundChannel.preloadSamples()` at boot * so the WAVs are decoded before the first emit. * * Forward reference: `SemaCascadeRule` is declared in `resolver.ts` to * keep DOM-matching code colocated with the resolver. We use a structural * type here to avoid a circular import. */ export interface Sema { name: string; cascade: readonly { selector: string; priority?: number; channels?: readonly SemaChannelId[]; sound?: SemaSignatureOverride['sound']; haptic?: SemaSignatureOverride['haptic']; }[]; preloadSamples?: readonly string[]; } export interface SemaMap { readonly version: string; readonly families: Record; readonly intents: Record; } // ── The map ──────────────────────────────────────────────────────────────── export const SEMA_MAP: SemaMap = { version: '0.8.0', families: { contact: { base: { sound: { pitch: 800, centroid: 2000, roughness: 0.1, attack: 4, decay: 40, duration: 60, contour: 'flat', gain: 0.25 }, haptic: { kind: 'tick', intensity: 0.3, duration: 8, delay: 0 } }, activeChannels: ['sound', 'haptic'], hold: 'glimpse' }, commit: { base: { sound: { pitch: 700, centroid: 1800, roughness: 0.1, attack: 8, decay: 120, duration: 100, contour: 'flat', gain: 0.3 }, haptic: { kind: 'tap', intensity: 0.5, duration: 20, delay: 60 } }, activeChannels: ['sound', 'haptic'], hold: 'brief' }, signal: { base: { sound: { pitch: 900, centroid: 2400, roughness: 0.3, attack: 3, decay: 150, duration: 180, contour: 'arc', gain: 0.4 }, haptic: { kind: 'pulse', intensity: 0.7, duration: 40, delay: 0 } }, activeChannels: ['sound', 'haptic'], hold: 'noticed' }, emerge: { base: { sound: { pitch: 600, centroid: 1500, roughness: 0.05, attack: 12, decay: 200, duration: 150, contour: 'ascending', gain: 0.2 } }, activeChannels: ['sound'], hold: 'brief' }, shift: { // Shift = cambio de marco operativo (modal bloqueante, navegación, // cambio de régimen). Se diferencia de emerge en que reorganiza // los planos: backdrop pesado y shadow más profundo comunican // "cruce de umbral", no solo "aparición". Sound es más sutil que // emerge para no competir con el signal que el marco pueda // contener (regla del libro: el marco no absorbe el mensaje). base: { sound: { pitch: 500, centroid: 1400, roughness: 0.05, attack: 14, decay: 220, duration: 160, contour: 'ascending', gain: 0.18 } }, activeChannels: ['sound'], hold: 'noticed' }, handle: { base: { haptic: { kind: 'tick', intensity: 0.2, duration: 6, delay: 0 } }, activeChannels: ['haptic'], hold: 'brief' }, sustain: { base: {}, activeChannels: [], hold: 'noticed' }, // Delegate = reparto de iniciativa entre usuario y sistema (libro // cap. 29). Estructural; no carga intent por sí mismo. Mantiene perfil // bajo en sonido/háptica — la lectura se apoya en presencia + texto // + sustain. Los canales fuertes aparecen cuando delegate se compone // con signal (revisión, alerta) o commit (apply). delegate: { base: {}, activeChannels: [], hold: 'noticed' } }, intents: { threat: { deltas: { sound: { pitch: -200, roughness: 0.4, contour: 'descending', gain: 0.1 }, haptic: { kind: 'error', pattern: [40, 60, 40, 60, 40] } } }, risk: { deltas: { sound: { pitch: -100, roughness: 0.2 }, haptic: { kind: 'warning', pattern: [30, 50, 30] } } }, neutral: { deltas: {} }, affirm: { deltas: { sound: { pitch: 100 }, haptic: { kind: 'success' } } }, fulfill: { deltas: { sound: { pitch: 300, contour: 'ascending', gain: 0.05 }, haptic: { kind: 'success', intensity: { op: 'add', value: 0.2 } } } }, loss: { // Loss = consecuencia consumada (negativo + baja activación, // posterior). Se diferencia de threat (anterior, alta activación, // convoca acción): loss registra, no convoca. Por eso desciende // auditivamente y cae en haptic, sin arrastrar visual concerns. deltas: { sound: { pitch: -150, contour: 'descending' }, haptic: { kind: 'thud', intensity: 0.8, duration: 80 } } } } };