web-player: app chrome (layout, navigation, player)

PlayerExtras is merged into PlayerBar, which now renders volume and
the queue button inline and uses the new Slider (a real range input
with thumb thickness controlled via CSS variables) for both the
progress bar and volume, replacing SeekBar and VolumeControl (grep
confirmed SeekBar's `compact` mode was dead code).

Sidebar and TabsBar migrate to appNavLinks(user, counts?), removing
the duplicated nav links between the two. Decision made without
blocking: Sidebar gains the gradient Logo mark it didn't have before,
for visual consistency with MobileHeader and the login screen.

PlayerDock and MobileHeader's z-index is fixed to z-(--z-sticky)
(they're fixed bars, not menus), and SegmentedControl's to
z-(--z-raised), per the plan's note B.1.5. glass-bar and
theme-transition are applied to PlayerBar (both variants), TabsBar,
Queue and Home (hero/spotlight), replacing duplicated
blur/transition styles.

Verified with npm run check (0 errors) and npm run build; smoke-tested
/login with no console errors. No authenticated visual QA (Docker
unavailable this session).
This commit is contained in:
2026-09-21 14:39:20 +02:00
parent 8975e43729
commit 5fa901c2e6
15 changed files with 155 additions and 185 deletions
@@ -1,12 +1,21 @@
<script lang="ts">
import TrackInfo from './TrackInfo.svelte';
import TransportControls from './TransportControls.svelte';
import SeekBar from './SeekBar.svelte';
import PlayerExtras from './PlayerExtras.svelte';
import Slider from '$lib/components/ui/Slider.svelte';
import IconButton from '$lib/components/ui/IconButton.svelte';
import { player } from '$lib/player/player.svelte';
import { queuePanel } from '$lib/player/queuePanel.svelte';
import { fmtTime } from '$lib/format';
import Volume2 from '@lucide/svelte/icons/volume-2';
import ListMusic from '@lucide/svelte/icons/list-music';
function seek(value: number) {
if (player.current.duration > 0) player.seekFraction(value / player.current.duration);
}
</script>
<div
class="animate-enter pointer-events-auto flex items-center gap-4.5 border-t border-hairline bg-[image:var(--mf-bar-bg)] px-4 py-2.75 backdrop-blur-xl transition-[background] duration-500 sm:hidden"
class="animate-enter pointer-events-auto flex items-center gap-4.5 glass-bar px-4 py-2.75 sm:hidden"
>
<div class="min-w-0 flex-1">
<TrackInfo compact />
@@ -15,14 +24,46 @@
</div>
<div
class="animate-enter pointer-events-auto hidden items-center gap-5 border-t border-hairline bg-[image:var(--mf-bar-bg)] px-4 py-2.75 backdrop-blur-xl transition-[background] duration-500 sm:flex"
class="animate-enter pointer-events-auto hidden items-center gap-5 glass-bar px-4 py-2.75 sm:flex"
>
<div class="w-64 min-w-0 shrink-0">
<div class="w-(--mf-player-side-w) min-w-0 shrink-0">
<TrackInfo />
</div>
<div class="flex min-w-0 flex-1 flex-col items-center gap-2">
<TransportControls />
<SeekBar />
<div class="flex w-full max-w-hero items-center gap-2.75">
<span class="min-w-8 text-right text-xs text-fg-3 tabular-nums">
{fmtTime(player.progress)}
</span>
<Slider
value={player.progress}
max={player.current.duration}
label="Barra de progreso"
oninput={seek}
variant="seek"
/>
<span class="min-w-8 text-xs text-fg-3 tabular-nums">
{fmtTime(player.current.duration)}
</span>
</div>
</div>
<div class="hidden w-(--mf-player-side-w) shrink-0 items-center justify-end gap-3.5 sm:flex">
<Volume2 class="size-icon-sm shrink-0 text-fg-3" strokeWidth={1.8} />
<Slider
value={player.volume}
max={100}
label="Volumen"
oninput={(value) => player.setVolume(value)}
variant="volume"
/>
<IconButton
label="Cola de reproducción"
tone="plain"
size="xs"
pressed={queuePanel.open}
onclick={() => queuePanel.toggle()}
>
<ListMusic class="size-icon-sm" strokeWidth={1.8} />
</IconButton>
</div>
<PlayerExtras />
</div>
@@ -15,7 +15,7 @@
</script>
<div
class="pointer-events-none fixed inset-x-0 bottom-0 z-40 flex flex-col lg:left-[var(--mf-sidebar-w)]"
class="pointer-events-none fixed inset-x-0 bottom-0 z-(--z-sticky) flex flex-col lg:left-(--mf-sidebar-w)"
>
{#if player.error}
<div class="pointer-events-auto px-4 pb-2.5">
@@ -1,19 +0,0 @@
<script lang="ts">
import VolumeControl from './VolumeControl.svelte';
import IconButton from '$lib/components/ui/IconButton.svelte';
import { queuePanel } from '$lib/player/queuePanel.svelte';
import ListMusic from '@lucide/svelte/icons/list-music';
</script>
<div class="hidden w-(--mf-player-side-w) shrink-0 items-center justify-end gap-3.5 sm:flex">
<VolumeControl />
<IconButton
label="Cola de reproducción"
tone="plain"
size="xs"
pressed={queuePanel.open}
onclick={() => queuePanel.toggle()}
>
<ListMusic class="size-icon-sm" strokeWidth={1.8} />
</IconButton>
</div>
@@ -18,7 +18,7 @@
{#if queuePanel.open}
<aside
transition:fly={{ x: 24, duration: 220, easing: cubicOut }}
class="hidden shrink-0 flex-col border-l border-hairline bg-bg px-4 pt-5 transition-[background] duration-500 sm:flex"
class="hidden shrink-0 flex-col border-l border-hairline bg-bg px-4 pt-5 theme-transition sm:flex"
style="width:var(--mf-queue-w); padding-bottom:calc(var(--mf-player-h) + 2rem)"
>
<div class="mb-5 flex items-center justify-between">
@@ -1,48 +0,0 @@
<script lang="ts">
import { player } from '$lib/player/player.svelte';
import { fmtTime } from '$lib/format';
interface Props {
compact?: boolean;
}
let { compact = false }: Props = $props();
function seek(event: MouseEvent) {
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
player.seekFraction((event.clientX - rect.left) / rect.width);
}
</script>
{#if compact}
<button
type="button"
onclick={seek}
aria-label="Barra de progreso"
class="relative block h-1 w-full cursor-pointer bg-track"
>
<div
class="absolute inset-y-0 left-0 bg-accent/70"
style="width:{player.progressPercent}%"
></div>
</button>
{:else}
<div class="flex w-full max-w-hero items-center gap-2.75">
<span class="min-w-8 text-right text-xs text-fg-3 tabular-nums">
{fmtTime(player.progress)}
</span>
<button
type="button"
onclick={seek}
aria-label="Barra de progreso"
class="group relative h-3.5 flex-1 cursor-pointer"
>
<div class="h-[3.5px] w-full overflow-hidden rounded-full bg-track">
<div class="h-full rounded-full bg-accent" style="width:{player.progressPercent}%"></div>
</div>
</button>
<span class="min-w-8 text-xs text-fg-3 tabular-nums">
{fmtTime(player.current.duration)}
</span>
</div>
{/if}
@@ -1,52 +0,0 @@
<script lang="ts">
import { player } from '$lib/player/player.svelte';
import Volume2 from '@lucide/svelte/icons/volume-2';
function handleVolumeChange(event: Event) {
const target = event.currentTarget as HTMLInputElement;
player.setVolume(Number(target.value));
}
</script>
<div class="flex items-center gap-2.25">
<Volume2 class="h-3.75 w-3.75 shrink-0 text-fg-3" strokeWidth={1.8} />
<input
type="range"
min="0"
max="100"
value={player.volume}
oninput={handleVolumeChange}
aria-label="Volumen"
class="mf-volume"
style="background:linear-gradient(to right, var(--mf-text-2) {player.volume}%, var(--mf-track) {player.volume}%)"
/>
</div>
<style>
.mf-volume {
-webkit-appearance: none;
appearance: none;
width: 84px;
height: 3.5px;
border-radius: 100px;
outline: none;
cursor: pointer;
}
.mf-volume::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 11px;
height: 11px;
border-radius: 50%;
background: var(--mf-text);
cursor: pointer;
}
.mf-volume::-moz-range-thumb {
width: 11px;
height: 11px;
border-radius: 50%;
background: var(--mf-text);
border: none;
cursor: pointer;
}
</style>