Sync page: collapse tabs to brand-logo chips with active label pill

The sync-tabs row had 14 sources jostling for horizontal space —
labels wrapped to 2 lines, the active pill ate disproportionate
room, the whole strip felt cramped and would only get worse as
more sources get added.

Restyled the strip as circular brand-logo chips. Inactive tabs
are 40px discs that show only the source's icon; the currently-
active tab swells into a pill that reveals its label inline.
Hover surfaces the source name as a native tooltip via the
title attr. Each chip carries its source's brand color as a
hover ring + active fill (Spotify green, Tidal orange, Qobuz
blue, Deezer purple, iTunes coral, YouTube red, Beatport green,
LB orange, Last.fm red, SSD teal).

Three sources share a logo with another source (Spotify Link
/ Spotify, Deezer Link / Deezer, iTunes Link / no native iTunes
but same logo family). Each "Link" variant carries a small
chain-link badge bottom-right so the chip disambiguates without
forcing the label to always be visible.

CSS-only swap — same JS handlers, same .active class, same
data-tab routing. HTML edit wraps each tab's label in a
``<span class="sync-tab-label">`` and adds ``data-link="true"``
to the Link variants so the CSS can target them.

Responsive: chips collapse to 36px on laptop / tablet and 32px
on mobile; the divider hides on mobile and gap tightens.
This commit is contained in:
Broque Thomas 2026-05-27 22:04:28 -07:00
parent f976a6da53
commit 4ae5aee528
3 changed files with 231 additions and 30 deletions

View file

