import SwiftUI /// Gemeinsame Karten-Tile mit Fan-Stack-Hintergrund-Layern. /// Basis für `DeckStackTile` (eigene Decks) und `PublicDeckCard` /// (Marketplace-Decks). Web-Vorbild: /// `cards/apps/web/src/lib/components/DeckStack.svelte` und /// `MarketplaceDeckStack.svelte` — selbe Größe, selbes Stack-Visual, /// nur der Footer variiert. struct DeckCoverTile: View { let title: String let description: String? let category: DeckCategory? let seed: String let colorAccentHex: String? let isFeatured: Bool @ViewBuilder let footer: () -> Footer init( title: String, description: String? = nil, category: DeckCategory? = nil, seed: String, colorAccentHex: String? = nil, isFeatured: Bool = false, @ViewBuilder footer: @escaping () -> Footer ) { self.title = title self.description = description self.category = category self.seed = seed self.colorAccentHex = colorAccentHex self.isFeatured = isFeatured self.footer = footer } var body: some View { ZStack { ForEach(Array(layers.enumerated()), id: \.offset) { _, layer in RoundedRectangle(cornerRadius: 14, style: .continuous) .fill(CardsTheme.surface) .overlay( RoundedRectangle(cornerRadius: 14, style: .continuous) .stroke(CardsTheme.border, lineWidth: 1) ) .opacity(layer.opacity) .rotationEffect(.degrees(layer.tilt)) .offset(x: layer.dx, y: layer.dy) .shadow(color: CardsTheme.foreground.opacity(0.05), radius: 2, y: 1) } CardSurface(size: .md, elevation: .standard, colorAccentHex: colorAccentHex) { cardContent } } .aspectRatio(5.0 / 7.0, contentMode: .fit) .frame(maxWidth: 280) } private var cardContent: some View { VStack(alignment: .leading, spacing: 6) { HStack(alignment: .top) { if isFeatured { Image(systemName: "star.fill") .font(.caption) .foregroundStyle(CardsTheme.warning) } Spacer() Image(systemName: category?.systemImageName ?? "rectangle.stack") .font(.title2) .foregroundStyle(CardsTheme.primary.opacity(0.85)) } Spacer(minLength: 0) VStack(alignment: .leading, spacing: 6) { Text(title) .font(.system(size: 17, weight: .semibold)) .foregroundStyle(CardsTheme.foreground) .lineLimit(3) if let description, !description.isEmpty { Text(description) .font(.caption) .foregroundStyle(CardsTheme.mutedForeground) .lineLimit(2) } } Spacer(minLength: 0) footer() } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) } private var layers: [DeckCoverStackLayer] { var hash = UInt64(0) for byte in seed.utf8 { hash = hash &* 31 &+ UInt64(byte) } return (0 ..< 3).map { index in let seedHash = hash &+ UInt64(index) &* 17 let tiltRaw = Double((seedHash >> 8) & 0xFF) / 255.0 - 0.5 let xRaw = Double((seedHash >> 16) & 0xFF) / 255.0 - 0.5 let yRaw = Double((seedHash >> 24) & 0xFF) / 255.0 - 0.5 let depth = Double(index + 1) return DeckCoverStackLayer( tilt: tiltRaw * 4.0, dx: xRaw * 6.0, dy: depth * 3.0 + yRaw * 2.0, opacity: 0.7 - depth * 0.18 ) } } } private struct DeckCoverStackLayer { let tilt: Double let dx: Double let dy: Double let opacity: Double }