fix(ui): close gaps with Studio Reference mockup

Side-by-side comparison surfaced several layout regressions vs. the
mockup. This commit lands all of them at once.

Header
- Restore centered "Media Server / Studio Reference Edition" wordmark
  in italic Fraunces
- Move folio marks to fixed page corners (visible on every tab):
  TL = green pulse + "Connected · Local 8765"
  TR = "Vol. I — Studio Reference · v0.x.x"
- Replace boxed version-label badge with copper mono inline in folio.tr
- Reduce header-to-content gap (container padding-top 28→56 with the
  folio now anchored above)

Player view
- Spectrum bars: smaller height (32px), centered with max-width so
  they don't span the whole right column
- Spectrogram canvas: hidden by default (opacity 0); reveals only when
  visualizer toggle is active. No more leaking into bottom-left.
- VU cluster volume controls: strip legacy box (background, padding,
  border-radius); compact stacked layout with thin slider, small mute
  button, mono "VOL · XX%" readout
- Disable legacy applyVinylMode() — the .vinyl class added a SECOND
  rotation animation on top of the structural .vinyl-stage spin,
  causing visible compounding. Vinyl is now purely structural.

Toggles
- Remove vinyl mode toggle button (vinyl is always on)
- Keep audio visualizer (spectrum vis) toggle — still shown by JS
  when supported

Mini player
- Force always-visible on non-player tabs regardless of scroll, by
  short-circuiting setMiniPlayerVisible when activeTab !== 'player'

i18n
- New keys: header.connected, header.volume, header.edition,
  header.edition_sub
