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-player-rtl.md

21 KiB

CONTINUE — el media-player (nació del hilo RTL, que ya cerró)

Kickoff: "Lee docs/process/CONTINUE-player-rtl.md y sigue por la cola de §0." Fecha: 2026-08-01 · actualizado 2026-08-04 · Rama alpha-0.1-dir-prefs (sale de alpha-0.1-sec-dom). §1, §2, §3 y §4 están CERRADAS y verificadas — no las rehagas. El documento arrancó siendo del eje RTL; hoy es el handoff del componente. Empieza por §0, que es la cola priorizada; §1–§4 son el registro de lo hecho, y se leen sólo si necesitas el porqué de una decisión.


0. La cola — por orden de valor (estado a 2026-08-04)

Dónde está el componente

component:audit --only media-player = PASS (0 errores; los 2 avisos son falsos positivos del script, que busca MutationObserver y uix.events.emit como TEXTO en la página y no los ve porque la demo los factoriza en DemoTrace y SemaPanel — comprobado en el código de ambos).

Guard Estado
check 0 errores propios (la base global fluctúa por las otras sesiones)
vitest soma del player 14/14
eidos-lint media-player invalid 0 · class-hooks 0 (morfo-backed 29)
morfo:check player sin fallos (7 preexistentes ajenos)
smoke de la ruta PASS
perm:check SIN CORRER — es el largo; el topbar dio paso a todas las rutas

Partes declaradas sin implementar: 1 de 23. Con Captions cerrado sólo queda settings-button.

0.1 · settings-button — el ítem grande restante

Menú de velocidad / calidad / pista. Exige componer DropdownMenu a nivel soma; la mecánica ya existe (setPlaybackRate, cycleRate, y el RateButton los usa). Cerrarlo pone el contrato del morfo en 23/23, que es lo único que separa al componente de tenerlo completo.

0.2 · Waveform como scrubber — DESBLOQUEADO, faltan dos precisiones

Las decisiones del usuario (§5) resolvieron el nudo. Antes de implementar hay que decidir —y NO improvisar— dos cosas:

  1. Qué es exactamente «genérico»: ¿patrón fijo, pseudoaleatorio estable derivado de la fuente, o barras planas?
  2. ¿Basta la ausencia de peaks como señal de streaming, o hace falta una prop aparte?

0.3 · Defectos vivos, medidos

  • La variante bar se estruja a 720px: el time-slider colapsa a ancho 0 y el mute a 19px (los demás miden 36). Pasa igual en LTR y RTL, así que no es del eje de dirección — la fila no tiene sitio para lo que mete.
  • audio-player rojo en eidos/lint.test.ts: la capa de variantes no tiene morfo propio y el guard no tiene categoría para «capa de variantes sobre morfo hermano». Es un guard compartido; decisión pendiente (¿categoría nueva o KNOWN_MISSING_MORFO?). No tocar en solitario.

0.4 · Sin verificar por ojos

La matriz 4 variantes × claro/oscuro × RTL. Cubierto a 2026-08-04: vídeo LTR/RTL, audio card en RTL, y los subtítulos en claro y oscuro. Faltan row / bar / inline y casi todo el modo oscuro.

0.5 · Diferidos con disposición escrita (no son deuda oculta)

Doble-tap-seek · preview de miniatura en el scrubber —que es un gap del Slider, no del player— · migrar los botones a renderProps, que es un barrido global del catálogo.

0.6 · Ajeno al player, abierto por estas sesiones

  • El interruptor Sound del topbar no silencia los earcons (§4.2). Es de la app de demos, no del framework.
  • src/arts/adom/__scratch-verify.ts: fichero de sonda que dejó un agente, sin trackear, suma 1 error a check. Sobra; no se borró por la regla de no eliminar sin instrucción explícita.
  • Del hilo de dirección: html lang no sigue al idioma (el dir sí) y la deuda ajena de CONTINUE-direction.md §8.4.

El hilo de audio propiamente dicho sigue en CONTINUE-sound-engine.md; este documento recoge lo que la mirada del usuario abrió el 2026-08-01 y quedó SIN cerrar.

