@momo-kits/native-kits 0.162.2-debug → 0.162.2-sp.1-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/settings.local.json +55 -0
- package/compose/build.gradle.kts +1 -1
- package/compose/compose.podspec +1 -1
- package/gradle.properties +1 -1
- package/ios/Application/Components.swift +34 -43
- package/ios/Application/FloatingButton.swift +10 -8
- package/ios/Application/Navigation/BottomTab/BottomTab.swift +102 -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 +151 -0
- package/ios/Application/Navigation/NavigationOptions.swift +109 -0
- package/ios/Application/Navigation/Navigator.swift +360 -0
- package/ios/Application/Navigation/Overplay/BottomSheet.swift +176 -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/StackScreen.swift +449 -0
- package/ios/Avatar/Avatar.swift +193 -0
- package/ios/Badge/Badge.swift +11 -13
- package/ios/Badge/BadgeRibbon.swift +12 -19
- package/ios/BaselineView/BaselineView.swift +163 -0
- package/ios/Button/Button.swift +62 -23
- package/ios/Carousel/Carousel.swift +181 -0
- package/ios/Checkbox/Checkbox.swift +14 -12
- package/ios/Chip/Chip.swift +23 -9
- package/ios/Collapse/Collapse.swift +182 -0
- package/ios/Colors+Radius+Spacing/Colors.swift +1 -1
- 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/Icon/Icon.swift +6 -1
- package/ios/IconButton/IconButton.swift +112 -0
- package/ios/Information/Information.swift +151 -0
- package/ios/Input/Input.swift +42 -25
- package/ios/Input/InputPhoneNumber.swift +21 -19
- package/ios/Input/InputSearch.swift +101 -45
- package/ios/Input/InputTextArea.swift +67 -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/OTPKeyboard/KeyboardButton.swift +1 -1
- 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 +10 -12
- package/ios/Popup/PopupInput.swift +2 -6
- 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/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/Template/TrustBanner/TrustBanner.swift +2 -0
- package/ios/Title/Title.swift +269 -0
- package/ios/Tooltip/Tooltip.swift +482 -0
- package/ios/Typography/Text.swift +77 -5
- package/ios/Typography/Typography.swift +9 -0
- package/ios/Uploader/Uploader.swift +183 -0
- package/ios/native-kits.podspec +58 -8
- package/ios-demo/MoMoUIKitsDemo.podspec +18 -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 +72 -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 +261 -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 +50 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/UIKitsDemoModels.swift +15 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/UIKitsDemoRegistry.swift +73 -0
- package/local.properties +8 -0
- package/package.json +1 -1
- package/publish.sh +50 -0
- package/scripts/gen-ios-color.mjs +136 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
//
|
|
2
|
+
// InputDropDown.swift
|
|
3
|
+
// Pods
|
|
4
|
+
//
|
|
5
|
+
// Hand-written SwiftUI reimplementation of Compose `InputDropDown`.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import SwiftUI
|
|
9
|
+
|
|
10
|
+
// MARK: - RenderRightIcon (mirrors Compose RenderRightIcon for InputDropDown)
|
|
11
|
+
|
|
12
|
+
private struct RenderRightIcon: View {
|
|
13
|
+
let loading: Bool
|
|
14
|
+
let icon: String
|
|
15
|
+
let color: Color
|
|
16
|
+
let onPressed: () -> Void
|
|
17
|
+
|
|
18
|
+
var body: some View {
|
|
19
|
+
if loading {
|
|
20
|
+
ActivityIndicator(isAnimating: .constant(true), style: .medium)
|
|
21
|
+
.frame(width: 16, height: 16)
|
|
22
|
+
} else if !icon.isEmpty {
|
|
23
|
+
SwiftUI.Button(action: onPressed) {
|
|
24
|
+
Icon(source: icon, size: 24, color: color)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// MARK: - InputDropDown
|
|
31
|
+
|
|
32
|
+
public struct InputDropDown: View {
|
|
33
|
+
@Environment(\.appTheme) private var theme
|
|
34
|
+
@Binding public var value: String
|
|
35
|
+
|
|
36
|
+
public var floatingValue: String
|
|
37
|
+
public var floatingValueColor: Color?
|
|
38
|
+
public var floatingIcon: String
|
|
39
|
+
public var floatingIconColor: Color?
|
|
40
|
+
public var placeholder: String
|
|
41
|
+
public var size: InputSize
|
|
42
|
+
public var onPress: () -> Void
|
|
43
|
+
public var hintText: String
|
|
44
|
+
public var error: String
|
|
45
|
+
public var errorSpacing: Bool
|
|
46
|
+
public var disabled: Bool
|
|
47
|
+
public var icon: String
|
|
48
|
+
public var iconColor: Color?
|
|
49
|
+
public var onRightIconPressed: () -> Void
|
|
50
|
+
public var leadingIcon: String
|
|
51
|
+
public var leadingIconColor: Color?
|
|
52
|
+
public var loading: Bool
|
|
53
|
+
public var required: Bool
|
|
54
|
+
|
|
55
|
+
public init(
|
|
56
|
+
value: Binding<String>,
|
|
57
|
+
floatingValue: String = "",
|
|
58
|
+
floatingValueColor: Color? = nil,
|
|
59
|
+
floatingIcon: String = "",
|
|
60
|
+
floatingIconColor: Color? = nil,
|
|
61
|
+
placeholder: String = "",
|
|
62
|
+
size: InputSize = .small,
|
|
63
|
+
onPress: @escaping () -> Void = {},
|
|
64
|
+
hintText: String = "",
|
|
65
|
+
error: String = "",
|
|
66
|
+
errorSpacing: Bool = false,
|
|
67
|
+
disabled: Bool = false,
|
|
68
|
+
icon: String = "arrow_chevron_down_small",
|
|
69
|
+
iconColor: Color? = nil,
|
|
70
|
+
onRightIconPressed: @escaping () -> Void = {},
|
|
71
|
+
leadingIcon: String = "",
|
|
72
|
+
leadingIconColor: Color? = nil,
|
|
73
|
+
loading: Bool = false,
|
|
74
|
+
required: Bool = false
|
|
75
|
+
) {
|
|
76
|
+
self._value = value
|
|
77
|
+
self.floatingValue = floatingValue
|
|
78
|
+
self.floatingValueColor = floatingValueColor
|
|
79
|
+
self.floatingIcon = floatingIcon
|
|
80
|
+
self.floatingIconColor = floatingIconColor
|
|
81
|
+
self.placeholder = placeholder
|
|
82
|
+
self.size = size
|
|
83
|
+
self.onPress = onPress
|
|
84
|
+
self.hintText = hintText
|
|
85
|
+
self.error = error
|
|
86
|
+
self.errorSpacing = errorSpacing
|
|
87
|
+
self.disabled = disabled
|
|
88
|
+
self.icon = icon
|
|
89
|
+
self.iconColor = iconColor
|
|
90
|
+
self.onRightIconPressed = onRightIconPressed
|
|
91
|
+
self.leadingIcon = leadingIcon
|
|
92
|
+
self.leadingIconColor = leadingIconColor
|
|
93
|
+
self.loading = loading
|
|
94
|
+
self.required = required
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
public var body: some View {
|
|
98
|
+
VStack(alignment: .leading, spacing: 4) {
|
|
99
|
+
ZStack(alignment: .topLeading) {
|
|
100
|
+
// Floating label
|
|
101
|
+
if !floatingValue.isEmpty || !floatingIcon.isEmpty {
|
|
102
|
+
HStack(spacing: 0) {
|
|
103
|
+
MomoText(floatingValue, typography: .labelSMedium, color: floatingTitleColor)
|
|
104
|
+
if required {
|
|
105
|
+
MomoText("*", typography: .labelSMedium, color: theme.colors.error.primary)
|
|
106
|
+
}
|
|
107
|
+
if !floatingIcon.isEmpty {
|
|
108
|
+
Icon(source: floatingIcon, size: 16, color: floatingIconTintColor)
|
|
109
|
+
.padding(.leading, Spacing.XS)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
.padding(.horizontal, Spacing.S)
|
|
113
|
+
.background(theme.colors.background.surface)
|
|
114
|
+
.offset(x: Spacing.S, y: -8)
|
|
115
|
+
.zIndex(10)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Input container
|
|
119
|
+
HStack(alignment: .center, spacing: 0) {
|
|
120
|
+
if !leadingIcon.isEmpty {
|
|
121
|
+
Icon(source: leadingIcon, size: size == .small ? 24 : 32, color: leadingIconTintColor)
|
|
122
|
+
.padding(.trailing, Spacing.M)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
MomoText(
|
|
126
|
+
value.isEmpty ? placeholder : value,
|
|
127
|
+
typography: .bodyDefaultRegular,
|
|
128
|
+
color: value.isEmpty ? placeholderColor : textColor
|
|
129
|
+
)
|
|
130
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
131
|
+
|
|
132
|
+
RenderRightIcon(
|
|
133
|
+
loading: loading,
|
|
134
|
+
icon: icon,
|
|
135
|
+
color: iconTintColor,
|
|
136
|
+
onPressed: onRightIconPressed
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
.padding(.horizontal, Spacing.M)
|
|
140
|
+
.frame(height: size == .small ? 48 : 56)
|
|
141
|
+
.overlay(
|
|
142
|
+
RoundedRectangle(cornerRadius: Radius.S)
|
|
143
|
+
.stroke(getBorderColor(isFocused: false, error: error, disabled: disabled), lineWidth: 1)
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
.contentShape(Rectangle())
|
|
147
|
+
.onTapGesture {
|
|
148
|
+
if !disabled { onPress() }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
ErrorView(
|
|
152
|
+
errorMessage: error,
|
|
153
|
+
errorSpacing: errorSpacing,
|
|
154
|
+
hintText: hintText
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
.accessibilityElement(children: .combine)
|
|
158
|
+
.accessibility(label: Text(floatingValue))
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// MARK: - Computed colors
|
|
162
|
+
|
|
163
|
+
private var textColor: Color {
|
|
164
|
+
disabled ? theme.colors.text.disable : theme.colors.text.default
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private var placeholderColor: Color {
|
|
168
|
+
disabled ? theme.colors.text.disable : theme.colors.text.hint
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private var iconTintColor: Color {
|
|
172
|
+
disabled ? theme.colors.text.disable : (iconColor ?? theme.colors.text.default)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private var floatingTitleColor: Color {
|
|
176
|
+
disabled ? theme.colors.text.disable : (floatingValueColor ?? theme.colors.text.hint)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private var floatingIconTintColor: Color {
|
|
180
|
+
disabled ? theme.colors.text.disable : (floatingIconColor ?? theme.colors.text.default)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private var leadingIconTintColor: Color {
|
|
184
|
+
disabled ? theme.colors.text.disable : (leadingIconColor ?? theme.colors.text.hint)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private func getBorderColor(isFocused: Bool, error: String, disabled: Bool) -> Color {
|
|
188
|
+
if disabled {
|
|
189
|
+
return theme.colors.border.disable
|
|
190
|
+
} else if !error.isEmpty {
|
|
191
|
+
return theme.colors.error.primary
|
|
192
|
+
} else if isFocused {
|
|
193
|
+
return theme.colors.primary
|
|
194
|
+
} else {
|
|
195
|
+
return theme.colors.border.default
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -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
|
+
}
|