feat(search): frontend content search (text + warnings) + manga warning banner

Surfaces the analysis pipeline in the UI:

- api clients: searchPages() + PageSearchItem/ContentWarning types;
  listMangas cwInclude/cwExclude; MangaDetail.content_warnings.
- /search gains a content-search mode: free-text over OCR+scene and
  tri-state content-warning toggles (include/exclude), driven by URL
  params (text, cw_include, cw_exclude) and the new /me/page-search
  endpoint. Tag browsing is preserved when no content filter is active.
- manga detail renders a deduped content-warning banner.

Tests: vitest param serialization (searchPages CSV/text/cw, listMangas
cw); Playwright text-search + cw-toggle flows (route-mocked). svelte-check
+ build clean; full vitest (258) + search e2e (7) green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-13 19:12:34 +02:00
parent bd39476ac7
commit d36f24e9af
11 changed files with 394 additions and 4 deletions

View File

@@ -14,6 +14,7 @@
import { resyncManga } from '$lib/api/admin';
import { chapterLabel } from '$lib/api/chapters';
import { listTags, type Tag } from '$lib/api/tags';
import type { ContentWarning } from '$lib/api/page_tags';
import { session } from '$lib/session.svelte';
import Chip from '$lib/components/Chip.svelte';
import MangaCard from '$lib/components/MangaCard.svelte';
@@ -62,6 +63,8 @@
const authors = $derived<AuthorRef[]>(manga.authors);
const genres = $derived<GenreRef[]>(manga.genres);
/** Deduped content warnings across the manga's pages (analysis worker). */
const contentWarnings = $derived<ContentWarning[]>(manga.content_warnings ?? []);
/** Up to 5 mangas with the most similar tags; empty hides the section. */
const similar = $derived(data.similar);
@@ -395,6 +398,15 @@
</details>
{/if}
{#if contentWarnings.length > 0}
<div class="content-warnings" data-testid="manga-content-warnings" role="note">
<span class="cw-label">⚠ Content warning</span>
{#each contentWarnings as w (w)}
<span class="cw-chip">{w}</span>
{/each}
</div>
{/if}
{#if genres.length > 0}
<div class="chip-row" data-testid="manga-genres">
<span class="chip-row-label">Genres</span>
@@ -785,6 +797,33 @@
position: relative;
}
.content-warnings {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-2);
margin: var(--space-3) 0;
padding: var(--space-2) var(--space-3);
border: 1px solid color-mix(in srgb, #d4762a 50%, transparent);
border-radius: var(--radius-md, 8px);
background: color-mix(in srgb, #d4762a 12%, transparent);
}
.cw-label {
font-size: var(--font-sm);
font-weight: 600;
color: #b85e1a;
}
.cw-chip {
font-size: var(--font-sm);
padding: 2px var(--space-2);
border-radius: var(--radius-sm, 4px);
background: color-mix(in srgb, #d4762a 22%, transparent);
color: var(--text);
text-transform: capitalize;
}
.tag-form {
display: inline-flex;
align-items: center;