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 */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue