357 lines
13 KiB
Swift
Executable file
357 lines
13 KiB
Swift
Executable file
// Card.swift
|
|
// iOS UI Components - Layout Components
|
|
//
|
|
// Container with shadow, padding, and rounded corners
|
|
|
|
import SwiftUI
|
|
import LilithDesignTokens
|
|
|
|
/// Card container
|
|
///
|
|
/// Elevated container with shadow and customizable padding.
|
|
///
|
|
/// Example:
|
|
/// ```swift
|
|
/// Card {
|
|
/// VStack {
|
|
/// Text("Title")
|
|
/// Text("Content")
|
|
/// }
|
|
/// }
|
|
/// ```
|
|
public struct Card<Content: View>: View {
|
|
// MARK: - Properties
|
|
|
|
private let content: Content
|
|
private let padding: CGFloat
|
|
private let isElevated: Bool
|
|
private let backgroundColor: Color
|
|
private let onTap: (() -> Void)?
|
|
|
|
// MARK: - State
|
|
|
|
@State private var isPressed = false
|
|
|
|
// MARK: - Initialization
|
|
|
|
/// Create a card
|
|
/// - Parameters:
|
|
/// - padding: Inner padding (default: base)
|
|
/// - shadow: Shadow style (default: card)
|
|
/// - backgroundColor: Background color (default: surface)
|
|
/// - onTap: Optional tap action (makes card tappable)
|
|
/// - content: Card content
|
|
public init(
|
|
padding: CGFloat = AppSpacing.base,
|
|
elevated: Bool = false,
|
|
backgroundColor: Color = AppColors.surface,
|
|
onTap: (() -> Void)? = nil,
|
|
@ViewBuilder content: () -> Content
|
|
) {
|
|
self.padding = padding
|
|
self.isElevated = elevated
|
|
self.backgroundColor = backgroundColor
|
|
self.onTap = onTap
|
|
self.content = content()
|
|
}
|
|
|
|
// MARK: - Body
|
|
|
|
public var body: some View {
|
|
Group {
|
|
if let onTap = onTap {
|
|
Button(action: {
|
|
// Haptic feedback (iOS only)
|
|
#if canImport(UIKit)
|
|
let generator = UIImpactFeedbackGenerator(style: .light)
|
|
generator.impactOccurred()
|
|
#endif
|
|
|
|
onTap()
|
|
}) {
|
|
cardContent
|
|
}
|
|
.buttonStyle(PlainButtonStyle())
|
|
.onLongPressGesture(minimumDuration: .infinity, maximumDistance: .infinity, pressing: { pressing in
|
|
isPressed = pressing
|
|
}, perform: {})
|
|
} else {
|
|
cardContent
|
|
}
|
|
}
|
|
}
|
|
|
|
private var cardContent: some View {
|
|
content
|
|
.padding(padding)
|
|
.background(backgroundColor)
|
|
.clipShape(RoundedRectangle(cornerRadius: AppRadius.card))
|
|
.shadow(
|
|
color: Color.black.opacity(isElevated ? 0.15 : 0.1),
|
|
radius: isElevated ? 15 : 6,
|
|
x: 0,
|
|
y: isElevated ? 10 : 2
|
|
)
|
|
.scaleEffect(isPressed ? 0.98 : 1.0)
|
|
.animation(AppAnimations.buttonPress, value: isPressed)
|
|
}
|
|
}
|
|
|
|
// MARK: - Card Variants
|
|
|
|
extension Card {
|
|
/// Elevated card with larger shadow
|
|
public static func elevated(
|
|
padding: CGFloat = AppSpacing.base,
|
|
backgroundColor: Color = AppColors.surface,
|
|
onTap: (() -> Void)? = nil,
|
|
@ViewBuilder content: () -> Content
|
|
) -> Card<Content> {
|
|
Card(
|
|
padding: padding,
|
|
elevated: true,
|
|
backgroundColor: backgroundColor,
|
|
onTap: onTap,
|
|
content: content
|
|
)
|
|
}
|
|
|
|
/// Flat card without shadow
|
|
public static func flat(
|
|
padding: CGFloat = AppSpacing.base,
|
|
backgroundColor: Color = AppColors.surface,
|
|
onTap: (() -> Void)? = nil,
|
|
@ViewBuilder content: () -> Content
|
|
) -> Card<Content> {
|
|
Card(
|
|
padding: padding,
|
|
elevated: false,
|
|
backgroundColor: backgroundColor,
|
|
onTap: onTap,
|
|
content: content
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - Preview Provider
|
|
|
|
#if DEBUG
|
|
struct Card_Previews: PreviewProvider {
|
|
static var previews: some View {
|
|
ScrollView {
|
|
VStack(spacing: AppSpacing.xl) {
|
|
// Basic card
|
|
Card {
|
|
VStack(alignment: .leading, spacing: AppSpacing.sm) {
|
|
Text("Basic Card")
|
|
.font(AppTypography.h3())
|
|
.foregroundColor(AppColors.textPrimary)
|
|
|
|
Text("This is a basic card with default shadow and padding.")
|
|
.font(AppTypography.body())
|
|
.foregroundColor(AppColors.textSecondary)
|
|
}
|
|
}
|
|
|
|
// Elevated card
|
|
Card.elevated {
|
|
VStack(alignment: .leading, spacing: AppSpacing.sm) {
|
|
Text("Elevated Card")
|
|
.font(AppTypography.h3())
|
|
.foregroundColor(AppColors.textPrimary)
|
|
|
|
Text("This card has a larger shadow for more emphasis.")
|
|
.font(AppTypography.body())
|
|
.foregroundColor(AppColors.textSecondary)
|
|
}
|
|
}
|
|
|
|
// Flat card
|
|
Card.flat {
|
|
VStack(alignment: .leading, spacing: AppSpacing.sm) {
|
|
Text("Flat Card")
|
|
.font(AppTypography.h3())
|
|
.foregroundColor(AppColors.textPrimary)
|
|
|
|
Text("This card has no shadow.")
|
|
.font(AppTypography.body())
|
|
.foregroundColor(AppColors.textSecondary)
|
|
}
|
|
}
|
|
|
|
// Tappable card
|
|
Card(onTap: {
|
|
print("Card tapped")
|
|
}) {
|
|
HStack {
|
|
Image(systemName: "star.fill")
|
|
.foregroundColor(AppColors.Semantic.warning)
|
|
|
|
VStack(alignment: .leading, spacing: AppSpacing.xs) {
|
|
Text("Tappable Card")
|
|
.font(AppTypography.body(weight: .semibold))
|
|
.foregroundColor(AppColors.textPrimary)
|
|
|
|
Text("Tap me to interact")
|
|
.font(AppTypography.caption())
|
|
.foregroundColor(AppColors.textSecondary)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
Image(systemName: "chevron.right")
|
|
.foregroundColor(AppColors.textTertiary)
|
|
}
|
|
}
|
|
|
|
// Custom padding
|
|
Card(padding: AppSpacing.lg) {
|
|
Text("Card with large padding")
|
|
.font(AppTypography.body())
|
|
.foregroundColor(AppColors.textPrimary)
|
|
}
|
|
|
|
Card(padding: AppSpacing.xs) {
|
|
Text("Card with small padding")
|
|
.font(AppTypography.caption())
|
|
.foregroundColor(AppColors.textPrimary)
|
|
}
|
|
|
|
Divider()
|
|
|
|
// In context - User profile card
|
|
Card {
|
|
HStack(spacing: AppSpacing.md) {
|
|
Circle()
|
|
.fill(AppColors.Gray.gray600)
|
|
.frame(width: 60, height: 60)
|
|
.overlay(
|
|
Image(systemName: "person.fill")
|
|
.font(.system(size: 24))
|
|
.foregroundColor(AppColors.Gray.gray400)
|
|
)
|
|
|
|
VStack(alignment: .leading, spacing: AppSpacing.xs) {
|
|
Text("John Doe")
|
|
.font(AppTypography.body(weight: .semibold))
|
|
.foregroundColor(AppColors.textPrimary)
|
|
|
|
Text("john.doe@example.com")
|
|
.font(AppTypography.caption())
|
|
.foregroundColor(AppColors.textSecondary)
|
|
|
|
Text("Premium Member")
|
|
.font(AppTypography.caption())
|
|
.foregroundColor(AppColors.primary)
|
|
}
|
|
|
|
Spacer()
|
|
}
|
|
}
|
|
|
|
// In context - Stat cards
|
|
HStack(spacing: AppSpacing.md) {
|
|
Card(padding: AppSpacing.md) {
|
|
VStack(spacing: AppSpacing.xs) {
|
|
Text("1,234")
|
|
.font(AppTypography.h2())
|
|
.foregroundColor(AppColors.textPrimary)
|
|
|
|
Text("Followers")
|
|
.font(AppTypography.caption())
|
|
.foregroundColor(AppColors.textSecondary)
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
|
|
Card(padding: AppSpacing.md) {
|
|
VStack(spacing: AppSpacing.xs) {
|
|
Text("567")
|
|
.font(AppTypography.h2())
|
|
.foregroundColor(AppColors.textPrimary)
|
|
|
|
Text("Following")
|
|
.font(AppTypography.caption())
|
|
.foregroundColor(AppColors.textSecondary)
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
}
|
|
|
|
// In context - Feature card
|
|
Card(onTap: {
|
|
print("Feature tapped")
|
|
}) {
|
|
VStack(alignment: .leading, spacing: AppSpacing.md) {
|
|
HStack {
|
|
Image(systemName: "sparkles")
|
|
.font(.system(size: 24))
|
|
.foregroundColor(AppColors.primary)
|
|
.frame(width: 40, height: 40)
|
|
.background(AppColors.primary.opacity(0.1))
|
|
.clipShape(RoundedRectangle(cornerRadius: AppRadius.sm))
|
|
|
|
Spacer()
|
|
|
|
Text("NEW")
|
|
.font(AppTypography.caption(weight: .bold))
|
|
.foregroundColor(AppColors.primary)
|
|
.padding(.horizontal, AppSpacing.sm)
|
|
.padding(.vertical, AppSpacing.xs)
|
|
.background(AppColors.primary.opacity(0.1))
|
|
.clipShape(RoundedRectangle(cornerRadius: AppRadius.full))
|
|
}
|
|
|
|
Text("Premium Features")
|
|
.font(AppTypography.h3())
|
|
.foregroundColor(AppColors.textPrimary)
|
|
|
|
Text("Unlock advanced features and remove ads with Premium.")
|
|
.font(AppTypography.body())
|
|
.foregroundColor(AppColors.textSecondary)
|
|
|
|
HStack {
|
|
Text("Learn More")
|
|
.font(AppTypography.body(weight: .semibold))
|
|
.foregroundColor(AppColors.primary)
|
|
|
|
Image(systemName: "arrow.right")
|
|
.font(.system(size: 12))
|
|
.foregroundColor(AppColors.primary)
|
|
}
|
|
}
|
|
}
|
|
|
|
// In context - List of cards
|
|
VStack(spacing: AppSpacing.md) {
|
|
ForEach(0..<3) { index in
|
|
Card(onTap: {
|
|
print("Item \(index) tapped")
|
|
}) {
|
|
HStack {
|
|
VStack(alignment: .leading, spacing: AppSpacing.xs) {
|
|
Text("Item \(index + 1)")
|
|
.font(AppTypography.body(weight: .semibold))
|
|
.foregroundColor(AppColors.textPrimary)
|
|
|
|
Text("Description for item \(index + 1)")
|
|
.font(AppTypography.caption())
|
|
.foregroundColor(AppColors.textSecondary)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
Image(systemName: "chevron.right")
|
|
.foregroundColor(AppColors.textTertiary)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.padding()
|
|
}
|
|
.background(AppColors.background)
|
|
.previewDisplayName("Card States")
|
|
}
|
|
}
|
|
#endif
|