Revamp watchlist & wishlist buttons with dark glass design and animated gradient border

- Dark translucent background with backdrop blur instead of bright colored fills
- Animated flowing gradient border using CSS mask-composite technique
- Color-tinted labels and count badges (amber for watchlist, accent for wishlist)
- Shimmer sweep clipped inside button bounds
- Structured HTML: separate icon, label, badge, and shimmer elements
- Badge pulses when count > 0
- Worker orbs: 7s delay before collapsing back after mouse leaves header
This commit is contained in:
Broque Thomas 2026-03-16 18:10:10 -07:00
parent 775307c2b5
commit 46d3309835
4 changed files with 255 additions and 31 deletions

View file

@ -551,9 +551,18 @@
<img src="https://cdn-icons-png.flaticon.com/512/8765/8765164.png" alt="Import"
class="import-logo">
</button>
<button class="header-button watchlist-button" id="watchlist-button">👁️ Watchlist
(0)</button>
<button class="header-button wishlist-button" id="wishlist-button">🎵 Wishlist (0)</button>
<button class="header-button watchlist-button" id="watchlist-button">
<span class="hero-btn-icon">👁️</span>
<span class="hero-btn-label">Watchlist</span>
<span class="hero-btn-badge" id="watchlist-badge">0</span>
<span class="hero-btn-shimmer"></span>
</button>
<button class="header-button wishlist-button" id="wishlist-button">
<span class="hero-btn-icon">🎵</span>
<span class="hero-btn-label">Wishlist</span>
<span class="hero-btn-badge" id="wishlist-badge">0</span>
<span class="hero-btn-shimmer"></span>
</button>
</div>
</div>

View file

