dj sets + lyrics: a mix that names its own tracks, and words pulled off the separated voice

This commit is contained in:
KeelrLeek committed 2026-08-24 10:49:10 -05:00
1 parent 4efe85b6bf
commit 1dd8573808
41 files changed
+9288 -647

No files matched your search

+121
View File
@@ -51,6 +51,7 @@
import { openPreview } from "../lib/preview/previewSession.svelte.js";
import { traceToSvg, svgDataUrl } from "../lib/trace.js";
import Icon from "../components/Icon.svelte";
import PlaylistMenu from "../components/PlaylistMenu.svelte";
import { consumeScroll, restoreScroll } from "../lib/navigation.svelte.js";
import HeroGpuCanvas from "../components/HeroGpuCanvas.svelte";
@@ -5071,6 +5072,19 @@
<span class="track-album">{track.album}</span>
<span class="lis" class:unknown={track.listeners == null} title={track.listeners == null ? "No Last.fm listener data for this track" : `${track.listeners.toLocaleString()} Last.fm listeners`}>{fmtListeners(track.listeners)}</span>
<span class="tdur">{fmtDuration(track.duration_secs)}</span>
<PlaylistMenu
compact
iconSize={13}
align="right"
tracks={() => [{
title: track.title,
artist: page.artist.name,
album: track.album ?? null,
duration_secs: track.duration_secs ?? null,
trackId: track.id ?? null,
artUrl: track.cover ?? null,
}]}
/>
<button
class="mini-get"
title="Save to library"
@@ -5425,6 +5439,19 @@
<span class="tdur">{fmtDuration(t.duration_secs)}</span>
{/if}
{#if !locked}
<PlaylistMenu
compact
iconSize={13}
align="right"
tracks={() => [{
title: t.title,
artist: page.artist.name,
album: openAlbum?.title ?? null,
duration_secs: t.duration_secs ?? null,
trackId: t.id ?? null,
artUrl: t.cover ?? openAlbum?.cover ?? null,
}]}
/>
<button
class="mini-btn play"
title="Play"
@@ -7633,6 +7660,11 @@
}
.popular-row {
/* The anchor the absolute row actions were always written against —
without it the save button resolved against the page and its
reveal rules never existed, so it was dead UI. The 24px last
column is the space the overlaid actions land on. */
position: relative;
display: grid;
grid-template-columns: 26px 40px minmax(130px, 1fr) minmax(100px, 0.75fr) 62px 40px 24px;
align-items: center;
@@ -7647,6 +7679,56 @@
background: var(--bg-highlight);
}
/* Hover slides the actions over the right end and the duration gets out
of the way — the album page's treatment. The listener count stays: the
list is ordered by it. */
.popular-row :global(.plm) {
position: absolute;
top: 50%;
right: 36px;
transform: translateY(-50%);
opacity: 0;
transition: opacity 0.12s ease;
}
.popular-row :global(.plm-trigger) {
display: grid;
place-items: center;
width: 26px;
height: 26px;
border-radius: 50%;
color: var(--text-secondary);
}
.popular-row :global(.plm-trigger:hover) {
color: var(--accent-hover);
background: rgba(255, 255, 255, 0.08);
}
.popular-row:hover .mini-get,
.popular-row:focus-within .mini-get,
.popular-row:hover :global(.plm),
.popular-row:focus-within :global(.plm) {
opacity: 1;
}
.popular-row:hover .tdur,
.popular-row:focus-within .tdur {
/* pointer-events too: the faded duration is a stacking context and
must never eat clicks meant for the buttons over it. */
opacity: 0;
pointer-events: none;
}
/* While its menu is open the pointer is off in the portaled dropdown, so
hover alone would drop the cluster mid-interaction. */
.popular-row:has(:global(.plm-trigger[aria-expanded="true"])) :global(.plm),
.popular-row:has(:global(.plm-trigger[aria-expanded="true"])) .mini-get {
opacity: 1;
}
.popular-row:has(:global(.plm-trigger[aria-expanded="true"])) .tdur {
opacity: 0;
pointer-events: none;
}
.popular-row:has(:global(.plm-trigger[aria-expanded="true"])) {
background: var(--bg-highlight);
}
.popular-row.now .rank,
.popular-row.now .track-name {
color: var(--accent-hover);
@@ -8984,6 +9066,45 @@
.trow:hover .tdur,
.trow:focus-within .tdur {
opacity: 0;
pointer-events: none;
}
/* Add-to-playlist joins the expander row's overlay pair, one slot left of
Play; its menu is portaled, so hover alone would drop the cluster while
the menu is open — the aria-expanded rules hold it. */
.trow :global(.plm) {
position: absolute;
top: 50%;
right: 64px;
transform: translateY(-50%);
opacity: 0;
transition: opacity 0.12s ease;
}
.trow :global(.plm-trigger) {
display: grid;
place-items: center;
width: 26px;
height: 26px;
border-radius: 50%;
color: var(--text-secondary);
}
.trow :global(.plm-trigger:hover) {
color: var(--accent-hover);
background: rgba(255, 255, 255, 0.08);
}
.trow:hover :global(.plm),
.trow:focus-within :global(.plm),
.trow:has(:global(.plm-trigger[aria-expanded="true"])) :global(.plm),
.trow:has(:global(.plm-trigger[aria-expanded="true"])) .mini-btn,
.trow:has(:global(.plm-trigger[aria-expanded="true"])) .mini-get {
opacity: 1;
}
.trow:has(:global(.plm-trigger[aria-expanded="true"])) .tdur {
opacity: 0;
pointer-events: none;
}
.trow:has(:global(.plm-trigger[aria-expanded="true"])) {
background: var(--bg-highlight);
}
.trow.locked {