release / govulncheck (push) Successful in 17s
release / web (push) Successful in 1m11s
release / go (push) Successful in 1m26s
release / integration (push) Successful in 4m18s
release / android (push) Successful in 4m44s
release / Build signed APK (releases and dev) (push) Successful in 4m53s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 26s
release / Verify release artifacts (tag releases only) (push) Skipped
The raw accent fails AA as text on every dark surface, not only on its own tint: 3.04:1 on the page, 2.70 on iron, 2.21 on slate, against 4.5. accent-fg (the house formula, 45% toward parchment) measures 5.62 at worst across both modes. The operator chose the readable colour over the signature teal for text, on 2026-10-08. - 36 sites swap. They are 35 Tailwind uses: links, "Now playing", the ingest progress line, active shuffle/repeat, the liked heart, the app download icon and its hover. The last is the alphabet rail's pending spinner in CSS. Icons follow the text: as graphics they need only 3:1, and the raw accent misses even that on iron. - check-tint-contrast adds accent to TEXT_NEVER_RAW, so a new raw text-accent or color: var(--fs-accent) fails the web lane. Run against the files before the swap, it finds all 36. Borders, rings and fills keep the raw accent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
95 lines
3.4 KiB
Svelte
95 lines
3.4 KiB
Svelte
<script lang="ts">
|
|
import { pageTitle } from '$lib/branding';
|
|
import { createArtistsQuery } from '$lib/api/queries';
|
|
import ArtistCard from '$lib/components/ArtistCard.svelte';
|
|
import AlphabeticalGrid from '$lib/components/AlphabeticalGrid.svelte';
|
|
import ApiErrorBanner from '$lib/components/ApiErrorBanner.svelte';
|
|
import InfiniteScrollSentinel from '$lib/components/InfiniteScrollSentinel.svelte';
|
|
import QuickFilter from '$lib/components/QuickFilter.svelte';
|
|
import EmptyState from '$lib/components/EmptyState.svelte';
|
|
import { useDelayed } from '$lib/utils/useDelayed.svelte';
|
|
import type { ArtistRef } from '$lib/api/types';
|
|
|
|
const queryStore = $derived(createArtistsQuery('alpha'));
|
|
const query = $derived($queryStore);
|
|
const artists = $derived(query.data?.pages?.flatMap((p) => p.items) ?? []);
|
|
const total = $derived(query.data?.pages?.[0]?.total ?? 0);
|
|
const showSkeleton = useDelayed(() => query.isPending);
|
|
|
|
let filter = $state('');
|
|
const filtered = $derived.by(() => {
|
|
const q = filter.trim().toLowerCase();
|
|
if (!q) return artists;
|
|
return artists.filter((a) => a.name.toLowerCase().includes(q));
|
|
});
|
|
</script>
|
|
|
|
<svelte:head><title>{pageTitle('Library · Artists')}</title></svelte:head>
|
|
|
|
<div class="space-y-4">
|
|
<header class="flex flex-wrap items-end justify-between gap-3">
|
|
<div>
|
|
<h1 class="font-display text-2xl font-medium text-text-primary">Artists</h1>
|
|
{#if !query.isPending && !query.isError}
|
|
<p class="text-sm text-text-secondary">
|
|
{total} {total === 1 ? 'artist' : 'artists'}
|
|
</p>
|
|
{/if}
|
|
</div>
|
|
{#if artists.length > 0}
|
|
<QuickFilter bind:value={filter} placeholder="Filter artists" />
|
|
{/if}
|
|
</header>
|
|
|
|
{#if query.isError}
|
|
<ApiErrorBanner error={query.error} onRetry={query.refetch} />
|
|
{:else if showSkeleton.value && artists.length === 0}
|
|
<p class="text-text-secondary">Loading…</p>
|
|
{:else if !query.isPending && total === 0}
|
|
<EmptyState
|
|
title="No artists yet"
|
|
hint="Once a library folder has been scanned, artists will appear here."
|
|
>
|
|
{#snippet actions()}
|
|
<a
|
|
href="/settings"
|
|
class="inline-flex items-center rounded-md bg-action-secondary px-4 py-2 text-sm text-action-fg hover:opacity-90"
|
|
>
|
|
Open settings
|
|
</a>
|
|
{/snippet}
|
|
</EmptyState>
|
|
{:else if filter.trim() && filtered.length === 0}
|
|
<p class="text-text-secondary">
|
|
No artists in your loaded library match
|
|
<span class="font-medium">'{filter.trim()}'</span>.
|
|
<a href={`/search/artists?q=${encodeURIComponent(filter.trim())}`} class="text-accent-fg hover:underline">
|
|
Search the full library →
|
|
</a>
|
|
</p>
|
|
{:else}
|
|
<AlphabeticalGrid
|
|
items={filtered}
|
|
getKey={(a: ArtistRef) => a.sort_name || a.name}
|
|
hasMore={!!query.hasNextPage}
|
|
onLoadMore={() => query.fetchNextPage()}
|
|
>
|
|
{#snippet item(a: ArtistRef)}
|
|
<ArtistCard artist={a} />
|
|
{/snippet}
|
|
</AlphabeticalGrid>
|
|
|
|
{#if query.hasNextPage}
|
|
<InfiniteScrollSentinel
|
|
enabled={!query.isFetchingNextPage}
|
|
onIntersect={() => query.fetchNextPage()}
|
|
/>
|
|
{#if query.isFetchingNextPage}
|
|
<p class="py-2 text-center text-sm text-text-secondary">Loading more…</p>
|
|
{/if}
|
|
{:else if artists.length > 0}
|
|
<p class="py-2 text-center text-sm text-text-secondary">End of library</p>
|
|
{/if}
|
|
{/if}
|
|
</div>
|