- Removed unused: player.folio_left, player.folio_right
- en.json + ru.json updated
This commit is contained in:
2026-04-25 01:32:34 +03:00
parent 14e9f2294e
commit 265b001b99
6 changed files with 202 additions and 57 deletions
+182 -41
View File
@@ -4112,52 +4112,121 @@ body.mini-player-visible footer {
/* ─── Container & header ────────────────────────────────────── */
.container {
max-width: 1280px;
padding: 28px 48px 140px;
padding: 56px 48px 140px;
}
@media (max-width: 720px) {
.container { padding: 18px 18px 140px; }
.container { padding: 48px 18px 140px; }
}
header {
padding: 0 0 22px 0;
border-bottom: 1px solid var(--rule-strong);
margin-bottom: 18px;
position: relative;
}
/* Folio mark on left side of header */
header > div:first-child {
/* ─── Folio marks (page corners, all tabs) ────────────────── */
body > .folio {
position: fixed;
top: 16px;
z-index: 50;
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.14em;
font-size: 10px;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--ink-mute);
color: var(--ink-faint);
display: inline-flex;
align-items: center;
gap: 8px;
pointer-events: none;
}
body > .folio.tl { left: 24px; }
body > .folio.tr { right: 24px; }
@media (max-width: 720px) {
body > .folio { font-size: 9px; letter-spacing: 0.16em; }
body > .folio.tl { left: 14px; }
body > .folio.tr { right: 14px; }
}
.status-dot {
width: 8px;
height: 8px;
/* The status-dot now lives inside the folio */
body > .folio .status-dot {
width: 7px;
height: 7px;
background: var(--jade);
border-radius: 50%;
box-shadow: 0 0 0 0 rgba(122, 178, 148, 0.55);
animation: sr-pulse 2.4s ease-in-out infinite;
}
@keyframes sr-pulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(122, 178, 148, 0.55); }
50% { box-shadow: 0 0 0 8px rgba(122, 178, 148, 0); }
50% { box-shadow: 0 0 0 6px rgba(122, 178, 148, 0); }
}
.version-label {
font-family: var(--mono);
/* Hide the old in-header status-dot if any rendering remnants exist */
header .status-dot { display: none; }
/* The version-label now lives inside the folio.tr — remove the old badge styling */
.version-label,
body > .folio #version-label {
background: transparent;
color: var(--ink-mute);
border: 1px solid var(--rule-strong);
border-radius: 0;
padding: 2px 10px;
border: 0;
padding: 0;
color: var(--copper);
font-family: var(--mono);
font-size: 10px;
letter-spacing: 0.16em;
text-transform: uppercase;
border-radius: 0;
}
/* ─── Header (3-column: brand center, toolbar right) ─────── */
header {
padding: 0 0 22px 0;
border-bottom: 1px solid var(--rule-strong);
margin-bottom: 28px;
position: relative;
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
gap: 20px;
}
/* Brand wordmark (centered) */
header .brand {
text-align: center;
grid-column: 2;
line-height: 1;
}
header .brand-name {
font-family: var(--serif);
font-style: italic;
font-weight: 400;
font-size: 30px;
letter-spacing: -0.015em;
color: var(--ink);
font-variation-settings: 'opsz' 144;
display: block;
}
header .brand-sub {
display: block;
font-family: var(--mono);
font-size: 9px;
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--ink-mute);
margin-top: 6px;
}
@media (max-width: 720px) {
header { grid-template-columns: 1fr; gap: 14px; }
header .brand { grid-column: 1; }
header .brand-name { font-size: 24px; }
header .header-toolbar { justify-self: center; }
}
.header-toolbar {
grid-column: 3;
justify-self: end;
}
@media (max-width: 720px) {
.header-toolbar { grid-column: 1; }
}
.header-toolbar {
@@ -4478,15 +4547,22 @@ header > div:first-child {
@keyframes sr-vinyl-spin { to { transform: rotate(360deg); } }
/* Spectrogram canvas hidden by default — visualizer toggle reveals it */
.vinyl-stage .spectrogram-canvas {
position: absolute;
bottom: -52px;
left: 0;
right: 0;
width: 100%;
height: 44px;
bottom: -56px;
left: 7%;
right: 7%;
width: 86%;
height: 38px;
border-radius: 0;
opacity: 0.7;
opacity: 0;
transition: opacity 240ms var(--ease);
pointer-events: none;
}
.vinyl-stage.visualizer-active .spectrogram-canvas,
body.visualizer-active .vinyl-stage .spectrogram-canvas {
opacity: 0.6;
}
/* ─── Player details (right column / masthead) ──────────────── */
@@ -4616,17 +4692,20 @@ header > div:first-child {
/* Hide the legacy .playback-state container (its data is now in meta-grid) */
.track-info > .playback-state { display: none; }
/* Spectrum decorative bars */
/* Spectrum decorative bars (centered, compact) */
.spectrum {
display: flex;
align-items: flex-end;
justify-content: center;
gap: 3px;
height: 38px;
margin-top: 28px;
margin-bottom: 8px;
height: 32px;
margin: 28px auto 8px;
max-width: 360px;
}
.spectrum span {
flex: 1;
display: block;
width: 3px;
flex: 0 0 3px;
background: linear-gradient(to top, var(--copper-lo), var(--copper-hi));
opacity: 0.85;
border-radius: 99px 99px 0 0;
@@ -4747,32 +4826,94 @@ header > div:first-child {
box-shadow: 0 0 8px var(--copper-glow);
}
/* Volume container nested inside vu-cluster — compact stacked controls */
/* Volume container nested inside vu-cluster — strip legacy box & make compact */
.vu-cluster .volume-container {
margin-top: 0;
padding-top: 0;
background: transparent;
border: 0;
border-top: 0;
border-radius: 0;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 8px;
gap: 6px;
align-items: stretch;
min-width: 140px;
min-width: 0;
width: 110px;
box-shadow: none;
}
.vu-cluster .volume-container > .mute-btn {
align-self: flex-start;
align-self: flex-end;
width: 28px;
height: 28px;
border-radius: 50%;
background: transparent;
border: 1px solid var(--rule-strong);
color: var(--ink-soft);
padding: 0;
margin: 0;
}
.vu-cluster .volume-container > .mute-btn:hover {
border-color: var(--copper);
color: var(--copper);
}
.vu-cluster .volume-container > .mute-btn svg {
width: 12px;
height: 12px;
fill: currentColor;
}
.vu-cluster .volume-container > #volume-slider {
width: 100%;
height: 2px;
flex: none;
background: var(--rule-strong);
border-radius: 0;
margin: 0;
padding: 0;
-webkit-appearance: none;
appearance: none;
}
.vu-cluster .volume-container > #volume-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 10px;
height: 10px;
background: var(--copper);
border-radius: 50%;
box-shadow: 0 0 8px var(--copper-glow);
border: 0;
}
.vu-cluster .volume-container > .volume-display {
text-align: right;
font-family: var(--mono);
font-size: 10px;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--ink-mute);
background: transparent;
border: 0;
padding: 0;
margin: 0;
font-variant-numeric: tabular-nums;
width: auto;
min-width: 0;
font-weight: 500;
}
.vu-cluster .volume-container > .volume-display::before {
content: "VOL · ";
color: var(--ink-faint);
font-weight: 400;
}
/* Make the player view source-info row tighter (visualizer toggle row) */
.player-details > .source-info {
margin-top: 18px;
padding-top: 14px;
border-top: 1px solid var(--rule);
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
}
/* ─── Progress (hairline editorial) ─────────────────────────── */
+8 -10
View File
@@ -75,11 +75,15 @@
</div>
</div>
<!-- Folio marks at page corners -->
<span class="folio tl"><span class="status-dot" id="status-dot" aria-live="polite"></span><span data-i18n="header.connected">Connected</span> · <span id="folio-host">Local 8765</span></span>
<span class="folio tr"><span data-i18n="header.volume">Vol. I</span><span data-i18n="header.edition">Studio Reference</span> · <span id="version-label"></span></span>
<div class="container">
<header>
<div style="display: flex; align-items: center; gap: 0.5rem;">
<span class="status-dot" id="status-dot" aria-live="polite"></span>
<span class="version-label" id="version-label"></span>
<div class="brand">
<span class="brand-name" data-i18n="app.title">Media Server</span>
<span class="brand-sub" data-i18n="header.edition_sub">Studio Reference Edition</span>
</div>
<div class="header-toolbar">
<div id="headerLinks" class="header-links"></div>
@@ -153,9 +157,6 @@
</div>
<div class="player-container" data-tab-content="player" role="tabpanel" id="panel-player">
<span class="folio tl"><span data-i18n="player.folio_left">Now Spinning</span> · <span id="folio-version">v—</span></span>
<span class="folio tr" data-i18n="player.folio_right">Vol. I — Studio Reference</span>
<div class="player-layout now-playing">
<!-- Vinyl stage with album art as label -->
@@ -280,15 +281,12 @@
</div>
</div>
<!-- Player toggles -->
<!-- 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="toggleVinylMode()" id="vinylToggle" data-i18n-title="player.vinyl" title="Vinyl mode">
<svg viewBox="0 0 24 24" width="16" height="16"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="1.5"/><circle cx="12" cy="12" r="3" fill="currentColor"/><circle cx="12" cy="12" r="6.5" fill="none" stroke="currentColor" stroke-width="0.5" opacity="0.5"/></svg>
</button>
<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>
+2 -2
View File
@@ -163,8 +163,8 @@ window.addEventListener('DOMContentLoaded', async () => {
navigator.serviceWorker.register('/sw.js').catch(() => {});
}
// Initialize vinyl mode
applyVinylMode();
// Vinyl is now structural / always-on via CSS — no init call needed.
// applyVinylMode();
// Initialize audio visualizer
checkVisualizerAvailability().then(() => {
+2
View File
@@ -19,6 +19,8 @@ import { IconSelect } from './icon-select.js';
export let activeTab = 'player';
export function setMiniPlayerVisible(visible) {
// On any non-player tab the mini player must stay visible regardless of scroll.
if (activeTab !== 'player') visible = true;
const miniPlayer = document.getElementById('mini-player');
if (visible) {
miniPlayer.classList.remove('hidden');
+4 -2
View File
@@ -21,8 +21,10 @@
"player.no_media": "No media playing",
"player.kicker": "Now Playing",
"player.modes": "Modes",
"player.folio_left": "Now Spinning",
"player.folio_right": "Vol. I — Studio Reference",
"header.connected": "Connected",
"header.volume": "Vol. I",
"header.edition": "Studio Reference",
"header.edition_sub": "Studio Reference Edition",
"meta.state": "State",
"meta.source": "Source",
"meta.elapsed": "Elapsed",
+4 -2
View File
@@ -21,8 +21,10 @@
"player.no_media": "Медиа не воспроизводится",
"player.kicker": "Сейчас играет",
"player.modes": "Режимы",
"player.folio_left": "Сейчас играет",
"player.folio_right": "Том I — Studio Reference",
"header.connected": "Подключено",
"header.volume": "Том I",
"header.edition": "Studio Reference",
"header.edition_sub": "Studio Reference Edition",
"meta.state": "Состояние",
"meta.source": "Источник",
"meta.elapsed": "Прошло",