diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/PreviewPickers.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/PreviewPickers.swift index 621847a1..fc20a585 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/PreviewPickers.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/PreviewPickers.swift @@ -40,15 +40,14 @@ struct ComponentColorPicker: View { var body: some View { Picker("Color", selection: self.$selection) { Text("Primary").tag(ComponentColor.primary) - Text("Secondary").tag(ComponentColor.secondary) Text("Accent").tag(ComponentColor.accent) Text("Success").tag(ComponentColor.success) Text("Warning").tag(ComponentColor.warning) Text("Danger").tag(ComponentColor.danger) Text("Custom").tag(ComponentColor( main: .universal(.uiColor(.systemPurple)), - contrast: .universal(.uiColor(.systemYellow))) - ) + contrast: .universal(.uiColor(.systemYellow)) + )) } } } @@ -62,15 +61,14 @@ struct ComponentOptionalColorPicker: View { Picker("Color", selection: self.$selection) { Text("Default").tag(Optional.none) Text("Primary").tag(ComponentColor.primary) - Text("Secondary").tag(ComponentColor.secondary) Text("Accent").tag(ComponentColor.accent) Text("Success").tag(ComponentColor.success) Text("Warning").tag(ComponentColor.warning) Text("Danger").tag(ComponentColor.danger) Text("Custom").tag(ComponentColor( main: .universal(.uiColor(.systemPurple)), - contrast: .universal(.uiColor(.systemYellow))) - ) + contrast: .universal(.uiColor(.systemYellow)) + )) } } } @@ -93,18 +91,84 @@ struct CornerRadiusPicker: View { } } -// MARK: - FontPicker +// MARK: - FontPickers -struct FontPicker: View { +struct BodyFontPicker: View { + let title: String + @Binding var selection: UniversalFont? + + init(title: String = "Font", selection: Binding) { + self.title = title + self._selection = selection + } + + var body: some View { + Picker(self.title, selection: self.$selection) { + Text("Default").tag(Optional.none) + Text("Small").tag(UniversalFont.smBody) + Text("Medium").tag(UniversalFont.mdBody) + Text("Large").tag(UniversalFont.lgBody) + Text("Custom: system semibold of size 16").tag(UniversalFont.system(size: 16, weight: .semibold)) + } + } +} + +struct ButtonFontPicker: View { + let title: String + @Binding var selection: UniversalFont? + + init(title: String = "Font", selection: Binding) { + self.title = title + self._selection = selection + } + + var body: some View { + Picker(self.title, selection: self.$selection) { + Text("Default").tag(Optional.none) + Text("Small").tag(UniversalFont.smButton) + Text("Medium").tag(UniversalFont.mdButton) + Text("Large").tag(UniversalFont.lgButton) + Text("Custom: system bold of size 16").tag(UniversalFont.system(size: 16, weight: .bold)) + } + } +} + +struct HeadlineFontPicker: View { + let title: String @Binding var selection: UniversalFont? + + init(title: String = "Font", selection: Binding) { + self.title = title + self._selection = selection + } var body: some View { - Picker("Font", selection: self.$selection) { + Picker(self.title, selection: self.$selection) { + Text("Default").tag(Optional.none) + Text("Small").tag(UniversalFont.smHeadline) + Text("Medium").tag(UniversalFont.mdHeadline) + Text("Large").tag(UniversalFont.lgHeadline) + Text("Custom: system bold of size 20").tag(UniversalFont.system(size: 20, weight: .bold)) + } + } +} + +struct CaptionFontPicker: View { + let title: String + @Binding var selection: UniversalFont? + + init(title: String = "Font", selection: Binding) { + self.title = title + self._selection = selection + } + + var body: some View { + Picker(self.title, selection: self.$selection) { Text("Default").tag(Optional.none) - Text("Small").tag(UniversalFont.Component.small) - Text("Medium").tag(UniversalFont.Component.medium) - Text("Large").tag(UniversalFont.Component.large) - Text("Custom: system bold of size 18").tag(UniversalFont.system(size: 18, weight: .bold)) + Text("Small").tag(UniversalFont.smCaption) + Text("Medium").tag(UniversalFont.mdCaption) + Text("Large").tag(UniversalFont.lgCaption) + Text("Custom: system semibold of size 12").tag(UniversalFont.system(size: 12, weight: .semibold)) } } } @@ -173,7 +237,6 @@ struct UniversalColorPicker: View { var body: some View { Picker(self.title, selection: self.$selection) { Text("Primary").tag(UniversalColor.primary) - Text("Secondary").tag(UniversalColor.secondary) Text("Accent").tag(UniversalColor.accent) Text("Success").tag(UniversalColor.success) Text("Warning").tag(UniversalColor.warning) diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/PreviewWrapper.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/PreviewWrapper.swift index 06497702..17cee0bb 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/PreviewWrapper.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/PreviewWrapper.swift @@ -21,8 +21,8 @@ struct PreviewWrapper: View { LinearGradient( gradient: Gradient( colors: [ - Palette.Brand.blue.color(for: self.colorScheme), - Palette.Brand.purple.color(for: self.colorScheme) + UniversalColor.blue.color(for: self.colorScheme), + UniversalColor.purple.color(for: self.colorScheme), ] ), startPoint: .topLeading, @@ -42,3 +42,16 @@ struct PreviewWrapper: View { .padding(.horizontal) } } + +// MARK: - Colors + +extension UniversalColor { + fileprivate static let blue: Self = .themed( + light: .hex("#3684F8"), + dark: .hex("#0058DB") + ) + fileprivate static let purple: Self = .themed( + light: .hex("#A920FD"), + dark: .hex("#7800C1") + ) +} diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift index 8ab4d830..b4897535 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift @@ -18,11 +18,11 @@ struct ButtonPreview: View { } Form { AnimationScalePicker(selection: self.$model.animationScale) - ComponentColorPicker(selection: self.$model.color) + ComponentOptionalColorPicker(selection: self.$model.color) CornerRadiusPicker(selection: self.$model.cornerRadius) { Text("Custom: 20px").tag(ComponentRadius.custom(20)) } - FontPicker(selection: self.$model.font) + ButtonFontPicker(selection: self.$model.font) Toggle("Enabled", isOn: self.$model.isEnabled) Toggle("Full Width", isOn: self.$model.isFullWidth) SizePicker(selection: self.$model.size) @@ -32,7 +32,6 @@ struct ButtonPreview: View { Text("Bordered with small border").tag(ButtonVM.Style.bordered(.small)) Text("Bordered with medium border").tag(ButtonVM.Style.bordered(.medium)) Text("Bordered with large border").tag(ButtonVM.Style.bordered(.large)) - Text("Bordered with custom border: 6px").tag(ButtonVM.Style.bordered(.custom(6))) } } } diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CheckboxPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CheckboxPreview.swift index 05e7ead2..8e4bff6d 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CheckboxPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CheckboxPreview.swift @@ -34,7 +34,7 @@ struct CheckboxPreview: View { CornerRadiusPicker(selection: self.$model.cornerRadius) { Text("Custom: 2px").tag(ComponentRadius.custom(2)) } - FontPicker(selection: self.$model.font) + BodyFontPicker(selection: self.$model.font) Toggle("Enabled", isOn: self.$model.isEnabled) SizePicker(selection: self.$model.size) } diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CountdownPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CountdownPreview.swift index b611a8b9..d95cd978 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CountdownPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CountdownPreview.swift @@ -16,7 +16,6 @@ struct CountdownPreview: View { } Form { ComponentOptionalColorPicker(selection: self.$model.color) - FontPicker(selection: self.$model.font) Picker("Locale", selection: self.$model.locale) { Text("Current").tag(Locale.current) Text("EN").tag(Locale(identifier: "en")) @@ -30,6 +29,8 @@ struct CountdownPreview: View { Text("HI").tag(Locale(identifier: "hi")) Text("PT").tag(Locale(identifier: "pt")) } + HeadlineFontPicker(title: "Main Font", selection: self.$model.mainFont) + CaptionFontPicker(title: "Secondary Font", selection: self.$model.secondaryFont) SizePicker(selection: self.$model.size) Picker("Style", selection: self.$model.style) { Text("Plain").tag(CountdownVM.Style.plain) diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/DividerPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/DividerPreview.swift index a81808dc..971e01cd 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/DividerPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/DividerPreview.swift @@ -15,21 +15,23 @@ struct DividerPreview: View { SUDivider(model: self.model) } Form { + Picker("Color", selection: self.$model.color) { + Text("Default").tag(Optional.none) + Text("Primary").tag(ComponentColor.primary) + Text("Accent").tag(ComponentColor.accent) + Text("Success").tag(ComponentColor.success) + Text("Warning").tag(ComponentColor.warning) + Text("Danger").tag(ComponentColor.danger) + Text("Custom").tag(ComponentColor( + main: .universal(.uiColor(.systemPurple)), + contrast: .universal(.uiColor(.systemYellow)) + )) + } Picker("Orientation", selection: self.$model.orientation) { Text("Horizontal").tag(DividerVM.Orientation.horizontal) Text("Vertical").tag(DividerVM.Orientation.vertical) } SizePicker(selection: self.$model.size) - Picker("Color", selection: self.$model.color) { - Text("Default").tag(Palette.Base.divider) - Text("Primary").tag(UniversalColor.primary) - Text("Secondary").tag(UniversalColor.secondary) - Text("Accent").tag(UniversalColor.accent) - Text("Success").tag(UniversalColor.success) - Text("Warning").tag(UniversalColor.warning) - Text("Danger").tag(UniversalColor.danger) - Text("Custom").tag(UniversalColor.universal(.uiColor(.systemPurple))) - } } } } diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/InputFieldPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/InputFieldPreview.swift index 1034f1d3..5adc97d2 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/InputFieldPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/InputFieldPreview.swift @@ -17,6 +17,7 @@ struct InputFieldPreview: View { self.inputField .preview .onAppear { + self.inputField.text = "" self.inputField.model = Self.initialModel } .onChange(of: self.model) { newValue in @@ -38,7 +39,7 @@ struct InputFieldPreview: View { Text("Custom: 20px").tag(ComponentRadius.custom(20)) } Toggle("Enabled", isOn: self.$model.isEnabled) - FontPicker(selection: self.$model.font) + BodyFontPicker(selection: self.$model.font) KeyboardTypePicker(selection: self.$model.keyboardType) Toggle("Placeholder", isOn: .init( get: { diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/RadioGroupPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/RadioGroupPreview.swift index 2c7b8b57..3fdfc9e5 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/RadioGroupPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/RadioGroupPreview.swift @@ -33,7 +33,7 @@ struct RadioGroupPreview: View { AnimationScalePicker(selection: self.$model.animationScale) UniversalColorPicker(title: "Color", selection: self.$model.color) Toggle("Enabled", isOn: self.$model.isEnabled) - FontPicker(selection: self.$model.font) + BodyFontPicker(selection: self.$model.font) SizePicker(selection: self.$model.size) Picker("Spacing", selection: self.$model.spacing) { Text("8px").tag(CGFloat(8)) diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/SegmentedControlPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/SegmentedControlPreview.swift index 1b33aea2..e144f116 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/SegmentedControlPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/SegmentedControlPreview.swift @@ -46,7 +46,7 @@ struct SegmentedControlPreview: View { Text("Custom: 4px").tag(ComponentRadius.custom(4)) } Toggle("Enabled", isOn: self.$model.isEnabled) - FontPicker(selection: self.$model.font) + BodyFontPicker(selection: self.$model.font) Toggle("Full Width", isOn: self.$model.isFullWidth) SizePicker(selection: self.$model.size) } diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/TextInputPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/TextInputPreview.swift index fed21f67..2dd55593 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/TextInputPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/TextInputPreview.swift @@ -17,6 +17,7 @@ struct TextInputPreviewPreview: View { self.textInput .preview .onAppear { + self.textInput.text = "" self.textInput.model = Self.initialModel } .onChange(of: self.model) { newValue in @@ -38,7 +39,7 @@ struct TextInputPreviewPreview: View { Text("Custom: 20px").tag(ComponentRadius.custom(20)) } Toggle("Enabled", isOn: self.$model.isEnabled) - FontPicker(selection: self.$model.font) + BodyFontPicker(selection: self.$model.font) KeyboardTypePicker(selection: self.$model.keyboardType) Picker("Max Rows", selection: self.$model.maxRows) { Text("2 Rows").tag(2) diff --git a/Examples/DemosApp/DemosApp/Demos/Login/SwiftUILogin.swift b/Examples/DemosApp/DemosApp/Demos/Login/SwiftUILogin.swift index f04208c9..cbbf57f1 100644 --- a/Examples/DemosApp/DemosApp/Demos/Login/SwiftUILogin.swift +++ b/Examples/DemosApp/DemosApp/Demos/Login/SwiftUILogin.swift @@ -38,7 +38,7 @@ struct SwiftUILogin: View { var body: some View { ZStack { - Palette.Base.background.color(for: self.colorScheme) + UniversalColor.background.color(for: self.colorScheme) ScrollView { VStack(spacing: 20) { @@ -62,7 +62,7 @@ struct SwiftUILogin: View { ? "Welcome back" : "Create an account" ) - .font(.system(size: 30, weight: .bold)) + .font(UniversalFont.lgHeadline.font) .padding(.vertical, 30) if self.selectedPage == .signUp { @@ -129,6 +129,7 @@ struct SwiftUILogin: View { model: .init { $0.title = "Continue" $0.isFullWidth = true + $0.color = .primary $0.isEnabled = self.isButtonEnabled }, action: { diff --git a/Examples/DemosApp/DemosApp/Demos/Login/UIKitLogin.swift b/Examples/DemosApp/DemosApp/Demos/Login/UIKitLogin.swift index 8630464a..8b4d604c 100644 --- a/Examples/DemosApp/DemosApp/Demos/Login/UIKitLogin.swift +++ b/Examples/DemosApp/DemosApp/Demos/Login/UIKitLogin.swift @@ -36,7 +36,7 @@ final class UIKitLogin: UIViewController { ) private let titleLabel: UILabel = { let label = UILabel() - label.font = .systemFont(ofSize: 30, weight: .bold) + label.font = UniversalFont.lgHeadline.uiFont return label }() private let nameInput = UKInputField( @@ -74,6 +74,7 @@ final class UIKitLogin: UIViewController { model: .init { $0.title = "Continue" $0.isFullWidth = true + $0.color = .primary } ) private let loader = UKLoading() @@ -157,7 +158,7 @@ final class UIKitLogin: UIViewController { } private func style() { - self.scrollView.backgroundColor = Palette.Base.background.uiColor + self.scrollView.backgroundColor = UniversalColor.background.uiColor self.stackView.setCustomSpacing(50, after: self.pageControl) self.stackView.setCustomSpacing(50, after: self.titleLabel) diff --git a/Examples/DemosApp/DemosApp/Helpers/Palette+Colors.swift b/Examples/DemosApp/DemosApp/Helpers/Palette+Colors.swift deleted file mode 100644 index b6010622..00000000 --- a/Examples/DemosApp/DemosApp/Helpers/Palette+Colors.swift +++ /dev/null @@ -1,14 +0,0 @@ -import ComponentsKit - -extension Palette { - enum Brand { - static let blue: UniversalColor = .themed( - light: .hex("#3684F8"), - dark: .hex("#0058DB") - ) - static let purple: UniversalColor = .themed( - light: .hex("#A920FD"), - dark: .hex("#7800C1") - ) - } -} diff --git a/README.md b/README.md index ec3fcb90..a8bd9b89 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ inputField.resignFirstResponder() **Config** -The library comes with predefined sizes and colors, but you can change these values to customize the appearance of your app. To do this, alter the config: +The library comes with predefined fonts, sizes and colors, but you can change these values to customize the appearance of your app. To do this, alter the config: ```swift ComponentsKitConfig.shared.update { @@ -89,7 +89,7 @@ ComponentsKitConfig.shared.update { $0.colors.primary = ... // Update layout - $0.layout.componentFont.medium = ... + $0.layout.componentRadius.medium = ... } ``` @@ -120,47 +120,43 @@ All colors from the config can be used within the app. For example: ```swift // in UIKit -view.backgroundColor = Palette.Base.background.uiColor +view.backgroundColor = UniversalColor.background.uiColor // in SwiftUI @Environment(\.colorScheme) var colorScheme -Palette.Base.background.color(for: colorScheme) +UniversalColor.background.color(for: colorScheme) ``` -If you want to use additional colors that are not included in the config, you can extend `Palette`: +If you want to use additional colors that are not included in the config, you can extend `UniversalColor`: ```swift -extension Palette { - enum MyColors { - static var special: UniversalColor { - if selectedTheme == .halloween { - return ... - } else { - return ... - } +extension UniversalColor { + static var special: UniversalColor { + if selectedTheme == .halloween { + return ... + } else { + return ... } } } // Then in your class let view = UIView() -view.backgroundColor = Palette.MyColors.special.uiColor +view.backgroundColor = UniversalColor.special.uiColor ``` **Extend Fonts** -The config defines only three font sizes, but if you want to use semantic font values in your app, you can extend the `UniversalFont` struct: +If you want to use additional fonts that are not included in the config, you can extend `UniversalFont`: ```swift extension UniversalFont { - enum Text { - static let body: UniversalFont = .system(size: 16, weight: .regular) - } + static let title: UniversalFont = .system(size: 16, weight: .regular) } // Then in your view Text("Hello, World") - .font(UniversalFont.Text.body.font) + .font(UniversalFont.title.font) ``` You can also extend `UniversalFont` for easier access to custom fonts: diff --git a/Sources/ComponentsKit/Button/Models/ButtonStyle.swift b/Sources/ComponentsKit/Components/Button/Models/ButtonStyle.swift similarity index 100% rename from Sources/ComponentsKit/Button/Models/ButtonStyle.swift rename to Sources/ComponentsKit/Components/Button/Models/ButtonStyle.swift diff --git a/Sources/ComponentsKit/Button/Models/ButtonVM.swift b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift similarity index 82% rename from Sources/ComponentsKit/Button/Models/ButtonVM.swift rename to Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift index 7225def9..6d59e8e0 100644 --- a/Sources/ComponentsKit/Button/Models/ButtonVM.swift +++ b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift @@ -11,9 +11,7 @@ public struct ButtonVM: ComponentVM { public var animationScale: AnimationScale = .medium /// The color of the button. - /// - /// Defaults to `.primary`. - public var color: ComponentColor = .primary + public var color: ComponentColor? /// The corner radius of the button. /// @@ -53,18 +51,12 @@ public struct ButtonVM: ComponentVM { extension ButtonVM { private var mainColor: UniversalColor { - return self.isEnabled - ? self.color.main - : self.color.main.withOpacity( - ComponentsKitConfig.shared.layout.disabledOpacity - ) + let color = self.color?.main ?? .content2 + return color.enabled(self.isEnabled) } private var contrastColor: UniversalColor { - return self.isEnabled - ? self.color.contrast - : self.color.contrast.withOpacity( - ComponentsKitConfig.shared.layout.disabledOpacity - ) + let color = self.color?.contrast ?? .foreground + return color.enabled(self.isEnabled) } var backgroundColor: UniversalColor? { switch self.style { @@ -81,7 +73,8 @@ extension ButtonVM { case .plain: return self.mainColor case .bordered: - return self.mainColor + let color = self.color?.main ?? .foreground + return color.enabled(self.isEnabled) } } var borderWidth: CGFloat { @@ -97,7 +90,11 @@ extension ButtonVM { case .filled, .plain: return nil case .bordered: - return self.mainColor + if let color { + return color.main.enabled(self.isEnabled) + } else { + return .divider + } } } var preferredFont: UniversalFont { @@ -107,25 +104,25 @@ extension ButtonVM { switch self.size { case .small: - return UniversalFont.Component.small + return .smButton case .medium: - return UniversalFont.Component.medium + return .mdButton case .large: - return UniversalFont.Component.large + return .mdButton } } var height: CGFloat { return switch self.size { case .small: 36 - case .medium: 50 - case .large: 70 + case .medium: 44 + case .large: 52 } } var horizontalPadding: CGFloat { return switch self.size { - case .small: 8 - case .medium: 12 - case .large: 16 + case .small: 16 + case .medium: 20 + case .large: 24 } } } diff --git a/Sources/ComponentsKit/Button/SUButton.swift b/Sources/ComponentsKit/Components/Button/SUButton.swift similarity index 100% rename from Sources/ComponentsKit/Button/SUButton.swift rename to Sources/ComponentsKit/Components/Button/SUButton.swift diff --git a/Sources/ComponentsKit/Button/UKButton.swift b/Sources/ComponentsKit/Components/Button/UKButton.swift similarity index 100% rename from Sources/ComponentsKit/Button/UKButton.swift rename to Sources/ComponentsKit/Components/Button/UKButton.swift diff --git a/Sources/ComponentsKit/Checkbox/Helpers/CheckboxAnimationDurations.swift b/Sources/ComponentsKit/Components/Checkbox/Helpers/CheckboxAnimationDurations.swift similarity index 100% rename from Sources/ComponentsKit/Checkbox/Helpers/CheckboxAnimationDurations.swift rename to Sources/ComponentsKit/Components/Checkbox/Helpers/CheckboxAnimationDurations.swift diff --git a/Sources/ComponentsKit/Checkbox/Models/CheckboxVM.swift b/Sources/ComponentsKit/Components/Checkbox/Models/CheckboxVM.swift similarity index 79% rename from Sources/ComponentsKit/Checkbox/Models/CheckboxVM.swift rename to Sources/ComponentsKit/Components/Checkbox/Models/CheckboxVM.swift index 352e6fff..37ffb131 100644 --- a/Sources/ComponentsKit/Checkbox/Models/CheckboxVM.swift +++ b/Sources/ComponentsKit/Components/Checkbox/Models/CheckboxVM.swift @@ -38,32 +38,16 @@ public struct CheckboxVM: ComponentVM { extension CheckboxVM { var backgroundColor: UniversalColor { - return self.color.main.withOpacity( - self.isEnabled - ? 1.0 - : ComponentsKitConfig.shared.layout.disabledOpacity - ) + return self.color.main.enabled(self.isEnabled) } var foregroundColor: UniversalColor { - return self.color.contrast.withOpacity( - self.isEnabled - ? 1.0 - : ComponentsKitConfig.shared.layout.disabledOpacity - ) + return self.color.contrast.enabled(self.isEnabled) } var titleColor: UniversalColor { - return Palette.Text.primary.withOpacity( - self.isEnabled - ? 1.0 - : ComponentsKitConfig.shared.layout.disabledOpacity - ) + return .foreground.enabled(self.isEnabled) } var borderColor: UniversalColor { - return .universal(.uiColor(.lightGray)).withOpacity( - self.isEnabled - ? 1.0 - : ComponentsKitConfig.shared.layout.disabledOpacity - ) + return .divider } var borderWidth: CGFloat { return 2.0 @@ -84,11 +68,11 @@ extension CheckboxVM { var checkboxSide: CGFloat { switch self.size { case .small: - return 18.0 + return 16.0 case .medium: - return 24.0 + return 20.0 case .large: - return 32.0 + return 24.0 } } var checkboxCornerRadius: CGFloat { @@ -114,11 +98,11 @@ extension CheckboxVM { switch self.size { case .small: - return UniversalFont.Component.small + return .smBody case .medium: - return UniversalFont.Component.medium + return .mdBody case .large: - return UniversalFont.Component.large + return .lgBody } } } diff --git a/Sources/ComponentsKit/Checkbox/SUCheckbox.swift b/Sources/ComponentsKit/Components/Checkbox/SUCheckbox.swift similarity index 100% rename from Sources/ComponentsKit/Checkbox/SUCheckbox.swift rename to Sources/ComponentsKit/Components/Checkbox/SUCheckbox.swift diff --git a/Sources/ComponentsKit/Checkbox/UKCheckbox.swift b/Sources/ComponentsKit/Components/Checkbox/UKCheckbox.swift similarity index 98% rename from Sources/ComponentsKit/Checkbox/UKCheckbox.swift rename to Sources/ComponentsKit/Components/Checkbox/UKCheckbox.swift index 0a4b727b..bf97064d 100644 --- a/Sources/ComponentsKit/Checkbox/UKCheckbox.swift +++ b/Sources/ComponentsKit/Components/Checkbox/UKCheckbox.swift @@ -262,6 +262,7 @@ open class UKCheckbox: UIView, UKComponent { self.checkboxContainer.layer.borderColor = self.isSelected ? UIColor.clear.cgColor : self.model.borderColor.uiColor.cgColor + Self.Style.checkmarkLayer(self.checkmarkLayer, model: self.model) } } @@ -275,7 +276,7 @@ extension UKCheckbox { stackView.alignment = .center } static func titleLabel(_ label: UILabel, model: Model) { - label.textColor = Palette.Text.primary.uiColor + label.textColor = model.titleColor.uiColor label.numberOfLines = 0 label.text = model.title label.textColor = model.titleColor.uiColor diff --git a/Sources/ComponentsKit/Countdown/Helpers/CountdownHelpers.swift b/Sources/ComponentsKit/Components/Countdown/Helpers/CountdownHelpers.swift similarity index 100% rename from Sources/ComponentsKit/Countdown/Helpers/CountdownHelpers.swift rename to Sources/ComponentsKit/Components/Countdown/Helpers/CountdownHelpers.swift diff --git a/Sources/ComponentsKit/Countdown/Helpers/CountdownWidthCalculator.swift b/Sources/ComponentsKit/Components/Countdown/Helpers/CountdownWidthCalculator.swift similarity index 68% rename from Sources/ComponentsKit/Countdown/Helpers/CountdownWidthCalculator.swift rename to Sources/ComponentsKit/Components/Countdown/Helpers/CountdownWidthCalculator.swift index 7487cb30..293fe222 100644 --- a/Sources/ComponentsKit/Countdown/Helpers/CountdownWidthCalculator.swift +++ b/Sources/ComponentsKit/Components/Countdown/Helpers/CountdownWidthCalculator.swift @@ -10,10 +10,9 @@ struct CountdownWidthCalculator { model: CountdownVM ) -> CGFloat { self.style(label, with: model) - label.attributedText = attributedText + self.label.attributedText = attributedText - let targetSize = CGSize(width: CGFloat.greatestFiniteMagnitude, height: model.height) - let estimatedSize = label.sizeThatFits(targetSize) + let estimatedSize = self.label.sizeThatFits(UIView.layoutFittingExpandedSize) return estimatedSize.width } diff --git a/Sources/ComponentsKit/Countdown/Localization/UnitsLocalization.swift b/Sources/ComponentsKit/Components/Countdown/Localization/UnitsLocalization.swift similarity index 100% rename from Sources/ComponentsKit/Countdown/Localization/UnitsLocalization.swift rename to Sources/ComponentsKit/Components/Countdown/Localization/UnitsLocalization.swift diff --git a/Sources/ComponentsKit/Countdown/Manager/CountdownManager.swift b/Sources/ComponentsKit/Components/Countdown/Manager/CountdownManager.swift similarity index 100% rename from Sources/ComponentsKit/Countdown/Manager/CountdownManager.swift rename to Sources/ComponentsKit/Components/Countdown/Manager/CountdownManager.swift diff --git a/Sources/ComponentsKit/Countdown/Models/CountdownStyle.swift b/Sources/ComponentsKit/Components/Countdown/Models/CountdownStyle.swift similarity index 100% rename from Sources/ComponentsKit/Countdown/Models/CountdownStyle.swift rename to Sources/ComponentsKit/Components/Countdown/Models/CountdownStyle.swift diff --git a/Sources/ComponentsKit/Countdown/Models/CountdownVM.swift b/Sources/ComponentsKit/Components/Countdown/Models/CountdownVM.swift similarity index 80% rename from Sources/ComponentsKit/Countdown/Models/CountdownVM.swift rename to Sources/ComponentsKit/Components/Countdown/Models/CountdownVM.swift index d3cb2854..ea7db580 100644 --- a/Sources/ComponentsKit/Countdown/Models/CountdownVM.swift +++ b/Sources/ComponentsKit/Components/Countdown/Models/CountdownVM.swift @@ -2,12 +2,15 @@ import SwiftUI /// A model that defines the appearance properties for a countdown component. public struct CountdownVM: ComponentVM { - /// The font used for displaying the countdown numbers and units. - public var font: UniversalFont? - /// The color of the countdown. public var color: ComponentColor? + /// The font used for displaying the countdown numbers and trailing units. + public var mainFont: UniversalFont? + + /// The font used for displaying the countdown bottom units. + public var secondaryFont: UniversalFont? + /// The predefined size of the countdown. /// /// Defaults to `.medium`. @@ -52,59 +55,55 @@ public struct CountdownVM: ComponentVM { // MARK: - Shared Helpers extension CountdownVM { - var preferredFont: UniversalFont { - if let font = self.font { - return font + var preferredMainFont: UniversalFont { + if let mainFont { + return mainFont } switch self.size { case .small: - return UniversalFont.Component.small + return .smHeadline case .medium: - return UniversalFont.Component.medium + return .mdHeadline case .large: - return UniversalFont.Component.large + return .lgHeadline } } - var unitFontSize: CGFloat { - let preferredFontSize = self.preferredFont.uiFont.pointSize - return preferredFontSize * 0.6 - } - var unitFont: UniversalFont { - return self.preferredFont.withSize(self.unitFontSize) + private var preferredSecondaryFont: UniversalFont { + if let secondaryFont { + return secondaryFont + } + + switch self.size { + case .small: + return .smCaption + case .medium: + return .mdCaption + case .large: + return .lgCaption + } } var backgroundColor: UniversalColor { - if let color { - return color.main.withOpacity(0.15) - } else { - return .init( - light: .rgba(r: 244, g: 244, b: 245, a: 1.0), - dark: .rgba(r: 39, g: 39, b: 42, a: 1.0) - ) - } + return self.color?.background ?? .content1 } var foregroundColor: UniversalColor { - let foregroundColor = self.color?.main ?? .init( - light: .rgba(r: 0, g: 0, b: 0, a: 1.0), - dark: .rgba(r: 255, g: 255, b: 255, a: 1.0) - ) - return foregroundColor + return self.color?.main ?? .foreground } var colonColor: UniversalColor { - return self.foregroundColor.withOpacity(0.5) + return self.color?.main ?? .secondaryForeground } - var height: CGFloat { + var defaultMinWidth: CGFloat { return switch self.size { - case .small: 45 - case .medium: 55 - case .large: 60 + case .small: 20 + case .medium: 25 + case .large: 30 } } - var defaultMinWidth: CGFloat { + var lightBackgroundMinHight: CGFloat { return switch self.size { - case .small: 40 - case .medium: 50 - case .large: 55 + case .small: 45 + case .medium: 55 + case .large: 65 } } var lightBackgroundMinWidth: CGFloat { @@ -115,7 +114,12 @@ extension CountdownVM { } } var horizontalPadding: CGFloat { - return 4 + switch self.style { + case .light: + return 4 + case .plain: + return 0 + } } var spacing: CGFloat { switch self.style { @@ -164,7 +168,7 @@ extension CountdownVM { unit: CountdownHelpers.Unit ) -> NSAttributedString { let mainTextAttributes: [NSAttributedString.Key: Any] = [ - .font: self.preferredFont.uiFont, + .font: self.preferredMainFont.uiFont, .foregroundColor: self.foregroundColor.uiColor ] @@ -179,7 +183,7 @@ extension CountdownVM { let localized = self.localizedUnit(unit, length: .short) let trailingString = " " + localized let trailingAttributes: [NSAttributedString.Key: Any] = [ - .font: self.preferredFont.uiFont, + .font: self.preferredMainFont.uiFont, .foregroundColor: self.foregroundColor.uiColor ] result.append(NSAttributedString(string: trailingString, attributes: trailingAttributes)) @@ -189,7 +193,7 @@ extension CountdownVM { let localized = self.localizedUnit(unit, length: .long) let bottomString = "\n" + localized let bottomAttributes: [NSAttributedString.Key: Any] = [ - .font: self.unitFont.uiFont, + .font: self.preferredSecondaryFont.uiFont, .foregroundColor: self.foregroundColor.uiColor ] result.append(NSAttributedString(string: bottomString, attributes: bottomAttributes)) @@ -202,7 +206,8 @@ extension CountdownVM { func shouldRecalculateWidth(_ oldModel: Self) -> Bool { return self.unitsStyle != oldModel.unitsStyle || self.style != oldModel.style - || self.preferredFont != oldModel.preferredFont + || self.mainFont != oldModel.mainFont + || self.secondaryFont != oldModel.secondaryFont || self.size != oldModel.size || self.locale != oldModel.locale } @@ -238,6 +243,6 @@ extension CountdownVM { func shouldUpdateHeight(_ oldModel: Self) -> Bool { return self.style != oldModel.style - || self.height != oldModel.height + || self.size != oldModel.size } } diff --git a/Sources/ComponentsKit/Countdown/SUCountdown.swift b/Sources/ComponentsKit/Components/Countdown/SUCountdown.swift similarity index 96% rename from Sources/ComponentsKit/Countdown/SUCountdown.swift rename to Sources/ComponentsKit/Components/Countdown/SUCountdown.swift index 4d1d03d9..e2dbcc20 100644 --- a/Sources/ComponentsKit/Countdown/SUCountdown.swift +++ b/Sources/ComponentsKit/Components/Countdown/SUCountdown.swift @@ -85,7 +85,7 @@ public struct SUCountdown: View { private var colonView: some View { Text(":") - .font(self.model.preferredFont.font) + .font(self.model.preferredMainFont.font) .foregroundColor(self.model.colonColor.color(for: self.colorScheme)) } @@ -94,7 +94,7 @@ public struct SUCountdown: View { unit: CountdownHelpers.Unit ) -> some View { return self.styledTime(value: value, unit: unit) - .frame(minHeight: self.model.height) + .frame(minHeight: self.model.lightBackgroundMinHight) .frame(minWidth: self.model.lightBackgroundMinWidth) .background(RoundedRectangle(cornerRadius: 8) .fill(self.model.backgroundColor.color(for: self.colorScheme)) diff --git a/Sources/ComponentsKit/Countdown/UKCountdown.swift b/Sources/ComponentsKit/Components/Countdown/UKCountdown.swift similarity index 97% rename from Sources/ComponentsKit/Countdown/UKCountdown.swift rename to Sources/ComponentsKit/Components/Countdown/UKCountdown.swift index 982d3fb0..ad60f58b 100644 --- a/Sources/ComponentsKit/Countdown/UKCountdown.swift +++ b/Sources/ComponentsKit/Components/Countdown/UKCountdown.swift @@ -162,7 +162,7 @@ public class UKCountdown: UIView, UKComponent { ) self.daysConstraints.width?.isActive = true - self.daysConstraints.height = self.daysLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: self.model.height) + self.daysConstraints.height = self.daysLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: self.model.lightBackgroundMinHight) self.daysConstraints.height?.isActive = true self.hoursLabel.widthAnchor.constraint(equalTo: self.daysLabel.widthAnchor).isActive = true @@ -202,7 +202,7 @@ public class UKCountdown: UIView, UKComponent { self.daysConstraints.height?.isActive = false case .light: self.daysConstraints.height?.isActive = true - self.daysConstraints.height?.constant = self.model.height + self.daysConstraints.height?.constant = self.model.lightBackgroundMinHight } } @@ -246,7 +246,6 @@ extension UKCountdown { label.layer.cornerRadius = 8 label.clipsToBounds = true } - label.font = model.preferredFont.uiFont label.textColor = model.foregroundColor.uiColor label.textAlignment = .center label.numberOfLines = 0 @@ -255,7 +254,7 @@ extension UKCountdown { static func colonLabel(_ label: UILabel, model: CountdownVM) { label.text = ":" - label.font = model.preferredFont.uiFont + label.font = model.preferredMainFont.uiFont label.textColor = model.colonColor.uiColor label.textAlignment = .center label.isVisible = model.isColumnLabelVisible diff --git a/Sources/ComponentsKit/Divider/Models/DividerOrientation.swift b/Sources/ComponentsKit/Components/Divider/Models/DividerOrientation.swift similarity index 100% rename from Sources/ComponentsKit/Divider/Models/DividerOrientation.swift rename to Sources/ComponentsKit/Components/Divider/Models/DividerOrientation.swift diff --git a/Sources/ComponentsKit/Divider/Models/DividerVM.swift b/Sources/ComponentsKit/Components/Divider/Models/DividerVM.swift similarity index 86% rename from Sources/ComponentsKit/Divider/Models/DividerVM.swift rename to Sources/ComponentsKit/Components/Divider/Models/DividerVM.swift index 23d7764d..ab3b4056 100644 --- a/Sources/ComponentsKit/Divider/Models/DividerVM.swift +++ b/Sources/ComponentsKit/Components/Divider/Models/DividerVM.swift @@ -9,8 +9,8 @@ public struct DividerVM: ComponentVM { /// The color of the divider. /// - /// Defaults to `Palette.Base.divider`. - public var color: UniversalColor = Palette.Base.divider + /// Defaults to `.divider`. + public var color: ComponentColor? /// The predefined size of the divider, which affects its thickness. /// @@ -24,6 +24,9 @@ public struct DividerVM: ComponentVM { // MARK: - Shared Helpers extension DividerVM { + var lineColor: UniversalColor { + return self.color?.background ?? .divider + } var lineSize: CGFloat { switch self.size { case .small: diff --git a/Sources/ComponentsKit/Divider/SUDivider.swift b/Sources/ComponentsKit/Components/Divider/SUDivider.swift similarity index 92% rename from Sources/ComponentsKit/Divider/SUDivider.swift rename to Sources/ComponentsKit/Components/Divider/SUDivider.swift index 21be76b7..391c5389 100644 --- a/Sources/ComponentsKit/Divider/SUDivider.swift +++ b/Sources/ComponentsKit/Components/Divider/SUDivider.swift @@ -22,7 +22,7 @@ public struct SUDivider: View { public var body: some View { Rectangle() - .fill(self.model.color.color(for: self.colorScheme)) + .fill(self.model.lineColor.color(for: self.colorScheme)) .frame( maxWidth: self.model.orientation == .vertical ? self.model.lineSize : nil, maxHeight: self.model.orientation == .horizontal ? self.model.lineSize : nil diff --git a/Sources/ComponentsKit/Divider/UKDivider.swift b/Sources/ComponentsKit/Components/Divider/UKDivider.swift similarity index 93% rename from Sources/ComponentsKit/Divider/UKDivider.swift rename to Sources/ComponentsKit/Components/Divider/UKDivider.swift index af491b0a..9f667846 100644 --- a/Sources/ComponentsKit/Divider/UKDivider.swift +++ b/Sources/ComponentsKit/Components/Divider/UKDivider.swift @@ -35,7 +35,7 @@ open class UKDivider: UIView, UKComponent { // MARK: - Setup private func style() { - self.backgroundColor = self.model.color.uiColor + self.backgroundColor = self.model.lineColor.uiColor self.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) self.setContentCompressionResistancePriority(.defaultLow, for: .vertical) } @@ -45,7 +45,7 @@ open class UKDivider: UIView, UKComponent { public func update(_ oldModel: DividerVM) { guard self.model != oldModel else { return } - self.backgroundColor = self.model.color.uiColor + self.backgroundColor = self.model.lineColor.uiColor if self.model.shouldUpdateLayout(oldModel) { self.invalidateIntrinsicContentSize() diff --git a/Sources/ComponentsKit/InputField/Models/InputFieldVM.swift b/Sources/ComponentsKit/Components/InputField/Models/InputFieldVM.swift similarity index 88% rename from Sources/ComponentsKit/InputField/Models/InputFieldVM.swift rename to Sources/ComponentsKit/Components/InputField/Models/InputFieldVM.swift index edc707da..dd339bb1 100644 --- a/Sources/ComponentsKit/InputField/Models/InputFieldVM.swift +++ b/Sources/ComponentsKit/Components/InputField/Models/InputFieldVM.swift @@ -81,18 +81,18 @@ extension InputFieldVM { switch self.size { case .small: - return UniversalFont.Component.medium + return .smBody case .medium: - return UniversalFont.Component.medium + return .mdBody case .large: - return UniversalFont.Component.large + return .lgBody } } var height: CGFloat { return switch self.size { case .small: 40 - case .medium: 60 - case .large: 80 + case .medium: 48 + case .large: 56 } } var horizontalPadding: CGFloat { @@ -107,26 +107,18 @@ extension InputFieldVM { return self.title.isNotNilAndEmpty ? 12 : 0 } var backgroundColor: UniversalColor { - if let color { - return color.main.withOpacity(0.25) - } else { - return .init( - light: .rgba(r: 244, g: 244, b: 245, a: 1.0), - dark: .rgba(r: 39, g: 39, b: 42, a: 1.0) - ) - } + return self.color?.background ?? .content1 } var foregroundColor: UniversalColor { - let foregroundColor = self.color?.main ?? .init( - light: .rgba(r: 0, g: 0, b: 0, a: 1.0), - dark: .rgba(r: 255, g: 255, b: 255, a: 1.0) - ) - return foregroundColor.withOpacity( - self.isEnabled ? 1.0 : 0.5 - ) + let color = self.color?.main ?? .foreground + return color.enabled(self.isEnabled) } var placeholderColor: UniversalColor { - return self.foregroundColor.withOpacity(self.isEnabled ? 0.7 : 0.3) + if let color { + return color.main.withOpacity(self.isEnabled ? 0.7 : 0.3) + } else { + return .secondaryForeground.enabled(self.isEnabled) + } } } diff --git a/Sources/ComponentsKit/InputField/SUInputField.swift b/Sources/ComponentsKit/Components/InputField/SUInputField.swift similarity index 97% rename from Sources/ComponentsKit/InputField/SUInputField.swift rename to Sources/ComponentsKit/Components/InputField/SUInputField.swift index 9828faf6..583869d9 100644 --- a/Sources/ComponentsKit/InputField/SUInputField.swift +++ b/Sources/ComponentsKit/Components/InputField/SUInputField.swift @@ -55,9 +55,6 @@ public struct SUInputField: View { if let title = self.model.attributedTitle { Text(title) .font(self.model.preferredFont.font) - .foregroundStyle( - self.model.foregroundColor.color(for: self.colorScheme) - ) } Group { diff --git a/Sources/ComponentsKit/InputField/UKInputField.swift b/Sources/ComponentsKit/Components/InputField/UKInputField.swift similarity index 100% rename from Sources/ComponentsKit/InputField/UKInputField.swift rename to Sources/ComponentsKit/Components/InputField/UKInputField.swift diff --git a/Sources/ComponentsKit/Loading/Models/LoadingStyle.swift b/Sources/ComponentsKit/Components/Loading/Models/LoadingStyle.swift similarity index 100% rename from Sources/ComponentsKit/Loading/Models/LoadingStyle.swift rename to Sources/ComponentsKit/Components/Loading/Models/LoadingStyle.swift diff --git a/Sources/ComponentsKit/Loading/Models/LoadingVM.swift b/Sources/ComponentsKit/Components/Loading/Models/LoadingVM.swift similarity index 95% rename from Sources/ComponentsKit/Loading/Models/LoadingVM.swift rename to Sources/ComponentsKit/Components/Loading/Models/LoadingVM.swift index 9930d10d..c9f7f30b 100644 --- a/Sources/ComponentsKit/Loading/Models/LoadingVM.swift +++ b/Sources/ComponentsKit/Components/Loading/Models/LoadingVM.swift @@ -4,8 +4,8 @@ import Foundation public struct LoadingVM: ComponentVM { /// The color of the loading indicator. /// - /// Defaults to `.primary`. - public var color: ComponentColor = .primary + /// Defaults to `.accent`. + public var color: ComponentColor = .accent /// The width of the lines used in the loading indicator. /// diff --git a/Sources/ComponentsKit/Loading/SULoading.swift b/Sources/ComponentsKit/Components/Loading/SULoading.swift similarity index 100% rename from Sources/ComponentsKit/Loading/SULoading.swift rename to Sources/ComponentsKit/Components/Loading/SULoading.swift diff --git a/Sources/ComponentsKit/Loading/UKLoading.swift b/Sources/ComponentsKit/Components/Loading/UKLoading.swift similarity index 100% rename from Sources/ComponentsKit/Loading/UKLoading.swift rename to Sources/ComponentsKit/Components/Loading/UKLoading.swift diff --git a/Sources/ComponentsKit/RadioGroup/Models/RadioGroupVM.swift b/Sources/ComponentsKit/Components/RadioGroup/Models/RadioGroupVM.swift similarity index 84% rename from Sources/ComponentsKit/RadioGroup/Models/RadioGroupVM.swift rename to Sources/ComponentsKit/Components/RadioGroup/Models/RadioGroupVM.swift index ab205cf9..bc985340 100644 --- a/Sources/ComponentsKit/RadioGroup/Models/RadioGroupVM.swift +++ b/Sources/ComponentsKit/Components/RadioGroup/Models/RadioGroupVM.swift @@ -9,7 +9,7 @@ public struct RadioGroupVM: ComponentVM { public var animationScale: AnimationScale = .medium /// The color of the selected radio button. - public var color: UniversalColor = .primary + public var color: UniversalColor = .accent /// The font used for the radio items' titles. public var font: UniversalFont? @@ -93,11 +93,11 @@ extension RadioGroupVM { switch self.size { case .small: - return UniversalFont.Component.small + return .smBody case .medium: - return UniversalFont.Component.medium + return .mdBody case .large: - return UniversalFont.Component.large + return .lgBody } } @@ -114,18 +114,15 @@ extension RadioGroupVM { } func radioItemColor(for item: RadioItemVM, isSelected: Bool) -> UniversalColor { - let defaultColor = UniversalColor.universal(.uiColor(.lightGray)) - let color = isSelected ? self.color : defaultColor - return self.isItemEnabled(item) - ? color - : color.withOpacity(ComponentsKitConfig.shared.layout.disabledOpacity) + if isSelected { + return self.color.enabled(self.isItemEnabled(item)) + } else { + return .divider + } } func textColor(for item: RadioItemVM) -> UniversalColor { - let baseColor = Palette.Text.primary - return self.isItemEnabled(item) - ? baseColor - : baseColor.withOpacity(ComponentsKitConfig.shared.layout.disabledOpacity) + return .foreground.enabled(self.isItemEnabled(item)) } } diff --git a/Sources/ComponentsKit/RadioGroup/Models/RadioItemVM.swift b/Sources/ComponentsKit/Components/RadioGroup/Models/RadioItemVM.swift similarity index 100% rename from Sources/ComponentsKit/RadioGroup/Models/RadioItemVM.swift rename to Sources/ComponentsKit/Components/RadioGroup/Models/RadioItemVM.swift diff --git a/Sources/ComponentsKit/RadioGroup/SwiftUI/SURadioGroup.swift b/Sources/ComponentsKit/Components/RadioGroup/SwiftUI/SURadioGroup.swift similarity index 100% rename from Sources/ComponentsKit/RadioGroup/SwiftUI/SURadioGroup.swift rename to Sources/ComponentsKit/Components/RadioGroup/SwiftUI/SURadioGroup.swift diff --git a/Sources/ComponentsKit/RadioGroup/UIKit/RadioGroupItemView.swift b/Sources/ComponentsKit/Components/RadioGroup/UIKit/RadioGroupItemView.swift similarity index 89% rename from Sources/ComponentsKit/RadioGroup/UIKit/RadioGroupItemView.swift rename to Sources/ComponentsKit/Components/RadioGroup/UIKit/RadioGroupItemView.swift index 055ad5ee..532ecf7b 100644 --- a/Sources/ComponentsKit/RadioGroup/UIKit/RadioGroupItemView.swift +++ b/Sources/ComponentsKit/Components/RadioGroup/UIKit/RadioGroupItemView.swift @@ -57,6 +57,12 @@ public class RadioGroupItemView: UIView { self.addSubview(self.radioView) self.radioView.addSubview(self.innerCircle) self.addSubview(self.titleLabel) + + if #available(iOS 17.0, *) { + self.registerForTraitChanges([UITraitUserInterfaceStyle.self]) { (view: Self, _: UITraitCollection) in + view.handleTraitChanges() + } + } } // MARK: Style @@ -152,6 +158,26 @@ public class RadioGroupItemView: UIView { completion: nil ) } + + // MARK: UIView Methods + + public override func traitCollectionDidChange( + _ previousTraitCollection: UITraitCollection? + ) { + super.traitCollectionDidChange(previousTraitCollection) + self.handleTraitChanges() + } + + // MARK: Helpers + + @objc private func handleTraitChanges() { + Self.Style.radioView( + self.radioView, + itemVM: self.itemVM, + groupVM: self.groupVM, + isSelected: self.isSelected + ) + } } // MARK: - Style Helpers diff --git a/Sources/ComponentsKit/RadioGroup/UIKit/UKRadioGroup.swift b/Sources/ComponentsKit/Components/RadioGroup/UIKit/UKRadioGroup.swift similarity index 100% rename from Sources/ComponentsKit/RadioGroup/UIKit/UKRadioGroup.swift rename to Sources/ComponentsKit/Components/RadioGroup/UIKit/UKRadioGroup.swift diff --git a/Sources/ComponentsKit/SegmentedControl/Models/SegmentedControlItemVM.swift b/Sources/ComponentsKit/Components/SegmentedControl/Models/SegmentedControlItemVM.swift similarity index 100% rename from Sources/ComponentsKit/SegmentedControl/Models/SegmentedControlItemVM.swift rename to Sources/ComponentsKit/Components/SegmentedControl/Models/SegmentedControlItemVM.swift diff --git a/Sources/ComponentsKit/SegmentedControl/Models/SegmentedControlVM.swift b/Sources/ComponentsKit/Components/SegmentedControl/Models/SegmentedControlVM.swift similarity index 72% rename from Sources/ComponentsKit/SegmentedControl/Models/SegmentedControlVM.swift rename to Sources/ComponentsKit/Components/SegmentedControl/Models/SegmentedControlVM.swift index dfd3c55e..6aab2aad 100644 --- a/Sources/ComponentsKit/SegmentedControl/Models/SegmentedControlVM.swift +++ b/Sources/ComponentsKit/Components/SegmentedControl/Models/SegmentedControlVM.swift @@ -52,26 +52,14 @@ public struct SegmentedControlVM: ComponentVM { extension SegmentedControlVM { var backgroundColor: UniversalColor { - return .init( - light: .rgba(r: 244, g: 244, b: 245, a: 1.0), - dark: .rgba(r: 39, g: 39, b: 42, a: 1.0) - ) - .withOpacity( - self.isEnabled - ? 1.0 - : ComponentsKitConfig.shared.layout.disabledOpacity - ) + return .content1 } var selectedSegmentColor: UniversalColor { - let selectedSegmentColor = self.color?.main ?? .init( - light: .rgba(r: 255, g: 255, b: 255, a: 1.0), - dark: .rgba(r: 62, g: 62, b: 69, a: 1.0) - ) - return selectedSegmentColor.withOpacity( - self.isEnabled - ? 1.0 - : ComponentsKitConfig.shared.layout.disabledOpacity + let color = self.color?.main ?? .themed( + light: UniversalColor.white.light, + dark: UniversalColor.content2.dark ) + return color.enabled(self.isEnabled) } func item(for id: ID) -> SegmentedControlItemVM? { return self.items.first(where: { $0.id == id }) @@ -79,25 +67,11 @@ extension SegmentedControlVM { func foregroundColor(id: ID, selectedId: ID) -> UniversalColor { let isItemEnabled = self.item(for: id)?.isEnabled == true let isSelected = id == selectedId && isItemEnabled - let defaultColor = UniversalColor( - light: .rgba(r: 0, g: 0, b: 0, a: 1.0), - dark: .rgba(r: 255, g: 255, b: 255, a: 1.0) - ) - guard isSelected else { - return defaultColor.withOpacity( - self.isEnabled && isItemEnabled - ? 0.7 - : 0.7 * ComponentsKitConfig.shared.layout.disabledOpacity - ) - } - - let foregroundColor = self.color?.contrast ?? defaultColor - return foregroundColor.withOpacity( - self.isEnabled - ? 1.0 - : ComponentsKitConfig.shared.layout.disabledOpacity - ) + let color = isSelected + ? self.color?.contrast ?? .foreground + : .secondaryForeground + return color.enabled(self.isEnabled && isItemEnabled) } var horizontalInnerPaddings: CGFloat? { guard !self.isFullWidth else { @@ -118,8 +92,17 @@ extension SegmentedControlVM { var height: CGFloat { return switch self.size { case .small: 36 - case .medium: 50 - case .large: 70 + case .medium: 44 + case .large: 52 + } + } + func selectedSegmentCornerRadius(for height: CGFloat = 10_000) -> CGFloat { + let componentRadius = self.cornerRadius.value(for: height) + switch self.cornerRadius { + case .none, .full, .custom: + return componentRadius + case .small, .medium, .large: + return max(0, componentRadius - self.outerPaddings / 2) } } func preferredFont(for id: ID) -> UniversalFont { @@ -131,11 +114,11 @@ extension SegmentedControlVM { switch self.size { case .small: - return UniversalFont.Component.small + return .smBody case .medium: - return UniversalFont.Component.medium + return .mdBody case .large: - return UniversalFont.Component.large + return .lgBody } } } diff --git a/Sources/ComponentsKit/SegmentedControl/SUSegmentedControl.swift b/Sources/ComponentsKit/Components/SegmentedControl/SUSegmentedControl.swift similarity index 97% rename from Sources/ComponentsKit/SegmentedControl/SUSegmentedControl.swift rename to Sources/ComponentsKit/Components/SegmentedControl/SUSegmentedControl.swift index 9754fb78..c62da1da 100644 --- a/Sources/ComponentsKit/SegmentedControl/SUSegmentedControl.swift +++ b/Sources/ComponentsKit/Components/SegmentedControl/SUSegmentedControl.swift @@ -55,7 +55,7 @@ public struct SUSegmentedControl: View { ZStack { if itemVM.isEnabled, self.selectedId == itemVM.id { RoundedRectangle( - cornerRadius: self.model.cornerRadius.value() + cornerRadius: self.model.selectedSegmentCornerRadius() ) .fill(self.model.selectedSegmentColor.color( for: self.colorScheme diff --git a/Sources/ComponentsKit/SegmentedControl/UKSegmentedControl.swift b/Sources/ComponentsKit/Components/SegmentedControl/UKSegmentedControl.swift similarity index 98% rename from Sources/ComponentsKit/SegmentedControl/UKSegmentedControl.swift rename to Sources/ComponentsKit/Components/SegmentedControl/UKSegmentedControl.swift index d5e74ca0..15fd9588 100644 --- a/Sources/ComponentsKit/SegmentedControl/UKSegmentedControl.swift +++ b/Sources/ComponentsKit/Components/SegmentedControl/UKSegmentedControl.swift @@ -171,7 +171,7 @@ open class UKSegmentedControl: UIView, UKComponent { open override func layoutSubviews() { super.layoutSubviews() - self.selectedSegment.layer.cornerRadius = self.model.cornerRadius.value( + self.selectedSegment.layer.cornerRadius = self.model.selectedSegmentCornerRadius( for: self.container.bounds.height ) self.layer.cornerRadius = self.model.cornerRadius.value( @@ -317,9 +317,7 @@ extension UKSegmentedControl { static func selectedSegment(_ view: UIView, model: Model) { view.backgroundColor = model.selectedSegmentColor.uiColor - view.layer.cornerRadius = model.cornerRadius.value( - for: view.bounds.height - ) + view.layer.cornerRadius = model.selectedSegmentCornerRadius(for: view.bounds.height) } static func segment( diff --git a/Sources/ComponentsKit/TextInput/Helpers/TextInputHeightCalculator.swift b/Sources/ComponentsKit/Components/TextInput/Helpers/TextInputHeightCalculator.swift similarity index 100% rename from Sources/ComponentsKit/TextInput/Helpers/TextInputHeightCalculator.swift rename to Sources/ComponentsKit/Components/TextInput/Helpers/TextInputHeightCalculator.swift diff --git a/Sources/ComponentsKit/TextInput/Models/TextInputVM.swift b/Sources/ComponentsKit/Components/TextInput/Models/TextInputVM.swift similarity index 86% rename from Sources/ComponentsKit/TextInput/Models/TextInputVM.swift rename to Sources/ComponentsKit/Components/TextInput/Models/TextInputVM.swift index 7675aa7e..db000784 100644 --- a/Sources/ComponentsKit/TextInput/Models/TextInputVM.swift +++ b/Sources/ComponentsKit/Components/TextInput/Models/TextInputVM.swift @@ -93,11 +93,11 @@ extension TextInputVM { switch self.size { case .small: - return UniversalFont.Component.small + return .smBody case .medium: - return UniversalFont.Component.medium + return .mdBody case .large: - return UniversalFont.Component.large + return .lgBody } } @@ -106,28 +106,20 @@ extension TextInputVM { } var backgroundColor: UniversalColor { - if let color { - return color.main.withOpacity(0.25) - } else { - return .init( - light: .rgba(r: 244, g: 244, b: 245, a: 1.0), - dark: .rgba(r: 39, g: 39, b: 42, a: 1.0) - ) - } + return self.color?.background ?? .content1 } var foregroundColor: UniversalColor { - let foregroundColor = self.color?.main ?? .init( - light: .rgba(r: 0, g: 0, b: 0, a: 1.0), - dark: .rgba(r: 255, g: 255, b: 255, a: 1.0) - ) - return foregroundColor.withOpacity( - self.isEnabled ? 1.0 : 0.5 - ) + let color = self.color?.main ?? .foreground + return color.enabled(self.isEnabled) } var placeholderColor: UniversalColor { - return self.foregroundColor.withOpacity(self.isEnabled ? 0.7 : 0.3) + if let color { + return color.main.withOpacity(self.isEnabled ? 0.7 : 0.3) + } else { + return .secondaryForeground.enabled(self.isEnabled) + } } var minTextInputHeight: CGFloat { diff --git a/Sources/ComponentsKit/TextInput/SUTextInput.swift b/Sources/ComponentsKit/Components/TextInput/SUTextInput.swift similarity index 100% rename from Sources/ComponentsKit/TextInput/SUTextInput.swift rename to Sources/ComponentsKit/Components/TextInput/SUTextInput.swift diff --git a/Sources/ComponentsKit/TextInput/UKTextInput.swift b/Sources/ComponentsKit/Components/TextInput/UKTextInput.swift similarity index 100% rename from Sources/ComponentsKit/TextInput/UKTextInput.swift rename to Sources/ComponentsKit/Components/TextInput/UKTextInput.swift diff --git a/Sources/ComponentsKit/Configuration/Config.swift b/Sources/ComponentsKit/Configuration/Config.swift new file mode 100644 index 00000000..c31c807a --- /dev/null +++ b/Sources/ComponentsKit/Configuration/Config.swift @@ -0,0 +1,24 @@ +import Foundation + +/// A configuration structure for customizing colors and layout attributes of the components. +public struct ComponentsKitConfig: Initializable, Updatable { + // MARK: - Properties + + /// The palette of colors. + public var colors: Palette = .init() + + /// The layout configuration. + public var layout: Layout = .init() + + // MARK: - Initialization + + /// Initializes a new `ComponentsKitConfig` instance with default values. + public init() {} +} + +// MARK: - ComponentsKitConfig + Shared + +extension ComponentsKitConfig { + /// A shared instance of `ComponentsKitConfig` for global use. + public static var shared: Self = .init() +} diff --git a/Sources/ComponentsKit/Configuration/Layout.swift b/Sources/ComponentsKit/Configuration/Layout.swift new file mode 100644 index 00000000..b3bd7d90 --- /dev/null +++ b/Sources/ComponentsKit/Configuration/Layout.swift @@ -0,0 +1,193 @@ +import Foundation + +extension ComponentsKitConfig { + /// A structure that defines the layout-related configurations for components in the framework. + public struct Layout: Initializable, Updatable { + // MARK: - Radius + + /// A structure representing radius values for components. + public struct Radius { + /// The small radius size. + public var small: CGFloat + /// The medium radius size. + public var medium: CGFloat + /// The large radius size. + public var large: CGFloat + + /// Initializes a new `Radius` instance. + /// + /// - Parameters: + /// - small: The small radius size. + /// - medium: The medium radius size. + /// - large: The large radius size. + public init(small: CGFloat, medium: CGFloat, large: CGFloat) { + self.small = small + self.medium = medium + self.large = large + } + } + + // MARK: - BorderWidth + + /// A structure representing border width values for components. + public struct BorderWidth { + /// The small border width. + public var small: CGFloat + /// The medium border width. + public var medium: CGFloat + /// The large border width. + public var large: CGFloat + + /// Initializes a new `BorderWidth` instance. + /// + /// - Parameters: + /// - small: The small border width. + /// - medium: The medium border width. + /// - large: The large border width. + public init(small: CGFloat, medium: CGFloat, large: CGFloat) { + self.small = small + self.medium = medium + self.large = large + } + } + + // MARK: - AnimationScale + + /// A structure representing animation scale values for components. + /// + /// The values must be between `0.0` and `1.0`. + public struct AnimationScale { + /// The small animation scale. + public var small: CGFloat + /// The medium animation scale. + public var medium: CGFloat + /// The large animation scale. + public var large: CGFloat + + /// Initializes a new `AnimationScale` instance. + /// + /// - Parameters: + /// - small: The small animation scale (0.0–1.0). + /// - medium: The medium animation scale (0.0–1.0). + /// - large: The large animation scale (0.0–1.0). + /// - Warning: This initializer will crash if the values are outside the range of `0.0` to `1.0`. + public init(small: CGFloat, medium: CGFloat, large: CGFloat) { + guard small >= 0 && small <= 1.0, + medium >= 0 && medium <= 1.0, + large >= 0 && large <= 1.0 + else { + fatalError("Animation scale values should be between 0 and 1") + } + + self.small = small + self.medium = medium + self.large = large + } + } + + // MARK: - Typography + + /// A structure representing a set of fonts for different component sizes. + public struct FontSet { + /// The small font. + public var small: UniversalFont + /// The medium font. + public var medium: UniversalFont + /// The large font. + public var large: UniversalFont + + /// Initializes a new `FontSet` instance. + /// + /// - Parameters: + /// - small: The small font. + /// - medium: The medium font. + /// - large: The large font. + public init(small: UniversalFont, medium: UniversalFont, large: UniversalFont) { + self.small = small + self.medium = medium + self.large = large + } + } + + /// A structure representing typography settings for various components. + public struct Typography { + /// The font set for headlines. + public var headline: FontSet + /// The font set for body text. + public var body: FontSet + /// The font set for buttons. + public var button: FontSet + /// The font set for captions. + public var caption: FontSet + + /// Initializes a new `Typography` instance. + /// + /// - Parameters: + /// - headline: The font set for headlines. + /// - body: The font set for body text. + /// - button: The font set for buttons. + /// - caption: The font set for captions. + public init(headline: FontSet, body: FontSet, button: FontSet, caption: FontSet) { + self.headline = headline + self.body = body + self.button = button + self.caption = caption + } + } + + // MARK: - Properties + + /// The opacity level for disabled components. + public var disabledOpacity: CGFloat = 0.5 + + /// The radius configuration for components. + public var componentRadius: Radius = .init( + small: 10.0, + medium: 12.0, + large: 16.0 + ) + + /// The border width configuration for components. + public var borderWidth: BorderWidth = .init( + small: 1.0, + medium: 2.0, + large: 3.0 + ) + + /// The animation scale configuration for components. + public var animationScale: AnimationScale = .init( + small: 0.99, + medium: 0.98, + large: 0.95 + ) + + /// The typography configuration for components. + public var typography: Typography = .init( + headline: .init( + small: .system(size: 14, weight: .semibold), + medium: .system(size: 20, weight: .semibold), + large: .system(size: 28, weight: .semibold) + ), + body: .init( + small: .system(size: 14, weight: .regular), + medium: .system(size: 16, weight: .regular), + large: .system(size: 18, weight: .regular) + ), + button: .init( + small: .system(size: 14, weight: .medium), + medium: .system(size: 16, weight: .medium), + large: .system(size: 20, weight: .medium) + ), + caption: .init( + small: .system(size: 10, weight: .regular), + medium: .system(size: 12, weight: .regular), + large: .system(size: 14, weight: .regular) + ) + ) + + // MARK: - Initialization + + /// Initializes a new `Layout` instance with default values. + public init() {} + } +} diff --git a/Sources/ComponentsKit/Configuration/Palette.swift b/Sources/ComponentsKit/Configuration/Palette.swift new file mode 100644 index 00000000..d247e19d --- /dev/null +++ b/Sources/ComponentsKit/Configuration/Palette.swift @@ -0,0 +1,215 @@ +import Foundation + +extension ComponentsKitConfig { + /// Defines a set of colors that are used for styling components and interfaces. + public struct Palette: Initializable, Updatable { + /// The color for the main background of the interface. + public var background: UniversalColor = .themed( + light: .hex("#FFFFFF"), + dark: .hex("#000000") + ) + /// The color for the secondary background of the interface. + public var secondaryBackground: UniversalColor = .themed( + light: .hex("#F5F5F5"), + dark: .hex("#323335") + ) + /// The color for text labels that contain primary content. + public var foreground: UniversalColor = .themed( + light: .hex("#0B0C0E"), + dark: .hex("#FFFFFF") + ) + /// The color for text labels that contain secondary content. + public var secondaryForeground: UniversalColor = .themed( + light: .hex("#424355"), + dark: .hex("#D6D6D7") + ) + /// The first content color. + public var content1: UniversalColor = .themed( + light: .hex("#EFEFF0"), + dark: .hex("#27272a") + ) + /// The second content color. + public var content2: UniversalColor = .themed( + light: .hex("#D4D4D8"), + dark: .hex("#3F3F46") + ) + /// The third content color. + public var content3: UniversalColor = .themed( + light: .hex("#B4BDC8"), + dark: .hex("#52525b") + ) + /// The forth content color. + public var content4: UniversalColor = .themed( + light: .hex("#8C9197"), + dark: .hex("#86898B") + ) + /// The color for thin borders or divider lines. + public var divider: UniversalColor = .themed( + light: .rgba(r: 11, g: 12, b: 14, a: 0.12), + dark: .rgba(r: 255, g: 255, b: 255, a: 0.15) + ) + /// The primary color. + public var primary: ComponentColor = .init( + main: .themed( + light: .hex("#0B0C0E"), + dark: .hex("#FFFFFF") + ), + contrast: .themed( + light: .hex("#FFFFFF"), + dark: .hex("#0B0C0E") + ), + background: .themed( + light: .hex("#D9D9D9"), + dark: .hex("#515253") + ) + ) + /// The accent color. + public var accent: ComponentColor = .init( + main: .universal(.hex("#007AFF")), + contrast: .universal(.hex("#FFFFFF")), + background: .themed( + light: .hex("#E1EEFE"), + dark: .hex("#2B3E53") + ) + ) + /// The success state color, used for indicating positive actions or statuses. + public var success: ComponentColor = .init( + main: .themed( + light: .hex("#37D45C"), + dark: .hex("#1EC645") + ), + contrast: .themed( + light: .hex("#FFFFFF"), + dark: .hex("#0B0C0E") + ), + background: .themed( + light: .hex("#E1FBE7"), + dark: .hex("#344B3C") + ) + ) + /// The warning state color, used for indicating caution or non-critical alerts. + public var warning: ComponentColor = .init( + main: .themed( + light: .hex("#F4B300"), + dark: .hex("#F4B300") + ), + contrast: .universal(.hex("#0B0C0E")), + background: .themed( + light: .hex("#FFF6DD"), + dark: .hex("#514A35") + ) + ) + /// The danger state color, used for indicating errors, destructive actions, or critical alerts. + public var danger: ComponentColor = .init( + main: .themed( + light: .hex("#F03E53"), + dark: .hex("#D22338") + ), + contrast: .universal(.hex("#FFFFFF")), + background: .themed( + light: .hex("#FFE5E8"), + dark: .hex("#4F353A") + ) + ) + + /// Initializes a new instance of `Palette` with default values. + public init() {} + } +} + +// MARK: - ComponentColor + Palette Colors + +extension ComponentColor { + /// The primary color. + public static var primary: Self { + return ComponentsKitConfig.shared.colors.primary + } + /// The accent color. + public static var accent: Self { + return ComponentsKitConfig.shared.colors.accent + } + /// The success state color, used for indicating positive actions or statuses. + public static var success: Self { + return ComponentsKitConfig.shared.colors.success + } + /// The warning state color, used for indicating caution or non-critical alerts. + public static var warning: Self { + return ComponentsKitConfig.shared.colors.warning + } + /// The danger state color, used for indicating errors, destructive actions, or critical alerts. + public static var danger: Self { + return ComponentsKitConfig.shared.colors.danger + } +} + +// MARK: - UniversalColor + Neutral Colors + +extension UniversalColor { + public static var black: Self { + return .universal(.hex("#000000")) + } + public static var white: Self { + return .universal(.hex("#FFFFFF")) + } +} + +// MARK: - UniversalColor + Palette Colors + +extension UniversalColor { + /// The color for the main background of the interface. + public static var background: Self { + return ComponentsKitConfig.shared.colors.background + } + /// The color for the secondary background of the interface. + public static var secondaryBackground: Self { + return ComponentsKitConfig.shared.colors.secondaryBackground + } + /// The color for text labels that contain primary content. + public static var foreground: Self { + return ComponentsKitConfig.shared.colors.foreground + } + /// The color for text labels that contain secondary content. + public static var secondaryForeground: Self { + return ComponentsKitConfig.shared.colors.secondaryForeground + } + /// The color for thin borders or divider lines. + public static var divider: Self { + return ComponentsKitConfig.shared.colors.divider + } + /// The first content color. + public static var content1: Self { + return ComponentsKitConfig.shared.colors.content1 + } + /// The second content color. + public static var content2: Self { + return ComponentsKitConfig.shared.colors.content2 + } + /// The third content color. + public static var content3: Self { + return ComponentsKitConfig.shared.colors.content3 + } + /// The forth content color. + public static var content4: Self { + return ComponentsKitConfig.shared.colors.content4 + } + /// The primary color. + public static var primary: Self { + return ComponentsKitConfig.shared.colors.primary.main + } + /// The accent color. + public static var accent: Self { + return ComponentsKitConfig.shared.colors.accent.main + } + /// The success state color, used for indicating positive actions or statuses. + public static var success: Self { + return ComponentsKitConfig.shared.colors.success.main + } + /// The warning state color, used for indicating caution or non-critical alerts. + public static var warning: Self { + return ComponentsKitConfig.shared.colors.warning.main + } + /// The danger state color, used for indicating errors, destructive actions, or critical alerts. + public static var danger: Self { + return ComponentsKitConfig.shared.colors.danger.main + } +} diff --git a/Sources/ComponentsKit/Shared/AnimationScale.swift b/Sources/ComponentsKit/Shared/AnimationScale.swift deleted file mode 100644 index 542f90f8..00000000 --- a/Sources/ComponentsKit/Shared/AnimationScale.swift +++ /dev/null @@ -1,30 +0,0 @@ -import Foundation - -public struct AnimationScale: Hashable { - var value: CGFloat - - init(_ value: CGFloat) { - self.value = value - } -} - -extension AnimationScale { - public static var none: Self { - return Self(1.0) - } - public static var small: Self { - return Self(ComponentsKitConfig.shared.layout.animationScale.small) - } - public static var medium: Self { - return Self(ComponentsKitConfig.shared.layout.animationScale.medium) - } - public static var large: Self { - return Self(ComponentsKitConfig.shared.layout.animationScale.large) - } - public static func custom(_ value: CGFloat) -> Self { - guard value >= 0 && value <= 1.0 else { - fatalError("Animation scale value should be between 0 and 1") - } - return Self(value) - } -} diff --git a/Sources/ComponentsKit/Shared/BorderWidth.swift b/Sources/ComponentsKit/Shared/BorderWidth.swift deleted file mode 100644 index e181af39..00000000 --- a/Sources/ComponentsKit/Shared/BorderWidth.swift +++ /dev/null @@ -1,24 +0,0 @@ -import Foundation - -public struct BorderWidth: Hashable { - var value: CGFloat - - init(_ value: CGFloat) { - self.value = value - } -} - -extension BorderWidth { - public static var small: Self { - return Self(ComponentsKitConfig.shared.layout.borderWidth.small) - } - public static var medium: Self { - return Self(ComponentsKitConfig.shared.layout.borderWidth.medium) - } - public static var large: Self { - return Self(ComponentsKitConfig.shared.layout.borderWidth.large) - } - public static func custom(_ value: CGFloat) -> Self { - return Self(value) - } -} diff --git a/Sources/ComponentsKit/Shared/Colors/ComponentColor.swift b/Sources/ComponentsKit/Shared/Colors/ComponentColor.swift new file mode 100644 index 00000000..967c6a30 --- /dev/null +++ b/Sources/ComponentsKit/Shared/Colors/ComponentColor.swift @@ -0,0 +1,33 @@ +import Foundation + +/// A structure that defines a color set for components. +public struct ComponentColor: Hashable { + // MARK: - Properties + + /// The primary color used for the component. + public let main: UniversalColor + + /// The contrast color, typically used for text or elements displayed on top of the `main` color. + public let contrast: UniversalColor + + /// The background color for the component. + public let background: UniversalColor + + // MARK: - Initialization + + /// Initializer. + /// + /// - Parameters: + /// - main: The primary color for the component. + /// - contrast: The color that contrasts with the `main` color, typically used for text or icons. + /// - background: The background color for the component. Defaults to `main` color with 15% opacity if `nil`. + public init( + main: UniversalColor, + contrast: UniversalColor, + background: UniversalColor? = nil + ) { + self.main = main + self.contrast = contrast + self.background = background ?? main.withOpacity(0.15) + } +} diff --git a/Sources/ComponentsKit/Shared/Colors/UniversalColor.swift b/Sources/ComponentsKit/Shared/Colors/UniversalColor.swift new file mode 100644 index 00000000..49749a57 --- /dev/null +++ b/Sources/ComponentsKit/Shared/Colors/UniversalColor.swift @@ -0,0 +1,192 @@ +import SwiftUI +import UIKit + +/// A structure that represents an universal color that can be used in both UIKit and SwiftUI, +/// with light and dark theme variants. +public struct UniversalColor: Hashable { + // MARK: - ColorRepresentable + + /// An enumeration that defines the possible representations of a color. + public enum ColorRepresentable: Hashable { + /// A color defined by its RGBA components. + /// + /// - Parameters: + /// - r: The red component (0–255). + /// - g: The green component (0–255). + /// - b: The blue component (0–255). + /// - a: The alpha (opacity) component (0.0–1.0). + case rgba(r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat) + + /// A color represented by a `UIColor` instance. + case uiColor(UIColor) + + /// A color represented by a SwiftUI `Color` instance. + case color(Color) + + /// Creates a `ColorRepresentable` instance from a hexadecimal string. + /// + /// - Parameter value: A hex string representing the color (e.g., `"#FFFFFF"` or `"FFFFFF"`). + /// - Returns: A `ColorRepresentable` instance with the corresponding RGBA values. + /// - Note: This method assumes the input string has exactly six hexadecimal characters. + /// - Warning: This method will trigger an assertion failure if the input is invalid. + public static func hex(_ value: String) -> Self { + let start: String.Index + if value.hasPrefix("#") { + start = value.index(value.startIndex, offsetBy: 1) + } else { + start = value.startIndex + } + + let hexColor = String(value[start...]) + let scanner = Scanner(string: hexColor) + var hexNumber: UInt64 = 0 + + if hexColor.count == 6 && scanner.scanHexInt64(&hexNumber) { + let r = CGFloat((hexNumber & 0x00ff0000) >> 16) + let g = CGFloat((hexNumber & 0x0000ff00) >> 8) + let b = CGFloat(hexNumber & 0x000000ff) + + return .rgba(r: r, g: g, b: b, a: 1.0) + } else { + assertionFailure( + "Unable to initialize color from the provided hex value: \(value)" + ) + return .rgba(r: 0, g: 0, b: 0, a: 1.0) + } + } + + /// Returns a new `ColorRepresentable` with the specified opacity. + /// + /// - Parameter alpha: The desired opacity (0.0–1.0). + /// - Returns: A `ColorRepresentable` instance with the adjusted opacity. + fileprivate func withOpacity(_ alpha: CGFloat) -> Self { + switch self { + case .rgba(let r, let g, let b, _): + return .rgba(r: r, g: g, b: b, a: alpha) + case .uiColor(let uiColor): + return .uiColor(uiColor.withAlphaComponent(alpha)) + case .color(let color): + return .color(color.opacity(alpha)) + } + } + + /// Converts the `ColorRepresentable` to a `UIColor` instance. + fileprivate var uiColor: UIColor { + switch self { + case .rgba(let red, let green, let blue, let alpha): + return UIColor( + red: red / 255, + green: green / 255, + blue: blue / 255, + alpha: alpha + ) + case .uiColor(let uiColor): + return uiColor + case .color(let color): + return UIColor(color) + } + } + + /// Converts the `ColorRepresentable` to a SwiftUI `Color` instance. + fileprivate var color: Color { + switch self { + case .rgba(let r, let g, let b, let a): + return Color( + red: r / 255, + green: g / 255, + blue: b / 255, + opacity: a + ) + case .uiColor(let uiColor): + return Color(uiColor: uiColor) + case .color(let color): + return color + } + } + } + + // MARK: - Properties + + /// The color used in light mode. + let light: ColorRepresentable + + /// The color used in dark mode. + let dark: ColorRepresentable + + // MARK: - Initialization + + /// Creates a `UniversalColor` with distinct light and dark mode colors. + /// + /// - Parameters: + /// - light: The color to use in light mode. + /// - dark: The color to use in dark mode. + /// - Returns: A new `UniversalColor` instance. + public static func themed( + light: ColorRepresentable, + dark: ColorRepresentable + ) -> Self { + return Self(light: light, dark: dark) + } + + /// Creates a `UniversalColor` with a single color used for both light and dark modes. + /// + /// - Parameter universal: The universal color to use. + /// - Returns: A new `UniversalColor` instance. + public static func universal(_ universal: ColorRepresentable) -> Self { + return Self(light: universal, dark: universal) + } + + // MARK: - Methods + + /// Returns a new `UniversalColor` with the specified opacity. + /// + /// - Parameter alpha: The desired opacity (0.0–1.0). + /// - Returns: A new `UniversalColor` instance with the adjusted opacity. + public func withOpacity(_ alpha: CGFloat) -> Self { + return .init( + light: self.light.withOpacity(alpha), + dark: self.dark.withOpacity(alpha) + ) + } + + /// Returns a disabled version of the color based on a global opacity configuration. + /// + /// - Parameter isEnabled: A Boolean value indicating whether the color should be enabled. + /// - Returns: A new `UniversalColor` instance with reduced opacity if `isEnabled` is `false`. + public func enabled(_ isEnabled: Bool) -> Self { + return isEnabled + ? self + : self.withOpacity(ComponentsKitConfig.shared.layout.disabledOpacity) + } + + // MARK: - Colors + + /// Returns the `UIColor` representation of the color, adapting to the current system theme. + public var uiColor: UIColor { + return UIColor { trait in + switch trait.userInterfaceStyle { + case.light: + return self.light.uiColor + case .dark: + return self.dark.uiColor + default: + return self.light.uiColor + } + } + } + + /// Returns the `Color` representation of the color for a given SwiftUI `ColorScheme`. + /// + /// - Parameter colorScheme: The current color scheme (`.light` or `.dark`). + /// - Returns: The corresponding `Color` instance. + public func color(for colorScheme: ColorScheme) -> Color { + switch colorScheme { + case .light: + return self.light.color + case .dark: + return self.dark.color + @unknown default: + return self.light.color + } + } +} diff --git a/Sources/ComponentsKit/Shared/ComponentColor.swift b/Sources/ComponentsKit/Shared/ComponentColor.swift deleted file mode 100644 index 11d1dc20..00000000 --- a/Sources/ComponentsKit/Shared/ComponentColor.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation - -public struct ComponentColor: Hashable { - // MARK: Properties - - let main: UniversalColor - let contrast: UniversalColor - - // MARK: Initialization - - public init(main: UniversalColor, contrast: UniversalColor) { - self.main = main - self.contrast = contrast - } -} diff --git a/Sources/ComponentsKit/Shared/ComponentRadius.swift b/Sources/ComponentsKit/Shared/ComponentRadius.swift deleted file mode 100644 index d6fcb991..00000000 --- a/Sources/ComponentsKit/Shared/ComponentRadius.swift +++ /dev/null @@ -1,26 +0,0 @@ -import Foundation -import SwiftUI - -public enum ComponentRadius: Hashable { - case none - case small - case medium - case large - case full - case custom(CGFloat) -} - -extension ComponentRadius { - func value(for height: CGFloat = 10_000) -> CGFloat { - let maxValue = height / 2 - let value = switch self { - case .none: CGFloat(0) - case .small: ComponentsKitConfig.shared.layout.componentRadius.small - case .medium: ComponentsKitConfig.shared.layout.componentRadius.medium - case .large: ComponentsKitConfig.shared.layout.componentRadius.large - case .full: height / 2 - case .custom(let value): value - } - return min(value, maxValue) - } -} diff --git a/Sources/ComponentsKit/Shared/ComponentSize.swift b/Sources/ComponentsKit/Shared/ComponentSize.swift deleted file mode 100644 index 80280067..00000000 --- a/Sources/ComponentsKit/Shared/ComponentSize.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Foundation - -public enum ComponentSize: Hashable { - case small - case medium - case large -} diff --git a/Sources/ComponentsKit/Shared/Config.swift b/Sources/ComponentsKit/Shared/Config.swift deleted file mode 100644 index 2f9f7632..00000000 --- a/Sources/ComponentsKit/Shared/Config.swift +++ /dev/null @@ -1,14 +0,0 @@ -import Foundation - -public struct ComponentsKitConfig: Initializable, Updatable { - public var colors: Palette = .init() - public var layout: Layout = .init() - - public init() {} -} - -// MARK: - ComponentsKitConfig + Shared - -extension ComponentsKitConfig { - public static var shared: Self = .init() -} diff --git a/Sources/ComponentsKit/Shared/Fonts/UniversalFont.swift b/Sources/ComponentsKit/Shared/Fonts/UniversalFont.swift new file mode 100644 index 00000000..cd1ec1f0 --- /dev/null +++ b/Sources/ComponentsKit/Shared/Fonts/UniversalFont.swift @@ -0,0 +1,210 @@ +import SwiftUI +import UIKit + +/// A structure that represents an universal font that can be used in both UIKit and SwiftUI, +/// with support for custom and system fonts. +public enum UniversalFont: Hashable { + /// An enumeration that defines the weight of a font. + public enum Weight: Hashable { + /// Ultra-light font weight. + case ultraLight + /// Thin font weight. + case thin + /// Light font weight. + case light + /// Regular font weight. + case regular + /// Medium font weight. + case medium + /// Semi-bold font weight. + case semibold + /// Bold font weight. + case bold + /// Heavy font weight. + case heavy + /// Black (extra-bold) font weight. + case black + } + + /// A custom font with a specific name and size. + /// + /// - Parameters: + /// - name: The name of the font. + /// - size: The size of the font. + case custom(name: String, size: CGFloat) + + /// A system font with a specific size and weight. + /// + /// - Parameters: + /// - size: The size of the font. + /// - weight: The weight of the font, defined by `UniversalFont.Weight`. + case system(size: CGFloat, weight: Weight) + + // MARK: Fonts + + /// Converts the `UniversalFont` to a `UIFont` instance. + /// + /// - Returns: A `UIFont` representation of the `UniversalFont`. + public var uiFont: UIFont { + switch self { + case .custom(let name, let size): + guard let font = UIFont(name: name, size: size) else { + assertionFailure("Unable to initialize font '\(name)'") + return UIFont.systemFont(ofSize: size) + } + return font + case let .system(size, weight): + return UIFont.systemFont(ofSize: size, weight: weight.uiFontWeight) + } + } + + /// Converts the `UniversalFont` to a SwiftUI `Font` instance. + /// + /// - Returns: A `Font` representation of the `UniversalFont`. + public var font: Font { + switch self { + case .custom(let name, let size): + return Font.custom(name, size: size) + case .system(let size, let weight): + return Font.system(size: size, weight: weight.swiftUIFontWeight) + } + } + + // MARK: Helpers + + /// Returns a new `UniversalFont` with the specified size. + /// + /// - Parameter size: The new size for the font. + /// - Returns: A new `UniversalFont` instance with the updated size. + public func withSize(_ size: CGFloat) -> Self { + switch self { + case .custom(let name, _): + return .custom(name: name, size: size) + case .system(_, let weight): + return .system(size: size, weight: weight) + } + } + + /// Returns a new `UniversalFont` with a size adjusted by a relative value. + /// + /// - Parameter shift: The amount to adjust the font size by. + /// - Returns: A new `UniversalFont` instance with the adjusted size. + public func withRelativeSize(_ shift: CGFloat) -> Self { + switch self { + case .custom(let name, let size): + return .custom(name: name, size: size + shift) + case .system(let size, let weight): + return .system(size: size + shift, weight: weight) + } + } +} + +// MARK: Helpers + +extension UniversalFont.Weight { + /// Converts `UniversalFont.Weight` to `UIFont.Weight`. + var uiFontWeight: UIFont.Weight { + switch self { + case .ultraLight: + return .ultraLight + case .thin: + return .thin + case .light: + return .light + case .regular: + return .regular + case .medium: + return .medium + case .semibold: + return .semibold + case .bold: + return .bold + case .heavy: + return .heavy + case .black: + return .black + } + } +} + +extension UniversalFont.Weight { + /// Converts `UniversalFont.Weight` to SwiftUI `Font.Weight`. + var swiftUIFontWeight: Font.Weight { + switch self { + case .ultraLight: + return .ultraLight + case .thin: + return .thin + case .light: + return .light + case .regular: + return .regular + case .medium: + return .medium + case .semibold: + return .semibold + case .bold: + return .bold + case .heavy: + return .heavy + case .black: + return .black + } + } +} + +// MARK: - UniversalFont + Config + +extension UniversalFont { + /// Small headline font. + public static var smHeadline: UniversalFont { + return ComponentsKitConfig.shared.layout.typography.headline.small + } + /// Medium headline font. + public static var mdHeadline: UniversalFont { + return ComponentsKitConfig.shared.layout.typography.headline.medium + } + /// Large headline font. + public static var lgHeadline: UniversalFont { + return ComponentsKitConfig.shared.layout.typography.headline.large + } + + /// Small body font. + public static var smBody: UniversalFont { + return ComponentsKitConfig.shared.layout.typography.body.small + } + /// Medium body font. + public static var mdBody: UniversalFont { + return ComponentsKitConfig.shared.layout.typography.body.medium + } + /// Large body font. + public static var lgBody: UniversalFont { + return ComponentsKitConfig.shared.layout.typography.body.large + } + + /// Small button font. + public static var smButton: UniversalFont { + return ComponentsKitConfig.shared.layout.typography.button.small + } + /// Medium button font. + public static var mdButton: UniversalFont { + return ComponentsKitConfig.shared.layout.typography.button.medium + } + /// Large button font. + public static var lgButton: UniversalFont { + return ComponentsKitConfig.shared.layout.typography.button.large + } + + /// Small caption font. + public static var smCaption: UniversalFont { + return ComponentsKitConfig.shared.layout.typography.caption.small + } + /// Medium caption font. + public static var mdCaption: UniversalFont { + return ComponentsKitConfig.shared.layout.typography.caption.medium + } + /// Large caption font. + public static var lgCaption: UniversalFont { + return ComponentsKitConfig.shared.layout.typography.caption.large + } +} diff --git a/Sources/ComponentsKit/Shared/Layout.swift b/Sources/ComponentsKit/Shared/Layout.swift deleted file mode 100644 index 6dc7eb23..00000000 --- a/Sources/ComponentsKit/Shared/Layout.swift +++ /dev/null @@ -1,92 +0,0 @@ -import Foundation - -public struct Layout: Initializable, Updatable { - // MARK: Radius - - public struct Radius { - public var small: CGFloat - public var medium: CGFloat - public var large: CGFloat - - public init(small: CGFloat, medium: CGFloat, large: CGFloat) { - self.small = small - self.medium = medium - self.large = large - } - } - - // MARK: BorderWidth - - public struct BorderWidth { - public var small: CGFloat - public var medium: CGFloat - public var large: CGFloat - - public init(small: CGFloat, medium: CGFloat, large: CGFloat) { - self.small = small - self.medium = medium - self.large = large - } - } - - // MARK: AnimationScale - - public struct AnimationScale { - public var small: CGFloat - public var medium: CGFloat - public var large: CGFloat - - public init(small: CGFloat, medium: CGFloat, large: CGFloat) { - guard small >= 0 && small <= 1.0, - medium >= 0 && medium <= 1.0, - large >= 0 && large <= 1.0 - else { - fatalError("Animation scale values should be between 0 and 1") - } - - self.small = small - self.medium = medium - self.large = large - } - } - - // MARK: Font - - public struct Font { - public var small: UniversalFont - public var medium: UniversalFont - public var large: UniversalFont - - public init(small: UniversalFont, medium: UniversalFont, large: UniversalFont) { - self.small = small - self.medium = medium - self.large = large - } - } - - // MARK: Properties - - public var disabledOpacity: CGFloat = 0.5 - public var componentRadius: Radius = .init( - small: 10.0, - medium: 14.0, - large: 18.0 - ) - public var borderWidth: BorderWidth = .init( - small: 1.0, - medium: 2.0, - large: 3.0 - ) - public var animationScale: AnimationScale = .init( - small: 0.99, - medium: 0.98, - large: 0.95 - ) - public var componentFont: Font = .init( - small: .system(size: 14, weight: .regular), - medium: .system(size: 18, weight: .regular), - large: .system(size: 22, weight: .regular) - ) - - public init() {} -} diff --git a/Sources/ComponentsKit/Shared/Palette.swift b/Sources/ComponentsKit/Shared/Palette.swift deleted file mode 100644 index cb8602fa..00000000 --- a/Sources/ComponentsKit/Shared/Palette.swift +++ /dev/null @@ -1,129 +0,0 @@ -import Foundation - -public struct Palette: Initializable, Updatable { - /// The UniversalColor for the main background of your interface. - public var background: UniversalColor = .universal(.uiColor(.systemBackground)) - /// The UniversalColor for content layered on top of the main background. - public var secondaryBackground: UniversalColor = .universal(.uiColor(.secondarySystemBackground)) - /// The UniversalColor for text labels that contain primary content. - public var label: UniversalColor = .universal(.uiColor(.label)) - /// The UniversalColor for text labels that contain secondary content. - public var secondaryLabel: UniversalColor = .universal(.uiColor(.secondaryLabel)) - /// The UniversalColor for thin borders or divider lines that allows some underlying content to be visible. - public var divider: UniversalColor = .universal(.uiColor(.separator)) - public var primary: ComponentColor = .init( - main: .universal(.uiColor(.label)), - contrast: .universal(.uiColor(.systemBackground)) - ) - public var secondary: ComponentColor = .init( - main: .universal(.uiColor(.lightGray)), - contrast: .universal(.uiColor(.black)) - ) - public var accent: ComponentColor = .init( - main: .universal(.uiColor(.systemBlue)), - contrast: .universal(.uiColor(.white)) - ) - public var success: ComponentColor = .init( - main: .universal(.uiColor(.systemGreen)), - contrast: .universal(.uiColor(.black)) - ) - public var warning: ComponentColor = .init( - main: .universal(.uiColor(.systemOrange)), - contrast: .universal(.uiColor(.black)) - ) - public var danger: ComponentColor = .init( - main: .universal(.uiColor(.systemRed)), - contrast: .universal(.uiColor(.white)) - ) - - public init() {} -} - -// MARK: - Palette + Config - -extension Palette { - public enum Base { - public static var background: UniversalColor { - return ComponentsKitConfig.shared.colors.background - } - public static var secondaryBackground: UniversalColor { - return ComponentsKitConfig.shared.colors.background - } - public static var divider: UniversalColor { - return ComponentsKitConfig.shared.colors.divider - } - } - public enum Text { - public static var primary: UniversalColor { - return ComponentsKitConfig.shared.colors.label - } - public static var secondary: UniversalColor { - return ComponentsKitConfig.shared.colors.secondaryLabel - } - public static var accent: UniversalColor { - return ComponentsKitConfig.shared.colors.accent.main - } - } - public enum Components { - public static var primary: ComponentColor { - return .primary - } - public static var secondary: ComponentColor { - return .secondary - } - public static var accent: ComponentColor { - return .accent - } - public static var success: ComponentColor { - return .success - } - public static var warning: ComponentColor { - return .warning - } - public static var danger: ComponentColor { - return .danger - } - } -} - -extension ComponentColor { - public static var primary: Self { - return ComponentsKitConfig.shared.colors.primary - } - public static var secondary: Self { - return ComponentsKitConfig.shared.colors.secondary - } - public static var accent: Self { - return ComponentsKitConfig.shared.colors.accent - } - public static var success: Self { - return ComponentsKitConfig.shared.colors.success - } - public static var warning: Self { - return ComponentsKitConfig.shared.colors.warning - } - public static var danger: Self { - return ComponentsKitConfig.shared.colors.danger - } -} - -extension UniversalColor { - public static var primary: Self { - return ComponentsKitConfig.shared.colors.primary.main - } - public static var secondary: Self { - return ComponentsKitConfig.shared.colors.secondary.main - } - public static var accent: Self { - return ComponentsKitConfig.shared.colors.accent.main - } - public static var success: Self { - return ComponentsKitConfig.shared.colors.success.main - } - public static var warning: Self { - return ComponentsKitConfig.shared.colors.warning.main - } - public static var danger: Self { - return ComponentsKitConfig.shared.colors.danger.main - } -} diff --git a/Sources/ComponentsKit/Shared/ComponentVM.swift b/Sources/ComponentsKit/Shared/Protocols/ComponentVM.swift similarity index 100% rename from Sources/ComponentsKit/Shared/ComponentVM.swift rename to Sources/ComponentsKit/Shared/Protocols/ComponentVM.swift diff --git a/Sources/ComponentsKit/Shared/Initializable.swift b/Sources/ComponentsKit/Shared/Protocols/Initializable.swift similarity index 100% rename from Sources/ComponentsKit/Shared/Initializable.swift rename to Sources/ComponentsKit/Shared/Protocols/Initializable.swift diff --git a/Sources/ComponentsKit/Shared/Protocols/UKComponent.swift b/Sources/ComponentsKit/Shared/Protocols/UKComponent.swift new file mode 100644 index 00000000..b47e46dc --- /dev/null +++ b/Sources/ComponentsKit/Shared/Protocols/UKComponent.swift @@ -0,0 +1,21 @@ +import UIKit + +/// A protocol that defines a UIKit component with a configurable model. +/// +/// Types conforming to `UKComponent` are responsible for updating their appearance +/// based on changes to their associated model. +public protocol UKComponent: UIView { + /// A type of the model that defines the appearance properties. + associatedtype Model + + /// A model that defines the appearance properties. + var model: Model { get set } + + /// Updates the component when the model changes. + /// + /// This method is called when the `model` property changes, providing an opportunity + /// to compare the new and old models and update the component's appearance. + /// + /// - Parameter oldModel: The previous model before the update. + func update(_ oldModel: Model) +} diff --git a/Sources/ComponentsKit/Shared/Updatable.swift b/Sources/ComponentsKit/Shared/Protocols/Updatable.swift similarity index 100% rename from Sources/ComponentsKit/Shared/Updatable.swift rename to Sources/ComponentsKit/Shared/Protocols/Updatable.swift diff --git a/Sources/ComponentsKit/Shared/Types/AnimationScale.swift b/Sources/ComponentsKit/Shared/Types/AnimationScale.swift new file mode 100644 index 00000000..9a6f0695 --- /dev/null +++ b/Sources/ComponentsKit/Shared/Types/AnimationScale.swift @@ -0,0 +1,46 @@ +import Foundation + +/// An enumeration that defines how much a component shrinks or expands during animations. +public enum AnimationScale: Hashable { + /// No scaling is applied, meaning the component remains at its original size. + case none + /// A small scaling effect is applied, using a predefined value from the configuration. + case small + /// A medium scaling effect is applied, using a predefined value from the configuration. + case medium + /// A large scaling effect is applied, using a predefined value from the configuration. + case large + /// A custom scaling value. + /// + /// - Parameter value: The custom scale value (0.0–1.0). + case custom(_ value: CGFloat) +} + +extension AnimationScale { + /// The scaling value represented as a `CGFloat`. + /// + /// - Returns: + /// - `1.0` for `.none` (no scaling). + /// - Predefined values from `ComponentsKitConfig` for `.small`, `.medium`, and `.large`. + /// - The custom value provided for `.custom`, constrained between `0.0` and `1.0`. + /// - Note: If the custom value is outside the range `0.0–1.0`, an assertion failure occurs, + /// and a default value of `1.0` is returned. + public var value: CGFloat { + switch self { + case .none: + return 1.0 + case .small: + return ComponentsKitConfig.shared.layout.animationScale.small + case .medium: + return ComponentsKitConfig.shared.layout.animationScale.medium + case .large: + return ComponentsKitConfig.shared.layout.animationScale.large + case .custom(let value): + guard value >= 0 && value <= 1.0 else { + assertionFailure("Animation scale value should be between 0 and 1") + return 1.0 + } + return value + } + } +} diff --git a/Sources/ComponentsKit/Shared/Types/BorderWidth.swift b/Sources/ComponentsKit/Shared/Types/BorderWidth.swift new file mode 100644 index 00000000..f5bd6d32 --- /dev/null +++ b/Sources/ComponentsKit/Shared/Types/BorderWidth.swift @@ -0,0 +1,25 @@ +import Foundation + +/// An enumeration that defines border thickness for components. +public enum BorderWidth: Hashable { + /// A small border width. + case small + /// A medium border width. + case medium + /// A large border width. + case large +} + +extension BorderWidth { + /// The numeric value of the border width as a `CGFloat`. + public var value: CGFloat { + switch self { + case .small: + return ComponentsKitConfig.shared.layout.borderWidth.small + case .medium: + return ComponentsKitConfig.shared.layout.borderWidth.medium + case .large: + return ComponentsKitConfig.shared.layout.borderWidth.large + } + } +} diff --git a/Sources/ComponentsKit/Shared/Types/ComponentRadius.swift b/Sources/ComponentsKit/Shared/Types/ComponentRadius.swift new file mode 100644 index 00000000..f8af07c3 --- /dev/null +++ b/Sources/ComponentsKit/Shared/Types/ComponentRadius.swift @@ -0,0 +1,39 @@ +import Foundation +import SwiftUI + +/// An enumeration that defines the corner radius options for components. +public enum ComponentRadius: Hashable { + /// No corner radius, resulting in sharp edges. + case none + /// A small corner radius. + case small + /// A medium corner radius. + case medium + /// A large corner radius. + case large + /// A fully rounded corner radius, where the radius is half of the component's height. + case full + /// A custom corner radius with a specific value. + /// + /// - Parameter value: The radius value as a `CGFloat`. + case custom(CGFloat) +} + +extension ComponentRadius { + /// Returns the numeric value of the corner radius, ensuring it does not exceed half the component's height. + /// + /// - Parameter height: The height of the component. Defaults to a large number (10,000) for unrestricted calculations. + /// - Returns: The calculated corner radius as a `CGFloat`, capped at half of the height for `full` rounding or custom values. + func value(for height: CGFloat = 10_000) -> CGFloat { + let maxValue = height / 2 + let value = switch self { + case .none: CGFloat(0) + case .small: ComponentsKitConfig.shared.layout.componentRadius.small + case .medium: ComponentsKitConfig.shared.layout.componentRadius.medium + case .large: ComponentsKitConfig.shared.layout.componentRadius.large + case .full: height / 2 + case .custom(let value): value + } + return min(value, maxValue) + } +} diff --git a/Sources/ComponentsKit/Shared/Types/ComponentSize.swift b/Sources/ComponentsKit/Shared/Types/ComponentSize.swift new file mode 100644 index 00000000..14e0360f --- /dev/null +++ b/Sources/ComponentsKit/Shared/Types/ComponentSize.swift @@ -0,0 +1,11 @@ +import Foundation + +/// An enumeration that defines size options for a component. +public enum ComponentSize: Hashable { + /// A small-sized component. + case small + /// A medium-sized component. + case medium + /// A large-sized component. + case large +} diff --git a/Sources/ComponentsKit/Shared/SubmitType.swift b/Sources/ComponentsKit/Shared/Types/SubmitType.swift similarity index 100% rename from Sources/ComponentsKit/Shared/SubmitType.swift rename to Sources/ComponentsKit/Shared/Types/SubmitType.swift diff --git a/Sources/ComponentsKit/Shared/TextAutocapitalization.swift b/Sources/ComponentsKit/Shared/Types/TextAutocapitalization.swift similarity index 100% rename from Sources/ComponentsKit/Shared/TextAutocapitalization.swift rename to Sources/ComponentsKit/Shared/Types/TextAutocapitalization.swift diff --git a/Sources/ComponentsKit/Shared/UKComponent.swift b/Sources/ComponentsKit/Shared/UKComponent.swift deleted file mode 100644 index 93e6de9f..00000000 --- a/Sources/ComponentsKit/Shared/UKComponent.swift +++ /dev/null @@ -1,9 +0,0 @@ -import UIKit - -public protocol UKComponent: UIView { - associatedtype Model - - var model: Model { get set } - - func update(_ oldModel: Model) -} diff --git a/Sources/ComponentsKit/Shared/UniversalColor.swift b/Sources/ComponentsKit/Shared/UniversalColor.swift deleted file mode 100644 index 09f63e57..00000000 --- a/Sources/ComponentsKit/Shared/UniversalColor.swift +++ /dev/null @@ -1,131 +0,0 @@ -import SwiftUI -import UIKit - -public struct UniversalColor: Hashable { - // MARK: ColorRepresentable - - public enum ColorRepresentable: Hashable { - case rgba(r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat) - case uiColor(UIColor) - case color(Color) - - public static func hex(_ value: String) -> Self { - let start: String.Index - if value.hasPrefix("#") { - start = value.index(value.startIndex, offsetBy: 1) - } else { - start = value.startIndex - } - - let hexColor = String(value[start...]) - let scanner = Scanner(string: hexColor) - var hexNumber: UInt64 = 0 - - if hexColor.count == 6 && scanner.scanHexInt64(&hexNumber) { - let r = CGFloat((hexNumber & 0x00ff0000) >> 16) - let g = CGFloat((hexNumber & 0x0000ff00) >> 8) - let b = CGFloat(hexNumber & 0x000000ff) - - return .rgba(r: r, g: g, b: b, a: 1.0) - } else { - fatalError("Unable to initialize color from the provided hex value: \(value)") - } - } - - fileprivate func withOpacity(_ alpha: CGFloat) -> Self { - switch self { - case .rgba(let r, let g, let b, _): - return .rgba(r: r, g: g, b: b, a: alpha) - case .uiColor(let uiColor): - return .uiColor(uiColor.withAlphaComponent(alpha)) - case .color(let color): - return .color(color.opacity(alpha)) - } - } - - fileprivate var uiColor: UIColor { - switch self { - case .rgba(let red, let green, let blue, let alpha): - return UIColor( - red: red / 255, - green: green / 255, - blue: blue / 255, - alpha: alpha - ) - case .uiColor(let uiColor): - return uiColor - case .color(let color): - return UIColor(color) - } - } - - fileprivate var color: Color { - switch self { - case .rgba(let r, let g, let b, let a): - return Color( - red: r / 255, - green: g / 255, - blue: b / 255, - opacity: a - ) - case .uiColor(let uiColor): - return Color(uiColor: uiColor) - case .color(let color): - return color - } - } - } - - // MARK: Properties - - let light: ColorRepresentable - let dark: ColorRepresentable - - // MARK: Initialization - - public static func themed( - light: ColorRepresentable, - dark: ColorRepresentable - ) -> Self { - return Self(light: light, dark: dark) - } - - public static func universal(_ universal: ColorRepresentable) -> Self { - return Self(light: universal, dark: universal) - } - - // MARK: Methods - - public func withOpacity(_ alpha: CGFloat) -> Self { - return .init( - light: self.light.withOpacity(alpha), - dark: self.dark.withOpacity(alpha) - ) - } - - // MARK: Colors - - public var uiColor: UIColor { - return UIColor { trait in - switch trait.userInterfaceStyle { - case.light: - return self.light.uiColor - case .dark: - return self.dark.uiColor - default: - return self.light.uiColor - } - } - } - - public func color(for colorScheme: ColorScheme) -> Color { - switch colorScheme { - case .light: - return self.light.color - case .dark: - return self.dark.color - @unknown default: - return self.light.color - } - } -} diff --git a/Sources/ComponentsKit/Shared/UniversalFont.swift b/Sources/ComponentsKit/Shared/UniversalFont.swift deleted file mode 100644 index b5ea0f0c..00000000 --- a/Sources/ComponentsKit/Shared/UniversalFont.swift +++ /dev/null @@ -1,129 +0,0 @@ -import SwiftUI -import UIKit - -public enum UniversalFont: Hashable { - public enum Weight: Hashable { - case ultraLight - case thin - case light - case regular - case medium - case semibold - case bold - case heavy - case black - } - case custom(name: String, size: CGFloat) - case system(size: CGFloat, weight: Weight) - - // MARK: Fonts - - public var uiFont: UIFont { - switch self { - case .custom(let name, let size): - guard let font = UIFont(name: name, size: size) else { - fatalError("Unable to initialize font '\(name)'") - } - return font - case let .system(size, weight): - return UIFont.systemFont(ofSize: size, weight: weight.uiFontWeight) - } - } - - public var font: Font { - switch self { - case .custom(let name, let size): - return Font.custom(name, size: size) - case .system(let size, let weight): - return Font.system(size: size, weight: weight.swiftUIFontWeight) - } - } - - // MARK: Helpers - - public func withSize(_ size: CGFloat) -> Self { - switch self { - case .custom(let name, _): - return .custom(name: name, size: size) - case .system(_, let weight): - return .system(size: size, weight: weight) - } - } - - public func withRelativeSize(_ shift: CGFloat) -> Self { - switch self { - case .custom(let name, let size): - return .custom(name: name, size: size + shift) - case .system(let size, let weight): - return .system(size: size + shift, weight: weight) - } - } -} - -// MARK: Helpers - -extension UniversalFont.Weight { - var uiFontWeight: UIFont.Weight { - switch self { - case .ultraLight: - return .ultraLight - case .thin: - return .thin - case .light: - return .light - case .regular: - return .regular - case .medium: - return .medium - case .semibold: - return .semibold - case .bold: - return .bold - case .heavy: - return .heavy - case .black: - return .black - } - } -} - -extension UniversalFont.Weight { - var swiftUIFontWeight: Font.Weight { - switch self { - case .ultraLight: - return .ultraLight - case .thin: - return .thin - case .light: - return .light - case .regular: - return .regular - case .medium: - return .medium - case .semibold: - return .semibold - case .bold: - return .bold - case .heavy: - return .heavy - case .black: - return .black - } - } -} - -// MARK: - UniversalFont + Config - -extension UniversalFont { - public enum Component { - public static var small: UniversalFont { - return ComponentsKitConfig.shared.layout.componentFont.small - } - public static var medium: UniversalFont { - return ComponentsKitConfig.shared.layout.componentFont.medium - } - public static var large: UniversalFont { - return ComponentsKitConfig.shared.layout.componentFont.large - } - } -}