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/PLAN-audio-player.md

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 $sound exista (su F5 desbloquea éste). Kickoff para sesión nueva: "Lee docs/process/PLAN-sound-engine.md primero; 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:

  1. 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 slider 0…end−start, timeBase: 'clip' | 'source' en Time, misma base en aria-valuetext) · clamp en el provider, no en el puerto · commit-complete dispara en el fin del clip (mismo evento, frontera movida) · con loop NO hay commit-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.
  2. G-2 (pista secundaria del Slider) sube a BLOQUEANTE de v1: ya tiene tres consumidores — buffer + ventana de clip + capítulos.
  3. 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, $sound ya existirá y el player solo declara si lo consume.
  4. Dos invariantes a escribir en el contrato: (a) prefs.sound nunca 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 al Slider compuesto: existe resolveSliderDragSound y 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].
  5. 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 de component-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:

  1. 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».
  2. La identidad sustituye al fotograma. Carátula, título, artista/serie, capítulo. Ninguna existe hoy más allá de un Title de una línea.
  3. 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: md no 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 en recipe-css-contract.test.ts).
  • variant responsive — 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-valuetext hablado en el scrubber (cierra H-4): «2 minutos 7 segundos de 41 minutos», no «127». Es gap de framework (§8).
  • Artwork con alt real; el resto de la identidad como texto real (nunca aria-label sobre divs vacíos).
  • commit-fail ya declara requiresLiveRegion + reducedMotionFallback: 'text' — se conserva.
  • Contraste: la barra bar sobre superficie arbitraria consume data-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) → blocks o app; se registra en next-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 ## Gaps del README.
  • Vendorizar motores (hls.js, Howler, wavesurfer) → el puerto MediaProvider ya es la respuesta; doctrina de cero dependencias.
  • Visualizador/espectro → si algún día se hace, su hogar es $scene (EngineScene ya resuelve frame-loop, pausa fuera de vista, cap de DPR, reduced-motion obligatoria, pérdida de contexto y presupuesto) o el tier packs — 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

Powered by TurnKey Linux.