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-stores": "workspace:*",
"@mana/shared-tailwind": "workspace:*", "@mana/shared-tailwind": "workspace:*",
"@mana/shared-theme": "workspace:*", "@mana/shared-theme": "workspace:*",
"@mana/shared-theme-ui": "workspace:*",
"@mana/shared-types": "workspace:*", "@mana/shared-types": "workspace:*",
"@mana/shared-utils": "workspace:*", "@mana/shared-utils": "workspace:*",
"dexie": "^4.4.1", "dexie": "^4.4.1",

View file

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

View file

@ -1,5 +1,5 @@
<!doctype html> <!doctype html>
<html lang="de" class="dark"> <html lang="de">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.svg" /> <link rel="icon" href="%sveltekit.assets%/favicon.svg" />
@ -8,7 +8,7 @@
<meta name="description" content="Cards — Karteikarten mit Spaced Repetition." /> <meta name="description" content="Cards — Karteikarten mit Spaced Repetition." />
%sveltekit.head% %sveltekit.head%
</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> <div style="display: contents">%sveltekit.body%</div>
</body> </body>
</html> </html>

View file

@ -101,12 +101,12 @@
} }
</script> </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"> <div class="mb-2 flex items-center justify-between">
<span class="text-sm font-medium">✨ Karten aus Text generieren</span> <span class="text-sm font-medium">✨ Karten aus Text generieren</span>
{#if stage !== 'idle'} {#if stage !== 'idle'}
<button <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} onclick={stage === 'generating' ? cancelGenerate : reset}
> >
{stage === 'generating' ? 'Abbrechen' : 'Zurücksetzen'} {stage === 'generating' ? 'Abbrechen' : 'Zurücksetzen'}
@ -118,25 +118,25 @@
<textarea <textarea
bind:value={source} bind:value={source}
placeholder="Text einfügen — Notizen, Lehrbuch-Absatz, Definition…" 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> ></textarea>
{#if stage === 'error' && error} {#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} {/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"> <div class="flex items-center gap-3">
<span>{source.length} Zeichen</span> <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>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button <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()} onclick={() => pdfPicker?.click()}
> >
📄 PDF laden 📄 PDF laden
</button> </button>
<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} onclick={handleGenerate}
disabled={!source.trim()} disabled={!source.trim()}
> >
@ -152,17 +152,17 @@
onchange={handlePdfPick} onchange={handlePdfPick}
/> />
{:else if stage === 'reading-pdf'} {: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'} {: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'} {:else if stage === 'preview'}
<div class="space-y-2 text-sm"> <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: {generated.length} Karten generiert. Wähle aus, was übernommen werden soll:
</div> </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)} {#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 <input
type="checkbox" type="checkbox"
bind:checked={selected[i]} bind:checked={selected[i]}
@ -170,27 +170,27 @@
id="ai-card-{i}" id="ai-card-{i}"
/> />
<label for="ai-card-{i}" class="min-w-0 flex-1 cursor-pointer"> <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="font-medium text-foreground">{card.front}</div>
<div class="text-xs text-neutral-400">{card.back}</div> <div class="text-xs text-muted-foreground">{card.back}</div>
</label> </label>
</li> </li>
{/each} {/each}
</ul> </ul>
<div class="flex justify-end gap-2 pt-1"> <div class="flex justify-end gap-2 pt-1">
<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(() => true))} onclick={() => (selected = selected.map(() => true))}
> >
Alle Alle
</button> </button>
<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))} onclick={() => (selected = selected.map(() => false))}
> >
Keine Keine
</button> </button>
<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} onclick={handleConfirm}
disabled={!selected.some(Boolean)} disabled={!selected.some(Boolean)}
> >
@ -199,10 +199,10 @@
</div> </div>
</div> </div>
{:else if stage === 'creating'} {: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'} {:else if stage === 'done'}
<div class="text-sm text-green-400">{createdCount} Karten angelegt.</div> <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 Weiteren Text generieren
</button> </button>
{/if} {/if}

View file

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

View file

@ -62,35 +62,35 @@
} }
</script> </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"> <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})` : ''} Diskussion {comments.length > 0 ? `(${comments.length})` : ''}
</h3> </h3>
{#if loading} {#if loading}
<span class="text-xs text-neutral-600">Lädt…</span> <span class="text-xs text-muted-foreground/60">Lädt…</span>
{/if} {/if}
</header> </header>
{#if error} {#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}
{#if comments.length === 0 && !loading} {#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} {:else}
<ul class="space-y-2"> <ul class="space-y-2">
{#each comments as c (c.id)} {#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"> <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"> <div class="flex shrink-0 items-center gap-2">
{#if authStore.user?.id !== c.authorUserId} {#if authStore.user?.id !== c.authorUserId}
<ReportButton {deckSlug} cardContentHash={c.cardContentHash} variant="icon" /> <ReportButton {deckSlug} cardContentHash={c.cardContentHash} variant="icon" />
{/if} {/if}
{#if authStore.user?.id === c.authorUserId} {#if authStore.user?.id === c.authorUserId}
<button <button
class="text-xs text-neutral-600 hover:text-red-400" class="text-xs text-muted-foreground/60 hover:text-error"
onclick={() => hide(c)} onclick={() => hide(c)}
title="Ausblenden" title="Ausblenden"
aria-label="Ausblenden" aria-label="Ausblenden"
@ -100,7 +100,7 @@
{/if} {/if}
</div> </div>
</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')} {new Date(c.createdAt).toLocaleString('de-DE')}
</p> </p>
</li> </li>
@ -117,13 +117,13 @@
}} }}
> >
<input <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…" placeholder="Kommentar zur Karte…"
bind:value={draft} bind:value={draft}
disabled={posting} disabled={posting}
/> />
<button <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" type="submit"
disabled={posting || !draft.trim()} disabled={posting || !draft.trim()}
> >

View file

@ -41,7 +41,7 @@
case 'cloze': { case 'cloze': {
const r = renderCloze(card.fields.text ?? '', subIndex); const r = renderCloze(card.fields.text ?? '', subIndex);
const extra = card.fields.extra 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 }; return { prompt: r.front + extra, answer: r.back + extra, expected: r.answer };
} }
@ -57,15 +57,13 @@
</script> </script>
<article class="space-y-4"> <article class="space-y-4">
<div <div class="card-content rounded-xl border border-border bg-card p-6 text-lg leading-relaxed">
class="card-content rounded-xl border border-neutral-800 bg-neutral-900 p-6 text-lg leading-relaxed"
>
{@html view.prompt} {@html view.prompt}
</div> </div>
{#if isTypeIn} {#if isTypeIn}
<input <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" type="text"
placeholder="Antwort eingeben…" placeholder="Antwort eingeben…"
value={typedAnswer} value={typedAnswer}
@ -80,8 +78,8 @@
{isTypeIn {isTypeIn
? matched ? matched
? 'border-green-500 bg-green-500/5' ? 'border-green-500 bg-green-500/5'
: 'border-red-500 bg-red-500/5' : 'border-red-500 bg-error/5'
: 'border-indigo-500 bg-indigo-500/5'}" : 'border-indigo-500 bg-app-accent/5'}"
> >
{@html view.answer} {@html view.answer}
</div> </div>

View file

@ -53,20 +53,20 @@
<section class="mt-10"> <section class="mt-10">
<header class="mb-3 flex items-center justify-between"> <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})` : ''} Karten {cards.length > 0 ? `(${cards.length})` : ''}
</h2> </h2>
{#if loading} {#if loading}
<span class="text-xs text-neutral-600">Lädt…</span> <span class="text-xs text-muted-foreground/60">Lädt…</span>
{/if} {/if}
</header> </header>
{#if error} {#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} {error}
</p> </p>
{:else if cards.length === 0 && !loading} {: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. Diese Version enthält keine Karten.
</p> </p>
{:else} {:else}
@ -74,18 +74,18 @@
{#each cards as c (c.contentHash)} {#each cards as c (c.contentHash)}
{@const n = counts[c.contentHash] ?? 0} {@const n = counts[c.contentHash] ?? 0}
{@const isOpen = openHash === c.contentHash} {@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 <button
class="flex w-full items-center justify-between gap-3 text-left" class="flex w-full items-center justify-between gap-3 text-left"
onclick={() => (openHash = isOpen ? null : c.contentHash)} onclick={() => (openHash = isOpen ? null : c.contentHash)}
> >
<div class="min-w-0 flex-1"> <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} #{c.ord + 1} · {c.type}
</div> </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>
<div class="shrink-0 text-xs text-neutral-500"> <div class="shrink-0 text-xs text-muted-foreground/80">
{#if n > 0} {#if n > 0}
💬 {n} 💬 {n}
{:else} {:else}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -96,40 +96,38 @@
role="dialog" role="dialog"
aria-modal="true" 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"> <header class="mb-4 flex items-center justify-between">
<h2 class="text-lg font-semibold">Verbesserung vorschlagen</h2> <h2 class="text-lg font-semibold">Verbesserung vorschlagen</h2>
<button <button
class="text-neutral-400 hover:text-neutral-100" class="text-muted-foreground hover:text-foreground"
onclick={onClose} onclick={onClose}
aria-label="Schließen">✕</button aria-label="Schließen">✕</button
> >
</header> </header>
{#if success} {#if success}
<p <p class="rounded-lg border border-success/30 bg-success/10 p-3 text-sm text-success">
class="rounded-lg border border-emerald-500/30 bg-emerald-500/10 p-3 text-sm text-emerald-300"
>
Pull Request gesendet — der Author wird benachrichtigt. Pull Request gesendet — der Author wird benachrichtigt.
</p> </p>
{:else} {: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 <button
class="rounded px-3 py-1 text-xs" 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 onclick={() => (mode = 'modify')}>Inhalt ändern</button
> >
<button <button
class="rounded px-3 py-1 text-xs" 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 onclick={() => (mode = 'remove')}>Karte entfernen</button
> >
</div> </div>
<label class="mb-3 block"> <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 <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} bind:value={title}
placeholder="Kurzbeschreibung der Verbesserung" placeholder="Kurzbeschreibung der Verbesserung"
/> />
@ -139,9 +137,9 @@
<div class="mb-3 space-y-2"> <div class="mb-3 space-y-2">
{#each fieldKeys as key (key)} {#each fieldKeys as key (key)}
<label class="block"> <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 <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" rows="2"
bind:value={editedFields[key]} bind:value={editedFields[key]}
></textarea> ></textarea>
@ -150,16 +148,16 @@
</div> </div>
{:else} {:else}
<p <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. Diese Karte wird beim Merge aus dem Deck entfernt.
</p> </p>
{/if} {/if}
<label class="mb-4 block"> <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 <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" rows="3"
bind:value={body} bind:value={body}
placeholder="Warum ist diese Änderung sinnvoll?" placeholder="Warum ist diese Änderung sinnvoll?"
@ -167,17 +165,17 @@
</label> </label>
{#if error} {#if error}
<p class="mb-3 text-sm text-red-400">{error}</p> <p class="mb-3 text-sm text-error">{error}</p>
{/if} {/if}
<div class="flex items-center justify-end gap-2"> <div class="flex items-center justify-end gap-2">
<button <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} onclick={onClose}
disabled={busy}>Abbrechen</button disabled={busy}>Abbrechen</button
> >
<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} onclick={submit}
disabled={busy || !hasChanges} 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"> <script lang="ts">
import '../app.css'; import '../app.css';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
import { onDestroy } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { page } from '$app/state'; import { page } from '$app/state';
import { AuthGate } from '@mana/shared-auth-ui'; import { AuthGate } from '@mana/shared-auth-ui';
import ThemeToggle from '@mana/shared-theme-ui/ThemeToggle.svelte';
import { authStore } from '$lib/stores/auth.svelte'; import { authStore } from '$lib/stores/auth.svelte';
import { theme, applyCardsAccent } from '$lib/stores/theme';
import { startSync, stopSync } from '$lib/data/sync'; import { startSync, stopSync } from '$lib/data/sync';
import { useStreak } from '$lib/queries'; import { useStreak } from '$lib/queries';
import { pwaInfo } from 'virtual:pwa-info'; import { pwaInfo } from 'virtual:pwa-info';
@ -35,6 +37,14 @@
// manifest → no install icon, no A2HS on mobile. // manifest → no install icon, no A2HS on mobile.
const webManifestLink = $derived(pwaInfo?.webManifest.linkTag ?? ''); 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()); onDestroy(() => stopSync());
</script> </script>
@ -46,25 +56,26 @@
{@render children()} {@render children()}
{:else} {:else}
<AuthGate {authStore} {goto} onReady={handleAuthReady}> <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"> <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"> <a href="/" class="flex items-center gap-2 text-sm font-semibold tracking-tight">
<span class="text-base">🃏</span> Cards <span class="text-base">🃏</span> Cards
</a> </a>
<nav class="flex items-center gap-4 text-xs text-neutral-400"> <nav class="flex items-center gap-4 text-xs text-muted-foreground">
<a href="/" class="hover:text-neutral-100">Meine Decks</a> <a href="/" class="hover:text-foreground">Meine Decks</a>
<a href="/explore" class="hover:text-neutral-100">Entdecken</a> <a href="/explore" class="hover:text-foreground">Entdecken</a>
<a href="/me/purchases" class="hover:text-neutral-100">Käufe</a> <a href="/me/purchases" class="hover:text-foreground">Käufe</a>
</nav> </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} {#if streak > 0}
<span <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" title="{streak} {streak === 1 ? 'Tag' : 'Tage'} in Folge gelernt"
> >
🔥 {streak} 🔥 {streak}
</span> </span>
{/if} {/if}
<ThemeToggle {theme} size={16} />
{#if authStore.user?.email} {#if authStore.user?.email}
<span class="hidden sm:inline">{authStore.user.email}</span> <span class="hidden sm:inline">{authStore.user.email}</span>
{/if} {/if}
@ -74,7 +85,7 @@
await authStore.signOut(); await authStore.signOut();
goto('/login'); 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 Abmelden
</button> </button>

View file

@ -43,15 +43,15 @@
<header class="mb-8 flex items-center justify-between"> <header class="mb-8 flex items-center justify-between">
<div> <div>
<h1 class="text-3xl font-semibold tracking-tight">Cards</h1> <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}
{decks.length === 1 ? 'Deck' : 'Decks'}{#if totalDue > 0} {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} {/if}
</p> </p>
</div> </div>
<button <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)} onclick={() => (showNew = true)}
> >
Neues Deck Neues Deck
@ -60,7 +60,7 @@
{#if showNew} {#if showNew}
<form <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) => { onsubmit={(e) => {
e.preventDefault(); e.preventDefault();
handleCreate(); handleCreate();
@ -71,19 +71,19 @@
type="text" type="text"
bind:value={newTitle} bind:value={newTitle}
placeholder="Titel (z.B. Spanisch Vokabeln)" 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 autofocus
required required
/> />
<textarea <textarea
bind:value={newDesc} bind:value={newDesc}
placeholder="Beschreibung (optional)" 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> ></textarea>
<div class="flex justify-end gap-2"> <div class="flex justify-end gap-2">
<button <button
type="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={() => { onclick={() => {
showNew = false; showNew = false;
newTitle = ''; newTitle = '';
@ -94,7 +94,7 @@
</button> </button>
<button <button
type="submit" 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} disabled={!newTitle.trim() || creating}
> >
{creating ? 'Lege an…' : 'Anlegen'} {creating ? 'Lege an…' : 'Anlegen'}
@ -104,11 +104,11 @@
{/if} {/if}
{#if decks.length === 0 && !showNew} {#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> <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 <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)} onclick={() => (showNew = true)}
> >
Erstes Deck anlegen Erstes Deck anlegen
@ -121,21 +121,21 @@
<li> <li>
<a <a
href={`/decks/${deck.id}`} 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="h-3 w-3 shrink-0 rounded-full" style="background: {deck.color}"></span>
<span class="flex-1 truncate"> <span class="flex-1 truncate">
<span class="block font-medium">{deck.title}</span> <span class="block font-medium">{deck.title}</span>
{#if deck.description} {#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} {/if}
</span> </span>
{#if due > 0} {#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 {due} fällig
</span> </span>
{/if} {/if}
<span class="text-xs text-neutral-500">{deck.cardCount}</span> <span class="text-xs text-muted-foreground/80">{deck.cardCount}</span>
</a> </a>
</li> </li>
{/each} {/each}
@ -150,5 +150,7 @@
<AnkiImport /> <AnkiImport />
</div> </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> </main>

View file

@ -57,12 +57,12 @@
function badgeClass(c: DeckReportItem['category']) { function badgeClass(c: DeckReportItem['category']) {
const map: Record<DeckReportItem['category'], string> = { 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', copyright: 'bg-blue-500/15 text-blue-300',
nsfw: 'bg-pink-500/15 text-pink-300', nsfw: 'bg-pink-500/15 text-pink-300',
misinformation: 'bg-violet-500/15 text-violet-300', misinformation: 'bg-violet-500/15 text-violet-300',
hate: 'bg-red-500/15 text-red-300', hate: 'bg-error/15 text-error',
other: 'bg-neutral-800 text-neutral-300', other: 'bg-muted text-foreground/80',
}; };
return map[c]; return map[c];
} }
@ -76,34 +76,34 @@
<header class="mb-6 flex items-center justify-between"> <header class="mb-6 flex items-center justify-between">
<h1 class="text-2xl font-semibold tracking-tight">Moderation-Inbox</h1> <h1 class="text-2xl font-semibold tracking-tight">Moderation-Inbox</h1>
{#if stage === 'ok'} {#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 Aktualisieren
</button> </button>
{/if} {/if}
</header> </header>
{#if stage === 'loading'} {#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} {:else if stage === 'forbidden' || !isAdmin}
<p <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. Nur Admins haben Zugang zur Moderation-Inbox.
</p> </p>
{:else if stage === 'error'} {: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} {error}
</p> </p>
{:else if reports.length === 0} {:else if reports.length === 0}
<p <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. Keine offenen Reports.
</p> </p>
{:else} {:else}
<ul class="space-y-3"> <ul class="space-y-3">
{#each reports as r (r.id)} {#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"> <header class="mb-2 flex items-start justify-between gap-2">
<div class="min-w-0"> <div class="min-w-0">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
@ -112,17 +112,17 @@
</span> </span>
<a <a
href="/d/{r.deckSlug}" 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} {r.deckTitle}
</a> </a>
{#if r.cardContentHash} {#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 >· Karte {r.cardContentHash.slice(0, 8)}</span
> >
{/if} {/if}
</div> </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')} {new Date(r.createdAt).toLocaleString('de-DE')}
</p> </p>
</div> </div>
@ -130,19 +130,19 @@
{#if r.body} {#if r.body}
<p <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} {r.body}
</p> </p>
{/if} {/if}
{#if error} {#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}
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<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={() => resolve(r, 'dismiss')} onclick={() => resolve(r, 'dismiss')}
disabled={busy === r.id} disabled={busy === r.id}
> >
@ -156,7 +156,7 @@
Deck entfernen Deck entfernen
</button> </button>
<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')} onclick={() => resolve(r, 'ban-author')}
disabled={busy === r.id} disabled={busy === r.id}
> >

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -64,13 +64,15 @@
<main class="mx-auto max-w-3xl px-6 py-8"> <main class="mx-auto max-w-3xl px-6 py-8">
{#if stage === 'loading'} {#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'} {: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"> <p
Profil <code class="rounded bg-neutral-800 px-1">@{slug}</code> existiert nicht. 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> </p>
{:else if stage === 'error'} {: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} {error}
</p> </p>
{:else if author} {:else if author}
@ -79,11 +81,11 @@
<img <img
src={author.avatarUrl} src={author.avatarUrl}
alt="" 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} {:else}
<div <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()} {author.displayName.slice(0, 1).toUpperCase()}
</div> </div>
@ -92,29 +94,29 @@
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<h1 class="text-2xl font-semibold">{author.displayName}</h1> <h1 class="text-2xl font-semibold">{author.displayName}</h1>
{#if author.verifiedMana} {#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 🛡️ Mana
</span> </span>
{/if} {/if}
{#if author.verifiedCommunity} {#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 ⭐ Community
</span> </span>
{/if} {/if}
</div> </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', { @{author.slug} · seit {new Date(author.joinedAt).toLocaleDateString('de-DE', {
year: 'numeric', year: 'numeric',
month: 'short', month: 'short',
})} })}
</p> </p>
{#if author.bio} {#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} {/if}
</div> </div>
{#if authStore.isAuthenticated} {#if authStore.isAuthenticated}
<button <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} onclick={toggleFollow}
disabled={busy} disabled={busy}
> >
@ -123,13 +125,14 @@
{/if} {/if}
</header> </header>
<h2 class="mb-3 text-sm font-medium text-neutral-300"> <h2 class="mb-3 text-sm font-medium text-foreground/80">
{decks.length} {decks.length === 1 ? 'Deck' : 'Decks'} {decks.length}
{decks.length === 1 ? 'Deck' : 'Decks'}
</h2> </h2>
<DeckGrid {decks} emptyText="Dieser Author hat noch keine Decks veröffentlicht." /> <DeckGrid {decks} emptyText="Dieser Author hat noch keine Decks veröffentlicht." />
{/if} {/if}
<p class="mt-12 text-center text-xs text-neutral-600"> <p class="mt-12 text-center text-xs text-muted-foreground/60">
<a href="/explore" class="hover:text-neutral-300">← Marktplatz</a> <a href="/explore" class="hover:text-foreground/80">← Marktplatz</a>
</p> </p>
</main> </main>

View file

@ -98,6 +98,13 @@
--color-card-foreground: hsl(var(--color-card-foreground) / <alpha-value>); --color-card-foreground: hsl(var(--color-card-foreground) / <alpha-value>);
--color-accent: hsl(var(--color-accent) / <alpha-value>); --color-accent: hsl(var(--color-accent) / <alpha-value>);
--color-accent-foreground: hsl(var(--color-accent-foreground) / <alpha-value>); --color-accent-foreground: hsl(var(--color-accent-foreground) / <alpha-value>);
/* Per-app brand accent set by the host app's layout via
`style="--color-app-accent: <H S% L%>"` on documentElement.
Theme-agnostic: stays the same across light/dark/lume/etc. so
the app's identity reads consistently. Defaults to Mana brand
below in :root if no host overrides. */
--color-app-accent: hsl(var(--color-app-accent) / <alpha-value>);
} }
@theme { @theme {
@ -170,6 +177,13 @@
--color-branch-social: 38 92% 50%; /* amber — warmth, relationships */ --color-branch-social: 38 92% 50%; /* amber — warmth, relationships */
--color-branch-practical: 173 80% 40%; /* teal — craft, tools */ --color-branch-practical: 173 80% 40%; /* teal — craft, tools */
--color-branch-mindset: 142 71% 45%; /* green — calm, growth */ --color-branch-mindset: 142 71% 45%; /* green — calm, growth */
/* Per-app brand accent default Mana indigo. Host apps override
at runtime by writing `--color-app-accent: <H S% L%>` onto
documentElement, typically from MANA_APPS[<id>].color via the
hexToHsl() helper in @mana/shared-theme/utils. Theme-agnostic:
not redefined in .dark / [data-theme="…"] blocks. */
--color-app-accent: 239 84% 67%;
} }
/* ===== Default Theme (Lume Light) ===== */ /* ===== Default Theme (Lume Light) ===== */

352
pnpm-lock.yaml generated
View file

@ -141,14 +141,14 @@ importers:
version: link:../../../../packages/shared-landing-ui version: link:../../../../packages/shared-landing-ui
astro: astro:
specifier: ^5.16.0 specifier: ^5.16.0
version: 5.18.1(@azure/storage-blob@12.31.0)(@types/node@20.19.39)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) version: 5.18.1(@azure/storage-blob@12.31.0)(@types/node@20.19.39)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)
typescript: typescript:
specifier: ^5.9.2 specifier: ^5.9.2
version: 5.9.3 version: 5.9.3
devDependencies: devDependencies:
'@astrojs/tailwind': '@astrojs/tailwind':
specifier: ^6.0.2 specifier: ^6.0.2
version: 6.0.2(astro@5.18.1(@azure/storage-blob@12.31.0)(@types/node@20.19.39)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3)) version: 6.0.2(astro@5.18.1(@azure/storage-blob@12.31.0)(@types/node@20.19.39)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3))
'@tailwindcss/typography': '@tailwindcss/typography':
specifier: ^0.5.18 specifier: ^0.5.18
version: 0.5.19(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3)) version: 0.5.19(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3))
@ -157,13 +157,13 @@ importers:
version: 20.19.39 version: 20.19.39
eslint: eslint:
specifier: ^9.0.0 specifier: ^9.0.0
version: 9.39.4(jiti@1.21.7) version: 9.39.4(jiti@2.6.1)
eslint-config-prettier: eslint-config-prettier:
specifier: ^9.1.0 specifier: ^9.1.0
version: 9.1.2(eslint@9.39.4(jiti@1.21.7)) version: 9.1.2(eslint@9.39.4(jiti@2.6.1))
eslint-plugin-astro: eslint-plugin-astro:
specifier: ^1.0.0 specifier: ^1.0.0
version: 1.6.0(eslint@9.39.4(jiti@1.21.7)) version: 1.6.0(eslint@9.39.4(jiti@2.6.1))
prettier: prettier:
specifier: ^3.6.2 specifier: ^3.6.2
version: 3.8.1 version: 3.8.1
@ -220,6 +220,9 @@ importers:
'@mana/shared-theme': '@mana/shared-theme':
specifier: workspace:* specifier: workspace:*
version: link:../../../../packages/shared-theme version: link:../../../../packages/shared-theme
'@mana/shared-theme-ui':
specifier: workspace:*
version: link:../../../../packages/shared-theme-ui
'@mana/shared-types': '@mana/shared-types':
specifier: workspace:* specifier: workspace:*
version: link:../../../../packages/shared-types version: link:../../../../packages/shared-types
@ -321,10 +324,10 @@ importers:
version: 3.7.2 version: 3.7.2
'@astrojs/tailwind': '@astrojs/tailwind':
specifier: ^6.0.0 specifier: ^6.0.0
version: 6.0.2(astro@5.18.1(@azure/storage-blob@12.31.0)(@types/node@24.12.2)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3)) version: 6.0.2(astro@5.18.1(@azure/storage-blob@12.31.0)(@types/node@24.12.2)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3))
astro: astro:
specifier: ^5.16.11 specifier: ^5.16.11
version: 5.18.1(@azure/storage-blob@12.31.0)(@types/node@24.12.2)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) version: 5.18.1(@azure/storage-blob@12.31.0)(@types/node@24.12.2)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)
tailwindcss: tailwindcss:
specifier: ^3.4.17 specifier: ^3.4.17
version: 3.4.19(tsx@4.21.0)(yaml@2.8.3) version: 3.4.19(tsx@4.21.0)(yaml@2.8.3)
@ -17369,16 +17372,6 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- ts-node - ts-node
'@astrojs/tailwind@6.0.2(astro@5.18.1(@azure/storage-blob@12.31.0)(@types/node@20.19.39)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3))':
dependencies:
astro: 5.18.1(@azure/storage-blob@12.31.0)(@types/node@20.19.39)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)
autoprefixer: 10.4.27(postcss@8.5.8)
postcss: 8.5.8
postcss-load-config: 4.0.2(postcss@8.5.8)
tailwindcss: 3.4.19(tsx@4.21.0)(yaml@2.8.3)
transitivePeerDependencies:
- ts-node
'@astrojs/tailwind@6.0.2(astro@5.18.1(@azure/storage-blob@12.31.0)(@types/node@20.19.39)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3))': '@astrojs/tailwind@6.0.2(astro@5.18.1(@azure/storage-blob@12.31.0)(@types/node@20.19.39)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3))':
dependencies: dependencies:
astro: 5.18.1(@azure/storage-blob@12.31.0)(@types/node@20.19.39)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) astro: 5.18.1(@azure/storage-blob@12.31.0)(@types/node@20.19.39)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)
@ -17399,6 +17392,16 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- ts-node - ts-node
'@astrojs/tailwind@6.0.2(astro@5.18.1(@azure/storage-blob@12.31.0)(@types/node@24.12.2)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3))':
dependencies:
astro: 5.18.1(@azure/storage-blob@12.31.0)(@types/node@24.12.2)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)
autoprefixer: 10.4.27(postcss@8.5.8)
postcss: 8.5.8
postcss-load-config: 4.0.2(postcss@8.5.8)
tailwindcss: 3.4.19(tsx@4.21.0)(yaml@2.8.3)
transitivePeerDependencies:
- ts-node
'@astrojs/tailwind@6.0.2(astro@5.18.1(@azure/storage-blob@12.31.0)(@types/node@24.12.2)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3))': '@astrojs/tailwind@6.0.2(astro@5.18.1(@azure/storage-blob@12.31.0)(@types/node@24.12.2)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3))(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3))':
dependencies: dependencies:
astro: 5.18.1(@azure/storage-blob@12.31.0)(@types/node@24.12.2)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) astro: 5.18.1(@azure/storage-blob@12.31.0)(@types/node@24.12.2)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)
@ -19570,11 +19573,6 @@ snapshots:
'@esbuild/win32-x64@0.27.7': '@esbuild/win32-x64@0.27.7':
optional: true optional: true
'@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@1.21.7))':
dependencies:
eslint: 9.39.4(jiti@1.21.7)
eslint-visitor-keys: 3.4.3
'@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.6.1))': '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.6.1))':
dependencies: dependencies:
eslint: 9.39.4(jiti@2.6.1) eslint: 9.39.4(jiti@2.6.1)
@ -24683,108 +24681,6 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
astro@5.18.1(@azure/storage-blob@12.31.0)(@types/node@20.19.39)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3):
dependencies:
'@astrojs/compiler': 2.13.1
'@astrojs/internal-helpers': 0.7.6
'@astrojs/markdown-remark': 6.3.11
'@astrojs/telemetry': 3.3.0
'@capsizecss/unpack': 4.0.0
'@oslojs/encoding': 1.1.0
'@rollup/pluginutils': 5.3.0(rollup@4.60.1)
acorn: 8.16.0
aria-query: 5.3.2
axobject-query: 4.1.0
boxen: 8.0.1
ci-info: 4.4.0
clsx: 2.1.1
common-ancestor-path: 1.0.1
cookie: 1.1.1
cssesc: 3.0.0
debug: 4.4.3
deterministic-object-hash: 2.0.2
devalue: 5.7.0
diff: 8.0.4
dlv: 1.1.3
dset: 3.1.4
es-module-lexer: 1.7.0
esbuild: 0.27.7
estree-walker: 3.0.3
flattie: 1.1.1
fontace: 0.4.1
github-slugger: 2.0.0
html-escaper: 3.0.3
http-cache-semantics: 4.2.0
import-meta-resolve: 4.2.0
js-yaml: 4.1.1
magic-string: 0.30.21
magicast: 0.5.2
mrmime: 2.0.1
neotraverse: 0.6.18
p-limit: 6.2.0
p-queue: 8.1.1
package-manager-detector: 1.6.0
piccolore: 0.1.3
picomatch: 4.0.4
prompts: 2.4.2
rehype: 13.0.2
semver: 7.7.4
shiki: 3.23.0
smol-toml: 1.6.1
svgo: 4.0.1
tinyexec: 1.0.4
tinyglobby: 0.2.15
tsconfck: 3.1.6(typescript@5.9.3)
ultrahtml: 1.6.0
unifont: 0.7.4
unist-util-visit: 5.1.0
unstorage: 1.17.5(@azure/storage-blob@12.31.0)(ioredis@5.10.1)
vfile: 6.0.3
vite: 6.4.2(@types/node@20.19.39)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vitefu: 1.1.3(vite@6.4.2(@types/node@20.19.39)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
xxhash-wasm: 1.1.0
yargs-parser: 21.1.1
yocto-spinner: 0.2.3
zod: 3.25.76
zod-to-json-schema: 3.25.2(zod@3.25.76)
zod-to-ts: 1.2.0(typescript@5.9.3)(zod@3.25.76)
optionalDependencies:
sharp: 0.34.5
transitivePeerDependencies:
- '@azure/app-configuration'
- '@azure/cosmos'
- '@azure/data-tables'
- '@azure/identity'
- '@azure/keyvault-secrets'
- '@azure/storage-blob'
- '@capacitor/preferences'
- '@deno/kv'
- '@netlify/blobs'
- '@planetscale/database'
- '@types/node'
- '@upstash/redis'
- '@vercel/blob'
- '@vercel/functions'
- '@vercel/kv'
- aws4fetch
- db0
- idb-keyval
- ioredis
- jiti
- less
- lightningcss
- rollup
- sass
- sass-embedded
- stylus
- sugarss
- supports-color
- terser
- tsx
- typescript
- uploadthing
- yaml
astro@5.18.1(@azure/storage-blob@12.31.0)(@types/node@20.19.39)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3): astro@5.18.1(@azure/storage-blob@12.31.0)(@types/node@20.19.39)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3):
dependencies: dependencies:
'@astrojs/compiler': 2.13.1 '@astrojs/compiler': 2.13.1
@ -24989,6 +24885,108 @@ snapshots:
- uploadthing - uploadthing
- yaml - yaml
astro@5.18.1(@azure/storage-blob@12.31.0)(@types/node@24.12.2)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3):
dependencies:
'@astrojs/compiler': 2.13.1
'@astrojs/internal-helpers': 0.7.6
'@astrojs/markdown-remark': 6.3.11
'@astrojs/telemetry': 3.3.0
'@capsizecss/unpack': 4.0.0
'@oslojs/encoding': 1.1.0
'@rollup/pluginutils': 5.3.0(rollup@4.60.1)
acorn: 8.16.0
aria-query: 5.3.2
axobject-query: 4.1.0
boxen: 8.0.1
ci-info: 4.4.0
clsx: 2.1.1
common-ancestor-path: 1.0.1
cookie: 1.1.1
cssesc: 3.0.0
debug: 4.4.3
deterministic-object-hash: 2.0.2
devalue: 5.7.0
diff: 8.0.4
dlv: 1.1.3
dset: 3.1.4
es-module-lexer: 1.7.0
esbuild: 0.27.7
estree-walker: 3.0.3
flattie: 1.1.1
fontace: 0.4.1
github-slugger: 2.0.0
html-escaper: 3.0.3
http-cache-semantics: 4.2.0
import-meta-resolve: 4.2.0
js-yaml: 4.1.1
magic-string: 0.30.21
magicast: 0.5.2
mrmime: 2.0.1
neotraverse: 0.6.18
p-limit: 6.2.0
p-queue: 8.1.1
package-manager-detector: 1.6.0
piccolore: 0.1.3
picomatch: 4.0.4
prompts: 2.4.2
rehype: 13.0.2
semver: 7.7.4
shiki: 3.23.0
smol-toml: 1.6.1
svgo: 4.0.1
tinyexec: 1.0.4
tinyglobby: 0.2.15
tsconfck: 3.1.6(typescript@5.9.3)
ultrahtml: 1.6.0
unifont: 0.7.4
unist-util-visit: 5.1.0
unstorage: 1.17.5(@azure/storage-blob@12.31.0)(ioredis@5.10.1)
vfile: 6.0.3
vite: 6.4.2(@types/node@24.12.2)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vitefu: 1.1.3(vite@6.4.2(@types/node@24.12.2)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
xxhash-wasm: 1.1.0
yargs-parser: 21.1.1
yocto-spinner: 0.2.3
zod: 3.25.76
zod-to-json-schema: 3.25.2(zod@3.25.76)
zod-to-ts: 1.2.0(typescript@5.9.3)(zod@3.25.76)
optionalDependencies:
sharp: 0.34.5
transitivePeerDependencies:
- '@azure/app-configuration'
- '@azure/cosmos'
- '@azure/data-tables'
- '@azure/identity'
- '@azure/keyvault-secrets'
- '@azure/storage-blob'
- '@capacitor/preferences'
- '@deno/kv'
- '@netlify/blobs'
- '@planetscale/database'
- '@types/node'
- '@upstash/redis'
- '@vercel/blob'
- '@vercel/functions'
- '@vercel/kv'
- aws4fetch
- db0
- idb-keyval
- ioredis
- jiti
- less
- lightningcss
- rollup
- sass
- sass-embedded
- stylus
- sugarss
- supports-color
- terser
- tsx
- typescript
- uploadthing
- yaml
astro@5.18.1(@azure/storage-blob@12.31.0)(@types/node@24.12.2)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3): astro@5.18.1(@azure/storage-blob@12.31.0)(@types/node@24.12.2)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3):
dependencies: dependencies:
'@astrojs/compiler': 2.13.1 '@astrojs/compiler': 2.13.1
@ -26816,11 +26814,6 @@ snapshots:
eslint: 9.39.4(jiti@2.6.1) eslint: 9.39.4(jiti@2.6.1)
semver: 7.7.4 semver: 7.7.4
eslint-compat-utils@0.6.5(eslint@9.39.4(jiti@1.21.7)):
dependencies:
eslint: 9.39.4(jiti@1.21.7)
semver: 7.7.4
eslint-compat-utils@0.6.5(eslint@9.39.4(jiti@2.6.1)): eslint-compat-utils@0.6.5(eslint@9.39.4(jiti@2.6.1)):
dependencies: dependencies:
eslint: 9.39.4(jiti@2.6.1) eslint: 9.39.4(jiti@2.6.1)
@ -26830,10 +26823,6 @@ snapshots:
dependencies: dependencies:
eslint: 9.39.4(jiti@2.6.1) eslint: 9.39.4(jiti@2.6.1)
eslint-config-prettier@9.1.2(eslint@9.39.4(jiti@1.21.7)):
dependencies:
eslint: 9.39.4(jiti@1.21.7)
eslint-config-prettier@9.1.2(eslint@9.39.4(jiti@2.6.1)): eslint-config-prettier@9.1.2(eslint@9.39.4(jiti@2.6.1)):
dependencies: dependencies:
eslint: 9.39.4(jiti@2.6.1) eslint: 9.39.4(jiti@2.6.1)
@ -26878,20 +26867,6 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-plugin-astro@1.6.0(eslint@9.39.4(jiti@1.21.7)):
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@1.21.7))
'@jridgewell/sourcemap-codec': 1.5.5
'@typescript-eslint/types': 8.58.0
astro-eslint-parser: 1.4.0
eslint: 9.39.4(jiti@1.21.7)
eslint-compat-utils: 0.6.5(eslint@9.39.4(jiti@1.21.7))
globals: 16.5.0
postcss: 8.5.8
postcss-selector-parser: 7.1.1
transitivePeerDependencies:
- supports-color
eslint-plugin-astro@1.6.0(eslint@9.39.4(jiti@2.6.1)): eslint-plugin-astro@1.6.0(eslint@9.39.4(jiti@2.6.1)):
dependencies: dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1))
@ -27065,47 +27040,6 @@ snapshots:
eslint-visitor-keys@5.0.1: {} eslint-visitor-keys@5.0.1: {}
eslint@9.39.4(jiti@1.21.7):
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@1.21.7))
'@eslint-community/regexpp': 4.12.2
'@eslint/config-array': 0.21.2
'@eslint/config-helpers': 0.4.2
'@eslint/core': 0.17.0
'@eslint/eslintrc': 3.3.5
'@eslint/js': 9.39.4
'@eslint/plugin-kit': 0.4.1
'@humanfs/node': 0.16.7
'@humanwhocodes/module-importer': 1.0.1
'@humanwhocodes/retry': 0.4.3
'@types/estree': 1.0.8
ajv: 6.14.0
chalk: 4.1.2
cross-spawn: 7.0.6
debug: 4.4.3
escape-string-regexp: 4.0.0
eslint-scope: 8.4.0
eslint-visitor-keys: 4.2.1
espree: 10.4.0
esquery: 1.7.0
esutils: 2.0.3
fast-deep-equal: 3.1.3
file-entry-cache: 8.0.0
find-up: 5.0.0
glob-parent: 6.0.2
ignore: 5.3.2
imurmurhash: 0.1.4
is-glob: 4.0.3
json-stable-stringify-without-jsonify: 1.0.1
lodash.merge: 4.6.2
minimatch: 3.1.5
natural-compare: 1.4.0
optionator: 0.9.4
optionalDependencies:
jiti: 1.21.7
transitivePeerDependencies:
- supports-color
eslint@9.39.4(jiti@2.6.1): eslint@9.39.4(jiti@2.6.1):
dependencies: dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1))
@ -34184,23 +34118,6 @@ snapshots:
lightningcss: 1.32.0 lightningcss: 1.32.0
terser: 5.46.1 terser: 5.46.1
vite@6.4.2(@types/node@20.19.39)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3):
dependencies:
esbuild: 0.25.12
fdir: 6.5.0(picomatch@4.0.4)
picomatch: 4.0.4
postcss: 8.5.8
rollup: 4.60.1
tinyglobby: 0.2.15
optionalDependencies:
'@types/node': 20.19.39
fsevents: 2.3.3
jiti: 1.21.7
lightningcss: 1.32.0
terser: 5.46.1
tsx: 4.21.0
yaml: 2.8.3
vite@6.4.2(@types/node@20.19.39)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3): vite@6.4.2(@types/node@20.19.39)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3):
dependencies: dependencies:
esbuild: 0.25.12 esbuild: 0.25.12
@ -34235,6 +34152,23 @@ snapshots:
tsx: 4.21.0 tsx: 4.21.0
yaml: 2.8.3 yaml: 2.8.3
vite@6.4.2(@types/node@24.12.2)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3):
dependencies:
esbuild: 0.25.12
fdir: 6.5.0(picomatch@4.0.4)
picomatch: 4.0.4
postcss: 8.5.8
rollup: 4.60.1
tinyglobby: 0.2.15
optionalDependencies:
'@types/node': 24.12.2
fsevents: 2.3.3
jiti: 1.21.7
lightningcss: 1.32.0
terser: 5.46.1
tsx: 4.21.0
yaml: 2.8.3
vite@6.4.2(@types/node@24.12.2)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3): vite@6.4.2(@types/node@24.12.2)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3):
dependencies: dependencies:
esbuild: 0.25.12 esbuild: 0.25.12
@ -34252,10 +34186,6 @@ snapshots:
tsx: 4.21.0 tsx: 4.21.0
yaml: 2.8.3 yaml: 2.8.3
vitefu@1.1.3(vite@6.4.2(@types/node@20.19.39)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
optionalDependencies:
vite: 6.4.2(@types/node@20.19.39)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vitefu@1.1.3(vite@6.4.2(@types/node@20.19.39)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): vitefu@1.1.3(vite@6.4.2(@types/node@20.19.39)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
optionalDependencies: optionalDependencies:
vite: 6.4.2(@types/node@20.19.39)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) vite: 6.4.2(@types/node@20.19.39)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
@ -34264,6 +34194,10 @@ snapshots:
optionalDependencies: optionalDependencies:
vite: 6.4.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) vite: 6.4.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vitefu@1.1.3(vite@6.4.2(@types/node@24.12.2)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
optionalDependencies:
vite: 6.4.2(@types/node@24.12.2)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vitefu@1.1.3(vite@6.4.2(@types/node@24.12.2)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): vitefu@1.1.3(vite@6.4.2(@types/node@24.12.2)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
optionalDependencies: optionalDependencies:
vite: 6.4.2(@types/node@24.12.2)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) vite: 6.4.2(@types/node@24.12.2)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)

View file

@ -30,7 +30,7 @@ const REPO_ROOT = join(__dirname, '..');
const THEMES_CSS = join(REPO_ROOT, 'packages/shared-tailwind/src/themes.css'); const THEMES_CSS = join(REPO_ROOT, 'packages/shared-tailwind/src/themes.css');
/** Tokens defined once at :root that do NOT participate in parity. */ /** Tokens defined once at :root that do NOT participate in parity. */
const THEME_AGNOSTIC = /^--color-(?:branch-|mana$)/; const THEME_AGNOSTIC = /^--color-(?:branch-|mana$|app-accent$)/;
/** /**
* Parse themes.css into selector Set<tokenName>. A block starts at a * Parse themes.css into selector Set<tokenName>. A block starts at a

View file

@ -53,6 +53,8 @@ const REPO_ROOT = join(__dirname, '..');
const SCAN_GLOBS = [ const SCAN_GLOBS = [
'apps/mana/apps/web/src/lib/modules/**/*.svelte', 'apps/mana/apps/web/src/lib/modules/**/*.svelte',
'apps/mana/apps/web/src/routes/(app)/**/*.svelte', 'apps/mana/apps/web/src/routes/(app)/**/*.svelte',
'apps/cards/apps/web/src/lib/components/**/*.svelte',
'apps/cards/apps/web/src/routes/**/*.svelte',
]; ];
/** /**