refactor(deck-detail): Redesign mit Kategorie-Picker, Card-Menü, Markdown

- Deck-Header mit Farb-Dot, Titel, Aktions-Buttons (Lernen / Neue Karte)
- Kategorie-Picker (eingeklappt, inline wie /decks/new)
- Card-List mit kontextuellem 3-Punkte-Menü (Edit / Löschen)
- Karten-Vorschau mit Front/Back via marked
- NewDeckCard: Kategorie-Icon-Größe korrigiert

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-05-10 15:59:56 +02:00
parent 03ec7e7b3e
commit 731481ffe3
2 changed files with 527 additions and 149 deletions

View file

@ -25,6 +25,7 @@
let imageFiles = $state<File[]>([]); let imageFiles = $state<File[]>([]);
let imagePreviews = $state<string[]>([]); let imagePreviews = $state<string[]>([]);
let imageUrl = $state('');
let imageGenerating = $state(false); let imageGenerating = $state(false);
let imageError = $state<string | null>(null); let imageError = $state<string | null>(null);
let fileInput = $state<HTMLInputElement | null>(null); let fileInput = $state<HTMLInputElement | null>(null);
@ -76,16 +77,21 @@
for (const url of imagePreviews) URL.revokeObjectURL(url); for (const url of imagePreviews) URL.revokeObjectURL(url);
imageFiles = []; imageFiles = [];
imagePreviews = []; imagePreviews = [];
imageUrl = '';
imageGenerating = false; imageGenerating = false;
imageError = null; imageError = null;
} }
async function onFromImage() { async function onFromImage() {
if (imageFiles.length === 0 || !devUser.id || imageGenerating) return; if ((imageFiles.length === 0 && !imageUrl.trim()) || !devUser.id || imageGenerating) return;
imageError = null; imageError = null;
imageGenerating = true; imageGenerating = true;
try { try {
const result = await generateDeckFromImage(imageFiles, { count, language }); const result = await generateDeckFromImage(imageFiles, {
count,
language,
url: imageUrl.trim() || undefined,
});
toasts.success(`🖼 "${result.deck.name}" mit ${result.cards_created} Karten erstellt`); toasts.success(`🖼 "${result.deck.name}" mit ${result.cards_created} Karten erstellt`);
goto(`/decks/${result.deck.id}`); goto(`/decks/${result.deck.id}`);
} catch (err) { } catch (err) {
@ -173,11 +179,12 @@
<div class="cat-grid"> <div class="cat-grid">
{#each DECK_CATEGORY_IDS as id} {#each DECK_CATEGORY_IDS as id}
<button type="button" class="cat-btn" class:selected={category === id} <button type="button" class="cat-btn" class:selected={category === id}
onclick={() => pickCategory(id)} title={DECK_CATEGORY_LABELS[id]} onclick={() => pickCategory(id)}
aria-pressed={category === id}> aria-pressed={category === id}>
<DeckCategoryIcon category={id} size={16} <DeckCategoryIcon category={id} size={13}
color={category === id ? color : null} color={category === id ? color : null}
weight={category === id ? 'fill' : 'regular'} /> weight={category === id ? 'fill' : 'regular'} />
<span class="cat-label">{DECK_CATEGORY_LABELS[id]}</span>
</button> </button>
{/each} {/each}
</div> </div>
@ -248,6 +255,16 @@
</div> </div>
</div> </div>
<label class="field">
<span class="label">URL als Kontext (optional)</span>
<input
bind:value={imageUrl}
type="url"
placeholder="https://…"
class="input"
/>
</label>
{#if aiError} {#if aiError}
<p class="ai-error" role="alert">{aiError}</p> <p class="ai-error" role="alert">{aiError}</p>
{/if} {/if}
@ -268,8 +285,16 @@
<button type="button" disabled={generating || saving || imageGenerating || !name.trim()} onclick={onAi} class="btn-ai"> <button type="button" disabled={generating || saving || imageGenerating || !name.trim()} onclick={onAi} class="btn-ai">
{generating ? '✨ Generiere…' : '✨ Mit KI generieren'} {generating ? '✨ Generiere…' : '✨ Mit KI generieren'}
</button> </button>
<button type="button" disabled={imageFiles.length === 0 || imageGenerating || saving || generating} onclick={onFromImage} class="btn-ai"> <button type="button" disabled={(imageFiles.length === 0 && !imageUrl.trim()) || imageGenerating || saving || generating} onclick={onFromImage} class="btn-ai">
{imageGenerating ? '🖼 Analysiere…' : '🖼 Aus Bild'} {#if imageGenerating}
🖼 Analysiere…
{:else if imageFiles.length > 0 && imageUrl.trim()}
🖼 Bild + URL
{:else if imageUrl.trim()}
🖼 Aus URL
{:else}
🖼 Aus Bild
{/if}
</button> </button>
<button type="button" onclick={close} class="btn-cancel"> <button type="button" onclick={close} class="btn-cancel">
{t('deck_new.cancel')} {t('deck_new.cancel')}
@ -402,31 +427,39 @@
transform: rotate(180deg); transform: rotate(180deg);
} }
/* Kategorie-Picker — 4 Spalten Icon-Grid, erscheint direkt unter dem Trigger */ /* Kategorie-Picker — Flexwrap Pills mit Icon + Label */
.cat-grid { .cat-grid {
display: grid; display: flex;
grid-template-columns: repeat(4, 1fr); flex-wrap: wrap;
gap: 0.25rem; gap: 0.25rem;
padding-top: 0.25rem; padding-top: 0.25rem;
} }
.cat-btn { .cat-btn {
display: flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; gap: 0.25rem;
aspect-ratio: 1; padding: 0.25rem 0.5rem;
border-radius: 0.3125rem; border-radius: 9999px;
border: 1px solid hsl(var(--color-border)); border: 1px solid hsl(var(--color-border));
background: hsl(var(--color-surface)); background: hsl(var(--color-surface));
color: hsl(var(--color-muted-foreground));
cursor: pointer; cursor: pointer;
transition: border-color 0.12s, background 0.12s; transition: border-color 0.12s, background 0.12s, color 0.12s;
} }
.cat-btn:hover { .cat-btn:hover {
border-color: hsl(var(--color-primary) / 0.5); border-color: hsl(var(--color-primary) / 0.5);
color: hsl(var(--color-foreground));
} }
.cat-btn.selected { .cat-btn.selected {
border-color: hsl(var(--color-primary)); border-color: hsl(var(--color-primary));
background: hsl(var(--color-primary) / 0.08); background: hsl(var(--color-primary) / 0.08);
color: hsl(var(--color-foreground));
}
.cat-label {
font-size: 0.75rem;
font-weight: 500;
} }
/* Farbe — volle Breite, feste Höhe wie ein Input */ /* Farbe — volle Breite, feste Höhe wie ein Input */

View file

@ -10,14 +10,32 @@
import { devUser } from '$lib/auth/dev-stub.svelte.ts'; import { devUser } from '$lib/auth/dev-stub.svelte.ts';
import { toasts } from '$lib/stores/toasts.svelte.ts'; import { toasts } from '$lib/stores/toasts.svelte.ts';
import { t, tn } from '$lib/i18n/index.svelte.ts'; import { t, tn } from '$lib/i18n/index.svelte.ts';
import DeckFan from '$lib/components/DeckFan.svelte'; import CardSurface from '$lib/components/CardSurface.svelte';
import DeckCategoryIcon from '$lib/components/DeckCategoryIcon.svelte'; import DeckCategoryIcon from '$lib/components/DeckCategoryIcon.svelte';
import { Image, CaretRight, DotsThree, PencilSimple, Trash } from '@mana/shared-icons';
import { marked } from 'marked';
function md(text: string): string {
return marked.parse(text, { async: false }) as string;
}
let deck = $state<Deck | null>(null); let deck = $state<Deck | null>(null);
let cards = $state<Card[]>([]); let cards = $state<Card[]>([]);
let dueCount = $state(0); let dueCount = $state(0);
let loading = $state(true); let loading = $state(true);
let error = $state<string | null>(null); let error = $state<string | null>(null);
let categoryOpen = $state(false);
let openMenuId = $state<string | null>(null);
function toggleMenu(cardId: string, e: MouseEvent) {
e.preventDefault();
e.stopPropagation();
openMenuId = openMenuId === cardId ? null : cardId;
}
function closeMenu() {
openMenuId = null;
}
const deckId = $derived(page.params.id ?? ''); const deckId = $derived(page.params.id ?? '');
@ -53,6 +71,7 @@
const next = deck.category === id ? null : id; const next = deck.category === id ? null : id;
try { try {
deck = await updateDeck(deck.id, { category: next ?? undefined }); deck = await updateDeck(deck.id, { category: next ?? undefined });
categoryOpen = false;
} catch (e) { } catch (e) {
toasts.error((e as Error).message); toasts.error((e as Error).message);
} }
@ -68,48 +87,46 @@
toasts.error(t('card_edit.delete_failed', { msg: (e as Error).message })); toasts.error(t('card_edit.delete_failed', { msg: (e as Error).message }));
} }
} }
function cardFront(card: Card): string {
const f = card.fields as Record<string, string | undefined>;
if (card.type === 'cloze') return f.text ?? '';
return f.front ?? '';
}
function cardBack(card: Card): string | null {
const f = card.fields as Record<string, string | undefined>;
if (card.type === 'basic') return f.back ?? null;
return null;
}
</script> </script>
<svelte:window onclick={closeMenu} />
{#if loading} {#if loading}
<p class="text-[hsl(var(--color-muted-foreground))]">{t('decks.loading')}</p> <p class="text-[hsl(var(--color-muted-foreground))]">{t('decks.loading')}</p>
{:else if error} {:else if error}
<p class="text-[hsl(var(--color-error))]">{t('decks.error', { msg: error })}</p> <p class="text-[hsl(var(--color-error))]">{t('decks.error', { msg: error })}</p>
{:else if deck} {:else if deck}
<a href="/decks" class="text-sm text-[hsl(var(--color-muted-foreground))] hover:text-[hsl(var(--color-foreground))]" <a href="/decks" class="back-link">{t('nav.decks')}</a>
>← {t('nav.decks')}</a
>
<div class="mt-2 flex flex-wrap items-center justify-between gap-4"> <div class="deck-header">
<div class="flex items-center gap-3"> <div class="deck-title-row">
{#if deck.color} {#if deck.color}
<span <span class="color-dot" style="background:{deck.color}" aria-hidden="true"></span>
class="h-4 w-4 rounded-full"
style="background:{deck.color}"
aria-hidden="true"
></span>
{/if} {/if}
<h1 class="text-2xl font-semibold">{deck.name}</h1> <h1 class="deck-title">{deck.name}</h1>
</div> </div>
<div class="flex gap-2"> <div class="deck-actions">
<a <a href="/cards/new?deck={deck.id}" class="btn-outline">
href="/cards/new?deck={deck.id}"
class="rounded border border-[hsl(var(--color-border))] px-3 py-2 text-sm hover:border-[hsl(var(--color-primary))]"
>
+ {t('deck_detail.new_card')} + {t('deck_detail.new_card')}
</a> </a>
{#if dueCount > 0} {#if dueCount > 0}
<a <a href="/study/{deck.id}" class="btn-primary">
href="/study/{deck.id}"
class="rounded bg-[hsl(var(--color-primary))] px-4 py-2 text-sm text-[hsl(var(--color-primary-foreground))]"
>
{t('deck_detail.study_button')} ({t('study.due_count', { n: dueCount })}) {t('deck_detail.study_button')} ({t('study.due_count', { n: dueCount })})
</a> </a>
{:else} {:else}
<button <button disabled class="btn-primary opacity-50" title={t('study.none_due')}>
disabled
class="rounded bg-[hsl(var(--color-muted-foreground))] px-4 py-2 text-sm text-[hsl(var(--color-background))] opacity-50"
title={t('study.none_due')}
>
{t('deck_detail.study_button')} {t('deck_detail.study_button')}
</button> </button>
{/if} {/if}
@ -117,156 +134,484 @@
</div> </div>
{#if deck.description} {#if deck.description}
<p class="mt-2 text-[hsl(var(--color-muted-foreground))]">{deck.description}</p> <p class="deck-desc">{deck.description}</p>
{/if} {/if}
<div class="mt-2 text-sm text-[hsl(var(--color-muted-foreground))]"> <div class="deck-meta">
{tn('decks.card_count', cards.length)} · {t('study.due_count', { n: dueCount })} {tn('decks.card_count', cards.length)} · {t('study.due_count', { n: dueCount })}
</div> </div>
<div class="category-row mt-3"> <!-- Fach-Picker: eingeklappt, same style wie /decks/new -->
{#each DECK_CATEGORY_IDS as id} <div class="category-picker">
<button <button
class="cat-btn" class="cat-trigger"
class:active={deck.category === id} class:has-category={!!deck.category}
onclick={() => onSetCategory(id)} onclick={() => (categoryOpen = !categoryOpen)}
> aria-expanded={categoryOpen}
>
{#if deck.category}
<DeckCategoryIcon <DeckCategoryIcon
category={id} category={deck.category}
size={16} size={15}
color={deck.category === id ? (deck.color ?? null) : null} color={deck.color ?? null}
weight={deck.category === id ? 'fill' : 'regular'} weight="fill"
/> />
<span class="cat-label">{DECK_CATEGORY_LABELS[id]}</span> <span>{DECK_CATEGORY_LABELS[deck.category]}</span>
</button> {:else}
{/each} <span class="no-cat">Fach wählen</span>
{/if}
<span class="cat-chevron" class:open={categoryOpen}>
<CaretRight size={12} weight="bold" />
</span>
</button>
{#if categoryOpen}
<div class="category-grid">
{#each DECK_CATEGORY_IDS as id}
<button
type="button"
class="category-btn"
class:selected={deck.category === id}
onclick={() => onSetCategory(id)}
title={DECK_CATEGORY_LABELS[id]}
>
<DeckCategoryIcon
category={id}
size={16}
color={deck.category === id ? (deck.color ?? null) : null}
weight={deck.category === id ? 'fill' : 'regular'}
/>
<span class="category-label">{DECK_CATEGORY_LABELS[id]}</span>
</button>
{/each}
</div>
{/if}
</div> </div>
{#if cards.length === 0} {#if cards.length === 0}
<div <div class="empty-state">
class="mt-8 rounded-lg border border-dashed border-[hsl(var(--color-border))] p-12 text-center"
>
<p class="text-[hsl(var(--color-muted-foreground))]">{t('deck_detail.empty')}</p> <p class="text-[hsl(var(--color-muted-foreground))]">{t('deck_detail.empty')}</p>
<a <a href="/cards/new?deck={deck.id}" class="empty-cta">{t('deck_detail.empty_cta')}</a>
href="/cards/new?deck={deck.id}"
class="mt-4 inline-block text-[hsl(var(--color-primary))] hover:underline"
>{t('deck_detail.empty_cta')}</a
>
</div> </div>
{:else} {:else}
<!-- Auffächer-Hero: zeigt die obersten Karten als "Hand of Cards" --> <ul class="card-grid" aria-label="Karten">
<div class="mt-4">
<DeckFan
{deck}
{cards}
totalCount={cards.length}
{dueCount}
maxFanCards={7}
onCardClick={(card) => goto(`/cards/${card.id}/edit`)}
/>
</div>
<!-- Detail-Liste für Edit/Delete-Operationen — nicht jeder Klick soll
durch die Auffächerung müssen, Power-User wollen die Liste. -->
<details class="mt-2">
<summary class="list-toggle">{t('deck_detail.new_card')} · alle Karten</summary>
<ul class="mt-3 divide-y divide-[hsl(var(--color-border))] rounded-lg border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))]">
{#each cards as card (card.id)} {#each cards as card (card.id)}
<li class="group flex items-start justify-between gap-4 px-4 py-3"> <li class="card-item">
<a <CardSurface
size="md"
as="a"
href="/cards/{card.id}/edit" href="/cards/{card.id}/edit"
class="min-w-0 flex-1 hover:text-[hsl(var(--color-primary))]" colorAccent={deck.color ?? null}
ariaLabel="{card.type}{cardFront(card)}"
> >
<div class="flex items-center gap-2"> <div class="card-inner">
<span <!-- Header: Typ + Menü-Button -->
class="rounded bg-[hsl(var(--color-border))] px-2 py-0.5 text-xs text-[hsl(var(--color-muted-foreground))]" <div class="card-header">
>{card.type}</span> <span class="card-type">{card.type}</span>
</div> <button
<p class="mt-1 truncate text-sm"> class="card-menu-btn"
{#if card.type === 'cloze'} onclick={(e) => toggleMenu(card.id, e)}
<span class="font-medium">{card.fields.text ?? t('common.empty')}</span> aria-label="Kartenoptionen"
{:else if card.type === 'image-occlusion'} tabindex="-1"
<span class="font-medium">🖼 image-occlusion</span> >
<span class="text-[hsl(var(--color-muted-foreground))]"> <DotsThree size={18} weight="bold" />
· {card.fields.image_ref </button>
? card.fields.image_ref.slice(0, 12) </div>
: t('common.empty')}
</span> <!-- Menü-Popup -->
{:else} {#if openMenuId === card.id}
<span class="font-medium">{card.fields.front ?? t('common.empty')}</span> <div class="card-menu-popup" role="menu">
<span class="text-[hsl(var(--color-muted-foreground))]">{card.fields.back ?? t('common.empty')}</span> <a
href="/cards/{card.id}/edit"
class="menu-item"
role="menuitem"
onclick={(e) => e.stopPropagation()}
>
<PencilSimple size={14} weight="regular" />
Bearbeiten
</a>
<button
class="menu-item menu-item-danger"
role="menuitem"
onclick={(e) => { e.preventDefault(); e.stopPropagation(); openMenuId = null; onDeleteCard(card.id); }}
>
<Trash size={14} weight="regular" />
Löschen
</button>
</div>
{/if} {/if}
</p>
</a> <!-- Karteninhalt -->
<button {#if card.type === 'image-occlusion'}
class="text-sm text-[hsl(var(--color-muted-foreground))] opacity-0 hover:text-[hsl(var(--color-error))] focus-visible:opacity-100 group-hover:opacity-100" <div class="card-image-placeholder" aria-hidden="true">
onclick={() => onDeleteCard(card.id)} <Image size={32} weight="duotone" />
aria-label={t('deck_detail.card_delete_aria')} </div>
> {:else}
{t('deck_detail.card_delete_label')} <div class="card-content">
</button> <div class="card-side card-front md-content">
{@html md(cardFront(card))}
</div>
{#if cardBack(card)}
<div class="card-divider" aria-hidden="true"></div>
<div class="card-side card-back md-content">
{@html md(cardBack(card) ?? '')}
</div>
{/if}
</div>
{/if}
</div>
</CardSurface>
</li> </li>
{/each} {/each}
</ul> </ul>
</details>
{/if} {/if}
{/if} {/if}
<style> <style>
.category-row { .back-link {
display: flex; font-size: 0.875rem;
flex-wrap: wrap;
gap: 0.25rem;
}
.cat-btn {
display: inline-flex;
align-items: center;
gap: 0.3rem;
padding: 0.25rem 0.5rem;
border-radius: 0.375rem;
border: 1px solid transparent;
background: transparent;
color: hsl(var(--color-muted-foreground)); color: hsl(var(--color-muted-foreground));
cursor: pointer; text-decoration: none;
transition: border-color 0.12s, background 0.12s, color 0.12s;
} }
.back-link:hover {
.cat-btn:hover {
border-color: hsl(var(--color-border));
color: hsl(var(--color-foreground)); color: hsl(var(--color-foreground));
} }
.cat-btn.active { .deck-header {
border-color: hsl(var(--color-primary) / 0.4); margin-top: 0.5rem;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.deck-title-row {
display: flex;
align-items: center;
gap: 0.75rem;
}
.color-dot {
width: 1rem;
height: 1rem;
border-radius: 9999px;
flex-shrink: 0;
}
.deck-title {
font-size: 1.5rem;
font-weight: 600;
margin: 0;
}
.deck-actions {
display: flex;
gap: 0.5rem;
}
.btn-outline {
display: inline-block;
border: 1px solid hsl(var(--color-border));
border-radius: 0.375rem;
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
text-decoration: none;
color: inherit;
transition: border-color 0.12s;
}
.btn-outline:hover {
border-color: hsl(var(--color-primary));
}
.btn-primary {
display: inline-block;
border-radius: 0.375rem;
padding: 0.5rem 1rem;
font-size: 0.875rem;
font-family: inherit;
cursor: pointer;
text-decoration: none;
background: hsl(var(--color-primary));
color: hsl(var(--color-primary-foreground));
border: none;
}
.deck-desc {
margin-top: 0.5rem;
color: hsl(var(--color-muted-foreground));
}
.deck-meta {
margin-top: 0.25rem;
font-size: 0.875rem;
color: hsl(var(--color-muted-foreground));
}
/* ── Category picker ─────────────────────────────────── */
.category-picker {
margin-top: 0.875rem;
}
.cat-trigger {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.3rem 0.625rem;
border-radius: 0.375rem;
border: 1px solid hsl(var(--color-border));
background: transparent;
font-size: 0.8125rem;
font-family: inherit;
color: hsl(var(--color-muted-foreground));
cursor: pointer;
transition: border-color 0.12s, color 0.12s;
}
.cat-trigger:hover,
.cat-trigger[aria-expanded='true'] {
border-color: hsl(var(--color-primary) / 0.5);
color: hsl(var(--color-foreground));
}
.cat-trigger.has-category {
color: hsl(var(--color-foreground));
}
.no-cat {
font-style: italic;
}
.cat-chevron {
display: flex;
transition: transform 0.15s ease;
}
.cat-chevron.open {
transform: rotate(90deg);
}
.category-grid {
margin-top: 0.5rem;
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
}
.category-btn {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.375rem 0.75rem;
border-radius: 9999px;
border: 1px solid hsl(var(--color-border));
background: hsl(var(--color-surface));
color: hsl(var(--color-muted-foreground));
cursor: pointer;
transition: border-color 0.15s, background 0.15s, color 0.15s;
}
.category-btn:hover {
border-color: hsl(var(--color-primary) / 0.5);
color: hsl(var(--color-foreground));
}
.category-btn.selected {
border-color: hsl(var(--color-primary));
background: hsl(var(--color-primary) / 0.08); background: hsl(var(--color-primary) / 0.08);
color: hsl(var(--color-foreground)); color: hsl(var(--color-foreground));
} }
.cat-label { .category-label {
font-size: 0.75rem; font-size: 0.8125rem;
font-weight: 500; font-weight: 500;
} }
.list-toggle { /* ── Card grid ───────────────────────────────────────── */
cursor: pointer;
font-size: 0.875rem; .card-grid {
color: hsl(var(--color-muted-foreground));
padding: 0.5rem 0;
list-style: none; list-style: none;
margin: 1.25rem 0 0;
padding: 0;
display: grid;
gap: 1.5rem 1rem;
grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
justify-items: center;
} }
.list-toggle::-webkit-details-marker {
display: none; .card-item {
width: 100%;
display: flex;
justify-content: center;
} }
.list-toggle::before {
content: '▸ '; .card-inner {
display: inline-block; position: absolute;
transition: transform 0.15s ease; inset: 0;
display: flex;
flex-direction: column;
padding: 0.75rem 0.75rem 0.875rem 1.25rem;
overflow: hidden;
} }
details[open] .list-toggle::before {
transform: rotate(90deg); .card-header {
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
} }
.list-toggle:hover {
.card-type {
font-size: 0.625rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.07em;
color: hsl(var(--color-muted-foreground));
}
.card-menu-btn {
display: flex;
align-items: center;
justify-content: center;
width: 1.5rem;
height: 1.5rem;
border-radius: 0.25rem;
border: none;
background: transparent;
color: hsl(var(--color-muted-foreground));
cursor: pointer;
opacity: 0;
transition: opacity 0.12s, background 0.12s, color 0.12s;
padding: 0;
}
.card-menu-btn:hover {
background: hsl(var(--color-border));
color: hsl(var(--color-foreground)); color: hsl(var(--color-foreground));
} }
:global(.card-item:hover .card-menu-btn),
:global(.card-item:focus-within .card-menu-btn) {
opacity: 1;
}
.card-menu-popup {
position: absolute;
top: 2.25rem;
right: 0.5rem;
z-index: 10;
min-width: 9rem;
border-radius: 0.5rem;
border: 1px solid hsl(var(--color-border));
background: hsl(var(--color-card));
box-shadow: 0 8px 24px hsl(var(--color-foreground) / 0.12);
overflow: hidden;
display: flex;
flex-direction: column;
}
.menu-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.875rem;
font-size: 0.8125rem;
font-family: inherit;
color: hsl(var(--color-foreground));
text-decoration: none;
background: transparent;
border: none;
cursor: pointer;
text-align: left;
transition: background 0.1s;
}
.menu-item:hover {
background: hsl(var(--color-border) / 0.5);
}
.menu-item-danger {
color: hsl(var(--color-error));
}
/* Wrapper teilt den verfügbaren Raum zwischen Frage und Antwort */
.card-content {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
margin-top: 0.5rem;
}
.card-side {
flex: 1;
min-height: 0;
overflow: hidden;
font-size: 0.875rem;
line-height: 1.45;
}
.card-front {
font-weight: 500;
}
.card-divider {
margin: 0.5rem 0;
height: 1px;
background: hsl(var(--color-border));
flex-shrink: 0;
}
.card-back {
color: hsl(var(--color-foreground) / 0.75);
}
/* Markdown-rendered content */
.md-content :global(p) {
margin: 0 0 0.35em;
}
.md-content :global(p:last-child) {
margin-bottom: 0;
}
.md-content :global(strong) {
font-weight: 700;
}
.md-content :global(em) {
font-style: italic;
}
.md-content :global(code) {
font-family: ui-monospace, monospace;
font-size: 0.875em;
background: hsl(var(--color-border) / 0.7);
padding: 0.1em 0.35em;
border-radius: 3px;
}
.md-content :global(ul),
.md-content :global(ol) {
margin: 0 0 0.35em 1.1em;
padding: 0;
}
.md-content :global(li) {
margin-bottom: 0.15em;
}
.card-image-placeholder {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
color: hsl(var(--color-muted-foreground));
opacity: 0.4;
}
.empty-state {
margin-top: 2rem;
border: 1px dashed hsl(var(--color-border));
border-radius: 0.5rem;
padding: 3rem;
text-align: center;
}
.empty-cta {
margin-top: 1rem;
display: inline-block;
color: hsl(var(--color-primary));
text-decoration: none;
}
.empty-cta:hover {
text-decoration: underline;
}
</style> </style>