Lo que se cerró el 2026-08-01 (7 commits, verificados)

Commit Qué
df86a4e50 El anillo del commit dejaba de enmarcar contenedores (slider + media-player) · saltar al final ya no anuncia compleción falsa
e096f65e3 MediaSession verificada por el usuario (carátula + título + artista en el hub de Chrome)
905012507 Waveform: el buffered vuelve a banda recta con acento lavado
58acb778e Waveform F6 firmada — iniciativa COMPLETA
77f8a4e46 La demo del waveform carga solo la pista real; sample deja de fingir que es una prop
4356020e3 La carátula de card sale a token (--audio-player-artwork-size-card)
3ceda5b59 El botón de velocidad ya dice su velocidad (emit: 'value') · el player reenvía dir a sus dos sliders

1. PRIORIDAD — el Slider está roto en RTL · CERRADO 2026-08-02

Cerrado en 013ceac57 + 5469e05df (rama alpha-0.1-dir-prefs). La hipótesis de este documento acertó las dos piezas visibles — el Slider sí estampaba dir y slider.css sí mezclaba lógicas con físicas — pero eran síntomas, no la causa. Lo que resultó ser, medido:

Del slider slider.css emparejaba inset-inline-start: 50% (lógico) con translateX(-50%) (FÍSICO, no se voltea) en las tres reglas verticales. Medido en Chrome: raíz/pulgar/ticks en x=961, raíl y relleno en x=955. El Tick se libraba porque ya usaba margin-inline-start — ése era el idioma correcto del propio fichero. Mismo defecto en accordion.css (text-align: left), el único físico que quedaba en todo eidos.
La causa raíz, que no estaba en las hipótesis makeDimension().get() (arts/prefs) leía engine.snapshot() esquivando la celda $state, así que soma.prefs.getDir() era una lectura sin tracking y los ~64 componentes que resuelven su dirección desde prefs la congelaban al montarse. La proyección DOM se salvaba porque usa onChange: misma dimensión, dos caminos de lectura, uno solo reactivo.
Y el estampado 33 de 37 componentes escribían dir con un valor SIEMPRE concreto → una app que ponga <html dir="rtl"> sin registrar la preferencia se encontraba 33 islas del revés. Ahora el atributo se omite cuando nadie afirmó nada (modelo de Zag, aplicado uniforme).

No hizo falta elegir «un solo dueño de la inversión»: el JS sigue calculando físico y el CSS sigue siendo lógico. No había doble volteo — había un transform físico donde tocaba uno lógico.

✅ La contradicción del pulgar del volumen queda explicada: con getDir() congelado, el mismo slider daba lecturas distintas según cuándo se midiera respecto al montaje. No era una sonda mintiendo.

Guards que nacieron en rojo y ahora pasan: arts/prefs/test/active-prefs-reactivity.svelte.test.ts (intent · derivación · granularidad por clave) y uix/active-uix/test/prefs-view.svelte.test.ts.

De regalo: elegir árabe voltea la dirección por derivación desde el idioma — la cadena que active-architecture.md:407 documenta y que en esta app era código muerto, porque la shell fijaba intent desde el primer frame y no había ningún idioma RTL en el catálogo.

⚠️ Sigue abierto de este eje: html lang no sigue al idioma (el dir sí), y perm:check sin ejecutar — el topbar ya lleva data-perm-step, lo que hace que toda ruta tenga paso y la pasada sea mucho más larga.

2 y 3. Disposición de los botones + iconos de seek · CERRADAS 2026-08-04

Eran el mismo defecto visto desde dos lados. Las sondas de aquel día acertaban y §2 no era un defecto de disposición: re-medido con el contexto verificado ANTES de cada número (data-media / data-variant / data-dir leídos tras cada clic), la geometría mirroriza exacta en las cinco disposiciones — vídeo con DefaultControls y audio en card · row · bar · inline. Cada x en RTL es el espejo del LTR ±1px, comprobado punto por punto.

