diff --git a/packages/HorizonUI/Sources/HorizonUI/Sources/Components/Badge/HorizonUI.Badge.swift b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/Badge/HorizonUI.Badge.swift index 993ee4c332..eb3663a6cf 100644 --- a/packages/HorizonUI/Sources/HorizonUI/Sources/Components/Badge/HorizonUI.Badge.swift +++ b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/Badge/HorizonUI.Badge.swift @@ -52,6 +52,7 @@ public extension HorizonUI { .huiTypography(.tag) .frame(minWidth: 19, minHeight: 19) .padding(.huiSpaces.primitives.xxSmall) + .multilineTextAlignment(.center) case .icon(let icon): icon .resizable() diff --git a/packages/HorizonUI/Sources/HorizonUI/Sources/Components/Button/HorizonUI.ButtonStyles.Storybook.swift b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/Button/HorizonUI.ButtonStyles.Storybook.swift index f14a532ade..a7e2852d05 100644 --- a/packages/HorizonUI/Sources/HorizonUI/Sources/Components/Button/HorizonUI.ButtonStyles.Storybook.swift +++ b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/Button/HorizonUI.ButtonStyles.Storybook.swift @@ -19,109 +19,67 @@ import SwiftUI extension HorizonUI.ButtonStyles { + private struct StorybookConfiguration: Identifiable { + var id: String { title } + + let isSmall: Bool + let isDisabled: Bool + let title: String + } + struct Storybook: View { var body: some View { ScrollView { VStack(alignment: .leading, spacing: 24) { - Section(header: header("Regular Height - Relative Width Buttons")) { - VStack(alignment: .leading, spacing: 16) { - Button("Black Button") {} - .buttonStyle( - HorizonUI.ButtonStyles.black( - leading: HorizonUI.icons.addCircle, - trailing: HorizonUI.icons.addCircle - ) - ) - - Button("White Button") {} - .buttonStyle( - HorizonUI.ButtonStyles.white( - leading: HorizonUI.icons.addCircle, - trailing: HorizonUI.icons.addCircle - ) - ) - .huiElevation(level: .level3) - - Button("AI Button") {} - .buttonStyle(HorizonUI.ButtonStyles.ai()) - - Button("Blue Button") {} - .buttonStyle(HorizonUI.ButtonStyles.blue()) - - Button("Beige Button") {} - .buttonStyle(HorizonUI.ButtonStyles.beige()) - .huiElevation(level: .level3) - } - } - - Section(header: header("Small Height - Block Width Buttons")) { - VStack(alignment: .leading, spacing: 16) { - Button("Small Black Button") {} - .buttonStyle( - HorizonUI.ButtonStyles.black( - isSmall: true, - fillsWidth: true - ) - ) - Button("Small White Button") {} - .buttonStyle( - HorizonUI.ButtonStyles.white( - isSmall: true, - fillsWidth: true - ) - ) - .huiElevation(level: .level3) - Button("Small AI Button") {} - .buttonStyle( - HorizonUI.ButtonStyles.ai( - isSmall: true, - fillsWidth: true - ) - ) - Button("Small Blue Button") {} - .buttonStyle( - HorizonUI.ButtonStyles.blue( - isSmall: true, - fillsWidth: true - ) - ) - Button("Small Beige Button") {} - .buttonStyle( - HorizonUI.ButtonStyles.beige( - isSmall: true, - fillsWidth: true - ) - ) - .huiElevation(level: .level3) - } + ForEach( + [ + StorybookConfiguration( + isSmall: false, + isDisabled: false, + title: "Regular Height - Relative Width Buttons" + ), + StorybookConfiguration(isSmall: true, isDisabled: false, title: "Small Height - Block Width Buttons"), + StorybookConfiguration(isSmall: false, isDisabled: true, title: "Disabled Buttons"), + ] + ) { storybookConfiguration in + section(storybookConfiguration) } + .padding(16) + } + .background(Color(red: 88 / 100, green: 88 / 100, blue: 88 / 100)) + .navigationTitle("Buttons") + .navigationBarTitleDisplayMode(.large) + } + } - Section(header: header("Disabled Buttons")) { - VStack(alignment: .leading, spacing: 16) { - Button("Disabled Black Button") {} - .buttonStyle(HorizonUI.ButtonStyles.black()) - .disabled(true) - Button("Disabled White Button") {} - .buttonStyle(HorizonUI.ButtonStyles.white()) - .disabled(true) - .huiElevation(level: .level3) - Button("Disabled AI Button") {} - .buttonStyle(HorizonUI.ButtonStyles.ai()) - .disabled(true) - Button("Disabled Blue Button") {} - .buttonStyle(HorizonUI.ButtonStyles.blue()) - .disabled(true) - Button("Disabled Beige Button") {} - .buttonStyle(HorizonUI.ButtonStyles.beige()) - .disabled(true) - .huiElevation(level: .level3) - } + private func section(_ storybookConfiguration: StorybookConfiguration) -> some View { + Section(header: header(storybookConfiguration.title)) { + VStack(alignment: .leading, spacing: 16) { + ForEach(HorizonUI.ButtonStyles.ButtonType.allCases) { type in + row(type: type, isSmall: storybookConfiguration.isSmall, isDisabled: storybookConfiguration.isDisabled) } } - .padding(16) } - .navigationTitle("Buttons") - .navigationBarTitleDisplayMode(.large) + } + + private func row( + type: HorizonUI.ButtonStyles.ButtonType, + isSmall: Bool, + isDisabled: Bool + ) -> some View { + HStack(spacing: 16) { + Button("\(type.rawValue) Icon Button") {} + .buttonStyle( + HorizonUI.ButtonStyles.icon(type, isSmall: isSmall, badgeNumber: "99") + ) + .disabled(isDisabled) + + Button("\(type.rawValue) Button") {} + .buttonStyle( + HorizonUI.ButtonStyles.primary(type, isSmall: isSmall, fillsWidth: isSmall) + ) + .disabled(isDisabled) + } } private func header(_ title: String) -> some View { diff --git a/packages/HorizonUI/Sources/HorizonUI/Sources/Components/Button/HorizonUI.ButtonStyles.swift b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/Button/HorizonUI.ButtonStyles.swift index 72e8b71d41..dd06de08c2 100644 --- a/packages/HorizonUI/Sources/HorizonUI/Sources/Components/Button/HorizonUI.ButtonStyles.swift +++ b/packages/HorizonUI/Sources/HorizonUI/Sources/Components/Button/HorizonUI.ButtonStyles.swift @@ -19,121 +19,188 @@ import SwiftUI extension HorizonUI { - struct ButtonStyles: ButtonStyle { + public struct ButtonStyles: ButtonStyle { + // MARK: - Common Dependencies + @Environment(\.isEnabled) private var isEnabled - private let background: AnyShapeStyle - private let foreground: Color + private let backgroundColor: AnyShapeStyle + private let foregroundColor: Color private let isSmall: Bool + + // MARK: - Primary and Secondary Button Dependencies + private let fillsWidth: Bool private let leading: Image? private let trailing: Image? + private let smallButtonSize = 40.0 + private let largeButtonSize = 44.0 + + // MARK: - Icon Button Dependencies + + private let badgeNumber: String? + private let badgeStyle: HorizonUI.Badge.Style? + private let icon: Image? fileprivate init( - background: any ShapeStyle, - foreground: Color, + backgroundColor: any ShapeStyle, + foregroundColor: Color, isSmall: Bool = false, fillsWidth: Bool = false, leading: Image? = nil, trailing: Image? = nil ) { - self.background = AnyShapeStyle(background) - self.foreground = foreground + self.backgroundColor = AnyShapeStyle(backgroundColor) + self.foregroundColor = foregroundColor self.isSmall = isSmall self.fillsWidth = fillsWidth self.leading = leading self.trailing = trailing + + self.badgeNumber = nil + self.badgeStyle = nil + self.icon = nil } - func makeBody(configuration: Configuration) -> some View { - HStack { - leading? - .renderingMode(.template) - .foregroundColor(foreground) + fileprivate init( + backgroundColor: any ShapeStyle, + foregroundColor: Color, + badgeStyle: HorizonUI.Badge.Style, + isSmall: Bool = false, + icon: Image, + badgeNumber: String? = nil + ) { + self.backgroundColor = AnyShapeStyle(backgroundColor) + self.badgeNumber = badgeNumber + self.badgeStyle = badgeStyle + self.foregroundColor = foregroundColor + self.icon = icon + self.isSmall = isSmall + + self.fillsWidth = false + self.leading = nil + self.trailing = nil + } + + public func makeBody(configuration: Configuration) -> some View { + ZStack { + if let icon = icon { + ZStack { + icon + .renderingMode(.template) + .frame(width: isSmall ? smallButtonSize : largeButtonSize, + height: isSmall ? smallButtonSize : largeButtonSize) + .background(backgroundColor) + .foregroundStyle(foregroundColor) + .huiCornerRadius(level: .level6) + .foregroundColor(foregroundColor) + .opacity(isEnabled ? (configuration.isPressed ? 0.8 : 1.0) : 0.5) - configuration.label + if let badgeNumber = badgeNumber, let badgeStyle = badgeStyle { + HorizonUI.Badge(type: .number(badgeNumber), style: badgeStyle) + .offset(x: 15, y: -15) + } + } + } else { + HStack { + leading? + .renderingMode(.template) + .foregroundColor(foregroundColor) - trailing? - .renderingMode(.template) - .foregroundColor(foreground) + configuration.label + + trailing? + .renderingMode(.template) + .foregroundColor(foregroundColor) + } + .huiTypography(.buttonTextLarge) + .padding(.horizontal, .huiSpaces.primitives.mediumSmall) + .frame(height: isSmall ? smallButtonSize : largeButtonSize) + .frame(maxWidth: fillsWidth ? .infinity : nil) + .background(backgroundColor) + .foregroundStyle(foregroundColor) + .huiCornerRadius(level: .level6) + .opacity(isEnabled ? (configuration.isPressed ? 0.8 : 1.0) : 0.5) + } } - .huiTypography(.buttonTextLarge) - .tracking(100) - .padding(.horizontal, 16) - .frame(height: isSmall ? 40 : 44) - .frame(maxWidth: fillsWidth ? .infinity : nil) - .background(background) - .foregroundStyle(foreground) - .cornerRadius(isSmall ? 20 : 22) - .opacity(isEnabled ? (configuration.isPressed ? 0.8 : 1.0) : 0.5) } } } extension HorizonUI.ButtonStyles { - static func ai( - isSmall: Bool = false, - fillsWidth: Bool = false, - leading: Image? = nil, - trailing: Image? = nil - ) -> HorizonUI.ButtonStyles { - self.init( - background: LinearGradient( - gradient: Gradient(colors: [ - Color.huiColors.surface.aiGradientStart, - Color.huiColors.surface.aiGradientEnd - ]), - startPoint: .top, - endPoint: .bottom - ), - foreground: Color.huiColors.text.surfaceColored, - isSmall: isSmall, - fillsWidth: fillsWidth, - leading: leading, - trailing: trailing - ) - } + public enum ButtonType: String, CaseIterable, Identifiable { + case ai = "AI" + case beige = "Beige" + case blue = "Blue" + case black = "Black" + case white = "White" - static func beige( - isSmall: Bool = false, - fillsWidth: Bool = false, - leading: Image? = nil, - trailing: Image? = nil - ) -> HorizonUI.ButtonStyles { - self.init( - background: Color.huiColors.surface.pagePrimary, - foreground: Color.huiColors.text.title, - isSmall: isSmall, - fillsWidth: fillsWidth, - leading: leading, - trailing: trailing - ) - } + public var id: String { rawValue } - static func black( - isSmall: Bool = false, - fillsWidth: Bool = false, - leading: Image? = nil, - trailing: Image? = nil - ) -> HorizonUI.ButtonStyles { - .init( - background: Color.huiColors.surface.inversePrimary, - foreground: Color.huiColors.text.surfaceColored, - isSmall: isSmall, - fillsWidth: fillsWidth, - leading: leading, - trailing: trailing - ) + var background: any ShapeStyle { + switch self { + case .ai: + return LinearGradient( + gradient: Gradient(colors: [ + .huiColors.surface.institution, + .huiColors.primitives.green70 + ]), + startPoint: .top, + endPoint: .bottom + ) + case .beige: + return Color.huiColors.surface.pagePrimary + case .blue: + return Color.huiColors.surface.institution + case .black: + return Color.huiColors.surface.inversePrimary + case .white: + return Color.huiColors.surface.pageSecondary + } + } + + var foregroundColor: Color { + switch self { + case .ai: + return Color.huiColors.text.surfaceColored + case .beige: + return Color.huiColors.text.title + case .blue: + return Color.huiColors.text.surfaceColored + case .black: + return Color.huiColors.text.surfaceColored + case .white: + return Color.huiColors.text.title + } + } + + var badgeStyle: HorizonUI.Badge.Style { + switch self { + case .ai: + return .primaryWhite + case .beige: + return .primary + case .blue: + return .primaryWhite + case .black: + return .primaryWhite + case .white: + return .primary + } + } } +} - static func blue( +extension HorizonUI.ButtonStyles { + public static func primary( + _ type: HorizonUI.ButtonStyles.ButtonType, isSmall: Bool = false, fillsWidth: Bool = false, leading: Image? = nil, trailing: Image? = nil ) -> HorizonUI.ButtonStyles { .init( - background: Color.huiColors.surface.institution, - foreground: Color.huiColors.text.surfaceColored, + backgroundColor: type.background, + foregroundColor: type.foregroundColor, isSmall: isSmall, fillsWidth: fillsWidth, leading: leading, @@ -141,24 +208,39 @@ extension HorizonUI.ButtonStyles { ) } - static func white( + public static func icon( + _ type: HorizonUI.ButtonStyles.ButtonType, isSmall: Bool = false, - fillsWidth: Bool = false, - leading: Image? = nil, - trailing: Image? = nil + badgeNumber: String? = nil, + icon: Image? = nil ) -> HorizonUI.ButtonStyles { .init( - background: Color.huiColors.surface.pageSecondary, - foreground: Color.huiColors.text.title, + backgroundColor: type.background, + foregroundColor: type.foregroundColor, + badgeStyle: type.badgeStyle, isSmall: isSmall, - fillsWidth: fillsWidth, - leading: leading, - trailing: trailing + icon: icon ?? (type == .ai ? HorizonUI.icons.ai : HorizonUI.icons.add), + badgeNumber: badgeNumber ) } } -extension HorizonUI.Colors.Surface { - var aiGradientStart: Color { Color(hexString: "#09508C") } - var aiGradientEnd: Color { Color(hexString: "#02672D") } +#Preview(traits: .sizeThatFitsLayout) { + NavigationStack { + ScrollView { + VStack(spacing: 16) { + ForEach(HorizonUI.ButtonStyles.ButtonType.allCases, id: \.self) { type in + HStack { + Button("AI Icon Button") {} + .buttonStyle(HorizonUI.ButtonStyles.icon(type, badgeNumber: "99")) + .disabled(true) + Button("AI Button") {} + .buttonStyle(HorizonUI.ButtonStyles.primary(type)) + } + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + .background(Color(red: 88 / 100, green: 88 / 100, blue: 88 / 100)) + } }