@ -312,12 +312,20 @@ function handleServiceStatusUpdate(data) {
}
}
function _updateHeroBtnCount(buttonId, badgeId, count) {
const badge = document.getElementById(badgeId);
if (badge) {
badge.textContent = count;
badge.classList.toggle('has-items', count > 0);
}
}
function handleWatchlistCountUpdate(data) {
if (data.success) {
_updateHeroBtnCount('watchlist-button', 'watchlist-badge', data.count);
const watchlistButton = document.getElementById('watchlist-button');
if (watchlistButton) {
const countdownText = data.next_run_in_seconds ? formatCountdownTime(data.next_run_in_seconds) : '';
watchlistButton.textContent = `\u{1F441}\uFE0F Watchlist (${data.count})`;
if (countdownText) {
watchlistButton.title = `Next auto-scan in ${countdownText}`;
}
@ -399,11 +407,10 @@ function handleDashboardDbStats(stats) {
}
function handleDashboardWishlistCount(data) {
// Same logic as updateWishlistCount response handler
const count = data.count || 0;
_updateHeroBtnCount('wishlist-button', 'wishlist-badge', count);
const wishlistButton = document.getElementById('wishlist-button');
if (wishlistButton) {
wishlistButton.textContent = `\uD83C\uDFB5 Wishlist (${count})`;
if (count === 0) {
wishlistButton.classList.remove('wishlist-active');
wishlistButton.classList.add('wishlist-inactive');
@ -20816,11 +20823,9 @@ async function updateWishlistCount() {
const data = await response.json();
const count = data.count || 0;
_updateHeroBtnCount('wishlist-button', 'wishlist-badge', count);
const wishlistButton = document.getElementById('wishlist-button');
if (wishlistButton) {
wishlistButton.textContent = `🎵 Wishlist (${count})`;
// Update button styling based on count (matching GUI behavior)
if (count === 0) {
wishlistButton.classList.remove('wishlist-active');
wishlistButton.classList.add('wishlist-inactive');
@ -34060,11 +34065,10 @@ async function updateWatchlistButtonCount() {
const data = await response.json();
if (data.success) {
_updateHeroBtnCount('watchlist-button', 'watchlist-badge', data.count);
const watchlistButton = document.getElementById('watchlist-button');
if (watchlistButton) {
// Format countdown for button tooltip (optional enhancement)
const countdownText = data.next_run_in_seconds ? formatCountdownTime(data.next_run_in_seconds) : '';
watchlistButton.textContent = `👁️ Watchlist (${data.count})`;
if (countdownText) {
watchlistButton.title = `Next auto-scan in ${countdownText}`;
}

View file

@ -5727,40 +5727,236 @@ body {
inset 0 1px 0 rgba(255, 255, 255, 0.2);
}
/* ── Watchlist & Wishlist Hero Buttons ── */
.watchlist-button,
.wishlist-button {
background: linear-gradient(135deg, rgb(var(--accent-rgb)) 0%, rgb(var(--accent-light-rgb)) 100%);
position: relative;
display: inline-flex;
align-items: center;
gap: 9px;
padding: 12px 20px;
overflow: visible;
background: rgba(18, 18, 22, 0.85);
backdrop-filter: blur(16px) saturate(1.4);
color: #e0e0e0;
border: none;
border-radius: 22px;
z-index: 0;
}
.wishlist-button:hover {
background: linear-gradient(135deg, rgb(var(--accent-light-rgb)) 0%, rgb(var(--accent-neon-rgb)) 100%);
/* Animated gradient border using border-image-like technique */
.watchlist-button::before,
.wishlist-button::before {
content: '';
position: absolute;
inset: 0;
border-radius: 22px;
padding: 1.5px;
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
mask-composite: exclude;
pointer-events: none;
z-index: 1;
}
/* Wishlist button states based on count */
.wishlist-button.wishlist-inactive {
background: linear-gradient(135deg, #404040 0%, #505050 100%);
color: #888888;
.watchlist-button::before {
background: linear-gradient(
135deg,
#ff8f00, #ffc107, #ffd54f, #ffab00, #ffc107, #ff8f00
);
background-size: 300% 300%;
animation: heroBorderFlow 4s ease infinite;
}
.wishlist-button.wishlist-inactive:hover {
background: linear-gradient(135deg, #505050 0%, #666666 100%);
color: #999999;
.wishlist-button::before {
background: linear-gradient(
135deg,
rgb(var(--accent-rgb)),
rgb(var(--accent-light-rgb)),
rgb(var(--accent-neon-rgb)),
rgb(var(--accent-rgb)),
rgb(var(--accent-light-rgb)),
rgb(var(--accent-rgb))
);
background-size: 300% 300%;
animation: heroBorderFlow 4s ease infinite;
animation-delay: 2s;
}
.wishlist-button.wishlist-active {
background: linear-gradient(135deg, rgb(var(--accent-rgb)) 0%, rgb(var(--accent-light-rgb)) 100%);
color: #000000;
}
.wishlist-button.wishlist-active:hover {
background: linear-gradient(135deg, rgb(var(--accent-light-rgb)) 0%, rgb(var(--accent-neon-rgb)) 100%);
@keyframes heroBorderFlow {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
/* Outer glow */
.watchlist-button {
background: linear-gradient(135deg, #ffc107 0%, #ffca28 100%);
box-shadow: 0 0 14px rgba(255, 193, 7, 0.12), 0 4px 16px rgba(0, 0, 0, 0.3);
}
.wishlist-button {
box-shadow: 0 0 14px rgba(var(--accent-rgb), 0.12), 0 4px 16px rgba(0, 0, 0, 0.3);
}
.watchlist-button:hover {
background: linear-gradient(135deg, #ffca28 0%, #ffd54f 100%);
box-shadow: 0 0 24px rgba(255, 193, 7, 0.25), 0 4px 20px rgba(0, 0, 0, 0.4);
transform: translateY(-1px);
}
.wishlist-button:hover {
box-shadow: 0 0 24px rgba(var(--accent-rgb), 0.25), 0 4px 20px rgba(0, 0, 0, 0.4);
transform: translateY(-1px);
}
/* Icon */
.hero-btn-icon {
font-size: 16px;
line-height: 1;
}
/* Label */
.hero-btn-label {
font-weight: 700;
letter-spacing: 0.3px;
}
/* Watchlist text color tint */
.watchlist-button .hero-btn-label {
color: #ffd54f;
}
.wishlist-button .hero-btn-label {
color: rgb(var(--accent-light-rgb));
}
/* Count badge */
.hero-btn-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 22px;
height: 22px;
padding: 0 6px;
border-radius: 11px;
font-size: 11px;
font-weight: 800;
line-height: 1;
transition: all 0.3s ease;
}
.watchlist-button .hero-btn-badge {
background: rgba(255, 193, 7, 0.15);
color: #ffc107;
border: 1px solid rgba(255, 193, 7, 0.3);
}
.wishlist-button .hero-btn-badge {
background: rgba(var(--accent-rgb), 0.15);
color: rgb(var(--accent-light-rgb));
border: 1px solid rgba(var(--accent-rgb), 0.3);
}
.hero-btn-badge.has-items {
animation: badgePulse 2s ease-in-out infinite;
}
.watchlist-button .hero-btn-badge.has-items {
background: rgba(255, 193, 7, 0.25);
border-color: rgba(255, 193, 7, 0.5);
}
.wishlist-button .hero-btn-badge.has-items {
background: rgba(var(--accent-rgb), 0.25);
border-color: rgba(var(--accent-rgb), 0.5);
}
@keyframes badgePulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.12); }
}
/* Shimmer sweep — clipped inside button */
.hero-btn-shimmer {
position: absolute;
inset: 0;
border-radius: 22px;
overflow: hidden;
pointer-events: none;
z-index: 1;
}
.hero-btn-shimmer::after {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
animation: shimmerSweep 5s ease-in-out infinite;
}
.watchlist-button .hero-btn-shimmer::after {
background: linear-gradient(
105deg,
transparent 30%,
rgba(255, 193, 7, 0.1) 50%,
transparent 70%
);
}
.wishlist-button .hero-btn-shimmer::after {
background: linear-gradient(
105deg,
transparent 30%,
rgba(var(--accent-rgb), 0.1) 50%,
transparent 70%
);
animation-delay: 2.5s;
}
@keyframes shimmerSweep {
0%, 100% { left: -100%; }
40%, 60% { left: 100%; }
}
/* ── Wishlist inactive state ── */
.wishlist-button.wishlist-inactive {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
}
.wishlist-button.wishlist-inactive::before {
background: linear-gradient(135deg, #444, #555, #444);
background-size: 100% 100%;
animation: none;
}
.wishlist-button.wishlist-inactive .hero-btn-label {
color: #777;
}
.wishlist-button.wishlist-inactive .hero-btn-badge {
background: rgba(255, 255, 255, 0.06);
color: #666;
border-color: rgba(255, 255, 255, 0.1);
animation: none;
}
.wishlist-button.wishlist-inactive .hero-btn-shimmer {
display: none;
}
.wishlist-button.wishlist-inactive:hover {
box-shadow: 0 0 12px rgba(255, 255, 255, 0.08), 0 4px 20px rgba(0, 0, 0, 0.4);
}
/* Active wishlist just uses the normal accent styles above */
.wishlist-button.wishlist-active {
/* already styled by default .wishlist-button rules */
}
/* Service Status Grid */

View file

@ -43,6 +43,8 @@
let onDashboard = false;
let expandProgress = 0;
let staggerTimers = [];
let collapseDelay = null;
const COLLAPSE_DELAY_MS = 7000;
// ── Init ──
@ -251,6 +253,11 @@
function onMouseEnter() {
if (!onDashboard) return;
// Cancel any pending collapse
if (collapseDelay) {
clearTimeout(collapseDelay);
collapseDelay = null;
}
if (state === 'orbs' || state === 'collapsing') {
state = 'expanding';
expandProgress = 0;
@ -260,7 +267,14 @@
function onMouseLeave() {
if (!onDashboard) return;
if (state === 'expanded' || state === 'expanding') {
enterCollapsingState();
// Delay before collapsing back to orbs
if (collapseDelay) clearTimeout(collapseDelay);
collapseDelay = setTimeout(() => {
collapseDelay = null;
if (state === 'expanded' || state === 'expanding') {
enterCollapsingState();
}
}, COLLAPSE_DELAY_MS);
}
}
@ -551,6 +565,7 @@
sparks = [];
enterOrbState();
} else if (!onDashboard && wasDashboard) {
if (collapseDelay) { clearTimeout(collapseDelay); collapseDelay = null; }
stopLoop();
state = 'idle';
sparks = [];