31 KiB
PLAN — Reproductor de sonido (AudioPlayer)
Tipo: plan de creación por fases (process — efímero, NO fuente de verdad). Fecha: 2026-07-30 · Estado: ⏸️ APARCADO 2026-07-30 — bloqueado por
PLAN-sound-engine.md. Decisiones D-AP.1…D-AP.10 SIN FIRMAR. No arrancar F0 hasta que el art$soundexista (su F5 desbloquea éste). Kickoff para sesión nueva: "Leedocs/process/PLAN-sound-engine.mdprimero; este plan está aparcado detrás de él."⚠️ Correcciones pendientes de incorporar (sesión 2026-07-30, posteriores al cuerpo)
El cuerpo de abajo NO las refleja todavía. Al desaparcar, incorporarlas ANTES de presentar nada:
- Faltaba la reproducción de SEGMENTOS entera. No está en la matriz, ni en el contrato, ni en las variantes. Y Vidstack SÍ los tiene (
clipStartTime/clipEndTime, con los que calcula el rango seekable), así que es paridad pendiente, no superación. Diseño acordado:clip = { start, end, loop? }en el provider · capa de proyección absoluto↔relativo (dominio del slider0…end−start,timeBase: 'clip' | 'source'enTime, misma base enaria-valuetext) · clamp en el provider, no en el puerto ·commit-completedispara en el fin del clip (mismo evento, frontera movida) · conloopNO haycommit-complete(un bucle esperado no es ocurrencia — regla B.5) · limitación declarada:timeupdate~4/s ⇒ punto de bucle impreciso sin Web Audio · riesgo a probar: bug abierto de clip en Chrome/ Safari móvil (vidstack#1195).chapters= v2 ·regions= descartado.- G-2 (pista secundaria del
Slider) sube a BLOQUEANTE de v1: ya tiene tres consumidores — buffer + ventana de clip + capítulos.- D-AP.11 queda obsoleta y se reescribe contra el art. Su premisa («no hay motor, ¿dónde lo ponemos?») era falsa: el ecosistema ya tenía un motor Web Audio de 407 líneas dentro de
sema/chans/sound.ts. Cuando este plan se desaparque,$soundya existirá y el player solo declara si lo consume.- Dos invariantes a escribir en el contrato: (a)
prefs.soundnunca toca el volumen del contenido — gobierna el sonido de UI (sema); un reproductor de sonido es el primer componente del catálogo donde «sonido» significa dos cosas. (b) El silencio de UI en audio (D-AP.7) debe alcanzar alSlidercompuesto: existeresolveSliderDragSoundy arrastrar el scrubber sonorizaría por encima del contenido — verificar con stamp real y, si dispara, la regla es[data-media-player][data-media='audio'] [data-slider].- D-AP.7 se reescribe como ducking: con un motor dueño del earcon y del contenido, deja de ser regla de cascada y pasa a capacidad del motor.
Antes de escribir una línea de código: presentar al usuario las decisiones de §4 y obtener firma. Lo de aquí son propuestas razonadas, no decisiones tomadas — regla A32 (
component-guide.md) + §0.5 decomponent-audit.md("Get the user's sign-off on scope before writing any code").Precondición cumplida: corpus doctrinal leído completo antes de auditar (regla dura
feedback_read_full_doctrine_before_auditing): README · overview · active-architecture · CANON · morfo · sema · soma(+architecture) · eidos · active-uix · active-app · packs · blocks · agent · TSC · recipe-contract · vocabularies · component-guide · completion-checklist · component-audit · demo-authoring · theming(reference/guide/notes/motion/motion-guide/channels/ gradient-finish) · los 7 RFCs · book-deviations · design-text-effects · design-chat-block · glossary · comparison · authoring · testing-and-tooling · next-features · decisions · book-map · guía histórica · veredictos/_system/ _naming/_cierre de la auditoría ·eidos/components/README.md·arts/README.md.
0. Contexto — qué existe hoy y qué NO
El ecosistema ya tiene un reproductor: media-player, genérico
<video> + <audio>, cerrado como PASS en la matriz de aceptación.
| Capa | Estado | Evidencia |
|---|---|---|
| morfo | 534 líneas · 17 partes · 12 eventos · expression: 'pack' · apg: 'none — …' |
morfo/components/media-player.ts |
| soma | provider request-driven + puerto MediaProvider + adaptador nativo + 14 wrappers + test 9/9 |
soma/components/media-player/ |
| sema | pack «voz de transporte contenida» (3 reglas) | sema/components/media-player.ts |
| eidos | recipe Lumière (11.7 KB) + 17 wrappers + DefaultControls |
eidos/components/media-player/ |
| demo | 9 pestañas, grupo Media del sidebar | web/routes/uix/components/media-player/+page.svelte |
El modo audio existe, pero es un reproductor de vídeo degenerado. Todo el soporte «audio» del catálogo son tres reglas CSS:
media-player.css:79 [data-media='audio'] [data-media-player-media] → height: 0
media-player.css:161 [data-media='audio'] [data-media-player-title] → position: static
media-player.css:191 [data-media='audio'] [data-media-player-controls] → position: static
Es decir: se colapsa el vídeo a cero y se desabsolutizan dos cajas. No hay nada de lo que hace que un reproductor de sonido sea un reproductor de sonido: identidad de la pieza (carátula · título · artista), waveform, velocidad de reproducción visible, capítulos, formas compactas, integración con el SO.
0.1 Hallazgos de la auditoría del estado actual (evidencia leída)
| ID | Hallazgo | Evidencia | Consecuencia |
|---|---|---|---|
| H-1 | Regla sema probablemente muerta. El pack construye su selector sobre provider (semaSelector(morfo,'provider',{eventName:'commit-toggle-play'}) → [data-media-player][data-event='commit-toggle-play']) pero el evento estampa en su propio botón (target: v.partRef('play-button')). |
sema/components/media-player.ts:23 vs morfo/…/media-player.ts:95 |
La firma de play/pause nunca se aplicaría. Verificar en F0 con stamp real en navegador; si se confirma, es fix de una línea (onPlayButton). |
| H-2 | SettingsButton y Captions declarados e inertes ("render inertly until implemented", README soma:99-100). |
soma/…/README.md:96 |
El menú velocidad/pista es justo lo que más pesa en audio (podcast). Deuda declarada que este plan recoge. |
| H-3 | Slider no tiene pista secundaria. Partes: Provider · Range · Thumb · Tick. El buffer del player es un div eidos-only que lee --media-buffered. |
morfo/components/slider.ts |
Un waveform-seek necesita el mismo hueco. Gap de framework (§8). |
| H-4 | Slider no declara aria-valuetext. Solo aria-valuemin/max/now. |
morfo/components/slider.ts:107-109 |
El lector de pantalla anuncia «127», no «2:07 de 41:15». Fallo de nivel de referencia en el control MÁS importante del player. |
| H-5 | Formato de tiempo hecho a mano. formatTime() con padStart local, fuera de uix.format / $libs/days. |
soma/…/media-player-provider.svelte.ts:34-40 |
Contradice el eje de servicios (i18n · dígitos no arábigos · RTL). |
| H-6 | No hay --media-progress. Se eliminó por código muerto; solo sobrevive --media-buffered. |
README eidos:47 | Un waveform necesita el progreso como var continua otra vez (o su propio mecanismo). Decisión, no accidente. |
| H-7 | Cero integración MediaSession en todo el repo. | grep -rn mediaSession src/ = 0 |
Sin controles de pantalla de bloqueo / teclas de medios del SO. |
| H-8 | La carátula (Poster) es position:absolute; inset:0; object-fit:cover sobre una caja 16/9. En audio esa caja no existe (media a height:0). |
media-player.css:85-93 |
La carátula de un álbum no tiene hogar geométrico hoy. |
Ninguno de los ocho invalida el media-player: son exactamente la superficie
que un modo audio de nivel de referencia obliga a cerrar.
1. Qué es —de verdad— un reproductor de sonido
No es «un player sin imagen». Cambian tres cosas de raíz:
- La superficie deja de ser el contenido. En vídeo el marco es la obra y el chrome se esconde encima (scrim, auto-hide). En audio no hay marco: la UI es lo único visible, así que pasa de transparente a persistente, y su trabajo cambia de «no estorbar» a «identificar la pieza».
- La identidad sustituye al fotograma. Carátula, título, artista/serie,
capítulo. Ninguna existe hoy más allá de un
Titlede una línea. - La línea de tiempo se vuelve legible. En vídeo el scrubber se previsualiza con miniaturas; en audio la referencia del sector es la onda (SoundCloud, wavesurfer) o los capítulos (podcast). La barra desnuda es el mínimo, no el estándar.
Y aparecen tres afordancias que en vídeo son secundarias y aquí son de primera
fila: velocidad (podcast/curso), ±15/30 s (ya existe como SeekButton)
y continuidad con el SO (pantalla de bloqueo, auriculares, coche).
2. Comparativa contra referencias (F-1.2 exige ≥3; aquí 8 + la plataforma)
Consultado 2026-07-30. La columna «UIX hoy» = media-player en media="audio".
| Capacidad | Vidstack (DefaultAudioLayout) |
Media Chrome (<media-controller audio>) |
Plyr | Red Hat rh-audio-player |
wavesurfer.js 7 | react-h5-audio-player | UIX hoy | Veredicto v1 |
|---|---|---|---|---|---|---|---|---|
| play/pause · ±seek · mute · volumen · tiempo | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | — |
| Layout de audio propio (no vídeo colapsado) | ✅ DefaultAudioLayout |
✅ atributo audio |
✅ preset audio | ✅ layout |
n/a | ✅ | ❌ 3 reglas CSS | implementar |
| Formas / tamaños (full · compact · mini · barra) | ⚠️ smallLayoutWhen (2 tamaños) |
⚠️ composición manual | ❌ | ✅ full/compact/mini | n/a | ⚠️ | ❌ | implementar |
| Identidad: carátula · título · artista | ⚠️ Title+Poster |
⚠️ slots | ⚠️ | ✅ series/title/poster | ❌ | ✅ header | ⚠️ solo Title |
implementar |
| Velocidad de reproducción (UI) | ✅ menú + SpeedSlider |
✅ media-playback-rate-button |
✅ settings.speed | ✅ | n/a | ❌ | ❌ (setPlaybackRate existe, sin UI) |
implementar (cierra H-2) |
| Waveform | ❌ | ❌ | ❌ | ❌ | ✅ (el núcleo) | ❌ | ❌ | componente propio (D-AP.4) |
| Capítulos | ✅ SliderChapters + ChapterTitle + radio-group |
⚠️ vía cues | ✅ markers |
✅ transcript/cues | ✅ Regions | ❌ | ❌ | diferir v2 |
| Transcripción | ⚠️ captions | ⚠️ | ❌ | ✅ rh-transcript+rh-cue |
❌ | ❌ | ❌ | diferir v2 |
| Cola / playlist | ❌ | ❌ | ❌ | ❌ | ❌ | ⚠️ skip prev/next | ❌ | fuera (D-AP.6) |
| MediaSession (SO) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | superación (D-AP.5) |
| Ganancia >100 % | ✅ AudioGainSlider |
❌ | ❌ | ❌ | ⚠️ Envelope | ❌ | ❌ | diferir v2 |
| Loop / descarga | ⚠️ | ⚠️ | ✅ | ⚠️ | n/a | ✅ loop | ⚠️ nativo | loop v1 (prop), descarga = app |
| Teclado completo | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ (9 teclas) | ✅ |
aria-valuetext hablado en el scrubber |
✅ | ✅ | ✅ | ✅ | ❌ | ⚠️ | ❌ (H-4) | gap de framework (§8) |
| Motor enchufable (HLS/DASH/embed) | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ puerto MediaProvider |
✅ ya por encima |
| Semántica perceptiva (familia · intent · sonido/háptica) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ pack sema | ✅ exclusivo |
| Contrato declarativo verificable (morfo + guards) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ exclusivo |
Lectura. El transporte ya está a la altura (y el puerto de motor, por encima). Lo que falta es todo lo que distingue audio de vídeo: layout propio, formas, identidad, velocidad, onda. Dos capacidades que ninguna referencia tiene ya son nuestras (sema + morfo), y una que ninguna de las ocho implementa —MediaSession— es la palanca barata de superación.
Fuentes: Vidstack components · Vidstack Default Layout · Media Chrome · Plyr · Red Hat audio player · wavesurfer.js · react-h5-audio-player · MDN MediaSession · web.dev Media Session · W3C WAI media players.
3. La tensión arquitectónica (leerla antes de las decisiones)
Dos reglas del propio proyecto apuntan en direcciones opuestas:
- P-4 (norma de pickers,
architecture/eidos.md): «There are no per-variant components (MonthPicker,HourPicker). Those forms are<X kind='month'>». → un reproductor de audio es<MediaPlayer media="audio">. - Regla de admisión (packs/blocks): lo que tiene superficie de contrato propia se promueve a componente canónico y se compone. → la onda, el menú de velocidad y la identidad podrían querer casa propia.
La resolución no la dan las reglas sino las dos referencias primarias, que
coinciden: Vidstack mantiene UN <media-player> y cambia el layout
(DefaultAudioLayout); Media Chrome mantiene UN <media-controller> y cambia
la composición de la barra (atributo audio). Ninguna bifurca el motor.
Y el catálogo ya tiene el precedente exacto para «una segunda raíz visual sobre
el mismo contrato headless»: Toast / Toaster — «two independent roots (not
nested)… Toaster is NOT attached as Toast.Toaster because it is a competing
root, not a child» (architecture/eidos.md §Special case). DefaultControls
ya es, hoy, una composición eidos-only del mismo tipo.
4. Decisiones (gate del usuario — SIN FIRMAR)
| ID | Cuestión | Opciones | Recomendación |
|---|---|---|---|
| D-AP.1 | Pertenencia: ¿componente nuevo o modo del existente? | (a) un motor, un contrato: media-player conserva morfo/soma/sema y gana partes audio-only optional: true + un layout eidos · (b) audio-player nuevo que compone media-player (precedente picker/A27) · (c) fork con su propio provider |
(a). Duplicar transporte + puerto + teclado + eventos es exactamente la reinvención que el §4 del component-guide prohíbe; las dos referencias primarias hacen (a); el morfo YA declara data-media: ['video','audio'] y ya marca optional: true todas las partes no-núcleo (PipButton/FullscreenButton son video-only con ese mismo mecanismo — no hace falta inventar nada). (b) añade una capa de indirección sin contrato nuevo. |
| D-AP.2 | Puerta pública: ¿cómo se pide un reproductor de sonido? | (a) <MediaPlayer media="audio"> + <MediaPlayer.AudioLayout/> (espejo de DefaultControls) · (b) raíz competidora <AudioPlayer> en eidos sobre el mismo soma (precedente Toast/Toaster) · (c) ambas |
(c): AudioLayout como parte eidos-only (composición explícita, coherente con DefaultControls) y <AudioPlayer> como raíz competidora exportada aparte —no como MediaPlayer.AudioPlayer— que la monta con media="audio". El 90 % del uso es una etiqueta; el 10 % compone partes. Coste morfo/soma/sema: cero. |
| D-AP.3 | Formas y variaciones (petición explícita del usuario) | (a) solo size canónico · (b) variant propio de componente · (c) variant + size + responsive |
(c). variant: 'card' | 'row' | 'bar' | 'inline' como variante específica de componente —vía legítima y con precedente escrito (Banner: inline|overlay|persistent, Spinner: bars|dots|ring, theming §19)— declarada en su types.ts, NO en EIDOS_VARIANTS. size = subconjunto canónico xs..xl (la recipe ya lo mapea). Ambas admiten ResponsiveProp<T>. Detalle en §6. |
| D-AP.4 | Waveform | (a) componente canónico Waveform propio (ruta de 9 fases) · (b) parte eidos-only del player · (c) fuera |
(a), pero en su propia iniciativa, DESPUÉS del v1. Tiene superficie de contrato real (es un role="slider" con teclado, data-* y eventos handle-*), y sirve fuera del player (nota de voz en chat-message, editor). Meterlo como parte eidos-only sería crecerle el privilegio al player — el antipatrón que la regla de admisión existe para impedir. Los picos: peaks: number[] que aporta la app (ruta profesional: BBC audiowaveform/SoundCloud) con decodificador cliente opcional; el renderer reutiliza $libs/plots (scaleLinear + area/line), no inventa matemáticas. |
| D-AP.5 | MediaSession (SO) | (a) prop opt-in en el provider soma · (b) art nuevo $media-session · (c) app-land |
(a) opt-in, apagado por defecto, mediaSession={{ title, artist, album, artwork }}. Es exactamente el snapshot que el provider ya posee (playbackState, setPositionState, seekto), y dejarlo fuera obliga a cada consumidor a re-derivarlo. Riesgo declarado: navigator.mediaSession es singleton de documento — dos players activos se pisan; el provider debe registrar solo mientras !paused y liberar en pause/dispose. (b) es sobreingeniería para una API sin estado propio. Ninguna de las 8 referencias lo trae → superación barata. |
| D-AP.6 | Cola / playlist | (a) en el player · (b) block/app-land | (b), fuera de alcance. Una cola es una composición de Listbox + estado de aplicación: cero contrato nuevo → tier blocks o app. El player expone commit-complete y ya. Se registra en next-features.md. |
| D-AP.7 | Firma sema en modo audio | (a) heredar el pack actual · (b) silenciar el canal sonoro en audio · (c) pack propio completo | (b). Doctrina D.7 (book-deviations): «En eventos frecuentes, la prioridad es evitar fatiga. El silencio es una firma válida» + antipatrón cap. 34 §10 «sonido decorativo». Si el contenido ES audio, un tick de UI compite con la obra. Regla nueva en el pack: selector [data-media-player][data-media='audio'] → channels: ['haptic']. Es una regla que añade carácter sin tocar las primitivas del intent (regla dura de la cascada). |
| D-AP.8 | Capítulos + transcripción | (a) v1 · (b) v2 · (c) fuera | (b) v2, registrado con disposición en el README (F-1.4). Capítulos = pista secundaria del scrubber → depende de D-AP.4/H-3; transcripción = superficie de contenido con su propia a11y (candidata a componente propio, patrón Red Hat). |
| D-AP.9 | Responsive del layout | (a) ResponsiveProp (mecanismo del framework) · (b) container queries del TSC · (c) ambos |
(a) en v1, (b) evaluado en F6. El TSC ya tiene el eje container: {} documentado como «a themeable axis with 0 consumers today — an open cage» (canon/tsc.md): un player que vive dentro de un aside, una tarjeta o un pie de página es el primer consumidor natural del catálogo, y resolvería mejor que el smallLayoutWhen de Vidstack (que depende del contenedor, no del viewport). Abrir la jaula es un entregable con valor propio — pero no debe bloquear el v1. |
| D-AP.10 | Alcance del v1 | (a) paridad estricta con las referencias · (b) paridad + superación (MediaSession + silencio sema + container) | (a) + MediaSession + silencio sema; container y waveform como iniciativas hermanas. Criterio: el usuario pidió «como mínimo al mismo nivel» — la paridad es el suelo, no el techo, y las dos superaciones baratas caben. |
5. Arquitectura propuesta (tras firma de D-AP.1/2)
morfo/components/media-player.ts +6 partes optional (audio-only) · +2 eventos · +1 texto
│ (contrato ÚNICO — no se bifurca)
soma/components/media-player/ +sub-providers de las partes nuevas
│ +MediaSession opt-in en el provider raíz
│ +formatTime → uix.format (H-5)
sema/components/media-player.ts +regla de silencio en audio (D-AP.7) · fix H-1
│
eidos/components/media-player/ +audio-layout.svelte (composición, espejo de DefaultControls)
│ +recipe: variantes card/row/bar/inline
eidos/components/audio-player/ raíz competidora <AudioPlayer> (Toast/Toaster)
Lo que NO se toca: el puerto MediaProvider (ya es el acierto del
componente), el modo vídeo, el recipe Lumière existente, la demo de
media-player.
Dogfooding obligatorio (§4 component-guide): la barra compone IconButton
(ya lo hace), los sliders componen Slider (ya), el menú de velocidad compone
DropdownMenu, la carátula compone Image + AspectRatio, la identidad
compone Text/Heading, el estado vacío compone EmptyState. Cero
primitivas inline.
6. Formas y variaciones (el punto 3 del encargo, en detalle)
Cuatro formas, un solo contrato. La variante decide qué partes monta el layout y cómo se disponen, nunca qué puede hacer el player.
variant |
Forma | Partes que monta | Contexto de uso | Referencia del sector |
|---|---|---|---|---|
card |
Tarjeta vertical: carátula grande arriba, identidad, scrubber, transporte centrado | Artwork · Title · Artist · TimeSlider · Time×2 · Play · Seek± · Mute · Volume · Rate | Página de episodio, ficha de álbum, landing | Apple Podcasts / Bandcamp |
row |
Fila horizontal: carátula pequeña a la izquierda, identidad + scrubber apilados | Artwork(sm) · Title · Artist · TimeSlider · Play · Seek± · Rate | Lista de episodios, resultado de búsqueda, feed | SoundCloud track |
bar |
Barra persistente a ancho completo (composición pensada para sticky/pie) |
Artwork(xs) · Title · Play · Seek± · TimeSlider · Time · Mute · Volume · Rate | Reproductor global de la app | Spotify bottom bar |
inline |
Mínima, en línea con el texto: play + scrubber + tiempo | Play · TimeSlider · Time | Nota de voz en un chat, cita de audio en un artículo | mensaje de voz de WhatsApp |
Ejes ortogonales que componen con la variante:
size(xs..xl,ResponsiveProp) — densidad; la recipe ya mapea el bundle--size-{k}-*. Regla dura:mdno cambia de significado por viewport (theming §5); lo que cambia es qué size se elige.color— el sistema completo (rol / intent / 33 escalas / CSS crudo =ComponentColorProp), sin subconjuntos: decisión «abrir la jaula del color» (theming §25, guard enrecipe-css-contract.test.ts).variantresponsive —variant={{ base: 'inline', md: 'row', lg: 'card' }}.data-depth— la barra persistente estampa su plano (raised/overlay), no inventa sombras (contrato de recipe R-4.1).
Regla de forma (espejo de la que rige blocks): la variante no puede
añadir comportamiento. Si una forma necesitase un evento, un data-* que el
CSS deba seleccionar o una obligación a11y nueva, eso se promueve al contrato
—no se le crece el privilegio al layout.
7. Contrato propuesto (borrador para F1 — se cierra con la firma)
7.1 Partes nuevas (todas optional: true, audio-only)
| Parte | kebab | archetype | Elemento | Rol |
|---|---|---|---|---|
Artwork |
artwork |
image |
img |
Carátula. Compone Image+AspectRatio; alt real (no decorativa como Poster, porque en audio es la única representación visual de la obra). |
Artist |
artist |
description |
div |
Autoría / serie. |
RateButton |
rate-button |
trigger |
button |
Abre el menú de velocidad (compone DropdownMenu) o cicla el preset. Cierra H-2 junto a SettingsButton. |
Identity |
identity |
group |
div |
Agrupador de Artwork+Title+Artist (le da a la variante un bloque que mover). |
Transport |
transport |
group |
div |
Agrupador de los botones de transporte, hermano de Controls. |
LiveIndicator |
live-indicator |
indicator |
div |
Emisión en directo (data-live ya existe en el provider). |
7.2 Eventos nuevos
| Evento | Familia · verbo | Intent | Target | sequence |
Por qué |
|---|---|---|---|---|---|
commit-set-rate |
commit · set |
neutral |
rate-button |
post |
Fijar velocidad es un valor aplicado (commit.set, BOOK_CANON A.5: «un valor, criterio o parámetro ha quedado aplicado» — mismo caso que sort/slider). |
sustain-end |
sustain · end |
— | provider |
coincident |
Cierre del proceso de buffering. Solo si F0 confirma que sustain-loading (persistence: 'stateBound') no se limpia solo; si se limpia, no se declara (regla B.5: un cambio no perceptible no es evento). |
Nada más. loop es prop nativa sin evento (B.5). El cambio de carátula/título es
dato, no ocurrencia.
7.3 Teclado
Se hereda el contrato del provider (9 teclas). Se añaden, alineadas con el
sector (j/l de YouTube, </> de velocidad):
| Tecla | Acción | Nota |
|---|---|---|
j / l |
seek ∓ seekStep |
alias de ←/→, convención del sector |
< / > |
bajar / subir velocidad | requiere RateButton montado |
0–9 |
saltar al 0–90 % | solo cuando duration es finita (no en directo) |
7.4 A11y — el listón
aria-valuetexthablado en el scrubber (cierra H-4): «2 minutos 7 segundos de 41 minutos», no «127». Es gap de framework (§8).Artworkconaltreal; el resto de la identidad como texto real (nuncaaria-labelsobredivs vacíos).commit-failya declararequiresLiveRegion+reducedMotionFallback: 'text'— se conserva.- Contraste: la barra
barsobre superficie arbitraria consumedata-depth, no color inventado (R-2.1/R-4.6). - Sin robo de foco al cambiar de pista.
8. Gaps de framework a promover ANTES (regla de admisión)
Se construyen primero, en el canon, y el player los compone. Ninguno es un parche local del player.
| # | Gap | Dónde | Justificación 2-de-3 | Coste |
|---|---|---|---|---|
| G-1 | aria-valuetext en el morfo de Slider (v.propRef('valueText')) + prop valueText en soma |
morfo/components/slider.ts + provider |
aria es campo morfo por definición (soma emite, eidos puede seleccionar) |
bajo |
| G-2 | Pista secundaria («buffered»/«secondary range») en Slider |
morfo + soma + recipe |
Lo consumen soma (valor) y eidos (pintura) → 2-de-3 ✓. Hoy lo simula un div eidos-only en media-player y lo simularía otro en Waveform — dos copias = la señal de que falta la pieza |
medio |
| G-3 | formatDuration en $libs/days (o uix.format) |
$libs/days |
Regla A23: si es puro y reutilizable, va a la lib — nunca reimplementar dentro de soma | bajo |
| G-4 | Verificar/arreglar H-1 (selector del pack sema) | sema/components/media-player.ts |
fix de una línea si se confirma | trivial |
Waveform (D-AP.4) no está en esta tabla: no es un gap del player sino una
iniciativa hermana con su propia ruta de 9 fases.
9. Fases (tras el gate)
| Fase | Contenido | Verify |
|---|---|---|
| F0 — Verificación y firma | Confirmar H-1 (stamp real en navegador) · confirmar si sustain-loading se auto-limpia (decide 7.2) · leer DefaultControls, Slider provider y la recipe del scrubber ANTES de diseñar (regla feedback_study_real_recipes_before_building) · presentar §4 y §6 al usuario y firmar |
tabla de decisiones firmada en el chat; este plan actualizado |
| F1 — Gaps de framework | G-1 · G-2 · G-3 · G-4, cada uno con su test | check 0 propios · vitest del slider verde · component:audit --only slider PASS · morfo:vocabulary exit 0 |
| F2 — Morfo | 6 partes + 1–2 eventos + texts + catálogo langs/components/media-player.ts |
validateMorfo · test del morfo · morfo:vocabulary · translations:check |
| F3 — Soma | sub-providers de las partes nuevas · MediaSession opt-in (con guarda de singleton) · formatTime→G-3 · tests del provider |
test del provider verde · check 0 propios |
| F4 — Sema | regla de silencio en audio (D-AP.7) + fix H-1; expression sigue 'pack' |
morfo:vocabulary (coherencia pack↔expression, guard S11d) · stamp verificado en navegador |
| F5 — Eidos | audio-layout.svelte + raíz <AudioPlayer> + recipe de las 4 variantes + tokens --audio-player-* en lib/recipes/base.ts |
generate:eidos-css (TSC) · recipe-css-contract · eidos-lint 0 inválidos · component-api-contract |
| F6 — Formas | responsive de variant/size; evaluar container queries (D-AP.9) |
prueba visual de las 4 variantes × light/dark × RTL × densidad, con captura y mirada (regla dura feedback_verify_visually_before_showing) |
| F7 — Demo + dossier + cierre | demo v2 de 9 pestañas (cada prop = control vivo, paridad de chips D-7.4) · README eidos (Baseline/Comparativa≥3/Decisiones/Gaps con disposición) · README soma actualizado · data-perm-step |
component:audit --only media-player PASS 0 errores · smoke · perm:check · check 0 propios · docs:check |
Método: no-cascada — una pieza, verificar, la siguiente (método canonizado
en _cierre.md). Gates completos al cerrar
cada fase.
10. Fuera de alcance
- Cola / playlist (D-AP.6) →
blockso app; se registra ennext-features.md. - Waveform (D-AP.4) → iniciativa hermana, ruta de 9 fases propia.
- Capítulos, transcripción, ganancia >100 %, ecualizador, grabación → v2,
cada uno con disposición escrita en el
## Gapsdel README. - Vendorizar motores (hls.js, Howler, wavesurfer) → el puerto
MediaProviderya es la respuesta; doctrina de cero dependencias. - Visualizador/espectro → si algún día se hace, su hogar es
$scene(EngineSceneya resuelve frame-loop, pausa fuera de vista, cap de DPR, reduced-motion obligatoria, pérdida de contexto y presupuesto) o el tierpacks— nunca un canvas suelto dentro del recipe del player.
11. Riesgos declarados
| Riesgo | Mitigación |
|---|---|
El morfo de media-player ya es el mayor del catálogo; crece más |
Todas las partes nuevas optional: true; si tras F2 el fichero pasa de ~700 líneas, reabrir D-AP.1(b) con datos, no con intuición |
| MediaSession es singleton de documento | Registro solo mientras suena + liberación en pause/dispose; documentado en el README como limitación conocida |
La variante bar vive sobre fondos arbitrarios |
data-depth + el layer de estado; nunca color crudo |
| Sobrecarga de alcance | El v1 es la columna «implementar» de §2 y nada más; todo lo demás lleva disposición escrita |