Bedrock/Sources/Bedrock/Views/Settings/SelectableRow.swift
Matt Bruce 3e5e6b7447 feature updates
Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
2026-02-07 10:58:42 -06:00

150 lines
4.2 KiB
Swift

//
// SelectableRow.swift
// Bedrock
//
// A card-like selectable row with title, subtitle, optional badge, and selection indicator.
//
import SwiftUI
/// A card-like selectable row with title, subtitle, optional badge, and selection indicator.
///
/// Use this for settings pickers, option lists, or any selectable item.
///
/// ```swift
/// SelectableRow(
/// title: "Premium",
/// subtitle: "Unlock all features",
/// isSelected: plan == .premium
/// ) {
/// plan = .premium
/// }
/// ```
public struct SelectableRow<Badge: View>: View {
/// The main title text.
public let title: String
/// The subtitle/description text.
public let subtitle: String
/// Whether this row is currently selected.
public let isSelected: Bool
/// Optional badge view.
public let badge: Badge?
/// The accent color for selection highlighting.
public let accentColor: Color
/// Action when tapped.
public let action: () -> Void
/// Creates a selectable row.
/// - Parameters:
/// - title: The main title.
/// - subtitle: The subtitle description.
/// - isSelected: Whether this row is selected.
/// - accentColor: Color for selection (default: primary accent).
/// - badge: Optional badge view.
/// - action: Action when tapped.
public init(
title: String,
subtitle: String,
isSelected: Bool,
accentColor: Color = .Accent.primary,
@ViewBuilder badge: () -> Badge? = { nil as EmptyView? },
action: @escaping () -> Void
) {
self.title = title
self.subtitle = subtitle
self.isSelected = isSelected
self.accentColor = accentColor
self.badge = badge()
self.action = action
}
public var body: some View {
Button(action: action) {
HStack {
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
Text(title).styled(.calloutEmphasis, emphasis: .inverse)
Text(subtitle).styled(.subheading, emphasis: .tertiary)
}
Spacer()
if let badge = badge {
badge
}
SelectionIndicator(isSelected: isSelected, accentColor: accentColor)
}
.padding()
.background(
RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
.fill(isSelected ? accentColor.opacity(Design.Opacity.subtle) : Color.clear)
)
.overlay(
RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
.strokeBorder(
isSelected ? accentColor.opacity(Design.Opacity.medium) : Color.white.opacity(Design.Opacity.subtle),
lineWidth: Design.LineWidth.thin
)
)
}
.buttonStyle(.plain)
.sensoryFeedback(.selection, trigger: isSelected)
}
}
// MARK: - Convenience Initializer
extension SelectableRow where Badge == EmptyView {
/// Creates a selectable row without a badge.
public init(
title: String,
subtitle: String,
isSelected: Bool,
accentColor: Color = .Accent.primary,
action: @escaping () -> Void
) {
self.title = title
self.subtitle = subtitle
self.isSelected = isSelected
self.accentColor = accentColor
self.badge = nil
self.action = action
}
}
// MARK: - Preview
#Preview {
VStack(spacing: Design.Spacing.small) {
SelectableRow(
title: "Light",
subtitle: "Always use light mode",
isSelected: true,
action: {}
)
SelectableRow(
title: "Dark",
subtitle: "Always use dark mode",
isSelected: false,
action: {}
)
SelectableRow(
title: "Premium",
subtitle: "Unlock all features",
isSelected: false,
badge: { BadgePill(text: "$9.99", isSelected: false) },
action: {}
)
}
.padding()
.background(Color.Surface.overlay)
}