Lo que el ojo rechazaba eran los glifos, que no se volteaban (transform: none, rotate: none). En RTL el transporte se leía ▶▶ ▶ ◀◀: las dos flechas exteriores apuntando HACIA DENTRO. Es literalmente el síntoma que carousel.css:235 y §9.9 de CONTINUE-direction.md ya habían documentado y arreglado en los chevrons del carousel.

LTR (izq→der) RTL antes RTL ahora
transporte (vídeo) ▷ ◁◁ ▷▷ ▷▷ ◁◁ ▷ ✗ dentro ◁◁ ▷▷ ◁ ✓ fuera
transporte (audio) ◁◁ ▷ ▷▷ ▷▷ ▷ ◁◁ ✗ dentro ◁◁ ◁ ▷▷ ✓ fuera

Alcance elegido por el usuario: todo lo direccional — seek · play/pause · altavoz de volumen/mute · esquina del PiP. Fuera se quedan fullscreen (sus cuatro esquinas son simétricas) y captions, que es la única llamada que el catálogo no ha hecho: si el glifo de subtítulos cuenta como direccional está sin decidir, y se dejó SIN voltear.

El mecanismo, heredado del precedente del carousel:

  • scale: -1 1 en el recipe, no transform — la propiedad independiente COMPONE con lo que un icono escriba inline en vez de reemplazarlo.
  • scale, no rotate: 180deg: sólo coinciden en glifos verticalmente simétricos. Rotar el icono del PiP le llevaría el recuadro a la esquina superior izquierda en vez de a la inferior izquierda.
  • Anclado a [data-media-player][data-dir='rtl'] — el atributo PROPIO del componente, no el dir del DOM, así que no es el [dir='rtl'] prohibido y garantiza que el glifo coincida con la matemática del teclado, que resuelve de esa misma fuente. eidos-lint media-player: invalid 0 · class-hooks 0.

De paso: el --slider-range-bg del scrubber era un linear-gradient(90deg) FÍSICO, así que en RTL la rampa corría al revés respecto al sentido en el que crece el rango. Par :dir() con 270deg, como la rampa de saturación del color-picker.

2.1 · El dir del player estaba PARTIDO POR LA MITAD — encontrado por el camino

<MediaPlayer dir="rtl"> sin dir ambiental dejaba el cromo dispuesto en LTR y sus dos Sliders en RTL. El wrapper pasaba la prop CRUDA (readableActive(() => dir)) y el provider sólo la reenviaba a los sliders: no resolvía la cadena ni estampaba nada. En la demo estaba enmascarado porque el stage estampa dir además de pasar la prop — por eso ninguna medida anterior lo vio.

⚠️ El censo de canonización de CONTINUE-direction.md §9.15 no lo tenía: el media-player no es ni canónico, ni grupo A, ni grupo B. Era un cuarto caso — expone dir, pero no lo resuelve NI lo estampa. Si alguien vuelve a auditar la canonización, ese censo cuenta wrappers y providers, no componentes que simplemente reenvían la prop a un hijo.

Canonizado con el patrón de §9.16: activeDir(() => dir, soma) en el wrapper · resolvedDir en el provider · dir crudo + data-dir resuelto en los props. El tipo pasó de dir?: Direction a dir: Direction | undefined (la clave siempre la pone el wrapper; sólo el VALOR puede faltar) — ensanchar el tipo primero, que es lo que destapó los 11 puntos del test.

2.2 · El teclado seguía siendo físico

El morfo declaraba ArrowLeft→seek-backward / ArrowRight→seek-forward, ciego a la dirección, mientras soma ya tiene getDirectionalKeys(dir, orientation) justo para esto y el Slider embebido sí es direccional. Decisión del usuario: direccional. Ahora resuelve por ese helper. j / l se quedan quietas: son alias semánticos, no flechas.

Medido, y raíz y pulgar de acuerdo (un solo seekStep, sin doble salto):

LTR RTL
ArrowRight (raíz / pulgar del scrubber) +10 −10
ArrowLeft (raíz / pulgar del scrubber) −10 +10

2.3 · Verificación