@ -996,51 +996,51 @@
<!-- Left Panel: Tabbed Playlist Section -->
<div class="sync-main-panel">
<div class="sync-tabs">
<button class="sync-tab-button sync-tab-server active" data-tab="server">
<span class="tab-icon server-icon"></span> Server Playlists
<button class="sync-tab-button sync-tab-server active" data-tab="server" title="Server Playlists">
<span class="tab-icon server-icon"></span><span class="sync-tab-label">Server Playlists</span>
</button>
<div class="sync-tab-divider"></div>
<button class="sync-tab-button" data-tab="spotify">
<span class="tab-icon spotify-icon"></span> Spotify
<button class="sync-tab-button" data-tab="spotify" title="Spotify">
<span class="tab-icon spotify-icon"></span><span class="sync-tab-label">Spotify</span>
</button>
<button class="sync-tab-button" data-tab="spotify-public">
<span class="tab-icon spotify-icon"></span> Spotify Link
<button class="sync-tab-button" data-tab="spotify-public" data-link="true" title="Spotify Link">
<span class="tab-icon spotify-icon"></span><span class="sync-tab-label">Spotify Link</span>
</button>
<button class="sync-tab-button" data-tab="itunes-link">
<span class="tab-icon itunes-icon"></span> iTunes Link
<button class="sync-tab-button" data-tab="itunes-link" data-link="true" title="iTunes Link">
<span class="tab-icon itunes-icon"></span><span class="sync-tab-label">iTunes Link</span>
</button>
<button class="sync-tab-button" data-tab="tidal">
<span class="tab-icon tidal-icon"></span> Tidal
<button class="sync-tab-button" data-tab="tidal" title="Tidal">
<span class="tab-icon tidal-icon"></span><span class="sync-tab-label">Tidal</span>
</button>
<button class="sync-tab-button" data-tab="qobuz">
<span class="tab-icon qobuz-icon"></span> Qobuz
<button class="sync-tab-button" data-tab="qobuz" title="Qobuz">
<span class="tab-icon qobuz-icon"></span><span class="sync-tab-label">Qobuz</span>
</button>
<button class="sync-tab-button" data-tab="deezer">
<span class="tab-icon deezer-icon"></span> Deezer
<button class="sync-tab-button" data-tab="deezer" title="Deezer">
<span class="tab-icon deezer-icon"></span><span class="sync-tab-label">Deezer</span>
</button>
<button class="sync-tab-button" data-tab="deezer-link">
<span class="tab-icon deezer-icon"></span> Deezer Link
<button class="sync-tab-button" data-tab="deezer-link" data-link="true" title="Deezer Link">
<span class="tab-icon deezer-icon"></span><span class="sync-tab-label">Deezer Link</span>
</button>
<button class="sync-tab-button" data-tab="youtube">
<span class="tab-icon youtube-icon"></span> YouTube
<button class="sync-tab-button" data-tab="youtube" title="YouTube">
<span class="tab-icon youtube-icon"></span><span class="sync-tab-label">YouTube</span>
</button>
<button class="sync-tab-button" data-tab="beatport">
<span class="tab-icon beatport-icon"></span> Beatport
<button class="sync-tab-button" data-tab="beatport" title="Beatport">
<span class="tab-icon beatport-icon"></span><span class="sync-tab-label">Beatport</span>
</button>
<button class="sync-tab-button" data-tab="listenbrainz-sync">
<span class="tab-icon listenbrainz-icon"></span> ListenBrainz
<button class="sync-tab-button" data-tab="listenbrainz-sync" title="ListenBrainz">
<span class="tab-icon listenbrainz-icon"></span><span class="sync-tab-label">ListenBrainz</span>
</button>
<button class="sync-tab-button" data-tab="lastfm-sync">
<span class="tab-icon lastfm-icon"></span> Last.fm
<button class="sync-tab-button" data-tab="lastfm-sync" title="Last.fm">
<span class="tab-icon lastfm-icon"></span><span class="sync-tab-label">Last.fm</span>
</button>
<button class="sync-tab-button" data-tab="soulsync-discovery-sync">
<span class="tab-icon soulsync-discovery-icon"></span> SoulSync Discovery
<button class="sync-tab-button" data-tab="soulsync-discovery-sync" title="SoulSync Discovery">
<span class="tab-icon soulsync-discovery-icon"></span><span class="sync-tab-label">SoulSync Discovery</span>
</button>
<button class="sync-tab-button" data-tab="import-file">
<span class="tab-icon import-file-icon"></span> Import
<button class="sync-tab-button" data-tab="import-file" title="Import">
<span class="tab-icon import-file-icon"></span><span class="sync-tab-label">Import</span>
</button>
<button class="sync-tab-button" data-tab="mirrored">
<span class="tab-icon mirrored-icon"></span> Mirrored
<button class="sync-tab-button" data-tab="mirrored" title="Mirrored">
<span class="tab-icon mirrored-icon"></span><span class="sync-tab-label">Mirrored</span>
</button>
</div>

View file

