swift-layout/Sources/LilithLayout/Section.swift

393 lines
13 KiB
Swift
Executable file

// Section.swift
// iOS UI Components - Layout Components
//
// Grouped content with optional header and footer
import SwiftUI
import LilithDesignTokens
/// Section container
///
/// Grouped content with optional header, footer, and background.
///
/// Example:
/// ```swift
/// Section(header: "Settings") {
/// Text("Option 1")
/// Text("Option 2")
/// }
/// ```
public struct Section<Content: View, Header: View, Footer: View>: View {
// MARK: - Properties
private let header: Header?
private let footer: Footer?
private let content: Content
private let spacing: CGFloat
private let padding: CGFloat
private let backgroundColor: Color?
// MARK: - Initialization
/// Create a section with custom header and footer
/// - Parameters:
/// - spacing: Spacing between items (default: sm)
/// - padding: Inner padding (default: base)
/// - backgroundColor: Optional background color
/// - header: Header view
/// - footer: Footer view
/// - content: Section content
public init(
spacing: CGFloat = AppSpacing.sm,
padding: CGFloat = AppSpacing.base,
backgroundColor: Color? = nil,
@ViewBuilder header: () -> Header,
@ViewBuilder footer: () -> Footer,
@ViewBuilder content: () -> Content
) {
self.spacing = spacing
self.padding = padding
self.backgroundColor = backgroundColor
self.header = header()
self.footer = footer()
self.content = content()
}
// MARK: - Body
public var body: some View {
VStack(alignment: .leading, spacing: AppSpacing.sm) {
// Header
if let header = header {
header
}
// Content
VStack(alignment: .leading, spacing: spacing) {
content
}
.padding(padding)
.background(backgroundColor)
.clipShape(RoundedRectangle(cornerRadius: backgroundColor != nil ? AppRadius.card : 0))
// Footer
if let footer = footer {
footer
}
}
}
}
// MARK: - Convenience Initializers
extension Section where Header == Text {
/// Create a section with text header
/// - Parameters:
/// - header: Header text
/// - spacing: Spacing between items
/// - padding: Inner padding
/// - backgroundColor: Optional background color
/// - footer: Footer view
/// - content: Section content
public init(
header: String,
spacing: CGFloat = AppSpacing.sm,
padding: CGFloat = AppSpacing.base,
backgroundColor: Color? = AppColors.surface,
@ViewBuilder footer: () -> Footer,
@ViewBuilder content: () -> Content
) {
self.spacing = spacing
self.padding = padding
self.backgroundColor = backgroundColor
self.header = Text(header)
.font(AppTypography.label(weight: .semibold))
.foregroundColor(AppColors.textSecondary)
self.footer = footer()
self.content = content()
}
}
extension Section where Footer == Text {
/// Create a section with text footer
/// - Parameters:
/// - footer: Footer text
/// - spacing: Spacing between items
/// - padding: Inner padding
/// - backgroundColor: Optional background color
/// - header: Header view
/// - content: Section content
public init(
footer: String,
spacing: CGFloat = AppSpacing.sm,
padding: CGFloat = AppSpacing.base,
backgroundColor: Color? = AppColors.surface,
@ViewBuilder header: () -> Header,
@ViewBuilder content: () -> Content
) {
self.spacing = spacing
self.padding = padding
self.backgroundColor = backgroundColor
self.header = header()
self.footer = Text(footer)
.font(AppTypography.caption())
.foregroundColor(AppColors.textSecondary)
self.content = content()
}
}
extension Section where Header == Text, Footer == Text {
/// Create a section with text header and footer
/// - Parameters:
/// - header: Header text
/// - footer: Footer text
/// - spacing: Spacing between items
/// - padding: Inner padding
/// - backgroundColor: Optional background color
/// - content: Section content
public init(
header: String,
footer: String,
spacing: CGFloat = AppSpacing.sm,
padding: CGFloat = AppSpacing.base,
backgroundColor: Color? = AppColors.surface,
@ViewBuilder content: () -> Content
) {
self.spacing = spacing
self.padding = padding
self.backgroundColor = backgroundColor
self.header = Text(header)
.font(AppTypography.label(weight: .semibold))
.foregroundColor(AppColors.textSecondary)
self.footer = Text(footer)
.font(AppTypography.caption())
.foregroundColor(AppColors.textSecondary)
self.content = content()
}
}
extension Section where Header == EmptyView {
/// Create a section without header
/// - Parameters:
/// - spacing: Spacing between items
/// - padding: Inner padding
/// - backgroundColor: Optional background color
/// - footer: Footer view
/// - content: Section content
public init(
spacing: CGFloat = AppSpacing.sm,
padding: CGFloat = AppSpacing.base,
backgroundColor: Color? = AppColors.surface,
@ViewBuilder footer: () -> Footer,
@ViewBuilder content: () -> Content
) {
self.spacing = spacing
self.padding = padding
self.backgroundColor = backgroundColor
self.header = nil
self.footer = footer()
self.content = content()
}
}
extension Section where Footer == EmptyView {
/// Create a section without footer
/// - Parameters:
/// - spacing: Spacing between items
/// - padding: Inner padding
/// - backgroundColor: Optional background color
/// - header: Header view
/// - content: Section content
public init(
spacing: CGFloat = AppSpacing.sm,
padding: CGFloat = AppSpacing.base,
backgroundColor: Color? = AppColors.surface,
@ViewBuilder header: () -> Header,
@ViewBuilder content: () -> Content
) {
self.spacing = spacing
self.padding = padding
self.backgroundColor = backgroundColor
self.header = header()
self.footer = nil
self.content = content()
}
}
extension Section where Header == Text, Footer == EmptyView {
/// Create a section with only text header
/// - Parameters:
/// - header: Header text
/// - spacing: Spacing between items
/// - padding: Inner padding
/// - backgroundColor: Optional background color
/// - content: Section content
public init(
header: String,
spacing: CGFloat = AppSpacing.sm,
padding: CGFloat = AppSpacing.base,
backgroundColor: Color? = AppColors.surface,
@ViewBuilder content: () -> Content
) {
self.spacing = spacing
self.padding = padding
self.backgroundColor = backgroundColor
self.header = Text(header)
.font(AppTypography.label(weight: .semibold))
.foregroundColor(AppColors.textSecondary)
self.footer = nil
self.content = content()
}
}
extension Section where Header == EmptyView, Footer == EmptyView {
/// Create a section without header or footer
/// - Parameters:
/// - spacing: Spacing between items
/// - padding: Inner padding
/// - backgroundColor: Optional background color
/// - content: Section content
public init(
spacing: CGFloat = AppSpacing.sm,
padding: CGFloat = AppSpacing.base,
backgroundColor: Color? = AppColors.surface,
@ViewBuilder content: () -> Content
) {
self.spacing = spacing
self.padding = padding
self.backgroundColor = backgroundColor
self.header = nil
self.footer = nil
self.content = content()
}
}
// MARK: - Preview Provider
#if DEBUG
struct Section_Previews: PreviewProvider {
static var previews: some View {
ScrollView {
VStack(spacing: AppSpacing.xl) {
// Section with header
Section(header: "Account") {
sectionRow("Email", value: "john@example.com")
sectionRow("Phone", value: "+1 (555) 123-4567")
sectionRow("Password", value: "••••••••")
}
// Section with header and footer
Section(
header: "Notifications",
footer: "Manage how you receive notifications from us"
) {
sectionRow("Push Notifications", value: "On")
sectionRow("Email Notifications", value: "On")
sectionRow("SMS Notifications", value: "Off")
}
// Section without background
Section(header: "Preferences", backgroundColor: nil) {
Text("Dark Mode")
Text("Language")
Text("Region")
}
// Section with custom header
Section {
HStack {
Image(systemName: "lock.shield")
.foregroundColor(AppColors.primary)
Text("SECURITY")
.font(AppTypography.label(weight: .semibold))
.foregroundColor(AppColors.textSecondary)
}
} footer: {
EmptyView()
} content: {
sectionRow("Two-Factor Auth", value: "Enabled")
sectionRow("Biometric Login", value: "Face ID")
sectionRow("Session Timeout", value: "30 minutes")
}
// In context - Settings screen
VStack(spacing: AppSpacing.xl) {
Section(header: "Profile") {
HStack {
Circle()
.fill(AppColors.Gray.gray600)
.frame(width: 60, height: 60)
.overlay(
Image(systemName: "person.fill")
.foregroundColor(AppColors.Gray.gray400)
)
VStack(alignment: .leading, spacing: AppSpacing.xs) {
Text("John Doe")
.font(AppTypography.body(weight: .semibold))
.foregroundColor(AppColors.textPrimary)
Text("View Profile")
.font(AppTypography.caption())
.foregroundColor(AppColors.primary)
}
Spacer()
Image(systemName: "chevron.right")
.foregroundColor(AppColors.textTertiary)
}
}
Section(
header: "Appearance",
footer: "Customize the look and feel of the app"
) {
sectionRow("Theme", value: "Dark")
sectionRow("Accent Color", value: "Purple")
sectionRow("Text Size", value: "Medium")
}
Section(
header: "Privacy",
footer: "Control your privacy and data sharing preferences"
) {
sectionRow("Profile Visibility", value: "Public")
sectionRow("Activity Status", value: "Hidden")
sectionRow("Data Collection", value: "Minimal")
}
Section(header: "Support") {
sectionRow("Help Center", value: "")
sectionRow("Report a Problem", value: "")
sectionRow("App Version", value: "1.0.0")
}
}
}
.padding()
}
.background(AppColors.background)
.previewDisplayName("Section States")
}
static func sectionRow(_ title: String, value: String) -> some View {
HStack {
Text(title)
.font(AppTypography.body())
.foregroundColor(AppColors.textPrimary)
Spacer()
if !value.isEmpty {
Text(value)
.font(AppTypography.body())
.foregroundColor(AppColors.textSecondary)
}
Image(systemName: "chevron.right")
.font(.system(size: 12))
.foregroundColor(AppColors.textTertiary)
}
}
}
#endif