check 74 = línea base · rtl:check 1 (el de palabras, preexistente) · morfo:check 7 fallos, ninguno del player (preexistentes) · eidos-lint media-player invalid 0 · vitest del player + los dos guards de dirección 16/16. A ojo en el Chrome real del usuario, A/B completo en LTR y RTL, vídeo y audio/card.

4. Medición del escenario mixto · CERRADA 2026-08-04

La receta que este documento prescribía no hacía falta. «Instrumentar antes del boot + navegación client-side» era la respuesta a la pregunta equivocada. El problema real es que se parcheaba el CONSTRUCTOR, que sólo puede responder «¿se creó uno nuevo?». La solución es parchear el PROTOTIPO:

Cualquier contexto —nacido cuando sea— tiene que llamar a createGain / createOscillator / resume para hacer algo. Recogiendo el this de esas llamadas en un Set se enumeran las INSTANCIAS, no las creaciones, y un contexto preexistente aparece igual. Sin inyección pre-boot y sin tocar app.html.

Con eso, medido en el Chrome del usuario sobre /uix/components/media-player, audio card, pista real de 6:12 sonando sin mutear a volumen 1:

Contextos distintos que trabajaron 1 (running, 44,1 kHz)
Earcons disparados encima 26 (52 osciladores + 78 gains + 26 biquads — el par de osciladores + biquad lowpass que describe PLAN-sound-engine.md §4)
createMediaElementSource 0 — la media NUNCA entra en el grafo
Contextos cerrados / churn 0 / 0 (contraste con el «antes» de §15.2: 10 creados, 9 cerrados)

La invariante de un solo AudioContext por documento se sostiene en el escenario mixto.

4.1 · Y el contexto preexistente, por fin fechado

Es real y la instrumentación vieja no podía verlo. Con la sonda instalada en t=10,9s y el primer earcon a t≈30s, el reloj del contexto marcaba 25,86s → el contexto nació en t ≈ 4,1s, sin ningún gesto de usuario, y ya running. Dos señales independientes lo confirman: constructed: 0 (el parche del constructor no lo vio nacer) y la primera aparición es createOscillator, no createGain — los tres createGain que createContext() hace de inmediato para el master y los dos buses ya habían pasado.

⚠️ Mi primera hipótesis —que lo creaba el registro de sound.media(el) del player— es FALSA, y el control la tumbó: en /uix/components/button, una página sin ningún elemento media, el contexto nace en t ≈ 4,08s, lo mismo. Es del arranque de la app, no del reproductor.

Auditoría de código en paralelo (29 agentes, cada afirmación refutada adversarialmente) que respalda la lectura del navegador:

  • Sitio de construcción único: arts/sound/engine-sound.ts:452, y el ctor se busca en globalThis en cada llamada, nunca cacheado a nivel de módulo — por eso constructed: 0 significa de verdad «nació antes del parche» y no «se coló por una referencia capturada».
  • Embudo único createContext() con tres llamadores (prime · getOrCreateContext · ensureContext); el motor no toca Web Audio hasta uno de ellos.
  • El player estructuralmente no puede meter la media en el grafo: guarda el handle tipado como MediaProvider, puerto que no declara attach(). Es más fuerte que «es opt-in y nadie lo activa».

4.2 · Hallazgo NUEVO y ajeno: el interruptor de Sound del topbar no silencia

Salió de la auditoría y lo verifiqué midiendo. Con data-sound="reduce" (interruptor OFF, soundOn: false persistido) un clic en un Button sigue construyendo 2 osciladores + 3 gains, y la envolvente es idéntica:

Sound ON   [0, 0.25, 0.25, 0.0001]
Sound OFF  [0, 0.25, 0.25, 0.0001]      ← mismo pico, mismos nodos

El toggle escribe el intent en prefs y estampa data-sound en <html>, pero el root de composición (web/routes/uix/+layout@.svelte) no pasa preferences al canal de sonido de sema, que cae a nivel 'full'. El framework sí lo soporta (sema/engine.ts reenvía soundOptions.preferences ?? opts.preferences); es la app de demos la que no lo cablea.

