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/process/CONTINUE-sound-engine.md

495 lines
59 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# CONTINUE — el hilo del audio: REDISEÑO en curso (`$sound` = orquestador)
> **Kickoff para la sesión siguiente**: _"Lee `docs/process/CONTINUE-sound-engine.md`
> y sigue por donde toque."_
> **Fecha**: 2026-07-31 (tercera sesión) · Rama `alpha-0.1-sec-dom`.
## Lo que pasó el 2026-07-31 — giro de raíz
Se presentó el gate del reproductor (D-AP.1…13) y el usuario lo **rechazó de
raíz**: el sistema de sonido es un frankenstein — nació de los diseños de otros
módulos (sema, media-player) y se parchea en función de lo que había, en vez de
partir de su función: **orquestador de TODOS los aspectos del sonido**, como
`motion` / `timers` / `scene`. Los consumidores se adaptan al servicio, no al
revés. El rediseño se hizo clean-room y su gate quedó **FIRMADO** el mismo día
(aprobación del plan + D-SR.4/D-SR.7 explícitas).
## Estado
| Iniciativa | Estado |
| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`PLAN-sound-engine.md`](./PLAN-sound-engine.md) | ✅ CERRADA (histórica). Su §16.1 («la voz es del art») queda **REVOCADA** por el rediseño |
| [`PLAN-sound-redesign.md`](./PLAN-sound-redesign.md) | ✅ **COMPLETA (las dos entregas, 2026-07-31) — sin commitear.** E1: F0–F2 + escucha A/B del usuario («suena igual»). E2: F3 (media citizenship: `sound.media(el)` provee el transporte con la forma del puerto `MediaProvider` + foco + `duckUiWhileContent` + visibilidad content-aware + `attach()` opt-in) · F4 (MediaSession de la fuente activa) · F5 (factoría con `EngineSoundPolicyOptions`; `contracts.ts` y estudio sin tocar, declarado). **AUDITADA (§9 del plan): AU-1…AU-9 resueltas** — duck ahora PERSISTENTE (estado derivado, holder = ducker más reciente; test nacido en rojo) · guards post-dispose · re-registro idempotente por elemento · drift guard soma↔art · bundle medido (16,2 KB min / 5,6 KB gz) · 3 guards vistos fallar por mutación. **28 suites / 305 tests verdes · check 73 = baseline, 0 propios · docs:check 0 · prettier limpio en lo propio.** Pendientes que exigen una página consumidora (llegan con el re-plan del player): medición Chrome del escenario mixto · prueba manual de teclas de medios |
| [`PLAN-audio-player.md`](./PLAN-audio-player.md) | 🗄️ ARCHIVO (hechos + análisis que el v2 referencia) |
| [`PLAN-audio-player-v2.md`](./PLAN-audio-player-v2.md) | ✅ **GATE FIRMADO 2026-07-31** («ok, continua»; H-9 = CABLEAR · D-AP2.7 = solo duck global). **F1 ✅ · F2 ✅ COMPLETAS (2026-07-31; morfo 8/8 · player 18/18 · check 74=baseline 0 propios)**. Detalle F2 en la fila del plan; el commit de rate cabalga `ratechange` (patrón G-5 de nacimiento). ⚠️ `translations:check` BLOQUEADO POR AJENO (`langs/components/palabras.ts` con `url:` rompe el stripper del script; palabras = excluido, no se toca) — correr cuando se arregle. **F3 ✅ COMPLETA (2026-07-31; player 10/10 · morfo 8/8 · check 74=baseline viva, 0 propios)** — detalle en la fila F3 del plan: transporte por el servicio como default (pin con motor REAL) · `metadata` público (opts+types+wrapper) · `nativeMediaProvider` retirado (media-provider.ts = contrato puro; drift-guard borrado) · G-5 aplicado · `formatDuration` · 6 sub-providers+wrappers+exports · README (tablas al día). **F4 ✅ (docblock del pack = doctrina D-AP2.7: el silencio lo posee el servicio vía `duckUiWhileContent`, receta escrita; la resta-de-gain v1 queda diseñada SIN cablear a propósito; stamp en navegador → F7). F5 EN CURSO — buffer-swap ✅ COMPLETO (29/29 · check 74=baseline 0 propios · eidos-lint limpio)**: el `TimeSlider` soma pasa `secondaryValue={bufferedTime}` y monta `Slider.SecondaryRange`; el div `.mp-buffered` + `--media-buffered` + `--_mp-buffered` + `--_mp-rail` RETIRADOS (css/provider/test/morfo-docblocks/READMEs al día); el look se conserva (42% → `--slider-secondary-bg` en scope). **F5 ✅ COMPLETA** (detalle en la fila F5 del plan: 6 wrappers + AudioLayout + `<AudioPlayer>` con metadata derivada + 4 variantes + tokens; 37/37 contratos · lint 0 inválidos · check 78 = baseline+4 ajenos NUEVOS de `arts/connection`, 0 propios). **D-AP2.7 v2 COMMITEADA (`85b269f74`) + fix composición demos (`63f20f345`: el pack NO estaba en la lista del layout — packs sema son OPT-IN por app) + AMPLIADA a sliders por escucha real (`5d2816a0c`: pick/drag → `channels:['haptic']` por descendientes — estructurales sin intent, tirar su canal no borra carga evaluativa): el transporte del player es SILENCIOSO por defecto** — pack D.5 (resta de gain; `contact.silent` nuevo en sounds.ts) sobre commits de botones + descendientes (contact de Buttons, commit-set de Sliders); `commit-fail`/`commit-complete` audibles; `handle-drag` conserva su resolver por mecanismo (declarado); voz reactivable en capa 5b. **F7-construible ✅ COMMITEADO en `c4d64ba98`** (live badge localizado vía soma · demo con `<AudioPlayer>` + selector de variante + snippet en paridad · README eidos §modo-audio · `component:audit --only media-player` PASS · check 74 = baseline, 0 propios). `smoke`+`perm:check` corridos (2026-07-31): **todas las rutas del hilo PASS** (301/305; los 4 rojos son ajenos — ethereal·heroscrolling·dome-gallery·card, hipótesis: los M vivos de tipografía de otra sesión — y combobox/stepper en perm); la demo del player sin `data-perm-step` aún (opt-in para el dossier). **Waveform: gate D-WF.1…8 FIRMADO («ok») · F1–F5 ✅ HECHAS — F4+F5 el 2026-08-01, SIN commitear** (F1–F3 en `0ab6cc822`; F4 eidos completo: wrapper+Wave+recipe+tokens `waveform:{height-xs…xl, bar-width, color, color-played, playhead-width}` — `playhead-width` añadido sobre la lista firmada, declarado — + guards visual-attrs/lint-allowlist; F5: demo `/uix/components/waveform` con picos REALES de `/sounds/*.wav` vía `decode`+`peaks`, READMEs soma+eidos, `component:audit --only waveform` PASS, `docs:check` 0; **fix F3 declarado**: `dir` no se reenviaba al Slider embebido — completado y verificado en vivo. Detalle en las filas F4/F5 del plan). **QUEDA F6 mirada** (ojos del usuario; la sonda en el 5173 vivo verificó estructura/cascada/interacción, pero el panel oculto no composita → sin captura). Los packs sema de waveform NO existen (delegated ✓ nada que registrar). **QUEDAN — y todos exigen ojos o la máquina del usuario**: F6 verificación VISUAL de las 4 variantes × light/dark × RTL (abrir `/uix/components/media-player`, chip `audio`, recorrer variantes; captura y MIRADA) · medición Chrome del escenario mixto (earcon + media, 1 contexto — patrón `javascript_tool` del §15 del plan del motor) · teclas de medios / pantalla de bloqueo (metadata en el SO) · stamp sema en navegador (`data-event="commit-toggle-play"` aterrizando en el play-button con la regla del pack casando). Nota demo: `duckUiWhileContent` se activa componiendo el app con `defineEngineSound({duckUiWhileContent: 0.2})` — la demo actual no recompone el uix global; se muestra como receta en el README (decisión: no tocar la composición raíz de las demos desde la página del player). Después: F4 sema · F5 eidos (AudioLayout+AudioPlayer+variantes+buffer en `Slider.SecondaryRange`) · F6 formas · F7 demo+medición Chrome+teclas de medios. ⚠️ baseline de `check` fluctúa por la rama viva (~74, 0 propios; filtrar SIEMPRE con `\\\\` e incluir consumidores). Lección F1: claves nuevas de `ActiveProps` van OPCIONALES o rompen todos los call-sites |
> ⚠️ **2026-08-12** — donde la fila del plan v2 dice «`handle-drag` conserva su
> resolver por mecanismo (declarado)», quedó rancio el 2026-08-06: el rediseño
> del sonido pasó los gestos a REPETICIÓN (`sound: 'step'` por emisión) y los 3
> resolvers quedaron sin llamador; hoy están RETIRADOS
> ([`AUDIT-docs-code-ledger.md` §D10](./AUDIT-docs-code-ledger.md)). El
> silencio por canal de los sliders del player (D-AP2.7 v2) no cambia.
> **2026-08-01 — hay un handoff hermano.** Lo que la mirada del usuario abrió ese
> día y quedó SIN cerrar (el **Slider roto en RTL**, la disposición de los botones
> del player, los iconos de seek, la medición del earcon mal hecha, `Captions` y
> el cableado del waveform) vive en
> [`CONTINUE-player-rtl.md`](./CONTINUE-player-rtl.md). **Empezar por ahí.**
## Defectos que la MIRADA del usuario encontró (2026-08-01) — arreglados
La verificación visual F6 destapó cuatro cosas que ninguna sonda había visto. Se
arreglaron el mismo día; todas verificadas, **el guard de cada una nacido en rojo**.
1. **El anillo alrededor de los sliders** («se viene arrastrando desde el
principio»). NO era del arquetipo ni del slider: es la **firma global de la
familia `commit`** — `BUILTIN_SIGNATURES.commit` (`lib/motion/presets/css.ts`)
monta `commit-settle`, cuyo 30% pinta `box-shadow: 0 0 0 3px`. El `commit-set`
del Slider apunta al **provider**, así que el anillo enmarcaba la caja entera
(medido: 544×40). Un aro lee como feedback en un botón y como borde parásito en
un raíl ancho. **Arreglo**: exención acotada en `slider.css`, el patrón que
`gradient-builder.css` ya practicaba. Alcanza a todo slider embebido
(waveform, media-player, color-picker, time-picker) porque todos llevan
`[data-slider]`.
2. **El mismo anillo en el media-player**: `commit-complete` / `commit-fail`
apuntan al provider → borde alrededor de los ~700px del reproductor. Misma
exención en `media-player.css`. Los botones conservan su aro pequeño (20×36),
que es donde el feedback pertenece.
3. **El botón de saltar al final anunciaba una compleción FALSA.** Los botones de
salto no tienen evento propio (D-AP2.13 delega en el `commit-set` del
scrubber — cierto para el scrubber, **vacío para los botones**, que no tienen
Slider detrás). Al saltar más allá del final el elemento dispara `ended` →
`commit-complete` en el provider → sonido (es audible por D-AP2.7) + anillo.
El de retroceso quedaba mudo: asimetría audible. **Doctrina**: saltar al final
NO es completar, es abandonar la obra en el final. `seek()`/`scrubTo()` marcan
el salto que aterriza en el final y `ended` no anuncia; un `play` posterior
limpia la marca, así que el final natural sí se sigue anunciando.
4. **MediaSession sin overlay del SO**: el modo **audio ya funcionaba**
(metadata completa, medida en vivo); el roto era **vídeo**, que es el modo por
defecto de la demo. `<AudioPlayer>` deriva su metadata de title/artist/artwork;
el `<MediaPlayer>` compositivo no puede leer su hijo `Title` sin volverse
data-driven, así que la declara el consumidor — y la demo no lo hacía.
**Lección de método**: se propuso al usuario cambiar el keyframe `commit-settle`
(doctrina) SIN haber leído el corpus, y el usuario lo paró: _«es una temeridad
plantear esto sin haber hecho la lectura de todo el sistema»_. La lectura después
lo desmintió — `motion.md` §4 fija que la firma es genérica por decisión
estructural («un `commit` asienta igual en todo el sistema»), eidos es el ÚNICO
dueño del canal visual, los packs de sema solo transportan `sound`/`haptic`, y el
`VisualChannel` no consulta `activeChannels`: **la regla acotada en la receta ES
la puerta**, no un olor. Lo único que queda en pie de aquel informe es una
observación menor: `commit-settle` escribe `3px` a pelo donde sus hermanos
`announce-pulse-*` usan `var(--focus-ring-width)`.
Precedente relacionado que YA estaba decidido y no se había leído: `VirtualList`
/ `commit-set-resize` lleva **`channels: []`** (`design-chat-block.md`) — el
override de morfo que salta proyección, dispatch y hold, para eventos que son
contabilidad estructural y no gesto del usuario. Está cableado en
`engine.ts` (no está latente); lo latente es el silenciado PARCIAL (callar lo
visual conservando el sonido), que hoy no tiene palanca declarativa.
~~**Abierto, sin tocar**: los subtítulos nativos se solapan con la barra de
controles.~~ **CERRADO 2026-08-04.** El diagnóstico era correcto y el arreglo
fue el que predecía: la pista se mantiene en `mode='hidden'` —activa, con
`activeCues` y `cuechange`, pero sin pintado del navegador— y el texto se monta
en la parte `Captions`, que llevaba declarada desde el principio y nunca se
había implementado. Verificado a ojo en Chrome, claro y oscuro: el texto sale
**encima** de la barra con 8px de aire. El espejo de una sola dirección de
`captionsOn` cerró en el mismo arreglo. Detalle en el README de eidos del
media-player, §Captions.
## Qué es el rediseño (resumen; la verdad está en el plan)
`$sound` gobierna 7 dominios: sustrato (contexto único — se conserva, verificado)
· **mezcla** (master → buses `ui`/`content`; `prefs.sound` ↔ `ui` SOLO) ·
**voces** (calibración = dato del consumidor; sema registra la suya; default =
valores actuales, cero cambio audible) · **ciudadanía de media** (el servicio
PROVEE el transporte con la forma del puerto `MediaProvider`) · política/foco
(`mixed`/`exclusive`/`duck`; `duckUiWhileContent` sustituye el parche D-AP.7) ·
percepción (`decode`; analyser/picos v2) · MediaSession (del servicio).
## MediaSession — VERIFICADA POR EL USUARIO (2026-08-01) ✅
El pendiente que arrastraba tres sesiones («prueba manual de teclas de medios /
pantalla de bloqueo — cuando exista una página que consuma `sound.media()`»)
queda **CERRADO**. El usuario abrió el media hub de Chrome con la demo de audio
sonando y confirmó: **carátula + título + artista + transporte**, origen
`localhost:5173`.
Notas de la verificación, para no repetir el camino:
- **Las teclas de VOLUMEN no sirven** para esto: muestran el OSD de volumen de
Windows y nada más. En Windows 11 Microsoft retiró el panel de medios que
antes se pegaba a ese OSD. La superficie fiable es el **botón de nota musical
de la barra de Chrome** (solo existe mientras suenan medios), o las teclas de
Play/Pause / pista.
- **La sospecha de la carátula era infundada**: se temió que `artwork` sin
`sizes`/`type` y sin CORS (`media.w3.org` no manda
`Access-Control-Allow-Origin`) hiciera que el `MediaImageManager` la
descartara. **Carga perfectamente.** No se tocó nada; queda registrado para
que nadie «arregle» un no-problema.
- **Anterior/siguiente pista salen apagados y es CORRECTO**: `setActive()`
registra `play`, `pause` y `seekto` — un reproductor de una sola fuente no
tiene lista, y declarar esos handlers mentiría sobre su capacidad. Los ±10s
del hub los sintetiza Chrome desde `seekto`.
- **Lección de método**: se afirmó «el audio ya funcionaba» apoyándose en una
medición JS (`navigator.mediaSession.metadata` poblada) como si zanjara lo que
el usuario ve. No es lo mismo: el contrato JS puede cumplirse y el overlay del
SO no aparecer (media muteada, modo equivocado, tecla equivocada). Una
medición del contrato NO es una verificación de la experiencia.
## Escenario mixto — MEDIDO Y CERRADO (2026-08-04) ✅
El pendiente que arrastraba desde E2 («medición Chrome del escenario mixto —
earcon + media, 1 contexto») queda cerrado. Detalle completo en
[`CONTINUE-player-rtl.md`](./CONTINUE-player-rtl.md) §4; lo esencial:
- **Con media real sonando (sin mutear, vol 1) y 26 earcons encima: UN solo
`AudioContext`.** `createMediaElementSource` = 0 → la media nunca entra en el
grafo. 0 contextos cerrados, cero churn.
- **La receta que los handoffs prescribían no hacía falta.** No hay que
instrumentar antes del boot ni navegar client-side: eso resuelve el parche del
CONSTRUCTOR, que sólo sabe decir «¿se creó uno nuevo?». Se parchea el
**PROTOTIPO** (`createGain` / `createOscillator` / `resume`) y se recoge el
`this` en un `Set`: enumera INSTANCIAS, así que un contexto preexistente
aparece igual. **Es el patrón a usar la próxima vez.**
- Fechado: el contexto nace en **t ≈ 4,1s tras la carga, sin gesto de usuario**,
ya `running`. Y **no lo crea el player** — en `/uix/components/button`, sin
ningún elemento media, nace igual (t ≈ 4,08s). Es del arranque de la app.
⚠️ ~~**Hallazgo nuevo, de la app de demos y NO tocado**~~ **CABLEADO el
2026-08-06 (`d161cedcc`) y MEDIDO el 2026-08-12**: `+layout@.svelte` pasa ya un
`SemaPreferences` VIVO (`$state`) a `events.preferences`, y el toggle escribe
`semaPreferences.sound = 'full' | 'off'` además del intent de prefs. A/B en
navegador (parche de PROTOTIPO contando instancias, el patrón de abajo): click
con Sound ON → **+2 osciladores** (el par del earcon); mismo click con Sound
OFF → **+0**. Y desde `fa79cc542` (S-27/S-41) el `off` tampoco abre contexto ni
registra listeners en `prepare`. El texto original se conserva como registro de
la medición del 2026-08-04: el interruptor **Sound**
del topbar **no silencia los earcons**. Con `data-sound="reduce"` la envolvente
del earcon es idéntica a con Sound ON (`[0, 0.25, 0.25, 0.0001]`, mismos nodos).
El toggle escribe el intent y estampa el atributo, pero
`web/routes/uix/+layout@.svelte` no pasa `preferences` al canal de sonido de
sema, que se queda en nivel `'full'`. El framework sí lo soporta
(`sema/engine.ts` reenvía `soundOptions.preferences ?? opts.preferences`).
Medido sobre la envolvente del propio earcon; una ganancia de bus aguas abajo
NO se midió, así que el veredicto de «se oye o no» es del oído.
## Stamp de sema en navegador — CERRADO por las dos mitades (2026-08-04) ✅
**Mitad 1 — dónde aterriza.** Medido con un `MutationObserver` sobre el subárbol
del player, filtrando `data-event*`. Un clic en play produce, **sobre el propio
`[data-media-player-play-button]`** y no sobre el provider:
| evento | familia · intent | fase |
| -------------------- | -------------------- | -------- |
| `contact-activate` | `contact` | `active` |
| `commit-toggle-play` | `commit` · `neutral` | `active` |
…y a continuación los atributos se limpian, que es la ventana del hold
cerrándose. El stamp aterriza en el **botón**, como el morfo promete.
**Mitad 2 — la regla del pack CASA.** A/B dentro del propio player, midiendo el
pico de la envolvente programada (parche sobre `AudioParam.prototype`):
| disparo | pico | veredicto |
| -------------------------------- | ---------- | ---------- |
| play-button (transporte) | **0.0001** | silenciado |
| mute-button (transporte) | **0.0001** | silenciado |
| `commit-complete` (fin de pista) | **0.05** | audible |
Los osciladores **sí se construyen** (8 parámetros programados) pero la ganancia
nunca sube: es la resta de gain de D-AP2.7 / pack D.5, funcionando. Un `<Button>`
corriente fuera del player mide **0.25** — 2500× el transporte.
⚠️ **Trampa en la que caí**: comparé ese 0.25 de un Button de otra página con el
transporte y creí ver una contradicción con D-AP2.7. No la había — son
poblaciones distintas. **El A/B tiene que ser dentro del mismo componente.**
## F6 — matriz visual del modo audio: MEDIDA, 1 defecto (2026-08-04)
Las **16 combinaciones** (4 variantes × claro/oscuro × LTR/RTL) recorridas
programáticamente, buscando partes colapsadas, botones estrujados y desbordes:
- `card` y `row`: limpias en las 4. Sólo se oculta `live-indicator`, que es lo
correcto (la receta lo esconde si no hay directo).
- `inline`: oculta artwork/title/artist/time/rate/mute/volume — **por diseño**,
es lo que la receta declara para la nota de voz.
- **`bar`: colapsa, pero SÓLO por debajo de ~800px.** Barrido de anchos del
contenedor, con la variante verificada en la misma medida:
| ancho | scrubber | mute |
| -----: | -------: | -----: |
| 720px | **0** | **20** |
| 900px | 147 | 36 |
| 1100px | 347 | 36 |
| 1800px | 1047 | 36 |
De 900px en adelante está **sano**. Los 720px son el ancho al que **la demo**
encierra al player, no un ancho propio de una barra de app persistente (la
referencia declarada es Spotify, que ocupa la ventana entera). **Visto a
720px**: el scrubber queda reducido a un punto ámbar entre `0:00` y `6:12`
mientras el volumen conserva sus 64px. Ningún desborde en ninguna combinación.
### El diagnóstico correcto — corrección del usuario (2026-08-04)
La pregunta que yo había planteado, **«¿qué cede en `bar`?», es la pregunta
equivocada a nivel de framework**. El usuario lo zanjó: _«¿no es un diseño por
composición? debe de ser por composición y eso lo elegirá el diseñador»_. Y
encaja con la doctrina que el propio `audio-player.css` ya declara — un variant
sólo decide **qué se muestra y cómo fluye**, nunca comportamiento — y con la
salida que ya existe: componer `<MediaPlayer media='audio'>` a mano mantiene el
flujo sin opinión, porque todo el CSS de variantes cuelga de `[data-variant]`,
que **sólo estampa `<AudioPlayer>`**. Quien quiera otra mezcla a 720px, compone.
Lo que NO se disuelve en composición: que un preset **colapse en silencio**. El
diseñador no recibe ninguna señal de que está por debajo del ancho viable — el
control simplemente desaparece.
### ARREGLADO — el usuario fijó el criterio (2026-08-04)
Aclaración del usuario: no era `bar` lo que estaba mal, era **el track dentro de
`bar`**, «reducido a un punto». Y con ella, las dos decisiones de diseño, suyas:
> _«no puede ser que el slider del tracking sea más pequeño que la del
> volumen»_ · _«lo que sí quitaría del bar es lo que aparece ahora con
> Vicen…, sólo dejaría el título»_
**La causa era de una línea.** `media-player.css` le da a todo scrubber un suelo
(`min-width: var(--space-8)`) y la regla de `bar` lo **anulaba explícitamente**
con `min-inline-size: 0` y mayor especificidad — por eso flex podía encogerlo
hasta la nada y dejar sólo el pulgar.
Aplicado:
- El ancho del volumen sale a token (`--_mp-volume-width`) y el scrubber de
`bar` **se apoya en él** como suelo, así que los dos no pueden divergir cuando
alguien retoque uno.
- El **artista se oculta** en `bar` (`card` y `row` lo conservan: tienen sitio).
Medido tras el arreglo, con la variante verificada en la misma pasada:
| ancho | scrubber | volumen | artista | mute |
| -----: | -------: | ------: | ------: | ---: |
| 560px | **64** | 64 | oculto | 20 |
| 720px | **64** | 64 | oculto | 25 |
| 900px | 229 | 64 | oculto | 36 |
| 1200px | 529 | 64 | oculto | 36 |
Sin desborde en ninguno. **Visto**: los dos sliders se leen ya del mismo tamaño
y el título gana el espacio del artista.
⚠️ **Queda un estrujado que NO toqué**, porque arreglarlo sí arbitra: el
`mute-button` baja a **20–25px** por debajo de 800px (su tamaño es 36). Ponerle
`flex: none` empujaría el problema a otro control, y eso es decisión de diseño.
Lo que sigue sin resolverse en composición y queda vivo:
- ~~documentar en el README el ancho mínimo de `bar` (~900px), que es un valor de
diseño de la variante, como ya lo son los tamaños de carátula.~~ **HECHO
2026-08-12** (README de eidos, sección de variantes, con la disposición del
orden de descarte firmada).
## Pendiente que solo puede hacer el usuario
- ~~Escucha A/B de F2~~ ✅ **hecha: «suena igual» (2026-07-31)**.
- Prueba manual de teclas de medios / pantalla de bloqueo — **cuando exista una
página que consuma `sound.media()`** (el re-plan del player la traerá).
## Por dónde sigue (reescrito 2026-08-04 — lo anterior estaba cumplido)
⚠️ **Higiene del documento**: la tabla de Estado de arriba dice «sin commitear»
del rediseño y de las fases F4/F5 del waveform. **Es rancio y está verificado**:
el rediseño es `2f872ef86`, el waveform `0ab6cc822` + `905012507`, y esas rutas
están limpias en el árbol. El «re-plan del reproductor sobre el servicio» que
esta sección pedía **también está hecho** (F3: el player consume
`uix.sound.media(el, { metadata })`), igual que sus dos pendientes de cierre —
escenario mixto y teclas de medios.
De este hilo quedan **cuatro cosas, y ninguna es código a ciegas**:
1. ~~**La mirada del waveform (su F6)**~~ ✅ **APROBADA por el usuario
(2026-08-04): «waveform … las veo bien».** Cierra el pendiente que arrastraba
desde el 2026-08-01.
2. ~~**La mirada sobre el resto del modo audio**~~ ✅ **LAS CUATRO APROBADAS.**
`card` el 2026-08-04 («la variante card la veo bien») y **`row` e `inline` el
2026-08-05** («ya te las di por buenas row e inline»). El eje visual del modo
audio queda CERRADO — no volver a listarlas como pendientes.
3. ~~**`bar` por debajo de su ancho**~~ ✅ **ARREGLADO** (ver §F6): el scrubber
ya nunca se dibuja más estrecho que el volumen y el artista sale de `bar`,
las dos por decisión del usuario. **Queda vivo** el `mute-button` estrujado a
20–25px bajo 800px — no lo toqué porque arreglarlo empuja el problema a otro
control, y eso es diseño.
**Disposición FIRMADA (2026-08-12)**: el ancho mínimo (~900px) queda
documentado en el README de eidos como valor de diseño de la variante; el
arreglo real firmado es ORDEN DE DESCARTE — bajo el umbral cae el slider de
volumen y el mute recupera sus 36px — con container query como mecanismo
honesto: el primer caso concreto que justifica reabrir D-AP2.9.
4. ~~**El rojo de `audio-player` en `lint.test.ts`**~~ **RESUELTO — la decisión
ya estaba tomada en el código** (verificado 2026-08-12, 18/18 verde): existe
la categoría `COMPETING_ROOTS` (`lint.test.ts:70`) con la razón escrita,
separada a propósito de `KNOWN_MISSING_MORFO`, que «prometería una
construcción que no debe ocurrir jamás».
## Sesión 2026-08-04 (tarde) — cuatro entregas, todas verificadas en navegador
Commits `ee51172d8` · `5cbb03904` · `dbd239bcc` (+ el waveform, `127c4c95b`).
El detalle está en los mensajes y en el README de eidos del componente; aquí
sólo lo que cambia el mapa.
1. **El waveform ya se VE en la demo.** No se veía por dos causas encadenadas:
el scrubber compone el Waveform de _soma_, así que su envoltorio de eidos
nunca montaba y **su receta no se importaba** (onda sin estilar, 255px de
alto); y la muestra local era `whoosh.wav`, de **0,18 s**. El player carga
ahora `waveform.css` igual que ya cargaba `slider.css`, y la demo usa
`/music/t2.wav` (2:44). ⚠️ **Lección reutilizable**: componer un primitivo
desde soma deja su receta fuera — la carga el componente que lo compone.
2. **La velocidad ya se puede BAJAR** (`RateFloat`). El `RateButton` sólo subía
y recortaba en `2×`. Ahora `AudioLayout` compone un `DropdownMenu` +
`RadioGroup`; el disparador sigue siendo el `RateButton` real, con su ciclado
anulado vía `preventDefault()` en la costura. ⚠️ Dos defectos del panel
portado, los mismos que el float de volumen: el suelo de `12rem` del menú, y
**la etiqueta redimensionaba el botón y desplazaba la botonera** (ancho fijo
`4.6em`, medido 64·74·88·121px).
3. **API de eventos para la app** — `onPlay` · `onPause` · `onEnded` ·
`onError`. Antes no había NINGUNA: la única vía era enchufar un
`MediaProvider` entero. Dos divergencias deliberadas respecto a la capa
perceptual, las dos en el test: el `pause` del final natural no se reporta, y
`onEnded` SÍ dispara para un final por salto (que `commit-complete` exime).
4. **Transporte de lista** — `RestartButton` + `SkipButton` (morfo **25** partes,
las dos opt-in, sin eventos sema nuevos por decisión del usuario). El player
**no tiene cola a propósito**: `onPrevious`/`onNext` piden y la app cambia el
`src`. Una dirección sin manejador se pinta deshabilitada, que es justo el
aspecto de los extremos de una cola.
5. **Selector de pista de subtítulos** (`CaptionFloat`, `21d817c1f`).
`toggleCaptions()` sólo alcanza la PRIMERA pista — a propósito, es un toggle,
y las demás quedan en `disabled` porque **`hidden` obliga al navegador a
descargar el VTT**. Con dos idiomas no había forma de llegar al segundo.
Añadidos a soma: `selectCaptionTrack(track|null)` y los espejos reactivos
`captionTrackList` / `activeCaptionTrack` (el `TextTrackList` vivo NO
notifica, así que un chrome que LISTA pistas no veía las que llegan tarde).
⚠️ Cambiar de idioma **no** dispara `commit-toggle-captions`: sólo lo hace una
transición real de encendido/apagado.
6. **La calidad, especificada sin implementar** (`e0206f6b2`).
`docs/decisions/design-media-quality.md`. Grep del puerto: **cero** campos de
calidad, y no es descuido — los niveles los aporta un motor HLS/DASH que este
framework no vendoriza. Por eso `settings-button` **sigue sin implementar**:
la velocidad tiene lista propia, la pista también, y el tercer eje no existe.
Un engranaje con un solo cajón sería el `CaptionFloat` disfrazado.
## Cola de la revisión adversarial — CERRADA (2026-08-05)
Una revisión adversarial (5 lentes × 3 escépticos) devolvió 15 supervivientes de 19. Los míos están arreglados en `aaced3ebe` (portal + `dir` + paridad de la demo
- tabla de eventos) y los cuatro que dependían del provider, en cuanto la otra
sesión commiteó (`5983b00d5`), quedaron así:
1. ✅ **`toggleCaptions` ya no descarta la pista elegida.** Recuerda la última
elegida (`preferredCaptionTrack`) y la RESTAURA al volver a encender; cae en
la primera sólo si nadie ha elegido, que es el caso de una sola pista y el
comportamiento de siempre. Medido: elegir Español, `c`, `c` → sigue Español.
2. ✅ **Fuga de teclas desde el disparador.** La guarda de propiedad se ensanchó
a `[data-slider],[role='menu'],[aria-haspopup='menu']`, que es justo lo que su
propio comentario ya predicaba («the exemption is by ownership, not by tag»).
Medido: con el foco en el disparador, cuatro teclas y nada se mueve; con el
foco en el player, `ArrowDown` sigue bajando el volumen — no se ensanchó de
más.
3. ⚠️ **`aria-pressed` + `aria-keyshortcuts='c'` sobre el disparador del
`CaptionFloat`: NO arreglado, y no se puede desde ahí.** Se intentó anularlos
en la costura y **se ignoran en silencio**: soma compone
`mergeProps(restProps, state.props)`, así que los atributos que posee el morfo
ganan a lo que le pases — que es el sentido del contrato. El override muerto
se retiró para que nadie crea que funciona.
**Queda como DECISIÓN DE MORFO** (necesita tu criterio): el `caption-button`
declara `aria-pressed` incondicionalmente. Dentro del float se comporta como
disparador de menú (`aria-haspopup` + `aria-expanded`) y anunciarse además
como alternancia pulsada es contradictorio. Opciones: (a) dejarlo —una parte
compuesta de dos formas no puede tener dos ARIA—, (b) hacer `aria-pressed`
condicional en el morfo, (c) declarar una parte aparte para el disparador de
lista. **No lo decido yo.**
**FIRMADA Y EJECUTADA (2026-08-12): la (b).** `aria-pressed` es condicional
en el morfo (`prop-falsy menuTrigger` — el patrón `fieldLabelled`,
`morfo.md` Step 4); el provider publica la fuente por parte y el
`CaptionFloat` pasa `menuTrigger`. Test visto fallar: compuesto como
disparador de lista el attr queda genuinamente AUSENTE; solo, sigue
anunciando su estado. `aria-keyshortcuts='c'` se queda — la tecla alterna
los subtítulos desde cualquier foco, así que el anuncio es verdad.
4. ✅ **`aria-keyshortcuts='c'`** deja de ser un problema de comportamiento en
cuanto el punto 1 está arreglado: la tecla ya no descarta nada. Queda sólo
como parte del punto 3 (qué se ANUNCIA sobre ese botón).
## Lo que NO he podido verificar (pendiente de tus ojos)
⚠️ **El teclado real sobre los tres floats.** El panel de navegador embebido dejó
de componer frames a media tarde (`document.visibilityState === 'hidden'`), y con
él **no llegan las teclas**: instrumenté un listener y una pulsación real de
Escape registró **cero** eventos. Llegué a creer que había un defecto —«Escape no
cierra el float dentro del player»— y **era falso**: lo roto era mi vía de
entrada, no el componente. Misma familia que las transiciones CSS congeladas.
Verificado igualmente con eventos despachados: un `keydown` de Enter/Espacio abre
el menú, y el `click` que el navegador sintetiza detrás **no** toca los
subtítulos (el `preventDefault()` de la costura aguanta). Falta comprobar con
teclado de verdad: abrir con Enter desde cerrado, moverse con flechas, y cerrar
con Escape.
⚠️ **Otra sesión commitea en esta misma rama** (hilo de dirección): verificar
`HEAD` antes de dar nada por perdido, y `git reset -q` + `add` sólo lo propio.
El siguiente ítem de CÓDIGO del componente no es de este hilo sino del handoff
hermano: **`settings-button`**, lo único que separa al morfo de 25/25 — y su
alcance ENCOGIÓ: la velocidad ya tiene su lista, así que al menú de ajustes sólo
le quedan calidad y pista.
Ver [`CONTINUE-player-rtl.md`](./CONTINUE-player-rtl.md) §0.
## Trampas de este repo (verificadas a base de perder tiempo)
- **NO arrancar un dev server si el del usuario está vivo** (`vite dev --force`
reescribe `deps` → dos runtimes → `lifecycle_outside_component`). Acoplarse al
puerto 5173. Recuperación: parar node → borrar `node_modules/.vite/deps` →
`npm run dev` → Ctrl+Shift+R.
- **El panel del navegador no pinta** (viewport 0×0); `javascript_tool` sí — con
eso se midieron los contextos. Matiz 2026-08-01: con el panel oculto las
CSSTransitions quedan `running` CONGELADAS → `getComputedStyle` devuelve el
valor intermedio del from, no el target de la cascada. Para verificar estilo,
leer las custom props (`getPropertyValue`), no los valores usados.
- **MUTEAR antes de reproducir, y PARAR al terminar.** Medir el escenario mixto
y los earcons exige reproducir de verdad, pero el Chrome que suena es el DEL
USUARIO: dejar una pista de 6 minutos sonando a volumen 1 mientras se hacen
otras cosas es una molestia real, y pasó (2026-08-04). `el.muted = true` basta
para casi todo lo que se mide —contextos, stamps, envolventes, geometría—
porque nada de eso depende de que se oiga; el volumen sólo hace falta para la
escucha, que es del usuario. Y al acabar cada medición, `el.pause()`.
- **Un guard que no has visto fallar no vale** — quitar el cableado y ver el rojo.
- **Una medición no es un diagnóstico** — README → CANON §8 → book-deviations →
código, antes de acusar a una pieza.
- **Filtrar `npm run check` con cuidado**: rutas con `\\` doble; un patrón con `/`
no casa y da falsos «0 propios».
- **El árbol está compartido con otras sesiones** — `git reset -q` + add solo lo
propio, en un bloque atómico. `contracts.test.ts` viene modificado de otra
sesión: **no tocarlo en E1** (su fila `sound` no pinea métodos y sigue verdadera).
## Ajenos y preexistentes detectados (NO tocados)
- `contracts.test.ts` 3 rojos ajenos: `aura`, `menubar`, `radio-group`.
- `docs:check` 1 error ajeno: `eidos/components/callout/README.md:23` («8 roles», son 9) — **ya no aparece el 2026-08-01 (docs:check 0)**; alguien lo arregló.
- `temas/sema/_lib/audition.ts`: 2 errores de tipos preexistentes.
- **`lint.test.ts` 1 rojo: `audio-player` sin morfo** — deuda de NUESTRO F5 del
player (`c4d64ba98`): es la capa de variantes GATED sobre el morfo del
media-player (su css engancha solo `data-media-player-*` + `data-live`), no
una violación morfo-first, pero el test no tiene categoría para «variant
layer sobre morfo hermano». Decisión pendiente (¿nueva categoría con
comentario vs KNOWN_MISSING_MORFO?) — es un guard compartido, no se tocó.
- Demo del slider: `sliderMorfo.parts[2].keyboard` roto por el desplazamiento
de índices al nacer SecondaryRange (G-2) — 1 error de `check` preexistente;
chip de tarea lanzado con el fix (seleccionar la part por kebab, patrón de la
demo del waveform).
- Prettier: `component-visual-attrs.test.ts`, `lib/recipes/base.ts` y
`scripts/eidos-lint-all.ts` ya fallaban `--check` EN HEAD (¿CRLF de otra
sesión?); mis líneas siguen el estilo del entorno y no se reformateó el
fichero entero (sería cascada).

Powered by TurnKey Linux.