diff --git a/BezierSwift/Sources/Components/Banners/BezierCardBanner.swift b/BezierSwift/Sources/Components/Banners/BezierCardBanner.swift new file mode 100644 index 00000000..1570807e --- /dev/null +++ b/BezierSwift/Sources/Components/Banners/BezierCardBanner.swift @@ -0,0 +1,120 @@ +// +// BezierCardBanner.swift +// +// +// Created by Tom on 9/19/24. +// + +import SwiftUI + +// - MARK: Metric +private enum Metric { + static let padding: CGFloat = 8 + static let cornerRadius: CGFloat = 14 + + static let contentHStackSpacing: CGFloat = 6 + static let contentHorizontalPadding: CGFloat = 6 + + static let iconVerticalPadding: CGFloat = 6 + static let iconLegnth: CGFloat = 20 + + static let descriptionVerticalPadding: CGFloat = 5 + + static let buttonIconLenght: CGFloat = 20 + static let buttonIconPadding: CGFloat = 6 +} + +// - MARK: BezierCardBanner +public struct BezierCardBanner: View { + public typealias Action = () -> Void + + // MARK: ActionType + public enum ActionType { + case closeButton(Action) + case chevronIcon(Action) + } + + // MARK: Properties + private let icon: BezierIcon + private let iconColor: BezierColor + private let description: String + private let actionType: ActionType? + + // MARK: Initializer + /// - Parameters: + /// - icon: 사용자에게 알림의 종류를 한눈에 알려주는 Icon 입니다. + /// - iconColor: Icon Color를 변경할 수 있습니다. 기본 값은 `fgBlackDark` 입니다. + /// - description: 배너의 본문 내용을 전달하는 역할을 합니다. 최대 4줄을 권장합니다. + /// - actionType: 배너 우측에 배치될 액션 버튼입니다. `closeButton` 또는 `chevronIcon`을 선택하여 사용자 인터랙션을 제공할 수 있으며, 이 값이 `nil`인 경우 버튼이 표시되지 않습니다. + public init( + icon: BezierIcon, + iconColor: BezierColor = .fgBlackDark, + description: String, + actionType: ActionType? = nil + ) { + self.icon = icon + self.iconColor = iconColor + self.description = description + self.actionType = actionType + } + + // MARK: Body + public var body: some View { + HStack(alignment: .top, spacing: .zero) { + HStack(alignment: .top, spacing: Metric.contentHStackSpacing) { + self.icon.image + .frame(length: Metric.iconLegnth) + .padding(.top, Metric.iconVerticalPadding) + .foregroundColor(self.iconColor.color) + + Text(self.description) + .applyBezierFontStyle(.body2Regular, bezierColor: .fgBlackDarkest) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.vertical, Metric.descriptionVerticalPadding) + } + .padding(.horizontal, Metric.contentHorizontalPadding) + + self.actionButton + } + .padding(Metric.padding) + .background( + RoundedRectangle(cornerRadius: Metric.cornerRadius, style: .circular) + .fill(BezierColor.bgGreyLighter.color) + ) + .applyBezierShadow(.shadow2) + } +} + +// - MARK: Style +extension BezierCardBanner { + @ViewBuilder + private var actionButton: some View { + switch self.actionType { + case .chevronIcon(let action): + Button { + action() + } label: { + BezierIcon.chevronSmallRight.image + .foregroundColor(BezierColor.fgBlackDark.color) + .frame(length: Metric.buttonIconLenght) + .padding(Metric.buttonIconPadding) + .contentShape(Rectangle()) + } + case .closeButton(let action): + Button { + action() + } label: { + BezierIcon.cancelSmall.image + .foregroundColor(BezierColor.fgBlackDark.color) + .frame(length: Metric.buttonIconLenght) + .padding(Metric.buttonIconPadding) + .contentShape(Rectangle()) + } + case .none: EmptyView() + } + } +} + +#Preview { + BezierCardBanner(icon: .info, description: "description") +} diff --git a/BezierSwift/Sources/Components/Banners/BezierFloatingBanner.swift b/BezierSwift/Sources/Components/Banners/BezierFloatingBanner.swift new file mode 100644 index 00000000..6b2a3152 --- /dev/null +++ b/BezierSwift/Sources/Components/Banners/BezierFloatingBanner.swift @@ -0,0 +1,120 @@ +// +// BezierFloatingBanner.swift +// +// +// Created by Tom on 9/18/24. +// + +import SwiftUI + +// - MARK: Metric +private enum Metric { + static let padding: CGFloat = 8 + static let cornerRadius: CGFloat = 14 + + static let contentHStackSpacing: CGFloat = 6 + static let contentHorizontalPadding: CGFloat = 6 + + static let iconVerticalPadding: CGFloat = 6 + static let iconLegnth: CGFloat = 20 + + static let descriptionVerticalPadding: CGFloat = 5 + + static let buttonIconLenght: CGFloat = 20 + static let buttonIconPadding: CGFloat = 6 +} + +// - MARK: BezierFloatingBanner +public struct BezierFloatingBanner: View { + public typealias Action = () -> Void + + // MARK: ActionType + public enum ActionType { + case closeButton(Action) + case chevronIcon(Action) + } + + // MARK: Properties + private let icon: BezierIcon + private let iconColor: BezierColor + private let description: String + private let actionType: ActionType? + + // MARK: Initializer + /// - Parameters: + /// - icon: 사용자에게 알림의 종류를 한눈에 알려주는 Icon 입니다. + /// - iconColor: Icon Color를 변경할 수 있습니다. 기본 값은 `fgBlackDark` 입니다. + /// - description: 배너의 본문 내용을 전달하는 역할을 합니다. 최대 4줄을 권장합니다. + /// - actionType: 배너 우측에 배치될 액션 버튼입니다. `closeButton` 또는 `chevronIcon`을 선택하여 사용자 인터랙션을 제공할 수 있으며, 이 값이 `nil`인 경우 버튼이 표시되지 않습니다. + public init( + icon: BezierIcon, + iconColor: BezierColor = .fgBlackDark, + description: String, + actionType: ActionType? = nil + ) { + self.icon = icon + self.iconColor = iconColor + self.description = description + self.actionType = actionType + } + + // MARK: Body + public var body: some View { + HStack(alignment: .top, spacing: .zero) { + HStack(alignment: .top, spacing: Metric.contentHStackSpacing) { + self.icon.image + .frame(length: Metric.iconLegnth) + .padding(.top, Metric.iconVerticalPadding) + .foregroundColor(self.iconColor.color) + + Text(self.description) + .applyBezierFontStyle(.body2Regular, bezierColor: .fgBlackDarkest) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.vertical, Metric.descriptionVerticalPadding) + } + .padding(.horizontal, Metric.contentHorizontalPadding) + + self.actionButton + } + .padding(Metric.padding) + .background( + RoundedRectangle(cornerRadius: Metric.cornerRadius, style: .circular) + .fill(BezierColor.bgGreyLighter.color) + ) + .applyBezierShadow(.shadow3) + } +} + +// - MARK: Style +extension BezierFloatingBanner { + @ViewBuilder + private var actionButton: some View { + switch self.actionType { + case .chevronIcon(let action): + Button { + action() + } label: { + BezierIcon.chevronSmallRight.image + .foregroundColor(BezierColor.fgBlackDark.color) + .frame(length: Metric.buttonIconLenght) + .padding(Metric.buttonIconPadding) + .contentShape(Rectangle()) + } + case .closeButton(let action): + Button { + action() + } label: { + BezierIcon.cancelSmall.image + .foregroundColor(BezierColor.fgBlackDark.color) + .frame(length: Metric.buttonIconLenght) + .padding(Metric.buttonIconPadding) + .contentShape(Rectangle()) + } + case .none: EmptyView() + } + } +} + +#Preview { + BezierFloatingBanner(icon: .info, description: "description") +} diff --git a/BezierSwift/Sources/Components/Banners/BezierInnerBanner.swift b/BezierSwift/Sources/Components/Banners/BezierInnerBanner.swift new file mode 100644 index 00000000..a4d275ae --- /dev/null +++ b/BezierSwift/Sources/Components/Banners/BezierInnerBanner.swift @@ -0,0 +1,168 @@ +// +// BezierInnerBanner.swift +// +// +// Created by Tom on 9/17/24. +// + +import SwiftUI + +// - MARK: Metric +private enum Metric { + static let hStackSpacing: CGFloat = 6 + static let leadingPadding: CGFloat = 12 + static let trailingPadding: CGFloat = 6 + static let verticalPadding: CGFloat = 8 + static let cornerRadius: CGFloat = 14 + + static let iconTopPadding: CGFloat = 6 + static let iconLegnth: CGFloat = 20 + + static let contentVStackSpacing: CGFloat = 4 + static let contentVerticalPadding: CGFloat = 5 + static let contentTrailingPadding: CGFloat = 6 + + static let buttonIconLenght: CGFloat = 20 + static let buttonIconPadding: CGFloat = 6 +} + +// - MARK: BezierInnerBanner +public struct BezierInnerBanner: View { + public typealias Action = () -> Void + + // MARK: Semantic + public enum Semantic { + case info + case tips + case success + case warning + case error + } + + // MARK: ActionType + public enum ActionType { + case closeButton(Action) + case chevronIcon(Action) + } + + // MARK: Properties + private let sematic: Semantic + private let icon: BezierIcon + private let title: String? + private let description: String + private let actionType: ActionType? + + // MARK: Initializer + /// - Parameters: + /// - sematic: 배너의 사용 의도에 따라 `info`, `tips`, `success`, `warning`, `error` 중 하나를 선택할 수 있습니다. + /// - icon: 사용자에게 알림의 종류를 한눈에 알려주는 Icon 입니다. 색상은 변경이 불가능합니다. + /// - title: 배너의 제목 텍스트로, 선택적으로 사용할 수 있습니다. `nil` 값을 전달하면 제목은 표시되지 않습니다. 최대 2줄을 권장합니다. + /// - description: 배너의 설명 텍스트로, 반드시 전달해야 하는 필수 값입니다. 배너의 본문 내용을 전달하는 역할을 합니다. 최대 4줄을 권장합니다. + /// - actionType: 배너 우측에 배치될 액션 버튼입니다. `closeButton` 또는 `chevronIcon`을 선택하여 사용자 인터랙션을 제공할 수 있으며, 이 값이 `nil`인 경우 버튼이 표시되지 않습니다. + public init( + sematic: Semantic = .info, + icon: BezierIcon, + title: String? = nil, + description: String, + actionType: ActionType? = nil + ) { + self.sematic = sematic + self.icon = icon + self.title = title + self.description = description + self.actionType = actionType + } + + // MARK: Body + public var body: some View { + HStack(alignment: .top, spacing: Metric.hStackSpacing) { + self.icon.image + .frame(length: Metric.iconLegnth) + .padding(.top, Metric.iconTopPadding) + .foregroundColor(self.foregroundColor.color) + + VStack(alignment: .leading, spacing: Metric.contentVStackSpacing) { + if let title { + Text(title) + .applyBezierFontStyle(.body2SemiBold, bezierColor: self.foregroundColor) + } + Text(self.description) + .applyBezierFontStyle(.body2Regular, bezierColor: self.foregroundColor) + } + .padding(.vertical, Metric.contentVerticalPadding) + .padding(.trailing, Metric.contentTrailingPadding) + .frame(maxWidth: .infinity, alignment: .leading) + + self.actionButton + } + .padding(.vertical, Metric.verticalPadding) + .padding(.leading, Metric.leadingPadding) + .padding(.trailing, Metric.trailingPadding) + .background( + RoundedRectangle(cornerRadius: Metric.cornerRadius, style: .circular) + .fill(self.backgroundColor.color) + ) + } +} + +// - MARK: Style +extension BezierInnerBanner { + private var backgroundColor: BezierColor { + switch self.sematic { + case .info: .bgBlackLightest + case .tips: .accentBgLightest + case .success: .successBgLightest + case .warning: .warningBgLightest + case .error: .criticalBgLightest + } + } + + private var foregroundColor: BezierColor { + switch self.sematic { + case .info: .fgBlackDarker + case .tips: .accentFgDark + case .success: .successFgDark + case .warning: .warningFgDark + case .error: .criticalFgDark + } + } + + @ViewBuilder + private var actionButton: some View { + switch self.actionType { + case .chevronIcon(let action): + Button { + action() + } label: { + BezierIcon.chevronSmallRight.image + .foregroundColor(BezierColor.fgBlackDark.color) + .frame(length: Metric.buttonIconLenght) + .padding(Metric.buttonIconPadding) + .contentShape(Rectangle()) + } + case .closeButton(let action): + Button { + action() + } label: { + BezierIcon.cancelSmall.image + .foregroundColor(BezierColor.fgBlackDark.color) + .frame(length: Metric.buttonIconLenght) + .padding(Metric.buttonIconPadding) + .contentShape(Rectangle()) + } + case .none: EmptyView() + } + } +} + +#Preview { + BezierInnerBanner( + sematic: .error, + icon: .info, + title: "Title Text (optional)", + description: "description", + actionType: .closeButton { + print("BezierInnerBanner") + } + ) +} diff --git a/Examples/SwiftUIExample/SwiftUIExample.xcodeproj/project.pbxproj b/Examples/SwiftUIExample/SwiftUIExample.xcodeproj/project.pbxproj index 136fe048..c062b2dc 100644 --- a/Examples/SwiftUIExample/SwiftUIExample.xcodeproj/project.pbxproj +++ b/Examples/SwiftUIExample/SwiftUIExample.xcodeproj/project.pbxproj @@ -20,6 +20,9 @@ E28D19612C365557009B34A2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28D195F2C365557009B34A2 /* SceneDelegate.swift */; }; E2A392B72C958A540015FA6F /* BezierAvatarExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A392B62C958A540015FA6F /* BezierAvatarExample.swift */; }; E2A392B92C98169D0015FA6F /* BezierAvatarGroupExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A392B82C98169D0015FA6F /* BezierAvatarGroupExample.swift */; }; + E2E5FAC92C9B265D00533C4A /* BezierInnerBannerExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E5FAC82C9B265D00533C4A /* BezierInnerBannerExample.swift */; }; + E2E5FACB2C9B266700533C4A /* BezierFloatingBannerExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E5FACA2C9B266700533C4A /* BezierFloatingBannerExample.swift */; }; + E2E5FACD2C9B267000533C4A /* BezierCardBannerExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E5FACC2C9B267000533C4A /* BezierCardBannerExample.swift */; }; E2EF05FC2C5A4CDC00C57676 /* BezierLoaderExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EF05FB2C5A4CDC00C57676 /* BezierLoaderExample.swift */; }; /* End PBXBuildFile section */ @@ -37,6 +40,9 @@ E28D195F2C365557009B34A2 /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; E2A392B62C958A540015FA6F /* BezierAvatarExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BezierAvatarExample.swift; sourceTree = ""; }; E2A392B82C98169D0015FA6F /* BezierAvatarGroupExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BezierAvatarGroupExample.swift; sourceTree = ""; }; + E2E5FAC82C9B265D00533C4A /* BezierInnerBannerExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BezierInnerBannerExample.swift; sourceTree = ""; }; + E2E5FACA2C9B266700533C4A /* BezierFloatingBannerExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BezierFloatingBannerExample.swift; sourceTree = ""; }; + E2E5FACC2C9B267000533C4A /* BezierCardBannerExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BezierCardBannerExample.swift; sourceTree = ""; }; E2EF05FB2C5A4CDC00C57676 /* BezierLoaderExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BezierLoaderExample.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -66,6 +72,7 @@ E258E6BA2C3D8AAC00F69680 /* Examples */ = { isa = PBXGroup; children = ( + E2E5FAC72C9B263D00533C4A /* Banner */, E220176E2C75D6E500578E64 /* Buttons */, E2EF05FB2C5A4CDC00C57676 /* BezierLoaderExample.swift */, E2A392B62C958A540015FA6F /* BezierAvatarExample.swift */, @@ -120,6 +127,16 @@ name = Frameworks; sourceTree = ""; }; + E2E5FAC72C9B263D00533C4A /* Banner */ = { + isa = PBXGroup; + children = ( + E2E5FAC82C9B265D00533C4A /* BezierInnerBannerExample.swift */, + E2E5FACA2C9B266700533C4A /* BezierFloatingBannerExample.swift */, + E2E5FACC2C9B267000533C4A /* BezierCardBannerExample.swift */, + ); + path = Banner; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -198,11 +215,14 @@ E2A392B92C98169D0015FA6F /* BezierAvatarGroupExample.swift in Sources */, E22017722C75E0A400578E64 /* BezierFloatingIconButtonExample.swift in Sources */, E28D19602C365557009B34A2 /* AppDelegate.swift in Sources */, + E2E5FACD2C9B267000533C4A /* BezierCardBannerExample.swift in Sources */, E28212342A4B32F700018327 /* ContentView.swift in Sources */, E28212322A4B32F700018327 /* SwiftUIExampleApp.swift in Sources */, E28D19612C365557009B34A2 /* SceneDelegate.swift in Sources */, + E2E5FAC92C9B265D00533C4A /* BezierInnerBannerExample.swift in Sources */, E22017702C75E09900578E64 /* BezierFloatingButtonExample.swift in Sources */, E2A392B72C958A540015FA6F /* BezierAvatarExample.swift in Sources */, + E2E5FACB2C9B266700533C4A /* BezierFloatingBannerExample.swift in Sources */, E220176D2C75D67900578E64 /* BezierIconButtonExample.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Examples/SwiftUIExample/SwiftUIExample/ContentView.swift b/Examples/SwiftUIExample/SwiftUIExample/ContentView.swift index 27e38ef8..e92b7a88 100644 --- a/Examples/SwiftUIExample/SwiftUIExample/ContentView.swift +++ b/Examples/SwiftUIExample/SwiftUIExample/ContentView.swift @@ -23,6 +23,25 @@ struct ContentView: View { } header: { Text("Status") } + Section { + NavigationLink { + BezierInnerBannerExample() + } label: { + Text("InnerBanner") + } + NavigationLink { + BezierFloatingBannerExample() + } label: { + Text("FloatingBanner") + } + NavigationLink { + BezierCardBannerExample() + } label: { + Text("CardBanner") + } + } header: { + Text("Feedback") + } Section { NavigationLink { BezierAvatarExample() diff --git a/Examples/SwiftUIExample/SwiftUIExample/Examples/Banner/BezierCardBannerExample.swift b/Examples/SwiftUIExample/SwiftUIExample/Examples/Banner/BezierCardBannerExample.swift new file mode 100644 index 00000000..31d28460 --- /dev/null +++ b/Examples/SwiftUIExample/SwiftUIExample/Examples/Banner/BezierCardBannerExample.swift @@ -0,0 +1,29 @@ +// +// BezierCardBannerExample.swift +// SwiftUIExample +// +// Created by Tom on 9/19/24. +// + +import SwiftUI + +import BezierSwift + +struct BezierCardBannerExample: View { + var body: some View { + VStack { + BezierCardBanner( + icon: .info, + description: "description", + actionType: .chevronIcon { + print("BezierFloatingBanner") + } + ) + } + .padding(.horizontal, 20) + } +} + +#Preview { + BezierCardBannerExample() +} diff --git a/Examples/SwiftUIExample/SwiftUIExample/Examples/Banner/BezierFloatingBannerExample.swift b/Examples/SwiftUIExample/SwiftUIExample/Examples/Banner/BezierFloatingBannerExample.swift new file mode 100644 index 00000000..21c425c3 --- /dev/null +++ b/Examples/SwiftUIExample/SwiftUIExample/Examples/Banner/BezierFloatingBannerExample.swift @@ -0,0 +1,29 @@ +// +// BezierFloatingBannerExample.swift +// SwiftUIExample +// +// Created by Tom on 9/19/24. +// + +import SwiftUI + +import BezierSwift + +struct BezierFloatingBannerExample: View { + var body: some View { + VStack { + BezierFloatingBanner( + icon: .info, + description: "description", + actionType: .chevronIcon { + print("BezierFloatingBanner") + } + ) + } + .padding(.horizontal, 20) + } +} + +#Preview { + BezierFloatingBannerExample() +} diff --git a/Examples/SwiftUIExample/SwiftUIExample/Examples/Banner/BezierInnerBannerExample.swift b/Examples/SwiftUIExample/SwiftUIExample/Examples/Banner/BezierInnerBannerExample.swift new file mode 100644 index 00000000..1884a656 --- /dev/null +++ b/Examples/SwiftUIExample/SwiftUIExample/Examples/Banner/BezierInnerBannerExample.swift @@ -0,0 +1,31 @@ +// +// BezierInnerBannerExample.swift +// SwiftUIExample +// +// Created by Tom on 9/19/24. +// + +import SwiftUI + +import BezierSwift + +struct BezierInnerBannerExample: View { + var body: some View { + VStack { + BezierInnerBanner( + sematic: .error, + icon: .android, + title: "Title text (optional)", + description: "description", + actionType: .chevronIcon { + print("BezierInnerBanner") + } + ) + } + .padding(.horizontal, 20) + } +} + +#Preview { + BezierInnerBannerExample() +}