434 lines
14 KiB
Swift
434 lines
14 KiB
Swift
//
|
|
// SettingsComponents.swift
|
|
// CasinoKit
|
|
//
|
|
// Reusable settings UI components for casino games.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
// MARK: - Settings Toggle
|
|
|
|
/// A toggle setting row with title and subtitle.
|
|
public struct SettingsToggle: View {
|
|
/// The main title text.
|
|
public let title: String
|
|
|
|
/// The subtitle/description text.
|
|
public let subtitle: String
|
|
|
|
/// Binding to the toggle state.
|
|
@Binding public var isOn: Bool
|
|
|
|
/// Creates a settings toggle.
|
|
/// - Parameters:
|
|
/// - title: The main title.
|
|
/// - subtitle: The subtitle description.
|
|
/// - isOn: Binding to toggle state.
|
|
public init(title: String, subtitle: String, isOn: Binding<Bool>) {
|
|
self.title = title
|
|
self.subtitle = subtitle
|
|
self._isOn = isOn
|
|
}
|
|
|
|
public var body: some View {
|
|
Toggle(isOn: $isOn) {
|
|
VStack(alignment: .leading, spacing: CasinoDesign.Spacing.xxSmall) {
|
|
Text(title)
|
|
.font(.system(size: CasinoDesign.BaseFontSize.medium, weight: .medium))
|
|
.foregroundStyle(.white)
|
|
|
|
Text(subtitle)
|
|
.font(.system(size: CasinoDesign.BaseFontSize.body))
|
|
.foregroundStyle(.white.opacity(CasinoDesign.Opacity.medium))
|
|
}
|
|
}
|
|
.tint(.yellow)
|
|
.padding(.vertical, CasinoDesign.Spacing.xSmall)
|
|
}
|
|
}
|
|
|
|
// MARK: - Speed Picker
|
|
|
|
/// A segmented picker for animation speed (Fast/Normal/Slow).
|
|
public struct SpeedPicker: View {
|
|
/// Binding to the speed value (0.5 = fast, 1.0 = normal, 2.0 = slow).
|
|
@Binding public var speed: Double
|
|
|
|
private let options: [(String, Double)] = [
|
|
("Fast", 0.5),
|
|
("Normal", 1.0),
|
|
("Slow", 2.0)
|
|
]
|
|
|
|
/// Creates a speed picker.
|
|
/// - Parameter speed: Binding to the speed multiplier.
|
|
public init(speed: Binding<Double>) {
|
|
self._speed = speed
|
|
}
|
|
|
|
public var body: some View {
|
|
VStack(alignment: .leading, spacing: CasinoDesign.Spacing.small) {
|
|
Text(String(localized: "Dealing Speed", bundle: .module))
|
|
.font(.system(size: CasinoDesign.BaseFontSize.medium, weight: .medium))
|
|
.foregroundStyle(.white)
|
|
|
|
HStack(spacing: CasinoDesign.Spacing.small) {
|
|
ForEach(options, id: \.1) { option in
|
|
Button {
|
|
speed = option.1
|
|
} label: {
|
|
Text(option.0)
|
|
.font(.system(size: CasinoDesign.BaseFontSize.body, weight: .medium))
|
|
.foregroundStyle(speed == option.1 ? .black : .white.opacity(CasinoDesign.Opacity.strong))
|
|
.padding(.vertical, CasinoDesign.Spacing.small)
|
|
.frame(maxWidth: .infinity)
|
|
.background(
|
|
Capsule()
|
|
.fill(speed == option.1 ? Color.yellow : Color.white.opacity(CasinoDesign.Opacity.subtle))
|
|
)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
}
|
|
.padding(.vertical, CasinoDesign.Spacing.xSmall)
|
|
}
|
|
}
|
|
|
|
// MARK: - Volume Picker
|
|
|
|
/// A volume slider with speaker icons.
|
|
public struct VolumePicker: View {
|
|
/// Binding to the volume level (0.0 to 1.0).
|
|
@Binding public var volume: Float
|
|
|
|
/// Creates a volume picker.
|
|
/// - Parameter volume: Binding to volume (0.0-1.0).
|
|
public init(volume: Binding<Float>) {
|
|
self._volume = volume
|
|
}
|
|
|
|
public var body: some View {
|
|
VStack(alignment: .leading, spacing: CasinoDesign.Spacing.small) {
|
|
HStack {
|
|
Text(String(localized: "Volume", bundle: .module))
|
|
.font(.system(size: CasinoDesign.BaseFontSize.medium, weight: .medium))
|
|
.foregroundStyle(.white)
|
|
|
|
Spacer()
|
|
|
|
Text("\(Int(volume * 100))%")
|
|
.font(.system(size: CasinoDesign.BaseFontSize.body, weight: .medium, design: .rounded))
|
|
.foregroundStyle(.white.opacity(CasinoDesign.Opacity.medium))
|
|
}
|
|
|
|
HStack(spacing: CasinoDesign.Spacing.medium) {
|
|
Image(systemName: "speaker.fill")
|
|
.font(.system(size: CasinoDesign.BaseFontSize.body))
|
|
.foregroundStyle(.white.opacity(CasinoDesign.Opacity.medium))
|
|
|
|
Slider(value: $volume, in: 0...1, step: 0.1)
|
|
.tint(.yellow)
|
|
|
|
Image(systemName: "speaker.wave.3.fill")
|
|
.font(.system(size: CasinoDesign.BaseFontSize.body))
|
|
.foregroundStyle(.white.opacity(CasinoDesign.Opacity.medium))
|
|
}
|
|
}
|
|
.padding(.vertical, CasinoDesign.Spacing.xSmall)
|
|
}
|
|
}
|
|
|
|
// MARK: - Selectable Row
|
|
|
|
/// A card-like selectable row with title, subtitle, optional badge, and selection indicator.
|
|
/// Use this for settings pickers like table limits, deck count, game style, etc.
|
|
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 (e.g., a pill showing "$10 - $1,000").
|
|
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: yellow).
|
|
/// - badge: Optional badge view.
|
|
/// - action: Action when tapped.
|
|
public init(
|
|
title: String,
|
|
subtitle: String,
|
|
isSelected: Bool,
|
|
accentColor: Color = .yellow,
|
|
@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: CasinoDesign.Spacing.xxSmall) {
|
|
Text(title)
|
|
.font(.system(size: CasinoDesign.BaseFontSize.large, weight: .semibold))
|
|
.foregroundStyle(.white)
|
|
|
|
Text(subtitle)
|
|
.font(.system(size: CasinoDesign.BaseFontSize.body))
|
|
.foregroundStyle(.white.opacity(CasinoDesign.Opacity.medium))
|
|
}
|
|
|
|
Spacer()
|
|
|
|
if let badge = badge {
|
|
badge
|
|
}
|
|
|
|
SelectionIndicator(isSelected: isSelected, accentColor: accentColor)
|
|
}
|
|
.padding()
|
|
.background(
|
|
RoundedRectangle(cornerRadius: CasinoDesign.CornerRadius.medium)
|
|
.fill(isSelected ? accentColor.opacity(CasinoDesign.Opacity.subtle) : Color.clear)
|
|
)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: CasinoDesign.CornerRadius.medium)
|
|
.strokeBorder(
|
|
isSelected ? accentColor.opacity(CasinoDesign.Opacity.medium) : Color.white.opacity(CasinoDesign.Opacity.subtle),
|
|
lineWidth: CasinoDesign.LineWidth.thin
|
|
)
|
|
)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
|
|
// Convenience initializer for rows without a badge
|
|
extension SelectableRow where Badge == EmptyView {
|
|
/// Creates a selectable row without a badge.
|
|
public init(
|
|
title: String,
|
|
subtitle: String,
|
|
isSelected: Bool,
|
|
accentColor: Color = .yellow,
|
|
action: @escaping () -> Void
|
|
) {
|
|
self.title = title
|
|
self.subtitle = subtitle
|
|
self.isSelected = isSelected
|
|
self.accentColor = accentColor
|
|
self.badge = nil
|
|
self.action = action
|
|
}
|
|
}
|
|
|
|
// MARK: - Selection Indicator
|
|
|
|
/// A circle indicator that shows selected (checkmark) or unselected (outline) state.
|
|
public struct SelectionIndicator: View {
|
|
/// Whether the item is selected.
|
|
public let isSelected: Bool
|
|
|
|
/// The accent color for the checkmark.
|
|
public let accentColor: Color
|
|
|
|
/// The size of the indicator.
|
|
public let size: CGFloat
|
|
|
|
/// Creates a selection indicator.
|
|
/// - Parameters:
|
|
/// - isSelected: Whether selected.
|
|
/// - accentColor: Color for checkmark (default: yellow).
|
|
/// - size: Size of the indicator (default: checkmark size from design).
|
|
public init(
|
|
isSelected: Bool,
|
|
accentColor: Color = .yellow,
|
|
size: CGFloat = CasinoDesign.Size.checkmark
|
|
) {
|
|
self.isSelected = isSelected
|
|
self.accentColor = accentColor
|
|
self.size = size
|
|
}
|
|
|
|
public var body: some View {
|
|
if isSelected {
|
|
Image(systemName: "checkmark.circle.fill")
|
|
.font(.system(size: size))
|
|
.foregroundStyle(accentColor)
|
|
} else {
|
|
Circle()
|
|
.strokeBorder(Color.white.opacity(CasinoDesign.Opacity.light), lineWidth: CasinoDesign.LineWidth.medium)
|
|
.frame(width: size, height: size)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Badge Pill
|
|
|
|
/// A capsule-shaped badge for displaying values like "$10 - $1,000".
|
|
public struct BadgePill: View {
|
|
/// The text to display in the badge.
|
|
public let text: String
|
|
|
|
/// Whether the parent row is selected.
|
|
public let isSelected: Bool
|
|
|
|
/// The accent color.
|
|
public let accentColor: Color
|
|
|
|
/// Creates a badge pill.
|
|
/// - Parameters:
|
|
/// - text: The badge text.
|
|
/// - isSelected: Whether the parent row is selected.
|
|
/// - accentColor: Color for the badge (default: yellow).
|
|
public init(text: String, isSelected: Bool, accentColor: Color = .yellow) {
|
|
self.text = text
|
|
self.isSelected = isSelected
|
|
self.accentColor = accentColor
|
|
}
|
|
|
|
public var body: some View {
|
|
Text(text)
|
|
.font(.system(size: CasinoDesign.BaseFontSize.body, weight: .bold, design: .rounded))
|
|
.foregroundStyle(isSelected ? .black : accentColor)
|
|
.padding(.horizontal, CasinoDesign.Spacing.small)
|
|
.padding(.vertical, CasinoDesign.Spacing.xSmall)
|
|
.background(
|
|
Capsule()
|
|
.fill(isSelected ? accentColor : accentColor.opacity(CasinoDesign.Opacity.hint))
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - Balance Picker
|
|
|
|
/// A grid picker for selecting a starting balance.
|
|
public struct BalancePicker: View {
|
|
/// Binding to the selected balance.
|
|
@Binding public var balance: Int
|
|
|
|
/// The available balance options.
|
|
public let options: [Int]
|
|
|
|
/// Creates a balance picker.
|
|
/// - Parameters:
|
|
/// - balance: Binding to selected balance.
|
|
/// - options: Available balance options (default: standard values).
|
|
public init(
|
|
balance: Binding<Int>,
|
|
options: [Int] = [1_000, 5_000, 10_000, 25_000, 50_000, 100_000]
|
|
) {
|
|
self._balance = balance
|
|
self.options = options
|
|
}
|
|
|
|
public var body: some View {
|
|
LazyVGrid(columns: [
|
|
GridItem(.flexible()),
|
|
GridItem(.flexible()),
|
|
GridItem(.flexible())
|
|
], spacing: CasinoDesign.Spacing.small) {
|
|
ForEach(options, id: \.self) { amount in
|
|
Button {
|
|
balance = amount
|
|
} label: {
|
|
Text(formattedAmount(amount))
|
|
.font(.system(size: CasinoDesign.BaseFontSize.body, weight: .bold))
|
|
.foregroundStyle(balance == amount ? .black : .white)
|
|
.padding(.vertical, CasinoDesign.Spacing.medium)
|
|
.frame(maxWidth: .infinity)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: CasinoDesign.CornerRadius.small)
|
|
.fill(balance == amount ? Color.yellow : Color.white.opacity(CasinoDesign.Opacity.subtle))
|
|
)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func formattedAmount(_ amount: Int) -> String {
|
|
if amount >= 1000 {
|
|
return "$\(amount / 1000)K"
|
|
}
|
|
return "$\(amount)"
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
ScrollView {
|
|
VStack(spacing: 20) {
|
|
// Selectable rows
|
|
VStack(spacing: CasinoDesign.Spacing.small) {
|
|
SelectableRow(
|
|
title: "Low Stakes",
|
|
subtitle: "Standard mini table",
|
|
isSelected: true,
|
|
badge: { BadgePill(text: "$10 - $1,000", isSelected: true) },
|
|
action: {}
|
|
)
|
|
|
|
SelectableRow(
|
|
title: "Medium Stakes",
|
|
subtitle: "Regular casino table",
|
|
isSelected: false,
|
|
badge: { BadgePill(text: "$25 - $5,000", isSelected: false) },
|
|
action: {}
|
|
)
|
|
|
|
SelectableRow(
|
|
title: "6 Decks",
|
|
subtitle: "Standard casino shoe",
|
|
isSelected: false,
|
|
action: {}
|
|
)
|
|
}
|
|
|
|
Divider().background(Color.white.opacity(0.1))
|
|
|
|
SettingsToggle(
|
|
title: "Sound Effects",
|
|
subtitle: "Play sounds for game events",
|
|
isOn: .constant(true)
|
|
)
|
|
|
|
Divider().background(Color.white.opacity(0.1))
|
|
|
|
SpeedPicker(speed: .constant(1.0))
|
|
|
|
Divider().background(Color.white.opacity(0.1))
|
|
|
|
VolumePicker(volume: .constant(0.8))
|
|
|
|
Divider().background(Color.white.opacity(0.1))
|
|
|
|
BalancePicker(balance: .constant(10_000))
|
|
}
|
|
.padding()
|
|
}
|
|
.background(Color.Sheet.background)
|
|
}
|
|
|