fix(ui): close more gaps with mockup (tabs, mini player, volume control)

Tabs
- Replace SVG icons with mockup-style numeric badges (01..05)
- Hide the legacy .tab-indicator (was rendering as a long copper
  bar above the strip); active tab gets its own copper underline
  via .tab-btn.active::after
- Numbers turn copper on the active tab

Player view
- Volume control restored: mute button + slim copper slider live
  inside the VU cluster, on the right of the readout, separated
  by a hairline. Slider is the existing #volume-slider so all JS
  hooks (bidirectional sync, drag, etc.) keep working.
- Track title font scaled down (clamp 34..64) and clamped to 3
  lines with ellipsis so long YouTube-style titles don't dominate
  the masthead. Adds word-break + overflow-wrap.
- #artist:empty and #album:empty are now display:none so blank
  rows don't leave a gap when the source provides no metadata.

Mini player
- Forced display: grid with 4 columns: track / controls / progress
  / volume. Was inheriting legacy display:flex which pushed elements
  into a single non-aligned row.
- Position locked: position: fixed, bottom: 0, left/right: 0 with
  !important. The strip is firmly anchored to the viewport bottom.
- Top edge progress (::before) painted in copper with glow.
- Responsive collapse: hide progress at <=880px, hide volume at
  <=540px, leaving track + controls on phones.

i18n
- tab.player default text aligned to "Now Spinning" (matches
  existing en/ru values added earlier).
