cards-native/Sources/Features/Study/RatingBar.swift
Till JS aa94601409 feat(ui): Cardecky-Web-Design — Fan-Stack-Tiles + CardSurface
UI-Refactor angelehnt an cards/apps/web. Drei Killer-Patterns
übernommen:

1. CardSurface (Sources/Core/Theme/CardSurface.swift)
   - Drei Sizes md/lg/hero mit identischem Border-Radius 14pt,
     1pt Border, layered Shadows je nach Elevation
   - Aspect-Ratio 5:7 für md/hero, 12:16.8 für lg
   - Optional Color-Accent-Stripe links (6pt, deck.color)

2. DeckStackTile (Sources/Features/Decks/DeckStackTile.swift)
   - Spielkarten-Stack-Visual: 3 gestaffelt-rotierte
     Hintergrund-Layer hinter der CardSurface
   - Layer-Offsets + Tilts deterministisch aus Deck-ID gehasht
     (gleiches Deck = gleiche Asymmetrie)
   - Inhalt: Category-Icon oben rechts, Titel + Description
     zentriert, Counts unten als Pill für dueCount

3. RatingBar mit Good-Emphasis (Features/Study/RatingBar.swift)
   - "Good" als full primary background (hero action)
   - again/hard/easy mit subtle border-tint + opacity-08-Background
   - Keyboard-Shortcut im Button-Label als kbd-Style-Pill

DeckListView komplett umgebaut:
- Horizontale ScrollView mit scrollTransition + viewAligned-Snap
- Zwei Sektionen: "Eigene Decks" und "Abonniert"
- Inbox-Banner als highlight (primary opacity 0.08 mit border)
- Pending-Share-Banner mit warning-Tint
- Section-Headers mit Icon + Title + Count

StudySessionView.cardSurface nutzt jetzt CardSurface(.hero, .raised).

Build 6 → 7. Drei native Targets bauen, 35 Tests grün.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 17:28:11 +02:00

93 lines
3.3 KiB
Swift

import SwiftUI
#if canImport(UIKit)
import UIKit
#endif
/// Vier Rating-Buttons mit emphasis auf "Good" (full-width primary).
/// Web-Vorbild: `cards/apps/web/src/routes/study/[deckId]/+page.svelte`
/// `.grade.again/.hard/.good/.easy`-Klassen.
struct RatingBar: View {
let onRate: (Rating) -> Void
var body: some View {
HStack(spacing: 8) {
ForEach(Rating.allCases, id: \.self) { rating in
Button {
triggerHaptic(for: rating)
onRate(rating)
} label: {
HStack(spacing: 6) {
Text(rating.label)
.font(.subheadline.weight(.semibold))
Text(rating.shortcut)
.font(.caption2.weight(.semibold))
.padding(.horizontal, 5)
.padding(.vertical, 1)
.background(kbdBackground(for: rating), in: RoundedRectangle(cornerRadius: 4))
.foregroundStyle(kbdForeground(for: rating))
}
.frame(maxWidth: .infinity)
.padding(.vertical, 14)
.background(background(for: rating), in: RoundedRectangle(cornerRadius: 10, style: .continuous))
.foregroundStyle(foreground(for: rating))
.overlay(
RoundedRectangle(cornerRadius: 10, style: .continuous)
.stroke(borderColor(for: rating), lineWidth: rating == .good ? 0 : 1)
)
}
.buttonStyle(.plain)
}
}
.padding(.horizontal, 16)
}
/// `good` ist die Hero-Action (primary full background) analog
/// zum Web-Default-Klick. Andere bekommen subtle tinted borders.
private func background(for rating: Rating) -> Color {
switch rating {
case .again: CardsTheme.error.opacity(0.06)
case .hard: CardsTheme.warning.opacity(0.06)
case .good: CardsTheme.primary
case .easy: CardsTheme.success.opacity(0.06)
}
}
private func foreground(for rating: Rating) -> Color {
switch rating {
case .again: CardsTheme.error
case .hard: CardsTheme.warning
case .good: CardsTheme.primaryForeground
case .easy: CardsTheme.success
}
}
private func borderColor(for rating: Rating) -> Color {
switch rating {
case .again: CardsTheme.error.opacity(0.4)
case .hard: CardsTheme.warning.opacity(0.4)
case .good: .clear
case .easy: CardsTheme.success.opacity(0.4)
}
}
private func kbdBackground(for rating: Rating) -> Color {
rating == .good
? CardsTheme.primaryForeground.opacity(0.18)
: CardsTheme.muted
}
private func kbdForeground(for rating: Rating) -> Color {
rating == .good
? CardsTheme.primaryForeground.opacity(0.85)
: CardsTheme.mutedForeground
}
private func triggerHaptic(for rating: Rating) {
#if canImport(UIKit)
let style: UIImpactFeedbackGenerator.FeedbackStyle =
rating == .easy ? .heavy : .medium
UIImpactFeedbackGenerator(style: style).impactOccurred()
#endif
}
}