Now Playing modal: full visual redesign + click-art visualizer, sleep timer, up-next
Player-revamp frontend (Phase 1). Brings the Now Playing modal to the approved mockup look + features: - Full restyle (override block in style.css): 28px modal radius, stronger art-driven ambient glow, 340px rounded art that scales while playing, bold 28px title, accent artist name, accent FLAC pill, dominant 70px gradient play button, accent-gradient progress/volume/visualizer. All driven by the existing --accent-rgb / --accent-light-rgb so it follows the settings accent. - Click album art -> Plexamp-style visualizer takeover, fed by the REAL music-synced Web Audio analyser (npStartVisualizerLoop), click again -> art. - Rich queue rows: album thumbnail + title/artist + duration, equalizer animation on the now-playing row, hover-reveal remove. - Up-next peek below the controls (shows the next queued track). - Sleep timer (cycles 15/30/60m, real setTimeout -> handleStop). - Crossfade toggle present (visual state + persisted pref; the dual-audio crossfade engine is the next step, not yet wired). Frontend-only; verified live in-browser by Boulder. No backend/test surface.
This commit is contained in:
parent
ca90c6ae6f
commit
3461d9235b
3 changed files with 333 additions and 4 deletions
|
|
@ -7017,13 +7017,18 @@
|
|||
<div class="np-body">
|
||||
<!-- Left: album art + track info -->
|
||||
<div class="np-left">
|
||||
<div class="np-album-art-container">
|
||||
<div class="np-album-art-container" id="np-album-art-container" title="Click for visualizer">
|
||||
<img class="np-album-art hidden" id="np-album-art" src="" alt="Album Art">
|
||||
<div class="np-album-art-placeholder" id="np-album-art-placeholder">
|
||||
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/><line x1="12" y1="2" x2="12" y2="5"/><line x1="12" y1="19" x2="12" y2="22"/>
|
||||
</svg>
|
||||
</div>
|
||||
<!-- Big music-synced visualizer takeover (Plexamp-style); click art to toggle -->
|
||||
<div class="np-art-viz" id="np-art-viz" aria-hidden="true"></div>
|
||||
<div class="np-art-hint" id="np-art-hint" title="Visualizer">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><rect x="3" y="10" width="3" height="11" rx="1"/><rect x="10.5" y="4" width="3" height="17" rx="1"/><rect x="18" y="13" width="3" height="8" rx="1"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="np-track-info">
|
||||
<div class="np-track-title" id="np-track-title">No track</div>
|
||||
|
|
@ -7041,6 +7046,16 @@
|
|||
</div>
|
||||
<!-- Right: controls -->
|
||||
<div class="np-right">
|
||||
<div class="np-util-row">
|
||||
<button class="np-util-btn" id="np-sleep-btn" title="Sleep timer">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
||||
<span id="np-sleep-label">Sleep</span>
|
||||
</button>
|
||||
<button class="np-util-btn" id="np-crossfade-btn" title="Crossfade between tracks">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 8l-4 4 4 4"/><path d="M17 8l4 4-4 4"/><line x1="3" y1="12" x2="21" y2="12"/></svg>
|
||||
<span>Crossfade</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="np-progress-section">
|
||||
<div class="np-progress-bar-container">
|
||||
<div class="np-progress-track">
|
||||
|
|
@ -7085,6 +7100,15 @@
|
|||
<input type="range" class="np-volume-slider" id="np-volume-slider" min="0" max="100" value="70">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Up-next peek (hidden when queue has no next track) -->
|
||||
<div class="np-upnext hidden" id="np-upnext">
|
||||
<span class="np-upnext-label">Next</span>
|
||||
<img class="np-upnext-art" id="np-upnext-art" alt="">
|
||||
<div class="np-upnext-info">
|
||||
<div class="np-upnext-title" id="np-upnext-title"></div>
|
||||
<div class="np-upnext-artist" id="np-upnext-artist"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="np-stop-row">
|
||||
<button class="np-btn np-btn-stop" id="np-stop-btn" title="Stop playback">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><rect x="4" y="4" width="16" height="16" rx="2"/></svg>
|
||||
|
|
|
|||
|
|
@ -1382,6 +1382,9 @@ let npAnalyser = null;
|
|||
let npMediaSource = null;
|
||||
let npVizAnimFrame = null;
|
||||
let npVizInitialized = false;
|
||||
let npCrossfadeOn = false;
|
||||
let npSleepMinutes = 0; // 0 = off
|
||||
let npSleepTimerId = null;
|
||||
|
||||
function npQueueHasNext() {
|
||||
if (npQueue.length === 0) return false;
|
||||
|
|
@ -1451,6 +1454,28 @@ function initExpandedPlayer() {
|
|||
// Control handlers
|
||||
playBtn.addEventListener('click', () => { togglePlayback(); });
|
||||
stopBtn.addEventListener('click', async () => { await handleStop(); closeNowPlayingModal(); });
|
||||
|
||||
// Click album art → toggle the music-synced visualizer takeover
|
||||
const artContainer = document.getElementById('np-album-art-container');
|
||||
if (artContainer) {
|
||||
artContainer.addEventListener('click', () => {
|
||||
const on = artContainer.classList.toggle('viz-on');
|
||||
if (on) { npBuildArtViz(); npInitVisualizer(); npStartVisualizerLoop(); }
|
||||
});
|
||||
}
|
||||
|
||||
// Sleep timer — cycles off → 15 → 30 → 60 min → off
|
||||
const sleepBtn = document.getElementById('np-sleep-btn');
|
||||
if (sleepBtn) sleepBtn.addEventListener('click', npCycleSleepTimer);
|
||||
|
||||
// Crossfade toggle (visual state now; dual-audio crossfade wired later)
|
||||
const xfadeBtn = document.getElementById('np-crossfade-btn');
|
||||
if (xfadeBtn) xfadeBtn.addEventListener('click', () => {
|
||||
npCrossfadeOn = !npCrossfadeOn;
|
||||
xfadeBtn.classList.toggle('active', npCrossfadeOn);
|
||||
try { localStorage.setItem('soulsync-crossfade', npCrossfadeOn ? '1' : '0'); } catch (e) {}
|
||||
});
|
||||
|
||||
shuffleBtn.addEventListener('click', handleNpShuffle);
|
||||
repeatBtn.addEventListener('click', handleNpRepeat);
|
||||
muteBtn.addEventListener('click', handleNpMuteToggle);
|
||||
|
|
@ -1588,6 +1613,10 @@ function syncExpandedPlayerUI() {
|
|||
const viz = document.getElementById('np-visualizer');
|
||||
if (viz) viz.classList.toggle('playing', isPlaying);
|
||||
|
||||
// Album-art scale-on-play (Phase A restyle — CSS keys off .np-modal.playing)
|
||||
const npModalEl = document.querySelector('.np-modal');
|
||||
if (npModalEl) npModalEl.classList.toggle('playing', isPlaying);
|
||||
|
||||
// Queue
|
||||
renderNpQueue();
|
||||
updateNpPrevNextButtons();
|
||||
|
|
@ -2074,6 +2103,18 @@ function renderNpQueue() {
|
|||
item.className = 'np-queue-item' + (i === npQueueIndex ? ' active' : '');
|
||||
item.onclick = () => playQueueItem(i);
|
||||
|
||||
// Album thumbnail
|
||||
const art = document.createElement('img');
|
||||
art.className = 'np-queue-item-art';
|
||||
art.alt = '';
|
||||
if (track.image_url) {
|
||||
art.src = track.image_url;
|
||||
art.onerror = () => { art.style.visibility = 'hidden'; };
|
||||
} else {
|
||||
art.style.visibility = 'hidden';
|
||||
}
|
||||
item.appendChild(art);
|
||||
|
||||
const info = document.createElement('div');
|
||||
info.className = 'np-queue-item-info';
|
||||
|
||||
|
|
@ -2089,6 +2130,19 @@ function renderNpQueue() {
|
|||
info.appendChild(artist);
|
||||
item.appendChild(info);
|
||||
|
||||
// Active row → equalizer animation; others → duration
|
||||
if (i === npQueueIndex) {
|
||||
const eq = document.createElement('div');
|
||||
eq.className = 'np-queue-item-eq';
|
||||
eq.innerHTML = '<i></i><i></i><i></i>';
|
||||
item.appendChild(eq);
|
||||
} else if (track.duration) {
|
||||
const dur = document.createElement('span');
|
||||
dur.className = 'np-queue-item-duration';
|
||||
dur.textContent = formatTime(track.duration);
|
||||
item.appendChild(dur);
|
||||
}
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'np-queue-item-remove';
|
||||
removeBtn.innerHTML = '✕';
|
||||
|
|
@ -2101,6 +2155,48 @@ function renderNpQueue() {
|
|||
|
||||
listEl.appendChild(item);
|
||||
});
|
||||
|
||||
npUpdateUpNext();
|
||||
}
|
||||
|
||||
// Up-next peek: show the track that plays after the current one.
|
||||
function npUpdateUpNext() {
|
||||
const box = document.getElementById('np-upnext');
|
||||
if (!box) return;
|
||||
const next = npQueue[npQueueIndex + 1];
|
||||
if (!next) { box.classList.add('hidden'); return; }
|
||||
box.classList.remove('hidden');
|
||||
const art = document.getElementById('np-upnext-art');
|
||||
const title = document.getElementById('np-upnext-title');
|
||||
const artist = document.getElementById('np-upnext-artist');
|
||||
if (title) title.textContent = next.title || 'Unknown Track';
|
||||
if (artist) artist.textContent = next.artist || 'Unknown Artist';
|
||||
if (art) {
|
||||
if (next.image_url) { art.src = next.image_url; art.style.visibility = ''; art.onerror = () => { art.style.visibility = 'hidden'; }; }
|
||||
else { art.style.visibility = 'hidden'; }
|
||||
}
|
||||
}
|
||||
|
||||
// Sleep timer: cycle off → 15 → 30 → 60 → off; stops playback when it fires.
|
||||
function npCycleSleepTimer() {
|
||||
const steps = [0, 15, 30, 60];
|
||||
npSleepMinutes = steps[(steps.indexOf(npSleepMinutes) + 1) % steps.length];
|
||||
const btn = document.getElementById('np-sleep-btn');
|
||||
const label = document.getElementById('np-sleep-label');
|
||||
if (npSleepTimerId) { clearTimeout(npSleepTimerId); npSleepTimerId = null; }
|
||||
if (npSleepMinutes > 0) {
|
||||
if (label) label.textContent = `Sleep ${npSleepMinutes}m`;
|
||||
if (btn) btn.classList.add('active');
|
||||
npSleepTimerId = setTimeout(() => {
|
||||
handleStop();
|
||||
npSleepMinutes = 0;
|
||||
if (label) label.textContent = 'Sleep';
|
||||
if (btn) btn.classList.remove('active');
|
||||
}, npSleepMinutes * 60 * 1000);
|
||||
} else {
|
||||
if (label) label.textContent = 'Sleep';
|
||||
if (btn) btn.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
function updateNpPrevNextButtons() {
|
||||
|
|
@ -2220,6 +2316,19 @@ function npInitVisualizer() {
|
|||
}
|
||||
}
|
||||
|
||||
// Number of bars in the big album-art visualizer takeover.
|
||||
const NP_ART_VIZ_BAR_COUNT = 28;
|
||||
|
||||
function npBuildArtViz() {
|
||||
const container = document.getElementById('np-art-viz');
|
||||
if (!container || container.children.length > 0) return;
|
||||
for (let i = 0; i < NP_ART_VIZ_BAR_COUNT; i++) {
|
||||
const bar = document.createElement('div');
|
||||
bar.className = 'np-art-viz-bar';
|
||||
container.appendChild(bar);
|
||||
}
|
||||
}
|
||||
|
||||
function npStartVisualizerLoop() {
|
||||
if (npVizAnimFrame) return; // Already running
|
||||
if (!npAnalyser) return; // No analyser — CSS fallback handles it
|
||||
|
|
@ -2229,7 +2338,6 @@ function npStartVisualizerLoop() {
|
|||
}
|
||||
|
||||
const bars = document.querySelectorAll('.np-viz-bar');
|
||||
if (bars.length === 0) return;
|
||||
const bufferLength = npAnalyser.frequencyBinCount;
|
||||
const dataArray = new Uint8Array(bufferLength);
|
||||
|
||||
|
|
@ -2237,14 +2345,25 @@ function npStartVisualizerLoop() {
|
|||
npVizAnimFrame = requestAnimationFrame(draw);
|
||||
npAnalyser.getByteFrequencyData(dataArray);
|
||||
|
||||
// Map 7 bars to frequency bins (skip bin 0 which is DC offset)
|
||||
const binCount = Math.min(bufferLength - 1, 7);
|
||||
// Map 7 transport bars to frequency bins (skip bin 0 = DC offset)
|
||||
for (let i = 0; i < bars.length; i++) {
|
||||
const binIndex = Math.min(i + 1, bufferLength - 1);
|
||||
const value = dataArray[binIndex] / 255; // 0..1
|
||||
const scale = Math.max(0.08, value); // minimum visible height
|
||||
bars[i].style.transform = `scaleY(${scale})`;
|
||||
}
|
||||
|
||||
// Big album-art visualizer (when toggled on) — same real analyser,
|
||||
// spread across more bars for a fuller spectrum.
|
||||
const artBars = document.querySelectorAll('.np-art-viz-bar');
|
||||
if (artBars.length) {
|
||||
const span = bufferLength - 1;
|
||||
for (let i = 0; i < artBars.length; i++) {
|
||||
const binIndex = 1 + Math.floor((i / artBars.length) * span);
|
||||
const value = dataArray[Math.min(binIndex, bufferLength - 1)] / 255;
|
||||
artBars[i].style.height = Math.max(6, value * 100) + '%';
|
||||
}
|
||||
}
|
||||
}
|
||||
draw();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48581,6 +48581,192 @@ textarea.enhanced-meta-field-input {
|
|||
background: rgba(255, 80, 80, 0.1);
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
NOW PLAYING — full redesign (player revamp). Comprehensive override of the
|
||||
.np-* modal styling to the approved mockup look. !important is used through-
|
||||
out to guarantee these win over the original block above regardless of
|
||||
source order. Accent-driven via --accent-rgb / --accent-light-rgb.
|
||||
══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
.np-modal {
|
||||
width: min(960px, 94vw) !important;
|
||||
max-width: 960px !important;
|
||||
border-radius: 28px !important;
|
||||
background: linear-gradient(180deg, rgba(26,26,28,0.97) 0%, rgba(11,11,12,0.99) 100%) !important;
|
||||
border: 1px solid rgba(255,255,255,0.08) !important;
|
||||
box-shadow: 0 40px 120px rgba(0,0,0,0.72),
|
||||
0 0 0 1px rgba(255,255,255,0.03) inset,
|
||||
0 1px 0 rgba(255,255,255,0.06) inset !important;
|
||||
}
|
||||
.np-modal::before {
|
||||
top: -30% !important;
|
||||
height: 72% !important;
|
||||
background: radial-gradient(ellipse at 28% 18%,
|
||||
rgba(var(--np-ambient-r), var(--np-ambient-g), var(--np-ambient-b), 0.45) 0%,
|
||||
rgba(var(--np-ambient-r), var(--np-ambient-g), var(--np-ambient-b), 0.12) 38%,
|
||||
transparent 68%) !important;
|
||||
filter: blur(48px) !important;
|
||||
}
|
||||
.np-body { padding: 54px 44px 38px !important; gap: 48px !important; align-items: flex-start !important; }
|
||||
|
||||
/* ── Album art: big, rounded, accent depth, scales while playing ── */
|
||||
.np-album-art-container {
|
||||
width: 340px !important;
|
||||
height: 340px !important;
|
||||
border-radius: 22px !important;
|
||||
box-shadow: 0 24px 70px rgba(0,0,0,0.6), 0 6px 22px rgba(var(--accent-rgb),0.16) !important;
|
||||
transition: transform 0.5s cubic-bezier(0.16,1,0.3,1), box-shadow 0.5s !important;
|
||||
}
|
||||
.np-modal.playing .np-album-art-container {
|
||||
transform: scale(1.02) !important;
|
||||
box-shadow: 0 30px 92px rgba(0,0,0,0.66), 0 12px 40px rgba(var(--accent-rgb),0.28) !important;
|
||||
}
|
||||
|
||||
/* ── Track meta ── */
|
||||
.np-track-info { text-align: left !important; }
|
||||
.np-track-title { font-size: 28px !important; font-weight: 800 !important; letter-spacing: -0.02em !important; line-height: 1.1 !important; }
|
||||
.np-artist-name { font-size: 15px !important; font-weight: 600 !important; color: rgb(var(--accent-light-rgb)) !important; }
|
||||
.np-album-name { font-size: 13px !important; color: rgba(255,255,255,0.4) !important; }
|
||||
|
||||
/* ── Format badges → accent pills ── */
|
||||
.np-format-badges { display: flex !important; gap: 8px !important; flex-wrap: wrap !important; margin-top: 16px !important; }
|
||||
.np-format-badge {
|
||||
font-size: 10.5px !important; font-weight: 700 !important; letter-spacing: 0.04em !important; text-transform: uppercase !important;
|
||||
padding: 5px 11px !important; border-radius: 999px !important;
|
||||
background: rgba(255,255,255,0.07) !important; border: 1px solid rgba(255,255,255,0.08) !important;
|
||||
color: rgba(255,255,255,0.62) !important;
|
||||
}
|
||||
.np-format-badge.flac {
|
||||
color: rgb(var(--accent-light-rgb)) !important;
|
||||
border-color: rgba(var(--accent-rgb),0.4) !important;
|
||||
background: rgba(var(--accent-rgb),0.12) !important;
|
||||
}
|
||||
|
||||
/* ── Progress / volume: thicker track, accent gradient fill, glow on hover ── */
|
||||
.np-progress-track, .np-volume-track { height: 6px !important; border-radius: 999px !important; background: rgba(255,255,255,0.12) !important; }
|
||||
.np-progress-fill, .np-volume-fill {
|
||||
background: linear-gradient(90deg, rgb(var(--accent-rgb)), rgb(var(--accent-light-rgb))) !important;
|
||||
border-radius: 999px !important;
|
||||
}
|
||||
.np-progress-bar-container:hover .np-progress-fill { box-shadow: 0 0 12px rgba(var(--accent-rgb),0.6) !important; }
|
||||
.np-time-display { font-variant-numeric: tabular-nums !important; }
|
||||
|
||||
/* ── Transport: dominant gradient play button ── */
|
||||
.np-controls-row { gap: 24px !important; }
|
||||
.np-btn {
|
||||
width: 46px !important; height: 46px !important; border-radius: 50% !important;
|
||||
color: rgba(255,255,255,0.62) !important; transition: all 0.16s ease !important;
|
||||
}
|
||||
.np-btn:hover { color: #fff !important; background: rgba(255,255,255,0.07) !important; transform: scale(1.08) !important; }
|
||||
.np-btn-play {
|
||||
width: 70px !important; height: 70px !important; border-radius: 50% !important;
|
||||
background: linear-gradient(180deg, rgb(var(--accent-light-rgb)), rgb(var(--accent-rgb))) !important;
|
||||
color: #06140b !important;
|
||||
box-shadow: 0 8px 30px rgba(var(--accent-rgb),0.5), inset 0 1px 0 rgba(255,255,255,0.35) !important;
|
||||
}
|
||||
.np-btn-play:hover {
|
||||
filter: none !important;
|
||||
transform: scale(1.06) !important;
|
||||
background: linear-gradient(180deg, rgb(var(--accent-light-rgb)), rgb(var(--accent-rgb))) !important;
|
||||
box-shadow: 0 12px 40px rgba(var(--accent-rgb),0.66), inset 0 1px 0 rgba(255,255,255,0.45) !important;
|
||||
}
|
||||
.np-btn-shuffle.active, .np-btn-repeat.active { color: rgb(var(--accent-light-rgb)) !important; }
|
||||
.np-btn-shuffle.active::after, .np-btn-repeat.active::after { background: rgb(var(--accent-light-rgb)) !important; }
|
||||
|
||||
/* ── Visualizer bars: accent gradient ── */
|
||||
.np-viz-bar {
|
||||
background: linear-gradient(180deg, rgb(var(--accent-light-rgb)), rgba(var(--accent-rgb),0.35)) !important;
|
||||
border-radius: 3px !important;
|
||||
}
|
||||
|
||||
/* ── Queue rows: rounded, hover, accent active row + accent title ── */
|
||||
.np-queue-item { border-radius: 12px !important; transition: background 0.14s !important; }
|
||||
.np-queue-item:hover { background: rgba(255,255,255,0.05) !important; }
|
||||
.np-queue-item.active { background: rgba(var(--accent-rgb),0.12) !important; }
|
||||
.np-queue-item.active .np-queue-item-title { color: rgb(var(--accent-light-rgb)) !important; }
|
||||
.np-radio-btn.active, .np-queue-toggle.active { color: rgb(var(--accent-light-rgb)) !important; }
|
||||
|
||||
/* ── Close button + lyrics active line ── */
|
||||
.np-close-btn:hover { color: #fff !important; background: rgba(255,255,255,0.1) !important; }
|
||||
.np-lyrics-line.active { color: rgb(var(--accent-light-rgb)) !important; font-weight: 700 !important; }
|
||||
|
||||
/* ── Click-art → music-synced visualizer takeover (Plexamp-style) ── */
|
||||
.np-album-art-container { cursor: pointer; }
|
||||
.np-art-hint {
|
||||
position: absolute; bottom: 12px; right: 12px; z-index: 4;
|
||||
width: 34px; height: 34px; border-radius: 50%;
|
||||
background: rgba(0,0,0,0.5); backdrop-filter: blur(8px);
|
||||
display: grid; place-items: center; color: #fff;
|
||||
opacity: 0; transition: opacity 0.2s ease; pointer-events: none;
|
||||
}
|
||||
.np-album-art-container:hover .np-art-hint { opacity: 1; }
|
||||
.np-album-art-container.viz-on .np-art-hint { opacity: 1; color: rgb(var(--accent-light-rgb)); }
|
||||
|
||||
.np-art-viz {
|
||||
position: absolute; inset: 0; z-index: 3;
|
||||
display: none; align-items: center; justify-content: center; gap: 5px;
|
||||
background: radial-gradient(circle at 50% 60%, rgba(var(--accent-rgb),0.20), rgba(0,0,0,0.88));
|
||||
}
|
||||
.np-album-art-container.viz-on .np-art-viz { display: flex; }
|
||||
.np-album-art-container.viz-on .np-album-art,
|
||||
.np-album-art-container.viz-on .np-album-art-placeholder { opacity: 0; transition: opacity 0.35s ease; }
|
||||
.np-art-viz-bar {
|
||||
width: 7px; border-radius: 5px; height: 12%;
|
||||
background: linear-gradient(180deg, rgb(var(--accent-light-rgb)), rgba(var(--accent-rgb),0.35));
|
||||
transform-origin: bottom; transition: height 0.08s linear;
|
||||
}
|
||||
|
||||
/* ── Sleep / Crossfade utility row (top-right of controls) ── */
|
||||
.np-util-row { display: flex; gap: 8px; justify-content: flex-end; margin-bottom: 4px; }
|
||||
.np-util-btn {
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
font-size: 11px; font-weight: 700; padding: 6px 11px; border-radius: 999px;
|
||||
background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08);
|
||||
color: rgba(255,255,255,0.4); cursor: pointer; transition: all 0.16s ease;
|
||||
}
|
||||
.np-util-btn:hover { color: rgba(255,255,255,0.7); border-color: rgba(255,255,255,0.18); }
|
||||
.np-util-btn.active {
|
||||
color: rgb(var(--accent-light-rgb));
|
||||
background: rgba(var(--accent-rgb),0.12);
|
||||
border-color: rgba(var(--accent-rgb),0.32);
|
||||
}
|
||||
|
||||
/* ── Up-next peek ── */
|
||||
.np-upnext {
|
||||
display: flex; align-items: center; gap: 11px;
|
||||
padding: 10px 12px; border-radius: 14px;
|
||||
background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08);
|
||||
}
|
||||
.np-upnext.hidden { display: none; }
|
||||
.np-upnext-label {
|
||||
font-size: 10px; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase;
|
||||
color: rgba(255,255,255,0.38); flex-shrink: 0;
|
||||
}
|
||||
.np-upnext-art { width: 38px; height: 38px; border-radius: 8px; object-fit: cover; flex-shrink: 0; background: rgba(255,255,255,0.05); }
|
||||
.np-upnext-info { min-width: 0; }
|
||||
.np-upnext-title { font-size: 13px; font-weight: 600; color: #fff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.np-upnext-artist { font-size: 11.5px; color: rgba(255,255,255,0.4); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
|
||||
/* ── Rich queue rows: thumbnail + meta + duration/eq + remove ── */
|
||||
.np-queue-item {
|
||||
display: grid !important;
|
||||
grid-template-columns: 44px 1fr auto auto !important;
|
||||
align-items: center !important; gap: 13px !important;
|
||||
padding: 8px 10px !important;
|
||||
}
|
||||
.np-queue-item-art { width: 44px; height: 44px; border-radius: 8px; object-fit: cover; background: rgba(255,255,255,0.05); }
|
||||
.np-queue-item-info { min-width: 0; }
|
||||
.np-queue-item-title { font-size: 14px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.np-queue-item-artist { font-size: 12px; color: rgba(255,255,255,0.4); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.np-queue-item-duration { font-size: 12px; font-variant-numeric: tabular-nums; color: rgba(255,255,255,0.38); }
|
||||
.np-queue-item-eq { display: inline-flex; align-items: flex-end; gap: 2px; height: 14px; }
|
||||
.np-queue-item-eq i { width: 3px; border-radius: 2px; background: rgb(var(--accent-light-rgb)); animation: npQueueEq 0.9s ease-in-out infinite; }
|
||||
.np-queue-item-eq i:nth-child(2) { animation-delay: 0.2s; }
|
||||
.np-queue-item-eq i:nth-child(3) { animation-delay: 0.4s; }
|
||||
@keyframes npQueueEq { 0%,100% { height: 30%; } 50% { height: 100%; } }
|
||||
.np-queue-item-remove { opacity: 0; transition: opacity 0.14s, color 0.14s; }
|
||||
.np-queue-item:hover .np-queue-item-remove { opacity: 1; }
|
||||
|
||||
/* Queue button in enhanced track table */
|
||||
.col-queue {
|
||||
width: 36px;
|
||||
|
|
|
|||
Loading…
Reference in a new issue