feat(decks): Edit-Icon auf Deck-Karten + Deck-Edit-Page

- DeckStack: Pencil-Icon absolut unten-rechts, erscheint beim Hover
  (z-index über Card, ausserhalb des <a>-Links zur Detail-Page)
- Neuer Route /decks/[id]/edit: Form für Name, Beschreibung, Farbe
- i18n deck_edit keys (de + en)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-05-10 15:57:37 +02:00
parent 9754718157
commit 03ec7e7b3e
4 changed files with 298 additions and 0 deletions

View file

@ -5,6 +5,7 @@
import { t, tn } from '$lib/i18n/index.svelte.ts';
import CardSurface from './CardSurface.svelte';
import DeckCategoryIcon from './DeckCategoryIcon.svelte';
import { PencilSimple } from '@mana/shared-icons';
interface Props {
deck: Deck;
@ -43,6 +44,16 @@
{/each}
{/if}
<a
href="/decks/{deck.id}/edit"
class="edit-btn"
onclick={(e) => e.stopPropagation()}
aria-label="Deck bearbeiten"
title="Deck bearbeiten"
>
<PencilSimple size={13} weight="bold" />
</a>
<CardSurface
size="md"
as={href ? 'a' : 'button'}
@ -170,4 +181,34 @@
border-style: dashed;
background: transparent;
}
.edit-btn {
position: absolute;
bottom: 0.625rem;
right: 0.625rem;
z-index: 10;
width: 1.625rem;
height: 1.625rem;
border-radius: 0.375rem;
background: hsl(var(--color-surface) / 0.92);
border: 1px solid hsl(var(--color-border));
display: flex;
align-items: center;
justify-content: center;
color: hsl(var(--color-muted-foreground));
text-decoration: none;
opacity: 0;
transition: opacity 0.15s, color 0.12s, background 0.12s, border-color 0.12s;
backdrop-filter: blur(4px);
}
.stack-wrap:hover .edit-btn {
opacity: 1;
}
.edit-btn:hover {
color: hsl(var(--color-foreground));
background: hsl(var(--color-surface));
border-color: hsl(var(--color-primary) / 0.5);
}
</style>

View file

