@momo-kits/native-kits 0.163.1-beta.3-debug → 0.163.1-beta.5-debug
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +1 -1
- package/compose/build.gradle.kts +2 -1
- package/compose/build.gradle.kts.backup +1 -0
- package/compose/compose.podspec +1 -1
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/OsFontScale.android.kt +7 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +8 -10
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +3 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +56 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +11 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +40 -55
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +69 -26
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +3 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +21 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +3 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +22 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +28 -21
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +4 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +112 -115
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +11 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +3 -4
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +3 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +2 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/OsFontScale.kt +9 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +66 -0
- package/gradle/libs.versions.toml +2 -0
- package/gradle.properties +1 -1
- package/ios/Application/ApplicationEnvironment.swift +11 -1
- package/ios/Application/Components.swift +34 -44
- package/ios/Application/FloatingButton.swift +10 -9
- package/ios/Application/HeaderRight.swift +79 -45
- package/ios/Application/Navigation/BottomTab/BottomTab.swift +103 -0
- package/ios/Application/Navigation/BottomTab/BottomTabBar.swift +215 -0
- package/ios/Application/Navigation/BottomTab/CurvedContainer.swift +65 -0
- package/ios/Application/Navigation/HeaderBackProps.swift +66 -0
- package/ios/Application/Navigation/HeaderTitle.swift +57 -0
- package/ios/Application/Navigation/HeaderUser.swift +209 -0
- package/ios/Application/Navigation/NavigationContainer.swift +176 -0
- package/ios/Application/Navigation/NavigationOptions.swift +109 -0
- package/ios/Application/Navigation/Navigator.swift +364 -0
- package/ios/Application/Navigation/Overplay/BottomSheet.swift +188 -0
- package/ios/Application/Navigation/Overplay/ModalScreen.swift +68 -0
- package/ios/Application/Navigation/Overplay/SnackBar.swift +144 -0
- package/ios/Application/Navigation/component/Header.swift +178 -0
- package/ios/Application/Navigation/component/HeaderBackground.swift +83 -0
- package/ios/Application/Navigation/component/HeaderColor.swift +65 -0
- package/ios/Application/Screen.swift +2 -1
- package/ios/Application/StackScreen.swift +453 -0
- package/ios/Avatar/Avatar.swift +193 -0
- package/ios/Badge/Badge.swift +11 -14
- package/ios/Badge/BadgeRibbon.swift +12 -21
- package/ios/BaselineView/BaselineView.swift +163 -0
- package/ios/Button/Button.swift +61 -22
- package/ios/Carousel/Carousel.swift +181 -0
- package/ios/Checkbox/Checkbox.swift +14 -13
- package/ios/Chip/Chip.swift +23 -9
- package/ios/Collapse/Collapse.swift +182 -0
- package/ios/Colors+Radius+Spacing/Colors.swift +12 -12
- package/ios/Colors+Radius+Spacing/Theme.swift +295 -0
- package/ios/DateTimePicker/DateTimePicker.swift +210 -0
- package/ios/DateTimePicker/DateTimePickerTypes.swift +55 -0
- package/ios/DateTimePicker/DateTimePickerUtils.swift +167 -0
- package/ios/DateTimePicker/WheelPicker.swift +161 -0
- package/ios/Divider/Divider.swift +16 -0
- package/ios/Extensions/Color++.swift +32 -0
- package/ios/Icon/Icon.swift +6 -1
- package/ios/IconButton/IconButton.swift +112 -0
- package/ios/Input/Input.swift +41 -25
- package/ios/Input/InputPhoneNumber.swift +20 -19
- package/ios/Input/InputSearch.swift +100 -47
- package/ios/Input/InputTextArea.swift +66 -40
- package/ios/InputDropDown/InputDropDown.swift +198 -0
- package/ios/InputMoney/InputMoney.swift +335 -0
- package/ios/InputOTP/InputOTP.swift +210 -0
- package/ios/Loader/Loader.swift +113 -0
- package/ios/MoMoUIKits.podspec +5 -0
- package/ios/Pagination/PaginationDot.swift +61 -0
- package/ios/Pagination/PaginationNumber.swift +35 -0
- package/ios/Pagination/PaginationScroll.swift +101 -0
- package/ios/Pagination/PaginationWhiteDot.swift +37 -0
- package/ios/Popup/PopupDisplay.swift +8 -16
- package/ios/Popup/PopupInput.swift +2 -8
- package/ios/Popup/PopupNotify.swift +219 -0
- package/ios/ProgressInfo/ProgressInfo.swift +294 -0
- package/ios/Radio/Radio.swift +70 -0
- package/ios/Rating/Rating.swift +82 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/AccentColor.colorset/Contents.json +11 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/Background.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/Border.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/Card.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/Contents.json +6 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/Error.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/Primary.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/PrimaryDark.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/PrimaryLight.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/Secondary.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/Success.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/Text.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/TextSecondary.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/Contents.json +9 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/background-default.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/background-disable.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/background-pressed.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/background-selected.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/background-surface.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/background-tonal.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/border-default.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/border-disable.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/gradient.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/primary.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/secondary.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/success-container.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/success-primary.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/success-secondary.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/text-default.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/text-disable.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/text-hint.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/text-secondary.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/Warning.colorset/Contents.json +38 -0
- package/ios/Skeleton/Skeleton.swift +99 -0
- package/ios/Slider/Slider.swift +237 -0
- package/ios/Stepper/Stepper.swift +220 -0
- package/ios/Steps/Steps.swift +451 -0
- package/ios/SuggestAction/SuggestAction.swift +97 -0
- package/ios/Swipeable/SwipeCell.swift +185 -0
- package/ios/Swipeable/SwipeCellModel.swift +21 -0
- package/ios/TabView/TabView.swift +530 -0
- package/ios/Tag/Tag.swift +98 -0
- package/ios/Title/Title.swift +269 -0
- package/ios/Tooltip/Tooltip.swift +482 -0
- package/ios/Typography/FontScaleStore.swift +19 -0
- package/ios/Typography/Text.swift +86 -17
- package/ios/Typography/Typography.swift +12 -14
- package/ios/Uploader/Uploader.swift +183 -0
- package/ios/native-kits.podspec +61 -8
- package/ios-demo/MoMoUIKitsDemo.podspec +20 -0
- package/ios-demo/README.md +120 -0
- package/ios-demo/Resources/dot_loading.json +558 -0
- package/ios-demo/Resources/icon.json +4052 -0
- package/ios-demo/Resources/lottie_circle_loader.json +1 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/DemoScaffold.swift +48 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsBadgeDemoView.swift +25 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsButtonDemoView.swift +26 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsCheckboxDemoView.swift +19 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsChipDemoView.swift +33 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsInputDemoView.swift +48 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsPopupDemoView.swift +21 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsRadioDemoView.swift +17 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsTypographyDemoView.swift +28 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/MoMoUIKitsDemoContext.swift +31 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/MoMoUIKitsDemoFactory.swift +24 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/AnimatedHeaderDemo.swift +110 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/AvatarDemo.swift +93 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/BadgeDemo.swift +269 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/BaselineView.swift +117 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/BottomTabDemo.swift +89 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ButtonDemo.swift +100 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/CarouselDemo.swift +126 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/CheckboxDemo.swift +63 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ChipDemo.swift +20 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/CollapseDemo.swift +93 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +107 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DateTimePickerDemo.swift +124 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DividerDemo.swift +14 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/HeaderUserDemo.swift +49 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +278 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IconButtonDemo.swift +58 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ImageDemo.swift +78 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InformationDemo.swift +138 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputDemo.swift +111 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputDropDownDemo.swift +136 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputMoneyDemo.swift +107 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputOTPDemo.swift +73 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputPhoneNumberDemo.swift +100 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputSearchDemo.swift +32 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputTextAreaDemo.swift +57 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/LoaderDemo.swift +70 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +227 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PaginationDemo.swift +72 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupDisplayDemo.swift +175 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupNotifyDemo.swift +121 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupPromotionDemo.swift +76 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ProgressInfoDemo.swift +89 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/RadioDemo.swift +32 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/RatingDemo.swift +56 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SkeletonDemo.swift +32 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SliderDemo.swift +150 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SnackBarDemo.swift +138 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/StepperDemo.swift +103 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/StepsDemo.swift +94 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SuggestActionDemo.swift +79 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SwipeDemo.swift +152 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SwitchDemo.swift +33 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TabViewDemo.swift +190 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TagDemo.swift +34 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ThemeDemo.swift +182 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TitleDemo.swift +109 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TooltipDemo.swift +187 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TypographyDemo.swift +87 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/UploaderDemo.swift +131 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SwiftUIDemoBrowserView.swift +51 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/UIKitsDemoModels.swift +15 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/UIKitsDemoRegistry.swift +73 -0
- package/package.json +1 -1
- package/publish.sh +50 -0
- package/scripts/gen-ios-color.mjs +136 -0
- package/example/ios/Example.xcodeproj/xcuserdata/huynhdung.xcuserdatad/xcschemes/xcschememanagement.plist +0 -14
- package/example/ios/Example.xcworkspace/xcuserdata/huynhdung.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- package/example/ios/Example.xcworkspace/xcuserdata/huynhdung.xcuserdatad/xcschemes/xcschememanagement.plist +0 -5
- package/example/ios/Pods/Pods.xcodeproj/xcuserdata/huynhdung.xcuserdatad/xcschemes/MoMoUIKits.xcscheme +0 -58
- package/example/ios/Pods/Pods.xcodeproj/xcuserdata/huynhdung.xcuserdatad/xcschemes/Pods-Example.xcscheme +0 -58
- package/example/ios/Pods/Pods.xcodeproj/xcuserdata/huynhdung.xcuserdatad/xcschemes/SDWebImage.xcscheme +0 -58
- package/example/ios/Pods/Pods.xcodeproj/xcuserdata/huynhdung.xcuserdatad/xcschemes/SDWebImageSwiftUI.xcscheme +0 -58
- package/example/ios/Pods/Pods.xcodeproj/xcuserdata/huynhdung.xcuserdatad/xcschemes/SkeletonUI.xcscheme +0 -58
- package/example/ios/Pods/Pods.xcodeproj/xcuserdata/huynhdung.xcuserdatad/xcschemes/xcschememanagement.plist +0 -46
- package/ios/Application/FontScaleStore.swift +0 -59
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
//
|
|
2
|
+
// InputMoney.swift
|
|
3
|
+
// MoMoUIKits
|
|
4
|
+
//
|
|
5
|
+
// Native SwiftUI reimplementation of Compose `InputMoney`.
|
|
6
|
+
// The bound `text` holds RAW DIGITS only (mirrors Compose, where the
|
|
7
|
+
// field stores the numeric string and a VisualTransformation renders the
|
|
8
|
+
// thousands-separated currency). Display formatting is applied live.
|
|
9
|
+
//
|
|
10
|
+
|
|
11
|
+
import Foundation
|
|
12
|
+
import SwiftUI
|
|
13
|
+
import UIKit
|
|
14
|
+
|
|
15
|
+
// MARK: - Currency formatting
|
|
16
|
+
|
|
17
|
+
/// Mirrors Compose `formatNumberToMoney` exactly (group separator ".").
|
|
18
|
+
/// Input is a raw-digit string; output has thousands separators + currency.
|
|
19
|
+
public func formatNumberToMoney(_ digits: String, currency: String = "đ") -> String {
|
|
20
|
+
let numeric = digits.filter { $0.isNumber }
|
|
21
|
+
if numeric.isEmpty || numeric == "0" {
|
|
22
|
+
return "0" + currency
|
|
23
|
+
}
|
|
24
|
+
// Strip leading zeros to match Long parsing semantics.
|
|
25
|
+
var trimmed = String(numeric.drop(while: { $0 == "0" }))
|
|
26
|
+
if trimmed.isEmpty { trimmed = "0" }
|
|
27
|
+
if trimmed == "0" {
|
|
28
|
+
return "0" + currency
|
|
29
|
+
}
|
|
30
|
+
if trimmed.count < 3 {
|
|
31
|
+
return trimmed + currency
|
|
32
|
+
}
|
|
33
|
+
var chars: [Character] = []
|
|
34
|
+
var count = 0
|
|
35
|
+
let reversed = Array(trimmed.reversed())
|
|
36
|
+
for i in 0..<reversed.count {
|
|
37
|
+
count += 1
|
|
38
|
+
chars.append(reversed[i])
|
|
39
|
+
// i counts from the least significant digit; original index = len-1-i.
|
|
40
|
+
let originalIndex = reversed.count - 1 - i
|
|
41
|
+
if count == 3 && originalIndex >= 1 {
|
|
42
|
+
chars.append(".")
|
|
43
|
+
count = 0
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return String(chars.reversed()) + currency
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// MARK: - InputMoney
|
|
50
|
+
|
|
51
|
+
public struct InputMoney: View {
|
|
52
|
+
@Environment(\.appTheme) private var theme
|
|
53
|
+
@Binding public var text: String
|
|
54
|
+
|
|
55
|
+
public var floatingValue: String
|
|
56
|
+
public var floatingValueColor: Color?
|
|
57
|
+
public var floatingIcon: String
|
|
58
|
+
public var floatingIconColor: Color?
|
|
59
|
+
public var placeholder: String
|
|
60
|
+
public var size: InputSize
|
|
61
|
+
public var error: String
|
|
62
|
+
public var errorSpacing: Bool
|
|
63
|
+
public var disabled: Bool
|
|
64
|
+
public var icon: String
|
|
65
|
+
public var iconColor: Color?
|
|
66
|
+
public var leadingIcon: String
|
|
67
|
+
public var leadingIconColor: Color?
|
|
68
|
+
public var loading: Bool
|
|
69
|
+
public var required: Bool
|
|
70
|
+
public var fontWeight: InputFontWeight
|
|
71
|
+
public var keyboardType: UIKeyboardType
|
|
72
|
+
public var onChangeText: ((String) -> Void)?
|
|
73
|
+
public var onPressIcon: (() -> Void)?
|
|
74
|
+
public var onFocus: (() -> Void)?
|
|
75
|
+
public var onBlur: (() -> Void)?
|
|
76
|
+
|
|
77
|
+
@State private var isFocused: Bool = false
|
|
78
|
+
|
|
79
|
+
public init(
|
|
80
|
+
text: Binding<String>,
|
|
81
|
+
floatingValue: String = "",
|
|
82
|
+
floatingValueColor: Color? = nil,
|
|
83
|
+
floatingIcon: String = "",
|
|
84
|
+
floatingIconColor: Color? = nil,
|
|
85
|
+
placeholder: String = "0đ",
|
|
86
|
+
size: InputSize = .small,
|
|
87
|
+
error: String = "",
|
|
88
|
+
errorSpacing: Bool = false,
|
|
89
|
+
disabled: Bool = false,
|
|
90
|
+
icon: String = "",
|
|
91
|
+
iconColor: Color? = nil,
|
|
92
|
+
leadingIcon: String = "",
|
|
93
|
+
leadingIconColor: Color? = nil,
|
|
94
|
+
loading: Bool = false,
|
|
95
|
+
required: Bool = false,
|
|
96
|
+
fontWeight: InputFontWeight = .bold,
|
|
97
|
+
keyboardType: UIKeyboardType = .numberPad,
|
|
98
|
+
onChangeText: ((String) -> Void)? = nil,
|
|
99
|
+
onPressIcon: (() -> Void)? = nil,
|
|
100
|
+
onFocus: (() -> Void)? = nil,
|
|
101
|
+
onBlur: (() -> Void)? = nil
|
|
102
|
+
) {
|
|
103
|
+
self._text = text
|
|
104
|
+
self.floatingValue = floatingValue
|
|
105
|
+
self.floatingValueColor = floatingValueColor
|
|
106
|
+
self.floatingIcon = floatingIcon
|
|
107
|
+
self.floatingIconColor = floatingIconColor
|
|
108
|
+
self.placeholder = placeholder
|
|
109
|
+
self.size = size
|
|
110
|
+
self.error = error
|
|
111
|
+
self.errorSpacing = errorSpacing
|
|
112
|
+
self.disabled = disabled
|
|
113
|
+
self.icon = icon
|
|
114
|
+
self.iconColor = iconColor
|
|
115
|
+
self.leadingIcon = leadingIcon
|
|
116
|
+
self.leadingIconColor = leadingIconColor
|
|
117
|
+
self.loading = loading
|
|
118
|
+
self.required = required
|
|
119
|
+
self.fontWeight = fontWeight
|
|
120
|
+
self.keyboardType = keyboardType
|
|
121
|
+
self.onChangeText = onChangeText
|
|
122
|
+
self.onPressIcon = onPressIcon
|
|
123
|
+
self.onFocus = onFocus
|
|
124
|
+
self.onBlur = onBlur
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// MARK: - Body
|
|
128
|
+
|
|
129
|
+
public var body: some View {
|
|
130
|
+
VStack(alignment: .leading, spacing: 4) {
|
|
131
|
+
ZStack(alignment: .topLeading) {
|
|
132
|
+
// Floating label
|
|
133
|
+
if !floatingValue.isEmpty || !floatingIcon.isEmpty {
|
|
134
|
+
HStack(spacing: Spacing.XS) {
|
|
135
|
+
MomoText(floatingValue, typography: .labelSMedium, color: floatingTitleColor)
|
|
136
|
+
if required {
|
|
137
|
+
MomoText("*", typography: .labelSMedium, color: theme.colors.error.primary)
|
|
138
|
+
}
|
|
139
|
+
if !floatingIcon.isEmpty {
|
|
140
|
+
Icon(source: floatingIcon, size: 16, color: floatingIconTintColor)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
.padding(.horizontal, Spacing.S)
|
|
144
|
+
.background(theme.colors.background.surface)
|
|
145
|
+
.offset(x: Spacing.S, y: -8)
|
|
146
|
+
.zIndex(10)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Input container
|
|
150
|
+
HStack(alignment: .center, spacing: 0) {
|
|
151
|
+
// Leading icon
|
|
152
|
+
if !leadingIcon.isEmpty {
|
|
153
|
+
Icon(source: leadingIcon, size: size == .small ? 24 : 32, color: leadingIconTintColor)
|
|
154
|
+
.padding(.trailing, Spacing.XS)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Money field
|
|
158
|
+
ZStack(alignment: .leading) {
|
|
159
|
+
if text.filter({ $0.isNumber }).isEmpty {
|
|
160
|
+
MomoText(
|
|
161
|
+
placeholder,
|
|
162
|
+
typography: fontWeight == .bold ? .actionSBold : .bodyDefaultRegular,
|
|
163
|
+
color: placeholderColor
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
MoneyTextField(
|
|
167
|
+
digits: $text,
|
|
168
|
+
keyboardType: keyboardType,
|
|
169
|
+
fontWeight: fontWeight == .bold ? .bold : .regular,
|
|
170
|
+
textColor: UIColor(textColor),
|
|
171
|
+
cursorColor: UIColor(theme.colors.primary),
|
|
172
|
+
isDisabled: disabled,
|
|
173
|
+
onFocusChange: { focused in handleFocusChange(focused) },
|
|
174
|
+
onChangeText: onChangeText
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
178
|
+
|
|
179
|
+
// Clear button (focused + has value)
|
|
180
|
+
if isFocused && !text.filter({ $0.isNumber }).isEmpty {
|
|
181
|
+
SwiftUI.Button(action: {
|
|
182
|
+
text = ""
|
|
183
|
+
onChangeText?("")
|
|
184
|
+
}) {
|
|
185
|
+
Icon(source: "24_navigation_close_circle_full", size: 16, color: theme.colors.text.hint)
|
|
186
|
+
.padding(.leading, Spacing.XS)
|
|
187
|
+
.accessibility(identifier: "ic_clear")
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Loading indicator
|
|
192
|
+
if loading {
|
|
193
|
+
ActivityIndicator(isAnimating: .constant(true), style: .medium)
|
|
194
|
+
.frame(width: 16, height: 16)
|
|
195
|
+
.padding(.leading, Spacing.S)
|
|
196
|
+
} else if !icon.isEmpty {
|
|
197
|
+
// Right icon
|
|
198
|
+
SwiftUI.Button(action: { onPressIcon?() }) {
|
|
199
|
+
Icon(source: icon, size: 24, color: iconTintColor)
|
|
200
|
+
.padding(.leading, Spacing.S)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
.padding(.horizontal, Spacing.M)
|
|
205
|
+
.frame(height: scaleSize(size == .small ? 48 : 56, 1.1))
|
|
206
|
+
.background(
|
|
207
|
+
RoundedRectangle(cornerRadius: Radius.S)
|
|
208
|
+
.fill(theme.colors.background.surface)
|
|
209
|
+
)
|
|
210
|
+
.overlay(
|
|
211
|
+
RoundedRectangle(cornerRadius: Radius.S)
|
|
212
|
+
.stroke(borderColor, lineWidth: 1)
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
ErrorView(errorMessage: error, errorSpacing: errorSpacing, hintText: "")
|
|
217
|
+
}
|
|
218
|
+
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { _ in
|
|
219
|
+
if isFocused {
|
|
220
|
+
DispatchQueue.main.async { handleFocusChange(false) }
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// MARK: - Helpers
|
|
226
|
+
|
|
227
|
+
private func handleFocusChange(_ focused: Bool) {
|
|
228
|
+
guard isFocused != focused else { return }
|
|
229
|
+
isFocused = focused
|
|
230
|
+
if focused { onFocus?() } else { onBlur?() }
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private var borderColor: Color {
|
|
234
|
+
if disabled { return theme.colors.border.disable }
|
|
235
|
+
if !error.isEmpty { return theme.colors.error.primary }
|
|
236
|
+
if isFocused { return theme.colors.primary }
|
|
237
|
+
return theme.colors.border.default
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private var textColor: Color { disabled ? theme.colors.text.disable : theme.colors.text.default }
|
|
241
|
+
private var placeholderColor: Color { disabled ? theme.colors.text.disable : theme.colors.text.hint }
|
|
242
|
+
private var iconTintColor: Color { disabled ? theme.colors.text.disable : (iconColor ?? theme.colors.text.default) }
|
|
243
|
+
private var leadingIconTintColor: Color { disabled ? theme.colors.text.disable : (leadingIconColor ?? theme.colors.text.hint) }
|
|
244
|
+
private var floatingTitleColor: Color { disabled ? theme.colors.text.disable : (floatingValueColor ?? theme.colors.text.hint) }
|
|
245
|
+
private var floatingIconTintColor: Color { disabled ? theme.colors.text.disable : (floatingIconColor ?? theme.colors.text.default) }
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// MARK: - MoneyTextField (UIViewRepresentable)
|
|
249
|
+
|
|
250
|
+
/// A UITextField that shows live thousands-separated currency while the bound
|
|
251
|
+
/// `digits` string holds only the raw numeric value. Caret is pinned before
|
|
252
|
+
/// the trailing currency symbol.
|
|
253
|
+
private struct MoneyTextField: UIViewRepresentable {
|
|
254
|
+
@Binding var digits: String
|
|
255
|
+
var keyboardType: UIKeyboardType
|
|
256
|
+
var fontWeight: UIFont.Weight
|
|
257
|
+
var textColor: UIColor
|
|
258
|
+
var cursorColor: UIColor
|
|
259
|
+
var isDisabled: Bool
|
|
260
|
+
var onFocusChange: (Bool) -> Void
|
|
261
|
+
var onChangeText: ((String) -> Void)?
|
|
262
|
+
|
|
263
|
+
func makeCoordinator() -> Coordinator { Coordinator(self) }
|
|
264
|
+
|
|
265
|
+
func makeUIView(context: Context) -> UITextField {
|
|
266
|
+
let field = UITextField()
|
|
267
|
+
field.delegate = context.coordinator
|
|
268
|
+
field.keyboardType = keyboardType
|
|
269
|
+
field.font = UIFont.systemFont(ofSize: scaleSize(20), weight: fontWeight)
|
|
270
|
+
field.textColor = textColor
|
|
271
|
+
field.isEnabled = !isDisabled
|
|
272
|
+
field.tintColor = cursorColor
|
|
273
|
+
field.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
274
|
+
field.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
275
|
+
field.text = displayValue(digits)
|
|
276
|
+
field.addTarget(
|
|
277
|
+
context.coordinator,
|
|
278
|
+
action: #selector(Coordinator.editingChanged(_:)),
|
|
279
|
+
for: .editingChanged
|
|
280
|
+
)
|
|
281
|
+
return field
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
func updateUIView(_ field: UITextField, context: Context) {
|
|
285
|
+
context.coordinator.parent = self
|
|
286
|
+
field.isEnabled = !isDisabled
|
|
287
|
+
field.textColor = textColor
|
|
288
|
+
field.tintColor = cursorColor
|
|
289
|
+
let expected = displayValue(digits)
|
|
290
|
+
if !field.isFirstResponder && field.text != expected {
|
|
291
|
+
field.text = expected
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private func displayValue(_ raw: String) -> String {
|
|
296
|
+
let numeric = raw.filter { $0.isNumber }
|
|
297
|
+
return numeric.isEmpty ? "" : formatNumberToMoney(numeric)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
final class Coordinator: NSObject, UITextFieldDelegate {
|
|
301
|
+
var parent: MoneyTextField
|
|
302
|
+
|
|
303
|
+
init(_ parent: MoneyTextField) { self.parent = parent }
|
|
304
|
+
|
|
305
|
+
func textFieldDidBeginEditing(_ textField: UITextField) {
|
|
306
|
+
parent.onFocusChange(true)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
func textFieldDidEndEditing(_ textField: UITextField) {
|
|
310
|
+
parent.onFocusChange(false)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
|
314
|
+
textField.resignFirstResponder()
|
|
315
|
+
return true
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
@objc func editingChanged(_ textField: UITextField) {
|
|
319
|
+
// Extract digits from whatever the user typed/deleted.
|
|
320
|
+
let numeric = (textField.text ?? "").filter { $0.isNumber }
|
|
321
|
+
let formatted = numeric.isEmpty ? "" : formatNumberToMoney(numeric)
|
|
322
|
+
if textField.text != formatted {
|
|
323
|
+
textField.text = formatted
|
|
324
|
+
// Keep caret before the currency suffix ("đ").
|
|
325
|
+
if let end = textField.position(from: textField.endOfDocument, offset: -1) {
|
|
326
|
+
textField.selectedTextRange = textField.textRange(from: end, to: end)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if parent.digits != numeric {
|
|
330
|
+
parent.digits = numeric
|
|
331
|
+
}
|
|
332
|
+
parent.onChangeText?(numeric)
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import SwiftUI
|
|
3
|
+
|
|
4
|
+
// MARK: - OTPCaret
|
|
5
|
+
|
|
6
|
+
/// Blinking caret shown at the active OTP cell, mirroring Compose `OTPCaret`.
|
|
7
|
+
private struct OTPCaret: View {
|
|
8
|
+
@Environment(\.appTheme) private var theme
|
|
9
|
+
@State private var visible: Bool = true
|
|
10
|
+
|
|
11
|
+
var body: some View {
|
|
12
|
+
HStack(spacing: 0) {
|
|
13
|
+
Rectangle()
|
|
14
|
+
.fill(theme.colors.primary)
|
|
15
|
+
.frame(width: 1, height: scaleSize(12))
|
|
16
|
+
.opacity(visible ? 1 : 0)
|
|
17
|
+
.animation(
|
|
18
|
+
.linear(duration: 0.5).repeatForever(autoreverses: true),
|
|
19
|
+
value: visible
|
|
20
|
+
)
|
|
21
|
+
MomoText("-", typography: .descriptionDefaultRegular, color: theme.colors.text.hint)
|
|
22
|
+
}
|
|
23
|
+
.onAppear { visible = false }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// MARK: - InputOTP
|
|
28
|
+
|
|
29
|
+
/// Native SwiftUI port of Compose `InputOTP`.
|
|
30
|
+
///
|
|
31
|
+
/// A row of OTP cells driven by a hidden `TextField`. When `length` is provided
|
|
32
|
+
/// it renders fixed-count slots (`-` placeholders + blinking caret); otherwise it
|
|
33
|
+
/// renders the typed characters followed by a trailing caret while focused.
|
|
34
|
+
public struct InputOTP: View {
|
|
35
|
+
@Environment(\.appTheme) private var theme
|
|
36
|
+
@Binding public var value: String
|
|
37
|
+
|
|
38
|
+
public var length: Int?
|
|
39
|
+
public var errorMessage: String
|
|
40
|
+
public var errorSpacing: Bool
|
|
41
|
+
public var floatingValue: String
|
|
42
|
+
public var placeholder: String
|
|
43
|
+
public var dataType: String
|
|
44
|
+
public var onChangeText: ((String) -> Void)?
|
|
45
|
+
public var onFocus: (() -> Void)?
|
|
46
|
+
public var onBlur: (() -> Void)?
|
|
47
|
+
|
|
48
|
+
@FocusState private var isFocused: Bool
|
|
49
|
+
@State private var hasBeenFocused: Bool = false
|
|
50
|
+
|
|
51
|
+
private var maxLength: Int { length ?? 10 }
|
|
52
|
+
|
|
53
|
+
public init(
|
|
54
|
+
value: Binding<String>,
|
|
55
|
+
length: Int? = nil,
|
|
56
|
+
errorMessage: String = "",
|
|
57
|
+
errorSpacing: Bool = false,
|
|
58
|
+
floatingValue: String = "Label",
|
|
59
|
+
placeholder: String = "",
|
|
60
|
+
dataType: String = "number",
|
|
61
|
+
onChangeText: ((String) -> Void)? = nil,
|
|
62
|
+
onFocus: (() -> Void)? = nil,
|
|
63
|
+
onBlur: (() -> Void)? = nil
|
|
64
|
+
) {
|
|
65
|
+
self._value = value
|
|
66
|
+
self.length = length
|
|
67
|
+
self.errorMessage = errorMessage
|
|
68
|
+
self.errorSpacing = errorSpacing
|
|
69
|
+
self.floatingValue = floatingValue
|
|
70
|
+
self.placeholder = placeholder
|
|
71
|
+
self.dataType = dataType
|
|
72
|
+
self.onChangeText = onChangeText
|
|
73
|
+
self.onFocus = onFocus
|
|
74
|
+
self.onBlur = onBlur
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public var body: some View {
|
|
78
|
+
VStack(alignment: .leading, spacing: 0) {
|
|
79
|
+
ZStack {
|
|
80
|
+
// Hidden text field that owns the keyboard / cursor.
|
|
81
|
+
hiddenField
|
|
82
|
+
|
|
83
|
+
// Floating label over the border.
|
|
84
|
+
if !floatingValue.isEmpty {
|
|
85
|
+
HStack {
|
|
86
|
+
MomoText(floatingValue, typography: .labelSMedium, color: theme.colors.text.hint)
|
|
87
|
+
}
|
|
88
|
+
.padding(.horizontal, Spacing.S)
|
|
89
|
+
.background(theme.colors.background.surface)
|
|
90
|
+
.offset(y: -28)
|
|
91
|
+
.zIndex(10)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Bordered container with cells.
|
|
95
|
+
cells
|
|
96
|
+
.frame(maxWidth: .infinity)
|
|
97
|
+
.frame(height: 56)
|
|
98
|
+
.overlay(
|
|
99
|
+
RoundedRectangle(cornerRadius: Radius.S)
|
|
100
|
+
.stroke(borderColor, lineWidth: 1)
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
// Trailing clear button.
|
|
104
|
+
HStack {
|
|
105
|
+
Spacer()
|
|
106
|
+
SwiftUI.Button(action: clearText) {
|
|
107
|
+
Icon(source: "24_navigation_close_circle_full", size: 12, color: theme.colors.text.hint)
|
|
108
|
+
.padding(.trailing, 12)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
.zIndex(10)
|
|
112
|
+
}
|
|
113
|
+
.frame(height: 56)
|
|
114
|
+
.contentShape(Rectangle())
|
|
115
|
+
.onTapGesture { isFocused = true }
|
|
116
|
+
|
|
117
|
+
ErrorView(errorMessage: errorMessage, errorSpacing: errorSpacing, hintText: "")
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// MARK: Subviews
|
|
122
|
+
|
|
123
|
+
private var hiddenField: some View {
|
|
124
|
+
TextField("", text: Binding(
|
|
125
|
+
get: { value },
|
|
126
|
+
set: { handleChangeText($0) }
|
|
127
|
+
))
|
|
128
|
+
.keyboardType(dataType == "number" ? .numberPad : .asciiCapable)
|
|
129
|
+
.focused($isFocused)
|
|
130
|
+
.foregroundColor(.clear)
|
|
131
|
+
.accentColor(.clear)
|
|
132
|
+
.frame(maxWidth: .infinity)
|
|
133
|
+
.frame(height: 56)
|
|
134
|
+
.opacity(0.01)
|
|
135
|
+
.onChange(of: isFocused) { focused in
|
|
136
|
+
if focused {
|
|
137
|
+
onFocus?()
|
|
138
|
+
hasBeenFocused = true
|
|
139
|
+
} else if hasBeenFocused {
|
|
140
|
+
onBlur?()
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
@ViewBuilder
|
|
146
|
+
private var cells: some View {
|
|
147
|
+
if let length = length {
|
|
148
|
+
HStack(spacing: Spacing.L) {
|
|
149
|
+
ForEach(0 ..< length, id: \.self) { i in
|
|
150
|
+
cell(at: i)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
.padding(.vertical, 8)
|
|
154
|
+
} else {
|
|
155
|
+
HStack(spacing: Spacing.L) {
|
|
156
|
+
if value.isEmpty && !isFocused && !placeholder.isEmpty {
|
|
157
|
+
MomoText(placeholder, typography: .descriptionDefaultRegular, color: theme.colors.text.hint)
|
|
158
|
+
} else {
|
|
159
|
+
ForEach(Array(value.enumerated()), id: \.offset) { _, ch in
|
|
160
|
+
MomoText(String(ch), typography: .headerDefaultBold, color: theme.colors.text.default)
|
|
161
|
+
}
|
|
162
|
+
if isFocused && value.count != maxLength {
|
|
163
|
+
OTPCaret()
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
.padding(.vertical, 8)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
@ViewBuilder
|
|
172
|
+
private func cell(at index: Int) -> some View {
|
|
173
|
+
if isFocused && value.count == index {
|
|
174
|
+
OTPCaret()
|
|
175
|
+
} else {
|
|
176
|
+
let chars = Array(value)
|
|
177
|
+
let filled = index < chars.count
|
|
178
|
+
MomoText(
|
|
179
|
+
filled ? String(chars[index]) : "-",
|
|
180
|
+
typography: filled ? .headerDefaultBold : .descriptionDefaultRegular,
|
|
181
|
+
color: filled ? theme.colors.text.default : theme.colors.text.hint
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// MARK: Logic
|
|
187
|
+
|
|
188
|
+
private func handleChangeText(_ text: String) {
|
|
189
|
+
let validData = dataType != "number" || text.allSatisfy { $0.isNumber }
|
|
190
|
+
let changed = text.count != value.count
|
|
191
|
+
if text.count <= maxLength && validData && changed {
|
|
192
|
+
value = text
|
|
193
|
+
onChangeText?(text)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private func clearText() {
|
|
198
|
+
handleChangeText("")
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private var borderColor: Color {
|
|
202
|
+
if !errorMessage.isEmpty {
|
|
203
|
+
return theme.colors.error.primary
|
|
204
|
+
}
|
|
205
|
+
if isFocused {
|
|
206
|
+
return theme.colors.primary
|
|
207
|
+
}
|
|
208
|
+
return theme.colors.border.default
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import SwiftUI
|
|
3
|
+
|
|
4
|
+
/// Loader variants, mirroring Compose `LoaderType`.
|
|
5
|
+
public enum LoaderType {
|
|
6
|
+
case dot
|
|
7
|
+
case spinner
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/// Native SwiftUI port of the Compose `Loader` component.
|
|
11
|
+
///
|
|
12
|
+
/// Mirrors Compose `Loader(type:, tintColor:, modifier:)` which delegates to
|
|
13
|
+
/// `DotLoader` / `SpinnerLoader`. Both variants render Lottie animations
|
|
14
|
+
/// (`files/dot_loading` and `files/lottie_circle_loader`) and optionally tint
|
|
15
|
+
/// the fill/stroke. The tint is reproduced here by masking a solid color rect
|
|
16
|
+
/// with the (monochrome) Lottie animation.
|
|
17
|
+
public struct Loader: View {
|
|
18
|
+
private let type: LoaderType
|
|
19
|
+
private let tintColor: Color?
|
|
20
|
+
|
|
21
|
+
public init(type: LoaderType = .dot, tintColor: Color? = nil) {
|
|
22
|
+
self.type = type
|
|
23
|
+
self.tintColor = tintColor
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public var body: some View {
|
|
27
|
+
switch type {
|
|
28
|
+
case .dot:
|
|
29
|
+
DotLoader(tintColor: tintColor)
|
|
30
|
+
case .spinner:
|
|
31
|
+
SpinnerLoader(tintColor: tintColor)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// Looping dot animation. Compose size: 52 x 18.
|
|
37
|
+
public struct DotLoader: View {
|
|
38
|
+
private let tintColor: Color?
|
|
39
|
+
|
|
40
|
+
public init(tintColor: Color? = nil) {
|
|
41
|
+
self.tintColor = tintColor
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public var body: some View {
|
|
45
|
+
TintedLottie(name: "dot_loading", tintColor: tintColor)
|
|
46
|
+
.frame(width: 52, height: 18)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// Looping circular spinner. Compose size: 24 x 24.
|
|
51
|
+
public struct SpinnerLoader: View {
|
|
52
|
+
private let tintColor: Color?
|
|
53
|
+
|
|
54
|
+
public init(tintColor: Color? = nil) {
|
|
55
|
+
self.tintColor = tintColor
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public var body: some View {
|
|
59
|
+
TintedLottie(name: "lottie_circle_loader", tintColor: tintColor)
|
|
60
|
+
.frame(width: 24, height: 24)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// Horizontal determinate progress bar.
|
|
65
|
+
///
|
|
66
|
+
/// Mirrors Compose `ProgressBar(percent:, color:, modifier:)`:
|
|
67
|
+
/// - `percent` clamped to 0...100, animated with a 200ms ease.
|
|
68
|
+
/// - track = theme background default (`0xFFF2F2F6`), fill = `color` or primary (pink_03).
|
|
69
|
+
/// - height 4, corner radius `Radius.XXS`, fills max width.
|
|
70
|
+
public struct ProgressBar: View {
|
|
71
|
+
@Environment(\.appTheme) private var theme
|
|
72
|
+
|
|
73
|
+
private let percent: CGFloat
|
|
74
|
+
private let color: Color?
|
|
75
|
+
|
|
76
|
+
public init(percent: CGFloat = 0, color: Color? = nil) {
|
|
77
|
+
self.percent = percent
|
|
78
|
+
self.color = color
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private var clampedFraction: CGFloat {
|
|
82
|
+
min(max(percent, 0), 100) / 100
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
public var body: some View {
|
|
86
|
+
GeometryReader { proxy in
|
|
87
|
+
ZStack(alignment: .leading) {
|
|
88
|
+
RoundedRectangle(cornerRadius: Radius.XXS)
|
|
89
|
+
.fill(theme.colors.background.default)
|
|
90
|
+
RoundedRectangle(cornerRadius: Radius.XXS)
|
|
91
|
+
.fill(color ?? theme.colors.primary)
|
|
92
|
+
.frame(width: proxy.size.width * clampedFraction)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
.frame(height: 4)
|
|
96
|
+
.animation(.easeInOut(duration: 0.2), value: clampedFraction)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/// Renders a looping Lottie animation, optionally recolored via a mask.
|
|
101
|
+
private struct TintedLottie: View {
|
|
102
|
+
let name: String
|
|
103
|
+
let tintColor: Color?
|
|
104
|
+
|
|
105
|
+
var body: some View {
|
|
106
|
+
if let tintColor = tintColor {
|
|
107
|
+
tintColor
|
|
108
|
+
.mask(LottieView(name: name, loopMode: .loop))
|
|
109
|
+
} else {
|
|
110
|
+
LottieView(name: name, loopMode: .loop)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|