Watchlist page: hued action chips, meta chips, Global Settings reskin (#831 round 3)

Boulder: the cards are good but everything around them was basic — six
identical grey pill buttons, a plain header, and a dated Global Settings modal.

Action chips (artist-detail button language — tinted gradient + hover lift +
icon scale): Scan is the primary CTA with the accent gradient and a shimmer
sweep; the rest get per-hue identity (similar-artists blue, settings slate,
origins green, history amber, blocklist/cancel red). One .wl-chip base class
with a --chip-rgb variable per hue. Header count/timer become pill meta chips
(timer accent-tinted).

Chip-safe labels: the scan/update handlers set button.textContent, which would
wipe the new svg + shimmer children on first use — added _wlSetChipLabel()
(preserves icon/shimmer, swaps the text node) and converted all 11 writes.

Global Settings modal: emoji + inline-styled header replaced with the
origins/blocklist house-style head (title/sub/✕); option cards now show live
checked-state feedback (:has(:checked) accent ring + grayscale-dimmed icons
when off — also upgrades the per-artist config modal, same components); the
master-override toggle gets a CSS .enabled treatment instead of the hard-coded
green inline border the JS used to write.

All element ids/onclicks unchanged; JS syntax-checked; 131 watchlist tests pass.
This commit is contained in:
BoulderBadgeDad 2026-06-09 21:22:02 -07:00
parent 34e0503fad
commit 111af5150e
3 changed files with 222 additions and 39 deletions

View file

@ -6967,8 +6967,8 @@
Watchlist
</h2>
<div class="watchlist-page-meta">
<span class="watchlist-page-count" id="watchlist-page-count">0 artists</span>
<span class="watchlist-page-timer" id="watchlist-next-auto-timer">Next Auto: --</span>
<span class="wl-meta-chip" id="watchlist-page-count">0 artists</span>
<span class="wl-meta-chip wl-meta-chip--accent" id="watchlist-next-auto-timer">Next Auto: --</span>
</div>
</div>
</div>
@ -7021,31 +7021,32 @@
<!-- Action buttons -->
<div class="watchlist-page-actions">
<button class="btn btn--primary" id="scan-watchlist-btn" onclick="startWatchlistScan()">
<button class="wl-chip wl-chip--cta" id="scan-watchlist-btn" onclick="startWatchlistScan()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
Scan for New Releases
<span class="wl-chip-shimmer"></span>
</button>
<button class="btn btn--secondary" id="cancel-watchlist-scan-btn" onclick="cancelWatchlistScan()" style="display: none;">
<button class="wl-chip wl-chip--red" id="cancel-watchlist-scan-btn" onclick="cancelWatchlistScan()" style="display: none;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>
Cancel Scan
</button>
<button class="btn btn--secondary" id="update-similar-artists-btn" onclick="updateSimilarArtists()">
<button class="wl-chip wl-chip--blue" id="update-similar-artists-btn" onclick="updateSimilarArtists()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
Update Similar Artists
</button>
<button class="btn btn--secondary" id="watchlist-page-settings-btn" onclick="openWatchlistGlobalSettingsModal()">
<button class="wl-chip wl-chip--slate" id="watchlist-page-settings-btn" onclick="openWatchlistGlobalSettingsModal()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
Global Settings
</button>
<button class="btn btn--secondary" id="watchlist-page-origins-btn" onclick="openDownloadOriginsModal('watchlist')" title="See every track your watchlist downloaded">
<button class="wl-chip wl-chip--green" id="watchlist-page-origins-btn" onclick="openDownloadOriginsModal('watchlist')" title="See every track your watchlist downloaded">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>
Download Origins
</button>
<button class="btn btn--secondary" id="watchlist-page-history-btn" onclick="openWatchlistHistoryModal()" title="Every past scan and the tracks it added to the wishlist">
<button class="wl-chip wl-chip--amber" id="watchlist-page-history-btn" onclick="openWatchlistHistoryModal()" title="Every past scan and the tracks it added to the wishlist">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v5h5"/><path d="M3.05 13A9 9 0 1 0 6 5.3L3 8"/><polyline points="12 7 12 12 15 15"/></svg>
History
</button>
<button class="btn btn--secondary" id="watchlist-page-blocklist-btn" onclick="openBlocklistModal('artist')" title="Block artists, albums or tracks from ever being downloaded">
<button class="wl-chip wl-chip--red" id="watchlist-page-blocklist-btn" onclick="openBlocklistModal('artist')" title="Block artists, albums or tracks from ever being downloaded">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="4.9" y1="4.9" x2="19.1" y2="19.1"/></svg>
Blocklist
</button>
@ -7727,18 +7728,13 @@
<!-- Watchlist Global Config Modal -->
<div class="modal-overlay hidden" id="watchlist-global-config-modal-overlay">
<div class="watchlist-artist-config-modal" id="watchlist-global-config-modal">
<div class="watchlist-artist-config-header" style="padding: 24px 28px;">
<div style="display: flex; align-items: center; gap: 12px;">
<span style="font-size: 28px;">⚙️</span>
<div>
<h2 style="color: #fff; margin: 0; font-size: 22px;">Global Watchlist Settings</h2>
<p style="color: #b3b3b3; margin: 4px 0 0; font-size: 13px;">
Override per-artist settings for all watchlist scans
</p>
</div>
<div class="watchlist-artist-config-modal wl-global-modal" id="watchlist-global-config-modal">
<div class="wl-global-modal-head">
<div>
<h2 class="wl-global-modal-title">Global Watchlist Settings</h2>
<p class="wl-global-modal-sub">Override per-artist settings for all watchlist scans.</p>
</div>
<span class="watchlist-artist-config-close" onclick="closeWatchlistGlobalSettingsModal()">&times;</span>
<button class="wl-global-modal-close" onclick="closeWatchlistGlobalSettingsModal()" aria-label="Close"></button>
</div>
<div class="watchlist-artist-config-content">

View file

@ -2830,12 +2830,10 @@ async function openWatchlistGlobalSettingsModal() {
// Update options visibility based on toggle state
toggleGlobalOverrideOptions();
// Update toggle label border
// Reflect enabled state on the toggle card (styled in CSS)
const toggleLabel = document.getElementById('global-override-toggle-label');
if (toggleLabel) {
toggleLabel.style.border = config.global_override_enabled
? '2px solid rgba(29, 185, 84, 0.5)'
: '2px solid rgba(255, 255, 255, 0.1)';
toggleLabel.classList.toggle('enabled', !!config.global_override_enabled);
}
// Show modal
@ -2867,12 +2865,10 @@ function toggleGlobalOverrideOptions() {
options.style.pointerEvents = enabled ? 'auto' : 'none';
}
// Update toggle label border
// Reflect enabled state on the toggle card (styled in CSS)
const toggleLabel = document.getElementById('global-override-toggle-label');
if (toggleLabel) {
toggleLabel.style.border = enabled
? '2px solid rgba(29, 185, 84, 0.5)'
: '2px solid rgba(255, 255, 255, 0.1)';
toggleLabel.classList.toggle('enabled', enabled);
}
}
@ -3144,7 +3140,7 @@ async function startWatchlistScan() {
try {
const button = document.getElementById('scan-watchlist-btn');
button.disabled = true;
button.textContent = 'Starting scan...';
_wlSetChipLabel(button, 'Starting scan...');
button.classList.add('btn-processing');
const response = await fetch('/api/watchlist/scan', {
@ -3157,7 +3153,7 @@ async function startWatchlistScan() {
throw new Error(data.error || 'Failed to start scan');
}
button.textContent = 'Scanning...';
_wlSetChipLabel(button, 'Scanning...');
// Show cancel button
const cancelBtn = document.getElementById('cancel-watchlist-scan-btn');
@ -3180,7 +3176,7 @@ async function startWatchlistScan() {
console.error('Error starting watchlist scan:', error);
const button = document.getElementById('scan-watchlist-btn');
button.disabled = false;
button.textContent = 'Scan for New Releases';
_wlSetChipLabel(button, 'Scan for New Releases');
button.classList.remove('btn-processing');
alert(`Error starting scan: ${error.message}`);
}
@ -3226,6 +3222,18 @@ function renderWatchlistScanTrackLedger(events) {
// Human-readable phase line for the scan deck ("checking_album_2_of_5" →
// "Checking album 2 of 5").
// Set a wl-chip button's label without destroying its icon/shimmer children
// (textContent wipes them; these buttons carry an svg + shimmer span).
function _wlSetChipLabel(btn, text) {
if (!btn) return;
const svg = btn.querySelector('svg');
const shimmer = btn.querySelector('.wl-chip-shimmer');
btn.textContent = '';
if (svg) btn.appendChild(svg);
btn.appendChild(document.createTextNode(' ' + text));
if (shimmer) btn.appendChild(shimmer);
}
function _wlPrettyPhase(data) {
const phase = data.current_phase || '';
if (!phase) return 'Working…';
@ -3360,7 +3368,7 @@ function handleWatchlistScanData(data) {
if (data.status === 'completed') {
if (button) {
button.disabled = false;
button.textContent = 'Scan for New Releases';
_wlSetChipLabel(button, 'Scan for New Releases');
button.classList.remove('btn-processing');
}
@ -3411,7 +3419,7 @@ function handleWatchlistScanData(data) {
} else if (data.status === 'cancelled') {
if (button) {
button.disabled = false;
button.textContent = 'Scan for New Releases';
_wlSetChipLabel(button, 'Scan for New Releases');
button.classList.remove('btn-processing');
}
@ -3459,7 +3467,7 @@ function handleWatchlistScanData(data) {
} else if (data.status === 'error') {
if (button) {
button.disabled = false;
button.textContent = 'Scan for New Releases';
_wlSetChipLabel(button, 'Scan for New Releases');
button.classList.remove('btn-processing');
}
@ -3510,7 +3518,7 @@ async function updateSimilarArtists() {
const scanButton = document.getElementById('scan-watchlist-btn');
button.disabled = true;
button.textContent = 'Updating...';
_wlSetChipLabel(button, 'Updating...');
button.classList.add('btn-processing');
if (scanButton) scanButton.disabled = true;
@ -3535,7 +3543,7 @@ async function updateSimilarArtists() {
const scanButton = document.getElementById('scan-watchlist-btn');
button.disabled = false;
button.textContent = 'Update Similar Artists';
_wlSetChipLabel(button, 'Update Similar Artists');
button.classList.remove('btn-processing');
if (scanButton) scanButton.disabled = false;
@ -3558,7 +3566,7 @@ async function pollSimilarArtistsUpdate() {
if (data.status === 'completed') {
if (button) {
button.disabled = false;
button.textContent = 'Update Similar Artists';
_wlSetChipLabel(button, 'Update Similar Artists');
button.classList.remove('btn-processing');
}
if (scanButton) scanButton.disabled = false;
@ -3569,7 +3577,7 @@ async function pollSimilarArtistsUpdate() {
} else if (data.status === 'error') {
if (button) {
button.disabled = false;
button.textContent = 'Update Similar Artists';
_wlSetChipLabel(button, 'Update Similar Artists');
button.classList.remove('btn-processing');
}
if (scanButton) scanButton.disabled = false;
@ -3596,7 +3604,7 @@ async function pollSimilarArtistsUpdate() {
if (button) {
button.disabled = false;
button.textContent = 'Update Similar Artists';
_wlSetChipLabel(button, 'Update Similar Artists');
button.classList.remove('btn-processing');
}
if (scanButton) scanButton.disabled = false;

View file

@ -20357,6 +20357,185 @@ body.helper-mode-active #dashboard-activity-feed:hover {
margin-bottom: 4px;
}
/* ── #831 round 3: watchlist action chips (artist-page button language) ── */
.wl-chip {
--chip-rgb: 148, 163, 184;
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 8px 16px;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.02em;
font-family: inherit;
color: rgba(var(--chip-rgb), 1);
background: linear-gradient(135deg, rgba(var(--chip-rgb), 0.15) 0%, rgba(var(--chip-rgb), 0.05) 100%);
border: 1px solid rgba(var(--chip-rgb), 0.3);
border-radius: 8px;
cursor: pointer;
transition: all 0.25s ease;
outline: none;
overflow: hidden;
}
.wl-chip:hover {
background: linear-gradient(135deg, rgba(var(--chip-rgb), 0.25) 0%, rgba(var(--chip-rgb), 0.12) 100%);
border-color: rgba(var(--chip-rgb), 0.5);
transform: translateY(-1px);
box-shadow: 0 4px 16px rgba(var(--chip-rgb), 0.25);
}
.wl-chip:active {
transform: translateY(0);
box-shadow: none;
}
.wl-chip svg {
transition: transform 0.25s ease;
flex-shrink: 0;
}
.wl-chip:hover svg {
transform: scale(1.12);
}
.wl-chip--blue { --chip-rgb: 96, 165, 250; }
.wl-chip--green { --chip-rgb: 111, 217, 154; }
.wl-chip--amber { --chip-rgb: 251, 191, 36; }
.wl-chip--red { --chip-rgb: 248, 113, 113; }
.wl-chip--slate { --chip-rgb: 165, 180, 203; }
/* Primary CTA — solid accent gradient + shimmer sweep */
.wl-chip--cta {
color: #fff;
background: linear-gradient(135deg, rgb(var(--accent-rgb)), rgb(var(--accent-light-rgb)));
border: 1px solid rgba(var(--accent-light-rgb), 0.6);
box-shadow: 0 4px 18px rgba(var(--accent-rgb), 0.35);
}
.wl-chip--cta:hover {
background: linear-gradient(135deg, rgb(var(--accent-rgb)), rgb(var(--accent-light-rgb)));
border-color: rgba(var(--accent-light-rgb), 0.9);
transform: translateY(-1px);
box-shadow: 0 6px 24px rgba(var(--accent-rgb), 0.5);
}
.wl-chip-shimmer {
position: absolute;
inset: 0;
background: linear-gradient(105deg, transparent 40%, rgba(255, 255, 255, 0.25) 50%, transparent 60%);
transform: translateX(-100%);
animation: wl-chip-shimmer 3.2s ease-in-out infinite;
pointer-events: none;
}
@keyframes wl-chip-shimmer {
0% { transform: translateX(-100%); }
55% { transform: translateX(100%); }
100% { transform: translateX(100%); }
}
/* Header meta chips */
.wl-meta-chip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 12px;
border-radius: 999px;
font-size: 12px;
font-weight: 600;
color: rgba(255, 255, 255, 0.7);
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.wl-meta-chip--accent {
color: rgb(var(--accent-light-rgb));
background: rgba(var(--accent-rgb), 0.1);
border-color: rgba(var(--accent-rgb), 0.3);
}
/* ── #831 round 3: Global Settings modal reskin ───────────────────────── */
.wl-global-modal-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
padding: 22px 28px 18px;
border-bottom: 1px solid rgba(255, 255, 255, 0.07);
}
.wl-global-modal-title {
margin: 0;
font-size: 20px;
font-weight: 800;
letter-spacing: -0.2px;
color: #fff;
}
.wl-global-modal-sub {
margin: 4px 0 0;
font-size: 13px;
color: rgba(255, 255, 255, 0.5);
}
.wl-global-modal-close {
flex-shrink: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.05);
color: rgba(255, 255, 255, 0.6);
font-size: 14px;
cursor: pointer;
transition: all 0.15s ease;
}
.wl-global-modal-close:hover {
background: rgba(255, 255, 255, 0.12);
color: #fff;
}
/* Option cards: live checked-state feedback (applies to the per-artist config
modal too same components, same standard). */
.config-option:has(input[type="checkbox"]:checked) {
border-color: rgba(var(--accent-rgb), 0.45);
background: rgba(var(--accent-rgb), 0.07);
}
.config-option:has(input[type="checkbox"]:checked) .config-option-icon {
filter: none;
opacity: 1;
}
.config-option:not(:has(input[type="checkbox"]:checked)) .config-option-icon {
filter: grayscale(0.7);
opacity: 0.55;
}
/* The global-override master toggle gets a stronger enabled treatment */
.global-override-toggle.enabled {
border-color: rgba(var(--accent-rgb), 0.6) !important;
background: linear-gradient(135deg, rgba(var(--accent-rgb), 0.12), rgba(var(--accent-rgb), 0.04)) !important;
box-shadow: 0 0 22px rgba(var(--accent-rgb), 0.12);
}
.wl-global-modal .config-section-title {
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.8px;
color: rgb(var(--accent-light-rgb));
}
/* Watchlist Search */
.watchlist-search-input {