fix(ui): snap player view directly from Studio Reference mockup

Wholesale replacement of the player view markup + a verbatim mockup
CSS block scoped to .now-playing. Previous approach kept restyling
legacy classes which left a layered, inconsistent result. This is a
clean snap: same DOM structure as the mockup, same CSS rules, mapped
onto existing JS-touched IDs.

Markup
- One <section class="now-playing"> with vinyl-stage on left and
  track-masthead on right
- Vinyl spins via data-playstate and contains album art as the
  circular center label (existing #album-art img preserved)
- SVG tonearm pivots in/out by data-playstate
- Track masthead: .kicker (copper italic mono), large italic-serif
  .track-title, italic .track-byline, mono .track-album
- Meta grid: 2 cells (State / Source) with mono labels + italic
  serif values
- 30 .spectrum bars between metadata and transport
- .transport: progress-row (timecode + .progress-track + timecode)
  and .controls (3 .btn-trans buttons + .vu-cluster)
- Volume slider, mute button, visualizer toggle moved to a
  .visually-hidden block — they remain functional for JS / a11y
  but no longer compete for visual real estate. Volume control
  happens via the always-visible mini player.

VU cluster (mockup-faithful)
- 140x60 VU meter with conic-gradient grid background, copper
  needle, "VU" label
- Stacked readout: "OUT <strong>SYS</strong>" / "VOL <strong>72%</strong>"
- Click anywhere on cluster toggles mute (calls toggleMute())
- When muted: needle turns rust, readout strong turns rust,
  OUT label switches to MUTE

JS hooks
- updatePlaybackState already sets :root[data-playstate] (drives
  spin + tonearm)
- Volume tick now updates #vu-vol and #vu-out
- updateMuteIcon updates #vu-out + .vu-cluster.muted class

Scoping
- All new CSS is .now-playing-prefixed so other tabs and dialogs
  are untouched
- Legacy .progress-bar:hover scaleY and ::after scale(0) are
  defeated with !important inside .now-playing
This commit is contained in:
2026-04-25 01:43:11 +03:00
parent d9d4672ca3
commit 77b39e5684
3 changed files with 646 additions and 70 deletions
+598 -20
View File
@@ -4361,7 +4361,7 @@ header .brand-sub {
}
/* ═══════════════════════════════════════════════════════════════
PLAYER VIEW — magazine spread with vinyl stage
PLAYER VIEW — verbatim from Studio Reference mockup
═══════════════════════════════════════════════════════════════ */
.player-container {
background: transparent;
@@ -4372,24 +4372,29 @@ header .brand-sub {
position: relative;
}
/* Folio marks at top corners of the player container */
.player-container > .folio {
/* Visually hidden — kept in DOM for a11y/JS but invisible. */
.now-playing .visually-hidden,
.player-container .visually-hidden {
position: absolute;
top: -42px;
font-family: var(--mono);
font-size: 10px;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--ink-faint);
z-index: 1;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.player-container > .folio.tl { left: 0; }
.player-container > .folio.tr { right: 0; }
.player-layout,
.now-playing {
display: grid;
grid-template-columns: minmax(280px, 460px) 1fr;
grid-template-columns: minmax(320px, 520px) 1fr;
gap: 64px;
align-items: start;
margin-top: 28px;
position: relative;
}
.player-layout {
display: grid;
grid-template-columns: minmax(320px, 520px) 1fr;
gap: 64px;
align-items: start;
margin-top: 28px;
@@ -6281,13 +6286,12 @@ footer .separator { color: var(--ink-ghost); margin: 0 8px; }
/* ─── Mobile breakpoint refinements ──────────────────────── */
@media (max-width: 720px) {
#track-title { font-size: 36px; }
.player-layout { gap: 24px; }
.album-art-container { padding: 10px; }
.album-art-container::before { font-size: 8px; bottom: -18px; }
#track-title,
.now-playing .track-title { font-size: 38px; }
.player-layout, .now-playing { gap: 28px; grid-template-columns: 1fr; }
.controls { gap: 12px; }
.controls button { width: 42px; height: 42px; }
.controls button.primary { width: 56px; height: 56px; }
.now-playing .controls .btn-trans { width: 42px; height: 42px; }
.now-playing .controls .btn-trans.primary { width: 56px; height: 56px; }
.mini-player { padding: 12px 16px; gap: 16px; }
.tab-btn { padding: 14px 12px 12px; font-size: 12px; }
.tab-btn.active { font-size: 15px; }
@@ -6299,3 +6303,577 @@ footer .separator { color: var(--ink-ghost); margin: 0 8px; }
.settings-section summary { font-size: 22px; }
footer { font-size: 9px; }
}
/* ════════════════════════════════════════════════════════════════
STUDIO REFERENCE — verbatim mockup snap for the player view.
Scoped to `.now-playing` so other tabs are unaffected. Wins over
earlier overrides through declaration order at equal specificity.
════════════════════════════════════════════════════════════════ */
.now-playing {
display: grid;
grid-template-columns: minmax(320px, 520px) 1fr;
gap: 64px;
align-items: start;
margin-top: 28px;
position: relative;
}
@media (max-width: 980px) {
.now-playing { grid-template-columns: 1fr; gap: 40px; }
}
/* ─── Vinyl + tonearm ─────────────────────────────────────── */
.now-playing .vinyl-stage {
position: relative;
aspect-ratio: 1;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: 0;
box-shadow: none;
padding: 0;
overflow: visible;
transform: none !important;
}
.now-playing .vinyl {
position: relative;
width: 86%;
aspect-ratio: 1;
border-radius: 50%;
background:
radial-gradient(circle at 50% 50%,
#0a0907 0%,
#0a0907 18%,
#1a1611 18.3%,
#0a0907 18.6%,
#14110c 22%,
#0a0907 22.3%,
#14110c 26%,
#0a0907 26.3%,
#14110c 30%,
#0a0907 30.3%,
#14110c 34%,
#0a0907 34.3%,
#14110c 38%,
#0a0907 38.3%,
#14110c 42%,
#0a0907 42.3%,
#14110c 46%,
#0a0907 46.3%,
#1c1812 47%,
#0a0907 100%);
box-shadow:
inset 0 0 60px rgba(0,0,0,0.7),
0 30px 80px rgba(0,0,0,0.6),
0 6px 20px rgba(0,0,0,0.5);
animation: sr-snap-spin 14s linear infinite;
animation-play-state: paused;
}
:root[data-playstate="playing"] .now-playing .vinyl {
animation-play-state: running;
}
.now-playing .vinyl::before {
content: "";
position: absolute; inset: 12%;
border-radius: 50%;
background:
conic-gradient(from 0deg,
rgba(255,255,255,0.04) 0deg,
transparent 30deg,
rgba(255,255,255,0.06) 90deg,
transparent 150deg,
rgba(255,255,255,0.03) 210deg,
transparent 270deg,
rgba(255,255,255,0.05) 330deg,
transparent 360deg);
mix-blend-mode: screen;
pointer-events: none;
}
/* Vinyl label = circular clip holding the actual album art */
.now-playing .vinyl-label {
position: absolute;
inset: 28%;
border-radius: 50%;
overflow: hidden;
background: var(--bg-card);
box-shadow:
inset 0 0 24px rgba(0,0,0,0.4),
0 0 0 4px var(--bg-deep),
0 0 0 5px var(--copper-lo);
z-index: 1;
}
.now-playing .vinyl-label::before {
/* Spindle hole */
content: "";
position: absolute;
width: 8%; height: 8%;
top: 46%; left: 46%;
border-radius: 50%;
background: var(--bg-deep);
box-shadow: inset 0 1px 2px rgba(255,255,255,0.1);
z-index: 3;
}
.now-playing .vinyl-label #album-art-glow {
position: absolute;
inset: -10%;
width: 120%;
height: 120%;
border-radius: 50%;
filter: blur(22px) saturate(1.4);
opacity: 0.5;
z-index: 0;
object-fit: cover;
}
.now-playing .vinyl-label #album-art {
position: relative;
width: 100%;
height: 100%;
object-fit: cover;
display: block;
border-radius: 50%;
z-index: 2;
}
/* Tonearm */
.now-playing .tonearm {
position: absolute;
top: -8%; right: -4%;
width: 58%; height: 58%;
pointer-events: none;
transform-origin: 88% 12%;
transform: rotate(-22deg);
transition: transform 1s var(--ease);
z-index: 3;
filter: drop-shadow(0 4px 12px rgba(0,0,0,0.5));
}
:root[data-playstate="playing"] .now-playing .tonearm {
transform: rotate(0deg);
}
@keyframes sr-snap-spin { to { transform: rotate(360deg); } }
/* Spectrogram canvas hidden by default; toggle reveals it. */
.now-playing .spectrogram-canvas {
position: absolute;
bottom: -52px;
left: 7%; right: 7%;
width: 86%;
height: 38px;
border-radius: 0;
opacity: 0;
transition: opacity 240ms var(--ease);
pointer-events: none;
}
.now-playing.visualizer-active .spectrogram-canvas,
body.visualizer-active .now-playing .spectrogram-canvas {
opacity: 0.6;
}
/* ─── Track masthead ──────────────────────────────────────── */
.now-playing .track-masthead {
display: flex;
flex-direction: column;
justify-content: center;
padding-top: 0;
gap: 0;
}
.now-playing .kicker {
display: flex;
align-items: center;
gap: 14px;
font-family: var(--mono);
font-size: 10px;
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--copper);
margin-bottom: 22px;
}
.now-playing .kicker::before,
.now-playing .kicker::after {
content: "";
height: 1px;
background: var(--copper);
opacity: 0.6;
flex: 0 0 24px;
}
.now-playing .kicker::after { flex: 1 0 auto; }
.now-playing .track-title,
.now-playing #track-title {
font-family: var(--serif);
font-weight: 400;
font-size: clamp(42px, 5.6vw, 78px);
line-height: 0.96;
letter-spacing: -0.02em;
font-variation-settings: 'opsz' 144;
margin-bottom: 18px;
color: var(--ink);
margin-top: 0;
}
.now-playing .track-title em {
font-style: italic;
color: var(--copper-hi);
}
.now-playing .track-byline,
.now-playing #artist {
font-family: var(--serif);
font-style: italic;
font-size: 22px;
font-weight: 300;
color: var(--ink-soft);
font-variation-settings: 'opsz' 60;
margin-bottom: 4px;
margin-top: 0;
}
.now-playing .track-album,
.now-playing #album {
font-family: var(--sans);
font-size: 13px;
letter-spacing: 0.04em;
color: var(--ink-mute);
font-style: normal;
font-weight: 400;
margin-top: 0;
}
/* ─── 2-cell metadata grid ────────────────────────────────── */
.now-playing .meta-grid {
display: grid;
grid-template-columns: minmax(180px, auto) 1fr;
gap: 0;
margin-top: 36px;
border-top: 1px solid var(--rule);
border-bottom: 1px solid var(--rule);
}
.now-playing .meta-cell {
padding: 16px 24px 16px 0;
border-right: 1px solid var(--rule);
min-width: 0;
}
.now-playing .meta-cell:last-child {
border-right: 0;
padding-left: 24px;
padding-right: 0;
}
.now-playing .meta-cell .label {
font-family: var(--mono);
font-size: 9px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--ink-faint);
margin-bottom: 8px;
}
.now-playing .meta-cell .value {
font-family: var(--serif);
font-style: italic;
font-weight: 400;
font-size: 18px;
color: var(--ink);
font-variation-settings: 'opsz' 30;
display: flex;
align-items: center;
gap: 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.2;
}
.now-playing .meta-cell .value .state-icon,
.now-playing .meta-cell .value .source-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
color: var(--copper);
fill: currentColor;
}
/* ─── Spectrum bars ───────────────────────────────────────── */
.now-playing .spectrum {
display: flex;
align-items: flex-end;
justify-content: center;
gap: 3px;
height: 38px;
margin: 36px auto 24px;
max-width: 480px;
}
.now-playing .spectrum span {
display: block;
width: 3px;
flex: 0 0 3px;
background: linear-gradient(to top, var(--copper-lo), var(--copper-hi));
opacity: 0.85;
transform-origin: bottom;
border-radius: 99px 99px 0 0;
animation: sr-snap-bar 1.1s ease-in-out infinite;
animation-play-state: paused;
}
:root[data-playstate="playing"] .now-playing .spectrum span {
animation-play-state: running;
}
.now-playing .spectrum span:nth-child(1) { animation-delay: -0.10s; height: 30%; }
.now-playing .spectrum span:nth-child(2) { animation-delay: -0.45s; height: 60%; }
.now-playing .spectrum span:nth-child(3) { animation-delay: -0.20s; height: 80%; }
.now-playing .spectrum span:nth-child(4) { animation-delay: -0.55s; height: 50%; }
.now-playing .spectrum span:nth-child(5) { animation-delay: -0.30s; height: 95%; }
.now-playing .spectrum span:nth-child(6) { animation-delay: -0.05s; height: 70%; }
.now-playing .spectrum span:nth-child(7) { animation-delay: -0.65s; height: 40%; }
.now-playing .spectrum span:nth-child(8) { animation-delay: -0.25s; height: 85%; }
.now-playing .spectrum span:nth-child(9) { animation-delay: -0.40s; height: 55%; }
.now-playing .spectrum span:nth-child(10) { animation-delay: -0.10s; height: 75%; }
.now-playing .spectrum span:nth-child(11) { animation-delay: -0.50s; height: 35%; }
.now-playing .spectrum span:nth-child(12) { animation-delay: -0.15s; height: 90%; }
.now-playing .spectrum span:nth-child(13) { animation-delay: -0.60s; height: 45%; }
.now-playing .spectrum span:nth-child(14) { animation-delay: -0.30s; height: 65%; }
.now-playing .spectrum span:nth-child(15) { animation-delay: -0.45s; height: 85%; }
.now-playing .spectrum span:nth-child(16) { animation-delay: -0.20s; height: 55%; }
.now-playing .spectrum span:nth-child(17) { animation-delay: -0.55s; height: 70%; }
.now-playing .spectrum span:nth-child(18) { animation-delay: -0.10s; height: 30%; }
.now-playing .spectrum span:nth-child(19) { animation-delay: -0.40s; height: 80%; }
.now-playing .spectrum span:nth-child(20) { animation-delay: -0.25s; height: 60%; }
.now-playing .spectrum span:nth-child(21) { animation-delay: -0.50s; height: 90%; }
.now-playing .spectrum span:nth-child(22) { animation-delay: -0.15s; height: 50%; }
.now-playing .spectrum span:nth-child(23) { animation-delay: -0.60s; height: 70%; }
.now-playing .spectrum span:nth-child(24) { animation-delay: -0.30s; height: 40%; }
.now-playing .spectrum span:nth-child(25) { animation-delay: -0.45s; height: 85%; }
.now-playing .spectrum span:nth-child(26) { animation-delay: -0.20s; height: 55%; }
.now-playing .spectrum span:nth-child(27) { animation-delay: -0.55s; height: 75%; }
.now-playing .spectrum span:nth-child(28) { animation-delay: -0.10s; height: 35%; }
.now-playing .spectrum span:nth-child(29) { animation-delay: -0.40s; height: 65%; }
.now-playing .spectrum span:nth-child(30) { animation-delay: -0.25s; height: 95%; }
@keyframes sr-snap-bar {
0%, 100% { transform: scaleY(0.4); }
50% { transform: scaleY(1); }
}
/* ─── Transport ───────────────────────────────────────────── */
.now-playing .transport { margin-top: 0; }
.now-playing .progress-row {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 18px;
margin-bottom: 28px;
}
.now-playing .timecode {
font-family: var(--mono);
font-size: 12px;
color: var(--ink-mute);
letter-spacing: 0.06em;
font-variant-numeric: tabular-nums;
line-height: 1;
}
.now-playing .timecode.elapsed { color: var(--copper); font-weight: 500; }
.now-playing .progress-track,
.now-playing .progress-bar {
height: 2px;
width: 100%;
background: var(--rule-strong);
position: relative;
cursor: pointer;
border-radius: 0;
overflow: visible;
transform: none !important;
transition: background 200ms var(--ease);
min-width: 0;
margin: 0;
}
.now-playing .progress-bar:hover,
.now-playing .progress-bar.dragging {
transform: none !important;
background: var(--rule-strong);
}
.now-playing .progress-fill {
position: absolute;
left: 0; top: 0;
height: 100%;
background: var(--copper);
box-shadow: 0 0 12px var(--copper-glow);
border-radius: 0;
width: 0;
transition: width 0.1s linear;
}
.now-playing .progress-fill::after {
content: "";
position: absolute;
right: -5px;
top: 50%;
transform: translateY(-50%) scale(1) !important;
width: 10px; height: 10px;
background: var(--copper);
border-radius: 50%;
box-shadow: 0 0 14px var(--copper-glow), 0 0 0 4px rgba(224, 128, 56, 0.12);
transition: none;
}
/* ─── Controls + VU cluster ───────────────────────────────── */
.now-playing .controls {
display: flex;
align-items: center;
gap: 18px;
margin-top: 0;
justify-content: flex-start;
}
.now-playing .btn-trans {
background: transparent;
border: 1px solid var(--rule-strong);
color: var(--ink-soft);
width: 48px; height: 48px;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: 50%;
padding: 0;
transition: all 200ms var(--ease);
}
.now-playing .btn-trans:hover {
border-color: var(--copper);
color: var(--copper);
background: rgba(224, 128, 56, 0.06);
}
.now-playing .btn-trans:disabled {
opacity: 0.35;
cursor: not-allowed;
border-color: var(--rule-strong);
color: var(--ink-mute);
background: transparent;
}
.now-playing .btn-trans.primary {
width: 64px; height: 64px;
background: var(--ink);
color: var(--bg-deep);
border-color: var(--ink);
box-shadow: 0 8px 28px rgba(0,0,0,0.45);
}
.now-playing .btn-trans.primary:hover {
background: var(--copper);
border-color: var(--copper);
color: var(--bg-deep);
transform: scale(1.04);
box-shadow: 0 8px 28px var(--copper-glow);
}
.now-playing .btn-trans svg {
width: 20px; height: 20px;
fill: currentColor;
}
.now-playing .btn-trans.primary svg {
width: 24px; height: 24px;
}
/* VU cluster — display-only, click toggles mute */
.now-playing .vu-cluster {
margin-left: auto;
display: flex;
align-items: center;
gap: 16px;
cursor: pointer;
user-select: none;
transition: opacity 200ms var(--ease);
}
.now-playing .vu-cluster:hover { opacity: 0.85; }
.now-playing .vu-cluster:focus-visible {
outline: 1px solid var(--copper);
outline-offset: 4px;
}
.now-playing .vu-cluster.muted .vu-needle {
background: linear-gradient(to top, var(--rust) 0%, var(--ink-mute) 100%);
box-shadow: 0 0 8px rgba(194, 85, 63, 0.4);
}
.now-playing .vu-cluster.muted .vu-readout strong { color: var(--rust); }
.now-playing .vu-meter {
position: relative;
width: 140px;
height: 60px;
background: linear-gradient(180deg, #1a1610 0%, #0e0c08 100%);
border: 1px solid var(--rule-strong);
border-radius: 4px 4px 0 0;
overflow: hidden;
box-shadow: inset 0 2px 6px rgba(0,0,0,0.5), inset 0 0 30px rgba(224,128,56,0.08);
flex-shrink: 0;
}
.now-playing .vu-meter::before {
content: "";
position: absolute;
inset: 0;
background: repeating-conic-gradient(from 195deg at 50% 100%,
transparent 0deg 4deg,
rgba(242, 235, 220, 0.08) 4deg 5deg,
transparent 5deg 9deg);
}
.now-playing .vu-meter::after {
content: "VU";
position: absolute;
bottom: 4px;
left: 50%;
transform: translateX(-50%);
font-family: var(--mono);
font-size: 8px;
letter-spacing: 0.3em;
color: var(--ink-faint);
}
.now-playing .vu-needle {
position: absolute;
bottom: 0;
left: 50%;
width: 1.5px;
height: 88%;
background: linear-gradient(to top, var(--copper) 0%, var(--copper-hi) 70%, var(--ink) 100%);
transform-origin: bottom center;
transform: rotate(-22deg);
transition: transform 350ms var(--ease);
box-shadow: 0 0 8px var(--copper-glow);
}
.now-playing .vu-readout {
font-family: var(--mono);
font-size: 11px;
color: var(--ink-mute);
letter-spacing: 0.06em;
font-variant-numeric: tabular-nums;
display: flex;
flex-direction: column;
gap: 4px;
line-height: 1.2;
white-space: nowrap;
}
.now-playing .vu-readout strong {
color: var(--copper);
font-weight: 400;
}
/* Mobile VU cluster: stack below controls */
@media (max-width: 720px) {
.now-playing .controls { flex-wrap: wrap; }
.now-playing .vu-cluster {
margin-left: 0;
width: 100%;
justify-content: space-between;
margin-top: 12px;
}
.now-playing .vu-meter { width: 110px; height: 50px; }
.now-playing .meta-grid {
grid-template-columns: 1fr;
}
.now-playing .meta-cell {
border-right: 0;
border-bottom: 1px solid var(--rule);
padding: 12px 0;
}
.now-playing .meta-cell:last-child {
border-bottom: 0;
padding-left: 0;
}
}
+39 -50
View File
@@ -157,10 +157,10 @@
</div>
<div class="player-container" data-tab-content="player" role="tabpanel" id="panel-player">
<div class="player-layout now-playing">
<section class="now-playing player-layout">
<!-- Vinyl stage with album art as label -->
<div class="album-art-container vinyl-stage">
<!-- Vinyl stage with album art as label, plus tonearm -->
<div class="vinyl-stage album-art-container">
<div class="vinyl">
<div class="vinyl-label">
<img id="album-art-glow" class="album-art-glow" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 300'%3E%3Crect fill='%23282828' width='300' height='300'/%3E%3C/svg%3E" alt="" aria-hidden="true">
@@ -188,21 +188,19 @@
</div>
<!-- Track masthead -->
<div class="player-details track-masthead">
<div class="track-masthead player-details">
<div class="kicker"><span data-i18n="player.kicker">Now Playing</span></div>
<div class="track-info">
<div id="track-title" data-i18n="player.no_media">No media playing</div>
<div id="artist"></div>
<div id="album"></div>
</div>
<h1 class="track-title" id="track-title" data-i18n="player.no_media">No media playing</h1>
<div class="track-byline" id="artist"></div>
<div class="track-album" id="album"></div>
<!-- Editorial metadata grid (State + Source; timecodes are on the timeline) -->
<!-- 2-cell metadata grid -->
<div class="meta-grid meta-grid-2">
<div class="meta-cell">
<div class="meta-label" data-i18n="meta.state">State</div>
<div class="meta-value">
<div class="label" data-i18n="meta.state">State</div>
<div class="value">
<svg class="state-icon" id="state-icon" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 14.5v-9l6 4.5-6 4.5z"/>
</svg>
@@ -210,8 +208,8 @@
</div>
</div>
<div class="meta-cell">
<div class="meta-label" data-i18n="meta.source">Source</div>
<div class="meta-value source-value">
<div class="label" data-i18n="meta.source">Source</div>
<div class="value source-value">
<span class="source-icon" id="sourceIcon"></span>
<span id="source" data-i18n="player.unknown_source">Unknown</span>
</div>
@@ -228,64 +226,55 @@
<span></span><span></span><span></span><span></span><span></span>
</div>
<!-- Transport: progress + controls + VU cluster -->
<!-- Transport -->
<div class="transport">
<div class="progress-container progress-row">
<div class="progress-row">
<span class="timecode elapsed" id="current-time">0:00</span>
<div class="progress-bar progress-track" id="progress-bar" data-duration="0" role="slider" aria-label="Playback position" aria-valuemin="0" aria-valuemax="0" aria-valuenow="0">
<div class="progress-track progress-bar" id="progress-bar" data-duration="0" role="slider" aria-label="Playback position" aria-valuemin="0" aria-valuemax="0" aria-valuenow="0">
<div class="progress-fill" id="progress-fill"></div>
</div>
<span class="timecode" id="total-time">0:00</span>
</div>
<div class="controls">
<button onclick="previousTrack()" data-i18n-title="player.previous" title="Previous" id="btn-previous">
<svg viewBox="0 0 24 24">
<path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/>
</svg>
<button class="btn-trans" onclick="previousTrack()" data-i18n-title="player.previous" title="Previous" id="btn-previous">
<svg viewBox="0 0 24 24"><path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/></svg>
</button>
<button class="primary" onclick="togglePlayPause()" data-i18n-title="player.play" title="Play/Pause" id="btn-play-pause">
<svg viewBox="0 0 24 24" id="play-pause-icon">
<path d="M8 5v14l11-7z"/>
</svg>
<button class="btn-trans primary" onclick="togglePlayPause()" data-i18n-title="player.play" title="Play/Pause" id="btn-play-pause">
<svg viewBox="0 0 24 24" id="play-pause-icon"><path d="M8 5v14l11-7z"/></svg>
</button>
<button onclick="nextTrack()" data-i18n-title="player.next" title="Next" id="btn-next">
<svg viewBox="0 0 24 24">
<path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/>
</svg>
<button class="btn-trans" onclick="nextTrack()" data-i18n-title="player.next" title="Next" id="btn-next">
<svg viewBox="0 0 24 24"><path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/></svg>
</button>
<!-- VU cluster: needle visual + slider + readout -->
<div class="vu-cluster">
<div class="vu-cluster" onclick="toggleMute()" title="Click to mute / use mini player to adjust volume" role="button" tabindex="0">
<div class="vu-meter" aria-hidden="true">
<div class="vu-needle" id="vuNeedle"></div>
</div>
<div class="volume-container">
<button class="mute-btn" onclick="toggleMute()" data-i18n-title="player.mute" title="Mute" id="btn-mute">
<svg viewBox="0 0 24 24" id="mute-icon">
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/>
</svg>
</button>
<input type="range" id="volume-slider" min="0" max="100" value="50" aria-label="Volume">
<div class="volume-display" id="volume-display">50%</div>
<div class="vu-readout">
<span>OUT <strong id="vu-out">SYS</strong></span>
<span>VOL <strong id="vu-vol">50%</strong></span>
</div>
</div>
</div>
</div>
<!-- Audio visualizer toggle (button shown by JS only when supported) -->
<div class="source-info">
<span class="source-label">
<span class="vinyl-mode-label" data-i18n="player.modes">Modes</span>
</span>
<div class="player-toggles">
<button class="vinyl-toggle-btn" onclick="toggleVisualizer()" id="visualizerToggle" data-i18n-title="player.visualizer" title="Audio visualizer" style="display:none">
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M3 18h2v-8H3v8zm4 0h2V6H7v12zm4 0h2V2h-2v16zm4 0h2v-6h-2v6zm4 0h2V9h-2v9z"/></svg>
</button>
</div>
<!-- Hidden but functional: slider + mute + visualizer toggle.
Adjustment happens via the always-visible mini player. -->
<div class="visually-hidden">
<button class="mute-btn" onclick="toggleMute()" data-i18n-title="player.mute" title="Mute" id="btn-mute">
<svg viewBox="0 0 24 24" id="mute-icon">
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/>
</svg>
</button>
<input type="range" id="volume-slider" min="0" max="100" value="50" aria-label="Volume">
<div id="volume-display">50%</div>
<button onclick="toggleVisualizer()" id="visualizerToggle" data-i18n-title="player.visualizer" title="Audio visualizer" style="display:none">
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M3 18h2v-8H3v8zm4 0h2V6H7v12zm4 0h2V2h-2v16zm4 0h2v-6h-2v6zm4 0h2V9h-2v9z"/></svg>
</button>
</div>
</div>
</div>
</section>
</div>
<!-- Media Browser Section -->
+9
View File
@@ -685,6 +685,11 @@ export function updateUI(status) {
const deg = -45 + (status.volume / 100) * 90;
needle.style.transform = `rotate(${deg}deg)`;
}
// Editorial VU readout: VOL XX% / OUT (SYS or MUTED)
const vuVol = document.getElementById('vu-vol');
if (vuVol) vuVol.textContent = `${status.volume}%`;
const vuOut = document.getElementById('vu-out');
if (vuOut) vuOut.textContent = status.muted ? 'MUTE' : 'SYS';
}
updateMuteIcon(status.muted);
@@ -788,4 +793,8 @@ function updateMuteIcon(muted) {
const path = muted ? SVG_MUTED : SVG_UNMUTED;
dom.muteIcon.innerHTML = path;
dom.miniMuteIcon.innerHTML = path;
const vuOut = document.getElementById('vu-out');
if (vuOut) vuOut.textContent = muted ? 'MUTE' : 'SYS';
const cluster = document.querySelector('.now-playing .vu-cluster');
if (cluster) cluster.classList.toggle('muted', muted);
}