⚠️ Alcance de lo que medí: la envolvente del PROPIO earcon. No medí una ganancia de bus aguas abajo, así que no puedo afirmar «se oye igual de fuerte» — eso lo zanja el oído. Dos líneas independientes (medida + auditoría) apuntan a que el interruptor es inerte. Es de la app de demos, no del player, y no se tocó.

4.bis · Ajeno al eje, encontrado midiendo — la variante bar se estruja

A 720px de ancho y size='md', la variante bar deja el time-slider en ancho 0 y el botón de mute en 19px (los otros miden 36). Pasa igual en LTR y en RTL, así que no es del eje de dirección: la fila no tiene sitio para todo lo que mete. Sin tocar.

5. Iniciativas dormidas (cada una con su gate)

  • Captions — CERRADA 2026-08-04. Se implementó tal como el diagnóstico predecía: mode='hidden' + pintar activeCues en la parte declarada. El espejo de una sola dirección de captionsOn también cerró: el estado se deriva de los modos reales y una pista que el SO encienda se adopta (degradada a hidden, para que no pinte encima). Detalle y las tres trampas de especificación que impuso —borrado imperativo, siembra del primer pintado, activeCues nulo en disabled— en el README de eidos, §Captions. Decisión declarada: el marcado inline se conserva, la metadata de posición se descarta.

  • Waveform como scrubber del player — cero cables hoy entre ambos, y es deliberado (D-AP2.4 + PLAN-waveform.md §0 lo aplazan a la v2 del player). El nudo NO era cambiar el componente: era de dónde salen los picos. El contrato del waveform (D-WF.2) dice que los provee la app, y un player streamea, así que no puede decodificar su fuente sin bajársela entera. Verificado además que fetch de media.w3.org y soundhelix.com falla por CORS mientras el <audio> las reproduce sin problema.

    ✅ NUDO RESUELTO — decisión del usuario, 2026-08-04. Dos piezas:

    1. Los picos se pasan como PROP. No se generan en el player ni se regeneran solos: los provee quien los tenga (el backend que ya los calculó al subir el fichero, un .json junto al audio, una decodificación previa). Ratifica D-WF.2 y lo lleva a la API del player. Evita, además, el coste de recalcularlos en cada montaje.
    2. Si la fuente se streamea, waveform GENÉRICO. Sin picos que decodificar, el player pinta una onda genérica y sigue sirviendo de scrubber. Se acabó el bloqueo del CORS y el «bajarse la pista entera»: el caso sin datos tiene ahora una respuesta declarada en vez de quedarse sin componente.

    Queda por decidir al implementarlo (NO decidido): qué es exactamente «genérico» — patrón fijo, pseudoaleatorio estable derivado de la fuente, o barras planas — y si peaks ausente basta como señal de streaming o hace falta una prop aparte.

Lecciones de método del 2026-08-01 — leer antes de repetirlas

  • Medir el contrato NO es verificar la experiencia. Se afirmó tres veces que algo estaba verificado midiendo JS: la MediaSession (metadata poblada ≠ overlay del SO visible), el botón de velocidad (cambiaba playbackRate perfectamente… y no lo mostraba) y el RTL (se comprobó LTR y se dio RTL por bueno). Las tres las cazó el usuario.
  • El panel del navegador embebido NO SIRVE para nada visual: innerWidth: 0, visibility: hidden, el stage mide 2px. Ese día inventó tres «overflow» inexistentes y un hueco de carátula que estaba bien. Screenshot falla con «not compositing frames». Para cualquier juicio visual, usar el Chrome real del usuario (mcp__claude-in-chrome__*).
  • Comprobar que el control se pulsó de verdad. Dos medidas de esa sesión midieron el modo vídeo creyendo que era audio porque el chip no cambió y nadie lo verificó. Leer siempre data-media / data-variant después de clicar.
  • La distancia RGB engaña: avaló un gris que el ojo del usuario rechazó al instante. Está dominada por la luminosidad y es ciega al croma.
  • La forma manda sobre el color: el buffered del waveform se rechazó dos veces; la segunda porque una copia de la onda es la onda y se camufla por construcción, por muy distinto que sea su color.

Powered by TurnKey Linux.