feat(media-player): el altavoz dice CUANTO volumen, no solo si esta mudo

El icono mostraba las mismas ondas llenas al 5% que al 100%: solo alternaba
Volume2 y VolumeX segun data-state. Ahora lee una banda de sonoridad.

morfo: data-level en la parte mute-button, enum ['muted','low','mid','high'].
Contract-only (sin fuente resoluble), asi que soma pone el valor — misma forma
que data-orientation en volume-slider. Declarar values es lo que evita que el
compilador lo degrade a bandera de presencia, la trampa que data-rate documenta
dos partes mas abajo.

soma: la banda se deriva de volume + muted. Mudo gana a cualquier nivel (un
player muteado a volumen 1 es silencio, y el icono debe decirlo), y volumen 0
sin la bandera tambien lee 'muted'. Las tres bandas audibles son tercios iguales
porque hay exactamente tres glifos no-mudos que mapear.

eidos: cuatro bandas, cuatro glifos — Volume / Volume1 / Volume2 / VolumeX, que
el set de iconos ya exportaba. Sin assets nuevos y sin icon-swap en CSS.

Medido en Chrome, con el recuento de trazos SVG confirmando cuatro glifos
distintos: 1.0 y 0.8 high(3) · 0.5 y 0.34 mid(2) · 0.2 y 0.05 low(1) · muteado
muted(3). check 0 errores propios · morfo:check PASS media-player · morfo 8/8 ·
player 14/14 · eidos-lint invalid 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alpha-0.1-dir-prefs
dev 2 months ago
parent e9454bebe7
commit 3b5eab5b68

@ -1,12 +1,15 @@
<script lang="ts">
/**
* Eidos `<MediaPlayer.MuteButton>` — composes `<IconButton>` (ghost) via
* soma's `child`; size from the player's eidos context. Glyph from the
* soma-computed `data-state` (`on` = muted).
* soma's `child`; size from the player's eidos context.
*
* The glyph reads the soma-computed `data-level`, not just `data-state`: a
* speaker showing full waves at 5% and at 100% alike says nothing about how
* loud it is. Four bands, four glyphs, and no icon-swap in CSS.
*/
import * as MediaPlayer from '$soma/components/media-player';
import { IconButton } from '$uix/eidos/components/icon-button';
import { Volume2, VolumeX } from '$uix/eidos/components/icon';
import { Volume, Volume1, Volume2, VolumeX } from '$uix/eidos/components/icon';
import { getMediaPlayerEidosCtx } from './context';
import type { MediaPlayerMuteButtonProps } from './types';
@ -28,9 +31,12 @@
{size}
aria-label={p['aria-label'] as string}
>
{#if children}{@render children()}{:else if p['data-state'] === 'on'}<VolumeX
{size}
/>{:else}<Volume2 {size} />{/if}
{#if children}
{@render children()}
{:else if p['data-level'] === 'muted'}<VolumeX {size} />
{:else if p['data-level'] === 'low'}<Volume {size} />
{:else if p['data-level'] === 'mid'}<Volume1 {size} />
{:else}<Volume2 {size} />{/if}
</IconButton>
{/if}
{/snippet}

@ -406,6 +406,13 @@ export const mediaPlayerMorfo = {
states: ['on', 'off'],
data: [
{ attr: 'data-state', values: ['on', 'off'], value: v.stateRef('on') },
// Loudness BAND, so the speaker glyph can say how loud — not just
// whether it is muted. `data-state` alone left the icon showing full
// waves at 5% and at 100% alike. Contract-only (an enum with no
// resolvable source), so soma owns the value; declaring `values` is
// what keeps the compiler from degrading it to a presence flag, the
// trap `data-rate` below documents.
{ attr: 'data-level', values: ['muted', 'low', 'mid', 'high'] },
{ attr: 'data-disabled', severity: 'optional' }
],
aria: [

@ -88,7 +88,7 @@ interface MediaProvider {
| `Controls` | `<div>` | ✅ | Control-bar container. Not a roving Toolbar (see below). |
| `PlayButton` | `<button>` | ✅ | Play/pause. Mirrors `data-paused` → `data-state`. |
| `SeekButton` | `<button>` | ✅ | ±`seekStep` seconds. `direction` prop. |
| `MuteButton` | `<button>` | ✅ | Mute toggle. `aria-pressed`. |
| `MuteButton` | `<button>` | ✅ | Mute toggle. `aria-pressed` + `data-level` (loudness band). |
| `VolumeSlider` | `<div>` | ✅ | Composes `Slider` (0–1 volume). |
| `TimeSlider` | `<div>` | ✅ | Composes `Slider` + an eidos-only buffered track. |
| `Time` | `<div>` | ✅ | `current` / `duration` / `remaining` readout. |

@ -1020,11 +1020,29 @@ export class MediaPlayerMuteButtonProvider {
this.provider.soma.langs.ts(this.provider.muted ? LANGS.unmute : LANGS.mute)
);
/**
* Loudness band for the speaker glyph. Muted wins over any level — a muted
* player at volume 1 is silent, and the icon must say so. The three audible
* bands are equal thirds because there are exactly three non-muted glyphs to
* map onto (`Volume` · `Volume1` · `Volume2`); volume 0 without the muted
* flag is silence too, so it reads `muted`.
*/
readonly level = $derived.by(() => {
const v = this.provider.volume;
if (this.provider.muted || v <= 0) return 'muted';
if (v < 1 / 3) return 'low';
if (v < 2 / 3) return 'mid';
return 'high';
});
readonly props = $derived.by(() =>
this.runtimePart.assert({
// type/role/aria-label/aria-keyshortcuts/aria-pressed/data-state are
// morfo-declared, resolved by renderProps.
...this.runtimePart.renderProps(),
// data-level is contract-only in the morfo (an enum with no resolvable
// source) — soma owns the value.
'data-level': this.level,
onclick: () => this.provider.toggleMute()
} as const)
);

Loading…
Cancel
Save

Powered by TurnKey Linux.