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:
parent
03ec7e7b3e
commit
731481ffe3
2 changed files with 527 additions and 149 deletions
|
|
@ -25,6 +25,7 @@
|
|||
|
||||
let imageFiles = $state<File[]>([]);
|
||||
let imagePreviews = $state<string[]>([]);
|
||||
let imageUrl = $state('');
|
||||
let imageGenerating = $state(false);
|
||||
let imageError = $state<string | null>(null);
|
||||
let fileInput = $state<HTMLInputElement | null>(null);
|
||||
|
|
@ -76,16 +77,21 @@
|
|||
for (const url of imagePreviews) URL.revokeObjectURL(url);
|
||||
imageFiles = [];
|
||||
imagePreviews = [];
|
||||
imageUrl = '';
|
||||
imageGenerating = false;
|
||||
imageError = null;
|
||||
}
|
||||
|
||||
async function onFromImage() {
|
||||
if (imageFiles.length === 0 || !devUser.id || imageGenerating) return;
|
||||
if ((imageFiles.length === 0 && !imageUrl.trim()) || !devUser.id || imageGenerating) return;
|
||||
imageError = null;
|
||||
imageGenerating = true;
|
||||
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`);
|
||||
goto(`/decks/${result.deck.id}`);
|
||||
} catch (err) {
|
||||
|
|
@ -173,11 +179,12 @@
|
|||
<div class="cat-grid">
|
||||
{#each DECK_CATEGORY_IDS as 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}>
|
||||
<DeckCategoryIcon category={id} size={16}
|
||||
<DeckCategoryIcon category={id} size={13}
|
||||
color={category === id ? color : null}
|
||||
weight={category === id ? 'fill' : 'regular'} />
|
||||
<span class="cat-label">{DECK_CATEGORY_LABELS[id]}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
@ -248,6 +255,16 @@
|
|||
</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}
|
||||
<p class="ai-error" role="alert">{aiError}</p>
|
||||
{/if}
|
||||
|
|
@ -268,8 +285,16 @@
|
|||
<button type="button" disabled={generating || saving || imageGenerating || !name.trim()} onclick={onAi} class="btn-ai">
|
||||
{generating ? '✨ Generiere…' : '✨ Mit KI generieren'}
|
||||
</button>
|
||||
<button type="button" disabled={imageFiles.length === 0 || imageGenerating || saving || generating} onclick={onFromImage} class="btn-ai">
|
||||
{imageGenerating ? '🖼 Analysiere…' : '🖼 Aus Bild'}
|
||||
<button type="button" disabled={(imageFiles.length === 0 && !imageUrl.trim()) || imageGenerating || saving || generating} onclick={onFromImage} class="btn-ai">
|
||||
{#if imageGenerating}
|
||||
🖼 Analysiere…
|
||||
{:else if imageFiles.length > 0 && imageUrl.trim()}
|
||||
🖼 Bild + URL
|
||||
{:else if imageUrl.trim()}
|
||||
🖼 Aus URL
|
||||
{:else}
|
||||
🖼 Aus Bild
|
||||
{/if}
|
||||
</button>
|
||||
<button type="button" onclick={close} class="btn-cancel">
|
||||
{t('deck_new.cancel')}
|
||||
|
|
@ -402,31 +427,39 @@
|
|||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* Kategorie-Picker — 4 Spalten Icon-Grid, erscheint direkt unter dem Trigger */
|
||||
/* Kategorie-Picker — Flexwrap Pills mit Icon + Label */
|
||||
.cat-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25rem;
|
||||
padding-top: 0.25rem;
|
||||
}
|
||||
|
||||
.cat-btn {
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 0.3125rem;
|
||||
gap: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
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.12s, background 0.12s;
|
||||
transition: border-color 0.12s, background 0.12s, color 0.12s;
|
||||
}
|
||||
.cat-btn:hover {
|
||||
border-color: hsl(var(--color-primary) / 0.5);
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
.cat-btn.selected {
|
||||
border-color: hsl(var(--color-primary));
|
||||
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 */
|
||||
|
|
|
|||
|
|
@ -10,14 +10,32 @@
|
|||
import { devUser } from '$lib/auth/dev-stub.svelte.ts';
|
||||
import { toasts } from '$lib/stores/toasts.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 { 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 cards = $state<Card[]>([]);
|
||||
let dueCount = $state(0);
|
||||
let loading = $state(true);
|
||||
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 ?? '');
|
||||
|
||||
|
|
@ -53,6 +71,7 @@
|
|||
const next = deck.category === id ? null : id;
|
||||
try {
|
||||
deck = await updateDeck(deck.id, { category: next ?? undefined });
|
||||
categoryOpen = false;
|
||||
} catch (e) {
|
||||
toasts.error((e as Error).message);
|
||||
}
|
||||
|
|
@ -68,48 +87,46 @@
|
|||
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>
|
||||
|
||||
<svelte:window onclick={closeMenu} />
|
||||
|
||||
{#if loading}
|
||||
<p class="text-[hsl(var(--color-muted-foreground))]">{t('decks.loading')}</p>
|
||||
{:else if error}
|
||||
<p class="text-[hsl(var(--color-error))]">{t('decks.error', { msg: error })}</p>
|
||||
{:else if deck}
|
||||
<a href="/decks" class="text-sm text-[hsl(var(--color-muted-foreground))] hover:text-[hsl(var(--color-foreground))]"
|
||||
>← {t('nav.decks')}</a
|
||||
>
|
||||
<a href="/decks" class="back-link">← {t('nav.decks')}</a>
|
||||
|
||||
<div class="mt-2 flex flex-wrap items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="deck-header">
|
||||
<div class="deck-title-row">
|
||||
{#if deck.color}
|
||||
<span
|
||||
class="h-4 w-4 rounded-full"
|
||||
style="background:{deck.color}"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span class="color-dot" style="background:{deck.color}" aria-hidden="true"></span>
|
||||
{/if}
|
||||
<h1 class="text-2xl font-semibold">{deck.name}</h1>
|
||||
<h1 class="deck-title">{deck.name}</h1>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<a
|
||||
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))]"
|
||||
>
|
||||
<div class="deck-actions">
|
||||
<a href="/cards/new?deck={deck.id}" class="btn-outline">
|
||||
+ {t('deck_detail.new_card')}
|
||||
</a>
|
||||
{#if dueCount > 0}
|
||||
<a
|
||||
href="/study/{deck.id}"
|
||||
class="rounded bg-[hsl(var(--color-primary))] px-4 py-2 text-sm text-[hsl(var(--color-primary-foreground))]"
|
||||
>
|
||||
<a href="/study/{deck.id}" class="btn-primary">
|
||||
{t('deck_detail.study_button')} ({t('study.due_count', { n: dueCount })})
|
||||
</a>
|
||||
{:else}
|
||||
<button
|
||||
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')}
|
||||
>
|
||||
<button disabled class="btn-primary opacity-50" title={t('study.none_due')}>
|
||||
{t('deck_detail.study_button')}
|
||||
</button>
|
||||
{/if}
|
||||
|
|
@ -117,156 +134,484 @@
|
|||
</div>
|
||||
|
||||
{#if deck.description}
|
||||
<p class="mt-2 text-[hsl(var(--color-muted-foreground))]">{deck.description}</p>
|
||||
<p class="deck-desc">{deck.description}</p>
|
||||
{/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 })}
|
||||
</div>
|
||||
|
||||
<div class="category-row mt-3">
|
||||
{#each DECK_CATEGORY_IDS as id}
|
||||
<button
|
||||
class="cat-btn"
|
||||
class:active={deck.category === id}
|
||||
onclick={() => onSetCategory(id)}
|
||||
>
|
||||
<!-- Fach-Picker: eingeklappt, same style wie /decks/new -->
|
||||
<div class="category-picker">
|
||||
<button
|
||||
class="cat-trigger"
|
||||
class:has-category={!!deck.category}
|
||||
onclick={() => (categoryOpen = !categoryOpen)}
|
||||
aria-expanded={categoryOpen}
|
||||
>
|
||||
{#if deck.category}
|
||||
<DeckCategoryIcon
|
||||
category={id}
|
||||
size={16}
|
||||
color={deck.category === id ? (deck.color ?? null) : null}
|
||||
weight={deck.category === id ? 'fill' : 'regular'}
|
||||
category={deck.category}
|
||||
size={15}
|
||||
color={deck.color ?? null}
|
||||
weight="fill"
|
||||
/>
|
||||
<span class="cat-label">{DECK_CATEGORY_LABELS[id]}</span>
|
||||
</button>
|
||||
{/each}
|
||||
<span>{DECK_CATEGORY_LABELS[deck.category]}</span>
|
||||
{:else}
|
||||
<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>
|
||||
|
||||
{#if cards.length === 0}
|
||||
<div
|
||||
class="mt-8 rounded-lg border border-dashed border-[hsl(var(--color-border))] p-12 text-center"
|
||||
>
|
||||
<div class="empty-state">
|
||||
<p class="text-[hsl(var(--color-muted-foreground))]">{t('deck_detail.empty')}</p>
|
||||
<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
|
||||
>
|
||||
<a href="/cards/new?deck={deck.id}" class="empty-cta">{t('deck_detail.empty_cta')}</a>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Auffächer-Hero: zeigt die obersten Karten als "Hand of Cards" -->
|
||||
<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))]">
|
||||
<ul class="card-grid" aria-label="Karten">
|
||||
{#each cards as card (card.id)}
|
||||
<li class="group flex items-start justify-between gap-4 px-4 py-3">
|
||||
<a
|
||||
<li class="card-item">
|
||||
<CardSurface
|
||||
size="md"
|
||||
as="a"
|
||||
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">
|
||||
<span
|
||||
class="rounded bg-[hsl(var(--color-border))] px-2 py-0.5 text-xs text-[hsl(var(--color-muted-foreground))]"
|
||||
>{card.type}</span>
|
||||
</div>
|
||||
<p class="mt-1 truncate text-sm">
|
||||
{#if card.type === 'cloze'}
|
||||
<span class="font-medium">{card.fields.text ?? t('common.empty')}</span>
|
||||
{:else if card.type === 'image-occlusion'}
|
||||
<span class="font-medium">🖼 image-occlusion</span>
|
||||
<span class="text-[hsl(var(--color-muted-foreground))]">
|
||||
· {card.fields.image_ref
|
||||
? card.fields.image_ref.slice(0, 12)
|
||||
: t('common.empty')}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="font-medium">{card.fields.front ?? t('common.empty')}</span>
|
||||
<span class="text-[hsl(var(--color-muted-foreground))]"> → {card.fields.back ?? t('common.empty')}</span>
|
||||
<div class="card-inner">
|
||||
<!-- Header: Typ + Menü-Button -->
|
||||
<div class="card-header">
|
||||
<span class="card-type">{card.type}</span>
|
||||
<button
|
||||
class="card-menu-btn"
|
||||
onclick={(e) => toggleMenu(card.id, e)}
|
||||
aria-label="Kartenoptionen"
|
||||
tabindex="-1"
|
||||
>
|
||||
<DotsThree size={18} weight="bold" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Menü-Popup -->
|
||||
{#if openMenuId === card.id}
|
||||
<div class="card-menu-popup" role="menu">
|
||||
<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}
|
||||
</p>
|
||||
</a>
|
||||
<button
|
||||
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"
|
||||
onclick={() => onDeleteCard(card.id)}
|
||||
aria-label={t('deck_detail.card_delete_aria')}
|
||||
>
|
||||
{t('deck_detail.card_delete_label')}
|
||||
</button>
|
||||
|
||||
<!-- Karteninhalt -->
|
||||
{#if card.type === 'image-occlusion'}
|
||||
<div class="card-image-placeholder" aria-hidden="true">
|
||||
<Image size={32} weight="duotone" />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="card-content">
|
||||
<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>
|
||||
{/each}
|
||||
</ul>
|
||||
</details>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.category-row {
|
||||
display: flex;
|
||||
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;
|
||||
.back-link {
|
||||
font-size: 0.875rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
cursor: pointer;
|
||||
transition: border-color 0.12s, background 0.12s, color 0.12s;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.cat-btn:hover {
|
||||
border-color: hsl(var(--color-border));
|
||||
.back-link:hover {
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
|
||||
.cat-btn.active {
|
||||
border-color: hsl(var(--color-primary) / 0.4);
|
||||
.deck-header {
|
||||
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);
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
|
||||
.cat-label {
|
||||
font-size: 0.75rem;
|
||||
.category-label {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.list-toggle {
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
padding: 0.5rem 0;
|
||||
/* ── Card grid ───────────────────────────────────────── */
|
||||
|
||||
.card-grid {
|
||||
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: '▸ ';
|
||||
display: inline-block;
|
||||
transition: transform 0.15s ease;
|
||||
|
||||
.card-inner {
|
||||
position: absolute;
|
||||
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));
|
||||
}
|
||||
: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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue