mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 16:41:08 +02:00
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:
parent
863311eefa
commit
ad3b99fe6d
28 changed files with 589 additions and 583 deletions
|
|
@ -37,6 +37,7 @@
|
|||
"@mana/shared-stores": "workspace:*",
|
||||
"@mana/shared-tailwind": "workspace:*",
|
||||
"@mana/shared-theme": "workspace:*",
|
||||
"@mana/shared-theme-ui": "workspace:*",
|
||||
"@mana/shared-types": "workspace:*",
|
||||
"@mana/shared-utils": "workspace:*",
|
||||
"dexie": "^4.4.1",
|
||||
|
|
|
|||
|
|
@ -1,33 +1,32 @@
|
|||
@import 'tailwindcss';
|
||||
@import '@mana/shared-tailwind/themes.css';
|
||||
@import '@mana/shared-tailwind/sources.css';
|
||||
|
||||
/* Phase-1 placeholder palette. Will swap for @mana/shared-theme tokens
|
||||
once the theming pass lands in Etappe 3c. */
|
||||
@theme {
|
||||
--color-cards-bg: #0a0a0a;
|
||||
--color-cards-surface: #161616;
|
||||
--color-cards-border: #2a2a2a;
|
||||
--color-cards-fg: #f5f5f5;
|
||||
--color-cards-muted: #a3a3a3;
|
||||
--color-cards-accent: #6366f1;
|
||||
}
|
||||
/* Phase A — Cards now lives on the unified @mana/shared-theme tokens.
|
||||
The placeholder --color-cards-* palette is gone; everything goes
|
||||
through `--color-{background,foreground,surface,muted,…}` from
|
||||
shared-tailwind. The runtime `createThemeStore({ appId: 'cards' })`
|
||||
in +layout.svelte writes the live variant + mode onto the
|
||||
document. */
|
||||
|
||||
/* Cloze rendering classes — produced by @mana/cards-core/render. */
|
||||
/* Cloze rendering — produced by @mana/cards-core/render. Uses the
|
||||
active app accent so the highlight follows the Cards brand. */
|
||||
.cloze-blank {
|
||||
background: rgba(99, 102, 241, 0.15);
|
||||
background: hsl(var(--color-app-accent) / 0.18);
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.05rem 0.4rem;
|
||||
color: #a5b4fc;
|
||||
color: hsl(var(--color-app-accent));
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
mark.cloze-active {
|
||||
background: rgba(34, 197, 94, 0.2);
|
||||
color: #86efac;
|
||||
background: hsl(var(--color-success) / 0.2);
|
||||
color: hsl(var(--color-success));
|
||||
padding: 0.05rem 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
/* Minimal styling for HTML produced by marked() — Tailwind 4 ships
|
||||
/* Minimal styling for HTML produced by marked() — Tailwind v4 ships
|
||||
without typography plugin so we set the basics by hand. */
|
||||
.card-content :where(p, ul, ol) {
|
||||
margin-block: 0.5rem;
|
||||
|
|
@ -41,19 +40,19 @@ mark.cloze-active {
|
|||
padding-inline-start: 1.25rem;
|
||||
}
|
||||
.card-content :where(code) {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
background: hsl(var(--color-muted) / 0.6);
|
||||
padding: 0.1rem 0.3rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
.card-content :where(pre) {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
background: hsl(var(--color-muted) / 0.4);
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.card-content :where(a) {
|
||||
color: #818cf8;
|
||||
color: hsl(var(--color-app-accent));
|
||||
text-decoration: underline;
|
||||
}
|
||||
.card-content :where(strong) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<!doctype html>
|
||||
<html lang="de" class="dark">
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.svg" />
|
||||
|
|
@ -8,7 +8,7 @@
|
|||
<meta name="description" content="Cards — Karteikarten mit Spaced Repetition." />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover" class="min-h-screen bg-neutral-950 text-neutral-100 antialiased">
|
||||
<body data-sveltekit-preload-data="hover" class="min-h-screen bg-background text-foreground antialiased">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -101,12 +101,12 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div class="rounded-xl border border-indigo-500/30 bg-neutral-900 p-4">
|
||||
<div class="rounded-xl border border-indigo-500/30 bg-card p-4">
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<span class="text-sm font-medium">✨ Karten aus Text generieren</span>
|
||||
{#if stage !== 'idle'}
|
||||
<button
|
||||
class="text-xs text-neutral-500 hover:text-neutral-300"
|
||||
class="text-xs text-muted-foreground/80 hover:text-foreground/80"
|
||||
onclick={stage === 'generating' ? cancelGenerate : reset}
|
||||
>
|
||||
{stage === 'generating' ? 'Abbrechen' : 'Zurücksetzen'}
|
||||
|
|
@ -118,25 +118,25 @@
|
|||
<textarea
|
||||
bind:value={source}
|
||||
placeholder="Text einfügen — Notizen, Lehrbuch-Absatz, Definition…"
|
||||
class="min-h-[120px] w-full rounded-lg border border-neutral-700 bg-neutral-950 px-3 py-2 text-sm outline-none focus:border-indigo-400"
|
||||
class="min-h-[120px] w-full rounded-lg border border-border-strong bg-background px-3 py-2 text-sm outline-none focus:border-indigo-400"
|
||||
></textarea>
|
||||
{#if stage === 'error' && error}
|
||||
<p class="mt-2 text-sm text-red-400">{error}</p>
|
||||
<p class="mt-2 text-sm text-error">{error}</p>
|
||||
{/if}
|
||||
<div class="mt-2 flex items-center justify-between gap-3 text-xs text-neutral-500">
|
||||
<div class="mt-2 flex items-center justify-between gap-3 text-xs text-muted-foreground/80">
|
||||
<div class="flex items-center gap-3">
|
||||
<span>{source.length} Zeichen</span>
|
||||
{#if pdfStatus}<span class="text-indigo-300">📄 {pdfStatus}</span>{/if}
|
||||
{#if pdfStatus}<span class="text-app-accent">📄 {pdfStatus}</span>{/if}
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
class="rounded-lg border border-neutral-700 px-3 py-1.5 text-neutral-300 hover:bg-neutral-800"
|
||||
class="rounded-lg border border-border-strong px-3 py-1.5 text-foreground/80 hover:bg-muted"
|
||||
onclick={() => pdfPicker?.click()}
|
||||
>
|
||||
📄 PDF laden
|
||||
</button>
|
||||
<button
|
||||
class="rounded-lg bg-indigo-500 px-4 py-1.5 text-sm text-white hover:bg-indigo-400 disabled:opacity-50"
|
||||
class="rounded-lg bg-app-accent px-4 py-1.5 text-sm text-white hover:bg-app-accent/90 disabled:opacity-50"
|
||||
onclick={handleGenerate}
|
||||
disabled={!source.trim()}
|
||||
>
|
||||
|
|
@ -152,17 +152,17 @@
|
|||
onchange={handlePdfPick}
|
||||
/>
|
||||
{:else if stage === 'reading-pdf'}
|
||||
<div class="py-6 text-center text-sm text-neutral-400">{pdfStatus ?? 'Lese PDF…'}</div>
|
||||
<div class="py-6 text-center text-sm text-muted-foreground">{pdfStatus ?? 'Lese PDF…'}</div>
|
||||
{:else if stage === 'generating'}
|
||||
<div class="py-6 text-center text-sm text-neutral-400">Modell denkt nach…</div>
|
||||
<div class="py-6 text-center text-sm text-muted-foreground">Modell denkt nach…</div>
|
||||
{:else if stage === 'preview'}
|
||||
<div class="space-y-2 text-sm">
|
||||
<div class="text-neutral-300">
|
||||
<div class="text-foreground/80">
|
||||
{generated.length} Karten generiert. Wähle aus, was übernommen werden soll:
|
||||
</div>
|
||||
<ul class="max-h-72 space-y-1 overflow-y-auto rounded-lg border border-neutral-800 p-2">
|
||||
<ul class="max-h-72 space-y-1 overflow-y-auto rounded-lg border border-border p-2">
|
||||
{#each generated as card, i (i)}
|
||||
<li class="flex items-start gap-2 rounded-md p-1 hover:bg-neutral-800/50">
|
||||
<li class="flex items-start gap-2 rounded-md p-1 hover:bg-muted/50">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={selected[i]}
|
||||
|
|
@ -170,27 +170,27 @@
|
|||
id="ai-card-{i}"
|
||||
/>
|
||||
<label for="ai-card-{i}" class="min-w-0 flex-1 cursor-pointer">
|
||||
<div class="font-medium text-neutral-100">{card.front}</div>
|
||||
<div class="text-xs text-neutral-400">{card.back}</div>
|
||||
<div class="font-medium text-foreground">{card.front}</div>
|
||||
<div class="text-xs text-muted-foreground">{card.back}</div>
|
||||
</label>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<div class="flex justify-end gap-2 pt-1">
|
||||
<button
|
||||
class="rounded-lg px-3 py-1.5 text-sm text-neutral-400 hover:text-neutral-100"
|
||||
class="rounded-lg px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground"
|
||||
onclick={() => (selected = selected.map(() => true))}
|
||||
>
|
||||
Alle
|
||||
</button>
|
||||
<button
|
||||
class="rounded-lg px-3 py-1.5 text-sm text-neutral-400 hover:text-neutral-100"
|
||||
class="rounded-lg px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground"
|
||||
onclick={() => (selected = selected.map(() => false))}
|
||||
>
|
||||
Keine
|
||||
</button>
|
||||
<button
|
||||
class="rounded-lg bg-indigo-500 px-4 py-1.5 text-sm text-white hover:bg-indigo-400 disabled:opacity-50"
|
||||
class="rounded-lg bg-app-accent px-4 py-1.5 text-sm text-white hover:bg-app-accent/90 disabled:opacity-50"
|
||||
onclick={handleConfirm}
|
||||
disabled={!selected.some(Boolean)}
|
||||
>
|
||||
|
|
@ -199,10 +199,10 @@
|
|||
</div>
|
||||
</div>
|
||||
{:else if stage === 'creating'}
|
||||
<div class="py-6 text-center text-sm text-neutral-400">Lege Karten an…</div>
|
||||
<div class="py-6 text-center text-sm text-muted-foreground">Lege Karten an…</div>
|
||||
{:else if stage === 'done'}
|
||||
<div class="text-sm text-green-400">✓ {createdCount} Karten angelegt.</div>
|
||||
<button class="mt-2 text-xs text-neutral-500 hover:text-neutral-300" onclick={reset}>
|
||||
<button class="mt-2 text-xs text-muted-foreground/80 hover:text-foreground/80" onclick={reset}>
|
||||
Weiteren Text generieren
|
||||
</button>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -69,20 +69,20 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div class="rounded-xl border border-neutral-800 bg-neutral-900 p-4">
|
||||
<div class="rounded-xl border border-border bg-card p-4">
|
||||
<div class="mb-2 text-sm font-medium">Aus Anki importieren</div>
|
||||
|
||||
{#if stage === 'idle'}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div
|
||||
class="rounded-lg border-2 border-dashed border-neutral-700 px-4 py-6 text-center text-sm text-neutral-400 transition-colors hover:border-indigo-400 hover:text-neutral-200"
|
||||
class="rounded-lg border-2 border-dashed border-border-strong px-4 py-6 text-center text-sm text-muted-foreground transition-colors hover:border-indigo-400 hover:text-foreground/90"
|
||||
ondragover={(e) => e.preventDefault()}
|
||||
ondrop={onDrop}
|
||||
onclick={() => fileInput?.click()}
|
||||
>
|
||||
<div class="mb-1">📦 .apkg-Datei hier ablegen oder klicken</div>
|
||||
<div class="text-xs text-neutral-500">
|
||||
<div class="text-xs text-muted-foreground/80">
|
||||
Basic, Basic + Reverse, Cloze · Bilder + Audio werden mit übernommen.
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -94,25 +94,25 @@
|
|||
onchange={onPick}
|
||||
/>
|
||||
{:else if stage === 'parsing'}
|
||||
<div class="py-6 text-center text-sm text-neutral-400">Lese {fileName}…</div>
|
||||
<div class="py-6 text-center text-sm text-muted-foreground">Lese {fileName}…</div>
|
||||
{:else if stage === 'preview' && parsed}
|
||||
<div class="space-y-2 text-sm">
|
||||
<div>
|
||||
<span class="text-neutral-400">Gefunden in</span>
|
||||
<code class="rounded bg-neutral-800 px-1 text-xs">{fileName}</code>:
|
||||
<span class="text-muted-foreground">Gefunden in</span>
|
||||
<code class="rounded bg-muted px-1 text-xs">{fileName}</code>:
|
||||
</div>
|
||||
<ul class="ml-4 list-disc text-neutral-300">
|
||||
<ul class="ml-4 list-disc text-foreground/80">
|
||||
<li>{parsed.decks.length} {parsed.decks.length === 1 ? 'Deck' : 'Decks'}</li>
|
||||
<li>{parsed.cards.length} {parsed.cards.length === 1 ? 'Karte' : 'Karten'}</li>
|
||||
{#if mediaCount > 0}
|
||||
<li>{mediaCount} Medien (Bilder/Audio)</li>
|
||||
{/if}
|
||||
{#if parsed.skipped > 0}
|
||||
<li class="text-amber-400">{parsed.skipped} übersprungen (unbekannter Typ)</li>
|
||||
<li class="text-warning">{parsed.skipped} übersprungen (unbekannter Typ)</li>
|
||||
{/if}
|
||||
</ul>
|
||||
{#if parsed.warnings.length > 0}
|
||||
<details class="text-xs text-neutral-500">
|
||||
<details class="text-xs text-muted-foreground/80">
|
||||
<summary class="cursor-pointer">Hinweise ({parsed.warnings.length})</summary>
|
||||
<ul class="mt-1 list-disc pl-4">
|
||||
{#each parsed.warnings.slice(0, 10) as w (w)}<li>{w}</li>{/each}
|
||||
|
|
@ -121,13 +121,13 @@
|
|||
{/if}
|
||||
<div class="flex justify-end gap-2 pt-2">
|
||||
<button
|
||||
class="rounded-lg px-3 py-1.5 text-sm text-neutral-400 hover:text-neutral-100"
|
||||
class="rounded-lg px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground"
|
||||
onclick={reset}
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
class="rounded-lg bg-indigo-500 px-4 py-1.5 text-sm text-white hover:bg-indigo-400"
|
||||
class="rounded-lg bg-app-accent px-4 py-1.5 text-sm text-white hover:bg-app-accent/90"
|
||||
onclick={confirmImport}
|
||||
>
|
||||
Importieren
|
||||
|
|
@ -135,11 +135,11 @@
|
|||
</div>
|
||||
</div>
|
||||
{:else if stage === 'uploading-media'}
|
||||
<div class="py-6 text-center text-sm text-neutral-400">
|
||||
<div class="py-6 text-center text-sm text-muted-foreground">
|
||||
<div>Lade Medien hoch · {mediaProgress.uploaded} / {mediaProgress.total}</div>
|
||||
<div class="mx-auto mt-3 h-1 w-48 overflow-hidden rounded-full bg-neutral-800">
|
||||
<div class="mx-auto mt-3 h-1 w-48 overflow-hidden rounded-full bg-muted">
|
||||
<div
|
||||
class="h-full bg-indigo-500 transition-all"
|
||||
class="h-full bg-app-accent transition-all"
|
||||
style="width: {mediaProgress.total === 0
|
||||
? 0
|
||||
: (mediaProgress.uploaded / mediaProgress.total) * 100}%"
|
||||
|
|
@ -147,7 +147,7 @@
|
|||
</div>
|
||||
</div>
|
||||
{:else if stage === 'importing'}
|
||||
<div class="py-6 text-center text-sm text-neutral-400">
|
||||
<div class="py-6 text-center text-sm text-muted-foreground">
|
||||
Importiere {parsed?.cards.length ?? 0} Karten…
|
||||
</div>
|
||||
{:else if stage === 'done' && result}
|
||||
|
|
@ -157,17 +157,17 @@
|
|||
{result.decksCreated === 1 ? 'Deck' : 'Decks'} angelegt.
|
||||
</div>
|
||||
{#if result.mediaUploaded > 0 || result.mediaFailed > 0}
|
||||
<div class="text-neutral-400">
|
||||
<div class="text-muted-foreground">
|
||||
{result.mediaUploaded} Medien übernommen{#if result.mediaFailed > 0}
|
||||
<span class="text-amber-400">· {result.mediaFailed} fehlgeschlagen</span>
|
||||
<span class="text-warning">· {result.mediaFailed} fehlgeschlagen</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if result.failed > 0}
|
||||
<div class="text-amber-400">{result.failed} Karten konnten nicht angelegt werden.</div>
|
||||
<div class="text-warning">{result.failed} Karten konnten nicht angelegt werden.</div>
|
||||
{/if}
|
||||
<button
|
||||
class="rounded-lg px-3 py-1.5 text-sm text-neutral-400 hover:text-neutral-100"
|
||||
class="rounded-lg px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground"
|
||||
onclick={reset}
|
||||
>
|
||||
Weitere Datei
|
||||
|
|
@ -175,9 +175,9 @@
|
|||
</div>
|
||||
{:else if stage === 'error'}
|
||||
<div class="space-y-2 text-sm">
|
||||
<div class="text-red-400">Fehler: {error}</div>
|
||||
<div class="text-error">Fehler: {error}</div>
|
||||
<button
|
||||
class="rounded-lg px-3 py-1.5 text-sm text-neutral-400 hover:text-neutral-100"
|
||||
class="rounded-lg px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground"
|
||||
onclick={reset}
|
||||
>
|
||||
Erneut versuchen
|
||||
|
|
|
|||
|
|
@ -62,35 +62,35 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<aside class="mt-4 rounded-xl border border-neutral-800 bg-neutral-950 p-4">
|
||||
<aside class="mt-4 rounded-xl border border-border bg-background p-4">
|
||||
<header class="mb-2 flex items-center justify-between">
|
||||
<h3 class="text-xs font-semibold uppercase tracking-wide text-neutral-500">
|
||||
<h3 class="text-xs font-semibold uppercase tracking-wide text-muted-foreground/80">
|
||||
Diskussion {comments.length > 0 ? `(${comments.length})` : ''}
|
||||
</h3>
|
||||
{#if loading}
|
||||
<span class="text-xs text-neutral-600">Lädt…</span>
|
||||
<span class="text-xs text-muted-foreground/60">Lädt…</span>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
{#if error}
|
||||
<p class="mb-2 text-xs text-red-400">{error}</p>
|
||||
<p class="mb-2 text-xs text-error">{error}</p>
|
||||
{/if}
|
||||
|
||||
{#if comments.length === 0 && !loading}
|
||||
<p class="text-xs text-neutral-600">Noch keine Kommentare zu dieser Karte.</p>
|
||||
<p class="text-xs text-muted-foreground/60">Noch keine Kommentare zu dieser Karte.</p>
|
||||
{:else}
|
||||
<ul class="space-y-2">
|
||||
{#each comments as c (c.id)}
|
||||
<li class="rounded-lg border border-neutral-800 bg-neutral-900 p-2 text-sm">
|
||||
<li class="rounded-lg border border-border bg-card p-2 text-sm">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<p class="whitespace-pre-line text-neutral-200">{c.body}</p>
|
||||
<p class="whitespace-pre-line text-foreground/90">{c.body}</p>
|
||||
<div class="flex shrink-0 items-center gap-2">
|
||||
{#if authStore.user?.id !== c.authorUserId}
|
||||
<ReportButton {deckSlug} cardContentHash={c.cardContentHash} variant="icon" />
|
||||
{/if}
|
||||
{#if authStore.user?.id === c.authorUserId}
|
||||
<button
|
||||
class="text-xs text-neutral-600 hover:text-red-400"
|
||||
class="text-xs text-muted-foreground/60 hover:text-error"
|
||||
onclick={() => hide(c)}
|
||||
title="Ausblenden"
|
||||
aria-label="Ausblenden"
|
||||
|
|
@ -100,7 +100,7 @@
|
|||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-neutral-600">
|
||||
<p class="mt-1 text-xs text-muted-foreground/60">
|
||||
{new Date(c.createdAt).toLocaleString('de-DE')}
|
||||
</p>
|
||||
</li>
|
||||
|
|
@ -117,13 +117,13 @@
|
|||
}}
|
||||
>
|
||||
<input
|
||||
class="flex-1 rounded-lg border border-neutral-800 bg-neutral-900 px-3 py-1.5 text-sm"
|
||||
class="flex-1 rounded-lg border border-border bg-card px-3 py-1.5 text-sm"
|
||||
placeholder="Kommentar zur Karte…"
|
||||
bind:value={draft}
|
||||
disabled={posting}
|
||||
/>
|
||||
<button
|
||||
class="rounded-lg bg-indigo-500 px-3 py-1.5 text-xs text-white hover:bg-indigo-400 disabled:opacity-50"
|
||||
class="rounded-lg bg-app-accent px-3 py-1.5 text-xs text-white hover:bg-app-accent/90 disabled:opacity-50"
|
||||
type="submit"
|
||||
disabled={posting || !draft.trim()}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@
|
|||
case 'cloze': {
|
||||
const r = renderCloze(card.fields.text ?? '', subIndex);
|
||||
const extra = card.fields.extra
|
||||
? `<div class="mt-3 text-sm text-neutral-400">${renderMarkdown(card.fields.extra)}</div>`
|
||||
? `<div class="mt-3 text-sm text-muted-foreground">${renderMarkdown(card.fields.extra)}</div>`
|
||||
: '';
|
||||
return { prompt: r.front + extra, answer: r.back + extra, expected: r.answer };
|
||||
}
|
||||
|
|
@ -57,15 +57,13 @@
|
|||
</script>
|
||||
|
||||
<article class="space-y-4">
|
||||
<div
|
||||
class="card-content rounded-xl border border-neutral-800 bg-neutral-900 p-6 text-lg leading-relaxed"
|
||||
>
|
||||
<div class="card-content rounded-xl border border-border bg-card p-6 text-lg leading-relaxed">
|
||||
{@html view.prompt}
|
||||
</div>
|
||||
|
||||
{#if isTypeIn}
|
||||
<input
|
||||
class="w-full rounded-lg border border-neutral-700 bg-neutral-950 px-3 py-2 text-base outline-none focus:border-indigo-400"
|
||||
class="w-full rounded-lg border border-border-strong bg-background px-3 py-2 text-base outline-none focus:border-indigo-400"
|
||||
type="text"
|
||||
placeholder="Antwort eingeben…"
|
||||
value={typedAnswer}
|
||||
|
|
@ -80,8 +78,8 @@
|
|||
{isTypeIn
|
||||
? matched
|
||||
? 'border-green-500 bg-green-500/5'
|
||||
: 'border-red-500 bg-red-500/5'
|
||||
: 'border-indigo-500 bg-indigo-500/5'}"
|
||||
: 'border-red-500 bg-error/5'
|
||||
: 'border-indigo-500 bg-app-accent/5'}"
|
||||
>
|
||||
{@html view.answer}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -53,20 +53,20 @@
|
|||
|
||||
<section class="mt-10">
|
||||
<header class="mb-3 flex items-center justify-between">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-neutral-400">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
|
||||
Karten {cards.length > 0 ? `(${cards.length})` : ''}
|
||||
</h2>
|
||||
{#if loading}
|
||||
<span class="text-xs text-neutral-600">Lädt…</span>
|
||||
<span class="text-xs text-muted-foreground/60">Lädt…</span>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
{#if error}
|
||||
<p class="mb-3 rounded-lg border border-red-500/30 bg-red-500/10 p-3 text-sm text-red-400">
|
||||
<p class="mb-3 rounded-lg border border-error/30 bg-error/10 p-3 text-sm text-error">
|
||||
{error}
|
||||
</p>
|
||||
{:else if cards.length === 0 && !loading}
|
||||
<p class="rounded-xl border border-neutral-800 bg-neutral-900 p-4 text-sm text-neutral-500">
|
||||
<p class="rounded-xl border border-border bg-card p-4 text-sm text-muted-foreground/80">
|
||||
Diese Version enthält keine Karten.
|
||||
</p>
|
||||
{:else}
|
||||
|
|
@ -74,18 +74,18 @@
|
|||
{#each cards as c (c.contentHash)}
|
||||
{@const n = counts[c.contentHash] ?? 0}
|
||||
{@const isOpen = openHash === c.contentHash}
|
||||
<li class="rounded-xl border border-neutral-800 bg-neutral-900 p-3">
|
||||
<li class="rounded-xl border border-border bg-card p-3">
|
||||
<button
|
||||
class="flex w-full items-center justify-between gap-3 text-left"
|
||||
onclick={() => (openHash = isOpen ? null : c.contentHash)}
|
||||
>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-xs uppercase tracking-wide text-neutral-500">
|
||||
<div class="text-xs uppercase tracking-wide text-muted-foreground/80">
|
||||
#{c.ord + 1} · {c.type}
|
||||
</div>
|
||||
<div class="mt-1 truncate text-sm text-neutral-200">{preview(c)}</div>
|
||||
<div class="mt-1 truncate text-sm text-foreground/90">{preview(c)}</div>
|
||||
</div>
|
||||
<div class="shrink-0 text-xs text-neutral-500">
|
||||
<div class="shrink-0 text-xs text-muted-foreground/80">
|
||||
{#if n > 0}
|
||||
💬 {n}
|
||||
{:else}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@
|
|||
let { decks, emptyText = 'Noch keine Decks.' }: Props = $props();
|
||||
|
||||
function badgeClass(d: DeckSummary): string {
|
||||
if (d.owner.verifiedMana) return 'bg-emerald-500/15 text-emerald-300';
|
||||
if (d.owner.verifiedCommunity) return 'bg-amber-500/15 text-amber-300';
|
||||
if (d.owner.verifiedMana) return 'bg-success/15 text-success';
|
||||
if (d.owner.verifiedCommunity) return 'bg-amber-500/15 text-warning';
|
||||
return '';
|
||||
}
|
||||
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
</script>
|
||||
|
||||
{#if decks.length === 0}
|
||||
<p class="rounded-xl border border-neutral-800 bg-neutral-900 p-8 text-center text-sm text-neutral-400">
|
||||
<p class="rounded-xl border border-border bg-card p-8 text-center text-sm text-muted-foreground">
|
||||
{emptyText}
|
||||
</p>
|
||||
{:else}
|
||||
|
|
@ -30,24 +30,24 @@
|
|||
<li>
|
||||
<a
|
||||
href={`/d/${deck.slug}`}
|
||||
class="block rounded-xl border border-neutral-800 bg-neutral-900 p-4 transition-colors hover:border-neutral-700 hover:bg-neutral-800"
|
||||
class="block rounded-xl border border-border bg-card p-4 transition-colors hover:border-border-strong hover:bg-muted"
|
||||
>
|
||||
<div class="mb-1 flex items-start justify-between gap-3">
|
||||
<h3 class="font-semibold leading-tight">{deck.title}</h3>
|
||||
{#if deck.priceCredits > 0}
|
||||
<span class="rounded-full bg-amber-500/15 px-2 py-0.5 text-xs text-amber-300">
|
||||
<span class="rounded-full bg-amber-500/15 px-2 py-0.5 text-xs text-warning">
|
||||
{deck.priceCredits} 💎
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if deck.description}
|
||||
<p class="mb-2 line-clamp-2 text-xs text-neutral-400">{deck.description}</p>
|
||||
<p class="mb-2 line-clamp-2 text-xs text-muted-foreground">{deck.description}</p>
|
||||
{/if}
|
||||
<div class="flex flex-wrap items-center gap-2 text-xs text-neutral-500">
|
||||
<div class="flex flex-wrap items-center gap-2 text-xs text-muted-foreground/80">
|
||||
<!-- Author shows as text inside the deck-link; the deck card
|
||||
navigates to the deck page, the author profile is one
|
||||
hop further from there. Keeps HTML valid (no nested <a>). -->
|
||||
<span class="text-neutral-300">{deck.owner.displayName}</span>
|
||||
<span class="text-foreground/80">{deck.owner.displayName}</span>
|
||||
{#if badgeText(deck)}
|
||||
<span class="rounded-full px-1.5 py-0.5 {badgeClass(deck)}">{badgeText(deck)}</span>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -135,28 +135,28 @@
|
|||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div
|
||||
class="w-full max-w-lg rounded-xl border border-neutral-800 bg-neutral-900 p-6"
|
||||
class="w-full max-w-lg rounded-xl border border-border bg-card p-6"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div class="mb-4 flex items-start justify-between">
|
||||
<h2 class="text-xl font-semibold">Deck veröffentlichen</h2>
|
||||
<button
|
||||
onclick={onClose}
|
||||
class="text-neutral-500 hover:text-neutral-200"
|
||||
class="text-muted-foreground/80 hover:text-foreground/90"
|
||||
aria-label="Schließen">✕</button
|
||||
>
|
||||
</div>
|
||||
|
||||
{#if stage === 'loading'}
|
||||
<div class="py-8 text-center text-sm text-neutral-400">Lade Author-Profil…</div>
|
||||
<div class="py-8 text-center text-sm text-muted-foreground">Lade Author-Profil…</div>
|
||||
{:else if stage === 'become-author'}
|
||||
<div class="space-y-4 text-sm">
|
||||
<p class="text-neutral-300">
|
||||
<p class="text-foreground/80">
|
||||
Erstelle ein Author-Profil — andere User finden deine Decks unter
|
||||
<code class="rounded bg-neutral-800 px-1 text-xs">cards.mana.how/u/dein-slug</code>.
|
||||
<code class="rounded bg-muted px-1 text-xs">cards.mana.how/u/dein-slug</code>.
|
||||
</p>
|
||||
<div>
|
||||
<label for="author-slug" class="mb-1 block text-xs text-neutral-400">
|
||||
<label for="author-slug" class="mb-1 block text-xs text-muted-foreground">
|
||||
Slug (3–60 Zeichen, a–z, 0–9, -)
|
||||
</label>
|
||||
<input
|
||||
|
|
@ -164,35 +164,37 @@
|
|||
type="text"
|
||||
bind:value={authorSlug}
|
||||
placeholder="anna-lang"
|
||||
class="w-full rounded-lg border border-neutral-700 bg-neutral-950 px-3 py-2 text-sm outline-none focus:border-indigo-400"
|
||||
class="w-full rounded-lg border border-border-strong bg-background px-3 py-2 text-sm outline-none focus:border-indigo-400"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="author-name" class="mb-1 block text-xs text-neutral-400">Anzeigename</label>
|
||||
<label for="author-name" class="mb-1 block text-xs text-muted-foreground"
|
||||
>Anzeigename</label
|
||||
>
|
||||
<input
|
||||
id="author-name"
|
||||
type="text"
|
||||
bind:value={authorName}
|
||||
placeholder="Anna Lang"
|
||||
class="w-full rounded-lg border border-neutral-700 bg-neutral-950 px-3 py-2 text-sm outline-none focus:border-indigo-400"
|
||||
class="w-full rounded-lg border border-border-strong bg-background px-3 py-2 text-sm outline-none focus:border-indigo-400"
|
||||
/>
|
||||
</div>
|
||||
<label class="flex items-start gap-2 text-xs text-neutral-400">
|
||||
<label class="flex items-start gap-2 text-xs text-muted-foreground">
|
||||
<input type="checkbox" bind:checked={authorPseudonym} class="mt-0.5" />
|
||||
<span>Pseudonym — Anzeigename ist nicht mein Klarname</span>
|
||||
</label>
|
||||
{#if authorStore.error}
|
||||
<p class="text-red-400">{authorStore.error}</p>
|
||||
<p class="text-error">{authorStore.error}</p>
|
||||
{/if}
|
||||
<div class="flex justify-end gap-2 pt-2">
|
||||
<button
|
||||
class="rounded-lg px-3 py-1.5 text-sm text-neutral-400 hover:text-neutral-100"
|
||||
class="rounded-lg px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground"
|
||||
onclick={onClose}
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
class="rounded-lg bg-indigo-500 px-4 py-1.5 text-sm text-white hover:bg-indigo-400 disabled:opacity-50"
|
||||
class="rounded-lg bg-app-accent px-4 py-1.5 text-sm text-white hover:bg-app-accent/90 disabled:opacity-50"
|
||||
onclick={submitAuthor}
|
||||
disabled={!authorSlug.trim() || !authorName.trim() || authorStore.loading}
|
||||
>
|
||||
|
|
@ -202,45 +204,45 @@
|
|||
</div>
|
||||
{:else if stage === 'meta'}
|
||||
<div class="space-y-4 text-sm">
|
||||
<p class="text-neutral-400">
|
||||
Veröffentlicht als <code class="rounded bg-neutral-800 px-1 text-xs"
|
||||
<p class="text-muted-foreground">
|
||||
Veröffentlicht als <code class="rounded bg-muted px-1 text-xs"
|
||||
>cards.mana.how/d/{deckSlug || '...'}</code
|
||||
>
|
||||
</p>
|
||||
<div>
|
||||
<label for="d-slug" class="mb-1 block text-xs text-neutral-400">Slug</label>
|
||||
<label for="d-slug" class="mb-1 block text-xs text-muted-foreground">Slug</label>
|
||||
<input
|
||||
id="d-slug"
|
||||
type="text"
|
||||
bind:value={deckSlug}
|
||||
class="w-full rounded-lg border border-neutral-700 bg-neutral-950 px-3 py-2 text-sm outline-none focus:border-indigo-400"
|
||||
class="w-full rounded-lg border border-border-strong bg-background px-3 py-2 text-sm outline-none focus:border-indigo-400"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="d-title" class="mb-1 block text-xs text-neutral-400">Titel</label>
|
||||
<label for="d-title" class="mb-1 block text-xs text-muted-foreground">Titel</label>
|
||||
<input
|
||||
id="d-title"
|
||||
type="text"
|
||||
bind:value={deckTitle}
|
||||
class="w-full rounded-lg border border-neutral-700 bg-neutral-950 px-3 py-2 text-sm outline-none focus:border-indigo-400"
|
||||
class="w-full rounded-lg border border-border-strong bg-background px-3 py-2 text-sm outline-none focus:border-indigo-400"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="d-desc" class="mb-1 block text-xs text-neutral-400">Beschreibung</label>
|
||||
<label for="d-desc" class="mb-1 block text-xs text-muted-foreground">Beschreibung</label>
|
||||
<textarea
|
||||
id="d-desc"
|
||||
bind:value={deckDescription}
|
||||
placeholder="Worum geht es in diesem Deck?"
|
||||
class="min-h-[80px] w-full rounded-lg border border-neutral-700 bg-neutral-950 px-3 py-2 text-sm outline-none focus:border-indigo-400"
|
||||
class="min-h-[80px] w-full rounded-lg border border-border-strong bg-background px-3 py-2 text-sm outline-none focus:border-indigo-400"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label for="d-lang" class="mb-1 block text-xs text-neutral-400">Sprache</label>
|
||||
<label for="d-lang" class="mb-1 block text-xs text-muted-foreground">Sprache</label>
|
||||
<select
|
||||
id="d-lang"
|
||||
bind:value={deckLanguage}
|
||||
class="w-full rounded-lg border border-neutral-700 bg-neutral-950 px-3 py-2 text-sm outline-none focus:border-indigo-400"
|
||||
class="w-full rounded-lg border border-border-strong bg-background px-3 py-2 text-sm outline-none focus:border-indigo-400"
|
||||
>
|
||||
<option value="de">Deutsch</option>
|
||||
<option value="en">English</option>
|
||||
|
|
@ -252,11 +254,11 @@
|
|||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="d-license" class="mb-1 block text-xs text-neutral-400">Lizenz</label>
|
||||
<label for="d-license" class="mb-1 block text-xs text-muted-foreground">Lizenz</label>
|
||||
<select
|
||||
id="d-license"
|
||||
bind:value={deckLicense}
|
||||
class="w-full rounded-lg border border-neutral-700 bg-neutral-950 px-3 py-2 text-sm outline-none focus:border-indigo-400"
|
||||
class="w-full rounded-lg border border-border-strong bg-background px-3 py-2 text-sm outline-none focus:border-indigo-400"
|
||||
>
|
||||
<option value="CC-BY-4.0">CC-BY 4.0 — frei mit Namensnennung</option>
|
||||
<option value="CC-BY-SA-4.0">CC-BY-SA 4.0 — share-alike</option>
|
||||
|
|
@ -267,17 +269,17 @@
|
|||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label for="d-semver" class="mb-1 block text-xs text-neutral-400">Version</label>
|
||||
<label for="d-semver" class="mb-1 block text-xs text-muted-foreground">Version</label>
|
||||
<input
|
||||
id="d-semver"
|
||||
type="text"
|
||||
bind:value={deckSemver}
|
||||
placeholder="1.0.0"
|
||||
class="w-full rounded-lg border border-neutral-700 bg-neutral-950 px-3 py-2 text-sm outline-none focus:border-indigo-400"
|
||||
class="w-full rounded-lg border border-border-strong bg-background px-3 py-2 text-sm outline-none focus:border-indigo-400"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="d-changelog" class="mb-1 block text-xs text-neutral-400">
|
||||
<label for="d-changelog" class="mb-1 block text-xs text-muted-foreground">
|
||||
Changelog (optional)
|
||||
</label>
|
||||
<input
|
||||
|
|
@ -285,24 +287,24 @@
|
|||
type="text"
|
||||
bind:value={deckChangelog}
|
||||
placeholder="Erste Version"
|
||||
class="w-full rounded-lg border border-neutral-700 bg-neutral-950 px-3 py-2 text-sm outline-none focus:border-indigo-400"
|
||||
class="w-full rounded-lg border border-border-strong bg-background px-3 py-2 text-sm outline-none focus:border-indigo-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-neutral-500">
|
||||
<p class="text-xs text-muted-foreground/80">
|
||||
{cards.length}
|
||||
{cards.length === 1 ? 'Karte' : 'Karten'} werden veröffentlicht. Das Deck durchläuft eine KI-Inhaltsprüfung
|
||||
— offensichtlich harmloses Material geht direkt durch.
|
||||
</p>
|
||||
<div class="flex justify-end gap-2 pt-2">
|
||||
<button
|
||||
class="rounded-lg px-3 py-1.5 text-sm text-neutral-400 hover:text-neutral-100"
|
||||
class="rounded-lg px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground"
|
||||
onclick={onClose}
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
class="rounded-lg bg-indigo-500 px-4 py-1.5 text-sm text-white hover:bg-indigo-400 disabled:opacity-50"
|
||||
class="rounded-lg bg-app-accent px-4 py-1.5 text-sm text-white hover:bg-app-accent/90 disabled:opacity-50"
|
||||
onclick={submitPublish}
|
||||
disabled={!deckSlug.trim() || !deckTitle.trim() || cards.length === 0}
|
||||
>
|
||||
|
|
@ -311,7 +313,7 @@
|
|||
</div>
|
||||
</div>
|
||||
{:else if stage === 'publishing'}
|
||||
<div class="py-8 text-center text-sm text-neutral-400">
|
||||
<div class="py-8 text-center text-sm text-muted-foreground">
|
||||
Lade {cards.length} Karten hoch und prüfe Inhalt…
|
||||
</div>
|
||||
{:else if stage === 'done' && result}
|
||||
|
|
@ -319,18 +321,18 @@
|
|||
<div class="text-green-400">
|
||||
✓ Veröffentlicht als Version {result.version.semver}
|
||||
</div>
|
||||
<div class="text-neutral-300">
|
||||
<div class="text-foreground/80">
|
||||
{result.version.cardCount} Karten · Lizenz: {result.deck.license}
|
||||
</div>
|
||||
{#if result.moderation.verdict === 'flag'}
|
||||
<div class="rounded-lg border border-amber-500/30 bg-amber-500/10 p-3 text-amber-300">
|
||||
<div class="rounded-lg border border-amber-500/30 bg-amber-500/10 p-3 text-warning">
|
||||
Inhalt wurde zur Moderations-Prüfung markiert ({result.moderation.categories.join(
|
||||
', '
|
||||
)}). Das Deck ist veröffentlicht; ein Mensch schaut bei Gelegenheit drüber.
|
||||
</div>
|
||||
{/if}
|
||||
<button
|
||||
class="rounded-lg bg-indigo-500 px-4 py-1.5 text-sm text-white hover:bg-indigo-400"
|
||||
class="rounded-lg bg-app-accent px-4 py-1.5 text-sm text-white hover:bg-app-accent/90"
|
||||
onclick={onClose}
|
||||
>
|
||||
Fertig
|
||||
|
|
@ -338,9 +340,9 @@
|
|||
</div>
|
||||
{:else if stage === 'error'}
|
||||
<div class="space-y-3 text-sm">
|
||||
<div class="text-red-400">Fehler: {error}</div>
|
||||
<div class="text-error">Fehler: {error}</div>
|
||||
<button
|
||||
class="rounded-lg px-3 py-1.5 text-sm text-neutral-400 hover:text-neutral-100"
|
||||
class="rounded-lg px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground"
|
||||
onclick={() => (stage = 'meta')}
|
||||
>
|
||||
Erneut versuchen
|
||||
|
|
|
|||
|
|
@ -77,10 +77,10 @@
|
|||
}
|
||||
|
||||
function statusBadgeClass(s: PullRequest['status']) {
|
||||
if (s === 'open') return 'bg-emerald-500/15 text-emerald-300';
|
||||
if (s === 'open') return 'bg-success/15 text-success';
|
||||
if (s === 'merged') return 'bg-violet-500/15 text-violet-300';
|
||||
if (s === 'rejected') return 'bg-red-500/15 text-red-300';
|
||||
return 'bg-neutral-800 text-neutral-400';
|
||||
if (s === 'rejected') return 'bg-error/15 text-error';
|
||||
return 'bg-muted text-muted-foreground';
|
||||
}
|
||||
|
||||
function diffSummary(pr: PullRequest) {
|
||||
|
|
@ -90,11 +90,11 @@
|
|||
|
||||
<section class="mt-10">
|
||||
<header class="mb-3 flex items-center justify-between">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-neutral-400">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
|
||||
Pull Requests {prs.length > 0 ? `(${prs.length})` : ''}
|
||||
</h2>
|
||||
<button
|
||||
class="text-xs text-neutral-500 hover:text-neutral-300"
|
||||
class="text-xs text-muted-foreground/80 hover:text-foreground/80"
|
||||
onclick={load}
|
||||
disabled={loading}
|
||||
>
|
||||
|
|
@ -103,37 +103,37 @@
|
|||
</header>
|
||||
|
||||
{#if error}
|
||||
<p class="mb-3 rounded-lg border border-red-500/30 bg-red-500/10 p-3 text-sm text-red-400">
|
||||
<p class="mb-3 rounded-lg border border-error/30 bg-error/10 p-3 text-sm text-error">
|
||||
{error}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
{#if loading && prs.length === 0}
|
||||
<p class="rounded-xl border border-neutral-800 bg-neutral-900 p-4 text-sm text-neutral-500">
|
||||
<p class="rounded-xl border border-border bg-card p-4 text-sm text-muted-foreground/80">
|
||||
Lädt…
|
||||
</p>
|
||||
{:else if prs.length === 0}
|
||||
<p class="rounded-xl border border-neutral-800 bg-neutral-900 p-4 text-sm text-neutral-500">
|
||||
<p class="rounded-xl border border-border bg-card p-4 text-sm text-muted-foreground/80">
|
||||
Noch keine Pull Requests. Abonnenten können Verbesserungen vorschlagen.
|
||||
</p>
|
||||
{:else}
|
||||
<ul class="space-y-2">
|
||||
{#each prs as pr (pr.id)}
|
||||
<li class="rounded-xl border border-neutral-800 bg-neutral-900 p-4">
|
||||
<li class="rounded-xl border border-border bg-card p-4">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="rounded-full px-2 py-0.5 text-xs {statusBadgeClass(pr.status)}">
|
||||
{pr.status}
|
||||
</span>
|
||||
<h3 class="truncate font-medium text-neutral-100">{pr.title}</h3>
|
||||
<h3 class="truncate font-medium text-foreground">{pr.title}</h3>
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-neutral-500">
|
||||
<p class="mt-1 text-xs text-muted-foreground/80">
|
||||
{diffSummary(pr)} · {new Date(pr.createdAt).toLocaleDateString('de-DE')}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
class="shrink-0 text-xs text-neutral-500 hover:text-neutral-300"
|
||||
class="shrink-0 text-xs text-muted-foreground/80 hover:text-foreground/80"
|
||||
onclick={() => (expanded[pr.id] = !expanded[pr.id])}
|
||||
>
|
||||
{expanded[pr.id] ? 'Einklappen' : 'Details'}
|
||||
|
|
@ -142,22 +142,22 @@
|
|||
|
||||
{#if expanded[pr.id]}
|
||||
{#if pr.body}
|
||||
<p class="mt-3 whitespace-pre-line text-sm text-neutral-300">{pr.body}</p>
|
||||
<p class="mt-3 whitespace-pre-line text-sm text-foreground/80">{pr.body}</p>
|
||||
{/if}
|
||||
|
||||
{#if pr.diff.modify.length > 0}
|
||||
<div class="mt-3">
|
||||
<div class="mb-1 text-xs uppercase text-neutral-500">Geändert</div>
|
||||
<div class="mb-1 text-xs uppercase text-muted-foreground/80">Geändert</div>
|
||||
<ul class="space-y-2">
|
||||
{#each pr.diff.modify as m (m.contentHash)}
|
||||
<li class="rounded-lg border border-neutral-800 bg-neutral-950 p-2 text-xs">
|
||||
<div class="text-neutral-500">
|
||||
<li class="rounded-lg border border-border bg-background p-2 text-xs">
|
||||
<div class="text-muted-foreground/80">
|
||||
← {m.contentHash.slice(0, 12)}…
|
||||
</div>
|
||||
{#each Object.entries(m.fields) as [k, v]}
|
||||
<div class="mt-1">
|
||||
<span class="text-neutral-500">{k}:</span>
|
||||
<span class="text-neutral-200">{v}</span>
|
||||
<span class="text-muted-foreground/80">{k}:</span>
|
||||
<span class="text-foreground/90">{v}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</li>
|
||||
|
|
@ -168,17 +168,17 @@
|
|||
|
||||
{#if pr.diff.add.length > 0}
|
||||
<div class="mt-3">
|
||||
<div class="mb-1 text-xs uppercase text-neutral-500">
|
||||
<div class="mb-1 text-xs uppercase text-muted-foreground/80">
|
||||
Neu (+{pr.diff.add.length})
|
||||
</div>
|
||||
<ul class="space-y-2">
|
||||
{#each pr.diff.add as a, i (i)}
|
||||
<li class="rounded-lg border border-neutral-800 bg-neutral-950 p-2 text-xs">
|
||||
<div class="text-neutral-500">{a.type}</div>
|
||||
<li class="rounded-lg border border-border bg-background p-2 text-xs">
|
||||
<div class="text-muted-foreground/80">{a.type}</div>
|
||||
{#each Object.entries(a.fields) as [k, v]}
|
||||
<div class="mt-1">
|
||||
<span class="text-neutral-500">{k}:</span>
|
||||
<span class="text-neutral-200">{v}</span>
|
||||
<span class="text-muted-foreground/80">{k}:</span>
|
||||
<span class="text-foreground/90">{v}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</li>
|
||||
|
|
@ -189,10 +189,10 @@
|
|||
|
||||
{#if pr.diff.remove.length > 0}
|
||||
<div class="mt-3">
|
||||
<div class="mb-1 text-xs uppercase text-neutral-500">
|
||||
<div class="mb-1 text-xs uppercase text-muted-foreground/80">
|
||||
Entfernt (−{pr.diff.remove.length})
|
||||
</div>
|
||||
<ul class="space-y-1 text-xs text-neutral-400">
|
||||
<ul class="space-y-1 text-xs text-muted-foreground">
|
||||
{#each pr.diff.remove as r (r.contentHash)}
|
||||
<li>· {r.contentHash.slice(0, 12)}…</li>
|
||||
{/each}
|
||||
|
|
@ -210,14 +210,14 @@
|
|||
{actionBusy === pr.id ? 'Mergt…' : 'Mergen'}
|
||||
</button>
|
||||
<button
|
||||
class="rounded-lg border border-red-500/40 px-3 py-1.5 text-xs text-red-300 hover:bg-red-500/10 disabled:opacity-50"
|
||||
class="rounded-lg border border-error/40 px-3 py-1.5 text-xs text-error hover:bg-error/10 disabled:opacity-50"
|
||||
onclick={() => reject(pr)}
|
||||
disabled={actionBusy === pr.id}
|
||||
>
|
||||
Ablehnen
|
||||
</button>
|
||||
<button
|
||||
class="rounded-lg border border-neutral-700 px-3 py-1.5 text-xs hover:bg-neutral-800 disabled:opacity-50"
|
||||
class="rounded-lg border border-border-strong px-3 py-1.5 text-xs hover:bg-muted disabled:opacity-50"
|
||||
onclick={() => close(pr)}
|
||||
disabled={actionBusy === pr.id}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@
|
|||
{#if authStore.isAuthenticated}
|
||||
{#if variant === 'icon'}
|
||||
<button
|
||||
class="text-xs text-neutral-600 hover:text-amber-300"
|
||||
class="text-xs text-muted-foreground/60 hover:text-warning"
|
||||
onclick={() => (open = true)}
|
||||
title="Melden"
|
||||
aria-label="Melden"
|
||||
|
|
@ -64,7 +64,10 @@
|
|||
🚩
|
||||
</button>
|
||||
{:else}
|
||||
<button class="text-xs text-neutral-500 hover:text-amber-300" onclick={() => (open = true)}>
|
||||
<button
|
||||
class="text-xs text-muted-foreground/80 hover:text-warning"
|
||||
onclick={() => (open = true)}
|
||||
>
|
||||
🚩 Melden
|
||||
</button>
|
||||
{/if}
|
||||
|
|
@ -76,29 +79,27 @@
|
|||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<div class="w-full max-w-md rounded-xl border border-neutral-800 bg-neutral-950 p-5">
|
||||
<div class="w-full max-w-md rounded-xl border border-border bg-background p-5">
|
||||
<header class="mb-4 flex items-center justify-between">
|
||||
<h2 class="text-base font-semibold">
|
||||
{cardContentHash ? 'Karte melden' : 'Deck melden'}
|
||||
</h2>
|
||||
<button
|
||||
class="text-neutral-400 hover:text-neutral-100"
|
||||
class="text-muted-foreground hover:text-foreground"
|
||||
onclick={close}
|
||||
aria-label="Schließen">✕</button
|
||||
>
|
||||
</header>
|
||||
|
||||
{#if done}
|
||||
<p
|
||||
class="rounded-lg border border-emerald-500/30 bg-emerald-500/10 p-3 text-sm text-emerald-300"
|
||||
>
|
||||
<p class="rounded-lg border border-success/30 bg-success/10 p-3 text-sm text-success">
|
||||
Danke — die Moderation prüft den Bericht.
|
||||
</p>
|
||||
{:else}
|
||||
<label class="mb-3 block">
|
||||
<span class="mb-1 block text-xs text-neutral-400">Kategorie</span>
|
||||
<span class="mb-1 block text-xs text-muted-foreground">Kategorie</span>
|
||||
<select
|
||||
class="w-full rounded-lg border border-neutral-800 bg-neutral-900 px-3 py-2 text-sm"
|
||||
class="w-full rounded-lg border border-border bg-card px-3 py-2 text-sm"
|
||||
bind:value={category}
|
||||
>
|
||||
{#each CATEGORIES as c (c.value)}
|
||||
|
|
@ -108,9 +109,9 @@
|
|||
</label>
|
||||
|
||||
<label class="mb-4 block">
|
||||
<span class="mb-1 block text-xs text-neutral-400">Begründung (optional)</span>
|
||||
<span class="mb-1 block text-xs text-muted-foreground">Begründung (optional)</span>
|
||||
<textarea
|
||||
class="w-full rounded-lg border border-neutral-800 bg-neutral-900 px-3 py-2 text-sm"
|
||||
class="w-full rounded-lg border border-border bg-card px-3 py-2 text-sm"
|
||||
rows="3"
|
||||
bind:value={body}
|
||||
placeholder="Was stimmt nicht?"
|
||||
|
|
@ -118,12 +119,12 @@
|
|||
</label>
|
||||
|
||||
{#if error}
|
||||
<p class="mb-3 text-sm text-red-400">{error}</p>
|
||||
<p class="mb-3 text-sm text-error">{error}</p>
|
||||
{/if}
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<button
|
||||
class="rounded-lg border border-neutral-800 px-4 py-2 text-sm hover:border-neutral-700"
|
||||
class="rounded-lg border border-border px-4 py-2 text-sm hover:border-border-strong"
|
||||
onclick={close}
|
||||
disabled={busy}>Abbrechen</button
|
||||
>
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@
|
|||
const max = $derived(rawDays.reduce((m, d) => Math.max(m, d.count), 0));
|
||||
|
||||
function bucket(count: number): string {
|
||||
if (count === 0) return 'bg-neutral-800';
|
||||
if (count === 0) return 'bg-muted';
|
||||
if (count <= Math.max(1, max * 0.25)) return 'bg-emerald-900';
|
||||
if (count <= max * 0.5) return 'bg-emerald-700';
|
||||
if (count <= max * 0.75) return 'bg-emerald-500';
|
||||
|
|
@ -58,10 +58,10 @@
|
|||
const activeDays = $derived(rawDays.filter((d) => d.count > 0).length);
|
||||
</script>
|
||||
|
||||
<div class="rounded-xl border border-neutral-800 bg-neutral-900 p-4">
|
||||
<div class="rounded-xl border border-border bg-card p-4">
|
||||
<div class="mb-3 flex items-center justify-between text-sm">
|
||||
<span class="font-medium">Lernaktivität</span>
|
||||
<span class="text-xs text-neutral-500">
|
||||
<span class="text-xs text-muted-foreground/80">
|
||||
{total} Karten · {activeDays} aktive {activeDays === 1 ? 'Tag' : 'Tage'} · letzte {weeks} Wochen
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -81,9 +81,9 @@
|
|||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="mt-3 flex items-center gap-1 text-xs text-neutral-500">
|
||||
<div class="mt-3 flex items-center gap-1 text-xs text-muted-foreground/80">
|
||||
<span>weniger</span>
|
||||
<span class="ml-1 h-3 w-3 rounded-sm bg-neutral-800"></span>
|
||||
<span class="ml-1 h-3 w-3 rounded-sm bg-muted"></span>
|
||||
<span class="h-3 w-3 rounded-sm bg-emerald-900"></span>
|
||||
<span class="h-3 w-3 rounded-sm bg-emerald-700"></span>
|
||||
<span class="h-3 w-3 rounded-sm bg-emerald-500"></span>
|
||||
|
|
|
|||
|
|
@ -96,40 +96,38 @@
|
|||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<div class="w-full max-w-xl rounded-xl border border-neutral-800 bg-neutral-950 p-6">
|
||||
<div class="w-full max-w-xl rounded-xl border border-border bg-background p-6">
|
||||
<header class="mb-4 flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold">Verbesserung vorschlagen</h2>
|
||||
<button
|
||||
class="text-neutral-400 hover:text-neutral-100"
|
||||
class="text-muted-foreground hover:text-foreground"
|
||||
onclick={onClose}
|
||||
aria-label="Schließen">✕</button
|
||||
>
|
||||
</header>
|
||||
|
||||
{#if success}
|
||||
<p
|
||||
class="rounded-lg border border-emerald-500/30 bg-emerald-500/10 p-3 text-sm text-emerald-300"
|
||||
>
|
||||
<p class="rounded-lg border border-success/30 bg-success/10 p-3 text-sm text-success">
|
||||
Pull Request gesendet — der Author wird benachrichtigt.
|
||||
</p>
|
||||
{:else}
|
||||
<div class="mb-4 inline-flex rounded-lg border border-neutral-800 p-1">
|
||||
<div class="mb-4 inline-flex rounded-lg border border-border p-1">
|
||||
<button
|
||||
class="rounded px-3 py-1 text-xs"
|
||||
class:bg-neutral-800={mode === 'modify'}
|
||||
class:bg-muted={mode === 'modify'}
|
||||
onclick={() => (mode = 'modify')}>Inhalt ändern</button
|
||||
>
|
||||
<button
|
||||
class="rounded px-3 py-1 text-xs"
|
||||
class:bg-neutral-800={mode === 'remove'}
|
||||
class:bg-muted={mode === 'remove'}
|
||||
onclick={() => (mode = 'remove')}>Karte entfernen</button
|
||||
>
|
||||
</div>
|
||||
|
||||
<label class="mb-3 block">
|
||||
<span class="mb-1 block text-xs text-neutral-400">Titel</span>
|
||||
<span class="mb-1 block text-xs text-muted-foreground">Titel</span>
|
||||
<input
|
||||
class="w-full rounded-lg border border-neutral-800 bg-neutral-900 px-3 py-2 text-sm"
|
||||
class="w-full rounded-lg border border-border bg-card px-3 py-2 text-sm"
|
||||
bind:value={title}
|
||||
placeholder="Kurzbeschreibung der Verbesserung"
|
||||
/>
|
||||
|
|
@ -139,9 +137,9 @@
|
|||
<div class="mb-3 space-y-2">
|
||||
{#each fieldKeys as key (key)}
|
||||
<label class="block">
|
||||
<span class="mb-1 block text-xs text-neutral-400">{key}</span>
|
||||
<span class="mb-1 block text-xs text-muted-foreground">{key}</span>
|
||||
<textarea
|
||||
class="w-full rounded-lg border border-neutral-800 bg-neutral-900 px-3 py-2 text-sm"
|
||||
class="w-full rounded-lg border border-border bg-card px-3 py-2 text-sm"
|
||||
rows="2"
|
||||
bind:value={editedFields[key]}
|
||||
></textarea>
|
||||
|
|
@ -150,16 +148,16 @@
|
|||
</div>
|
||||
{:else}
|
||||
<p
|
||||
class="mb-3 rounded-lg border border-amber-500/30 bg-amber-500/10 p-3 text-sm text-amber-200"
|
||||
class="mb-3 rounded-lg border border-amber-500/30 bg-amber-500/10 p-3 text-sm text-warning"
|
||||
>
|
||||
Diese Karte wird beim Merge aus dem Deck entfernt.
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<label class="mb-4 block">
|
||||
<span class="mb-1 block text-xs text-neutral-400">Begründung (optional)</span>
|
||||
<span class="mb-1 block text-xs text-muted-foreground">Begründung (optional)</span>
|
||||
<textarea
|
||||
class="w-full rounded-lg border border-neutral-800 bg-neutral-900 px-3 py-2 text-sm"
|
||||
class="w-full rounded-lg border border-border bg-card px-3 py-2 text-sm"
|
||||
rows="3"
|
||||
bind:value={body}
|
||||
placeholder="Warum ist diese Änderung sinnvoll?"
|
||||
|
|
@ -167,17 +165,17 @@
|
|||
</label>
|
||||
|
||||
{#if error}
|
||||
<p class="mb-3 text-sm text-red-400">{error}</p>
|
||||
<p class="mb-3 text-sm text-error">{error}</p>
|
||||
{/if}
|
||||
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<button
|
||||
class="rounded-lg border border-neutral-800 px-4 py-2 text-sm hover:border-neutral-700"
|
||||
class="rounded-lg border border-border px-4 py-2 text-sm hover:border-border-strong"
|
||||
onclick={onClose}
|
||||
disabled={busy}>Abbrechen</button
|
||||
>
|
||||
<button
|
||||
class="rounded-lg bg-indigo-500 px-4 py-2 text-sm text-white hover:bg-indigo-400 disabled:opacity-50"
|
||||
class="rounded-lg bg-app-accent px-4 py-2 text-sm text-white hover:bg-app-accent/90 disabled:opacity-50"
|
||||
onclick={submit}
|
||||
disabled={busy || !hasChanges}
|
||||
>
|
||||
|
|
|
|||
33
apps/cards/apps/web/src/lib/stores/theme.ts
Normal file
33
apps/cards/apps/web/src/lib/stores/theme.ts
Normal 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);
|
||||
}
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
<script lang="ts">
|
||||
import '../app.css';
|
||||
import type { Snippet } from 'svelte';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { AuthGate } from '@mana/shared-auth-ui';
|
||||
import ThemeToggle from '@mana/shared-theme-ui/ThemeToggle.svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { theme, applyCardsAccent } from '$lib/stores/theme';
|
||||
import { startSync, stopSync } from '$lib/data/sync';
|
||||
import { useStreak } from '$lib/queries';
|
||||
import { pwaInfo } from 'virtual:pwa-info';
|
||||
|
|
@ -35,6 +37,14 @@
|
|||
// manifest → no install icon, no A2HS on mobile.
|
||||
const webManifestLink = $derived(pwaInfo?.webManifest.linkTag ?? '');
|
||||
|
||||
onMount(() => {
|
||||
// Apply the Cards brand accent once at boot. The shared theme
|
||||
// store handles light/dark + variant via createThemeStore above
|
||||
// (ran during module init); this just sets --color-app-accent
|
||||
// so `bg-app-accent` etc. resolve to Cards' violet.
|
||||
applyCardsAccent();
|
||||
});
|
||||
|
||||
onDestroy(() => stopSync());
|
||||
</script>
|
||||
|
||||
|
|
@ -46,25 +56,26 @@
|
|||
{@render children()}
|
||||
{:else}
|
||||
<AuthGate {authStore} {goto} onReady={handleAuthReady}>
|
||||
<header class="border-b border-neutral-900">
|
||||
<header class="border-b border-border">
|
||||
<div class="mx-auto flex max-w-3xl items-center justify-between px-6 py-3">
|
||||
<a href="/" class="flex items-center gap-2 text-sm font-semibold tracking-tight">
|
||||
<span class="text-base">🃏</span> Cards
|
||||
</a>
|
||||
<nav class="flex items-center gap-4 text-xs text-neutral-400">
|
||||
<a href="/" class="hover:text-neutral-100">Meine Decks</a>
|
||||
<a href="/explore" class="hover:text-neutral-100">Entdecken</a>
|
||||
<a href="/me/purchases" class="hover:text-neutral-100">Käufe</a>
|
||||
<nav class="flex items-center gap-4 text-xs text-muted-foreground">
|
||||
<a href="/" class="hover:text-foreground">Meine Decks</a>
|
||||
<a href="/explore" class="hover:text-foreground">Entdecken</a>
|
||||
<a href="/me/purchases" class="hover:text-foreground">Käufe</a>
|
||||
</nav>
|
||||
<div class="flex items-center gap-3 text-xs text-neutral-500">
|
||||
<div class="flex items-center gap-3 text-xs text-muted-foreground">
|
||||
{#if streak > 0}
|
||||
<span
|
||||
class="inline-flex items-center gap-1 rounded-full bg-orange-500/15 px-2 py-0.5 text-orange-300"
|
||||
class="inline-flex items-center gap-1 rounded-full bg-warning/15 px-2 py-0.5 text-warning"
|
||||
title="{streak} {streak === 1 ? 'Tag' : 'Tage'} in Folge gelernt"
|
||||
>
|
||||
🔥 {streak}
|
||||
</span>
|
||||
{/if}
|
||||
<ThemeToggle {theme} size={16} />
|
||||
{#if authStore.user?.email}
|
||||
<span class="hidden sm:inline">{authStore.user.email}</span>
|
||||
{/if}
|
||||
|
|
@ -74,7 +85,7 @@
|
|||
await authStore.signOut();
|
||||
goto('/login');
|
||||
}}
|
||||
class="rounded-md border border-neutral-800 px-2 py-1 hover:border-neutral-700 hover:text-neutral-100"
|
||||
class="rounded-md border border-border px-2 py-1 hover:border-border-strong hover:text-foreground"
|
||||
>
|
||||
Abmelden
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -43,15 +43,15 @@
|
|||
<header class="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-semibold tracking-tight">Cards</h1>
|
||||
<p class="text-sm text-neutral-400">
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{decks.length}
|
||||
{decks.length === 1 ? 'Deck' : 'Decks'}{#if totalDue > 0}
|
||||
· <span class="text-amber-400">{totalDue} fällig</span>
|
||||
· <span class="text-warning">{totalDue} fällig</span>
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
class="rounded-lg bg-indigo-500 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-400"
|
||||
class="rounded-lg bg-app-accent px-4 py-2 text-sm font-medium text-white hover:bg-app-accent/90"
|
||||
onclick={() => (showNew = true)}
|
||||
>
|
||||
Neues Deck
|
||||
|
|
@ -60,7 +60,7 @@
|
|||
|
||||
{#if showNew}
|
||||
<form
|
||||
class="mb-6 space-y-3 rounded-xl border border-neutral-800 bg-neutral-900 p-4"
|
||||
class="mb-6 space-y-3 rounded-xl border border-border bg-card p-4"
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleCreate();
|
||||
|
|
@ -71,19 +71,19 @@
|
|||
type="text"
|
||||
bind:value={newTitle}
|
||||
placeholder="Titel (z.B. Spanisch Vokabeln)"
|
||||
class="w-full rounded-lg border border-neutral-700 bg-neutral-950 px-3 py-2 text-sm outline-none focus:border-indigo-400"
|
||||
class="w-full rounded-lg border border-border-strong bg-background px-3 py-2 text-sm outline-none focus:border-indigo-400"
|
||||
autofocus
|
||||
required
|
||||
/>
|
||||
<textarea
|
||||
bind:value={newDesc}
|
||||
placeholder="Beschreibung (optional)"
|
||||
class="min-h-[60px] w-full rounded-lg border border-neutral-700 bg-neutral-950 px-3 py-2 text-sm outline-none focus:border-indigo-400"
|
||||
class="min-h-[60px] w-full rounded-lg border border-border-strong bg-background px-3 py-2 text-sm outline-none focus:border-indigo-400"
|
||||
></textarea>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-lg px-3 py-1.5 text-sm text-neutral-400 hover:text-neutral-100"
|
||||
class="rounded-lg px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground"
|
||||
onclick={() => {
|
||||
showNew = false;
|
||||
newTitle = '';
|
||||
|
|
@ -94,7 +94,7 @@
|
|||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-lg bg-indigo-500 px-4 py-1.5 text-sm text-white hover:bg-indigo-400 disabled:opacity-50"
|
||||
class="rounded-lg bg-app-accent px-4 py-1.5 text-sm text-white hover:bg-app-accent/90 disabled:opacity-50"
|
||||
disabled={!newTitle.trim() || creating}
|
||||
>
|
||||
{creating ? 'Lege an…' : 'Anlegen'}
|
||||
|
|
@ -104,11 +104,11 @@
|
|||
{/if}
|
||||
|
||||
{#if decks.length === 0 && !showNew}
|
||||
<div class="rounded-xl border border-neutral-800 bg-neutral-900 p-10 text-center">
|
||||
<div class="rounded-xl border border-border bg-card p-10 text-center">
|
||||
<div class="mb-3 text-4xl">🃏</div>
|
||||
<p class="text-neutral-400">Noch keine Decks. Leg dein erstes an.</p>
|
||||
<p class="text-muted-foreground">Noch keine Decks. Leg dein erstes an.</p>
|
||||
<button
|
||||
class="mt-4 rounded-lg bg-indigo-500 px-4 py-2 text-sm text-white hover:bg-indigo-400"
|
||||
class="mt-4 rounded-lg bg-app-accent px-4 py-2 text-sm text-white hover:bg-app-accent/90"
|
||||
onclick={() => (showNew = true)}
|
||||
>
|
||||
Erstes Deck anlegen
|
||||
|
|
@ -121,21 +121,21 @@
|
|||
<li>
|
||||
<a
|
||||
href={`/decks/${deck.id}`}
|
||||
class="flex items-center gap-3 rounded-xl border border-neutral-800 bg-neutral-900 px-4 py-3 transition-colors hover:border-neutral-700 hover:bg-neutral-800"
|
||||
class="flex items-center gap-3 rounded-xl border border-border bg-card px-4 py-3 transition-colors hover:border-border-strong hover:bg-muted"
|
||||
>
|
||||
<span class="h-3 w-3 shrink-0 rounded-full" style="background: {deck.color}"></span>
|
||||
<span class="flex-1 truncate">
|
||||
<span class="block font-medium">{deck.title}</span>
|
||||
{#if deck.description}
|
||||
<span class="block truncate text-xs text-neutral-400">{deck.description}</span>
|
||||
<span class="block truncate text-xs text-muted-foreground">{deck.description}</span>
|
||||
{/if}
|
||||
</span>
|
||||
{#if due > 0}
|
||||
<span class="rounded-full bg-amber-500/15 px-2 py-0.5 text-xs text-amber-400">
|
||||
<span class="rounded-full bg-amber-500/15 px-2 py-0.5 text-xs text-warning">
|
||||
{due} fällig
|
||||
</span>
|
||||
{/if}
|
||||
<span class="text-xs text-neutral-500">{deck.cardCount}</span>
|
||||
<span class="text-xs text-muted-foreground/80">{deck.cardCount}</span>
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
|
|
@ -150,5 +150,7 @@
|
|||
<AnkiImport />
|
||||
</div>
|
||||
|
||||
<p class="mt-12 text-center text-xs text-neutral-600">Phase 1 · synct mit mana.how/cards</p>
|
||||
<p class="mt-12 text-center text-xs text-muted-foreground/60">
|
||||
Phase 1 · synct mit mana.how/cards
|
||||
</p>
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -57,12 +57,12 @@
|
|||
|
||||
function badgeClass(c: DeckReportItem['category']) {
|
||||
const map: Record<DeckReportItem['category'], string> = {
|
||||
spam: 'bg-amber-500/15 text-amber-300',
|
||||
spam: 'bg-amber-500/15 text-warning',
|
||||
copyright: 'bg-blue-500/15 text-blue-300',
|
||||
nsfw: 'bg-pink-500/15 text-pink-300',
|
||||
misinformation: 'bg-violet-500/15 text-violet-300',
|
||||
hate: 'bg-red-500/15 text-red-300',
|
||||
other: 'bg-neutral-800 text-neutral-300',
|
||||
hate: 'bg-error/15 text-error',
|
||||
other: 'bg-muted text-foreground/80',
|
||||
};
|
||||
return map[c];
|
||||
}
|
||||
|
|
@ -76,34 +76,34 @@
|
|||
<header class="mb-6 flex items-center justify-between">
|
||||
<h1 class="text-2xl font-semibold tracking-tight">Moderation-Inbox</h1>
|
||||
{#if stage === 'ok'}
|
||||
<button class="text-xs text-neutral-500 hover:text-neutral-200" onclick={load}>
|
||||
<button class="text-xs text-muted-foreground/80 hover:text-foreground/90" onclick={load}>
|
||||
Aktualisieren
|
||||
</button>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
{#if stage === 'loading'}
|
||||
<p class="py-12 text-center text-sm text-neutral-400">Lädt…</p>
|
||||
<p class="py-12 text-center text-sm text-muted-foreground">Lädt…</p>
|
||||
{:else if stage === 'forbidden' || !isAdmin}
|
||||
<p
|
||||
class="rounded-xl border border-neutral-800 bg-neutral-900 p-8 text-center text-sm text-neutral-400"
|
||||
class="rounded-xl border border-border bg-card p-8 text-center text-sm text-muted-foreground"
|
||||
>
|
||||
Nur Admins haben Zugang zur Moderation-Inbox.
|
||||
</p>
|
||||
{:else if stage === 'error'}
|
||||
<p class="rounded-lg border border-red-500/30 bg-red-500/10 p-4 text-sm text-red-400">
|
||||
<p class="rounded-lg border border-error/30 bg-error/10 p-4 text-sm text-error">
|
||||
{error}
|
||||
</p>
|
||||
{:else if reports.length === 0}
|
||||
<p
|
||||
class="rounded-xl border border-neutral-800 bg-neutral-900 p-8 text-center text-sm text-neutral-500"
|
||||
class="rounded-xl border border-border bg-card p-8 text-center text-sm text-muted-foreground/80"
|
||||
>
|
||||
Keine offenen Reports.
|
||||
</p>
|
||||
{:else}
|
||||
<ul class="space-y-3">
|
||||
{#each reports as r (r.id)}
|
||||
<li class="rounded-xl border border-neutral-800 bg-neutral-900 p-4">
|
||||
<li class="rounded-xl border border-border bg-card p-4">
|
||||
<header class="mb-2 flex items-start justify-between gap-2">
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
|
|
@ -112,17 +112,17 @@
|
|||
</span>
|
||||
<a
|
||||
href="/d/{r.deckSlug}"
|
||||
class="truncate text-sm font-medium hover:text-indigo-300"
|
||||
class="truncate text-sm font-medium hover:text-app-accent"
|
||||
>
|
||||
{r.deckTitle}
|
||||
</a>
|
||||
{#if r.cardContentHash}
|
||||
<span class="text-xs text-neutral-500"
|
||||
<span class="text-xs text-muted-foreground/80"
|
||||
>· Karte {r.cardContentHash.slice(0, 8)}…</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-neutral-500">
|
||||
<p class="mt-1 text-xs text-muted-foreground/80">
|
||||
{new Date(r.createdAt).toLocaleString('de-DE')}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -130,19 +130,19 @@
|
|||
|
||||
{#if r.body}
|
||||
<p
|
||||
class="mb-3 whitespace-pre-line rounded-lg bg-neutral-950 p-2 text-sm text-neutral-300"
|
||||
class="mb-3 whitespace-pre-line rounded-lg bg-background p-2 text-sm text-foreground/80"
|
||||
>
|
||||
{r.body}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
{#if error}
|
||||
<p class="mb-2 text-xs text-red-400">{error}</p>
|
||||
<p class="mb-2 text-xs text-error">{error}</p>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
class="rounded-lg border border-neutral-700 px-3 py-1.5 text-xs hover:bg-neutral-800 disabled:opacity-50"
|
||||
class="rounded-lg border border-border-strong px-3 py-1.5 text-xs hover:bg-muted disabled:opacity-50"
|
||||
onclick={() => resolve(r, 'dismiss')}
|
||||
disabled={busy === r.id}
|
||||
>
|
||||
|
|
@ -156,7 +156,7 @@
|
|||
Deck entfernen
|
||||
</button>
|
||||
<button
|
||||
class="rounded-lg bg-red-500 px-3 py-1.5 text-xs text-white hover:bg-red-400 disabled:opacity-50"
|
||||
class="rounded-lg bg-error px-3 py-1.5 text-xs text-white hover:bg-error/90 disabled:opacity-50"
|
||||
onclick={() => resolve(r, 'ban-author')}
|
||||
disabled={busy === r.id}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -126,15 +126,15 @@
|
|||
|
||||
<main class="mx-auto max-w-3xl px-6 py-8">
|
||||
{#if stage === 'loading'}
|
||||
<p class="py-12 text-center text-sm text-neutral-400">Lade Deck…</p>
|
||||
<p class="py-12 text-center text-sm text-muted-foreground">Lade Deck…</p>
|
||||
{:else if stage === 'not-found'}
|
||||
<p
|
||||
class="rounded-xl border border-neutral-800 bg-neutral-900 p-8 text-center text-sm text-neutral-400"
|
||||
class="rounded-xl border border-border bg-card p-8 text-center text-sm text-muted-foreground"
|
||||
>
|
||||
Deck <code class="rounded bg-neutral-800 px-1">{slug}</code> existiert nicht.
|
||||
Deck <code class="rounded bg-muted px-1">{slug}</code> existiert nicht.
|
||||
</p>
|
||||
{:else if stage === 'error'}
|
||||
<p class="rounded-lg border border-red-500/30 bg-red-500/10 p-4 text-sm text-red-400">
|
||||
<p class="rounded-lg border border-error/30 bg-error/10 p-4 text-sm text-error">
|
||||
{error}
|
||||
</p>
|
||||
{:else if deck}
|
||||
|
|
@ -142,41 +142,41 @@
|
|||
<header class="mb-6">
|
||||
<h1 class="text-3xl font-semibold tracking-tight">{deck.title}</h1>
|
||||
{#if deck.description}
|
||||
<p class="mt-2 text-sm text-neutral-400">{deck.description}</p>
|
||||
<p class="mt-2 text-sm text-muted-foreground">{deck.description}</p>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
<div class="mb-6 flex flex-wrap items-center gap-3 text-sm">
|
||||
{#if version}
|
||||
<span class="rounded-full bg-neutral-800 px-2 py-0.5 text-xs text-neutral-300">
|
||||
<span class="rounded-full bg-muted px-2 py-0.5 text-xs text-foreground/80">
|
||||
v{version.semver}
|
||||
</span>
|
||||
<span class="text-neutral-400">{version.cardCount} Karten</span>
|
||||
<span class="text-muted-foreground">{version.cardCount} Karten</span>
|
||||
{/if}
|
||||
<span class="text-neutral-400">{deck.license}</span>
|
||||
<span class="text-muted-foreground">{deck.license}</span>
|
||||
{#if deck.language}
|
||||
<span class="text-neutral-400">{deck.language.toUpperCase()}</span>
|
||||
<span class="text-muted-foreground">{deck.language.toUpperCase()}</span>
|
||||
{/if}
|
||||
{#if deck.priceCredits > 0}
|
||||
<span class="rounded-full bg-amber-500/15 px-2 py-0.5 text-xs text-amber-300">
|
||||
<span class="rounded-full bg-amber-500/15 px-2 py-0.5 text-xs text-warning">
|
||||
{deck.priceCredits} 💎
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if version?.changelog}
|
||||
<section class="mb-6 rounded-xl border border-neutral-800 bg-neutral-900 p-4">
|
||||
<h2 class="mb-1 text-xs font-medium uppercase tracking-wide text-neutral-500">
|
||||
<section class="mb-6 rounded-xl border border-border bg-card p-4">
|
||||
<h2 class="mb-1 text-xs font-medium uppercase tracking-wide text-muted-foreground/80">
|
||||
Changelog v{version.semver}
|
||||
</h2>
|
||||
<p class="whitespace-pre-line text-sm text-neutral-300">{version.changelog}</p>
|
||||
<p class="whitespace-pre-line text-sm text-foreground/80">{version.changelog}</p>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
{#if authStore.isAuthenticated}
|
||||
<button
|
||||
class="rounded-lg border border-indigo-500/40 px-4 py-2 text-sm text-indigo-300 hover:bg-indigo-500/10 disabled:opacity-50"
|
||||
class="rounded-lg border border-app-accent/40 px-4 py-2 text-sm text-app-accent hover:bg-app-accent/10 disabled:opacity-50"
|
||||
onclick={toggleStar}
|
||||
disabled={starBusy}
|
||||
>
|
||||
|
|
@ -185,7 +185,7 @@
|
|||
|
||||
{#if subscribed}
|
||||
<button
|
||||
class="rounded-lg border border-emerald-500/40 px-4 py-2 text-sm text-emerald-300 hover:bg-emerald-500/10 disabled:opacity-50"
|
||||
class="rounded-lg border border-success/40 px-4 py-2 text-sm text-success hover:bg-success/10 disabled:opacity-50"
|
||||
onclick={toggleSubscribe}
|
||||
disabled={subscribeBusy}
|
||||
title="Abo entfernen"
|
||||
|
|
@ -194,7 +194,7 @@
|
|||
</button>
|
||||
{#if subscribedDeckId}
|
||||
<button
|
||||
class="rounded-lg bg-indigo-500 px-4 py-2 text-sm text-white hover:bg-indigo-400"
|
||||
class="rounded-lg bg-app-accent px-4 py-2 text-sm text-white hover:bg-app-accent/90"
|
||||
onclick={() => goto(`/learn/${subscribedDeckId}`)}
|
||||
>
|
||||
Lernen
|
||||
|
|
@ -210,7 +210,7 @@
|
|||
</button>
|
||||
{:else}
|
||||
<button
|
||||
class="rounded-lg bg-indigo-500 px-4 py-2 text-sm text-white hover:bg-indigo-400 disabled:opacity-50"
|
||||
class="rounded-lg bg-app-accent px-4 py-2 text-sm text-white hover:bg-app-accent/90 disabled:opacity-50"
|
||||
onclick={toggleSubscribe}
|
||||
disabled={subscribeBusy || !version}
|
||||
title={version ? 'In meine Decks ziehen' : 'Deck hat noch keine Version'}
|
||||
|
|
@ -219,7 +219,7 @@
|
|||
</button>
|
||||
{#if isPaid && hasPurchased}
|
||||
<span
|
||||
class="rounded-full bg-emerald-500/15 px-2 py-1 text-xs text-emerald-300"
|
||||
class="rounded-full bg-success/15 px-2 py-1 text-xs text-success"
|
||||
title="Du besitzt dieses Deck"
|
||||
>
|
||||
✓ Gekauft
|
||||
|
|
@ -229,7 +229,7 @@
|
|||
{:else}
|
||||
<a
|
||||
href="/login"
|
||||
class="rounded-lg bg-indigo-500 px-4 py-2 text-sm text-white hover:bg-indigo-400"
|
||||
class="rounded-lg bg-app-accent px-4 py-2 text-sm text-white hover:bg-app-accent/90"
|
||||
>
|
||||
Anmelden um zu abonnieren
|
||||
</a>
|
||||
|
|
@ -237,10 +237,10 @@
|
|||
</div>
|
||||
|
||||
{#if error}
|
||||
<p class="mt-3 text-sm text-red-400">{error}</p>
|
||||
<p class="mt-3 text-sm text-error">{error}</p>
|
||||
{/if}
|
||||
|
||||
<div class="mt-10 flex items-center justify-between text-xs text-neutral-500">
|
||||
<div class="mt-10 flex items-center justify-between text-xs text-muted-foreground/80">
|
||||
<span>Veröffentlicht: {new Date(deck.createdAt).toLocaleDateString('de-DE')}</span>
|
||||
{#if !isOwner}
|
||||
<ReportButton deckSlug={deck.slug} />
|
||||
|
|
@ -248,7 +248,7 @@
|
|||
</div>
|
||||
|
||||
{#if deck.isTakedown}
|
||||
<p class="mt-3 rounded-lg border border-red-500/30 bg-red-500/10 p-3 text-sm text-red-300">
|
||||
<p class="mt-3 rounded-lg border border-error/30 bg-error/10 p-3 text-sm text-error">
|
||||
Dieses Deck wurde von der Moderation entfernt.
|
||||
</p>
|
||||
{/if}
|
||||
|
|
@ -261,7 +261,7 @@
|
|||
</article>
|
||||
{/if}
|
||||
|
||||
<p class="mt-12 text-center text-xs text-neutral-600">
|
||||
<a href="/explore" class="hover:text-neutral-300">← Marktplatz</a>
|
||||
<p class="mt-12 text-center text-xs text-muted-foreground/60">
|
||||
<a href="/explore" class="hover:text-foreground/80">← Marktplatz</a>
|
||||
</p>
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -187,7 +187,9 @@
|
|||
</svelte:head>
|
||||
|
||||
<main class="mx-auto max-w-3xl px-6 py-10">
|
||||
<a href="/" class="mb-6 inline-block text-sm text-neutral-400 hover:text-neutral-100">← Decks</a>
|
||||
<a href="/" class="mb-6 inline-block text-sm text-muted-foreground hover:text-foreground"
|
||||
>← Decks</a
|
||||
>
|
||||
|
||||
{#if deck}
|
||||
<header class="mb-6 flex items-start justify-between gap-4">
|
||||
|
|
@ -197,11 +199,11 @@
|
|||
<h1 class="text-2xl font-semibold">{deck.title}</h1>
|
||||
</div>
|
||||
{#if deck.description}
|
||||
<p class="text-sm text-neutral-400">{deck.description}</p>
|
||||
<p class="text-sm text-muted-foreground">{deck.description}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<button
|
||||
class="rounded-lg border border-red-500/30 px-3 py-1.5 text-sm text-red-400 hover:bg-red-500/10"
|
||||
class="rounded-lg border border-error/30 px-3 py-1.5 text-sm text-error hover:bg-error/10"
|
||||
onclick={() => (confirmDelete = true)}
|
||||
>
|
||||
Löschen
|
||||
|
|
@ -209,27 +211,27 @@
|
|||
</header>
|
||||
|
||||
{#if isSubscribed}
|
||||
<div class="mb-6 rounded-xl border border-emerald-500/30 bg-emerald-500/5 p-4 text-sm">
|
||||
<div class="mb-6 rounded-xl border border-success/30 bg-emerald-500/5 p-4 text-sm">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div class="font-medium text-emerald-300">
|
||||
<div class="font-medium text-success">
|
||||
📥 Abonniert · v{subscribedAtVersion}
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-neutral-400">
|
||||
<p class="mt-1 text-xs text-muted-foreground">
|
||||
Aus dem Marktplatz von <a
|
||||
href={`/d/${subscribedFromSlug}`}
|
||||
class="text-emerald-300 hover:underline">{subscribedFromSlug}</a
|
||||
class="text-success hover:underline">{subscribedFromSlug}</a
|
||||
>. Karten sind read-only — Author entscheidet über Inhalte. Forken um eigene Variante
|
||||
zu machen (Phase ε).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{#if updatePreview}
|
||||
<div class="mt-3 flex flex-wrap items-center gap-2 rounded-lg bg-emerald-500/10 p-2">
|
||||
<div class="mt-3 flex flex-wrap items-center gap-2 rounded-lg bg-success/10 p-2">
|
||||
<span class="text-xs font-medium text-emerald-200">
|
||||
Update auf v{updatePreview.to} verfügbar
|
||||
</span>
|
||||
<span class="text-xs text-neutral-400">
|
||||
<span class="text-xs text-muted-foreground">
|
||||
+{updatePreview.added} neu · ~{updatePreview.changed} geändert · −{updatePreview.removed}
|
||||
entfernt
|
||||
</span>
|
||||
|
|
@ -243,14 +245,14 @@
|
|||
</div>
|
||||
{/if}
|
||||
{#if updateError}
|
||||
<p class="mt-2 text-xs text-red-400">{updateError}</p>
|
||||
<p class="mt-2 text-xs text-error">{updateError}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mb-6 flex flex-wrap items-center gap-3">
|
||||
<button
|
||||
class="rounded-lg bg-indigo-500 px-5 py-2.5 text-sm font-medium text-white hover:bg-indigo-400 disabled:opacity-50"
|
||||
class="rounded-lg bg-app-accent px-5 py-2.5 text-sm font-medium text-white hover:bg-app-accent/90 disabled:opacity-50"
|
||||
onclick={() => goto(`/learn/${deckId}`)}
|
||||
disabled={dueCount === 0}
|
||||
>
|
||||
|
|
@ -263,7 +265,7 @@
|
|||
</button>
|
||||
{#if !isSubscribed}
|
||||
<button
|
||||
class="rounded-lg border border-indigo-500/30 px-4 py-2 text-sm text-indigo-300 hover:bg-indigo-500/10 disabled:opacity-50"
|
||||
class="rounded-lg border border-indigo-500/30 px-4 py-2 text-sm text-app-accent hover:bg-app-accent/10 disabled:opacity-50"
|
||||
onclick={() => (showPublish = true)}
|
||||
disabled={cards.length === 0}
|
||||
title={cards.length === 0
|
||||
|
|
@ -274,32 +276,33 @@
|
|||
</button>
|
||||
{/if}
|
||||
{#if dueCount === 0 && cards.length > 0}
|
||||
<span class="text-sm text-neutral-400">Heute alles gelernt — schau später wieder rein.</span
|
||||
<span class="text-sm text-muted-foreground"
|
||||
>Heute alles gelernt — schau später wieder rein.</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mb-6 grid grid-cols-2 gap-3 sm:grid-cols-3">
|
||||
<div class="rounded-xl border border-neutral-800 bg-neutral-900 p-4 text-center">
|
||||
<div class="rounded-xl border border-border bg-card p-4 text-center">
|
||||
<div class="text-2xl font-semibold">{cards.length}</div>
|
||||
<div class="text-xs text-neutral-400">Karten</div>
|
||||
<div class="text-xs text-muted-foreground">Karten</div>
|
||||
</div>
|
||||
<div class="rounded-xl border border-neutral-800 bg-neutral-900 p-4 text-center">
|
||||
<div class="text-2xl font-semibold text-amber-400">{dueCount}</div>
|
||||
<div class="text-xs text-neutral-400">Fällig</div>
|
||||
<div class="rounded-xl border border-border bg-card p-4 text-center">
|
||||
<div class="text-2xl font-semibold text-warning">{dueCount}</div>
|
||||
<div class="text-xs text-muted-foreground">Fällig</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if !isSubscribed}
|
||||
<div class="mb-6 flex flex-wrap items-center gap-3">
|
||||
<button
|
||||
class="rounded-lg bg-indigo-500 px-4 py-2 text-sm text-white hover:bg-indigo-400"
|
||||
class="rounded-lg bg-app-accent px-4 py-2 text-sm text-white hover:bg-app-accent/90"
|
||||
onclick={() => (showNew = true)}
|
||||
>
|
||||
Neue Karte
|
||||
</button>
|
||||
<button
|
||||
class="rounded-lg border border-indigo-500/30 px-4 py-2 text-sm text-indigo-300 hover:bg-indigo-500/10"
|
||||
class="rounded-lg border border-indigo-500/30 px-4 py-2 text-sm text-app-accent hover:bg-app-accent/10"
|
||||
onclick={() => (showAi = !showAi)}
|
||||
>
|
||||
✨ Aus Text generieren
|
||||
|
|
@ -314,7 +317,7 @@
|
|||
{/if}
|
||||
|
||||
{#if showNew}
|
||||
<div class="mb-6 rounded-xl border border-indigo-500/30 bg-neutral-900 p-4">
|
||||
<div class="mb-6 rounded-xl border border-indigo-500/30 bg-card p-4">
|
||||
<h3 class="mb-3 font-medium">Neue Karte</h3>
|
||||
|
||||
<div class="mb-4 grid grid-cols-2 gap-2 sm:grid-cols-4">
|
||||
|
|
@ -324,11 +327,11 @@
|
|||
onclick={() => (newType = opt.value)}
|
||||
class="rounded-lg border p-2 text-left text-sm transition-colors {newType ===
|
||||
opt.value
|
||||
? 'border-indigo-400 bg-indigo-500/10 text-indigo-300'
|
||||
: 'border-neutral-700 hover:bg-neutral-800'}"
|
||||
? 'border-indigo-400 bg-app-accent/10 text-app-accent'
|
||||
: 'border-border-strong hover:bg-muted'}"
|
||||
>
|
||||
<div class="font-medium">{opt.label}</div>
|
||||
<div class="text-xs text-neutral-400">{opt.hint}</div>
|
||||
<div class="text-xs text-muted-foreground">{opt.hint}</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
@ -337,10 +340,11 @@
|
|||
{#if newType === 'cloze'}
|
||||
<div>
|
||||
<div class="mb-1 flex items-center justify-between">
|
||||
<label for="card-cloze" class="text-sm text-neutral-400">Text mit Lücken</label>
|
||||
<label for="card-cloze" class="text-sm text-muted-foreground">Text mit Lücken</label
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="text-xs text-indigo-300 hover:text-indigo-200 disabled:opacity-50"
|
||||
class="text-xs text-app-accent hover:text-indigo-200 disabled:opacity-50"
|
||||
onclick={() => pickAttachment('cloze')}
|
||||
disabled={attachBusy !== null}
|
||||
>
|
||||
|
|
@ -359,22 +363,22 @@
|
|||
id="card-cloze"
|
||||
bind:value={newCloze}
|
||||
placeholder="Berlin ist die Hauptstadt von {{c1::Deutschland}}."
|
||||
class="min-h-[100px] w-full rounded-lg border border-neutral-700 bg-neutral-950 px-3 py-2 text-sm outline-none focus:border-indigo-400"
|
||||
class="min-h-[100px] w-full rounded-lg border border-border-strong bg-background px-3 py-2 text-sm outline-none focus:border-indigo-400"
|
||||
autofocus
|
||||
></textarea>
|
||||
<p class="mt-1 text-xs text-neutral-500">
|
||||
<p class="mt-1 text-xs text-muted-foreground/80">
|
||||
Markiere mit
|
||||
<code class="rounded bg-neutral-800 px-1">{{c1::Wort}}</code>
|
||||
— optional Hinweis: <code class="rounded bg-neutral-800 px-1">::Hinweis</code>.
|
||||
<code class="rounded bg-muted px-1">{{c1::Wort}}</code>
|
||||
— optional Hinweis: <code class="rounded bg-muted px-1">::Hinweis</code>.
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div>
|
||||
<div class="mb-1 flex items-center justify-between">
|
||||
<label for="card-front" class="text-sm text-neutral-400">Vorderseite</label>
|
||||
<label for="card-front" class="text-sm text-muted-foreground">Vorderseite</label>
|
||||
<button
|
||||
type="button"
|
||||
class="text-xs text-indigo-300 hover:text-indigo-200 disabled:opacity-50"
|
||||
class="text-xs text-app-accent hover:text-indigo-200 disabled:opacity-50"
|
||||
onclick={() => pickAttachment('front')}
|
||||
disabled={attachBusy !== null}
|
||||
>
|
||||
|
|
@ -394,16 +398,16 @@
|
|||
type="text"
|
||||
bind:value={newFront}
|
||||
placeholder="Frage oder Begriff…"
|
||||
class="w-full rounded-lg border border-neutral-700 bg-neutral-950 px-3 py-2 text-sm outline-none focus:border-indigo-400"
|
||||
class="w-full rounded-lg border border-border-strong bg-background px-3 py-2 text-sm outline-none focus:border-indigo-400"
|
||||
autofocus
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-1 flex items-center justify-between">
|
||||
<label for="card-back" class="text-sm text-neutral-400">Rückseite</label>
|
||||
<label for="card-back" class="text-sm text-muted-foreground">Rückseite</label>
|
||||
<button
|
||||
type="button"
|
||||
class="text-xs text-indigo-300 hover:text-indigo-200 disabled:opacity-50"
|
||||
class="text-xs text-app-accent hover:text-indigo-200 disabled:opacity-50"
|
||||
onclick={() => pickAttachment('back')}
|
||||
disabled={attachBusy !== null}
|
||||
>
|
||||
|
|
@ -421,16 +425,16 @@
|
|||
id="card-back"
|
||||
bind:value={newBack}
|
||||
placeholder="Antwort oder Erklärung…"
|
||||
class="min-h-[80px] w-full rounded-lg border border-neutral-700 bg-neutral-950 px-3 py-2 text-sm outline-none focus:border-indigo-400"
|
||||
class="min-h-[80px] w-full rounded-lg border border-border-strong bg-background px-3 py-2 text-sm outline-none focus:border-indigo-400"
|
||||
></textarea>
|
||||
</div>
|
||||
{/if}
|
||||
{#if attachError}
|
||||
<p class="text-xs text-red-400">{attachError}</p>
|
||||
<p class="text-xs text-error">{attachError}</p>
|
||||
{/if}
|
||||
<div class="flex justify-end gap-2">
|
||||
<button
|
||||
class="rounded-lg px-3 py-1.5 text-sm text-neutral-400 hover:text-neutral-100"
|
||||
class="rounded-lg px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground"
|
||||
onclick={() => {
|
||||
showNew = false;
|
||||
newFront = '';
|
||||
|
|
@ -441,7 +445,7 @@
|
|||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
class="rounded-lg bg-indigo-500 px-4 py-1.5 text-sm text-white hover:bg-indigo-400 disabled:opacity-50"
|
||||
class="rounded-lg bg-app-accent px-4 py-1.5 text-sm text-white hover:bg-app-accent/90 disabled:opacity-50"
|
||||
onclick={handleCreateCard}
|
||||
disabled={!canSubmit()}
|
||||
>
|
||||
|
|
@ -452,12 +456,12 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="rounded-xl border border-neutral-800 bg-neutral-900">
|
||||
<h2 class="border-b border-neutral-800 p-4 text-lg font-semibold">
|
||||
<div class="rounded-xl border border-border bg-card">
|
||||
<h2 class="border-b border-border p-4 text-lg font-semibold">
|
||||
Karten ({cards.length})
|
||||
</h2>
|
||||
{#if cards.length === 0}
|
||||
<div class="p-10 text-center text-neutral-400">
|
||||
<div class="p-10 text-center text-muted-foreground">
|
||||
Noch keine Karten. Erstelle deine erste!
|
||||
</div>
|
||||
{:else}
|
||||
|
|
@ -465,24 +469,24 @@
|
|||
{#each cards as card, i (card.id)}
|
||||
{@const p = preview(card)}
|
||||
<li class="flex items-start gap-4 p-4">
|
||||
<span class="mt-1 text-xs text-neutral-500">{i + 1}.</span>
|
||||
<span class="mt-1 text-xs text-muted-foreground/80">{i + 1}.</span>
|
||||
<div class="min-w-0 flex-1 space-y-1">
|
||||
<div class="card-content">
|
||||
{@html renderMarkdown(p.primary)}
|
||||
</div>
|
||||
{#if p.secondary}
|
||||
<div class="card-content text-sm text-neutral-400">
|
||||
<div class="card-content text-sm text-muted-foreground">
|
||||
{@html renderMarkdown(p.secondary)}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="rounded-full bg-neutral-800 px-2 py-0.5 text-xs text-neutral-400">
|
||||
<span class="rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground">
|
||||
{typeBadge(card.type)}
|
||||
</span>
|
||||
{#if !isSubscribed}
|
||||
<button
|
||||
class="rounded p-1 text-neutral-500 hover:text-red-400"
|
||||
class="rounded p-1 text-muted-foreground/80 hover:text-error"
|
||||
onclick={() => handleDeleteCard(card.id)}
|
||||
aria-label="Karte löschen"
|
||||
>
|
||||
|
|
@ -506,22 +510,22 @@
|
|||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div
|
||||
class="mx-4 w-full max-w-md rounded-xl border border-neutral-800 bg-neutral-900 p-6"
|
||||
class="mx-4 w-full max-w-md rounded-xl border border-border bg-card p-6"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<h3 class="mb-2 text-xl font-semibold">Deck löschen?</h3>
|
||||
<p class="mb-6 text-neutral-400">
|
||||
<p class="mb-6 text-muted-foreground">
|
||||
"{deck.title}" wird mit allen Karten gelöscht.
|
||||
</p>
|
||||
<div class="flex justify-end gap-3">
|
||||
<button
|
||||
class="rounded-lg px-4 py-2 text-sm text-neutral-400 hover:text-neutral-100"
|
||||
class="rounded-lg px-4 py-2 text-sm text-muted-foreground hover:text-foreground"
|
||||
onclick={() => (confirmDelete = false)}
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
class="rounded-lg bg-red-500 px-4 py-2 text-sm text-white hover:bg-red-400"
|
||||
class="rounded-lg bg-error px-4 py-2 text-sm text-white hover:bg-error/90"
|
||||
onclick={handleDeleteDeck}
|
||||
>
|
||||
Löschen
|
||||
|
|
@ -531,9 +535,9 @@
|
|||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="py-16 text-center text-neutral-400">
|
||||
<div class="py-16 text-center text-muted-foreground">
|
||||
Deck nicht gefunden.
|
||||
<a href="/" class="ml-2 text-indigo-400 hover:underline">zurück</a>
|
||||
<a href="/" class="ml-2 text-app-accent hover:underline">zurück</a>
|
||||
</div>
|
||||
{/if}
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@
|
|||
<main class="mx-auto max-w-3xl px-6 py-8">
|
||||
<header class="mb-6">
|
||||
<h1 class="text-3xl font-semibold tracking-tight">Entdecken</h1>
|
||||
<p class="text-sm text-neutral-400">
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Decks aus dem Cards-Marktplatz — kostenlos lernen oder eigene veröffentlichen.
|
||||
</p>
|
||||
</header>
|
||||
|
|
@ -72,11 +72,11 @@
|
|||
type="search"
|
||||
bind:value={searchQuery}
|
||||
placeholder="Suche nach Titel oder Beschreibung…"
|
||||
class="flex-1 rounded-lg border border-neutral-700 bg-neutral-950 px-3 py-2 text-sm outline-none focus:border-indigo-400"
|
||||
class="flex-1 rounded-lg border border-border-strong bg-background px-3 py-2 text-sm outline-none focus:border-indigo-400"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-lg bg-indigo-500 px-4 py-2 text-sm text-white hover:bg-indigo-400 disabled:opacity-50"
|
||||
class="rounded-lg bg-app-accent px-4 py-2 text-sm text-white hover:bg-app-accent/90 disabled:opacity-50"
|
||||
disabled={searchBusy}
|
||||
>
|
||||
{searchBusy ? 'Suche…' : 'Suchen'}
|
||||
|
|
@ -84,19 +84,22 @@
|
|||
</form>
|
||||
|
||||
{#if stage === 'loading'}
|
||||
<p class="py-12 text-center text-sm text-neutral-400">Lade Marktplatz…</p>
|
||||
<p class="py-12 text-center text-sm text-muted-foreground">Lade Marktplatz…</p>
|
||||
{:else if stage === 'error'}
|
||||
<p class="rounded-lg border border-red-500/30 bg-red-500/10 p-4 text-sm text-red-400">
|
||||
<p class="rounded-lg border border-error/30 bg-error/10 p-4 text-sm text-error">
|
||||
{error}
|
||||
<button class="ml-2 underline" onclick={loadLanding}>Erneut versuchen</button>
|
||||
</p>
|
||||
{:else if stage === 'search'}
|
||||
<section>
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<h2 class="text-sm font-medium text-neutral-300">
|
||||
<h2 class="text-sm font-medium text-foreground/80">
|
||||
{searchTotal} Treffer für „{searchQuery}"
|
||||
</h2>
|
||||
<button class="text-xs text-neutral-500 hover:text-neutral-200" onclick={loadLanding}>
|
||||
<button
|
||||
class="text-xs text-muted-foreground/80 hover:text-foreground/90"
|
||||
onclick={loadLanding}
|
||||
>
|
||||
Zurück
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -105,7 +108,7 @@
|
|||
{:else if stage === 'landing'}
|
||||
{#if featured.length > 0}
|
||||
<section class="mb-8">
|
||||
<h2 class="mb-3 text-sm font-medium text-neutral-300">
|
||||
<h2 class="mb-3 text-sm font-medium text-foreground/80">
|
||||
🛡️ Featured · vom Mana-Verein empfohlen
|
||||
</h2>
|
||||
<DeckGrid decks={featured} />
|
||||
|
|
@ -113,12 +116,15 @@
|
|||
{/if}
|
||||
|
||||
<section>
|
||||
<h2 class="mb-3 text-sm font-medium text-neutral-300">📈 Trending · letzte 7 Tage</h2>
|
||||
<DeckGrid decks={trending} emptyText="Noch keine Trends — sei der/die Erste mit einem Public-Deck." />
|
||||
<h2 class="mb-3 text-sm font-medium text-foreground/80">📈 Trending · letzte 7 Tage</h2>
|
||||
<DeckGrid
|
||||
decks={trending}
|
||||
emptyText="Noch keine Trends — sei der/die Erste mit einem Public-Deck."
|
||||
/>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<p class="mt-12 text-center text-xs text-neutral-600">
|
||||
<a href="/" class="hover:text-neutral-300">← Eigene Decks</a>
|
||||
<p class="mt-12 text-center text-xs text-muted-foreground/60">
|
||||
<a href="/" class="hover:text-foreground/80">← Eigene Decks</a>
|
||||
</p>
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@
|
|||
<header class="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<button
|
||||
class="text-sm text-neutral-400 hover:text-neutral-100"
|
||||
class="text-sm text-muted-foreground hover:text-foreground"
|
||||
onclick={() => goto(`/decks/${deckId}`)}
|
||||
>
|
||||
← {deckTitle}
|
||||
|
|
@ -107,33 +107,33 @@
|
|||
<h1 class="mt-1 text-xl font-semibold">Lernen</h1>
|
||||
</div>
|
||||
{#if queue.length > 0 && !finished}
|
||||
<div class="text-sm text-neutral-400">
|
||||
<div class="text-sm text-muted-foreground">
|
||||
{Math.min(currentIndex + 1, queue.length)} / {queue.length}
|
||||
</div>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
{#if empty}
|
||||
<div class="rounded-xl border border-neutral-800 bg-neutral-900 p-10 text-center">
|
||||
<div class="rounded-xl border border-border bg-card p-10 text-center">
|
||||
<div class="text-2xl">Alles gelernt</div>
|
||||
<p class="mt-2 text-sm text-neutral-400">
|
||||
<p class="mt-2 text-sm text-muted-foreground">
|
||||
Komm später wieder — fällige Karten erscheinen automatisch.
|
||||
</p>
|
||||
<button
|
||||
class="mt-4 rounded-lg bg-indigo-500 px-4 py-2 text-sm text-white hover:bg-indigo-400"
|
||||
class="mt-4 rounded-lg bg-app-accent px-4 py-2 text-sm text-white hover:bg-app-accent/90"
|
||||
onclick={() => goto(`/decks/${deckId}`)}
|
||||
>
|
||||
Zurück zum Deck
|
||||
</button>
|
||||
</div>
|
||||
{:else if finished}
|
||||
<div class="rounded-xl border border-neutral-800 bg-neutral-900 p-10 text-center">
|
||||
<div class="rounded-xl border border-border bg-card p-10 text-center">
|
||||
<div class="text-2xl">Session abgeschlossen</div>
|
||||
<p class="mt-2 text-sm text-neutral-400">
|
||||
<p class="mt-2 text-sm text-muted-foreground">
|
||||
{sessionCount} Karten in {Math.round((Date.now() - sessionStartedAt) / 1000)} s.
|
||||
</p>
|
||||
<button
|
||||
class="mt-4 rounded-lg bg-indigo-500 px-4 py-2 text-sm text-white hover:bg-indigo-400"
|
||||
class="mt-4 rounded-lg bg-app-accent px-4 py-2 text-sm text-white hover:bg-app-accent/90"
|
||||
onclick={() => goto(`/decks/${deckId}`)}
|
||||
>
|
||||
Fertig
|
||||
|
|
@ -151,14 +151,14 @@
|
|||
{#if canSuggest}
|
||||
<div class="mt-3 flex justify-end gap-3">
|
||||
<button
|
||||
class="text-xs text-neutral-500 hover:text-neutral-200"
|
||||
class="text-xs text-muted-foreground/80 hover:text-foreground/90"
|
||||
onclick={() => (discussionsOpen = !discussionsOpen)}
|
||||
title="Kommentare zur Karte"
|
||||
>
|
||||
💬 {discussionsOpen ? 'Diskussion ausblenden' : 'Diskussion'}
|
||||
</button>
|
||||
<button
|
||||
class="text-xs text-neutral-500 hover:text-indigo-300"
|
||||
class="text-xs text-muted-foreground/80 hover:text-app-accent"
|
||||
onclick={() => (suggestOpen = true)}
|
||||
title="Verbesserung dieser Karte vorschlagen"
|
||||
>
|
||||
|
|
@ -173,7 +173,7 @@
|
|||
|
||||
{#if !showBack}
|
||||
<button
|
||||
class="mt-6 w-full rounded-lg bg-indigo-500 py-3 text-base text-white hover:bg-indigo-400"
|
||||
class="mt-6 w-full rounded-lg bg-app-accent py-3 text-base text-white hover:bg-app-accent/90"
|
||||
onclick={reveal}
|
||||
>
|
||||
Aufdecken <span class="ml-2 text-xs opacity-70">(Leertaste)</span>
|
||||
|
|
@ -181,7 +181,7 @@
|
|||
{:else}
|
||||
<div class="mt-6 grid grid-cols-4 gap-2">
|
||||
<button
|
||||
class="rounded-lg bg-red-500 py-3 text-sm text-white hover:bg-red-400"
|
||||
class="rounded-lg bg-error py-3 text-sm text-white hover:bg-error/90"
|
||||
onclick={() => grade(1)}
|
||||
>
|
||||
Nochmal
|
||||
|
|
@ -211,7 +211,7 @@
|
|||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="text-center text-sm text-neutral-400">Lade…</div>
|
||||
<div class="text-center text-sm text-muted-foreground">Lade…</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -41,48 +41,46 @@
|
|||
<h1 class="mb-6 text-2xl font-semibold tracking-tight">Käufe & Auszahlungen</h1>
|
||||
|
||||
{#if error}
|
||||
<p class="mb-4 rounded-lg border border-red-500/30 bg-red-500/10 p-3 text-sm text-red-400">
|
||||
<p class="mb-4 rounded-lg border border-error/30 bg-error/10 p-3 text-sm text-error">
|
||||
{error}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<section class="mb-10">
|
||||
<header class="mb-3 flex items-baseline justify-between">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-neutral-400">Käufe</h2>
|
||||
<span class="text-xs text-neutral-500">Ausgegeben: {totalSpent} 💎</span>
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">Käufe</h2>
|
||||
<span class="text-xs text-muted-foreground/80">Ausgegeben: {totalSpent} 💎</span>
|
||||
</header>
|
||||
|
||||
{#if loading}
|
||||
<p class="rounded-xl border border-neutral-800 bg-neutral-900 p-4 text-sm text-neutral-500">
|
||||
<p class="rounded-xl border border-border bg-card p-4 text-sm text-muted-foreground/80">
|
||||
Lädt…
|
||||
</p>
|
||||
{:else if purchases.length === 0}
|
||||
<p class="rounded-xl border border-neutral-800 bg-neutral-900 p-4 text-sm text-neutral-500">
|
||||
<p class="rounded-xl border border-border bg-card p-4 text-sm text-muted-foreground/80">
|
||||
Du hast noch keine Decks gekauft.
|
||||
</p>
|
||||
{:else}
|
||||
<ul class="space-y-2">
|
||||
{#each purchases as p (p.id)}
|
||||
<li
|
||||
class="flex items-center justify-between rounded-xl border border-neutral-800 bg-neutral-900 p-4"
|
||||
>
|
||||
<li class="flex items-center justify-between rounded-xl border border-border bg-card p-4">
|
||||
<div class="min-w-0 flex-1">
|
||||
<a
|
||||
href="/d/{p.deckSlug}"
|
||||
class="truncate font-medium text-neutral-100 hover:text-indigo-300"
|
||||
class="truncate font-medium text-foreground hover:text-app-accent"
|
||||
>
|
||||
{p.deckTitle}
|
||||
</a>
|
||||
<p class="mt-1 text-xs text-neutral-500">
|
||||
<p class="mt-1 text-xs text-muted-foreground/80">
|
||||
v{p.versionSemver} · {new Date(p.purchasedAt).toLocaleDateString('de-DE')}
|
||||
{#if p.refundedAt}
|
||||
<span class="ml-2 rounded bg-amber-500/15 px-1.5 py-0.5 text-amber-300"
|
||||
<span class="ml-2 rounded bg-amber-500/15 px-1.5 py-0.5 text-warning"
|
||||
>Erstattet</span
|
||||
>
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
<span class="shrink-0 text-sm text-neutral-300">{p.priceCredits} 💎</span>
|
||||
<span class="shrink-0 text-sm text-foreground/80">{p.priceCredits} 💎</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
|
@ -92,14 +90,14 @@
|
|||
{#if payouts.length > 0 || (!loading && payouts.length === 0)}
|
||||
<section>
|
||||
<header class="mb-3 flex items-baseline justify-between">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-neutral-400">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
|
||||
Author-Auszahlungen
|
||||
</h2>
|
||||
<span class="text-xs text-neutral-500">Erhalten: {totalEarned} 💎</span>
|
||||
<span class="text-xs text-muted-foreground/80">Erhalten: {totalEarned} 💎</span>
|
||||
</header>
|
||||
|
||||
{#if payouts.length === 0}
|
||||
<p class="rounded-xl border border-neutral-800 bg-neutral-900 p-4 text-sm text-neutral-500">
|
||||
<p class="rounded-xl border border-border bg-card p-4 text-sm text-muted-foreground/80">
|
||||
Noch keine Auszahlungen — sobald jemand eines deiner kostenpflichtigen Decks kauft, landet
|
||||
die Author-Beteiligung hier.
|
||||
</p>
|
||||
|
|
@ -107,22 +105,22 @@
|
|||
<ul class="space-y-2">
|
||||
{#each payouts as p (p.id)}
|
||||
<li
|
||||
class="flex items-center justify-between rounded-xl border border-neutral-800 bg-neutral-900 p-4"
|
||||
class="flex items-center justify-between rounded-xl border border-border bg-card p-4"
|
||||
>
|
||||
<div class="min-w-0 flex-1">
|
||||
<a
|
||||
href="/d/{p.deckSlug}"
|
||||
class="truncate font-medium text-neutral-100 hover:text-indigo-300"
|
||||
class="truncate font-medium text-foreground hover:text-app-accent"
|
||||
>
|
||||
{p.deckTitle}
|
||||
</a>
|
||||
<p class="mt-1 text-xs text-neutral-500">
|
||||
<p class="mt-1 text-xs text-muted-foreground/80">
|
||||
Verkauf {p.priceCredits} 💎 · gutgeschrieben {new Date(
|
||||
p.grantedAt
|
||||
).toLocaleDateString('de-DE')}
|
||||
</p>
|
||||
</div>
|
||||
<span class="shrink-0 text-sm text-emerald-300">+{p.creditsGranted} 💎</span>
|
||||
<span class="shrink-0 text-sm text-success">+{p.creditsGranted} 💎</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -64,13 +64,15 @@
|
|||
|
||||
<main class="mx-auto max-w-3xl px-6 py-8">
|
||||
{#if stage === 'loading'}
|
||||
<p class="py-12 text-center text-sm text-neutral-400">Lade Profil…</p>
|
||||
<p class="py-12 text-center text-sm text-muted-foreground">Lade Profil…</p>
|
||||
{:else if stage === 'not-found'}
|
||||
<p class="rounded-xl border border-neutral-800 bg-neutral-900 p-8 text-center text-sm text-neutral-400">
|
||||
Profil <code class="rounded bg-neutral-800 px-1">@{slug}</code> existiert nicht.
|
||||
<p
|
||||
class="rounded-xl border border-border bg-card p-8 text-center text-sm text-muted-foreground"
|
||||
>
|
||||
Profil <code class="rounded bg-muted px-1">@{slug}</code> existiert nicht.
|
||||
</p>
|
||||
{:else if stage === 'error'}
|
||||
<p class="rounded-lg border border-red-500/30 bg-red-500/10 p-4 text-sm text-red-400">
|
||||
<p class="rounded-lg border border-error/30 bg-error/10 p-4 text-sm text-error">
|
||||
{error}
|
||||
</p>
|
||||
{:else if author}
|
||||
|
|
@ -79,11 +81,11 @@
|
|||
<img
|
||||
src={author.avatarUrl}
|
||||
alt=""
|
||||
class="h-16 w-16 rounded-full border border-neutral-800 object-cover"
|
||||
class="h-16 w-16 rounded-full border border-border object-cover"
|
||||
/>
|
||||
{:else}
|
||||
<div
|
||||
class="flex h-16 w-16 items-center justify-center rounded-full border border-neutral-800 bg-neutral-900 text-xl font-semibold text-neutral-400"
|
||||
class="flex h-16 w-16 items-center justify-center rounded-full border border-border bg-card text-xl font-semibold text-muted-foreground"
|
||||
>
|
||||
{author.displayName.slice(0, 1).toUpperCase()}
|
||||
</div>
|
||||
|
|
@ -92,29 +94,29 @@
|
|||
<div class="flex flex-wrap items-center gap-2">
|
||||
<h1 class="text-2xl font-semibold">{author.displayName}</h1>
|
||||
{#if author.verifiedMana}
|
||||
<span class="rounded-full bg-emerald-500/15 px-2 py-0.5 text-xs text-emerald-300">
|
||||
<span class="rounded-full bg-success/15 px-2 py-0.5 text-xs text-success">
|
||||
🛡️ Mana
|
||||
</span>
|
||||
{/if}
|
||||
{#if author.verifiedCommunity}
|
||||
<span class="rounded-full bg-amber-500/15 px-2 py-0.5 text-xs text-amber-300">
|
||||
<span class="rounded-full bg-amber-500/15 px-2 py-0.5 text-xs text-warning">
|
||||
⭐ Community
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<p class="text-xs text-neutral-500">
|
||||
<p class="text-xs text-muted-foreground/80">
|
||||
@{author.slug} · seit {new Date(author.joinedAt).toLocaleDateString('de-DE', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
})}
|
||||
</p>
|
||||
{#if author.bio}
|
||||
<p class="mt-2 text-sm text-neutral-300">{author.bio}</p>
|
||||
<p class="mt-2 text-sm text-foreground/80">{author.bio}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{#if authStore.isAuthenticated}
|
||||
<button
|
||||
class="rounded-lg border border-indigo-500/40 px-3 py-1.5 text-sm text-indigo-300 hover:bg-indigo-500/10 disabled:opacity-50"
|
||||
class="rounded-lg border border-app-accent/40 px-3 py-1.5 text-sm text-app-accent hover:bg-app-accent/10 disabled:opacity-50"
|
||||
onclick={toggleFollow}
|
||||
disabled={busy}
|
||||
>
|
||||
|
|
@ -123,13 +125,14 @@
|
|||
{/if}
|
||||
</header>
|
||||
|
||||
<h2 class="mb-3 text-sm font-medium text-neutral-300">
|
||||
{decks.length} {decks.length === 1 ? 'Deck' : 'Decks'}
|
||||
<h2 class="mb-3 text-sm font-medium text-foreground/80">
|
||||
{decks.length}
|
||||
{decks.length === 1 ? 'Deck' : 'Decks'}
|
||||
</h2>
|
||||
<DeckGrid {decks} emptyText="Dieser Author hat noch keine Decks veröffentlicht." />
|
||||
{/if}
|
||||
|
||||
<p class="mt-12 text-center text-xs text-neutral-600">
|
||||
<a href="/explore" class="hover:text-neutral-300">← Marktplatz</a>
|
||||
<p class="mt-12 text-center text-xs text-muted-foreground/60">
|
||||
<a href="/explore" class="hover:text-foreground/80">← Marktplatz</a>
|
||||
</p>
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -98,6 +98,13 @@
|
|||
--color-card-foreground: hsl(var(--color-card-foreground) / <alpha-value>);
|
||||
--color-accent: hsl(var(--color-accent) / <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 {
|
||||
|
|
@ -170,6 +177,13 @@
|
|||
--color-branch-social: 38 92% 50%; /* amber — warmth, relationships */
|
||||
--color-branch-practical: 173 80% 40%; /* teal — craft, tools */
|
||||
--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) ===== */
|
||||
|
|
|
|||
352
pnpm-lock.yaml
generated
352
pnpm-lock.yaml
generated
|
|
@ -141,14 +141,14 @@ importers:
|
|||
version: link:../../../../packages/shared-landing-ui
|
||||
astro:
|
||||
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:
|
||||
specifier: ^5.9.2
|
||||
version: 5.9.3
|
||||
devDependencies:
|
||||
'@astrojs/tailwind':
|
||||
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':
|
||||
specifier: ^0.5.18
|
||||
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
|
||||
eslint:
|
||||
specifier: ^9.0.0
|
||||
version: 9.39.4(jiti@1.21.7)
|
||||
version: 9.39.4(jiti@2.6.1)
|
||||
eslint-config-prettier:
|
||||
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:
|
||||
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:
|
||||
specifier: ^3.6.2
|
||||
version: 3.8.1
|
||||
|
|
@ -220,6 +220,9 @@ importers:
|
|||
'@mana/shared-theme':
|
||||
specifier: workspace:*
|
||||
version: link:../../../../packages/shared-theme
|
||||
'@mana/shared-theme-ui':
|
||||
specifier: workspace:*
|
||||
version: link:../../../../packages/shared-theme-ui
|
||||
'@mana/shared-types':
|
||||
specifier: workspace:*
|
||||
version: link:../../../../packages/shared-types
|
||||
|
|
@ -321,10 +324,10 @@ importers:
|
|||
version: 3.7.2
|
||||
'@astrojs/tailwind':
|
||||
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:
|
||||
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:
|
||||
specifier: ^3.4.17
|
||||
version: 3.4.19(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
|
@ -17369,16 +17372,6 @@ snapshots:
|
|||
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@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))':
|
||||
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)
|
||||
|
|
@ -17399,6 +17392,16 @@ snapshots:
|
|||
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@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))':
|
||||
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)
|
||||
|
|
@ -19570,11 +19573,6 @@ snapshots:
|
|||
'@esbuild/win32-x64@0.27.7':
|
||||
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))':
|
||||
dependencies:
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
|
|
@ -24683,108 +24681,6 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- 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):
|
||||
dependencies:
|
||||
'@astrojs/compiler': 2.13.1
|
||||
|
|
@ -24989,6 +24885,108 @@ snapshots:
|
|||
- uploadthing
|
||||
- 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):
|
||||
dependencies:
|
||||
'@astrojs/compiler': 2.13.1
|
||||
|
|
@ -26816,11 +26814,6 @@ snapshots:
|
|||
eslint: 9.39.4(jiti@2.6.1)
|
||||
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)):
|
||||
dependencies:
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
|
|
@ -26830,10 +26823,6 @@ snapshots:
|
|||
dependencies:
|
||||
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)):
|
||||
dependencies:
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
|
|
@ -26878,20 +26867,6 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- 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)):
|
||||
dependencies:
|
||||
'@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@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):
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1))
|
||||
|
|
@ -34184,23 +34118,6 @@ snapshots:
|
|||
lightningcss: 1.32.0
|
||||
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):
|
||||
dependencies:
|
||||
esbuild: 0.25.12
|
||||
|
|
@ -34235,6 +34152,23 @@ snapshots:
|
|||
tsx: 4.21.0
|
||||
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):
|
||||
dependencies:
|
||||
esbuild: 0.25.12
|
||||
|
|
@ -34252,10 +34186,6 @@ snapshots:
|
|||
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)):
|
||||
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)):
|
||||
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)
|
||||
|
|
@ -34264,6 +34194,10 @@ snapshots:
|
|||
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)
|
||||
|
||||
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)):
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ const REPO_ROOT = join(__dirname, '..');
|
|||
const THEMES_CSS = join(REPO_ROOT, 'packages/shared-tailwind/src/themes.css');
|
||||
|
||||
/** 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
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@ const REPO_ROOT = join(__dirname, '..');
|
|||
const SCAN_GLOBS = [
|
||||
'apps/mana/apps/web/src/lib/modules/**/*.svelte',
|
||||
'apps/mana/apps/web/src/routes/(app)/**/*.svelte',
|
||||
'apps/cards/apps/web/src/lib/components/**/*.svelte',
|
||||
'apps/cards/apps/web/src/routes/**/*.svelte',
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue