feat(web): copy-link button on owned public playlists (Tier C12)
test-web / test (push) Successful in 31s

Add a Link icon button to the playlist header that copies the
absolute /playlists/[id] URL to the clipboard. Visible only when
isOwner && pl.is_public. Falls back to a window.prompt() in
insecure contexts where navigator.clipboard is unavailable.

Tests: button visibility (private vs public owner) + clipboard
write target.
This commit is contained in:
2026-06-01 14:13:05 -04:00
parent 859aff8d30
commit cc81e0f183
2 changed files with 50 additions and 1 deletions
+24 -1
View File
@@ -2,7 +2,7 @@
import { page } from '$app/state';
import { goto } from '$app/navigation';
import { pageTitle } from '$lib/branding';
import { Pencil, Trash2 } from 'lucide-svelte';
import { Pencil, Trash2, Link as LinkIcon } from 'lucide-svelte';
import { useQueryClient } from '@tanstack/svelte-query';
import PlaylistTrackRow from '$lib/components/PlaylistTrackRow.svelte';
import ApiErrorBanner from '$lib/components/ApiErrorBanner.svelte';
@@ -130,6 +130,18 @@
}
}
async function onCopyLink() {
const href = `${window.location.origin}/playlists/${id}`;
try {
await navigator.clipboard.writeText(href);
pushToast('Link copied to clipboard.');
} catch {
// navigator.clipboard fails in insecure contexts and older browsers.
// Fall back to selecting the link in a prompt so the user can copy.
window.prompt('Copy the playlist link:', href);
}
}
// --- System-playlist refresh (#411 R2: generic by-kind) ---
let refreshingSystem = $state(false);
@@ -188,6 +200,17 @@
{refreshingSystem ? 'Refreshing…' : 'Refresh'}
</button>
{/if}
{#if isOwner && pl.is_public}
<button
type="button"
onclick={onCopyLink}
aria-label="Copy public link to this playlist"
title="Copy link"
class="rounded p-2 text-text-muted hover:bg-surface-hover hover:text-text-primary"
>
<LinkIcon size={16} strokeWidth={1} />
</button>
{/if}
{#if isOwner}
<button
type="button"