@ -57,6 +57,18 @@ export const de: TranslationNode = {
deck_stack: {
aria_label: 'Stapel "{name}" — {cards} Karten, {due} fällig',
},
deck_edit: {
title: 'Deck bearbeiten',
back: '← Zurück zum Deck',
name_label: 'Name',
description_label: 'Beschreibung (optional)',
color_label: 'Farbe',
save: 'Speichern',
saving: 'Speichere…',
cancel: 'Abbrechen',
save_failed: 'Speichern fehlgeschlagen: {msg}',
saved: 'Deck gespeichert',
},
deck_new: {
title: 'Neues Deck',
name_label: 'Name',

View file

@ -54,6 +54,18 @@ export const en: TranslationNode = {
deck_stack: {
aria_label: 'Stack "{name}" — {cards} cards, {due} due',
},
deck_edit: {
title: 'Edit deck',
back: '← Back to deck',
name_label: 'Name',
description_label: 'Description (optional)',
color_label: 'Color',
save: 'Save',
saving: 'Saving…',
cancel: 'Cancel',
save_failed: 'Save failed: {msg}',
saved: 'Deck saved',
},
deck_new: {
title: 'New deck',
name_label: 'Name',

View file

@ -0,0 +1,233 @@
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/state';
import { goto } from '$app/navigation';
import { getDeck, updateDeck } from '$lib/api/decks.ts';
import { devUser } from '$lib/auth/dev-stub.svelte.ts';
import { toasts } from '$lib/stores/toasts.svelte.ts';
import { t } from '$lib/i18n/index.svelte.ts';
const deckId = $derived(page.params.id ?? '');
let name = $state('');
let description = $state('');
let color = $state('#6366f1');
let loading = $state(true);
let saving = $state(false);
onMount(async () => {
if (!devUser.id) { goto('/'); return; }
try {
const deck = await getDeck(deckId);
name = deck.name;
description = deck.description ?? '';
color = deck.color ?? '#6366f1';
} catch (e) {
toasts.error((e as Error).message);
goto(`/decks/${deckId}`);
} finally {
loading = false;
}
});
const canSave = $derived(!saving && name.trim().length > 0);
async function onSubmit(e: SubmitEvent) {
e.preventDefault();
if (!canSave) return;
saving = true;
try {
await updateDeck(deckId, {
name: name.trim(),
description: description.trim() || undefined,
color,
});
toasts.success(t('deck_edit.saved'));
goto(`/decks/${deckId}`);
} catch (e) {
toasts.error(t('deck_edit.save_failed', { msg: (e as Error).message }));
saving = false;
}
}
</script>
{#if !loading}
<div class="page-shell">
<a href="/decks/{deckId}" class="back-link">{t('deck_edit.back')}</a>
<h1 class="page-title">{t('deck_edit.title')}</h1>
<form class="edit-form" onsubmit={onSubmit}>
<section class="form-section">
<label class="field">
<span class="field-label">{t('deck_edit.name_label')}</span>
<input
type="text"
bind:value={name}
required
maxlength="200"
placeholder="Deck-Name"
class="input"
/>
</label>
<label class="field">
<span class="field-label">{t('deck_edit.description_label')}</span>
<textarea
bind:value={description}
rows="3"
placeholder="Kurze Beschreibung…"
class="input"
></textarea>
</label>
<div class="field">
<span class="field-label">{t('deck_edit.color_label')}</span>
<div class="color-row">
<input type="color" bind:value={color} class="color-input" />
<span class="color-value">{color}</span>
<span class="color-preview" style="background:{color}" aria-hidden="true"></span>
</div>
</div>
</section>
<div class="actions">
<button type="submit" disabled={!canSave} class="btn-primary">
{saving ? t('deck_edit.saving') : t('deck_edit.save')}
</button>
<a href="/decks/{deckId}" class="btn-ghost">{t('deck_edit.cancel')}</a>
</div>
</form>
</div>
{/if}
<style>
.page-shell {
max-width: 36rem;
margin: 0 auto;
}
.back-link {
display: inline-block;
font-size: 0.875rem;
color: hsl(var(--color-muted-foreground));
text-decoration: none;
transition: color 0.12s;
}
.back-link:hover { color: hsl(var(--color-foreground)); }
.page-title {
margin-top: 0.5rem;
margin-bottom: 1.5rem;
font-size: 1.5rem;
font-weight: 600;
}
.edit-form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.form-section {
background: hsl(var(--color-card));
border: 1px solid hsl(var(--color-border));
border-radius: 0.75rem;
padding: 1.25rem;
display: flex;
flex-direction: column;
gap: 1.125rem;
}
.field {
display: flex;
flex-direction: column;
gap: 0.375rem;
}
.field-label {
font-size: 0.8125rem;
font-weight: 600;
letter-spacing: 0.01em;
}
.input {
display: block;
width: 100%;
border: 1px solid hsl(var(--color-border));
border-radius: 0.5rem;
background: hsl(var(--color-surface));
color: hsl(var(--color-foreground));
padding: 0.5rem 0.625rem;
font: inherit;
font-size: 0.875rem;
transition: border-color 0.12s, box-shadow 0.12s;
resize: vertical;
}
.input:focus {
outline: none;
border-color: hsl(var(--color-primary) / 0.6);
box-shadow: 0 0 0 3px hsl(var(--color-primary) / 0.1);
}
.color-row {
display: flex;
align-items: center;
gap: 0.75rem;
}
.color-input {
width: 3rem;
height: 2.25rem;
border-radius: 0.375rem;
border: 1px solid hsl(var(--color-border));
background: transparent;
cursor: pointer;
padding: 0.125rem;
}
.color-value {
font-size: 0.8125rem;
font-family: ui-monospace, monospace;
color: hsl(var(--color-muted-foreground));
}
.color-preview {
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
border: 1px solid hsl(var(--color-border));
flex-shrink: 0;
}
.actions {
display: flex;
align-items: center;
gap: 0.75rem;
padding-bottom: 1rem;
}
.btn-primary {
display: inline-flex;
align-items: center;
padding: 0.5rem 1.125rem;
border-radius: 0.5rem;
background: hsl(var(--color-primary));
color: hsl(var(--color-primary-foreground));
font: inherit;
font-size: 0.875rem;
font-weight: 500;
border: none;
cursor: pointer;
transition: background-color 0.12s;
}
.btn-primary:hover:not(:disabled) { background: hsl(var(--color-primary) / 0.88); }
.btn-primary:disabled { opacity: 0.45; cursor: default; }
.btn-ghost {
font-size: 0.875rem;
color: hsl(var(--color-muted-foreground));
text-decoration: none;
transition: color 0.12s;
}
.btn-ghost:hover { color: hsl(var(--color-foreground)); }
</style>