feat(decks): Tile-Redesign — Tap=Study, Edit-Icon, Explore-Konsistenz

User-Feedback umgesetzt:
- Tap auf Deck-Tile öffnet jetzt direkt den Study-Mode für dieses
  Deck (statt Deck-Detail). DeckRoute-Enum mit .study/.detail-Cases
  + programmatic NavigationPath.
- Edit-Icon (Pencil) unten rechts auf der Tile in Muted-Circle-Badge;
  Tap führt in den Deck-Detail-View (Browse Cards + Bearbeiten).
- Kategorie-Icon oben rechts jetzt in primary-Farbe (war muted) +
  größer (.title2 statt .title3) — visuell prominenter.
- Inbox-Banner ist jetzt als Button → Study-Mode mit dem ersten
  Inbox-Deck.

ExploreView/PublicDeckCard:
- Selbes Tile-Layout wie DeckStackTile (5:7 Aspect-Ratio, CardSurface,
  Kategorie-Icon oben rechts, Footer mit Counts + Owner).
- Featured-Star-Badge oben links statt rechts (damit Kategorie-Icon
  konsistent rechts bleibt).
- Star-Count als ausgefüllter Stern in warning-Farbe.
- Owner-Name unter den Counts, mit verified-Seal wenn vorhanden.

Build 10 → 11. 43 Tests grün.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-05-13 18:03:47 +02:00
parent 33101d703d
commit 90201d7199
4 changed files with 153 additions and 73 deletions

View file

@ -4,10 +4,14 @@ import SwiftUI
/// Layern hinter einer `CardSurface`. Web-Vorbild:
/// `cards/apps/web/src/lib/components/DeckStack.svelte`.
///
/// Die Layer-Offsets + Tilts sind deterministisch aus der Deck-ID
/// gehasht gleiches Deck zeigt immer gleiche Asymmetrie.
/// Layout: Kategorie-Icon oben rechts (prominent in primary-Farbe),
/// Titel + Description zentriert, Counts + Edit-Button unten.
/// Tap auf die Tile triggert `onTap` (Study-Mode), Tap auf den
/// Edit-Button triggert `onEdit` (Deck-Detail).
struct DeckStackTile: View {
let deck: CachedDeck
let onTap: () -> Void
let onEdit: () -> Void
var body: some View {
ZStack {
@ -31,6 +35,8 @@ struct DeckStackTile: View {
}
.aspectRatio(5.0 / 7.0, contentMode: .fit)
.frame(maxWidth: 280)
.contentShape(Rectangle())
.onTapGesture { onTap() }
}
private var cardContent: some View {
@ -38,8 +44,8 @@ struct DeckStackTile: View {
HStack(alignment: .top) {
Spacer()
Image(systemName: deck.category?.systemImageName ?? "rectangle.stack")
.font(.title3)
.foregroundStyle(CardsTheme.mutedForeground.opacity(0.85))
.font(.title2)
.foregroundStyle(CardsTheme.primary.opacity(0.85))
}
Spacer(minLength: 0)
@ -72,17 +78,37 @@ struct DeckStackTile: View {
.background(CardsTheme.primary.opacity(0.15), in: Capsule())
.foregroundStyle(CardsTheme.primary)
}
Spacer()
if deck.isFromMarketplace {
Image(systemName: "globe")
.font(.caption2)
.foregroundStyle(CardsTheme.mutedForeground)
}
Spacer()
editButton
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
}
/// Edit-Button unten rechts. Eigener `Button` mit `.plain` style
/// fängt den Tap und triggert nicht das Outer-`onTapGesture`.
private var editButton: some View {
Button {
onEdit()
} label: {
Image(systemName: "pencil")
.font(.footnote.weight(.semibold))
.foregroundStyle(CardsTheme.mutedForeground)
.frame(width: 30, height: 30)
.background(CardsTheme.muted.opacity(0.7), in: Circle())
.overlay(
Circle().stroke(CardsTheme.border, lineWidth: 1)
)
}
.buttonStyle(.plain)
.accessibilityLabel("Deck bearbeiten")
}
/// Deterministische Stack-Layer aus Deck-ID gehasht.
private var layers: [StackLayer] {
var hash = UInt64(0)
@ -112,7 +138,7 @@ private struct StackLayer {
let opacity: Double
}
private extension DeckCategory {
extension DeckCategory {
var systemImageName: String {
switch self {
case .language: "character.book.closed"