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