Wishlist Nebula — artist orb visualization replacing category cards
Bespoke wishlist page design: each artist is a glowing orb sized by track count (sm/md/lg), with a spinning conic gradient ring colored by artist name hash. Click to expand — albums appear as satellite rows with cover art, singles as compact pills. Remove buttons on hover at every level (album, single track). Search bar filters orbs in real-time. Download Albums/Singles buttons trigger the existing category download flow. Orbs sorted by track count (biggest artists first). Responsive layout with mobile breakpoints.
This commit is contained in:
parent
cd157fc692
commit
dd26437125
3 changed files with 238 additions and 97 deletions
|
|
@ -6772,32 +6772,32 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Category grid -->
|
||||
<div class="wishlist-category-grid" id="wishlist-page-categories">
|
||||
<div class="wishlist-category-card" data-category="albums" onclick="selectWishlistCategory('albums')">
|
||||
<div class="wishlist-mosaic-bg" id="wishlist-page-albums-mosaic"></div>
|
||||
<div class="wishlist-category-content">
|
||||
<div class="wishlist-category-icon">💿</div>
|
||||
<div class="wishlist-category-title">Albums / EPs</div>
|
||||
<div class="wishlist-category-count" id="wishlist-page-albums-count">0 tracks</div>
|
||||
<div class="wishlist-category-badge" id="wishlist-page-albums-badge" style="display: none;">Next in Queue</div>
|
||||
<!-- Nebula view — artist orbs with album satellites -->
|
||||
<div class="wl-nebula" id="wishlist-nebula">
|
||||
<!-- Action bar -->
|
||||
<div class="wl-nebula-bar">
|
||||
<input type="text" class="wl-nebula-search" id="wl-nebula-search" placeholder="Search artists, albums..." oninput="_filterNebula()">
|
||||
<div class="wl-nebula-bar-actions">
|
||||
<button class="watchlist-action-btn watchlist-action-primary" onclick="selectWishlistCategory('albums')">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||
Albums
|
||||
</button>
|
||||
<button class="watchlist-action-btn watchlist-action-secondary" onclick="selectWishlistCategory('singles')">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||
Singles
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wishlist-category-card" data-category="singles" onclick="selectWishlistCategory('singles')">
|
||||
<div class="wishlist-mosaic-bg" id="wishlist-page-singles-mosaic"></div>
|
||||
<div class="wishlist-category-content">
|
||||
<div class="wishlist-category-icon">🎵</div>
|
||||
<div class="wishlist-category-title">Singles</div>
|
||||
<div class="wishlist-category-count" id="wishlist-page-singles-count">0 tracks</div>
|
||||
<div class="wishlist-category-badge" id="wishlist-page-singles-badge" style="display: none;">Next in Queue</div>
|
||||
</div>
|
||||
<!-- Artist orbs container -->
|
||||
<div class="wl-nebula-field" id="wl-nebula-field">
|
||||
<!-- Populated by JS: artist orbs with album fans -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Track list (hidden until category selected) -->
|
||||
<!-- Legacy track list (used by selectWishlistCategory for download flow) -->
|
||||
<div id="wishlist-category-tracks" class="wishlist-category-tracks" style="display: none;">
|
||||
<div class="wishlist-category-header">
|
||||
<button class="wishlist-back-btn" onclick="backToCategories()">← Back</button>
|
||||
<button class="wishlist-back-btn" onclick="_nebulaBack()">← Back</button>
|
||||
<span id="wishlist-category-name" class="wishlist-category-name"></span>
|
||||
<div class="wishlist-category-header-right">
|
||||
<button class="wishlist-select-all-btn" id="wishlist-select-all-btn" onclick="toggleWishlistSelectAll()">Select All</button>
|
||||
|
|
@ -6806,10 +6806,6 @@
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wishlist-track-search-container">
|
||||
<svg class="wishlist-track-search-icon" width="14" height="14" viewBox="0 0 24 24" fill="rgba(255,255,255,0.35)"><path d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0016 9.5 6.5 6.5 0 109.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
|
||||
<input type="text" id="wishlist-track-search-input" class="wishlist-track-search-input" placeholder="Filter tracks..." oninput="filterWishlistTracks()">
|
||||
</div>
|
||||
<div class="wishlist-batch-bar" id="wishlist-batch-bar" style="display: none;">
|
||||
<span class="wishlist-batch-count" id="wishlist-batch-count">0 selected</span>
|
||||
<button class="watchlist-action-btn watchlist-action-secondary wishlist-batch-remove-btn" onclick="batchRemoveFromWishlist()">
|
||||
|
|
|
|||
|
|
@ -13851,11 +13851,11 @@ async function selectWishlistCategory(category) {
|
|||
|
||||
const tracksList = document.getElementById('wishlist-tracks-list');
|
||||
const categoryTracksSection = document.getElementById('wishlist-category-tracks');
|
||||
const categoryGrid = document.querySelector('.wishlist-category-grid');
|
||||
const nebulaEl = document.getElementById('wishlist-nebula');
|
||||
const downloadBtn = document.getElementById('wishlist-download-btn');
|
||||
const categoryName = document.getElementById('wishlist-category-name');
|
||||
|
||||
categoryGrid.style.display = 'none';
|
||||
if (nebulaEl) nebulaEl.style.display = 'none';
|
||||
categoryTracksSection.style.display = 'block';
|
||||
downloadBtn.style.display = 'inline-block';
|
||||
categoryName.textContent = category === 'albums' ? 'Albums / EPs' : 'Singles';
|
||||
|
|
@ -14131,18 +14131,7 @@ function _attachWishlistDelegation(container) {
|
|||
}
|
||||
|
||||
function backToCategories() {
|
||||
const categoryTracksSection = document.getElementById('wishlist-category-tracks');
|
||||
const categoryGrid = document.querySelector('.wishlist-category-grid');
|
||||
const downloadBtn = document.getElementById('wishlist-download-btn');
|
||||
const batchBar = document.getElementById('wishlist-batch-bar');
|
||||
const trackSearch = document.getElementById('wishlist-track-search-input');
|
||||
|
||||
categoryTracksSection.style.display = 'none';
|
||||
categoryGrid.style.display = 'grid';
|
||||
downloadBtn.style.display = 'none';
|
||||
if (batchBar) batchBar.style.display = 'none';
|
||||
if (trackSearch) trackSearch.value = '';
|
||||
window.selectedWishlistCategory = null;
|
||||
_nebulaBack();
|
||||
}
|
||||
|
||||
function toggleAlbumTracks(albumId) {
|
||||
|
|
@ -40496,94 +40485,47 @@ async function initializeWatchlistPage() {
|
|||
async function initializeWishlistPage() {
|
||||
try {
|
||||
const emptyEl = document.getElementById('wishlist-page-empty');
|
||||
const categoriesEl = document.getElementById('wishlist-page-categories');
|
||||
const nebulaEl = document.getElementById('wishlist-nebula');
|
||||
const countEl = document.getElementById('wishlist-page-count');
|
||||
const tracksSection = document.getElementById('wishlist-category-tracks');
|
||||
const statsStrip = document.getElementById('wishlist-stats-strip');
|
||||
|
||||
// Fetch stats and cycle
|
||||
const [statsRes, cycleRes] = await Promise.all([
|
||||
const [statsRes, cycleRes, albumRes, singleRes] = await Promise.all([
|
||||
fetch('/api/wishlist/stats').then(r => r.json()),
|
||||
fetch('/api/wishlist/cycle').then(r => r.json()),
|
||||
fetch('/api/wishlist/tracks?category=albums').then(r => r.json()),
|
||||
fetch('/api/wishlist/tracks?category=singles').then(r => r.json()),
|
||||
]);
|
||||
|
||||
const { singles = 0, albums = 0, total = 0 } = statsRes;
|
||||
const currentCycle = cycleRes.cycle || 'albums';
|
||||
|
||||
// Update count
|
||||
if (countEl) countEl.textContent = `${total} track${total !== 1 ? 's' : ''}`;
|
||||
|
||||
// Update nav badge
|
||||
const navBadge = document.getElementById('wishlist-nav-badge');
|
||||
if (navBadge) {
|
||||
navBadge.textContent = total;
|
||||
navBadge.classList.toggle('hidden', total === 0);
|
||||
}
|
||||
if (navBadge) { navBadge.textContent = total; navBadge.classList.toggle('hidden', total === 0); }
|
||||
|
||||
// Stats strip
|
||||
const statAlbums = document.getElementById('wishlist-stat-albums');
|
||||
const statSingles = document.getElementById('wishlist-stat-singles');
|
||||
const statCycle = document.getElementById('wishlist-stat-cycle');
|
||||
const statsStrip = document.getElementById('wishlist-stats-strip');
|
||||
if (statAlbums) statAlbums.textContent = albums;
|
||||
if (statSingles) statSingles.textContent = singles;
|
||||
if (statCycle) statCycle.textContent = currentCycle === 'albums' ? 'Albums/EPs' : 'Singles';
|
||||
|
||||
// Empty state
|
||||
if (total === 0) {
|
||||
if (emptyEl) emptyEl.style.display = '';
|
||||
if (categoriesEl) categoriesEl.style.display = 'none';
|
||||
if (nebulaEl) nebulaEl.style.display = 'none';
|
||||
if (tracksSection) tracksSection.style.display = 'none';
|
||||
if (statsStrip) statsStrip.style.display = 'none';
|
||||
wishlistPageState.isInitialized = true;
|
||||
return;
|
||||
}
|
||||
if (emptyEl) emptyEl.style.display = 'none';
|
||||
if (categoriesEl) categoriesEl.style.display = '';
|
||||
if (nebulaEl) nebulaEl.style.display = '';
|
||||
if (tracksSection) tracksSection.style.display = 'none';
|
||||
if (statsStrip) statsStrip.style.display = '';
|
||||
|
||||
// Reset to category view
|
||||
if (tracksSection) tracksSection.style.display = 'none';
|
||||
if (categoriesEl) categoriesEl.style.display = '';
|
||||
|
||||
// Update category cards
|
||||
const albumsCountEl = document.getElementById('wishlist-page-albums-count');
|
||||
const singlesCountEl = document.getElementById('wishlist-page-singles-count');
|
||||
const albumsBadge = document.getElementById('wishlist-page-albums-badge');
|
||||
const singlesBadge = document.getElementById('wishlist-page-singles-badge');
|
||||
|
||||
if (albumsCountEl) albumsCountEl.textContent = `${albums} tracks`;
|
||||
if (singlesCountEl) singlesCountEl.textContent = `${singles} tracks`;
|
||||
if (albumsBadge) albumsBadge.style.display = currentCycle === 'albums' ? '' : 'none';
|
||||
if (singlesBadge) singlesBadge.style.display = currentCycle === 'singles' ? '' : 'none';
|
||||
|
||||
// Add/remove next-in-queue class
|
||||
const albumCard = categoriesEl.querySelector('[data-category="albums"]');
|
||||
const singleCard = categoriesEl.querySelector('[data-category="singles"]');
|
||||
if (albumCard) albumCard.classList.toggle('next-in-queue', currentCycle === 'albums');
|
||||
if (singleCard) singleCard.classList.toggle('next-in-queue', currentCycle === 'singles');
|
||||
|
||||
// Load mosaic covers
|
||||
try {
|
||||
const [albumTracksData, singleTracksData] = await Promise.all([
|
||||
fetch('/api/wishlist/tracks?category=albums&limit=50').then(r => r.json()),
|
||||
fetch('/api/wishlist/tracks?category=singles&limit=50').then(r => r.json()),
|
||||
]);
|
||||
const albumCovers = extractUniqueCoverImages(albumTracksData.tracks || [], 20);
|
||||
const singleCovers = extractUniqueCoverImages(singleTracksData.tracks || [], 20);
|
||||
|
||||
const albumsMosaic = document.getElementById('wishlist-page-albums-mosaic');
|
||||
const singlesMosaic = document.getElementById('wishlist-page-singles-mosaic');
|
||||
if (albumsMosaic) albumsMosaic.innerHTML = generateMosaicBackground(albumCovers);
|
||||
if (singlesMosaic) singlesMosaic.innerHTML = generateMosaicBackground(singleCovers);
|
||||
} catch (e) {
|
||||
console.debug('Could not load mosaic covers:', e);
|
||||
}
|
||||
|
||||
// Start countdown timer
|
||||
const nextRunSeconds = statsRes.next_run_in_seconds || 0;
|
||||
const nextCycleText = currentCycle === 'albums' ? 'Albums/EPs' : 'Singles';
|
||||
startWishlistCountdownTimer(currentCycle, nextRunSeconds);
|
||||
|
||||
_renderWishlistNebula(albumRes.tracks || [], singleRes.tracks || []);
|
||||
startWishlistCountdownTimer(currentCycle, statsRes.next_run_in_seconds || 0);
|
||||
wishlistPageState.isInitialized = true;
|
||||
|
||||
} catch (error) {
|
||||
|
|
@ -40592,6 +40534,134 @@ async function initializeWishlistPage() {
|
|||
}
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
WISHLIST NEBULA — Artist orbs with album/single satellites
|
||||
═══════════════════════════════════════════════════════════════════ */
|
||||
|
||||
function _renderWishlistNebula(albumTracks, singleTracks) {
|
||||
const field = document.getElementById('wl-nebula-field');
|
||||
if (!field) return;
|
||||
|
||||
const artistMap = new Map();
|
||||
function _parse(track, type) {
|
||||
let sd = track.spotify_data;
|
||||
if (typeof sd === 'string') { try { sd = JSON.parse(sd); } catch (e) { return null; } }
|
||||
if (!sd) return null;
|
||||
const raw = sd.album;
|
||||
const albumName = (typeof raw === 'string' ? raw : raw?.name) || 'Unknown';
|
||||
const albumImage = (typeof raw === 'object' && raw?.images?.[0]?.url) || '';
|
||||
let artist = 'Unknown Artist';
|
||||
if (sd.artists?.[0]?.name) artist = sd.artists[0].name;
|
||||
else if (typeof sd.artists?.[0] === 'string') artist = sd.artists[0];
|
||||
return { track: sd.name || 'Unknown', artist, album: albumName, image: albumImage, type, id: track.spotify_track_id || track.id || '' };
|
||||
}
|
||||
|
||||
for (const t of albumTracks) { const p = _parse(t, 'album'); if (p) { if (!artistMap.has(p.artist)) artistMap.set(p.artist, { albums: new Map(), singles: [] }); const a = artistMap.get(p.artist); if (!a.albums.has(p.album)) a.albums.set(p.album, { image: p.image, tracks: [] }); a.albums.get(p.album).tracks.push(p); } }
|
||||
for (const t of singleTracks) { const p = _parse(t, 'single'); if (p) { if (!artistMap.has(p.artist)) artistMap.set(p.artist, { albums: new Map(), singles: [] }); artistMap.get(p.artist).singles.push(p); } }
|
||||
|
||||
if (artistMap.size === 0) { field.innerHTML = '<div class="wl-nebula-empty">Your wishlist is empty</div>'; return; }
|
||||
|
||||
const sorted = [...artistMap.entries()].sort((a, b) => {
|
||||
const ac = [...a[1].albums.values()].reduce((s, al) => s + al.tracks.length, 0) + a[1].singles.length;
|
||||
const bc = [...b[1].albums.values()].reduce((s, al) => s + al.tracks.length, 0) + b[1].singles.length;
|
||||
return bc - ac;
|
||||
});
|
||||
|
||||
function _hue(n) { let h = 0; for (let i = 0; i < n.length; i++) h = n.charCodeAt(i) + ((h << 5) - h); return Math.abs(h) % 360; }
|
||||
|
||||
let html = '';
|
||||
for (const [name, data] of sorted) {
|
||||
const total = [...data.albums.values()].reduce((s, a) => s + a.tracks.length, 0) + data.singles.length;
|
||||
const hue = _hue(name);
|
||||
const sz = total >= 10 ? 'orb-lg' : total >= 4 ? 'orb-md' : 'orb-sm';
|
||||
let img = '';
|
||||
for (const [, ad] of data.albums) { if (ad.image) { img = ad.image; break; } }
|
||||
if (!img && data.singles.length) img = data.singles[0].image || '';
|
||||
|
||||
html += `<div class="wl-orb-group" data-artist="${escapeHtml(name)}">`;
|
||||
html += `<div class="wl-orb ${sz}" style="--orb-hue:${hue}" onclick="_toggleOrbExpand(this)">`;
|
||||
html += `<div class="wl-orb-glow"></div>`;
|
||||
html += img ? `<img class="wl-orb-img" src="${img}" alt="" loading="lazy">` : `<div class="wl-orb-initials">${escapeHtml(name.substring(0, 2).toUpperCase())}</div>`;
|
||||
html += `<div class="wl-orb-ring"></div>`;
|
||||
html += `</div>`;
|
||||
html += `<div class="wl-orb-label">${escapeHtml(name)}</div>`;
|
||||
html += `<div class="wl-orb-meta">${total} track${total !== 1 ? 's' : ''}</div>`;
|
||||
|
||||
html += `<div class="wl-orb-expanded">`;
|
||||
if (data.albums.size > 0) {
|
||||
html += `<div class="wl-orb-albums">`;
|
||||
for (const [an, ad] of data.albums) {
|
||||
html += `<div class="wl-satellite" data-album="${escapeHtml(an)}">`;
|
||||
html += `<div class="wl-satellite-art">${ad.image ? `<img src="${ad.image}" alt="">` : ''}</div>`;
|
||||
html += `<div class="wl-satellite-info"><div class="wl-satellite-name">${escapeHtml(an)}</div><div class="wl-satellite-count">${ad.tracks.length} tracks</div></div>`;
|
||||
html += `<button class="wl-satellite-remove" onclick="event.stopPropagation();_removeWishlistAlbum('${escapeHtml(an)}')" title="Remove">✕</button>`;
|
||||
html += `</div>`;
|
||||
}
|
||||
html += `</div>`;
|
||||
}
|
||||
if (data.singles.length > 0) {
|
||||
html += `<div class="wl-orb-singles">`;
|
||||
for (const s of data.singles) {
|
||||
html += `<div class="wl-single-pill" data-track-id="${escapeHtml(s.id)}">`;
|
||||
if (s.image) html += `<img class="wl-pill-art" src="${s.image}" alt="">`;
|
||||
html += `<span class="wl-pill-name">${escapeHtml(s.track)}</span>`;
|
||||
html += `<button class="wl-pill-remove" onclick="event.stopPropagation();_removeWishlistTrack('${escapeHtml(s.id)}')" title="Remove">✕</button>`;
|
||||
html += `</div>`;
|
||||
}
|
||||
html += `</div>`;
|
||||
}
|
||||
html += `</div></div>`;
|
||||
}
|
||||
field.innerHTML = html;
|
||||
}
|
||||
|
||||
function _toggleOrbExpand(el) {
|
||||
const g = el.closest('.wl-orb-group');
|
||||
if (!g) return;
|
||||
const was = g.classList.contains('expanded');
|
||||
document.querySelectorAll('.wl-orb-group.expanded').forEach(o => o.classList.remove('expanded'));
|
||||
if (!was) g.classList.add('expanded');
|
||||
}
|
||||
|
||||
async function _removeWishlistAlbum(albumName) {
|
||||
if (!await showConfirmDialog({ title: 'Remove Album', message: `Remove all tracks from "${albumName}"?`, confirmText: 'Remove', destructive: true })) return;
|
||||
try {
|
||||
const res = await fetch('/api/wishlist/remove-album', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ album_name: albumName }) });
|
||||
const data = await res.json();
|
||||
if (data.success) { showToast(`Removed "${albumName}"`, 'success'); wishlistPageState.isInitialized = false; await initializeWishlistPage(); await updateWishlistCount(); }
|
||||
else showToast(data.error || 'Failed', 'error');
|
||||
} catch (err) { showToast('Error: ' + err.message, 'error'); }
|
||||
}
|
||||
|
||||
async function _removeWishlistTrack(trackId) {
|
||||
try {
|
||||
const res = await fetch('/api/wishlist/remove-track', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ spotify_track_id: trackId }) });
|
||||
const data = await res.json();
|
||||
if (data.success) { const p = document.querySelector(`.wl-single-pill[data-track-id="${trackId}"]`); if (p) p.remove(); showToast('Removed', 'success'); await updateWishlistCount(); }
|
||||
} catch (err) { showToast('Error: ' + err.message, 'error'); }
|
||||
}
|
||||
|
||||
function _filterNebula() {
|
||||
const q = (document.getElementById('wl-nebula-search')?.value || '').toLowerCase().trim();
|
||||
document.querySelectorAll('.wl-orb-group').forEach(g => {
|
||||
const a = (g.dataset.artist || '').toLowerCase();
|
||||
const albums = [...g.querySelectorAll('.wl-satellite')].map(s => (s.dataset.album || '').toLowerCase());
|
||||
const match = !q || a.includes(q) || albums.some(al => al.includes(q));
|
||||
g.style.display = match ? '' : 'none';
|
||||
if (!match) g.classList.remove('expanded');
|
||||
});
|
||||
}
|
||||
|
||||
function _nebulaBack() {
|
||||
const t = document.getElementById('wishlist-category-tracks');
|
||||
const n = document.getElementById('wishlist-nebula');
|
||||
if (t) t.style.display = 'none';
|
||||
if (n) n.style.display = '';
|
||||
window.selectedWishlistCategory = null;
|
||||
wishlistPageState.isInitialized = false;
|
||||
initializeWishlistPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort the watchlist artist grid by the selected criteria.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -57217,8 +57217,83 @@ body.reduce-effects *::after {
|
|||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
/* ── Category cards upgrade ── */
|
||||
#wishlist-page-categories {
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
WISHLIST NEBULA
|
||||
═══════════════════════════════════════════════════════════════════ */
|
||||
.wl-nebula { position: relative; }
|
||||
.wl-nebula-bar { display: flex; align-items: center; gap: 12px; margin-bottom: 20px; flex-wrap: wrap; }
|
||||
.wl-nebula-search { flex: 1; min-width: 200px; padding: 10px 16px; border: 1px solid rgba(255,255,255,0.08); border-radius: 10px; background: rgba(255,255,255,0.03); color: #fff; font-size: 13px; transition: all 0.2s; box-sizing: border-box; }
|
||||
.wl-nebula-search:focus { border-color: rgba(var(--accent-rgb),0.4); background: rgba(255,255,255,0.05); box-shadow: 0 0 12px rgba(var(--accent-rgb),0.1); outline: none; }
|
||||
.wl-nebula-search::placeholder { color: rgba(255,255,255,0.25); }
|
||||
.wl-nebula-bar-actions { display: flex; gap: 8px; }
|
||||
|
||||
.wl-nebula-field { display: flex; flex-wrap: wrap; gap: 20px; justify-content: center; padding: 10px 0 30px; }
|
||||
.wl-nebula-empty { width: 100%; text-align: center; padding: 60px 20px; color: rgba(255,255,255,0.2); font-size: 14px; }
|
||||
|
||||
/* ── Orb group ── */
|
||||
.wl-orb-group { display: flex; flex-direction: column; align-items: center; gap: 6px; transition: all 0.4s cubic-bezier(0.4,0,0.2,1); position: relative; }
|
||||
.wl-orb-group.expanded { background: rgba(255,255,255,0.02); border-radius: 16px; padding: 16px; box-shadow: 0 0 30px rgba(0,0,0,0.3); z-index: 5; flex-basis: 100%; max-width: 600px; }
|
||||
|
||||
/* ── Orb ── */
|
||||
.wl-orb { position: relative; border-radius: 50%; cursor: pointer; overflow: hidden; transition: all 0.35s cubic-bezier(0.4,0,0.2,1); }
|
||||
.wl-orb.orb-sm { width: 70px; height: 70px; }
|
||||
.wl-orb.orb-md { width: 90px; height: 90px; }
|
||||
.wl-orb.orb-lg { width: 110px; height: 110px; }
|
||||
.wl-orb:hover { transform: scale(1.1); }
|
||||
.wl-orb-group.expanded .wl-orb { transform: scale(1.05); }
|
||||
|
||||
.wl-orb-glow { position: absolute; inset: -4px; border-radius: 50%; background: conic-gradient(from 0deg, hsla(var(--orb-hue),70%,55%,0.4), hsla(calc(var(--orb-hue)+60),70%,55%,0.2), hsla(var(--orb-hue),70%,55%,0.4)); animation: orbGlowSpin 8s linear infinite; z-index: 0; filter: blur(3px); }
|
||||
@keyframes orbGlowSpin { to { transform: rotate(360deg); } }
|
||||
|
||||
.wl-orb-img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; position: relative; z-index: 1; }
|
||||
.wl-orb-initials { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; font-size: 22px; font-weight: 800; color: hsla(var(--orb-hue),70%,75%,0.9); background: hsla(var(--orb-hue),40%,15%,0.9); border-radius: 50%; position: relative; z-index: 1; }
|
||||
.wl-orb-ring { position: absolute; inset: -2px; border-radius: 50%; border: 2px solid hsla(var(--orb-hue),60%,50%,0.3); z-index: 2; pointer-events: none; }
|
||||
.wl-orb-group.expanded .wl-orb-ring { border-color: hsla(var(--orb-hue),70%,60%,0.6); box-shadow: 0 0 16px hsla(var(--orb-hue),70%,50%,0.3); }
|
||||
|
||||
.wl-orb-label { font-size: 11px; font-weight: 600; color: rgba(255,255,255,0.7); text-align: center; max-width: 120px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.wl-orb-meta { font-size: 9px; color: rgba(255,255,255,0.3); text-align: center; }
|
||||
|
||||
/* ── Expanded ── */
|
||||
.wl-orb-expanded { display: none; width: 100%; margin-top: 10px; }
|
||||
.wl-orb-group.expanded .wl-orb-expanded { display: block; animation: expandFadeIn 0.3s ease; }
|
||||
@keyframes expandFadeIn { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: translateY(0); } }
|
||||
|
||||
/* ── Satellites (albums) ── */
|
||||
.wl-orb-albums { display: flex; flex-direction: column; gap: 6px; margin-bottom: 10px; }
|
||||
.wl-satellite { display: flex; align-items: center; gap: 10px; padding: 8px 10px; background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.06); border-radius: 10px; transition: all 0.15s; }
|
||||
.wl-satellite:hover { background: rgba(255,255,255,0.06); border-color: rgba(255,255,255,0.1); }
|
||||
.wl-satellite-art { width: 44px; height: 44px; border-radius: 6px; overflow: hidden; flex-shrink: 0; background: rgba(255,255,255,0.05); }
|
||||
.wl-satellite-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||
.wl-satellite-info { flex: 1; min-width: 0; }
|
||||
.wl-satellite-name { font-size: 12px; font-weight: 600; color: #fff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.wl-satellite-count { font-size: 10px; color: rgba(255,255,255,0.35); }
|
||||
.wl-satellite-remove { width: 22px; height: 22px; border-radius: 50%; border: none; background: transparent; color: rgba(255,255,255,0.2); font-size: 10px; cursor: pointer; display: flex; align-items: center; justify-content: center; opacity: 0; transition: all 0.15s; flex-shrink: 0; }
|
||||
.wl-satellite:hover .wl-satellite-remove { opacity: 1; }
|
||||
.wl-satellite-remove:hover { background: rgba(239,68,68,0.15); color: #f87171; }
|
||||
|
||||
/* ── Single pills ── */
|
||||
.wl-orb-singles { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.wl-single-pill { display: flex; align-items: center; gap: 6px; padding: 4px 10px 4px 4px; background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.06); border-radius: 20px; font-size: 11px; color: rgba(255,255,255,0.6); transition: all 0.15s; }
|
||||
.wl-single-pill:hover { background: rgba(255,255,255,0.06); border-color: rgba(255,255,255,0.1); color: #fff; }
|
||||
.wl-pill-art { width: 22px; height: 22px; border-radius: 50%; object-fit: cover; flex-shrink: 0; }
|
||||
.wl-pill-name { max-width: 140px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.wl-pill-remove { width: 16px; height: 16px; border-radius: 50%; border: none; background: transparent; color: rgba(255,255,255,0.2); font-size: 9px; cursor: pointer; display: flex; align-items: center; justify-content: center; opacity: 0; transition: all 0.15s; flex-shrink: 0; }
|
||||
.wl-single-pill:hover .wl-pill-remove { opacity: 1; }
|
||||
.wl-pill-remove:hover { background: rgba(239,68,68,0.15); color: #f87171; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.wl-nebula-bar { flex-direction: column; }
|
||||
.wl-nebula-bar-actions { width: 100%; }
|
||||
.wl-nebula-bar-actions button { flex: 1; }
|
||||
.wl-orb.orb-lg { width: 80px; height: 80px; }
|
||||
.wl-orb.orb-md { width: 70px; height: 70px; }
|
||||
.wl-orb.orb-sm { width: 60px; height: 60px; }
|
||||
.wl-nebula-field { gap: 14px; }
|
||||
.wl-orb-group.expanded { max-width: 100%; }
|
||||
}
|
||||
|
||||
/* ── Legacy (hidden) ── */
|
||||
#wishlist-page-categories { display: none;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue