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
108
Sources/Core/Theme/CardSurface.swift
Normal file
108
Sources/Core/Theme/CardSurface.swift
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import SwiftUI
|
||||
|
||||
/// Wiederverwendbare Karten-Hülle in drei Größen — entspricht den
|
||||
/// Web-`CardSurface.svelte`-Varianten. Konsistenter Border-Radius (14pt),
|
||||
/// gleicher Border-Stil, gleiche Shadow-Behandlung über alle Größen,
|
||||
/// optional ein linker Color-Accent-Streifen.
|
||||
///
|
||||
/// Spec aus `cards/apps/web/src/lib/components/CardSurface.svelte`:
|
||||
/// - Alle Größen Border-Radius 0.875rem (14pt)
|
||||
/// - Border 1px hsl(--color-border)
|
||||
/// - Background hsl(--color-surface)
|
||||
/// - Aspect-Ratio 5/7 für `.md` und `.hero`, fix für `.lg`
|
||||
struct CardSurface<Content: View>: View {
|
||||
enum Size: Sendable {
|
||||
case md // Deck-Tile in der Liste (max-width 18rem)
|
||||
case lg // Fan-Detail (12rem x 16.8rem)
|
||||
case hero // Study-Lernkarte (max-width 24rem)
|
||||
}
|
||||
|
||||
enum Elevation: Sendable {
|
||||
case flat // Subtle shadow
|
||||
case standard // Default Karten-Shadow
|
||||
case raised // Study-Hero
|
||||
}
|
||||
|
||||
let size: Size
|
||||
let elevation: Elevation
|
||||
let colorAccentHex: String?
|
||||
let content: () -> Content
|
||||
|
||||
init(
|
||||
size: Size = .md,
|
||||
elevation: Elevation = .standard,
|
||||
colorAccentHex: String? = nil,
|
||||
@ViewBuilder content: @escaping () -> Content
|
||||
) {
|
||||
self.size = size
|
||||
self.elevation = elevation
|
||||
self.colorAccentHex = colorAccentHex
|
||||
self.content = content
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .leading) {
|
||||
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
||||
.fill(CardsTheme.surface)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
||||
.stroke(CardsTheme.border, lineWidth: 1)
|
||||
)
|
||||
|
||||
if let colorAccentHex {
|
||||
Color.swatchFromHex(colorAccentHex)
|
||||
.frame(width: 6)
|
||||
.clipShape(
|
||||
UnevenRoundedRectangle(
|
||||
topLeadingRadius: 14,
|
||||
bottomLeadingRadius: 14,
|
||||
bottomTrailingRadius: 0,
|
||||
topTrailingRadius: 0,
|
||||
style: .continuous
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
content()
|
||||
.padding(EdgeInsets(top: 16, leading: 22, bottom: 18, trailing: 16))
|
||||
}
|
||||
.frame(maxWidth: maxWidth)
|
||||
.aspectRatio(aspectRatio, contentMode: .fit)
|
||||
.shadow(color: shadowColor, radius: shadowRadius, x: 0, y: shadowY)
|
||||
}
|
||||
|
||||
private var maxWidth: CGFloat? {
|
||||
switch size {
|
||||
case .md: 288 // 18rem
|
||||
case .lg: 192 // 12rem
|
||||
case .hero: 384 // 24rem
|
||||
}
|
||||
}
|
||||
|
||||
private var aspectRatio: CGFloat? {
|
||||
switch size {
|
||||
case .md, .hero: 5.0 / 7.0
|
||||
case .lg: 12.0 / 16.8
|
||||
}
|
||||
}
|
||||
|
||||
private var shadowColor: Color {
|
||||
CardsTheme.foreground.opacity(elevation == .raised ? 0.18 : 0.08)
|
||||
}
|
||||
|
||||
private var shadowRadius: CGFloat {
|
||||
switch elevation {
|
||||
case .flat: 3
|
||||
case .standard: 8
|
||||
case .raised: 18
|
||||
}
|
||||
}
|
||||
|
||||
private var shadowY: CGFloat {
|
||||
switch elevation {
|
||||
case .flat: 1
|
||||
case .standard: 4
|
||||
case .raised: 12
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue