refactor(cards): Phase A + C — adopt @mana/shared-theme + per-app accent

Phase A — Cards joins the unified theme system:
- Drop placeholder --color-cards-* palette; app.css imports
  @mana/shared-tailwind/themes.css + sources.css.
- Remove hardcoded class="dark" from app.html; body uses
  bg-background text-foreground.
- New $lib/stores/theme.ts: createThemeStore({ appId: 'cards' }).
  ThemeToggle from @mana/shared-theme-ui in the header next to
  the streak chip.
- Sweep all neutral / red / emerald / amber / indigo utilities in
  apps/cards/apps/web/src to semantic tokens (560 substitutions
  across 19 files): bg-neutral-900 → bg-card, text-neutral-400 →
  text-muted-foreground, bg-red-500 → bg-error, etc. Domain
  literals kept (FSRS grade colors red/orange/green/blue, GitHub-
  violet PR-merged badge, marketplace-amber Buy button, admin-
  inbox category palette).
- Cards added to validate-theme-utilities scope so future drift
  fails CI.

Phase C — per-app accent token:
- New --color-app-accent in shared-tailwind/themes.css. Theme-
  agnostic (registered in validate-theme-parity's THEME_AGNOSTIC
  regex), so it stays the same across light/dark/lume/etc. Defaults
  to Mana indigo at :root.
- Cards layout writes 258 90% 66% (= #8b5cf6 violet, from
  MANA_APPS.cards.color) onto documentElement at boot via
  applyCardsAccent(). All Cards CTAs (Lernen, Abonnieren, Senden,
  links inside cloze cards) flow through bg-app-accent /
  text-app-accent now.

Net effect: Cards gets light/dark + 4 palette variants + a11y
toggles for free, and any future app can drop in by setting its
own --color-app-accent without touching shared-tailwind.
This commit is contained in:
Till JS 2026-05-08 01:54:16 +02:00
parent 863311eefa
commit ad3b99fe6d
28 changed files with 589 additions and 583 deletions

View file

@ -37,6 +37,7 @@
"@mana/shared-stores": "workspace:*",
"@mana/shared-tailwind": "workspace:*",
"@mana/shared-theme": "workspace:*",
"@mana/shared-theme-ui": "workspace:*",
"@mana/shared-types": "workspace:*",
"@mana/shared-utils": "workspace:*",
"dexie": "^4.4.1",

View file

@ -1,33 +1,32 @@
@import 'tailwindcss';
@import '@mana/shared-tailwind/themes.css';
@import '@mana/shared-tailwind/sources.css';
/* Phase-1 placeholder palette. Will swap for @mana/shared-theme tokens
once the theming pass lands in Etappe 3c. */
@theme {
--color-cards-bg: #0a0a0a;
--color-cards-surface: #161616;
--color-cards-border: #2a2a2a;
--color-cards-fg: #f5f5f5;
--color-cards-muted: #a3a3a3;
--color-cards-accent: #6366f1;
}
/* Phase A Cards now lives on the unified @mana/shared-theme tokens.
The placeholder --color-cards-* palette is gone; everything goes
through `--color-{background,foreground,surface,muted,}` from
shared-tailwind. The runtime `createThemeStore({ appId: 'cards' })`
in +layout.svelte writes the live variant + mode onto the
document. */
/* Cloze rendering classes — produced by @mana/cards-core/render. */
/* Cloze rendering produced by @mana/cards-core/render. Uses the
active app accent so the highlight follows the Cards brand. */
.cloze-blank {
background: rgba(99, 102, 241, 0.15);
background: hsl(var(--color-app-accent) / 0.18);
border-radius: 0.25rem;
padding: 0.05rem 0.4rem;
color: #a5b4fc;
color: hsl(var(--color-app-accent));
font-style: italic;
}
mark.cloze-active {
background: rgba(34, 197, 94, 0.2);
color: #86efac;
background: hsl(var(--color-success) / 0.2);
color: hsl(var(--color-success));
padding: 0.05rem 0.25rem;
border-radius: 0.25rem;
}
/* Minimal styling for HTML produced by marked() Tailwind 4 ships
/* Minimal styling for HTML produced by marked() Tailwind v4 ships
without typography plugin so we set the basics by hand. */
.card-content :where(p, ul, ol) {
margin-block: 0.5rem;
@ -41,19 +40,19 @@ mark.cloze-active {
padding-inline-start: 1.25rem;
}
.card-content :where(code) {
background: rgba(255, 255, 255, 0.06);
background: hsl(var(--color-muted) / 0.6);
padding: 0.1rem 0.3rem;
border-radius: 0.25rem;
font-size: 0.95em;
}
.card-content :where(pre) {
background: rgba(255, 255, 255, 0.04);
background: hsl(var(--color-muted) / 0.4);
padding: 0.75rem;
border-radius: 0.5rem;
overflow-x: auto;
}
.card-content :where(a) {
color: #818cf8;
color: hsl(var(--color-app-accent));
text-decoration: underline;
}
.card-content :where(strong) {

View file

@ -1,5 +1,5 @@
<!doctype html>
<html lang="de" class="dark">
<html lang="de">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.svg" />
@ -8,7 +8,7 @@
<meta name="description" content="Cards — Karteikarten mit Spaced Repetition." />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover" class="min-h-screen bg-neutral-950 text-neutral-100 antialiased">
<body data-sveltekit-preload-data="hover" class="min-h-screen bg-background text-foreground antialiased">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View file

@ -101,12 +101,12 @@
}
</script>
<div class="rounded-xl border border-indigo-500/30 bg-neutral-900 p-4">
<div class="rounded-xl border border-indigo-500/30 bg-card p-4">
<div class="mb-2 flex items-center justify-between">
<span class="text-sm font-medium">✨ Karten aus Text generieren</span>
{#if stage !== 'idle'}
<button
class="text-xs text-neutral-500 hover:text-neutral-300"
class="text-xs text-muted-foreground/80 hover:text-foreground/80"
onclick={stage === 'generating' ? cancelGenerate : reset}
>
{stage === 'generating' ? 'Abbrechen' : 'Zurücksetzen'}
@ -118,25 +118,25 @@
<textarea
bind:value={source}
placeholder="Text einfügen — Notizen, Lehrbuch-Absatz, Definition…"
class="min-h-[120px] w-full rounded-lg border border-neutral-700 bg-neutral-950 px-3 py-2 text-sm outline-none focus:border-indigo-400"
class="min-h-[120px] w-full rounded-lg border border-border-strong bg-background px-3 py-2 text-sm outline-none focus:border-indigo-400"
></textarea>
{#if stage === 'error' && error}
<p class="mt-2 text-sm text-red-400">{error}</p>
<p class="mt-2 text-sm text-error">{error}</p>
{/if}
<div class="mt-2 flex items-center justify-between gap-3 text-xs text-neutral-500">
<div class="mt-2 flex items-center justify-between gap-3 text-xs text-muted-foreground/80">
<div class="flex items-center gap-3">
<span>{source.length} Zeichen</span>
{#if pdfStatus}<span class="text-indigo-300">📄 {pdfStatus}</span>{/if}
{#if pdfStatus}<span class="text-app-accent">📄 {pdfStatus}</span>{/if}
</div>
<div class="flex items-center gap-2">
<button
class="rounded-lg border border-neutral-700 px-3 py-1.5 text-neutral-300 hover:bg-neutral-800"
class="rounded-lg border border-border-strong px-3 py-1.5 text-foreground/80 hover:bg-muted"
onclick={() => pdfPicker?.click()}
>
📄 PDF laden
</button>
<button
class="rounded-lg bg-indigo-500 px-4 py-1.5 text-sm text-white hover:bg-indigo-400 disabled:opacity-50"
class="rounded-lg bg-app-accent px-4 py-1.5 text-sm text-white hover:bg-app-accent/90 disabled:opacity-50"
onclick={handleGenerate}
disabled={!source.trim()}
>
@ -152,17 +152,17 @@
onchange={handlePdfPick}
/>
{:else if stage === 'reading-pdf'}
<div class="py-6 text-center text-sm text-neutral-400">{pdfStatus ?? 'Lese PDF…'}</div>
<div class="py-6 text-center text-sm text-muted-foreground">{pdfStatus ?? 'Lese PDF…'}</div>
{:else if stage === 'generating'}
<div class="py-6 text-center text-sm text-neutral-400">Modell denkt nach…</div>
<div class="py-6 text-center text-sm text-muted-foreground">Modell denkt nach…</div>
{:else if stage === 'preview'}
<div class="space-y-2 text-sm">
<div class="text-neutral-300">
<div class="text-foreground/80">
{generated.length} Karten generiert. Wähle aus, was übernommen werden soll:
</div>
<ul class="max-h-72 space-y-1 overflow-y-auto rounded-lg border border-neutral-800 p-2">
<ul class="max-h-72 space-y-1 overflow-y-auto rounded-lg border border-border p-2">
{#each generated as card, i (i)}
<li class="flex items-start gap-2 rounded-md p-1 hover:bg-neutral-800/50">
<li class="flex items-start gap-2 rounded-md p-1 hover:bg-muted/50">
<input
type="checkbox"
bind:checked={selected[i]}
@ -170,27 +170,27 @@
id="ai-card-{i}"
/>
<label for="ai-card-{i}" class="min-w-0 flex-1 cursor-pointer">
<div class="font-medium text-neutral-100">{card.front}</div>
<div class="text-xs text-neutral-400">{card.back}</div>
<div class="font-medium text-foreground">{card.front}</div>
<div class="text-xs text-muted-foreground">{card.back}</div>
</label>
</li>
{/each}
</ul>
<div class="flex justify-end gap-2 pt-1">
<button
class="rounded-lg px-3 py-1.5 text-sm text-neutral-400 hover:text-neutral-100"
class="rounded-lg px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground"
onclick={() => (selected = selected.map(() => true))}
>
Alle
</button>
<button
class="rounded-lg px-3 py-1.5 text-sm text-neutral-400 hover:text-neutral-100"
class="rounded-lg px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground"
onclick={() => (selected = selected.map(() => false))}
>
Keine
</button>
<button
class="rounded-lg bg-indigo-500 px-4 py-1.5 text-sm text-white hover:bg-indigo-400 disabled:opacity-50"
class="rounded-lg bg-app-accent px-4 py-1.5 text-sm text-white hover:bg-app-accent/90 disabled:opacity-50"
onclick={handleConfirm}
disabled={!selected.some(Boolean)}
>
@ -199,10 +199,10 @@
</div>
</div>
{:else if stage === 'creating'}
<div class="py-6 text-center text-sm text-neutral-400">Lege Karten an…</div>
<div class="py-6 text-center text-sm text-muted-foreground">Lege Karten an…</div>
{:else if stage === 'done'}
<div class="text-sm text-green-400">{createdCount} Karten angelegt.</div>
<button class="mt-2 text-xs text-neutral-500 hover:text-neutral-300" onclick={reset}>
<button class="mt-2 text-xs text-muted-foreground/80 hover:text-foreground/80" onclick={reset}>
Weiteren Text generieren
</button>
{/if}

View file

@ -69,20 +69,20 @@
}
</script>
<div class="rounded-xl border border-neutral-800 bg-neutral-900 p-4">
<div class="rounded-xl border border-border bg-card p-4">
<div class="mb-2 text-sm font-medium">Aus Anki importieren</div>
{#if stage === 'idle'}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div
class="rounded-lg border-2 border-dashed border-neutral-700 px-4 py-6 text-center text-sm text-neutral-400 transition-colors hover:border-indigo-400 hover:text-neutral-200"
class="rounded-lg border-2 border-dashed border-border-strong px-4 py-6 text-center text-sm text-muted-foreground transition-colors hover:border-indigo-400 hover:text-foreground/90"
ondragover={(e) => e.preventDefault()}
ondrop={onDrop}
onclick={() => fileInput?.click()}
>
<div class="mb-1">📦 .apkg-Datei hier ablegen oder klicken</div>
<div class="text-xs text-neutral-500">
<div class="text-xs text-muted-foreground/80">
Basic, Basic + Reverse, Cloze · Bilder + Audio werden mit übernommen.
</div>
</div>
@ -94,25 +94,25 @@
onchange={onPick}
/>
{:else if stage === 'parsing'}
<div class="py-6 text-center text-sm text-neutral-400">Lese {fileName}</div>
<div class="py-6 text-center text-sm text-muted-foreground">Lese {fileName}</div>
{:else if stage === 'preview' && parsed}
<div class="space-y-2 text-sm">
<div>
<span class="text-neutral-400">Gefunden in</span>
<code class="rounded bg-neutral-800 px-1 text-xs">{fileName}</code>:
<span class="text-muted-foreground">Gefunden in</span>
<code class="rounded bg-muted px-1 text-xs">{fileName}</code>:
</div>
<ul class="ml-4 list-disc text-neutral-300">
<ul class="ml-4 list-disc text-foreground/80">
<li>{parsed.decks.length} {parsed.decks.length === 1 ? 'Deck' : 'Decks'}</li>
<li>{parsed.cards.length} {parsed.cards.length === 1 ? 'Karte' : 'Karten'}</li>
{#if mediaCount > 0}
<li>{mediaCount} Medien (Bilder/Audio)</li>
{/if}
{#if parsed.skipped > 0}
<li class="text-amber-400">{parsed.skipped} übersprungen (unbekannter Typ)</li>
<li class="text-warning">{parsed.skipped} übersprungen (unbekannter Typ)</li>
{/if}
</ul>
{#if parsed.warnings.length > 0}
<details class="text-xs text-neutral-500">
<details class="text-xs text-muted-foreground/80">
<summary class="cursor-pointer">Hinweise ({parsed.warnings.length})</summary>
<ul class="mt-1 list-disc pl-4">
{#each parsed.warnings.slice(0, 10) as w (w)}<li>{w}</li>{/each}
@ -121,13 +121,13 @@
{/if}
<div class="flex justify-end gap-2 pt-2">
<button
class="rounded-lg px-3 py-1.5 text-sm text-neutral-400 hover:text-neutral-100"
class="rounded-lg px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground"
onclick={reset}
>
Abbrechen
</button>
<button
class="rounded-lg bg-indigo-500 px-4 py-1.5 text-sm text-white hover:bg-indigo-400"
class="rounded-lg bg-app-accent px-4 py-1.5 text-sm text-white hover:bg-app-accent/90"
onclick={confirmImport}
>
Importieren
@ -135,11 +135,11 @@
</div>
</div>
{:else if stage === 'uploading-media'}
<div class="py-6 text-center text-sm text-neutral-400">
<div class="py-6 text-center text-sm text-muted-foreground">
<div>Lade Medien hoch · {mediaProgress.uploaded} / {mediaProgress.total}</div>
<div class="mx-auto mt-3 h-1 w-48 overflow-hidden rounded-full bg-neutral-800">
<div class="mx-auto mt-3 h-1 w-48 overflow-hidden rounded-full bg-muted">
<div
class="h-full bg-indigo-500 transition-all"
class="h-full bg-app-accent transition-all"
style="width: {mediaProgress.total === 0
? 0
: (mediaProgress.uploaded / mediaProgress.total) * 100}%"
@ -147,7 +147,7 @@
</div>
</div>
{:else if stage === 'importing'}
<div class="py-6 text-center text-sm text-neutral-400">
<div class="py-6 text-center text-sm text-muted-foreground">
Importiere {parsed?.cards.length ?? 0} Karten…
</div>
{:else if stage === 'done' && result}
@ -157,17 +157,17 @@
{result.decksCreated === 1 ? 'Deck' : 'Decks'} angelegt.
</div>
{#if result.mediaUploaded > 0 || result.mediaFailed > 0}
<div class="text-neutral-400">
<div class="text-muted-foreground">
{result.mediaUploaded} Medien übernommen{#if result.mediaFailed > 0}
<span class="text-amber-400">· {result.mediaFailed} fehlgeschlagen</span>
<span class="text-warning">· {result.mediaFailed} fehlgeschlagen</span>
{/if}
</div>
{/if}
{#if result.failed > 0}
<div class="text-amber-400">{result.failed} Karten konnten nicht angelegt werden.</div>
<div class="text-warning">{result.failed} Karten konnten nicht angelegt werden.</div>
{/if}
<button
class="rounded-lg px-3 py-1.5 text-sm text-neutral-400 hover:text-neutral-100"
class="rounded-lg px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground"
onclick={reset}
>
Weitere Datei
@ -175,9 +175,9 @@
</div>
{:else if stage === 'error'}
<div class="space-y-2 text-sm">
<div class="text-red-400">Fehler: {error}</div>
<div class="text-error">Fehler: {error}</div>
<button
class="rounded-lg px-3 py-1.5 text-sm text-neutral-400 hover:text-neutral-100"
class="rounded-lg px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground"
onclick={reset}
>
Erneut versuchen

View file

@ -62,35 +62,35 @@
}
</script>
<aside class="mt-4 rounded-xl border border-neutral-800 bg-neutral-950 p-4">
<aside class="mt-4 rounded-xl border border-border bg-background p-4">
<header class="mb-2 flex items-center justify-between">
<h3 class="text-xs font-semibold uppercase tracking-wide text-neutral-500">
<h3 class="text-xs font-semibold uppercase tracking-wide text-muted-foreground/80">
Diskussion {comments.length > 0 ? `(${comments.length})` : ''}
</h3>
{#if loading}
<span class="text-xs text-neutral-600">Lädt…</span>
<span class="text-xs text-muted-foreground/60">Lädt…</span>
{/if}
</header>
{#if error}
<p class="mb-2 text-xs text-red-400">{error}</p>
<p class="mb-2 text-xs text-error">{error}</p>
{/if}
{#if comments.length === 0 && !loading}
<p class="text-xs text-neutral-600">Noch keine Kommentare zu dieser Karte.</p>
<p class="text-xs text-muted-foreground/60">Noch keine Kommentare zu dieser Karte.</p>
{:else}
<ul class="space-y-2">
{#each comments as c (c.id)}
<li class="rounded-lg border border-neutral-800 bg-neutral-900 p-2 text-sm">
<li class="rounded-lg border border-border bg-card p-2 text-sm">
<div class="flex items-start justify-between gap-2">
<p class="whitespace-pre-line text-neutral-200">{c.body}</p>
<p class="whitespace-pre-line text-foreground/90">{c.body}</p>
<div class="flex shrink-0 items-center gap-2">
{#if authStore.user?.id !== c.authorUserId}
<ReportButton {deckSlug} cardContentHash={c.cardContentHash} variant="icon" />
{/if}
{#if authStore.user?.id === c.authorUserId}
<button
class="text-xs text-neutral-600 hover:text-red-400"
class="text-xs text-muted-foreground/60 hover:text-error"
onclick={() => hide(c)}
title="Ausblenden"
aria-label="Ausblenden"
@ -100,7 +100,7 @@
{/if}
</div>
</div>
<p class="mt-1 text-xs text-neutral-600">
<p class="mt-1 text-xs text-muted-foreground/60">
{new Date(c.createdAt).toLocaleString('de-DE')}
</p>
</li>
@ -117,13 +117,13 @@
}}
>
<input
class="flex-1 rounded-lg border border-neutral-800 bg-neutral-900 px-3 py-1.5 text-sm"
class="flex-1 rounded-lg border border-border bg-card px-3 py-1.5 text-sm"
placeholder="Kommentar zur Karte…"
bind:value={draft}
disabled={posting}
/>
<button
class="rounded-lg bg-indigo-500 px-3 py-1.5 text-xs text-white hover:bg-indigo-400 disabled:opacity-50"
class="rounded-lg bg-app-accent px-3 py-1.5 text-xs text-white hover:bg-app-accent/90 disabled:opacity-50"
type="submit"
disabled={posting || !draft.trim()}
>

View file

@ -41,7 +41,7 @@
case 'cloze': {
const r = renderCloze(card.fields.text ?? '', subIndex);
const extra = card.fields.extra
? `<div class="mt-3 text-sm text-neutral-400">${renderMarkdown(card.fields.extra)}</div>`
? `<div class="mt-3 text-sm text-muted-foreground">${renderMarkdown(card.fields.extra)}</div>`
: '';
return { prompt: r.front + extra, answer: r.back + extra, expected: r.answer };
}
@ -57,15 +57,13 @@
</script>
<article class="space-y-4">
<div
class="card-content rounded-xl border border-neutral-800 bg-neutral-900 p-6 text-lg leading-relaxed"
>
<div class="card-content rounded-xl border border-border bg-card p-6 text-lg leading-relaxed">
{@html view.prompt}
</div>
{#if isTypeIn}
<input
class="w-full rounded-lg border border-neutral-700 bg-neutral-950 px-3 py-2 text-base outline-none focus:border-indigo-400"
class="w-full rounded-lg border border-border-strong bg-background px-3 py-2 text-base outline-none focus:border-indigo-400"
type="text"
placeholder="Antwort eingeben…"
value={typedAnswer}
@ -80,8 +78,8 @@
{isTypeIn
? matched
? 'border-green-500 bg-green-500/5'
: 'border-red-500 bg-red-500/5'
: 'border-indigo-500 bg-indigo-500/5'}"
: 'border-red-500 bg-error/5'
: 'border-indigo-500 bg-app-accent/5'}"
>
{@html view.answer}
</div>

View file

@ -53,20 +53,20 @@
<section class="mt-10">
<header class="mb-3 flex items-center justify-between">
<h2 class="text-sm font-semibold uppercase tracking-wide text-neutral-400">
<h2 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
Karten {cards.length > 0 ? `(${cards.length})` : ''}
</h2>
{#if loading}
<span class="text-xs text-neutral-600">Lädt…</span>
<span class="text-xs text-muted-foreground/60">Lädt…</span>
{/if}
</header>
{#if error}
<p class="mb-3 rounded-lg border border-red-500/30 bg-red-500/10 p-3 text-sm text-red-400">
<p class="mb-3 rounded-lg border border-error/30 bg-error/10 p-3 text-sm text-error">
{error}
</p>
{:else if cards.length === 0 && !loading}
<p class="rounded-xl border border-neutral-800 bg-neutral-900 p-4 text-sm text-neutral-500">
<p class="rounded-xl border border-border bg-card p-4 text-sm text-muted-foreground/80">
Diese Version enthält keine Karten.
</p>
{:else}
@ -74,18 +74,18 @@
{#each cards as c (c.contentHash)}
{@const n = counts[c.contentHash] ?? 0}
{@const isOpen = openHash === c.contentHash}
<li class="rounded-xl border border-neutral-800 bg-neutral-900 p-3">
<li class="rounded-xl border border-border bg-card p-3">
<button
class="flex w-full items-center justify-between gap-3 text-left"
onclick={() => (openHash = isOpen ? null : c.contentHash)}
>
<div class="min-w-0 flex-1">
<div class="text-xs uppercase tracking-wide text-neutral-500">
<div class="text-xs uppercase tracking-wide text-muted-foreground/80">
#{c.ord + 1} · {c.type}
</div>
<div class="mt-1 truncate text-sm text-neutral-200">{preview(c)}</div>
<div class="mt-1 truncate text-sm text-foreground/90">{preview(c)}</div>
</div>
<div class="shrink-0 text-xs text-neutral-500">
<div class="shrink-0 text-xs text-muted-foreground/80">
{#if n > 0}
💬 {n}
{:else}

View file

@ -8,8 +8,8 @@
let { decks, emptyText = 'Noch keine Decks.' }: Props = $props();
function badgeClass(d: DeckSummary): string {
if (d.owner.verifiedMana) return 'bg-emerald-500/15 text-emerald-300';
if (d.owner.verifiedCommunity) return 'bg-amber-500/15 text-amber-300';
if (d.owner.verifiedMana) return 'bg-success/15 text-success';
if (d.owner.verifiedCommunity) return 'bg-amber-500/15 text-warning';
return '';
}
@ -21,7 +21,7 @@
</script>
{#if decks.length === 0}
<p class="rounded-xl border border-neutral-800 bg-neutral-900 p-8 text-center text-sm text-neutral-400">
<p class="rounded-xl border border-border bg-card p-8 text-center text-sm text-muted-foreground">
{emptyText}
</p>
{:else}
@ -30,24 +30,24 @@
<li>
<a
href={`/d/${deck.slug}`}
class="block rounded-xl border border-neutral-800 bg-neutral-900 p-4 transition-colors hover:border-neutral-700 hover:bg-neutral-800"
class="block rounded-xl border border-border bg-card p-4 transition-colors hover:border-border-strong hover:bg-muted"
>
<div class="mb-1 flex items-start justify-between gap-3">
<h3 class="font-semibold leading-tight">{deck.title}</h3>
{#if deck.priceCredits > 0}
<span class="rounded-full bg-amber-500/15 px-2 py-0.5 text-xs text-amber-300">
<span class="rounded-full bg-amber-500/15 px-2 py-0.5 text-xs text-warning">
{deck.priceCredits} 💎
</span>
{/if}
</div>
{#if deck.description}
<p class="mb-2 line-clamp-2 text-xs text-neutral-400">{deck.description}</p>
<p class="mb-2 line-clamp-2 text-xs text-muted-foreground">{deck.description}</p>
{/if}
<div class="flex flex-wrap items-center gap-2 text-xs text-neutral-500">
<div class="flex flex-wrap items-center gap-2 text-xs text-muted-foreground/80">
<!-- Author shows as text inside the deck-link; the deck card
navigates to the deck page, the author profile is one
hop further from there. Keeps HTML valid (no nested <a>). -->
<span class="text-neutral-300">{deck.owner.displayName}</span>
<span class="text-foreground/80">{deck.owner.displayName}</span>
{#if badgeText(deck)}
<span class="rounded-full px-1.5 py-0.5 {badgeClass(deck)}">{badgeText(deck)}</span>
{/if}

View file

@ -135,28 +135,28 @@
<!-- svelte-ignore a11y_no_static_element_interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div
class="w-full max-w-lg rounded-xl border border-neutral-800 bg-neutral-900 p-6"
class="w-full max-w-lg rounded-xl border border-border bg-card p-6"
onclick={(e) => e.stopPropagation()}
>
<div class="mb-4 flex items-start justify-between">
<h2 class="text-xl font-semibold">Deck veröffentlichen</h2>
<button
onclick={onClose}
class="text-neutral-500 hover:text-neutral-200"
class="text-muted-foreground/80 hover:text-foreground/90"
aria-label="Schließen">✕</button
>
</div>
{#if stage === 'loading'}
<div class="py-8 text-center text-sm text-neutral-400">Lade Author-Profil…</div>
<div class="py-8 text-center text-sm text-muted-foreground">Lade Author-Profil…</div>
{:else if stage === 'become-author'}
<div class="space-y-4 text-sm">
<p class="text-neutral-300">
<p class="text-foreground/80">
Erstelle ein Author-Profil — andere User finden deine Decks unter
<code class="rounded bg-neutral-800 px-1 text-xs">cards.mana.how/u/dein-slug</code>.
<code class="rounded bg-muted px-1 text-xs">cards.mana.how/u/dein-slug</code>.
</p>
<div>
<label for="author-slug" class="mb-1 block text-xs text-neutral-400">
<label for="author-slug" class="mb-1 block text-xs text-muted-foreground">
Slug (360 Zeichen, az, 09, -)
</label>
<input
@ -164,35 +164,37 @@
type="text"
bind:value={authorSlug}
placeholder="anna-lang"
class="w-full rounded-lg border border-neutral-700 bg-neutral-950 px-3 py-2 text-sm outline-none focus:border-indigo-400"
class="w-full rounded-lg border border-border-strong bg-background px-3 py-2 text-sm outline-none focus:border-indigo-400"
/>
</div>
<div>
<label for="author-name" class="mb-1 block text-xs text-neutral-400">Anzeigename</label>
<label for="author-name" class="mb-1 block text-xs text-muted-foreground"
>Anzeigename</label
>
<input
id="author-name"
type="text"
bind:value={authorName}
placeholder="Anna Lang"
class="w-full rounded-lg border border-neutral-700 bg-neutral-950 px-3 py-2 text-sm outline-none focus:border-indigo-400"
class="w-full rounded-lg border border-border-strong bg-background px-3 py-2 text-sm outline-none focus:border-indigo-400"
/>
</div>
<label class="flex items-start gap-2 text-xs text-neutral-400">
<label class="flex items-start gap-2 text-xs text-muted-foreground">
<input type="checkbox" bind:checked={authorPseudonym} class="mt-0.5" />
<span>Pseudonym — Anzeigename ist nicht mein Klarname</span>
</label>
{#if authorStore.error}
<p class="text-red-400">{authorStore.error}</p>
<p class="text-error">{authorStore.error}</p>
{/if}
<div class="flex justify-end gap-2 pt-2">
<button
class="rounded-lg px-3 py-1.5 text-sm text-neutral-400 hover:text-neutral-100"
class="rounded-lg px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground"
onclick={onClose}
>
Abbrechen
</button>
<button
class="rounded-lg bg-indigo-500 px-4 py-1.5 text-sm text-white hover:bg-indigo-400 disabled:opacity-50"
class="rounded-lg bg-app-accent px-4 py-1.5 text-sm text-white hover:bg-app-accent/90 disabled:opacity-50"
onclick={submitAuthor}
disabled={!authorSlug.trim() || !authorName.trim() || authorStore.loading}
>
@ -202,45 +204,45 @@
</div>
{:else if stage === 'meta'}
<div class="space-y-4 text-sm">
<p class="text-neutral-400">
Veröffentlicht als <code class="rounded bg-neutral-800 px-1 text-xs"
<p class="text-muted-foreground">
Veröffentlicht als <code class="rounded bg-muted px-1 text-xs"
>cards.mana.how/d/{deckSlug || '...'}</code
>
</p>
<div>
<label for="d-slug" class="mb-1 block text-xs text-neutral-400">Slug</label>
<label for="d-slug" class="mb-1 block text-xs text-muted-foreground">Slug</label>
<input
id="d-slug"
type="text"
bind:value={deckSlug}
class="w-full rounded-lg border border-neutral-700 bg-neutral-950 px-3 py-2 text-sm outline-none focus:border-indigo-400"
class="w-full rounded-lg border border-border-strong bg-background px-3 py-2 text-sm outline-none focus:border-indigo-400"
/>
</div>
<div>
<label for="d-title" class="mb-1 block text-xs text-neutral-400">Titel</label>
<label for="d-title" class="mb-1 block text-xs text-muted-foreground">Titel</label>
<input
id="d-title"
type="text"
bind:value={deckTitle}
class="w-full rounded-lg border border-neutral-700 bg-neutral-950 px-3 py-2 text-sm outline-none focus:border-indigo-400"
class="w-full rounded-lg border border-border-strong bg-background px-3 py-2 text-sm outline-none focus:border-indigo-400"
/>
</div>
<div>
<label for="d-desc" class="mb-1 block text-xs text-neutral-400">Beschreibung</label>
<label for="d-desc" class="mb-1 block text-xs text-muted-foreground">Beschreibung</label>
<textarea
id="d-desc"
bind:value={deckDescription}
placeholder="Worum geht es in diesem Deck?"
class="min-h-[80px] w-full rounded-lg border border-neutral-700 bg-neutral-950 px-3 py-2 text-sm outline-none focus:border-indigo-400"
class="min-h-[80px] w-full rounded-lg border border-border-strong bg-background px-3 py-2 text-sm outline-none focus:border-indigo-400"
></textarea>
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label for="d-lang" class="mb-1 block text-xs text-neutral-400">Sprache</label>
<label for="d-lang" class="mb-1 block text-xs text-muted-foreground">Sprache</label>
<select
id="d-lang"
bind:value={deckLanguage}
class="w-full rounded-lg border border-neutral-700 bg-neutral-950 px-3 py-2 text-sm outline-none focus:border-indigo-400"
class="w-full rounded-lg border border-border-strong bg-background px-3 py-2 text-sm outline-none focus:border-indigo-400"
>
<option value="de">Deutsch</option>
<option value="en">English</option>
@ -252,11 +254,11 @@
</select>
</div>
<div>
<label for="d-license" class="mb-1 block text-xs text-neutral-400">Lizenz</label>
<label for="d-license" class="mb-1 block text-xs text-muted-foreground">Lizenz</label>
<select
id="d-license"
bind:value={deckLicense}
class="w-full rounded-lg border border-neutral-700 bg-neutral-950 px-3 py-2 text-sm outline-none focus:border-indigo-400"
class="w-full rounded-lg border border-border-strong bg-background px-3 py-2 text-sm outline-none focus:border-indigo-400"
>
<option value="CC-BY-4.0">CC-BY 4.0 — frei mit Namensnennung</option>
<option value="CC-BY-SA-4.0">CC-BY-SA 4.0 — share-alike</option>
@ -267,17 +269,17 @@
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label for="d-semver" class="mb-1 block text-xs text-neutral-400">Version</label>
<label for="d-semver" class="mb-1 block text-xs text-muted-foreground">Version</label>
<input
id="d-semver"
type="text"
bind:value={deckSemver}
placeholder="1.0.0"
class="w-full rounded-lg border border-neutral-700 bg-neutral-950 px-3 py-2 text-sm outline-none focus:border-indigo-400"
class="w-full rounded-lg border border-border-strong bg-background px-3 py-2 text-sm outline-none focus:border-indigo-400"
/>
</div>
<div>
<label for="d-changelog" class="mb-1 block text-xs text-neutral-400">
<label for="d-changelog" class="mb-1 block text-xs text-muted-foreground">
Changelog (optional)
</label>
<input
@ -285,24 +287,24 @@
type="text"
bind:value={deckChangelog}
placeholder="Erste Version"
class="w-full rounded-lg border border-neutral-700 bg-neutral-950 px-3 py-2 text-sm outline-none focus:border-indigo-400"
class="w-full rounded-lg border border-border-strong bg-background px-3 py-2 text-sm outline-none focus:border-indigo-400"
/>
</div>
</div>
<p class="text-xs text-neutral-500">
<p class="text-xs text-muted-foreground/80">
{cards.length}
{cards.length === 1 ? 'Karte' : 'Karten'} werden veröffentlicht. Das Deck durchläuft eine KI-Inhaltsprüfung
— offensichtlich harmloses Material geht direkt durch.
</p>
<div class="flex justify-end gap-2 pt-2">
<button
class="rounded-lg px-3 py-1.5 text-sm text-neutral-400 hover:text-neutral-100"
class="rounded-lg px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground"
onclick={onClose}
>
Abbrechen
</button>
<button
class="rounded-lg bg-indigo-500 px-4 py-1.5 text-sm text-white hover:bg-indigo-400 disabled:opacity-50"
class="rounded-lg bg-app-accent px-4 py-1.5 text-sm text-white hover:bg-app-accent/90 disabled:opacity-50"
onclick={submitPublish}
disabled={!deckSlug.trim() || !deckTitle.trim() || cards.length === 0}
>
@ -311,7 +313,7 @@
</div>
</div>
{:else if stage === 'publishing'}
<div class="py-8 text-center text-sm text-neutral-400">
<div class="py-8 text-center text-sm text-muted-foreground">
Lade {cards.length} Karten hoch und prüfe Inhalt…
</div>
{:else if stage === 'done' && result}
@ -319,18 +321,18 @@
<div class="text-green-400">
✓ Veröffentlicht als Version {result.version.semver}
</div>
<div class="text-neutral-300">
<div class="text-foreground/80">
{result.version.cardCount} Karten · Lizenz: {result.deck.license}
</div>
{#if result.moderation.verdict === 'flag'}
<div class="rounded-lg border border-amber-500/30 bg-amber-500/10 p-3 text-amber-300">
<div class="rounded-lg border border-amber-500/30 bg-amber-500/10 p-3 text-warning">
Inhalt wurde zur Moderations-Prüfung markiert ({result.moderation.categories.join(
', '
)}). Das Deck ist veröffentlicht; ein Mensch schaut bei Gelegenheit drüber.
</div>
{/if}
<button
class="rounded-lg bg-indigo-500 px-4 py-1.5 text-sm text-white hover:bg-indigo-400"
class="rounded-lg bg-app-accent px-4 py-1.5 text-sm text-white hover:bg-app-accent/90"
onclick={onClose}
>
Fertig
@ -338,9 +340,9 @@
</div>
{:else if stage === 'error'}
<div class="space-y-3 text-sm">
<div class="text-red-400">Fehler: {error}</div>
<div class="text-error">Fehler: {error}</div>
<button
class="rounded-lg px-3 py-1.5 text-sm text-neutral-400 hover:text-neutral-100"
class="rounded-lg px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground"
onclick={() => (stage = 'meta')}
>
Erneut versuchen

View file

@ -77,10 +77,10 @@
}
function statusBadgeClass(s: PullRequest['status']) {
if (s === 'open') return 'bg-emerald-500/15 text-emerald-300';
if (s === 'open') return 'bg-success/15 text-success';
if (s === 'merged') return 'bg-violet-500/15 text-violet-300';
if (s === 'rejected') return 'bg-red-500/15 text-red-300';
return 'bg-neutral-800 text-neutral-400';
if (s === 'rejected') return 'bg-error/15 text-error';
return 'bg-muted text-muted-foreground';
}
function diffSummary(pr: PullRequest) {
@ -90,11 +90,11 @@
<section class="mt-10">
<header class="mb-3 flex items-center justify-between">
<h2 class="text-sm font-semibold uppercase tracking-wide text-neutral-400">
<h2 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
Pull Requests {prs.length > 0 ? `(${prs.length})` : ''}
</h2>
<button
class="text-xs text-neutral-500 hover:text-neutral-300"
class="text-xs text-muted-foreground/80 hover:text-foreground/80"
onclick={load}
disabled={loading}
>
@ -103,37 +103,37 @@
</header>
{#if error}
<p class="mb-3 rounded-lg border border-red-500/30 bg-red-500/10 p-3 text-sm text-red-400">
<p class="mb-3 rounded-lg border border-error/30 bg-error/10 p-3 text-sm text-error">
{error}
</p>
{/if}
{#if loading && prs.length === 0}
<p class="rounded-xl border border-neutral-800 bg-neutral-900 p-4 text-sm text-neutral-500">
<p class="rounded-xl border border-border bg-card p-4 text-sm text-muted-foreground/80">
Lädt…
</p>
{:else if prs.length === 0}
<p class="rounded-xl border border-neutral-800 bg-neutral-900 p-4 text-sm text-neutral-500">
<p class="rounded-xl border border-border bg-card p-4 text-sm text-muted-foreground/80">
Noch keine Pull Requests. Abonnenten können Verbesserungen vorschlagen.
</p>
{:else}
<ul class="space-y-2">
{#each prs as pr (pr.id)}
<li class="rounded-xl border border-neutral-800 bg-neutral-900 p-4">
<li class="rounded-xl border border-border bg-card p-4">
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<div class="flex items-center gap-2">
<span class="rounded-full px-2 py-0.5 text-xs {statusBadgeClass(pr.status)}">
{pr.status}
</span>
<h3 class="truncate font-medium text-neutral-100">{pr.title}</h3>
<h3 class="truncate font-medium text-foreground">{pr.title}</h3>
</div>
<p class="mt-1 text-xs text-neutral-500">
<p class="mt-1 text-xs text-muted-foreground/80">
{diffSummary(pr)} · {new Date(pr.createdAt).toLocaleDateString('de-DE')}
</p>
</div>
<button
class="shrink-0 text-xs text-neutral-500 hover:text-neutral-300"
class="shrink-0 text-xs text-muted-foreground/80 hover:text-foreground/80"
onclick={() => (expanded[pr.id] = !expanded[pr.id])}
>
{expanded[pr.id] ? 'Einklappen' : 'Details'}
@ -142,22 +142,22 @@
{#if expanded[pr.id]}
{#if pr.body}
<p class="mt-3 whitespace-pre-line text-sm text-neutral-300">{pr.body}</p>
<p class="mt-3 whitespace-pre-line text-sm text-foreground/80">{pr.body}</p>
{/if}
{#if pr.diff.modify.length > 0}
<div class="mt-3">
<div class="mb-1 text-xs uppercase text-neutral-500">Geändert</div>
<div class="mb-1 text-xs uppercase text-muted-foreground/80">Geändert</div>
<ul class="space-y-2">
{#each pr.diff.modify as m (m.contentHash)}
<li class="rounded-lg border border-neutral-800 bg-neutral-950 p-2 text-xs">
<div class="text-neutral-500">
<li class="rounded-lg border border-border bg-background p-2 text-xs">
<div class="text-muted-foreground/80">
{m.contentHash.slice(0, 12)}
</div>
{#each Object.entries(m.fields) as [k, v]}
<div class="mt-1">
<span class="text-neutral-500">{k}:</span>
<span class="text-neutral-200">{v}</span>
<span class="text-muted-foreground/80">{k}:</span>
<span class="text-foreground/90">{v}</span>
</div>
{/each}
</li>
@ -168,17 +168,17 @@
{#if pr.diff.add.length > 0}
<div class="mt-3">
<div class="mb-1 text-xs uppercase text-neutral-500">
<div class="mb-1 text-xs uppercase text-muted-foreground/80">
Neu (+{pr.diff.add.length})
</div>
<ul class="space-y-2">
{#each pr.diff.add as a, i (i)}
<li class="rounded-lg border border-neutral-800 bg-neutral-950 p-2 text-xs">
<div class="text-neutral-500">{a.type}</div>
<li class="rounded-lg border border-border bg-background p-2 text-xs">
<div class="text-muted-foreground/80">{a.type}</div>
{#each Object.entries(a.fields) as [k, v]}
<div class="mt-1">
<span class="text-neutral-500">{k}:</span>
<span class="text-neutral-200">{v}</span>
<span class="text-muted-foreground/80">{k}:</span>
<span class="text-foreground/90">{v}</span>
</div>
{/each}
</li>
@ -189,10 +189,10 @@
{#if pr.diff.remove.length > 0}
<div class="mt-3">
<div class="mb-1 text-xs uppercase text-neutral-500">
<div class="mb-1 text-xs uppercase text-muted-foreground/80">
Entfernt ({pr.diff.remove.length})
</div>
<ul class="space-y-1 text-xs text-neutral-400">
<ul class="space-y-1 text-xs text-muted-foreground">
{#each pr.diff.remove as r (r.contentHash)}
<li>· {r.contentHash.slice(0, 12)}</li>
{/each}
@ -210,14 +210,14 @@
{actionBusy === pr.id ? 'Mergt…' : 'Mergen'}
</button>
<button
class="rounded-lg border border-red-500/40 px-3 py-1.5 text-xs text-red-300 hover:bg-red-500/10 disabled:opacity-50"
class="rounded-lg border border-error/40 px-3 py-1.5 text-xs text-error hover:bg-error/10 disabled:opacity-50"
onclick={() => reject(pr)}
disabled={actionBusy === pr.id}
>
Ablehnen
</button>
<button
class="rounded-lg border border-neutral-700 px-3 py-1.5 text-xs hover:bg-neutral-800 disabled:opacity-50"
class="rounded-lg border border-border-strong px-3 py-1.5 text-xs hover:bg-muted disabled:opacity-50"
onclick={() => close(pr)}
disabled={actionBusy === pr.id}
>

View file

@ -56,7 +56,7 @@
{#if authStore.isAuthenticated}
{#if variant === 'icon'}
<button
class="text-xs text-neutral-600 hover:text-amber-300"
class="text-xs text-muted-foreground/60 hover:text-warning"
onclick={() => (open = true)}
title="Melden"
aria-label="Melden"
@ -64,7 +64,10 @@
🚩
</button>
{:else}
<button class="text-xs text-neutral-500 hover:text-amber-300" onclick={() => (open = true)}>
<button
class="text-xs text-muted-foreground/80 hover:text-warning"
onclick={() => (open = true)}
>
🚩 Melden
</button>
{/if}
@ -76,29 +79,27 @@
role="dialog"
aria-modal="true"
>
<div class="w-full max-w-md rounded-xl border border-neutral-800 bg-neutral-950 p-5">
<div class="w-full max-w-md rounded-xl border border-border bg-background p-5">
<header class="mb-4 flex items-center justify-between">
<h2 class="text-base font-semibold">
{cardContentHash ? 'Karte melden' : 'Deck melden'}
</h2>
<button
class="text-neutral-400 hover:text-neutral-100"
class="text-muted-foreground hover:text-foreground"
onclick={close}
aria-label="Schließen">✕</button
>
</header>
{#if done}
<p
class="rounded-lg border border-emerald-500/30 bg-emerald-500/10 p-3 text-sm text-emerald-300"
>
<p class="rounded-lg border border-success/30 bg-success/10 p-3 text-sm text-success">
Danke — die Moderation prüft den Bericht.
</p>
{:else}
<label class="mb-3 block">
<span class="mb-1 block text-xs text-neutral-400">Kategorie</span>
<span class="mb-1 block text-xs text-muted-foreground">Kategorie</span>
<select
class="w-full rounded-lg border border-neutral-800 bg-neutral-900 px-3 py-2 text-sm"
class="w-full rounded-lg border border-border bg-card px-3 py-2 text-sm"
bind:value={category}
>
{#each CATEGORIES as c (c.value)}
@ -108,9 +109,9 @@
</label>
<label class="mb-4 block">
<span class="mb-1 block text-xs text-neutral-400">Begründung (optional)</span>
<span class="mb-1 block text-xs text-muted-foreground">Begründung (optional)</span>
<textarea
class="w-full rounded-lg border border-neutral-800 bg-neutral-900 px-3 py-2 text-sm"
class="w-full rounded-lg border border-border bg-card px-3 py-2 text-sm"
rows="3"
bind:value={body}
placeholder="Was stimmt nicht?"
@ -118,12 +119,12 @@
</label>
{#if error}
<p class="mb-3 text-sm text-red-400">{error}</p>
<p class="mb-3 text-sm text-error">{error}</p>
{/if}
<div class="flex justify-end gap-2">
<button
class="rounded-lg border border-neutral-800 px-4 py-2 text-sm hover:border-neutral-700"
class="rounded-lg border border-border px-4 py-2 text-sm hover:border-border-strong"
onclick={close}
disabled={busy}>Abbrechen</button
>

View file

@ -38,7 +38,7 @@
const max = $derived(rawDays.reduce((m, d) => Math.max(m, d.count), 0));
function bucket(count: number): string {
if (count === 0) return 'bg-neutral-800';
if (count === 0) return 'bg-muted';
if (count <= Math.max(1, max * 0.25)) return 'bg-emerald-900';
if (count <= max * 0.5) return 'bg-emerald-700';
if (count <= max * 0.75) return 'bg-emerald-500';
@ -58,10 +58,10 @@
const activeDays = $derived(rawDays.filter((d) => d.count > 0).length);
</script>
<div class="rounded-xl border border-neutral-800 bg-neutral-900 p-4">
<div class="rounded-xl border border-border bg-card p-4">
<div class="mb-3 flex items-center justify-between text-sm">
<span class="font-medium">Lernaktivität</span>
<span class="text-xs text-neutral-500">
<span class="text-xs text-muted-foreground/80">
{total} Karten · {activeDays} aktive {activeDays === 1 ? 'Tag' : 'Tage'} · letzte {weeks} Wochen
</span>
</div>
@ -81,9 +81,9 @@
</div>
{/each}
</div>
<div class="mt-3 flex items-center gap-1 text-xs text-neutral-500">
<div class="mt-3 flex items-center gap-1 text-xs text-muted-foreground/80">
<span>weniger</span>
<span class="ml-1 h-3 w-3 rounded-sm bg-neutral-800"></span>
<span class="ml-1 h-3 w-3 rounded-sm bg-muted"></span>
<span class="h-3 w-3 rounded-sm bg-emerald-900"></span>
<span class="h-3 w-3 rounded-sm bg-emerald-700"></span>
<span class="h-3 w-3 rounded-sm bg-emerald-500"></span>

View file

@ -96,40 +96,38 @@
role="dialog"
aria-modal="true"
>
<div class="w-full max-w-xl rounded-xl border border-neutral-800 bg-neutral-950 p-6">
<div class="w-full max-w-xl rounded-xl border border-border bg-background p-6">
<header class="mb-4 flex items-center justify-between">
<h2 class="text-lg font-semibold">Verbesserung vorschlagen</h2>
<button
class="text-neutral-400 hover:text-neutral-100"
class="text-muted-foreground hover:text-foreground"
onclick={onClose}
aria-label="Schließen">✕</button
>
</header>
{#if success}
<p
class="rounded-lg border border-emerald-500/30 bg-emerald-500/10 p-3 text-sm text-emerald-300"
>
<p class="rounded-lg border border-success/30 bg-success/10 p-3 text-sm text-success">
Pull Request gesendet — der Author wird benachrichtigt.
</p>
{:else}
<div class="mb-4 inline-flex rounded-lg border border-neutral-800 p-1">
<div class="mb-4 inline-flex rounded-lg border border-border p-1">
<button
class="rounded px-3 py-1 text-xs"
class:bg-neutral-800={mode === 'modify'}
class:bg-muted={mode === 'modify'}
onclick={() => (mode = 'modify')}>Inhalt ändern</button
>
<button
class="rounded px-3 py-1 text-xs"
class:bg-neutral-800={mode === 'remove'}
class:bg-muted={mode === 'remove'}
onclick={() => (mode = 'remove')}>Karte entfernen</button
>
</div>
<label class="mb-3 block">
<span class="mb-1 block text-xs text-neutral-400">Titel</span>
<span class="mb-1 block text-xs text-muted-foreground">Titel</span>
<input
class="w-full rounded-lg border border-neutral-800 bg-neutral-900 px-3 py-2 text-sm"
class="w-full rounded-lg border border-border bg-card px-3 py-2 text-sm"
bind:value={title}
placeholder="Kurzbeschreibung der Verbesserung"
/>
@ -139,9 +137,9 @@
<div class="mb-3 space-y-2">
{#each fieldKeys as key (key)}
<label class="block">
<span class="mb-1 block text-xs text-neutral-400">{key}</span>
<span class="mb-1 block text-xs text-muted-foreground">{key}</span>
<textarea
class="w-full rounded-lg border border-neutral-800 bg-neutral-900 px-3 py-2 text-sm"
class="w-full rounded-lg border border-border bg-card px-3 py-2 text-sm"
rows="2"
bind:value={editedFields[key]}
></textarea>
@ -150,16 +148,16 @@
</div>
{:else}
<p
class="mb-3 rounded-lg border border-amber-500/30 bg-amber-500/10 p-3 text-sm text-amber-200"
class="mb-3 rounded-lg border border-amber-500/30 bg-amber-500/10 p-3 text-sm text-warning"
>
Diese Karte wird beim Merge aus dem Deck entfernt.
</p>
{/if}
<label class="mb-4 block">
<span class="mb-1 block text-xs text-neutral-400">Begründung (optional)</span>
<span class="mb-1 block text-xs text-muted-foreground">Begründung (optional)</span>
<textarea
class="w-full rounded-lg border border-neutral-800 bg-neutral-900 px-3 py-2 text-sm"
class="w-full rounded-lg border border-border bg-card px-3 py-2 text-sm"
rows="3"
bind:value={body}
placeholder="Warum ist diese Änderung sinnvoll?"
@ -167,17 +165,17 @@
</label>
{#if error}
<p class="mb-3 text-sm text-red-400">{error}</p>
<p class="mb-3 text-sm text-error">{error}</p>
{/if}
<div class="flex items-center justify-end gap-2">
<button
class="rounded-lg border border-neutral-800 px-4 py-2 text-sm hover:border-neutral-700"
class="rounded-lg border border-border px-4 py-2 text-sm hover:border-border-strong"
onclick={onClose}
disabled={busy}>Abbrechen</button
>
<button
class="rounded-lg bg-indigo-500 px-4 py-2 text-sm text-white hover:bg-indigo-400 disabled:opacity-50"
class="rounded-lg bg-app-accent px-4 py-2 text-sm text-white hover:bg-app-accent/90 disabled:opacity-50"
onclick={submit}
disabled={busy || !hasChanges}
>

View file

@ -0,0 +1,33 @@
/**
* Cards Theme Store
*
* Uses the shared theme system. The Cards brand accent (#8b5cf6 from
* MANA_APPS) becomes `--color-app-accent` on document.documentElement
* so the existing `bg-app-accent` / `text-app-accent` utilities work
* everywhere Lernen-CTA, cloze highlight, link colours, etc.
*
* The accent is theme-agnostic by design: it stays the same whether
* the user picks Lume / Nature / Stone / Ocean × Light / Dark, so the
* Cards identity reads consistently across variants.
*/
import { createThemeStore } from '@mana/shared-theme';
export type { ThemeMode, ThemeVariant, EffectiveMode } from '@mana/shared-theme';
// Cards brand: #8b5cf6 (violet-500) → HSL channels.
const CARDS_ACCENT_HSL = '258 90% 66%';
export const theme = createThemeStore({
appId: 'cards',
});
/**
* Write the Cards app accent onto documentElement once at boot. The
* shared theme store doesn't know about per-app accents it only
* touches the variant tokens so we set this independently and it
* survives every variant switch.
*/
export function applyCardsAccent(): void {
if (typeof document === 'undefined') return;
document.documentElement.style.setProperty('--color-app-accent', CARDS_ACCENT_HSL);
}

View file

@ -1,11 +1,13 @@
<script lang="ts">
import '../app.css';
import type { Snippet } from 'svelte';
import { onDestroy } from 'svelte';
import { onDestroy, onMount } from 'svelte';
import { goto } from '$app/navigation';
import { page } from '$app/state';
import { AuthGate } from '@mana/shared-auth-ui';
import ThemeToggle from '@mana/shared-theme-ui/ThemeToggle.svelte';
import { authStore } from '$lib/stores/auth.svelte';
import { theme, applyCardsAccent } from '$lib/stores/theme';
import { startSync, stopSync } from '$lib/data/sync';
import { useStreak } from '$lib/queries';
import { pwaInfo } from 'virtual:pwa-info';
@ -35,6 +37,14 @@
// manifest → no install icon, no A2HS on mobile.
const webManifestLink = $derived(pwaInfo?.webManifest.linkTag ?? '');
onMount(() => {
// Apply the Cards brand accent once at boot. The shared theme
// store handles light/dark + variant via createThemeStore above
// (ran during module init); this just sets --color-app-accent
// so `bg-app-accent` etc. resolve to Cards' violet.
applyCardsAccent();
});
onDestroy(() => stopSync());
</script>
@ -46,25 +56,26 @@
{@render children()}
{:else}
<AuthGate {authStore} {goto} onReady={handleAuthReady}>
<header class="border-b border-neutral-900">
<header class="border-b border-border">
<div class="mx-auto flex max-w-3xl items-center justify-between px-6 py-3">
<a href="/" class="flex items-center gap-2 text-sm font-semibold tracking-tight">
<span class="text-base">🃏</span> Cards
</a>
<nav class="flex items-center gap-4 text-xs text-neutral-400">
<a href="/" class="hover:text-neutral-100">Meine Decks</a>
<a href="/explore" class="hover:text-neutral-100">Entdecken</a>
<a href="/me/purchases" class="hover:text-neutral-100">Käufe</a>
<nav class="flex items-center gap-4 text-xs text-muted-foreground">
<a href="/" class="hover:text-foreground">Meine Decks</a>
<a href="/explore" class="hover:text-foreground">Entdecken</a>
<a href="/me/purchases" class="hover:text-foreground">Käufe</a>
</nav>
<div class="flex items-center gap-3 text-xs text-neutral-500">
<div class="flex items-center gap-3 text-xs text-muted-foreground">
{#if streak > 0}
<span
class="inline-flex items-center gap-1 rounded-full bg-orange-500/15 px-2 py-0.5 text-orange-300"
class="inline-flex items-center gap-1 rounded-full bg-warning/15 px-2 py-0.5 text-warning"
title="{streak} {streak === 1 ? 'Tag' : 'Tage'} in Folge gelernt"
>
🔥 {streak}
</span>
{/if}
<ThemeToggle {theme} size={16} />
{#if authStore.user?.email}
<span class="hidden sm:inline">{authStore.user.email}</span>
{/if}
@ -74,7 +85,7 @@
await authStore.signOut();
goto('/login');
}}
class="rounded-md border border-neutral-800 px-2 py-1 hover:border-neutral-700 hover:text-neutral-100"
class="rounded-md border border-border px-2 py-1 hover:border-border-strong hover:text-foreground"
>
Abmelden
</button>

View file

@ -43,15 +43,15 @@
<header class="mb-8 flex items-center justify-between">
<div>
<h1 class="text-3xl font-semibold tracking-tight">Cards</h1>
<p class="text-sm text-neutral-400">
<p class="text-sm text-muted-foreground">
{decks.length}
{decks.length === 1 ? 'Deck' : 'Decks'}{#if totalDue > 0}
· <span class="text-amber-400">{totalDue} fällig</span>
· <span class="text-warning">{totalDue} fällig</span>
{/if}
</p>
</div>
<button
class="rounded-lg bg-indigo-500 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-400"
class="rounded-lg bg-app-accent px-4 py-2 text-sm font-medium text-white hover:bg-app-accent/90"
onclick={() => (showNew = true)}
>
Neues Deck
@ -60,7 +60,7 @@
{#if showNew}
<form
class="mb-6 space-y-3 rounded-xl border border-neutral-800 bg-neutral-900 p-4"
class="mb-6 space-y-3 rounded-xl border border-border bg-card p-4"
onsubmit={(e) => {
e.preventDefault();
handleCreate();
@ -71,19 +71,19 @@
type="text"
bind:value={newTitle}
placeholder="Titel (z.B. Spanisch Vokabeln)"
class="w-full rounded-lg border border-neutral-700 bg-neutral-950 px-3 py-2 text-sm outline-none focus:border-indigo-400"
class="w-full rounded-lg border border-border-strong bg-background px-3 py-2 text-sm outline-none focus:border-indigo-400"
autofocus
required
/>
<textarea
bind:value={newDesc}
placeholder="Beschreibung (optional)"
class="min-h-[60px] w-full rounded-lg border border-neutral-700 bg-neutral-950 px-3 py-2 text-sm outline-none focus:border-indigo-400"
class="min-h-[60px] w-full rounded-lg border border-border-strong bg-background px-3 py-2 text-sm outline-none focus:border-indigo-400"
></textarea>
<div class="flex justify-end gap-2">
<button
type="button"
class="rounded-lg px-3 py-1.5 text-sm text-neutral-400 hover:text-neutral-100"
class="rounded-lg px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground"
onclick={() => {
showNew = false;
newTitle = '';
@ -94,7 +94,7 @@
</button>
<button
type="submit"
class="rounded-lg bg-indigo-500 px-4 py-1.5 text-sm text-white hover:bg-indigo-400 disabled:opacity-50"
class="rounded-lg bg-app-accent px-4 py-1.5 text-sm text-white hover:bg-app-accent/90 disabled:opacity-50"
disabled={!newTitle.trim() || creating}
>
{creating ? 'Lege an…' : 'Anlegen'}
@ -104,11 +104,11 @@
{/if}
{#if decks.length === 0 && !showNew}
<div class="rounded-xl border border-neutral-800 bg-neutral-900 p-10 text-center">
<div class="rounded-xl border border-border bg-card p-10 text-center">
<div class="mb-3 text-4xl">🃏</div>
<p class="text-neutral-400">Noch keine Decks. Leg dein erstes an.</p>
<p class="text-muted-foreground">Noch keine Decks. Leg dein erstes an.</p>
<button
class="mt-4 rounded-lg bg-indigo-500 px-4 py-2 text-sm text-white hover:bg-indigo-400"
class="mt-4 rounded-lg bg-app-accent px-4 py-2 text-sm text-white hover:bg-app-accent/90"
onclick={() => (showNew = true)}
>
Erstes Deck anlegen
@ -121,21 +121,21 @@
<li>
<a
href={`/decks/${deck.id}`}
class="flex items-center gap-3 rounded-xl border border-neutral-800 bg-neutral-900 px-4 py-3 transition-colors hover:border-neutral-700 hover:bg-neutral-800"
class="flex items-center gap-3 rounded-xl border border-border bg-card px-4 py-3 transition-colors hover:border-border-strong hover:bg-muted"
>
<span class="h-3 w-3 shrink-0 rounded-full" style="background: {deck.color}"></span>
<span class="flex-1 truncate">
<span class="block font-medium">{deck.title}</span>
{#if deck.description}
<span class="block truncate text-xs text-neutral-400">{deck.description}</span>
<span class="block truncate text-xs text-muted-foreground">{deck.description}</span>
{/if}
</span>
{#if due > 0}
<span class="rounded-full bg-amber-500/15 px-2 py-0.5 text-xs text-amber-400">
<span class="rounded-full bg-amber-500/15 px-2 py-0.5 text-xs text-warning">
{due} fällig
</span>
{/if}
<span class="text-xs text-neutral-500">{deck.cardCount}</span>
<span class="text-xs text-muted-foreground/80">{deck.cardCount}</span>
</a>
</li>
{/each}
@ -150,5 +150,7 @@
<AnkiImport />
</div>
<p class="mt-12 text-center text-xs text-neutral-600">Phase 1 · synct mit mana.how/cards</p>
<p class="mt-12 text-center text-xs text-muted-foreground/60">
Phase 1 · synct mit mana.how/cards
</p>
</main>

View file

@ -57,12 +57,12 @@
function badgeClass(c: DeckReportItem['category']) {
const map: Record<DeckReportItem['category'], string> = {
spam: 'bg-amber-500/15 text-amber-300',
spam: 'bg-amber-500/15 text-warning',
copyright: 'bg-blue-500/15 text-blue-300',
nsfw: 'bg-pink-500/15 text-pink-300',
misinformation: 'bg-violet-500/15 text-violet-300',
hate: 'bg-red-500/15 text-red-300',
other: 'bg-neutral-800 text-neutral-300',
hate: 'bg-error/15 text-error',
other: 'bg-muted text-foreground/80',
};
return map[c];
}
@ -76,34 +76,34 @@
<header class="mb-6 flex items-center justify-between">
<h1 class="text-2xl font-semibold tracking-tight">Moderation-Inbox</h1>
{#if stage === 'ok'}
<button class="text-xs text-neutral-500 hover:text-neutral-200" onclick={load}>
<button class="text-xs text-muted-foreground/80 hover:text-foreground/90" onclick={load}>
Aktualisieren
</button>
{/if}
</header>
{#if stage === 'loading'}
<p class="py-12 text-center text-sm text-neutral-400">Lädt…</p>
<p class="py-12 text-center text-sm text-muted-foreground">Lädt…</p>
{:else if stage === 'forbidden' || !isAdmin}
<p
class="rounded-xl border border-neutral-800 bg-neutral-900 p-8 text-center text-sm text-neutral-400"
class="rounded-xl border border-border bg-card p-8 text-center text-sm text-muted-foreground"
>
Nur Admins haben Zugang zur Moderation-Inbox.
</p>
{:else if stage === 'error'}
<p class="rounded-lg border border-red-500/30 bg-red-500/10 p-4 text-sm text-red-400">
<p class="rounded-lg border border-error/30 bg-error/10 p-4 text-sm text-error">
{error}
</p>
{:else if reports.length === 0}
<p
class="rounded-xl border border-neutral-800 bg-neutral-900 p-8 text-center text-sm text-neutral-500"
class="rounded-xl border border-border bg-card p-8 text-center text-sm text-muted-foreground/80"
>
Keine offenen Reports.
</p>
{:else}
<ul class="space-y-3">
{#each reports as r (r.id)}
<li class="rounded-xl border border-neutral-800 bg-neutral-900 p-4">
<li class="rounded-xl border border-border bg-card p-4">
<header class="mb-2 flex items-start justify-between gap-2">
<div class="min-w-0">
<div class="flex items-center gap-2">
@ -112,17 +112,17 @@
</span>
<a
href="/d/{r.deckSlug}"
class="truncate text-sm font-medium hover:text-indigo-300"
class="truncate text-sm font-medium hover:text-app-accent"
>
{r.deckTitle}
</a>
{#if r.cardContentHash}
<span class="text-xs text-neutral-500"
<span class="text-xs text-muted-foreground/80"
>· Karte {r.cardContentHash.slice(0, 8)}</span
>
{/if}
</div>
<p class="mt-1 text-xs text-neutral-500">
<p class="mt-1 text-xs text-muted-foreground/80">
{new Date(r.createdAt).toLocaleString('de-DE')}
</p>
</div>
@ -130,19 +130,19 @@
{#if r.body}
<p
class="mb-3 whitespace-pre-line rounded-lg bg-neutral-950 p-2 text-sm text-neutral-300"
class="mb-3 whitespace-pre-line rounded-lg bg-background p-2 text-sm text-foreground/80"
>
{r.body}
</p>
{/if}
{#if error}
<p class="mb-2 text-xs text-red-400">{error}</p>
<p class="mb-2 text-xs text-error">{error}</p>
{/if}
<div class="flex flex-wrap gap-2">
<button
class="rounded-lg border border-neutral-700 px-3 py-1.5 text-xs hover:bg-neutral-800 disabled:opacity-50"
class="rounded-lg border border-border-strong px-3 py-1.5 text-xs hover:bg-muted disabled:opacity-50"
onclick={() => resolve(r, 'dismiss')}
disabled={busy === r.id}
>
@ -156,7 +156,7 @@
Deck entfernen
</button>
<button
class="rounded-lg bg-red-500 px-3 py-1.5 text-xs text-white hover:bg-red-400 disabled:opacity-50"
class="rounded-lg bg-error px-3 py-1.5 text-xs text-white hover:bg-error/90 disabled:opacity-50"
onclick={() => resolve(r, 'ban-author')}
disabled={busy === r.id}
>

View file

@ -126,15 +126,15 @@
<main class="mx-auto max-w-3xl px-6 py-8">
{#if stage === 'loading'}
<p class="py-12 text-center text-sm text-neutral-400">Lade Deck…</p>
<p class="py-12 text-center text-sm text-muted-foreground">Lade Deck…</p>
{:else if stage === 'not-found'}
<p
class="rounded-xl border border-neutral-800 bg-neutral-900 p-8 text-center text-sm text-neutral-400"
class="rounded-xl border border-border bg-card p-8 text-center text-sm text-muted-foreground"
>
Deck <code class="rounded bg-neutral-800 px-1">{slug}</code> existiert nicht.
Deck <code class="rounded bg-muted px-1">{slug}</code> existiert nicht.
</p>
{:else if stage === 'error'}
<p class="rounded-lg border border-red-500/30 bg-red-500/10 p-4 text-sm text-red-400">
<p class="rounded-lg border border-error/30 bg-error/10 p-4 text-sm text-error">
{error}
</p>
{:else if deck}
@ -142,41 +142,41 @@
<header class="mb-6">
<h1 class="text-3xl font-semibold tracking-tight">{deck.title}</h1>
{#if deck.description}
<p class="mt-2 text-sm text-neutral-400">{deck.description}</p>
<p class="mt-2 text-sm text-muted-foreground">{deck.description}</p>
{/if}
</header>
<div class="mb-6 flex flex-wrap items-center gap-3 text-sm">
{#if version}
<span class="rounded-full bg-neutral-800 px-2 py-0.5 text-xs text-neutral-300">
<span class="rounded-full bg-muted px-2 py-0.5 text-xs text-foreground/80">
v{version.semver}
</span>
<span class="text-neutral-400">{version.cardCount} Karten</span>
<span class="text-muted-foreground">{version.cardCount} Karten</span>
{/if}
<span class="text-neutral-400">{deck.license}</span>
<span class="text-muted-foreground">{deck.license}</span>
{#if deck.language}
<span class="text-neutral-400">{deck.language.toUpperCase()}</span>
<span class="text-muted-foreground">{deck.language.toUpperCase()}</span>
{/if}
{#if deck.priceCredits > 0}
<span class="rounded-full bg-amber-500/15 px-2 py-0.5 text-xs text-amber-300">
<span class="rounded-full bg-amber-500/15 px-2 py-0.5 text-xs text-warning">
{deck.priceCredits} 💎
</span>
{/if}
</div>
{#if version?.changelog}
<section class="mb-6 rounded-xl border border-neutral-800 bg-neutral-900 p-4">
<h2 class="mb-1 text-xs font-medium uppercase tracking-wide text-neutral-500">
<section class="mb-6 rounded-xl border border-border bg-card p-4">
<h2 class="mb-1 text-xs font-medium uppercase tracking-wide text-muted-foreground/80">
Changelog v{version.semver}
</h2>
<p class="whitespace-pre-line text-sm text-neutral-300">{version.changelog}</p>
<p class="whitespace-pre-line text-sm text-foreground/80">{version.changelog}</p>
</section>
{/if}
<div class="flex flex-wrap items-center gap-2">
{#if authStore.isAuthenticated}
<button
class="rounded-lg border border-indigo-500/40 px-4 py-2 text-sm text-indigo-300 hover:bg-indigo-500/10 disabled:opacity-50"
class="rounded-lg border border-app-accent/40 px-4 py-2 text-sm text-app-accent hover:bg-app-accent/10 disabled:opacity-50"
onclick={toggleStar}
disabled={starBusy}
>
@ -185,7 +185,7 @@
{#if subscribed}
<button
class="rounded-lg border border-emerald-500/40 px-4 py-2 text-sm text-emerald-300 hover:bg-emerald-500/10 disabled:opacity-50"
class="rounded-lg border border-success/40 px-4 py-2 text-sm text-success hover:bg-success/10 disabled:opacity-50"
onclick={toggleSubscribe}
disabled={subscribeBusy}
title="Abo entfernen"
@ -194,7 +194,7 @@
</button>
{#if subscribedDeckId}
<button
class="rounded-lg bg-indigo-500 px-4 py-2 text-sm text-white hover:bg-indigo-400"
class="rounded-lg bg-app-accent px-4 py-2 text-sm text-white hover:bg-app-accent/90"
onclick={() => goto(`/learn/${subscribedDeckId}`)}
>
Lernen
@ -210,7 +210,7 @@
</button>
{:else}
<button
class="rounded-lg bg-indigo-500 px-4 py-2 text-sm text-white hover:bg-indigo-400 disabled:opacity-50"
class="rounded-lg bg-app-accent px-4 py-2 text-sm text-white hover:bg-app-accent/90 disabled:opacity-50"
onclick={toggleSubscribe}
disabled={subscribeBusy || !version}
title={version ? 'In meine Decks ziehen' : 'Deck hat noch keine Version'}
@ -219,7 +219,7 @@
</button>
{#if isPaid && hasPurchased}
<span
class="rounded-full bg-emerald-500/15 px-2 py-1 text-xs text-emerald-300"
class="rounded-full bg-success/15 px-2 py-1 text-xs text-success"
title="Du besitzt dieses Deck"
>
✓ Gekauft
@ -229,7 +229,7 @@
{:else}
<a
href="/login"
class="rounded-lg bg-indigo-500 px-4 py-2 text-sm text-white hover:bg-indigo-400"
class="rounded-lg bg-app-accent px-4 py-2 text-sm text-white hover:bg-app-accent/90"
>
Anmelden um zu abonnieren
</a>
@ -237,10 +237,10 @@
</div>
{#if error}
<p class="mt-3 text-sm text-red-400">{error}</p>
<p class="mt-3 text-sm text-error">{error}</p>
{/if}
<div class="mt-10 flex items-center justify-between text-xs text-neutral-500">
<div class="mt-10 flex items-center justify-between text-xs text-muted-foreground/80">
<span>Veröffentlicht: {new Date(deck.createdAt).toLocaleDateString('de-DE')}</span>
{#if !isOwner}
<ReportButton deckSlug={deck.slug} />
@ -248,7 +248,7 @@
</div>
{#if deck.isTakedown}
<p class="mt-3 rounded-lg border border-red-500/30 bg-red-500/10 p-3 text-sm text-red-300">
<p class="mt-3 rounded-lg border border-error/30 bg-error/10 p-3 text-sm text-error">
Dieses Deck wurde von der Moderation entfernt.
</p>
{/if}
@ -261,7 +261,7 @@
</article>
{/if}
<p class="mt-12 text-center text-xs text-neutral-600">
<a href="/explore" class="hover:text-neutral-300">← Marktplatz</a>
<p class="mt-12 text-center text-xs text-muted-foreground/60">
<a href="/explore" class="hover:text-foreground/80">← Marktplatz</a>
</p>
</main>

View file

@ -187,7 +187,9 @@
</svelte:head>
<main class="mx-auto max-w-3xl px-6 py-10">
<a href="/" class="mb-6 inline-block text-sm text-neutral-400 hover:text-neutral-100">← Decks</a>
<a href="/" class="mb-6 inline-block text-sm text-muted-foreground hover:text-foreground"
>← Decks</a
>
{#if deck}
<header class="mb-6 flex items-start justify-between gap-4">
@ -197,11 +199,11 @@
<h1 class="text-2xl font-semibold">{deck.title}</h1>
</div>
{#if deck.description}
<p class="text-sm text-neutral-400">{deck.description}</p>
<p class="text-sm text-muted-foreground">{deck.description}</p>
{/if}
</div>
<button
class="rounded-lg border border-red-500/30 px-3 py-1.5 text-sm text-red-400 hover:bg-red-500/10"
class="rounded-lg border border-error/30 px-3 py-1.5 text-sm text-error hover:bg-error/10"
onclick={() => (confirmDelete = true)}
>
Löschen
@ -209,27 +211,27 @@
</header>
{#if isSubscribed}
<div class="mb-6 rounded-xl border border-emerald-500/30 bg-emerald-500/5 p-4 text-sm">
<div class="mb-6 rounded-xl border border-success/30 bg-emerald-500/5 p-4 text-sm">
<div class="flex items-start justify-between gap-3">
<div>
<div class="font-medium text-emerald-300">
<div class="font-medium text-success">
📥 Abonniert · v{subscribedAtVersion}
</div>
<p class="mt-1 text-xs text-neutral-400">
<p class="mt-1 text-xs text-muted-foreground">
Aus dem Marktplatz von <a
href={`/d/${subscribedFromSlug}`}
class="text-emerald-300 hover:underline">{subscribedFromSlug}</a
class="text-success hover:underline">{subscribedFromSlug}</a
>. Karten sind read-only — Author entscheidet über Inhalte. Forken um eigene Variante
zu machen (Phase ε).
</p>
</div>
</div>
{#if updatePreview}
<div class="mt-3 flex flex-wrap items-center gap-2 rounded-lg bg-emerald-500/10 p-2">
<div class="mt-3 flex flex-wrap items-center gap-2 rounded-lg bg-success/10 p-2">
<span class="text-xs font-medium text-emerald-200">
Update auf v{updatePreview.to} verfügbar
</span>
<span class="text-xs text-neutral-400">
<span class="text-xs text-muted-foreground">
+{updatePreview.added} neu · ~{updatePreview.changed} geändert · {updatePreview.removed}
entfernt
</span>
@ -243,14 +245,14 @@
</div>
{/if}
{#if updateError}
<p class="mt-2 text-xs text-red-400">{updateError}</p>
<p class="mt-2 text-xs text-error">{updateError}</p>
{/if}
</div>
{/if}
<div class="mb-6 flex flex-wrap items-center gap-3">
<button
class="rounded-lg bg-indigo-500 px-5 py-2.5 text-sm font-medium text-white hover:bg-indigo-400 disabled:opacity-50"
class="rounded-lg bg-app-accent px-5 py-2.5 text-sm font-medium text-white hover:bg-app-accent/90 disabled:opacity-50"
onclick={() => goto(`/learn/${deckId}`)}
disabled={dueCount === 0}
>
@ -263,7 +265,7 @@
</button>
{#if !isSubscribed}
<button
class="rounded-lg border border-indigo-500/30 px-4 py-2 text-sm text-indigo-300 hover:bg-indigo-500/10 disabled:opacity-50"
class="rounded-lg border border-indigo-500/30 px-4 py-2 text-sm text-app-accent hover:bg-app-accent/10 disabled:opacity-50"
onclick={() => (showPublish = true)}
disabled={cards.length === 0}
title={cards.length === 0
@ -274,32 +276,33 @@
</button>
{/if}
{#if dueCount === 0 && cards.length > 0}
<span class="text-sm text-neutral-400">Heute alles gelernt — schau später wieder rein.</span
<span class="text-sm text-muted-foreground"
>Heute alles gelernt — schau später wieder rein.</span
>
{/if}
</div>
<div class="mb-6 grid grid-cols-2 gap-3 sm:grid-cols-3">
<div class="rounded-xl border border-neutral-800 bg-neutral-900 p-4 text-center">
<div class="rounded-xl border border-border bg-card p-4 text-center">
<div class="text-2xl font-semibold">{cards.length}</div>
<div class="text-xs text-neutral-400">Karten</div>
<div class="text-xs text-muted-foreground">Karten</div>
</div>
<div class="rounded-xl border border-neutral-800 bg-neutral-900 p-4 text-center">
<div class="text-2xl font-semibold text-amber-400">{dueCount}</div>
<div class="text-xs text-neutral-400">Fällig</div>
<div class="rounded-xl border border-border bg-card p-4 text-center">
<div class="text-2xl font-semibold text-warning">{dueCount}</div>
<div class="text-xs text-muted-foreground">Fällig</div>
</div>
</div>
{#if !isSubscribed}
<div class="mb-6 flex flex-wrap items-center gap-3">
<button
class="rounded-lg bg-indigo-500 px-4 py-2 text-sm text-white hover:bg-indigo-400"
class="rounded-lg bg-app-accent px-4 py-2 text-sm text-white hover:bg-app-accent/90"
onclick={() => (showNew = true)}
>
Neue Karte
</button>
<button
class="rounded-lg border border-indigo-500/30 px-4 py-2 text-sm text-indigo-300 hover:bg-indigo-500/10"
class="rounded-lg border border-indigo-500/30 px-4 py-2 text-sm text-app-accent hover:bg-app-accent/10"
onclick={() => (showAi = !showAi)}
>
✨ Aus Text generieren
@ -314,7 +317,7 @@
{/if}
{#if showNew}
<div class="mb-6 rounded-xl border border-indigo-500/30 bg-neutral-900 p-4">
<div class="mb-6 rounded-xl border border-indigo-500/30 bg-card p-4">
<h3 class="mb-3 font-medium">Neue Karte</h3>
<div class="mb-4 grid grid-cols-2 gap-2 sm:grid-cols-4">
@ -324,11 +327,11 @@
onclick={() => (newType = opt.value)}
class="rounded-lg border p-2 text-left text-sm transition-colors {newType ===
opt.value
? 'border-indigo-400 bg-indigo-500/10 text-indigo-300'
: 'border-neutral-700 hover:bg-neutral-800'}"
? 'border-indigo-400 bg-app-accent/10 text-app-accent'
: 'border-border-strong hover:bg-muted'}"
>
<div class="font-medium">{opt.label}</div>
<div class="text-xs text-neutral-400">{opt.hint}</div>
<div class="text-xs text-muted-foreground">{opt.hint}</div>
</button>
{/each}
</div>
@ -337,10 +340,11 @@
{#if newType === 'cloze'}
<div>
<div class="mb-1 flex items-center justify-between">
<label for="card-cloze" class="text-sm text-neutral-400">Text mit Lücken</label>
<label for="card-cloze" class="text-sm text-muted-foreground">Text mit Lücken</label
>
<button
type="button"
class="text-xs text-indigo-300 hover:text-indigo-200 disabled:opacity-50"
class="text-xs text-app-accent hover:text-indigo-200 disabled:opacity-50"
onclick={() => pickAttachment('cloze')}
disabled={attachBusy !== null}
>
@ -359,22 +363,22 @@
id="card-cloze"
bind:value={newCloze}
placeholder="Berlin ist die Hauptstadt von &#123;&#123;c1::Deutschland&#125;&#125;."
class="min-h-[100px] w-full rounded-lg border border-neutral-700 bg-neutral-950 px-3 py-2 text-sm outline-none focus:border-indigo-400"
class="min-h-[100px] w-full rounded-lg border border-border-strong bg-background px-3 py-2 text-sm outline-none focus:border-indigo-400"
autofocus
></textarea>
<p class="mt-1 text-xs text-neutral-500">
<p class="mt-1 text-xs text-muted-foreground/80">
Markiere mit
<code class="rounded bg-neutral-800 px-1">&#123;&#123;c1::Wort&#125;&#125;</code>
— optional Hinweis: <code class="rounded bg-neutral-800 px-1">::Hinweis</code>.
<code class="rounded bg-muted px-1">&#123;&#123;c1::Wort&#125;&#125;</code>
— optional Hinweis: <code class="rounded bg-muted px-1">::Hinweis</code>.
</p>
</div>
{:else}
<div>
<div class="mb-1 flex items-center justify-between">
<label for="card-front" class="text-sm text-neutral-400">Vorderseite</label>
<label for="card-front" class="text-sm text-muted-foreground">Vorderseite</label>
<button
type="button"
class="text-xs text-indigo-300 hover:text-indigo-200 disabled:opacity-50"
class="text-xs text-app-accent hover:text-indigo-200 disabled:opacity-50"
onclick={() => pickAttachment('front')}
disabled={attachBusy !== null}
>
@ -394,16 +398,16 @@
type="text"
bind:value={newFront}
placeholder="Frage oder Begriff…"
class="w-full rounded-lg border border-neutral-700 bg-neutral-950 px-3 py-2 text-sm outline-none focus:border-indigo-400"
class="w-full rounded-lg border border-border-strong bg-background px-3 py-2 text-sm outline-none focus:border-indigo-400"
autofocus
/>
</div>
<div>
<div class="mb-1 flex items-center justify-between">
<label for="card-back" class="text-sm text-neutral-400">Rückseite</label>
<label for="card-back" class="text-sm text-muted-foreground">Rückseite</label>
<button
type="button"
class="text-xs text-indigo-300 hover:text-indigo-200 disabled:opacity-50"
class="text-xs text-app-accent hover:text-indigo-200 disabled:opacity-50"
onclick={() => pickAttachment('back')}
disabled={attachBusy !== null}
>
@ -421,16 +425,16 @@
id="card-back"
bind:value={newBack}
placeholder="Antwort oder Erklärung…"
class="min-h-[80px] w-full rounded-lg border border-neutral-700 bg-neutral-950 px-3 py-2 text-sm outline-none focus:border-indigo-400"
class="min-h-[80px] w-full rounded-lg border border-border-strong bg-background px-3 py-2 text-sm outline-none focus:border-indigo-400"
></textarea>
</div>
{/if}
{#if attachError}
<p class="text-xs text-red-400">{attachError}</p>
<p class="text-xs text-error">{attachError}</p>
{/if}
<div class="flex justify-end gap-2">
<button
class="rounded-lg px-3 py-1.5 text-sm text-neutral-400 hover:text-neutral-100"
class="rounded-lg px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground"
onclick={() => {
showNew = false;
newFront = '';
@ -441,7 +445,7 @@
Abbrechen
</button>
<button
class="rounded-lg bg-indigo-500 px-4 py-1.5 text-sm text-white hover:bg-indigo-400 disabled:opacity-50"
class="rounded-lg bg-app-accent px-4 py-1.5 text-sm text-white hover:bg-app-accent/90 disabled:opacity-50"
onclick={handleCreateCard}
disabled={!canSubmit()}
>
@ -452,12 +456,12 @@
</div>
{/if}
<div class="rounded-xl border border-neutral-800 bg-neutral-900">
<h2 class="border-b border-neutral-800 p-4 text-lg font-semibold">
<div class="rounded-xl border border-border bg-card">
<h2 class="border-b border-border p-4 text-lg font-semibold">
Karten ({cards.length})
</h2>
{#if cards.length === 0}
<div class="p-10 text-center text-neutral-400">
<div class="p-10 text-center text-muted-foreground">
Noch keine Karten. Erstelle deine erste!
</div>
{:else}
@ -465,24 +469,24 @@
{#each cards as card, i (card.id)}
{@const p = preview(card)}
<li class="flex items-start gap-4 p-4">
<span class="mt-1 text-xs text-neutral-500">{i + 1}.</span>
<span class="mt-1 text-xs text-muted-foreground/80">{i + 1}.</span>
<div class="min-w-0 flex-1 space-y-1">
<div class="card-content">
{@html renderMarkdown(p.primary)}
</div>
{#if p.secondary}
<div class="card-content text-sm text-neutral-400">
<div class="card-content text-sm text-muted-foreground">
{@html renderMarkdown(p.secondary)}
</div>
{/if}
</div>
<div class="flex items-center gap-2">
<span class="rounded-full bg-neutral-800 px-2 py-0.5 text-xs text-neutral-400">
<span class="rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground">
{typeBadge(card.type)}
</span>
{#if !isSubscribed}
<button
class="rounded p-1 text-neutral-500 hover:text-red-400"
class="rounded p-1 text-muted-foreground/80 hover:text-error"
onclick={() => handleDeleteCard(card.id)}
aria-label="Karte löschen"
>
@ -506,22 +510,22 @@
<!-- svelte-ignore a11y_no_static_element_interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div
class="mx-4 w-full max-w-md rounded-xl border border-neutral-800 bg-neutral-900 p-6"
class="mx-4 w-full max-w-md rounded-xl border border-border bg-card p-6"
onclick={(e) => e.stopPropagation()}
>
<h3 class="mb-2 text-xl font-semibold">Deck löschen?</h3>
<p class="mb-6 text-neutral-400">
<p class="mb-6 text-muted-foreground">
"{deck.title}" wird mit allen Karten gelöscht.
</p>
<div class="flex justify-end gap-3">
<button
class="rounded-lg px-4 py-2 text-sm text-neutral-400 hover:text-neutral-100"
class="rounded-lg px-4 py-2 text-sm text-muted-foreground hover:text-foreground"
onclick={() => (confirmDelete = false)}
>
Abbrechen
</button>
<button
class="rounded-lg bg-red-500 px-4 py-2 text-sm text-white hover:bg-red-400"
class="rounded-lg bg-error px-4 py-2 text-sm text-white hover:bg-error/90"
onclick={handleDeleteDeck}
>
Löschen
@ -531,9 +535,9 @@
</div>
{/if}
{:else}
<div class="py-16 text-center text-neutral-400">
<div class="py-16 text-center text-muted-foreground">
Deck nicht gefunden.
<a href="/" class="ml-2 text-indigo-400 hover:underline">zurück</a>
<a href="/" class="ml-2 text-app-accent hover:underline">zurück</a>
</div>
{/if}
</main>

View file

@ -56,7 +56,7 @@
<main class="mx-auto max-w-3xl px-6 py-8">
<header class="mb-6">
<h1 class="text-3xl font-semibold tracking-tight">Entdecken</h1>
<p class="text-sm text-neutral-400">
<p class="text-sm text-muted-foreground">
Decks aus dem Cards-Marktplatz — kostenlos lernen oder eigene veröffentlichen.
</p>
</header>
@ -72,11 +72,11 @@
type="search"
bind:value={searchQuery}
placeholder="Suche nach Titel oder Beschreibung…"
class="flex-1 rounded-lg border border-neutral-700 bg-neutral-950 px-3 py-2 text-sm outline-none focus:border-indigo-400"
class="flex-1 rounded-lg border border-border-strong bg-background px-3 py-2 text-sm outline-none focus:border-indigo-400"
/>
<button
type="submit"
class="rounded-lg bg-indigo-500 px-4 py-2 text-sm text-white hover:bg-indigo-400 disabled:opacity-50"
class="rounded-lg bg-app-accent px-4 py-2 text-sm text-white hover:bg-app-accent/90 disabled:opacity-50"
disabled={searchBusy}
>
{searchBusy ? 'Suche…' : 'Suchen'}
@ -84,19 +84,22 @@
</form>
{#if stage === 'loading'}
<p class="py-12 text-center text-sm text-neutral-400">Lade Marktplatz…</p>
<p class="py-12 text-center text-sm text-muted-foreground">Lade Marktplatz…</p>
{:else if stage === 'error'}
<p class="rounded-lg border border-red-500/30 bg-red-500/10 p-4 text-sm text-red-400">
<p class="rounded-lg border border-error/30 bg-error/10 p-4 text-sm text-error">
{error}
<button class="ml-2 underline" onclick={loadLanding}>Erneut versuchen</button>
</p>
{:else if stage === 'search'}
<section>
<div class="mb-3 flex items-center justify-between">
<h2 class="text-sm font-medium text-neutral-300">
<h2 class="text-sm font-medium text-foreground/80">
{searchTotal} Treffer für „{searchQuery}"
</h2>
<button class="text-xs text-neutral-500 hover:text-neutral-200" onclick={loadLanding}>
<button
class="text-xs text-muted-foreground/80 hover:text-foreground/90"
onclick={loadLanding}
>
Zurück
</button>
</div>
@ -105,7 +108,7 @@
{:else if stage === 'landing'}
{#if featured.length > 0}
<section class="mb-8">
<h2 class="mb-3 text-sm font-medium text-neutral-300">
<h2 class="mb-3 text-sm font-medium text-foreground/80">
🛡️ Featured · vom Mana-Verein empfohlen
</h2>
<DeckGrid decks={featured} />
@ -113,12 +116,15 @@
{/if}
<section>
<h2 class="mb-3 text-sm font-medium text-neutral-300">📈 Trending · letzte 7 Tage</h2>
<DeckGrid decks={trending} emptyText="Noch keine Trends sei der/die Erste mit einem Public-Deck." />
<h2 class="mb-3 text-sm font-medium text-foreground/80">📈 Trending · letzte 7 Tage</h2>
<DeckGrid
decks={trending}
emptyText="Noch keine Trends — sei der/die Erste mit einem Public-Deck."
/>
</section>
{/if}
<p class="mt-12 text-center text-xs text-neutral-600">
<a href="/" class="hover:text-neutral-300">← Eigene Decks</a>
<p class="mt-12 text-center text-xs text-muted-foreground/60">
<a href="/" class="hover:text-foreground/80">← Eigene Decks</a>
</p>
</main>

View file

@ -99,7 +99,7 @@
<header class="mb-6 flex items-center justify-between">
<div>
<button
class="text-sm text-neutral-400 hover:text-neutral-100"
class="text-sm text-muted-foreground hover:text-foreground"
onclick={() => goto(`/decks/${deckId}`)}
>
{deckTitle}
@ -107,33 +107,33 @@
<h1 class="mt-1 text-xl font-semibold">Lernen</h1>
</div>
{#if queue.length > 0 && !finished}
<div class="text-sm text-neutral-400">
<div class="text-sm text-muted-foreground">
{Math.min(currentIndex + 1, queue.length)} / {queue.length}
</div>
{/if}
</header>
{#if empty}
<div class="rounded-xl border border-neutral-800 bg-neutral-900 p-10 text-center">
<div class="rounded-xl border border-border bg-card p-10 text-center">
<div class="text-2xl">Alles gelernt</div>
<p class="mt-2 text-sm text-neutral-400">
<p class="mt-2 text-sm text-muted-foreground">
Komm später wieder — fällige Karten erscheinen automatisch.
</p>
<button
class="mt-4 rounded-lg bg-indigo-500 px-4 py-2 text-sm text-white hover:bg-indigo-400"
class="mt-4 rounded-lg bg-app-accent px-4 py-2 text-sm text-white hover:bg-app-accent/90"
onclick={() => goto(`/decks/${deckId}`)}
>
Zurück zum Deck
</button>
</div>
{:else if finished}
<div class="rounded-xl border border-neutral-800 bg-neutral-900 p-10 text-center">
<div class="rounded-xl border border-border bg-card p-10 text-center">
<div class="text-2xl">Session abgeschlossen</div>
<p class="mt-2 text-sm text-neutral-400">
<p class="mt-2 text-sm text-muted-foreground">
{sessionCount} Karten in {Math.round((Date.now() - sessionStartedAt) / 1000)} s.
</p>
<button
class="mt-4 rounded-lg bg-indigo-500 px-4 py-2 text-sm text-white hover:bg-indigo-400"
class="mt-4 rounded-lg bg-app-accent px-4 py-2 text-sm text-white hover:bg-app-accent/90"
onclick={() => goto(`/decks/${deckId}`)}
>
Fertig
@ -151,14 +151,14 @@
{#if canSuggest}
<div class="mt-3 flex justify-end gap-3">
<button
class="text-xs text-neutral-500 hover:text-neutral-200"
class="text-xs text-muted-foreground/80 hover:text-foreground/90"
onclick={() => (discussionsOpen = !discussionsOpen)}
title="Kommentare zur Karte"
>
💬 {discussionsOpen ? 'Diskussion ausblenden' : 'Diskussion'}
</button>
<button
class="text-xs text-neutral-500 hover:text-indigo-300"
class="text-xs text-muted-foreground/80 hover:text-app-accent"
onclick={() => (suggestOpen = true)}
title="Verbesserung dieser Karte vorschlagen"
>
@ -173,7 +173,7 @@
{#if !showBack}
<button
class="mt-6 w-full rounded-lg bg-indigo-500 py-3 text-base text-white hover:bg-indigo-400"
class="mt-6 w-full rounded-lg bg-app-accent py-3 text-base text-white hover:bg-app-accent/90"
onclick={reveal}
>
Aufdecken <span class="ml-2 text-xs opacity-70">(Leertaste)</span>
@ -181,7 +181,7 @@
{:else}
<div class="mt-6 grid grid-cols-4 gap-2">
<button
class="rounded-lg bg-red-500 py-3 text-sm text-white hover:bg-red-400"
class="rounded-lg bg-error py-3 text-sm text-white hover:bg-error/90"
onclick={() => grade(1)}
>
Nochmal
@ -211,7 +211,7 @@
</div>
{/if}
{:else}
<div class="text-center text-sm text-neutral-400">Lade…</div>
<div class="text-center text-sm text-muted-foreground">Lade…</div>
{/if}
</div>

View file

@ -41,48 +41,46 @@
<h1 class="mb-6 text-2xl font-semibold tracking-tight">Käufe & Auszahlungen</h1>
{#if error}
<p class="mb-4 rounded-lg border border-red-500/30 bg-red-500/10 p-3 text-sm text-red-400">
<p class="mb-4 rounded-lg border border-error/30 bg-error/10 p-3 text-sm text-error">
{error}
</p>
{/if}
<section class="mb-10">
<header class="mb-3 flex items-baseline justify-between">
<h2 class="text-sm font-semibold uppercase tracking-wide text-neutral-400">Käufe</h2>
<span class="text-xs text-neutral-500">Ausgegeben: {totalSpent} 💎</span>
<h2 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">Käufe</h2>
<span class="text-xs text-muted-foreground/80">Ausgegeben: {totalSpent} 💎</span>
</header>
{#if loading}
<p class="rounded-xl border border-neutral-800 bg-neutral-900 p-4 text-sm text-neutral-500">
<p class="rounded-xl border border-border bg-card p-4 text-sm text-muted-foreground/80">
Lädt…
</p>
{:else if purchases.length === 0}
<p class="rounded-xl border border-neutral-800 bg-neutral-900 p-4 text-sm text-neutral-500">
<p class="rounded-xl border border-border bg-card p-4 text-sm text-muted-foreground/80">
Du hast noch keine Decks gekauft.
</p>
{:else}
<ul class="space-y-2">
{#each purchases as p (p.id)}
<li
class="flex items-center justify-between rounded-xl border border-neutral-800 bg-neutral-900 p-4"
>
<li class="flex items-center justify-between rounded-xl border border-border bg-card p-4">
<div class="min-w-0 flex-1">
<a
href="/d/{p.deckSlug}"
class="truncate font-medium text-neutral-100 hover:text-indigo-300"
class="truncate font-medium text-foreground hover:text-app-accent"
>
{p.deckTitle}
</a>
<p class="mt-1 text-xs text-neutral-500">
<p class="mt-1 text-xs text-muted-foreground/80">
v{p.versionSemver} · {new Date(p.purchasedAt).toLocaleDateString('de-DE')}
{#if p.refundedAt}
<span class="ml-2 rounded bg-amber-500/15 px-1.5 py-0.5 text-amber-300"
<span class="ml-2 rounded bg-amber-500/15 px-1.5 py-0.5 text-warning"
>Erstattet</span
>
{/if}
</p>
</div>
<span class="shrink-0 text-sm text-neutral-300">{p.priceCredits} 💎</span>
<span class="shrink-0 text-sm text-foreground/80">{p.priceCredits} 💎</span>
</li>
{/each}
</ul>
@ -92,14 +90,14 @@
{#if payouts.length > 0 || (!loading && payouts.length === 0)}
<section>
<header class="mb-3 flex items-baseline justify-between">
<h2 class="text-sm font-semibold uppercase tracking-wide text-neutral-400">
<h2 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
Author-Auszahlungen
</h2>
<span class="text-xs text-neutral-500">Erhalten: {totalEarned} 💎</span>
<span class="text-xs text-muted-foreground/80">Erhalten: {totalEarned} 💎</span>
</header>
{#if payouts.length === 0}
<p class="rounded-xl border border-neutral-800 bg-neutral-900 p-4 text-sm text-neutral-500">
<p class="rounded-xl border border-border bg-card p-4 text-sm text-muted-foreground/80">
Noch keine Auszahlungen — sobald jemand eines deiner kostenpflichtigen Decks kauft, landet
die Author-Beteiligung hier.
</p>
@ -107,22 +105,22 @@
<ul class="space-y-2">
{#each payouts as p (p.id)}
<li
class="flex items-center justify-between rounded-xl border border-neutral-800 bg-neutral-900 p-4"
class="flex items-center justify-between rounded-xl border border-border bg-card p-4"
>
<div class="min-w-0 flex-1">
<a
href="/d/{p.deckSlug}"
class="truncate font-medium text-neutral-100 hover:text-indigo-300"
class="truncate font-medium text-foreground hover:text-app-accent"
>
{p.deckTitle}
</a>
<p class="mt-1 text-xs text-neutral-500">
<p class="mt-1 text-xs text-muted-foreground/80">
Verkauf {p.priceCredits} 💎 · gutgeschrieben {new Date(
p.grantedAt
).toLocaleDateString('de-DE')}
</p>
</div>
<span class="shrink-0 text-sm text-emerald-300">+{p.creditsGranted} 💎</span>
<span class="shrink-0 text-sm text-success">+{p.creditsGranted} 💎</span>
</li>
{/each}
</ul>

View file

@ -64,13 +64,15 @@
<main class="mx-auto max-w-3xl px-6 py-8">
{#if stage === 'loading'}
<p class="py-12 text-center text-sm text-neutral-400">Lade Profil…</p>
<p class="py-12 text-center text-sm text-muted-foreground">Lade Profil…</p>
{:else if stage === 'not-found'}
<p class="rounded-xl border border-neutral-800 bg-neutral-900 p-8 text-center text-sm text-neutral-400">
Profil <code class="rounded bg-neutral-800 px-1">@{slug}</code> existiert nicht.
<p
class="rounded-xl border border-border bg-card p-8 text-center text-sm text-muted-foreground"
>
Profil <code class="rounded bg-muted px-1">@{slug}</code> existiert nicht.
</p>
{:else if stage === 'error'}
<p class="rounded-lg border border-red-500/30 bg-red-500/10 p-4 text-sm text-red-400">
<p class="rounded-lg border border-error/30 bg-error/10 p-4 text-sm text-error">
{error}
</p>
{:else if author}
@ -79,11 +81,11 @@
<img
src={author.avatarUrl}
alt=""
class="h-16 w-16 rounded-full border border-neutral-800 object-cover"
class="h-16 w-16 rounded-full border border-border object-cover"
/>
{:else}
<div
class="flex h-16 w-16 items-center justify-center rounded-full border border-neutral-800 bg-neutral-900 text-xl font-semibold text-neutral-400"
class="flex h-16 w-16 items-center justify-center rounded-full border border-border bg-card text-xl font-semibold text-muted-foreground"
>
{author.displayName.slice(0, 1).toUpperCase()}
</div>
@ -92,29 +94,29 @@
<div class="flex flex-wrap items-center gap-2">
<h1 class="text-2xl font-semibold">{author.displayName}</h1>
{#if author.verifiedMana}
<span class="rounded-full bg-emerald-500/15 px-2 py-0.5 text-xs text-emerald-300">
<span class="rounded-full bg-success/15 px-2 py-0.5 text-xs text-success">
🛡️ Mana
</span>
{/if}
{#if author.verifiedCommunity}
<span class="rounded-full bg-amber-500/15 px-2 py-0.5 text-xs text-amber-300">
<span class="rounded-full bg-amber-500/15 px-2 py-0.5 text-xs text-warning">
⭐ Community
</span>
{/if}
</div>
<p class="text-xs text-neutral-500">
<p class="text-xs text-muted-foreground/80">
@{author.slug} · seit {new Date(author.joinedAt).toLocaleDateString('de-DE', {
year: 'numeric',
month: 'short',
})}
</p>
{#if author.bio}
<p class="mt-2 text-sm text-neutral-300">{author.bio}</p>
<p class="mt-2 text-sm text-foreground/80">{author.bio}</p>
{/if}
</div>
{#if authStore.isAuthenticated}
<button
class="rounded-lg border border-indigo-500/40 px-3 py-1.5 text-sm text-indigo-300 hover:bg-indigo-500/10 disabled:opacity-50"
class="rounded-lg border border-app-accent/40 px-3 py-1.5 text-sm text-app-accent hover:bg-app-accent/10 disabled:opacity-50"
onclick={toggleFollow}
disabled={busy}
>
@ -123,13 +125,14 @@
{/if}
</header>
<h2 class="mb-3 text-sm font-medium text-neutral-300">
{decks.length} {decks.length === 1 ? 'Deck' : 'Decks'}
<h2 class="mb-3 text-sm font-medium text-foreground/80">
{decks.length}
{decks.length === 1 ? 'Deck' : 'Decks'}
</h2>
<DeckGrid {decks} emptyText="Dieser Author hat noch keine Decks veröffentlicht." />
{/if}
<p class="mt-12 text-center text-xs text-neutral-600">
<a href="/explore" class="hover:text-neutral-300">← Marktplatz</a>
<p class="mt-12 text-center text-xs text-muted-foreground/60">
<a href="/explore" class="hover:text-foreground/80">← Marktplatz</a>
</p>
</main>