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.
147 lines
6.4 KiB
147 lines
6.4 KiB
import { semaSelector } from '$uix/morfo';
|
|
import { mediaPlayerMorfo } from '$uix/morfo/components/media-player';
|
|
import { buttonMorfo } from '$uix/morfo/components/button';
|
|
import { sliderMorfo } from '$uix/morfo/components/slider';
|
|
import { SILENT } from '../channels';
|
|
import type { Sema } from '../sema-map';
|
|
|
|
/**
|
|
* Media-player perceptual defaults — SILENT transport by definition (signed
|
|
* 2026-07-31, D-AP2.7 v2).
|
|
*
|
|
* The player is the one component whose OUTPUT is audio: a UI tick on its own
|
|
* audio, un tick de UI compite con la obra»; «el silencio es una firma
|
|
* válida»). So the pack subtracts the family gain (the D.5 pattern — never
|
|
* replacing intent primitives, so `threat`/`fulfill` deltas still surface)
|
|
* on everything a user gesture fires:
|
|
*
|
|
* - the transport commits (`commit-toggle-play` / `commit-toggle-mute` /
|
|
* `commit-set-rate`), which stamp on their own buttons;
|
|
* - the composed Buttons' `contact-activate` (descendant rule — H-10: the
|
|
* silence must cover descendants, or the press still ticks);
|
|
* - the composed Sliders' release `commit-set` (descendant rule — scrubber
|
|
* and volume alike).
|
|
*
|
|
* Deliberately NOT silenced: `commit-fail` (risk must be heard — exactly what
|
|
* the gain subtraction preserves) and `commit-complete` (informs about the
|
|
* work itself; revisit with real listening if loops make it fatiguing).
|
|
*
|
|
* The Sliders' `handle-pick` / `handle-drag` are silenced by CHANNEL
|
|
* (`channels: ['haptic']`, higher-specificity descendant rules): both are
|
|
* STRUCTURAL events with no intent — the book concentrates evaluation on the
|
|
* drop, which the `commit-set` subtraction already covers — so dropping their
|
|
* sound channel here erases no evaluative load (the rule that forbids
|
|
* channel-dropping guards intents, and handle carries none). The drag's
|
|
* per-emit resolver still owns the sound PRIMITIVES, but a signal whose sound
|
|
* channel is inactive plays nothing. Real listening asked for this
|
|
* (2026-07-31: «los botones ya no… pero los sliders sí»).
|
|
*
|
|
* Reactivating the voice is the app layer's job (cascade 5b / own tunings),
|
|
* which is what that layer exists for — no component prop duplicates sema.
|
|
* Complementary, not redundant: this pack silences THE PLAYER's transport;
|
|
* `duckUiWhileContent` (the sound service) attenuates the REST of the app
|
|
* while content plays. Haptics stay — a tactile tap does not compete with
|
|
* the work.
|
|
*
|
|
* Sound is chosen, never authored: the family's verb table names it and the
|
|
* intent selects a variant. A rule here only appears when this component
|
|
* DIFFERS from that default — see `architecture/sema.md` §the sound pack.
|
|
*/
|
|
const onProvider = (matchers?: Parameters<typeof semaSelector<typeof mediaPlayerMorfo>>[2]) =>
|
|
semaSelector(mediaPlayerMorfo, 'provider', matchers);
|
|
const onPlayButton = (matchers?: Parameters<typeof semaSelector<typeof mediaPlayerMorfo>>[2]) =>
|
|
semaSelector(mediaPlayerMorfo, 'play-button', matchers);
|
|
const onRateButton = (matchers?: Parameters<typeof semaSelector<typeof mediaPlayerMorfo>>[2]) =>
|
|
semaSelector(mediaPlayerMorfo, 'rate-button', matchers);
|
|
|
|
/**
|
|
* Descendant selectors: both halves come from the typed builder (renames still
|
|
* break at compile time); only the combinator is literal.
|
|
*/
|
|
const inPlayer = (descendant: string) => `${onProvider()} ${descendant}`;
|
|
|
|
/**
|
|
* Same silence, rooted at the volume PART instead of the player.
|
|
*
|
|
* A descendant rule hung off the provider stops matching the moment the control
|
|
* is PORTALLED out of the player — which is exactly what the floating volume
|
|
* does — and the slider gets its full voice back. The part's own marker travels
|
|
* with it wherever it is rendered, so this is the root that survives the portal.
|
|
* Same class of escape as the `--_mp-*` tokens, which do not cross it either.
|
|
*/
|
|
const inVolume = (descendant: string) =>
|
|
`${semaSelector(mediaPlayerMorfo, 'volume-slider')} ${descendant}`;
|
|
|
|
export const mediaPlayerSema: Sema = {
|
|
name: 'media-player',
|
|
cascade: [
|
|
{
|
|
// The event's morfo target is the play-button (the stamp lands THERE,
|
|
// measured 2026-07-30, H-1/G-4). The subtraction is the commit family
|
|
// base (-0.3) — the tuning name comes from D.5.
|
|
selector: onPlayButton({ eventName: 'commit-toggle-play' }),
|
|
sound: SILENT,
|
|
haptic: { kind: 'tap' }
|
|
},
|
|
{
|
|
selector: semaSelector(mediaPlayerMorfo, 'mute-button', {
|
|
eventName: 'commit-toggle-mute'
|
|
}),
|
|
sound: SILENT,
|
|
haptic: { kind: 'tap' }
|
|
},
|
|
{
|
|
selector: onRateButton({ eventName: 'commit-set-rate' }),
|
|
sound: SILENT,
|
|
haptic: { kind: 'tap' }
|
|
},
|
|
{
|
|
// H-10: the composed Button's contact still ticked under any
|
|
// provider-level rule — the silence must reach descendants.
|
|
selector: inPlayer(semaSelector(buttonMorfo, 'provider', { eventName: 'contact-activate' })),
|
|
sound: SILENT
|
|
},
|
|
{
|
|
// The composed Sliders' release (scrubber + volume): same subtraction.
|
|
selector: inPlayer(semaSelector(sliderMorfo, 'provider', { eventName: 'commit-set' })),
|
|
sound: SILENT
|
|
},
|
|
{
|
|
// The Sliders' grab cue: structural, no intent — silenced by channel
|
|
// (the pickup cue ships absolute primitives, so a subtraction would
|
|
// couple to its value). The tactile tick stays.
|
|
selector: inPlayer(semaSelector(sliderMorfo, 'provider', { eventName: 'handle-pick' })),
|
|
channels: ['haptic']
|
|
},
|
|
{
|
|
// The Sliders' continuous drag: structural, no intent. Its per-emit
|
|
// resolver owns the sound primitives, but with the sound channel
|
|
// inactive for this signal nothing plays — the scrub stays tactile.
|
|
selector: inPlayer(semaSelector(sliderMorfo, 'provider', { eventName: 'handle-drag' })),
|
|
channels: ['haptic']
|
|
},
|
|
// ── The same three, rooted at the volume part ────────────────────────
|
|
// Not redundant: the floating volume PORTALS its slider out of the
|
|
// player, so the three rules above stop matching it and the control
|
|
// recovers its full voice. These follow it out.
|
|
{
|
|
selector: inVolume(semaSelector(sliderMorfo, 'provider', { eventName: 'commit-set' })),
|
|
sound: SILENT
|
|
},
|
|
{
|
|
selector: inVolume(semaSelector(sliderMorfo, 'provider', { eventName: 'handle-pick' })),
|
|
channels: ['haptic']
|
|
},
|
|
{
|
|
selector: inVolume(semaSelector(sliderMorfo, 'provider', { eventName: 'handle-drag' })),
|
|
channels: ['haptic']
|
|
},
|
|
{
|
|
selector: onProvider({ eventName: 'commit-complete' })
|
|
},
|
|
{
|
|
selector: onProvider({ eventName: 'commit-fail' })
|
|
}
|
|
]
|
|
};
|