@ -3415,6 +3415,7 @@ function closeHelperSearch() {
const WHATS_NEW = {
'2.6.3': [
{ unreleased: true },
{ title: 'Sync page: collapse tabs to brand-logo chips, active swells into a label pill', desc: 'with 14 sync sources now (Spotify, Spotify Link, iTunes Link, Tidal, Qobuz, Deezer, Deezer Link, YouTube, Beatport, ListenBrainz, Last.fm, SoulSync Discovery, Import, Mirrored, plus Server Playlists), the old "row of equal-width labeled pills" tab strip ran out of horizontal room — labels wrapped to 2 lines, the active pill ate disproportionate space, the whole row felt cramped. Restyled the tab strip as a row of circular brand-logo chips (40px on desktop, smaller on tablet/mobile); the currently-active tab swells into a pill with its label inline. Hover tooltip surfaces the source name via the title attribute. Each chip carries its source\'s brand color as a hover ring + active fill (Spotify green, Tidal orange, Qobuz blue, Deezer purple, iTunes coral, YouTube red, Beatport green, LB orange, Last.fm red, SSD teal). To disambiguate the three duplicate-logo sources (Spotify Link / Deezer Link / iTunes Link share a logo with their native-source siblings), each "Link" variant carries a small chain-link badge bottom-right. CSS-only swap — zero JS / behavior changes, same tab click handler, same data-tab routing. The active tab\'s label still shows so context is never lost; non-active tabs trade their label for ~70% horizontal savings.', page: 'sync' },
{ title: 'Fix: Soulseek album downloads stuck on "failed" after slskd finished the release (#715)', desc: 'when an album was downloaded via the Soulseek album-bundle path (the release-first flow added in 2.6), the batch would frequently mark itself as failed even though slskd had successfully downloaded every track of the album. Worst on users running slskd with the common ``directories.downloads.username = true`` config — files landed at ``<download_dir>/<username>/<filename>`` instead of the three hard-coded candidate paths the bundle resolver probed, so every file looked locally missing and the poll spun until the ~30 minute deadline expired. Per-track Soulseek downloads were unaffected because they already routed through a recursive walk-by-basename helper. Lifted that helper into ``core/downloads/file_finder.py`` and rewired the bundle path to use it — same logic the per-track flow has used since 2.5.9. Also: when the bundle poll detects that slskd reports every transfer Completed but no local file resolves within a 45-second grace window, it now exits with a clear log line pointing at the likely ``soulseek.download_path`` mismatch instead of silently spinning until the master deadline. Misleading "(0 tracks, quality=)" log on the preflight-reuse path also fixed — was reading attrs off a None object. 17 new unit tests pin every slskd layout shape (flat, username-prefixed, full-tree-preserved, deep nested, dedup-suffix, quarantine-skip, YouTube/Tidal encoded names, transfer-dir fallback, fuzzy punctuation variants). Closes #715.', page: 'downloads' },
{ title: 'Auto-Sync manager: full visual overhaul to match the dashboard vibe', desc: 'the Auto-Sync manager modal had been carrying its original visual treatment forward unchanged while the rest of the app moved toward the glassy / accent-radial / gradient-border aesthetic the dashboard now sets. Restyled every surface inside the modal to match — selector-based override layer at the end of style.css so functionality is untouched (zero JS or HTML changes). Touched surfaces: modal shell (glass shell with thin accent border + corner radial wash + inner top-edge highlight), header (gradient text title + accent-tinted hairline separator + spinning-X close), KPI summary tiles (dashboard-style gradient tiles with accent top-edge glow on hover + gradient stat numbers), live monitor strip (accent-tinted glass card with status-colored borders), refresh / intro buttons (pill primary with accent fill + glow on hover), tabs (underline-style with accent fill + soft radial glow on the active tab), sidebar (glass panel with accent-tinted source-group cards, accent border on scheduled playlists, accent ring on the filter input focus), board area (subtle accent radial spotlight + column cards with gradient headers + drag-over accent glow), drop zones (animated dashed pull with accent radial wash + accent-tinted text on drag-over), scheduled cards (accent left-edge stripe, gradient pill timing badges, pill "Run now" primary + rotating ghost X), run history rows (dashboard recent-activity aesthetic with accent hover lift + pill status badges), bulk-schedule popover, weekly editor (full glass modal matching the version-modal vibe, day-toggle pills, accent-pill save button), and empty / monitor-empty states. Soft accent-tinted scrollbars throughout. Delete the v2 block at the bottom of style.css to revert.', page: 'automations' },
{ title: 'Auto-Sync: weekly board cards now match the hourly board', desc: 'when the Weekly Board shipped, its scheduled-card visual diverged from the hourly board — weekly cards only showed the playlist name + weekly label + timezone, while hourly cards carry the full action row (Run now button, unschedule X, next-run countdown, health badge). Standardised the weekly card on the hourly shape so dropping a playlist onto a day column produces a card with the exact same affordances as dropping it onto an hourly bucket: health indicator dot when recent runs failed, source + track count meta line, weekly label + timezone + "next in Nh" pills, Run-now button (disabled while pipeline is running, same gating as hourly), and an unschedule X. Click anywhere outside the buttons still opens the weekly editor for changing days / time / tz. Weekly cards also became draggable between day columns now — drop on a new column appends that day to the schedule (matches the existing multi-day editor flow).', page: 'automations' },

View file

@ -63180,6 +63180,206 @@ body.reduce-effects .dash-card::after {
}
/* =====================================================================
* Sync page tabs icon-chip v2 layout
*
* 14+ tabs no longer fit in a single horizontal pill row. Collapse
* each tab to a circular brand-logo chip; the active tab swells
* into a pill that shows its label inline. Hover surfaces the
* tab title attribute as a native tooltip. ``[data-link="true"]``
* tabs (Spotify Link, Deezer Link, iTunes Link) carry a small
* chain-link badge bottom-right so they disambiguate from their
* native-source siblings (same brand logo, different source mode).
*
* Behavior untouched same JS event handlers, same .active class,
* same data-tab routing. CSS-only swap.
* ===================================================================== */
.sync-tabs {
flex-wrap: wrap;
gap: 6px;
padding: 8px 10px;
background:
radial-gradient(ellipse at top,
color-mix(in srgb, rgb(var(--accent-rgb)) 4%, transparent) 0%,
rgba(0, 0, 0, 0.22) 70%);
border: 1px solid rgba(255, 255, 255, 0.04);
border-radius: 14px;
align-items: center;
}
.sync-tab-button {
/* Reset: drop flex:1 stretch + heavy padding, become a chip. */
flex: 0 0 auto;
width: 40px;
height: 40px;
padding: 0;
gap: 0;
border-radius: 999px;
background:
linear-gradient(160deg,
rgba(255, 255, 255, 0.04) 0%,
rgba(0, 0, 0, 0.18) 100%);
border: 1px solid rgba(255, 255, 255, 0.06);
color: rgba(255, 255, 255, 0.78);
font-size: 12px;
overflow: hidden;
position: relative;
transition:
width 0.32s cubic-bezier(0.34, 1.2, 0.5, 1),
padding 0.32s cubic-bezier(0.34, 1.2, 0.5, 1),
background 0.25s ease,
border-color 0.25s ease,
box-shadow 0.25s ease,
transform 0.18s ease;
}
.sync-tab-button:hover:not(.active) {
background:
linear-gradient(160deg,
color-mix(in srgb, var(--sync-tab-accent, rgb(var(--accent-rgb))) 14%, transparent) 0%,
rgba(0, 0, 0, 0.18) 100%);
border-color: color-mix(in srgb, var(--sync-tab-accent, rgb(var(--accent-rgb))) 35%, rgba(255, 255, 255, 0.1));
color: #fff;
transform: translateY(-1px);
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.25);
}
/* Active state — swell into a pill that shows the label inline. */
.sync-tab-button.active {
width: auto;
padding: 0 14px 0 10px;
gap: 8px;
background: linear-gradient(135deg,
color-mix(in srgb, var(--sync-tab-accent, rgb(var(--accent-rgb))) 92%, white 8%),
color-mix(in srgb, var(--sync-tab-accent, rgb(var(--accent-rgb))) 78%, black 22%));
border-color: color-mix(in srgb, var(--sync-tab-accent, rgb(var(--accent-rgb))) 55%, transparent);
color: #fff;
box-shadow:
0 4px 18px color-mix(in srgb, var(--sync-tab-accent, rgb(var(--accent-rgb))) 28%, transparent),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
}
.sync-tab-server.active {
/* The Server Playlists tab kept its bespoke gradient — preserve it. */
background: linear-gradient(135deg,
rgba(var(--accent-rgb), 0.95),
rgba(var(--accent-rgb), 0.75)) !important;
color: #000 !important;
}
/* Per-tab accent. Used by both the active fill and the hover ring,
* so each chip lights up in its brand color. */
.sync-tab-button[data-tab="spotify"],
.sync-tab-button[data-tab="spotify-public"] { --sync-tab-accent: #1DB954; }
.sync-tab-button[data-tab="tidal"] { --sync-tab-accent: #ff6600; }
.sync-tab-button[data-tab="qobuz"] { --sync-tab-accent: #5b8dee; }
.sync-tab-button[data-tab="deezer"],
.sync-tab-button[data-tab="deezer-link"] { --sync-tab-accent: #a238ff; }
.sync-tab-button[data-tab="itunes-link"] { --sync-tab-accent: #fa586a; }
.sync-tab-button[data-tab="youtube"] { --sync-tab-accent: #ff0000; }
.sync-tab-button[data-tab="beatport"] { --sync-tab-accent: #01ff95; }
.sync-tab-button[data-tab="listenbrainz-sync"] { --sync-tab-accent: #eb743b; }
.sync-tab-button[data-tab="lastfm-sync"] { --sync-tab-accent: #d51007; }
.sync-tab-button[data-tab="soulsync-discovery-sync"] { --sync-tab-accent: #14b8a6; }
/* Active tabs use white-on-color, so the per-tab icon overrides
* shipped in the original CSS apply automatically. Nothing to do
* for the active-icon swap handled by existing rules above. */
/* Icon scaling chip is 40px, icon was 16px; size up to 20px so
* the logo fills the chip naturally. */
.sync-tab-button .tab-icon {
width: 20px;
height: 20px;
flex-shrink: 0;
}
/* Label collapses to width 0 when not active, so the chip stays
* circular. Reveals on .active via the gap + padding shift above. */
.sync-tab-label {
max-width: 0;
opacity: 0;
white-space: nowrap;
overflow: hidden;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.02em;
transition: max-width 0.32s cubic-bezier(0.34, 1.2, 0.5, 1),
opacity 0.22s ease;
}
.sync-tab-button.active .sync-tab-label {
max-width: 200px;
opacity: 1;
}
/* Chain-link badge for the ``Link`` variants (Spotify Link, Deezer
* Link, iTunes Link). Bottom-right corner of the chip, accent-tinted
* disc with a tiny link glyph. Disambiguates same-brand chips at
* a glance without forcing the label to always be visible. */
.sync-tab-button[data-link="true"]::after {
content: '';
position: absolute;
right: 1px;
bottom: 1px;
width: 14px;
height: 14px;
border-radius: 50%;
background:
color-mix(in srgb, var(--sync-tab-accent, rgb(var(--accent-rgb))) 85%, black 15%)
center / 70% no-repeat
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'><path d='M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71'/><path d='M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71'/></svg>");
border: 1.5px solid #1a1d28;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
z-index: 2;
}
/* When the link-variant tab is active, the badge inherits the
* darker outline of the now-bright pill bump the border to keep
* contrast against the brand-color fill. */
.sync-tab-button[data-link="true"].active::after {
border-color: rgba(0, 0, 0, 0.45);
}
/* Divider between Server Playlists and the rest slim it down so
* it doesn't dominate the compacted row. */
.sync-tab-divider {
height: 24px;
margin: 0 4px;
background: rgba(255, 255, 255, 0.08);
}
/* Responsive — collapse chip size on smaller widths to fit the row. */
@media (max-width: 1099px) {
.sync-tab-button {
width: 36px;
height: 36px;
}
.sync-tab-button .tab-icon { width: 18px; height: 18px; }
.sync-tab-button.active { padding: 0 12px 0 8px; }
.sync-tab-label { font-size: 11px; }
}
@media (max-width: 720px) {
.sync-tabs {
gap: 4px;
padding: 6px;
}
.sync-tab-button {
width: 32px;
height: 32px;
}
.sync-tab-button .tab-icon { width: 16px; height: 16px; }
.sync-tab-button[data-link="true"]::after {
width: 12px;
height: 12px;
border-width: 1px;
}
.sync-tab-divider { display: none; }
}
/* =====================================================================
* Auto-Sync Manager v2 visual overhaul
*