Media
Volume Meter code
Copy the implementation or inspect every file that belongs to this component unit.
resources/views/components/ui/volume-meter.blade.php
@props([
'level' => 0,
'peaks' => null,
'bars' => 16,
'orientation' => 'horizontal',
'label' => 'Levels',
'player' => null,
'live' => false,
])
@php
/*
| Audio level / volume meter with brand-coloured bars. Bind `level` (0–1)
| or pass `peaks`. When `live` is true, animates a soft idle pulse.
|
| Usage:
| <x-ui.volume-meter :level="0.62" />
| <x-ui.volume-meter orientation="vertical" :bars="20" live />
*/
$orientation = in_array($orientation, ['horizontal', 'vertical'], true) ? $orientation : 'horizontal';
$barCount = max(8, min(48, (int) $bars));
$normalizedPeaks = collect($peaks ?? [])
->map(fn ($peak) => max(0, min(1, (float) $peak)))
->values()
->all();
if ($normalizedPeaks === []) {
$base = max(0, min(1, (float) $level));
for ($i = 0; $i < $barCount; $i++) {
$falloff = 1 - abs(($i / max(1, $barCount - 1)) - $base);
$normalizedPeaks[] = max(0.08, min(1, $base * (0.35 + $falloff * 0.65)));
}
}
@endphp
<div
x-data="{
level: @js(max(0, min(1, (float) $level))),
peaks: @js($normalizedPeaks),
live: @js((bool) $live),
player: @js($player),
tick: 0,
timer: null,
get displayPeaks() {
if (! this.live) { return this.peaks; }
const t = this.tick / 8;
return this.peaks.map((peak, index) => {
const wave = 0.55 + 0.45 * Math.sin(t + index * 0.55);
return Math.max(0.08, Math.min(1, Math.max(this.level, peak * 0.35) * wave));
});
},
onVolume(detail) {
if (this.player && detail?.player && detail.player !== this.player) { return; }
if (detail?.volume != null) {
this.level = Math.max(0, Math.min(1, Number(detail.volume)));
}
if (detail?.muted) { this.level = 0; }
},
init() {
if (! this.live) { return; }
this.timer = setInterval(() => { this.tick += 1; }, 80);
},
destroy() {
if (this.timer) { clearInterval(this.timer); }
},
}"
@media-volume.window="onVolume($event.detail)"
{{ $attributes->merge([
'class' => $orientation === 'vertical'
? 'inline-flex min-h-24 min-w-0 flex-col items-center gap-2 rounded-2xl border border-gray-200 bg-white p-3 shadow-sm dark:border-gray-800 dark:bg-gray-900'
: 'min-w-0 max-w-full rounded-2xl border border-gray-200 bg-white p-3 shadow-sm dark:border-gray-800 dark:bg-gray-900 sm:p-4',
]) }}
role="meter"
aria-label="{{ $label }}"
:aria-valuemin="0"
:aria-valuemax="100"
:aria-valuenow="Math.round(level * 100)"
>
<div class="flex w-full items-center justify-between gap-2">
<p class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">{{ $label }}</p>
<p class="font-mono text-[11px] tabular-nums text-gray-400 dark:text-gray-500">
<span x-text="Math.round(level * 100)"></span>%
</p>
</div>
<div @class([
'mt-2 flex w-full',
'h-28 items-end justify-center gap-1' => $orientation === 'vertical',
'h-12 items-end gap-0.5 sm:h-14 sm:gap-1' => $orientation === 'horizontal',
])>
<template x-for="(peak, index) in displayPeaks" :key="index">
<span
@class([
'rounded-full bg-brand-500/90 transition-[height] duration-75 dark:bg-brand-400/90',
'w-2.5' => $orientation === 'vertical',
'min-w-[3px] flex-1' => $orientation === 'horizontal',
])
:style="`height: ${Math.max(8, peak * 100)}%`"
></span>
</template>
</div>
</div>