diff --git a/Sources/Bedrock/Views/Settings/BadgePill.swift b/Sources/Bedrock/Views/Settings/BadgePill.swift new file mode 100644 index 0000000..4a61764 --- /dev/null +++ b/Sources/Bedrock/Views/Settings/BadgePill.swift @@ -0,0 +1,61 @@ +// +// BadgePill.swift +// Bedrock +// +// A capsule-shaped badge for displaying short text values. +// + +import SwiftUI + +/// A capsule-shaped badge for displaying short text values. +/// +/// Use this to highlight values, tags, or status indicators. +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: primary accent). + public init( + text: String, + isSelected: Bool = false, + accentColor: Color = .Accent.primary + ) { + self.text = text + self.isSelected = isSelected + self.accentColor = accentColor + } + + public var body: some View { + Text(text) + .font(.system(size: Design.BaseFontSize.body, weight: .bold, design: .rounded)) + .foregroundStyle(isSelected ? .black : accentColor) + .padding(.horizontal, Design.Spacing.small) + .padding(.vertical, Design.Spacing.xSmall) + .background( + Capsule() + .fill(isSelected ? accentColor : accentColor.opacity(Design.Opacity.hint)) + ) + } +} + +// MARK: - Preview + +#Preview { + HStack(spacing: Design.Spacing.medium) { + BadgePill(text: "$9.99", isSelected: false) + BadgePill(text: "$9.99", isSelected: true) + BadgePill(text: "PRO", isSelected: false, accentColor: .orange) + } + .padding() + .background(Color.Surface.overlay) +} diff --git a/Sources/Bedrock/Views/Settings/SegmentedPicker.swift b/Sources/Bedrock/Views/Settings/SegmentedPicker.swift new file mode 100644 index 0000000..8af0826 --- /dev/null +++ b/Sources/Bedrock/Views/Settings/SegmentedPicker.swift @@ -0,0 +1,100 @@ +// +// SegmentedPicker.swift +// Bedrock +// +// A horizontal segmented picker with capsule-style buttons. +// + +import SwiftUI + +/// A horizontal segmented picker with capsule-style buttons. +/// +/// Use this for selecting from a small number of options (2-4). +/// +/// ```swift +/// SegmentedPicker( +/// title: "Theme", +/// options: [("Light", "light"), ("Dark", "dark"), ("System", "system")], +/// selection: $theme +/// ) +/// ``` +public struct SegmentedPicker: View { + /// The title/label for the picker. + public let title: String + + /// The available options as (label, value) pairs. + public let options: [(String, T)] + + /// Binding to the selected value. + @Binding public var selection: T + + /// The accent color for the selected button. + public let accentColor: Color + + /// Creates a segmented picker. + /// - Parameters: + /// - title: The title label. + /// - options: Array of (label, value) tuples. + /// - selection: Binding to selected value. + /// - accentColor: The accent color (default: primary accent). + public init( + title: String, + options: [(String, T)], + selection: Binding, + accentColor: Color = .Accent.primary + ) { + self.title = title + self.options = options + self._selection = selection + self.accentColor = accentColor + } + + public var body: some View { + VStack(alignment: .leading, spacing: Design.Spacing.small) { + Text(title) + .font(.system(size: Design.BaseFontSize.medium, weight: .medium)) + .foregroundStyle(.white) + + HStack(spacing: Design.Spacing.small) { + ForEach(options.indices, id: \.self) { index in + let option = options[index] + Button { + selection = option.1 + } label: { + Text(option.0) + .font(.system(size: Design.BaseFontSize.body, weight: .medium)) + .foregroundStyle(selection == option.1 ? .black : .white.opacity(Design.Opacity.strong)) + .padding(.vertical, Design.Spacing.small) + .frame(maxWidth: .infinity) + .background( + Capsule() + .fill(selection == option.1 ? accentColor : Color.white.opacity(Design.Opacity.subtle)) + ) + } + .buttonStyle(.plain) + } + } + } + .padding(.vertical, Design.Spacing.xSmall) + } +} + +// MARK: - Preview + +#Preview { + VStack(spacing: Design.Spacing.medium) { + SegmentedPicker( + title: "Animation Speed", + options: [("Fast", "fast"), ("Normal", "normal"), ("Slow", "slow")], + selection: .constant("normal") + ) + + SegmentedPicker( + title: "Theme", + options: [("Light", 0), ("Dark", 1)], + selection: .constant(1) + ) + } + .padding() + .background(Color.Surface.overlay) +} diff --git a/Sources/Bedrock/Views/Settings/SelectableRow.swift b/Sources/Bedrock/Views/Settings/SelectableRow.swift new file mode 100644 index 0000000..b2e2c97 --- /dev/null +++ b/Sources/Bedrock/Views/Settings/SelectableRow.swift @@ -0,0 +1,152 @@ +// +// 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: 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) + .font(.system(size: Design.BaseFontSize.large, weight: .semibold)) + .foregroundStyle(.white) + + Text(subtitle) + .font(.system(size: Design.BaseFontSize.body)) + .foregroundStyle(.white.opacity(Design.Opacity.medium)) + } + + 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) + } +} + +// 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) +} diff --git a/Sources/Bedrock/Views/Settings/SelectionIndicator.swift b/Sources/Bedrock/Views/Settings/SelectionIndicator.swift new file mode 100644 index 0000000..3e4042d --- /dev/null +++ b/Sources/Bedrock/Views/Settings/SelectionIndicator.swift @@ -0,0 +1,59 @@ +// +// SelectionIndicator.swift +// Bedrock +// +// A circle indicator that shows selected (checkmark) or unselected (outline) state. +// + +import SwiftUI + +/// 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: primary accent). + /// - size: Size of the indicator (default: checkmark size from design). + public init( + isSelected: Bool, + accentColor: Color = .Accent.primary, + size: CGFloat = Design.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(Design.Opacity.light), lineWidth: Design.LineWidth.medium) + .frame(width: size, height: size) + } + } +} + +// MARK: - Preview + +#Preview { + HStack(spacing: Design.Spacing.large) { + SelectionIndicator(isSelected: true) + SelectionIndicator(isSelected: false) + SelectionIndicator(isSelected: true, accentColor: .green) + } + .padding() + .background(Color.Surface.overlay) +} diff --git a/Sources/Bedrock/Views/Settings/SettingsComponents.swift b/Sources/Bedrock/Views/Settings/SettingsComponents.swift deleted file mode 100644 index 0476d46..0000000 --- a/Sources/Bedrock/Views/Settings/SettingsComponents.swift +++ /dev/null @@ -1,616 +0,0 @@ -// -// SettingsComponents.swift -// Bedrock -// -// Reusable settings UI components for building consistent settings screens. -// - -import SwiftUI - -// MARK: - Settings Toggle - -/// A toggle setting row with title and subtitle. -/// -/// Use this for boolean settings that can be turned on or off. -/// -/// ```swift -/// SettingsToggle( -/// title: "Dark Mode", -/// subtitle: "Use dark appearance", -/// isOn: $settings.darkMode -/// ) -/// ``` -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 - - /// The accent color for the toggle. - public let accentColor: Color - - /// Creates a settings toggle. - /// - Parameters: - /// - title: The main title. - /// - subtitle: The subtitle description. - /// - isOn: Binding to toggle state. - /// - accentColor: The accent color (default: primary accent). - public init( - title: String, - subtitle: String, - isOn: Binding, - accentColor: Color = .Accent.primary - ) { - self.title = title - self.subtitle = subtitle - self._isOn = isOn - self.accentColor = accentColor - } - - public var body: some View { - Toggle(isOn: $isOn) { - VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) { - Text(title) - .font(.system(size: Design.BaseFontSize.medium, weight: .medium)) - .foregroundStyle(.white) - - Text(subtitle) - .font(.system(size: Design.BaseFontSize.body)) - .foregroundStyle(.white.opacity(Design.Opacity.medium)) - } - } - .tint(accentColor) - .padding(.vertical, Design.Spacing.xSmall) - } -} - -// MARK: - Volume Picker - -/// A volume slider with speaker icons. -/// -/// Use this for audio volume or similar 0-1 range settings. -public struct VolumePicker: View { - /// The label for the picker. - public let label: String - - /// Binding to the volume level (0.0 to 1.0). - @Binding public var volume: Float - - /// The accent color for the slider. - public let accentColor: Color - - /// Creates a volume picker. - /// - Parameters: - /// - label: The label text (default: "Volume"). - /// - volume: Binding to volume (0.0-1.0). - /// - accentColor: The accent color (default: primary accent). - public init( - label: String = "Volume", - volume: Binding, - accentColor: Color = .Accent.primary - ) { - self.label = label - self._volume = volume - self.accentColor = accentColor - } - - public var body: some View { - VStack(alignment: .leading, spacing: Design.Spacing.small) { - HStack { - Text(label) - .font(.system(size: Design.BaseFontSize.medium, weight: .medium)) - .foregroundStyle(.white) - - Spacer() - - Text("\(Int(volume * 100))%") - .font(.system(size: Design.BaseFontSize.body, weight: .medium, design: .rounded)) - .foregroundStyle(.white.opacity(Design.Opacity.medium)) - } - - HStack(spacing: Design.Spacing.medium) { - Image(systemName: "speaker.fill") - .font(.system(size: Design.BaseFontSize.body)) - .foregroundStyle(.white.opacity(Design.Opacity.medium)) - - Slider(value: $volume, in: 0...1, step: 0.1) - .tint(accentColor) - - Image(systemName: "speaker.wave.3.fill") - .font(.system(size: Design.BaseFontSize.body)) - .foregroundStyle(.white.opacity(Design.Opacity.medium)) - } - } - .padding(.vertical, Design.Spacing.xSmall) - } -} - -// MARK: - Segmented Picker - -/// A horizontal segmented picker with capsule-style buttons. -/// -/// Use this for selecting from a small number of options (2-4). -/// -/// ```swift -/// SegmentedPicker( -/// title: "Theme", -/// options: [("Light", "light"), ("Dark", "dark"), ("System", "system")], -/// selection: $theme -/// ) -/// ``` -public struct SegmentedPicker: View { - /// The title/label for the picker. - public let title: String - - /// The available options as (label, value) pairs. - public let options: [(String, T)] - - /// Binding to the selected value. - @Binding public var selection: T - - /// The accent color for the selected button. - public let accentColor: Color - - /// Creates a segmented picker. - /// - Parameters: - /// - title: The title label. - /// - options: Array of (label, value) tuples. - /// - selection: Binding to selected value. - /// - accentColor: The accent color (default: primary accent). - public init( - title: String, - options: [(String, T)], - selection: Binding, - accentColor: Color = .Accent.primary - ) { - self.title = title - self.options = options - self._selection = selection - self.accentColor = accentColor - } - - public var body: some View { - VStack(alignment: .leading, spacing: Design.Spacing.small) { - Text(title) - .font(.system(size: Design.BaseFontSize.medium, weight: .medium)) - .foregroundStyle(.white) - - HStack(spacing: Design.Spacing.small) { - ForEach(options.indices, id: \.self) { index in - let option = options[index] - Button { - selection = option.1 - } label: { - Text(option.0) - .font(.system(size: Design.BaseFontSize.body, weight: .medium)) - .foregroundStyle(selection == option.1 ? .black : .white.opacity(Design.Opacity.strong)) - .padding(.vertical, Design.Spacing.small) - .frame(maxWidth: .infinity) - .background( - Capsule() - .fill(selection == option.1 ? accentColor : Color.white.opacity(Design.Opacity.subtle)) - ) - } - .buttonStyle(.plain) - } - } - } - .padding(.vertical, Design.Spacing.xSmall) - } -} - -// MARK: - Selectable Row - -/// 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: 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) - .font(.system(size: Design.BaseFontSize.large, weight: .semibold)) - .foregroundStyle(.white) - - Text(subtitle) - .font(.system(size: Design.BaseFontSize.body)) - .foregroundStyle(.white.opacity(Design.Opacity.medium)) - } - - 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) - } -} - -// 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 = .Accent.primary, - 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: primary accent). - /// - size: Size of the indicator (default: checkmark size from design). - public init( - isSelected: Bool, - accentColor: Color = .Accent.primary, - size: CGFloat = Design.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(Design.Opacity.light), lineWidth: Design.LineWidth.medium) - .frame(width: size, height: size) - } - } -} - -// MARK: - Badge Pill - -/// A capsule-shaped badge for displaying short text values. -/// -/// Use this to highlight values, tags, or status indicators. -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: primary accent). - public init( - text: String, - isSelected: Bool = false, - accentColor: Color = .Accent.primary - ) { - self.text = text - self.isSelected = isSelected - self.accentColor = accentColor - } - - public var body: some View { - Text(text) - .font(.system(size: Design.BaseFontSize.body, weight: .bold, design: .rounded)) - .foregroundStyle(isSelected ? .black : accentColor) - .padding(.horizontal, Design.Spacing.small) - .padding(.vertical, Design.Spacing.xSmall) - .background( - Capsule() - .fill(isSelected ? accentColor : accentColor.opacity(Design.Opacity.hint)) - ) - } -} - -// MARK: - Settings Section Header - -/// A section header for settings screens. -public struct SettingsSectionHeader: View { - /// The section title. - public let title: String - - /// Optional system image name. - public let systemImage: String? - - /// The accent color for the header. - public let accentColor: Color - - /// Creates a section header. - /// - Parameters: - /// - title: The section title. - /// - systemImage: Optional SF Symbol name. - /// - accentColor: The accent color (default: primary accent). - public init(title: String, systemImage: String? = nil, accentColor: Color = .Accent.primary) { - self.title = title - self.systemImage = systemImage - self.accentColor = accentColor - } - - public var body: some View { - HStack(spacing: Design.Spacing.small) { - if let systemImage { - Image(systemName: systemImage) - .font(.system(size: Design.BaseFontSize.medium)) - .foregroundStyle(accentColor.opacity(Design.Opacity.strong)) - } - - Text(title) - .font(.system(size: Design.BaseFontSize.caption, weight: .semibold)) - .foregroundStyle(.white.opacity(Design.Opacity.accent)) - .textCase(.uppercase) - .tracking(0.5) - - Spacer() - } - .padding(.horizontal, Design.Spacing.xSmall) - .padding(.top, Design.Spacing.large) - .padding(.bottom, Design.Spacing.xSmall) - } -} - -// MARK: - Settings Row - -/// A simple settings row with icon, title, and optional value/accessory. -public struct SettingsRow: View { - /// The row icon (SF Symbol name). - public let systemImage: String - - /// The row title. - public let title: String - - /// Optional value text. - public let value: String? - - /// The icon background color. - public let iconColor: Color - - /// Optional accessory view. - public let accessory: Accessory? - - /// Action when tapped. - public let action: () -> Void - - /// Creates a settings row. - public init( - systemImage: String, - title: String, - value: String? = nil, - iconColor: Color = .Accent.primary, - @ViewBuilder accessory: () -> Accessory? = { nil as EmptyView? }, - action: @escaping () -> Void - ) { - self.systemImage = systemImage - self.title = title - self.value = value - self.iconColor = iconColor - self.accessory = accessory() - self.action = action - } - - public var body: some View { - Button(action: action) { - HStack(spacing: Design.Spacing.medium) { - Image(systemName: systemImage) - .font(.system(size: Design.BaseFontSize.medium)) - .foregroundStyle(.white) - .frame(width: Design.Size.iconContainerSmall, height: Design.Size.iconContainerSmall) - .background(iconColor.opacity(Design.Opacity.heavy)) - .clipShape(.rect(cornerRadius: Design.CornerRadius.xSmall)) - - Text(title) - .font(.system(size: Design.BaseFontSize.medium)) - .foregroundStyle(.white) - - Spacer() - - if let value { - Text(value) - .font(.system(size: Design.BaseFontSize.body)) - .foregroundStyle(.white.opacity(Design.Opacity.medium)) - } - - if let accessory { - accessory - } else { - Image(systemName: "chevron.right") - .font(.system(size: Design.BaseFontSize.body, weight: .medium)) - .foregroundStyle(.white.opacity(Design.Opacity.light)) - } - } - .padding(.vertical, Design.Spacing.medium) - .padding(.horizontal, Design.Spacing.medium) - .background(Color.Surface.card) - .clipShape(.rect(cornerRadius: Design.CornerRadius.small)) - } - .buttonStyle(.plain) - } -} - -// Convenience initializer for rows without accessory -extension SettingsRow where Accessory == EmptyView { - /// Creates a settings row without an accessory. - public init( - systemImage: String, - title: String, - value: String? = nil, - iconColor: Color = .Accent.primary, - action: @escaping () -> Void - ) { - self.systemImage = systemImage - self.title = title - self.value = value - self.iconColor = iconColor - self.accessory = nil - self.action = action - } -} - -// MARK: - Preview - -#Preview { - ScrollView { - VStack(spacing: Design.Spacing.large) { - SettingsSectionHeader(title: "Appearance", systemImage: "paintbrush") - - // Selectable rows - 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: {} - ) - } - - SettingsSectionHeader(title: "Preferences", systemImage: "gearshape") - - SettingsToggle( - title: "Sound Effects", - subtitle: "Play sounds for events", - isOn: .constant(true) - ) - - SegmentedPicker( - title: "Animation Speed", - options: [("Fast", "fast"), ("Normal", "normal"), ("Slow", "slow")], - selection: .constant("normal") - ) - - VolumePicker(volume: .constant(0.8)) - - SettingsSectionHeader(title: "About", systemImage: "info.circle") - - SettingsRow( - systemImage: "star.fill", - title: "Rate App", - iconColor: .Status.warning, - action: {} - ) - - SettingsRow( - systemImage: "envelope.fill", - title: "Contact Us", - value: "support@example.com", - iconColor: .Status.info, - action: {} - ) - } - .padding() - } - .background(Color.Surface.overlay) -} diff --git a/Sources/Bedrock/Views/Settings/SettingsRow.swift b/Sources/Bedrock/Views/Settings/SettingsRow.swift new file mode 100644 index 0000000..29e678e --- /dev/null +++ b/Sources/Bedrock/Views/Settings/SettingsRow.swift @@ -0,0 +1,134 @@ +// +// SettingsRow.swift +// Bedrock +// +// A simple settings row with icon, title, and optional value/accessory. +// + +import SwiftUI + +/// A simple settings row with icon, title, and optional value/accessory. +public struct SettingsRow: View { + /// The row icon (SF Symbol name). + public let systemImage: String + + /// The row title. + public let title: String + + /// Optional value text. + public let value: String? + + /// The icon background color. + public let iconColor: Color + + /// Optional accessory view. + public let accessory: Accessory? + + /// Action when tapped. + public let action: () -> Void + + /// Creates a settings row. + public init( + systemImage: String, + title: String, + value: String? = nil, + iconColor: Color = .Accent.primary, + @ViewBuilder accessory: () -> Accessory? = { nil as EmptyView? }, + action: @escaping () -> Void + ) { + self.systemImage = systemImage + self.title = title + self.value = value + self.iconColor = iconColor + self.accessory = accessory() + self.action = action + } + + public var body: some View { + Button(action: action) { + HStack(spacing: Design.Spacing.medium) { + Image(systemName: systemImage) + .font(.system(size: Design.BaseFontSize.medium)) + .foregroundStyle(.white) + .frame(width: Design.Size.iconContainerSmall, height: Design.Size.iconContainerSmall) + .background(iconColor.opacity(Design.Opacity.heavy)) + .clipShape(.rect(cornerRadius: Design.CornerRadius.xSmall)) + + Text(title) + .font(.system(size: Design.BaseFontSize.medium)) + .foregroundStyle(.white) + + Spacer() + + if let value { + Text(value) + .font(.system(size: Design.BaseFontSize.body)) + .foregroundStyle(.white.opacity(Design.Opacity.medium)) + } + + if let accessory { + accessory + } else { + Image(systemName: "chevron.right") + .font(.system(size: Design.BaseFontSize.body, weight: .medium)) + .foregroundStyle(.white.opacity(Design.Opacity.light)) + } + } + .padding(.vertical, Design.Spacing.medium) + .padding(.horizontal, Design.Spacing.medium) + .background(Color.Surface.card) + .clipShape(.rect(cornerRadius: Design.CornerRadius.small)) + } + .buttonStyle(.plain) + } +} + +// MARK: - Convenience Initializer + +extension SettingsRow where Accessory == EmptyView { + /// Creates a settings row without an accessory. + public init( + systemImage: String, + title: String, + value: String? = nil, + iconColor: Color = .Accent.primary, + action: @escaping () -> Void + ) { + self.systemImage = systemImage + self.title = title + self.value = value + self.iconColor = iconColor + self.accessory = nil + self.action = action + } +} + +// MARK: - Preview + +#Preview { + VStack(spacing: Design.Spacing.small) { + SettingsRow( + systemImage: "star.fill", + title: "Rate App", + iconColor: .Status.warning, + action: {} + ) + + SettingsRow( + systemImage: "envelope.fill", + title: "Contact Us", + value: "support@example.com", + iconColor: .Status.info, + action: {} + ) + + SettingsRow( + systemImage: "bell.fill", + title: "Notifications", + iconColor: .Status.error, + action: {} + ) + } + .padding() + .background(Color.Surface.overlay) +} diff --git a/Sources/Bedrock/Views/Settings/SettingsSectionHeader.swift b/Sources/Bedrock/Views/Settings/SettingsSectionHeader.swift new file mode 100644 index 0000000..e449962 --- /dev/null +++ b/Sources/Bedrock/Views/Settings/SettingsSectionHeader.swift @@ -0,0 +1,64 @@ +// +// SettingsSectionHeader.swift +// Bedrock +// +// A section header for settings screens. +// + +import SwiftUI + +/// A section header for settings screens. +public struct SettingsSectionHeader: View { + /// The section title. + public let title: String + + /// Optional system image name. + public let systemImage: String? + + /// The accent color for the header. + public let accentColor: Color + + /// Creates a section header. + /// - Parameters: + /// - title: The section title. + /// - systemImage: Optional SF Symbol name. + /// - accentColor: The accent color (default: primary accent). + public init(title: String, systemImage: String? = nil, accentColor: Color = .Accent.primary) { + self.title = title + self.systemImage = systemImage + self.accentColor = accentColor + } + + public var body: some View { + HStack(spacing: Design.Spacing.small) { + if let systemImage { + Image(systemName: systemImage) + .font(.system(size: Design.BaseFontSize.medium)) + .foregroundStyle(accentColor.opacity(Design.Opacity.strong)) + } + + Text(title) + .font(.system(size: Design.BaseFontSize.caption, weight: .semibold)) + .foregroundStyle(.white.opacity(Design.Opacity.accent)) + .textCase(.uppercase) + .tracking(0.5) + + Spacer() + } + .padding(.horizontal, Design.Spacing.xSmall) + .padding(.top, Design.Spacing.large) + .padding(.bottom, Design.Spacing.xSmall) + } +} + +// MARK: - Preview + +#Preview { + VStack(alignment: .leading, spacing: 0) { + SettingsSectionHeader(title: "Appearance", systemImage: "paintbrush") + SettingsSectionHeader(title: "Preferences", systemImage: "gearshape") + SettingsSectionHeader(title: "Premium", systemImage: "crown", accentColor: .orange) + } + .padding() + .background(Color.Surface.overlay) +} diff --git a/Sources/Bedrock/Views/Settings/SettingsToggle.swift b/Sources/Bedrock/Views/Settings/SettingsToggle.swift new file mode 100644 index 0000000..0662565 --- /dev/null +++ b/Sources/Bedrock/Views/Settings/SettingsToggle.swift @@ -0,0 +1,87 @@ +// +// SettingsToggle.swift +// Bedrock +// +// A toggle setting row with title and subtitle. +// + +import SwiftUI + +/// A toggle setting row with title and subtitle. +/// +/// Use this for boolean settings that can be turned on or off. +/// +/// ```swift +/// SettingsToggle( +/// title: "Dark Mode", +/// subtitle: "Use dark appearance", +/// isOn: $settings.darkMode +/// ) +/// ``` +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 + + /// The accent color for the toggle. + public let accentColor: Color + + /// Creates a settings toggle. + /// - Parameters: + /// - title: The main title. + /// - subtitle: The subtitle description. + /// - isOn: Binding to toggle state. + /// - accentColor: The accent color (default: primary accent). + public init( + title: String, + subtitle: String, + isOn: Binding, + accentColor: Color = .Accent.primary + ) { + self.title = title + self.subtitle = subtitle + self._isOn = isOn + self.accentColor = accentColor + } + + public var body: some View { + Toggle(isOn: $isOn) { + VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) { + Text(title) + .font(.system(size: Design.BaseFontSize.medium, weight: .medium)) + .foregroundStyle(.white) + + Text(subtitle) + .font(.system(size: Design.BaseFontSize.body)) + .foregroundStyle(.white.opacity(Design.Opacity.medium)) + } + } + .tint(accentColor) + .padding(.vertical, Design.Spacing.xSmall) + } +} + +// MARK: - Preview + +#Preview { + VStack(spacing: Design.Spacing.medium) { + SettingsToggle( + title: "Sound Effects", + subtitle: "Play sounds for events", + isOn: .constant(true) + ) + + SettingsToggle( + title: "Notifications", + subtitle: "Receive push notifications", + isOn: .constant(false) + ) + } + .padding() + .background(Color.Surface.overlay) +} diff --git a/Sources/Bedrock/Views/Settings/VolumePicker.swift b/Sources/Bedrock/Views/Settings/VolumePicker.swift new file mode 100644 index 0000000..fcd735f --- /dev/null +++ b/Sources/Bedrock/Views/Settings/VolumePicker.swift @@ -0,0 +1,78 @@ +// +// VolumePicker.swift +// Bedrock +// +// A volume slider with speaker icons. +// + +import SwiftUI + +/// A volume slider with speaker icons. +/// +/// Use this for audio volume or similar 0-1 range settings. +public struct VolumePicker: View { + /// The label for the picker. + public let label: String + + /// Binding to the volume level (0.0 to 1.0). + @Binding public var volume: Float + + /// The accent color for the slider. + public let accentColor: Color + + /// Creates a volume picker. + /// - Parameters: + /// - label: The label text (default: "Volume"). + /// - volume: Binding to volume (0.0-1.0). + /// - accentColor: The accent color (default: primary accent). + public init( + label: String = "Volume", + volume: Binding, + accentColor: Color = .Accent.primary + ) { + self.label = label + self._volume = volume + self.accentColor = accentColor + } + + public var body: some View { + VStack(alignment: .leading, spacing: Design.Spacing.small) { + HStack { + Text(label) + .font(.system(size: Design.BaseFontSize.medium, weight: .medium)) + .foregroundStyle(.white) + + Spacer() + + Text("\(Int(volume * 100))%") + .font(.system(size: Design.BaseFontSize.body, weight: .medium, design: .rounded)) + .foregroundStyle(.white.opacity(Design.Opacity.medium)) + } + + HStack(spacing: Design.Spacing.medium) { + Image(systemName: "speaker.fill") + .font(.system(size: Design.BaseFontSize.body)) + .foregroundStyle(.white.opacity(Design.Opacity.medium)) + + Slider(value: $volume, in: 0...1, step: 0.1) + .tint(accentColor) + + Image(systemName: "speaker.wave.3.fill") + .font(.system(size: Design.BaseFontSize.body)) + .foregroundStyle(.white.opacity(Design.Opacity.medium)) + } + } + .padding(.vertical, Design.Spacing.xSmall) + } +} + +// MARK: - Preview + +#Preview { + VStack(spacing: Design.Spacing.medium) { + VolumePicker(volume: .constant(0.8)) + VolumePicker(label: "Music", volume: .constant(0.5)) + } + .padding() + .background(Color.Surface.overlay) +}