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>
This commit is contained in:
parent
0b0872c8c0
commit
aa94601409
6 changed files with 396 additions and 162 deletions
|
|
@ -4,8 +4,9 @@ import SwiftUI
|
|||
import UIKit
|
||||
#endif
|
||||
|
||||
/// Vier Rating-Buttons unten am Bildschirm. Tap → onRate(rating)
|
||||
/// plus Haptic-Feedback.
|
||||
/// 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
|
||||
|
||||
|
|
@ -16,17 +17,24 @@ struct RatingBar: View {
|
|||
triggerHaptic(for: rating)
|
||||
onRate(rating)
|
||||
} label: {
|
||||
VStack(spacing: 2) {
|
||||
HStack(spacing: 6) {
|
||||
Text(rating.label)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
Text(rating.shortcut)
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
.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))
|
||||
.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)
|
||||
}
|
||||
|
|
@ -34,12 +42,14 @@ struct RatingBar: View {
|
|||
.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.12)
|
||||
case .hard: CardsTheme.warning.opacity(0.12)
|
||||
case .good: CardsTheme.primary.opacity(0.12)
|
||||
case .easy: CardsTheme.success.opacity(0.12)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -47,17 +57,37 @@ struct RatingBar: View {
|
|||
switch rating {
|
||||
case .again: CardsTheme.error
|
||||
case .hard: CardsTheme.warning
|
||||
case .good: CardsTheme.primary
|
||||
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 generator = UIImpactFeedbackGenerator(
|
||||
style: rating == .easy ? .heavy : .medium
|
||||
)
|
||||
generator.impactOccurred()
|
||||
let style: UIImpactFeedbackGenerator.FeedbackStyle =
|
||||
rating == .easy ? .heavy : .medium
|
||||
UIImpactFeedbackGenerator(style: style).impactOccurred()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,21 +101,16 @@ struct StudySessionView: View {
|
|||
}
|
||||
|
||||
private func cardSurface(due: DueReview, isFlipped: Bool) -> some View {
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.fill(CardsTheme.surface)
|
||||
.overlay(
|
||||
CardRenderer(
|
||||
card: due.card,
|
||||
subIndex: due.review.subIndex,
|
||||
isFlipped: isFlipped
|
||||
)
|
||||
CardSurface(size: .hero, elevation: .raised) {
|
||||
CardRenderer(
|
||||
card: due.card,
|
||||
subIndex: due.review.subIndex,
|
||||
isFlipped: isFlipped
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.stroke(CardsTheme.border, lineWidth: 1)
|
||||
)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 12)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 12)
|
||||
}
|
||||
|
||||
private func finishedView(session: StudySession) -> some View {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue