Files
minstrel/web/src/routes/library/hidden/+page.svelte
T
bvandeusenandClaude Opus 5.5 fdee77eaec
release / web (push) Successful in 1m22s
release / govulncheck (push) Successful in 38s
release / go (push) Successful in 1m48s
release / integration (push) Successful in 5m1s
release / android (push) Successful in 5m41s
release / Build signed APK (releases and dev) (push) Successful in 5m11s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 1m17s
release / Verify release artifacts (tag releases only) (push) Skipped
fix(web): text on a tint of its own hue uses the house -fg tokens (#3150)
A hue painted as text on a color-mix tint of itself sits close to the
surface under it. On Minstrel's surfaces the raw accent on its 15% tint
measures 1.97:1 at worst (dark mode, hover surface), against AA's 4.5.

- tokens.json gains colors.fg: the five FabledSword -fg formulas (accent
  45%, success 45%, warning, error and info 50%), each mixed toward
  parchment so one declaration serves both modes. Success is Minstrel's
  moss. tokens-to-css emits them in :root.
- Tailwind exposes them as text-accent-fg, text-warning-fg, text-error-fg
  and text-info-fg.
- 23 sites swapped: 14 Tailwind class strings (PlayerBar and the admin
  count pills) and 9 CSS rules (StatusPill's four tones and five accent
  chips). Worst case after: accent-fg 5.03, error-fg 4.75, warning-fg
  4.92, success-fg 4.85.
- scripts/check-tint-contrast.js finds the pair in either spelling. Its
  test scans src in the web Vitest lane and fails on any new site, with
  fixture cases showing it can fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 22:51:52 -04:00

106 lines
3.8 KiB
Svelte

<script lang="ts">
import { pageTitle } from '$lib/branding';
import { Music2, RotateCcw } from 'lucide-svelte';
import { useQueryClient } from '@tanstack/svelte-query';
import { createMyQuarantineQuery, unflagTrack } from '$lib/api/quarantine';
import { qk } from '$lib/api/queries';
import type { LidarrQuarantineMineRow, LidarrQuarantineReason } from '$lib/api/types';
import ApiErrorBanner from '$lib/components/ApiErrorBanner.svelte';
import { coverUrl } from '$lib/media/covers';
import { relativeTime } from '$lib/utils/relativeTime';
const client = useQueryClient();
const queryStore = createMyQuarantineQuery();
const query = $derived($queryStore);
const rows = $derived((query.data ?? []) as LidarrQuarantineMineRow[]);
const REASON_LABELS: Record<LidarrQuarantineReason, string> = {
bad_rip: 'Bad rip',
wrong_file: 'Wrong file',
wrong_tags: 'Wrong tags',
duplicate: 'Duplicate',
other: 'Other'
};
async function onUnhide(trackID: string) {
try {
await unflagTrack(trackID);
await client.invalidateQueries({ queryKey: qk.myQuarantine() });
} catch {
// Silent in v1; the SPA will refetch on next mount.
}
}
</script>
<svelte:head><title>{pageTitle('Library · Hidden')}</title></svelte:head>
<div class="space-y-6">
<header class="space-y-1">
<h2 class="font-display text-2xl font-medium text-text-primary">Hidden</h2>
<p class="text-text-secondary">Tracks you've flagged as broken.</p>
</header>
{#if query.isError}
<ApiErrorBanner error={query.error} onRetry={query.refetch} />
{:else if !query.isPending && rows.length === 0}
<p class="text-text-secondary">Nothing hidden yet.</p>
{:else if rows.length > 0}
<ul class="space-y-2">
{#each rows as row (row.track_id)}
<li class="flex items-start gap-3 rounded-md border border-border bg-surface p-3">
<!-- Album art / fallback -->
<div class="flex h-14 w-14 shrink-0 items-center justify-center rounded-md bg-surface-hover">
{#if row.album_cover_art_path}
<img
src={coverUrl(row.album_id)}
alt=""
class="h-full w-full rounded-md object-cover"
loading="lazy"
/>
{:else}
<Music2 size={24} strokeWidth={1} class="text-text-muted" />
{/if}
</div>
<!-- Body -->
<div class="flex flex-1 flex-col gap-1">
<div class="flex items-center gap-2">
<span class="kind-pill">Track</span>
<span class="kind-pill">{REASON_LABELS[row.reason]}</span>
</div>
<div class="text-sm text-text-primary">{row.track_title}</div>
<div class="text-xs text-text-secondary">
by {row.artist_name} · {row.album_title} · flagged {relativeTime(row.created_at)}
</div>
{#if row.notes}
<div class="text-xs italic text-text-secondary">{row.notes}</div>
{/if}
</div>
<!-- Action -->
<button
type="button"
aria-label={`Un-hide ${row.track_title}`}
onclick={() => onUnhide(row.track_id)}
class="inline-flex shrink-0 items-center gap-1 rounded-md border border-border px-2.5 py-1.5 text-sm text-text-secondary hover:text-text-primary"
>
<RotateCcw size={14} strokeWidth={1} />
Un-hide
</button>
</li>
{/each}
</ul>
{/if}
</div>
<style>
.kind-pill {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 999px;
font-size: 11px;
line-height: 14px;
background: color-mix(in srgb, var(--fs-accent) 15%, transparent);
color: var(--fs-accent-fg);
}
</style>