Merges Renovate's kit 3 (PR #149) and adapter-static 4 (PR #148) bumps, plus the migration they need. The mechanical part is `sv migrate sveltekit-3`, run one task at a time and reviewed: - svelte.config.js is gone. Its options move into sveltekit() in vite.config.ts, exported as kitOptions so vitest.config.ts runs the same kit setup, including the $test-utils alias the tests import. - $lib becomes #lib through package.json "imports". There is no src/lib/index, so only the "#lib/*" entry is kept. - tsconfig extends $app/tsconfig. - Peer floors raised to kit 3's requirements: svelte ^5.57.1, vite ^8.0.12, svelte-check ^4.7.5. By hand, from the codemod's list of non-automated tasks: - goto's replaceState option is now replace; keepFocus becomes reset: false. For the search typeahead, reset: false also stops the scroll-to-top, which is wanted while typing. - The test setup mocks drop pushState/replaceState and $app/paths base/assets, which kit 3 removed, and mock refreshAll in place of invalidateAll. - The other flagged files only read page.url or goto internal routes, so they needed no change. TypeScript goes to ^6, not the ^7 Renovate offers: kit 3 declares typescript ^6 as a peer and svelte-check 4.7 accepts ^5 || ^6. Move to 7 once both accept it. With Tailwind 4 and kit 3 in, `npm audit` on the whole tree reports 0, so the web lane now audits every dependency rather than only what ships to browsers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
130 lines
4.2 KiB
Svelte
130 lines
4.2 KiB
Svelte
<script lang="ts">
|
||
import { X, ListPlus, Plus, ListMusic, Heart } from 'lucide-svelte';
|
||
import { useQueryClient } from '@tanstack/svelte-query';
|
||
import { selection, clearSelection } from '#lib/selection/store.svelte.js';
|
||
import { playNextMany, enqueueTracks } from '#lib/player/store.svelte.js';
|
||
import { likeEntity, createLikedIdsQuery } from '#lib/api/likes.js';
|
||
import AddToPlaylistMenu from './AddToPlaylistMenu.svelte';
|
||
|
||
const queryClient = useQueryClient();
|
||
const likedQ = $derived(createLikedIdsQuery());
|
||
const likedIds = $derived($likedQ?.data?.track_ids ?? []);
|
||
|
||
let addOpen = $state(false);
|
||
let busy = $state(false);
|
||
|
||
function onPlayNext() {
|
||
playNextMany(selection.tracks);
|
||
clearSelection();
|
||
}
|
||
|
||
function onAddQueue() {
|
||
enqueueTracks(selection.tracks);
|
||
clearSelection();
|
||
}
|
||
|
||
async function onLikeAll() {
|
||
if (busy) return;
|
||
busy = true;
|
||
try {
|
||
const likedSet = new Set(likedIds);
|
||
const toLike = selection.tracks.filter((t) => !likedSet.has(t.id));
|
||
for (const t of toLike) {
|
||
await likeEntity(queryClient, 'track', t.id);
|
||
}
|
||
clearSelection();
|
||
} finally {
|
||
busy = false;
|
||
}
|
||
}
|
||
|
||
function onClose() {
|
||
addOpen = false;
|
||
}
|
||
</script>
|
||
|
||
{#if selection.active}
|
||
<!-- Sits above PlayerBar (z-30 < 40). Anchors to the viewport bottom
|
||
with bottom-24 so PlayerBar's ~80px shell stays visible. The bar
|
||
is dismissed by the × button or by Escape (handled in layout). -->
|
||
<div
|
||
role="region"
|
||
aria-label="Selection actions"
|
||
class="pointer-events-none fixed inset-x-0 bottom-24 z-30 flex justify-center px-4"
|
||
>
|
||
<div
|
||
class="pointer-events-auto flex items-center gap-2 rounded-full border border-border
|
||
bg-surface px-3 py-2 shadow-lg"
|
||
>
|
||
<span class="px-2 text-sm text-text-primary">
|
||
{selection.count} selected
|
||
</span>
|
||
<span class="h-5 w-px bg-border"></span>
|
||
|
||
<button
|
||
type="button"
|
||
onclick={onPlayNext}
|
||
aria-label="Play selected next"
|
||
title="Play next"
|
||
class="flex items-center gap-1.5 rounded px-3 py-1.5 text-sm text-text-secondary hover:text-text-primary focus-visible:ring-2 focus-visible:ring-accent"
|
||
>
|
||
<ListPlus size={16} strokeWidth={1.5} />
|
||
<span class="hidden sm:inline">Play next</span>
|
||
</button>
|
||
|
||
<button
|
||
type="button"
|
||
onclick={onAddQueue}
|
||
aria-label="Add selected to queue"
|
||
title="Add to queue"
|
||
class="flex items-center gap-1.5 rounded px-3 py-1.5 text-sm text-text-secondary hover:text-text-primary focus-visible:ring-2 focus-visible:ring-accent"
|
||
>
|
||
<Plus size={16} strokeWidth={1.5} />
|
||
<span class="hidden sm:inline">Add to queue</span>
|
||
</button>
|
||
|
||
<button
|
||
type="button"
|
||
onclick={() => (addOpen = !addOpen)}
|
||
aria-haspopup="menu"
|
||
aria-expanded={addOpen}
|
||
aria-label="Add selected to playlist"
|
||
title="Add to playlist"
|
||
class="flex items-center gap-1.5 rounded px-3 py-1.5 text-sm text-text-secondary hover:text-text-primary focus-visible:ring-2 focus-visible:ring-accent"
|
||
>
|
||
<ListMusic size={16} strokeWidth={1.5} />
|
||
<span class="hidden sm:inline">Add to playlist</span>
|
||
</button>
|
||
|
||
<button
|
||
type="button"
|
||
onclick={onLikeAll}
|
||
disabled={busy}
|
||
aria-label="Like all selected"
|
||
title="Like all"
|
||
class="flex items-center gap-1.5 rounded px-3 py-1.5 text-sm text-text-secondary hover:text-text-primary focus-visible:ring-2 focus-visible:ring-accent disabled:opacity-50"
|
||
>
|
||
<Heart size={16} strokeWidth={1.5} />
|
||
<span class="hidden sm:inline">Like all</span>
|
||
</button>
|
||
|
||
<span class="h-5 w-px bg-border"></span>
|
||
<button
|
||
type="button"
|
||
onclick={clearSelection}
|
||
aria-label="Clear selection"
|
||
title="Clear selection"
|
||
class="rounded p-1.5 text-text-secondary hover:text-text-primary focus-visible:ring-2 focus-visible:ring-accent"
|
||
>
|
||
<X size={16} strokeWidth={1.5} />
|
||
</button>
|
||
</div>
|
||
|
||
{#if addOpen}
|
||
<div class="pointer-events-auto absolute bottom-full mb-2">
|
||
<AddToPlaylistMenu tracks={selection.tracks} {onClose} />
|
||
</div>
|
||
{/if}
|
||
</div>
|
||
{/if}
|