Decks fühlen sich jetzt wie echte Karten an, statt flacher Boxen.
Eine zentrale CardSurface-Komponente trägt die Karten-Optik
(Border, Radius 0.875rem, Shadow, Color-Stripe links) — drei
Sizes (md/lg/hero), drei Konsumenten, ein visuelles Familien-Set.
Komponenten:
- CardSurface.svelte Foundation für jede Card-Erscheinung in Cards
- DeckStack.svelte 5:7-Stapel mit 3 deterministisch tilted
Hintergrund-Layern (cyrb53-Hash der Deck-ID,
reproduzierbar pro Mount), Color-Stripe-Akzent,
Title/Description/Card-Count/Due-Badge
- DeckGrid.svelte Auto-fill responsive Grid; selectedId-Prop
triggert Stufe-1-Animation (others fade-out,
selected scale-up)
- DeckFan.svelte Auffächer-Detail-View (Hand of Cards) mit
Top-7-Karten als Spread, Mount-Animation
gestapelt → fanned via doppeltem rAF +
cubic-bezier-Transition
- utils/deck-tilt.ts cyrb53 + stackLayers für Pseudo-Random-Tilts
Routing-Wechsel: Klick auf Deck-Stack → 220ms Stufe-1-Fade →
goto('/study/<id>') direkt in den Lernmodus. Detail-View
(/decks/<id>) bleibt erreichbar über "Karten verwalten →"-Link
in der Study-Sidebar.
Lernmodus visuell als Karte:
- Globale Header (Logo + Nav + Sprach-Switcher) im Lernmodus
ausgeblendet (routes/+layout.svelte detektiert /study/<deckId>
per Regex), volle Konzentration auf die Karte
- Lern-Karte ist CardSurface size="hero" mit aspect-ratio 5:7
(Portrait, gleiches Verhältnis wie Deck-Stacks und Fan-Karten)
- Color-Stripe links zeigt die Deck-Farbe — visuelle Bindung an
Herkunft
- Sidebar oben links absolute-positioniert: ← Decks, Deck-Name,
Fortschritt, Karten verwalten → — kompakter UI-Block, der die
Karten-Zentrierung NICHT beeinflusst (Karte bleibt geometrisch
Bildschirm-Mitte)
- Reveal-Button + 4er-Grade-Grid (Wieder/Schwer/Gut/Leicht) als
Aktions-Leiste UNTER der Karte — keine weiteren Karten, ein
Review = eine Karte
Mobile (≤720px): Sidebar wird zur horizontalen Zeile oben, Karte
rückt darunter durch padding-top: 6.5rem.
Reduced-motion durchgängig respektiert (keine Tilts, keine Hover-
Lifts, keine Fan-Spread-Animation, kein Card-Transition).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
187 lines
4.1 KiB
Svelte
187 lines
4.1 KiB
Svelte
<script lang="ts">
|
|
import type { Snippet } from 'svelte';
|
|
|
|
/**
|
|
* Zentrale Karten-Optik der Cards-App.
|
|
*
|
|
* Eine Komponente — drei Erscheinungen, alle gleichen "Card-Feel":
|
|
* • size="md" → Deck-Stapel-Cover oder Hand-Karte (5:7-Format, kompakt)
|
|
* • size="lg" → Aufgefächerte Detail-Karte
|
|
* • size="hero" → Lern-Karte (groß, fokussiert, breiter)
|
|
*
|
|
* Alle drei tragen den gleichen Border-Radius, Border-Style, Shadow-
|
|
* Stil und Surface-Farbe. So fühlt sich jede Karten-Erscheinung wie
|
|
* eine Karte an — Deckblatt, Fan-Karte, Study-Karte sind visuell ein
|
|
* Familien-Set.
|
|
*
|
|
* 12-Token-Disziplin: nur `--color-surface`, `--color-border`,
|
|
* `--color-foreground`, `--color-primary*`. Kein Hex.
|
|
*/
|
|
|
|
type Size = 'md' | 'lg' | 'hero';
|
|
type As = 'div' | 'article' | 'a' | 'button';
|
|
|
|
interface Props {
|
|
size?: Size;
|
|
as?: As;
|
|
href?: string;
|
|
onclick?: (e: MouseEvent) => void;
|
|
colorAccent?: string | null;
|
|
ariaLabel?: string;
|
|
title?: string;
|
|
raised?: boolean;
|
|
flat?: boolean;
|
|
class?: string;
|
|
children?: Snippet;
|
|
}
|
|
|
|
let {
|
|
size = 'md',
|
|
as = 'div',
|
|
href,
|
|
onclick,
|
|
colorAccent = null,
|
|
ariaLabel,
|
|
title,
|
|
raised = false,
|
|
flat = false,
|
|
class: className = '',
|
|
children,
|
|
}: Props = $props();
|
|
</script>
|
|
|
|
{#if as === 'a' || href}
|
|
<a
|
|
class="surface size-{size} {className}"
|
|
class:raised
|
|
class:flat
|
|
{href}
|
|
aria-label={ariaLabel}
|
|
{title}
|
|
{onclick}
|
|
>
|
|
{#if colorAccent}
|
|
<span class="accent" style="background:{colorAccent}" aria-hidden="true"></span>
|
|
{/if}
|
|
{#if children}{@render children()}{/if}
|
|
</a>
|
|
{:else if as === 'button'}
|
|
<button
|
|
type="button"
|
|
class="surface size-{size} {className}"
|
|
class:raised
|
|
class:flat
|
|
aria-label={ariaLabel}
|
|
{title}
|
|
{onclick}
|
|
>
|
|
{#if colorAccent}
|
|
<span class="accent" style="background:{colorAccent}" aria-hidden="true"></span>
|
|
{/if}
|
|
{#if children}{@render children()}{/if}
|
|
</button>
|
|
{:else}
|
|
<svelte:element
|
|
this={as}
|
|
class="surface size-{size} {className}"
|
|
class:raised
|
|
class:flat
|
|
aria-label={ariaLabel}
|
|
{title}
|
|
>
|
|
{#if colorAccent}
|
|
<span class="accent" style="background:{colorAccent}" aria-hidden="true"></span>
|
|
{/if}
|
|
{#if children}{@render children()}{/if}
|
|
</svelte:element>
|
|
{/if}
|
|
|
|
<style>
|
|
.surface {
|
|
position: relative;
|
|
display: block;
|
|
background: hsl(var(--color-surface));
|
|
border: 1px solid hsl(var(--color-border));
|
|
border-radius: 0.875rem;
|
|
color: hsl(var(--color-foreground));
|
|
font-family: inherit;
|
|
text-align: left;
|
|
text-decoration: none;
|
|
overflow: hidden;
|
|
box-shadow:
|
|
0 4px 12px hsl(var(--color-foreground) / 0.08),
|
|
0 1px 3px hsl(var(--color-foreground) / 0.05);
|
|
transition: box-shadow 0.2s ease, transform 0.2s ease, border-color 0.15s ease;
|
|
}
|
|
|
|
/* button-/a-Variante als Karte ohne Default-Padding/-Margin */
|
|
button.surface,
|
|
a.surface {
|
|
padding: 0;
|
|
margin: 0;
|
|
font: inherit;
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* Sizes — definieren nur Maße, keine internen Inhalte */
|
|
.size-md {
|
|
width: 100%;
|
|
max-width: 18rem;
|
|
aspect-ratio: 5 / 7;
|
|
}
|
|
|
|
.size-lg {
|
|
width: 12rem;
|
|
height: 16.8rem;
|
|
}
|
|
|
|
.size-hero {
|
|
width: 100%;
|
|
max-width: 24rem;
|
|
aspect-ratio: 5 / 7;
|
|
}
|
|
|
|
/* Raised → echte Top-Karte mit stärkerem Schatten (Hand-of-Cards-Hover, Study-Hero) */
|
|
.surface.raised {
|
|
box-shadow:
|
|
0 14px 28px hsl(var(--color-foreground) / 0.16),
|
|
0 4px 10px hsl(var(--color-foreground) / 0.08);
|
|
}
|
|
|
|
/* Flat → reine Background-Layer (versteckte Hintergrund-Karten im Stapel) */
|
|
.surface.flat {
|
|
box-shadow: 0 1px 3px hsl(var(--color-foreground) / 0.06);
|
|
}
|
|
|
|
/* Color-Akzent links als Identitäts-Streifen */
|
|
.accent {
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 0.375rem;
|
|
}
|
|
|
|
.surface:focus-visible {
|
|
outline: 2px solid hsl(var(--color-primary));
|
|
outline-offset: 4px;
|
|
}
|
|
|
|
button.surface:hover,
|
|
a.surface:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow:
|
|
0 12px 28px hsl(var(--color-foreground) / 0.16),
|
|
0 4px 10px hsl(var(--color-foreground) / 0.08);
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.surface {
|
|
transition: none;
|
|
}
|
|
button.surface:hover,
|
|
a.surface:hover {
|
|
transform: none;
|
|
}
|
|
}
|
|
</style>
|