This commit is contained in:
2026-04-25 01:51:13 +03:00
parent 77b39e5684
commit e9e4165927
2 changed files with 241 additions and 42 deletions
+222 -25
View File
@@ -4294,21 +4294,22 @@ header .brand-sub {
margin: 14px 0 36px;
border-radius: 0;
gap: 0;
position: relative;
overflow-x: auto;
scrollbar-width: none;
}
.tab-bar::-webkit-scrollbar { display: none; }
/* Hide the legacy tab-indicator entirely — we use ::after on .tab-btn.active instead */
.tab-indicator {
background: var(--copper);
box-shadow: 0 0 12px var(--copper-glow);
height: 2px;
bottom: -1px;
border-radius: 0;
display: none !important;
}
.tab-btn {
background: transparent;
border: 0;
color: var(--ink-mute);
padding: 18px 22px 16px;
padding: 18px 26px 16px;
border-radius: 0;
font-family: var(--sans);
font-size: 13px;
@@ -4316,8 +4317,11 @@ header .brand-sub {
letter-spacing: 0.04em;
transition: color 180ms var(--ease);
position: relative;
gap: 10px;
display: inline-flex;
align-items: baseline;
gap: 10px;
white-space: nowrap;
cursor: pointer;
}
.tab-btn:hover { color: var(--ink-soft); background: transparent; }
.tab-btn.active {
@@ -4330,8 +4334,35 @@ header .brand-sub {
font-variation-settings: 'opsz' 60;
}
.tab-btn svg { opacity: 0.7; }
.tab-btn.active svg { opacity: 1; color: var(--copper); }
/* Tab number badge (replaces the icon) */
.tab-btn .tab-num {
font-family: var(--mono);
font-style: normal;
font-size: 10px;
color: var(--ink-faint);
letter-spacing: 0.15em;
font-weight: 400;
}
.tab-btn.active .tab-num {
color: var(--copper);
}
/* Hide any remaining legacy SVG icons on tabs */
.tab-btn svg {
display: none;
}
/* Active tab underline — only under the active tab */
.tab-btn.active::after {
content: "";
position: absolute;
bottom: -1px;
left: 0;
right: 0;
height: 2px;
background: var(--copper);
box-shadow: 0 0 12px var(--copper-glow);
}
/* ─── Update + connection banners ───────────────────────────── */
.update-banner,
@@ -5156,17 +5187,102 @@ body.visualizer-active .vinyl-stage .spectrogram-canvas {
}
/* ═══════════════════════════════════════════════════════════════
MINI PLAYER — console strip
MINI PLAYER — sticky console strip (3-column grid)
═══════════════════════════════════════════════════════════════ */
.mini-player {
background: linear-gradient(180deg, rgba(14, 13, 11, 0.7) 0%, rgba(14, 13, 11, 0.96) 30%);
/* Override legacy display: flex with explicit grid */
display: grid !important;
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) auto;
column-gap: 24px;
row-gap: 0;
align-items: center;
background: linear-gradient(180deg, rgba(14, 13, 11, 0.72) 0%, rgba(14, 13, 11, 0.96) 30%);
backdrop-filter: blur(20px) saturate(160%);
-webkit-backdrop-filter: blur(20px) saturate(160%);
border-top: 1px solid var(--rule-strong);
box-shadow: none;
padding: 14px 32px;
gap: 28px;
position: relative;
padding: 12px 32px;
/* Anchor firmly to the bottom of the viewport */
position: fixed !important;
bottom: 0 !important;
left: 0 !important;
right: 0 !important;
z-index: 1000;
}
/* Top edge progress line in copper */
.mini-player::before {
background: var(--copper) !important;
height: 2px !important;
box-shadow: 0 0 8px var(--copper-glow);
}
/* Column 1: track info */
.mini-player .mini-player-info {
grid-column: 1;
grid-row: 1;
min-width: 0;
overflow: hidden;
display: flex;
align-items: center;
gap: 12px;
}
/* Column 2: transport (centered) */
.mini-player .mini-controls {
grid-column: 2;
grid-row: 1;
justify-self: center;
display: flex;
align-items: center;
gap: 12px;
flex-shrink: 0;
}
/* Column 3: progress (timecodes + bar) */
.mini-player .mini-progress-container {
grid-column: 3;
grid-row: 1;
display: flex;
flex-direction: column;
gap: 4px;
justify-self: stretch;
align-items: stretch;
min-width: 180px;
max-width: 360px;
margin: 0 auto;
}
.mini-player .mini-progress-container .mini-time-display {
display: flex;
justify-content: space-between;
}
/* Column 4: volume controls */
.mini-player .mini-volume-container {
grid-column: 4;
grid-row: 1;
justify-self: end;
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
@media (max-width: 880px) {
.mini-player {
grid-template-columns: minmax(0, 1fr) auto auto;
column-gap: 14px;
}
.mini-player .mini-progress-container { display: none; }
.mini-player .mini-volume-container { grid-column: 3; }
}
@media (max-width: 540px) {
.mini-player {
grid-template-columns: minmax(0, 1fr) auto;
padding: 10px 14px;
}
.mini-player .mini-volume-container { display: none; }
}
.mini-player::before {
@@ -6507,13 +6623,22 @@ body.visualizer-active .now-playing .spectrogram-canvas {
.now-playing #track-title {
font-family: var(--serif);
font-weight: 400;
font-size: clamp(42px, 5.6vw, 78px);
line-height: 0.96;
font-size: clamp(34px, 4.4vw, 64px);
line-height: 0.98;
letter-spacing: -0.02em;
font-variation-settings: 'opsz' 144;
margin-bottom: 18px;
color: var(--ink);
margin-top: 0;
/* Long titles get clamped to 3 lines with ellipsis */
display: -webkit-box;
-webkit-line-clamp: 3;
line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
word-break: break-word;
overflow-wrap: anywhere;
hyphens: auto;
}
.now-playing .track-title em {
font-style: italic;
@@ -6531,7 +6656,6 @@ body.visualizer-active .now-playing .spectrogram-canvas {
margin-bottom: 4px;
margin-top: 0;
}
.now-playing .track-album,
.now-playing #album {
font-family: var(--sans);
@@ -6543,6 +6667,14 @@ body.visualizer-active .now-playing .spectrogram-canvas {
margin-top: 0;
}
/* Hide byline/album rows when their content is empty so they
don't leave a stretched blank gap. JS leaves them empty when
the source provides no metadata. */
.now-playing #artist:empty,
.now-playing #album:empty {
display: none;
}
/* ─── 2-cell metadata grid ────────────────────────────────── */
.now-playing .meta-grid {
display: grid;
@@ -6772,20 +6904,13 @@ body.visualizer-active .now-playing .spectrogram-canvas {
width: 24px; height: 24px;
}
/* VU cluster — display-only, click toggles mute */
/* VU cluster — meter + readout + integrated volume control */
.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%);
@@ -6793,6 +6918,78 @@ body.visualizer-active .now-playing .spectrogram-canvas {
}
.now-playing .vu-cluster.muted .vu-readout strong { color: var(--rust); }
/* Integrated volume control: tiny mute icon + slim copper slider */
.now-playing .vu-volume {
display: flex;
align-items: center;
gap: 10px;
padding-left: 12px;
border-left: 1px solid var(--rule);
margin-left: 4px;
}
.now-playing .vu-volume .mute-btn {
background: transparent;
border: 1px solid var(--rule-strong);
color: var(--ink-soft);
width: 30px;
height: 30px;
border-radius: 50%;
padding: 0;
margin: 0;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 180ms var(--ease);
flex-shrink: 0;
}
.now-playing .vu-volume .mute-btn:hover {
border-color: var(--copper);
color: var(--copper);
background: rgba(224, 128, 56, 0.06);
}
.now-playing .vu-volume .mute-btn svg {
width: 14px;
height: 14px;
fill: currentColor;
}
.now-playing .vu-volume #volume-slider {
-webkit-appearance: none;
appearance: none;
width: 80px;
height: 2px;
background: var(--rule-strong);
border-radius: 0;
margin: 0;
padding: 0;
cursor: pointer;
flex: none;
}
.now-playing .vu-volume #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;
cursor: grab;
}
.now-playing .vu-volume #volume-slider::-moz-range-thumb {
width: 10px;
height: 10px;
background: var(--copper);
border-radius: 50%;
border: 0;
cursor: grab;
}
.now-playing .vu-volume #volume-slider::-moz-range-track {
height: 2px;
background: var(--rule-strong);
border-radius: 0;
}
.now-playing .vu-meter {
position: relative;
width: 140px;
+19 -17
View File
@@ -131,27 +131,27 @@
<button class="connection-banner-btn" id="connectionBannerBtn" onclick="manualReconnect()" style="display: none;" data-i18n="connection.reconnect">Reconnect</button>
</div>
<!-- Tab Bar -->
<!-- Tab Bar (editorial: numbered, italic active) -->
<div class="tab-bar" id="tabBar" role="tablist">
<div class="tab-indicator" id="tabIndicator"></div>
<button class="tab-btn active" data-tab="player" onclick="switchTab('player')" role="tab" aria-selected="true" aria-controls="panel-player" tabindex="0">
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/></svg>
<span data-i18n="tab.player">Player</span>
<span class="tab-num">01</span>
<span data-i18n="tab.player">Now Spinning</span>
</button>
<button class="tab-btn" data-tab="display" onclick="switchTab('display')" role="tab" aria-selected="false" aria-controls="panel-display" tabindex="-1">
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M20 3H4c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h6v2H8v2h8v-2h-2v-2h6c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 12H4V5h16v10z"/></svg>
<span class="tab-num">02</span>
<span data-i18n="tab.display">Display</span>
</button>
<button class="tab-btn" data-tab="browser" onclick="switchTab('browser')" role="tab" aria-selected="false" aria-controls="panel-browser" tabindex="-1">
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg>
<span data-i18n="tab.browser">Browser</span>
<span class="tab-num">03</span>
<span data-i18n="tab.browser">Library</span>
</button>
<button class="tab-btn" data-tab="quick-actions" onclick="switchTab('quick-actions')" role="tab" aria-selected="false" aria-controls="panel-quick-actions" tabindex="-1">
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M7 2v11h3v9l7-12h-4l4-8z"/></svg>
<span class="tab-num">04</span>
<span data-i18n="tab.quick_access">Quick Access</span>
</button>
<button class="tab-btn" data-tab="settings" onclick="switchTab('settings')" role="tab" aria-selected="false" aria-controls="panel-settings" tabindex="-1">
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58a.49.49 0 00.12-.61l-1.92-3.32a.488.488 0 00-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54a.484.484 0 00-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.07.62-.07.94s.02.64.07.94l-2.03 1.58a.49.49 0 00-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></svg>
<span class="tab-num">05</span>
<span data-i18n="tab.settings">Settings</span>
</button>
</div>
@@ -247,7 +247,7 @@
<svg viewBox="0 0 24 24"><path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/></svg>
</button>
<div class="vu-cluster" onclick="toggleMute()" title="Click to mute / use mini player to adjust volume" role="button" tabindex="0">
<div class="vu-cluster">
<div class="vu-meter" aria-hidden="true">
<div class="vu-needle" id="vuNeedle"></div>
</div>
@@ -255,19 +255,21 @@
<span>OUT <strong id="vu-out">SYS</strong></span>
<span>VOL <strong id="vu-vol">50%</strong></span>
</div>
<!-- Volume control: mute + slim slider, integrated -->
<div class="vu-volume">
<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>
</div>
</div>
</div>
<!-- Hidden but functional: slider + mute + visualizer toggle.
Adjustment happens via the always-visible mini player. -->
<!-- Hidden but functional: legacy display + visualizer toggle. -->
<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>