@momo-kits/native-kits 0.162.30-debug → 0.162.31-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 +19 -0
- package/compose/build.gradle.kts +1 -1
- package/compose/compose.podspec +1 -1
- package/gradle.properties +1 -1
- package/ios/Application/ApplicationEnvironment.swift +14 -6
- package/ios/Application/LanguageSource.swift +93 -0
- package/ios/Application/Localize.swift +73 -4
- package/ios/Application/LocalizeModifier.swift +30 -0
- package/ios/Application/Navigation/NavigationContainer.swift +28 -18
- package/ios/Application/Navigation/Navigator.swift +121 -53
- package/ios/Application/Navigation/NavigatorManager.swift +49 -0
- package/ios/Application/Navigation/component/NavigationFooter.swift +90 -0
- package/ios/Application/StackScreen.swift +19 -28
- package/ios/Badge/Badge.swift +2 -1
- package/ios/Badge/BadgeRibbon.swift +6 -2
- package/ios/Button/Button.swift +41 -61
- package/ios/Chip/Chip.swift +6 -5
- package/ios/Icon/Icon.swift +13 -4
- package/ios/Input/ErrorView.swift +2 -1
- package/ios/Input/Input.swift +9 -2
- package/ios/Input/InputPhoneNumber.swift +2 -1
- package/ios/Input/InputSearch.swift +3 -2
- package/ios/Input/InputTextArea.swift +6 -1
- package/ios/InputMoney/InputMoney.swift +5 -2
- package/ios/InputOTP/InputOTP.swift +2 -1
- package/ios/Layout/GridContext.swift +30 -0
- package/ios/Layout/Item.swift +42 -0
- package/ios/Layout/Section.swift +134 -0
- package/ios/Popup/PopupNotify.swift +2 -1
- package/ios/ProgressInfo/ProgressInfo.swift +4 -3
- package/ios/Rating/Rating.swift +2 -1
- package/ios/Stepper/Stepper.swift +2 -1
- package/ios/SuggestAction/SuggestAction.swift +3 -2
- package/ios/Tag/Tag.swift +2 -1
- package/ios/Template/TrustBanner/TrustBanner.swift +4 -2
- package/ios/Title/Title.swift +3 -2
- package/ios/Tooltip/Tooltip.swift +2 -1
- package/ios/Typography/FontScale.swift +103 -0
- package/ios/Typography/FontScaleStore.swift +52 -13
- package/ios/Typography/Text.swift +12 -29
- package/ios/Typography/Typography.swift +66 -86
- package/ios/native-kits.podspec +2 -1
- package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/AvatarDemo.swift +87 -73
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/BadgeDemo.swift +55 -249
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ButtonDemo.swift +90 -82
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/CarouselDemo.swift +74 -104
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/CheckboxDemo.swift +47 -50
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ChipDemo.swift +63 -7
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/CollapseDemo.swift +90 -81
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DateTimePickerDemo.swift +73 -97
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DemoScreen.swift +420 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DividerDemo.swift +46 -4
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +54 -57
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IconButtonDemo.swift +80 -45
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IconDemo.swift +81 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ImageDemo.swift +49 -62
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InformationDemo.swift +66 -116
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputDemo.swift +104 -98
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputDropDownDemo.swift +76 -118
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputMoneyDemo.swift +73 -86
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputOTPDemo.swift +69 -54
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputPhoneNumberDemo.swift +74 -54
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputSearchDemo.swift +73 -20
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputTextAreaDemo.swift +68 -38
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/LoaderDemo.swift +55 -52
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/LocalizeDemo.swift +51 -4
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +14 -2
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PaginationDemo.swift +47 -51
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PlaygroundData.swift +76 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupNotifyDemo.swift +67 -93
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupPromotionDemo.swift +34 -43
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ProgressInfoDemo.swift +83 -71
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/RadioDemo.swift +55 -23
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/RatingDemo.swift +58 -46
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SectionDemo.swift +83 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SkeletonDemo.swift +41 -12
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SliderDemo.swift +67 -127
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SnackBarDemo.swift +54 -110
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/StepperDemo.swift +69 -79
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/StepsDemo.swift +78 -58
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SuggestActionDemo.swift +76 -65
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SwipeDemo.swift +82 -127
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SwitchDemo.swift +44 -19
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TabViewDemo.swift +92 -168
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TagDemo.swift +79 -27
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ThemeDemo.swift +0 -1
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TitleDemo.swift +87 -92
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TooltipDemo.swift +94 -163
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TypographyDemo.swift +70 -70
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/UploaderDemo.swift +68 -114
- package/ios-demo/Sources/MoMoUIKitsDemo/SwiftUIDemoBrowserView.swift +3 -1
- package/package.json +2 -2
package/ios/Button/Button.swift
CHANGED
|
@@ -133,19 +133,19 @@ public struct Button: View {
|
|
|
133
133
|
var action: () -> Void
|
|
134
134
|
var type: ButtonType
|
|
135
135
|
var size: ButtonSize
|
|
136
|
-
var iconLeft:
|
|
137
|
-
var iconRight:
|
|
136
|
+
var iconLeft: String
|
|
137
|
+
var iconRight: String
|
|
138
138
|
var isFull: Bool
|
|
139
139
|
var loading: Bool
|
|
140
140
|
var useTintColor: Bool
|
|
141
141
|
|
|
142
142
|
public init(
|
|
143
|
-
title: String = "",
|
|
143
|
+
title: String = "Button",
|
|
144
144
|
action: @escaping () -> Void,
|
|
145
145
|
type: ButtonType = .primary,
|
|
146
146
|
size: ButtonSize = .large,
|
|
147
|
-
iconLeft:
|
|
148
|
-
iconRight:
|
|
147
|
+
iconLeft: String = "",
|
|
148
|
+
iconRight: String = "",
|
|
149
149
|
isFull: Bool = true,
|
|
150
150
|
loading: Bool = false,
|
|
151
151
|
useTintColor: Bool = true
|
|
@@ -161,38 +161,9 @@ public struct Button: View {
|
|
|
161
161
|
self.useTintColor = useTintColor
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
/// solely to keep this initializer unambiguous alongside the `AnyView`-based one above —
|
|
168
|
-
/// pass `""` explicitly for an icon-right-only button.
|
|
169
|
-
public init(
|
|
170
|
-
title: String = "Button",
|
|
171
|
-
action: @escaping () -> Void,
|
|
172
|
-
type: ButtonType = .primary,
|
|
173
|
-
size: ButtonSize = .large,
|
|
174
|
-
iconLeft: String,
|
|
175
|
-
iconRight: String = "",
|
|
176
|
-
isFull: Bool = true,
|
|
177
|
-
loading: Bool = false,
|
|
178
|
-
useTintColor: Bool = true
|
|
179
|
-
) {
|
|
180
|
-
self.init(
|
|
181
|
-
title: title,
|
|
182
|
-
action: action,
|
|
183
|
-
type: type,
|
|
184
|
-
size: size,
|
|
185
|
-
iconLeft: iconLeft.isEmpty ? nil : AnyView(Icon(source: iconLeft)),
|
|
186
|
-
iconRight: iconRight.isEmpty ? nil : AnyView(Icon(source: iconRight)),
|
|
187
|
-
isFull: isFull,
|
|
188
|
-
loading: loading,
|
|
189
|
-
useTintColor: useTintColor
|
|
190
|
-
)
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
private func shouldLoadingOnLeft(_ left: AnyView?, _ right: AnyView?) -> Bool {
|
|
194
|
-
let hasLeft = left != nil
|
|
195
|
-
let hasRight = right != nil
|
|
164
|
+
private func shouldLoadingOnLeft(_ left: String, _ right: String) -> Bool {
|
|
165
|
+
let hasLeft = !left.isEmpty
|
|
166
|
+
let hasRight = !right.isEmpty
|
|
196
167
|
|
|
197
168
|
switch (hasLeft, hasRight) {
|
|
198
169
|
case (false, false): return true
|
|
@@ -217,27 +188,13 @@ public struct Button: View {
|
|
|
217
188
|
}
|
|
218
189
|
}) {
|
|
219
190
|
HStack(spacing: size.iconSpacing) {
|
|
220
|
-
|
|
221
|
-
LottieView(name: "lottie_circle_loader", loopMode: .loop)
|
|
222
|
-
.frame(width: size.iconSize, height: size.iconSize)
|
|
223
|
-
.colorMultiply(fg)
|
|
224
|
-
|
|
225
|
-
} else if let iconLeft = iconLeft {
|
|
226
|
-
tintedIcon(iconLeft, color: fg)
|
|
227
|
-
}
|
|
191
|
+
renderIcon(icon: iconLeft, forceLoading: loading && loadingOnLeft, textColor: fg)
|
|
228
192
|
|
|
229
193
|
MomoText(title, typography: size.typographyStyle, color: fg)
|
|
230
194
|
.lineLimit(1)
|
|
231
195
|
.truncationMode(.tail)
|
|
232
196
|
|
|
233
|
-
|
|
234
|
-
LottieView(name: "lottie_circle_loader", loopMode: .loop)
|
|
235
|
-
.frame(width: size.iconSize, height: size.iconSize)
|
|
236
|
-
.colorMultiply(fg)
|
|
237
|
-
|
|
238
|
-
} else if let iconRight = iconRight {
|
|
239
|
-
tintedIcon(iconRight, color: fg)
|
|
240
|
-
}
|
|
197
|
+
renderIcon(icon: iconRight, forceLoading: loading && !loadingOnLeft, textColor: fg)
|
|
241
198
|
}
|
|
242
199
|
.frame(maxWidth: isFull ? .infinity : nil)
|
|
243
200
|
.padding(.horizontal, size.padding)
|
|
@@ -249,20 +206,24 @@ public struct Button: View {
|
|
|
249
206
|
border: border,
|
|
250
207
|
borderWidth: borderWidth,
|
|
251
208
|
radius: size.radius,
|
|
252
|
-
height: size.height
|
|
209
|
+
height: size.height,
|
|
210
|
+
isFull: isFull
|
|
253
211
|
))
|
|
254
212
|
.disabled(!isEnabled)
|
|
255
213
|
}
|
|
256
214
|
|
|
215
|
+
/// Mirrors Kotlin `RenderIcon`: the loader replaces the icon slot entirely, and
|
|
216
|
+
/// `useTintColor == false` leaves the icon at its own colors.
|
|
257
217
|
@ViewBuilder
|
|
258
|
-
private func
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
} else {
|
|
264
|
-
icon
|
|
218
|
+
private func renderIcon(icon: String, forceLoading: Bool, textColor: Color) -> some View {
|
|
219
|
+
let color: Color? = useTintColor ? textColor : nil
|
|
220
|
+
|
|
221
|
+
if forceLoading {
|
|
222
|
+
LottieView(name: "lottie_circle_loader", loopMode: .loop)
|
|
265
223
|
.frame(width: size.iconSize, height: size.iconSize)
|
|
224
|
+
.colorMultiply(color ?? Colors.black01)
|
|
225
|
+
} else if !icon.isEmpty {
|
|
226
|
+
Icon(source: icon, size: size.iconSize, color: color)
|
|
266
227
|
}
|
|
267
228
|
}
|
|
268
229
|
}
|
|
@@ -277,11 +238,13 @@ private struct ButtonPressFeedbackStyle: ButtonStyle {
|
|
|
277
238
|
let borderWidth: CGFloat
|
|
278
239
|
let radius: CGFloat
|
|
279
240
|
let height: CGFloat
|
|
241
|
+
let isFull: Bool
|
|
280
242
|
|
|
281
243
|
func makeBody(configuration: Configuration) -> some View {
|
|
282
244
|
let pressed = configuration.isPressed && isEnabled
|
|
283
245
|
|
|
284
246
|
configuration.label
|
|
247
|
+
.modifier(HugContentOnLegacyOS(isFull: isFull))
|
|
285
248
|
.opacity(pressed ? 0.5 : 1)
|
|
286
249
|
.background(
|
|
287
250
|
RoundedRectangle(cornerRadius: radius)
|
|
@@ -297,3 +260,20 @@ private struct ButtonPressFeedbackStyle: ButtonStyle {
|
|
|
297
260
|
.animation(.easeInOut(duration: 0.1), value: pressed)
|
|
298
261
|
}
|
|
299
262
|
}
|
|
263
|
+
|
|
264
|
+
/// `ButtonStyleConfiguration.Label` is greedy on iOS 16 and earlier — it accepts
|
|
265
|
+
/// the full proposed width, so the background drawn in `makeBody` stretches past
|
|
266
|
+
/// the content even when `isFull == false`. iOS 17 fixed the label to hug its
|
|
267
|
+
/// content, so only legacy OSes need the `fixedSize` clamp (kept off on 17+ to
|
|
268
|
+
/// preserve tail truncation of long titles).
|
|
269
|
+
private struct HugContentOnLegacyOS: ViewModifier {
|
|
270
|
+
let isFull: Bool
|
|
271
|
+
|
|
272
|
+
func body(content: Content) -> some View {
|
|
273
|
+
if #available(iOS 17.0, *) {
|
|
274
|
+
content
|
|
275
|
+
} else {
|
|
276
|
+
content.fixedSize(horizontal: !isFull, vertical: false)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
package/ios/Chip/Chip.swift
CHANGED
|
@@ -3,6 +3,7 @@ import Foundation
|
|
|
3
3
|
|
|
4
4
|
public struct Chip: View {
|
|
5
5
|
@Environment(\.appTheme) private var theme
|
|
6
|
+
@Environment(\.momoFontScale) private var fontScale
|
|
6
7
|
public enum ChipSize { case small, large }
|
|
7
8
|
|
|
8
9
|
public struct Dimensions {
|
|
@@ -54,11 +55,11 @@ public struct Chip: View {
|
|
|
54
55
|
let dims = (size == .small) ? Self.Small : Self.Large
|
|
55
56
|
// Mirrors Compose's `scaleSize(...)` on every chip dimension so it responds
|
|
56
57
|
// to Dynamic Type scaling the same way the rest of the kit does.
|
|
57
|
-
let height =
|
|
58
|
-
let horizontal =
|
|
59
|
-
let iconSize =
|
|
60
|
-
let iconSpacing =
|
|
61
|
-
let radius =
|
|
58
|
+
let height = fontScale(dims.height)
|
|
59
|
+
let horizontal = fontScale(dims.horizontal)
|
|
60
|
+
let iconSize = fontScale(dims.iconSize)
|
|
61
|
+
let iconSpacing = fontScale(dims.iconSpacing)
|
|
62
|
+
let radius = fontScale(Radius.L)
|
|
62
63
|
|
|
63
64
|
let bg: Color = {
|
|
64
65
|
if selected { return theme.colors.background.selected }
|
package/ios/Icon/Icon.swift
CHANGED
|
@@ -26,17 +26,24 @@ let iconSources: [String: IconSource] = loadIconSources()
|
|
|
26
26
|
// Icons that must never be tinted (mirror Compose `noThemeIcons`)
|
|
27
27
|
let noThemeIcons: [String] = ["ic_round_momo_tiny"]
|
|
28
28
|
|
|
29
|
+
// Compose defaults `color` to the theme text color while `color = null` means "no tint".
|
|
30
|
+
// Swift can't tell an omitted argument from an explicit `nil`, so the default is this
|
|
31
|
+
// unrepresentable sentinel: omitted -> theme text color, `nil` -> untinted.
|
|
32
|
+
public let iconThemeColor = Color(.sRGB, red: -1, green: -1, blue: -1, opacity: -1)
|
|
33
|
+
|
|
29
34
|
// Icon ViewModel
|
|
30
35
|
public struct Icon: View {
|
|
36
|
+
@Environment(\.appTheme) private var theme
|
|
37
|
+
|
|
31
38
|
var source: String
|
|
32
39
|
var size: CGFloat
|
|
33
40
|
var color: Color?
|
|
34
41
|
var accessibilityLabel: String?
|
|
35
|
-
|
|
42
|
+
|
|
36
43
|
public init(
|
|
37
44
|
source: String,
|
|
38
45
|
size: CGFloat = 24,
|
|
39
|
-
color: Color? =
|
|
46
|
+
color: Color? = iconThemeColor,
|
|
40
47
|
accessibilityLabel: String? = nil
|
|
41
48
|
) {
|
|
42
49
|
self.source = source
|
|
@@ -46,9 +53,11 @@ public struct Icon: View {
|
|
|
46
53
|
}
|
|
47
54
|
|
|
48
55
|
public var body: some View {
|
|
49
|
-
|
|
56
|
+
// Mirror Compose: an https source is used as-is, anything else is a key into icon.json
|
|
57
|
+
if let uri = source.contains("https") ? source : iconSources[source]?.uri {
|
|
58
|
+
let requestedColor = color == iconThemeColor ? theme.colors.text.default : color
|
|
50
59
|
// Mirror Compose: certain icons (e.g. branded glyphs) are never tinted
|
|
51
|
-
let resolvedColor = noThemeIcons.contains(source) ? nil :
|
|
60
|
+
let resolvedColor = noThemeIcons.contains(source) ? nil : requestedColor
|
|
52
61
|
ImageView(uri, color: resolvedColor)
|
|
53
62
|
.frame(width: size, height: size)
|
|
54
63
|
// Mirrors Compose's auto-generated `contentDescription = "img|$iconUrl"`
|
|
@@ -9,6 +9,7 @@ import SwiftUI
|
|
|
9
9
|
|
|
10
10
|
public struct ErrorView: View {
|
|
11
11
|
@Environment(\.appTheme) private var theme
|
|
12
|
+
@Environment(\.momoFontScale) private var fontScale
|
|
12
13
|
|
|
13
14
|
public var errorMessage: String
|
|
14
15
|
public var errorSpacing: Bool
|
|
@@ -27,7 +28,7 @@ public struct ErrorView: View {
|
|
|
27
28
|
public var body: some View {
|
|
28
29
|
if !errorMessage.isEmpty || !hintText.isEmpty {
|
|
29
30
|
HStack(alignment: .top, spacing: 4) {
|
|
30
|
-
Icon(source: "ic_error", size:
|
|
31
|
+
Icon(source: "ic_error", size: fontScale(16), color: displayColor())
|
|
31
32
|
MomoText(
|
|
32
33
|
displayText(),
|
|
33
34
|
typography: .descriptionDefaultRegular,
|
package/ios/Input/Input.swift
CHANGED
|
@@ -4,6 +4,7 @@ import Combine
|
|
|
4
4
|
|
|
5
5
|
public struct Input: View {
|
|
6
6
|
@Environment(\.appTheme) private var theme
|
|
7
|
+
@Environment(\.momoFontScale) private var fontScale
|
|
7
8
|
@Binding public var text: String
|
|
8
9
|
|
|
9
10
|
public var placeholder: String
|
|
@@ -163,7 +164,7 @@ public struct Input: View {
|
|
|
163
164
|
SecureInputField(
|
|
164
165
|
text: $text,
|
|
165
166
|
keyboardType: keyboardType,
|
|
166
|
-
fontSize:
|
|
167
|
+
fontSize: fontScale(14),
|
|
167
168
|
fontWeight: fontWeight == .bold ? .bold : .regular,
|
|
168
169
|
textColor: UIColor(getTextColor()),
|
|
169
170
|
cursorColor: UIColor(theme.colors.primary),
|
|
@@ -189,6 +190,12 @@ public struct Input: View {
|
|
|
189
190
|
.onAppear {
|
|
190
191
|
if autofocus { isFieldFocused = true }
|
|
191
192
|
}
|
|
193
|
+
// SwiftUI reads a TextField's UIKit input traits only when it
|
|
194
|
+
// builds the backing UITextField, so a later `keyboardType`
|
|
195
|
+
// change is silently dropped. The secure path can call
|
|
196
|
+
// `reloadInputViews()` in `updateUIView`; here the only lever
|
|
197
|
+
// is to force a rebuild.
|
|
198
|
+
.id(keyboardType.rawValue)
|
|
192
199
|
}
|
|
193
200
|
}
|
|
194
201
|
|
|
@@ -230,7 +237,7 @@ public struct Input: View {
|
|
|
230
237
|
}
|
|
231
238
|
}
|
|
232
239
|
.padding(.horizontal, Spacing.M)
|
|
233
|
-
.frame(height:
|
|
240
|
+
.frame(height: fontScale(size == .small ? 48 : 56))
|
|
234
241
|
.background(
|
|
235
242
|
RoundedRectangle(cornerRadius: Radius.S)
|
|
236
243
|
.fill(theme.colors.background.surface)
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import SwiftUI
|
|
9
9
|
public struct InputPhoneNumber: View {
|
|
10
10
|
@Environment(\.appTheme) private var theme
|
|
11
|
+
@Environment(\.momoFontScale) private var fontScale
|
|
11
12
|
@Binding public var text: String
|
|
12
13
|
|
|
13
14
|
public var placeholder: String
|
|
@@ -143,7 +144,7 @@ public struct InputPhoneNumber: View {
|
|
|
143
144
|
}
|
|
144
145
|
}
|
|
145
146
|
.padding(.horizontal, Spacing.M)
|
|
146
|
-
.frame(height:
|
|
147
|
+
.frame(height: fontScale(size == .small ? 48 : 56))
|
|
147
148
|
.background(
|
|
148
149
|
RoundedRectangle(cornerRadius: Radius.S)
|
|
149
150
|
.fill(theme.colors.background.surface)
|
|
@@ -104,6 +104,7 @@ public struct InputSearchProps {
|
|
|
104
104
|
|
|
105
105
|
public struct InputSearch: View {
|
|
106
106
|
@Environment(\.appTheme) private var theme
|
|
107
|
+
@Environment(\.momoFontScale) private var fontScale
|
|
107
108
|
@Binding var text: String
|
|
108
109
|
var buttonText: String = ""
|
|
109
110
|
var showButtonText: Bool = false
|
|
@@ -214,7 +215,7 @@ public struct InputSearch: View {
|
|
|
214
215
|
} else {
|
|
215
216
|
Text(placeholder)
|
|
216
217
|
.foregroundColor(disabled ? theme.colors.text.disable : theme.colors.text.hint)
|
|
217
|
-
.font(.system(size:
|
|
218
|
+
.font(.system(size: fontScale(14), weight: fontWeight))
|
|
218
219
|
.ignoreBoldTextSetting()
|
|
219
220
|
}
|
|
220
221
|
}
|
|
@@ -231,7 +232,7 @@ public struct InputSearch: View {
|
|
|
231
232
|
}, onCommit: {})
|
|
232
233
|
.disabled(disabled)
|
|
233
234
|
.foregroundColor(disabled ? theme.colors.text.disable : theme.colors.text.default)
|
|
234
|
-
.font(.system(size:
|
|
235
|
+
.font(.system(size: fontScale(14), weight: fontWeight))
|
|
235
236
|
.ignoreBoldTextSetting()
|
|
236
237
|
.keyboardType(keyboardType)
|
|
237
238
|
.applyCursorColor(theme.colors.primary)
|
|
@@ -37,7 +37,12 @@ struct UITextViewWrapper: UIViewRepresentable {
|
|
|
37
37
|
}
|
|
38
38
|
uiView.textColor = UIColor(textColor)
|
|
39
39
|
uiView.tintColor = UIColor(cursorColor)
|
|
40
|
-
|
|
40
|
+
// Read through `context.environment` rather than the global `scaleSize`: that is what makes
|
|
41
|
+
// SwiftUI re-run updateUIView when the font scale changes.
|
|
42
|
+
uiView.font = UIFont.systemFont(
|
|
43
|
+
ofSize: context.environment.momoFontScale(16),
|
|
44
|
+
weight: fontWeight == .bold ? .bold : .regular
|
|
45
|
+
)
|
|
41
46
|
uiView.isEditable = !disabled
|
|
42
47
|
}
|
|
43
48
|
|
|
@@ -50,6 +50,7 @@ public func formatNumberToMoney(_ digits: String, currency: String = "đ") -> St
|
|
|
50
50
|
|
|
51
51
|
public struct InputMoney: View {
|
|
52
52
|
@Environment(\.appTheme) private var theme
|
|
53
|
+
@Environment(\.momoFontScale) private var fontScale
|
|
53
54
|
@Binding public var text: String
|
|
54
55
|
|
|
55
56
|
public var floatingValue: String
|
|
@@ -214,7 +215,7 @@ public struct InputMoney: View {
|
|
|
214
215
|
}
|
|
215
216
|
}
|
|
216
217
|
.padding(.horizontal, Spacing.M)
|
|
217
|
-
.frame(height:
|
|
218
|
+
.frame(height: fontScale(size == .small ? 48 : 56))
|
|
218
219
|
.background(
|
|
219
220
|
RoundedRectangle(cornerRadius: Radius.S)
|
|
220
221
|
.fill(theme.colors.background.surface)
|
|
@@ -278,7 +279,7 @@ private struct MoneyTextField: UIViewRepresentable {
|
|
|
278
279
|
let field = UITextField()
|
|
279
280
|
field.delegate = context.coordinator
|
|
280
281
|
field.keyboardType = keyboardType
|
|
281
|
-
field.font = UIFont.systemFont(ofSize:
|
|
282
|
+
field.font = UIFont.systemFont(ofSize: context.environment.momoFontScale(20), weight: fontWeight)
|
|
282
283
|
field.textColor = textColor
|
|
283
284
|
field.isEnabled = !isDisabled
|
|
284
285
|
field.tintColor = cursorColor
|
|
@@ -295,6 +296,8 @@ private struct MoneyTextField: UIViewRepresentable {
|
|
|
295
296
|
|
|
296
297
|
func updateUIView(_ field: UITextField, context: Context) {
|
|
297
298
|
context.coordinator.parent = self
|
|
299
|
+
// Re-applied here, not just in makeUIView, so a font-scale change reaches an existing field.
|
|
300
|
+
field.font = UIFont.systemFont(ofSize: context.environment.momoFontScale(20), weight: fontWeight)
|
|
298
301
|
field.isEnabled = !isDisabled
|
|
299
302
|
field.textColor = textColor
|
|
300
303
|
field.tintColor = cursorColor
|
|
@@ -7,12 +7,13 @@ import SwiftUI
|
|
|
7
7
|
private struct OTPCaret: View {
|
|
8
8
|
@Environment(\.appTheme) private var theme
|
|
9
9
|
@State private var visible: Bool = true
|
|
10
|
+
@Environment(\.momoFontScale) private var fontScale
|
|
10
11
|
|
|
11
12
|
var body: some View {
|
|
12
13
|
HStack(spacing: 0) {
|
|
13
14
|
Rectangle()
|
|
14
15
|
.fill(theme.colors.primary)
|
|
15
|
-
.frame(width: 1, height:
|
|
16
|
+
.frame(width: 1, height: fontScale(12))
|
|
16
17
|
.opacity(visible ? 1 : 0)
|
|
17
18
|
.animation(
|
|
18
19
|
.linear(duration: 0.5).repeatForever(autoreverses: true),
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
/// Grid metrics published by a layout container (`SectionView`) so span-based children
|
|
4
|
+
/// can size themselves. Mirrors `vn.momo.kits.layout.GridContext`.
|
|
5
|
+
public struct GridContext: Equatable {
|
|
6
|
+
public let numberOfColumns: Int
|
|
7
|
+
public let gutter: CGFloat
|
|
8
|
+
public let sizePerSpan: CGFloat
|
|
9
|
+
|
|
10
|
+
public init(numberOfColumns: Int = 12, gutter: CGFloat = Spacing.M, sizePerSpan: CGFloat = 0) {
|
|
11
|
+
self.numberOfColumns = numberOfColumns
|
|
12
|
+
self.gutter = gutter
|
|
13
|
+
self.sizePerSpan = sizePerSpan
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public func sizeForSpan(_ span: Int) -> CGFloat {
|
|
17
|
+
sizePerSpan * CGFloat(span) + gutter * CGFloat(span - 1)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private struct GridContextKey: EnvironmentKey {
|
|
22
|
+
static let defaultValue: GridContext? = nil
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public extension EnvironmentValues {
|
|
26
|
+
var gridContext: GridContext? {
|
|
27
|
+
get { self[GridContextKey.self] }
|
|
28
|
+
set { self[GridContextKey.self] = newValue }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
/// Span-sized cell inside a `SectionView`. Reads the grid published by its container;
|
|
4
|
+
/// without one it just wraps its content. Mirrors `vn.momo.kits.layout.Item`.
|
|
5
|
+
public struct ItemView<Content: View>: View {
|
|
6
|
+
// MARK: Lifecycle
|
|
7
|
+
|
|
8
|
+
public init(
|
|
9
|
+
widthSpan: Int = 12,
|
|
10
|
+
heightSpan: Int = 0,
|
|
11
|
+
@ViewBuilder content: @escaping () -> Content
|
|
12
|
+
) {
|
|
13
|
+
self.widthSpan = widthSpan
|
|
14
|
+
self.heightSpan = heightSpan
|
|
15
|
+
self.content = content
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// MARK: Public
|
|
19
|
+
|
|
20
|
+
public var body: some View {
|
|
21
|
+
// A VStack (not a ZStack) so multiple children flow VERTICALLY instead of stacking
|
|
22
|
+
// on top of each other — matches the React Item's flex-column content.
|
|
23
|
+
VStack(alignment: .leading, spacing: 0) { content() }
|
|
24
|
+
.frame(width: width, height: height, alignment: .topLeading)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// MARK: Private
|
|
28
|
+
|
|
29
|
+
private let widthSpan: Int
|
|
30
|
+
private let heightSpan: Int
|
|
31
|
+
private let content: () -> Content
|
|
32
|
+
|
|
33
|
+
@Environment(\.gridContext) private var grid
|
|
34
|
+
|
|
35
|
+
private var width: CGFloat? {
|
|
36
|
+
grid.map { $0.sizeForSpan(widthSpan) }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private var height: CGFloat? {
|
|
40
|
+
heightSpan > 0 ? grid.map { $0.sizeForSpan(heightSpan) } : nil
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
/// 12-column section container: horizontal margin, optional background image, and a
|
|
4
|
+
/// wrapping row of children that read their span size from `\.gridContext`.
|
|
5
|
+
/// Mirrors `vn.momo.kits.layout.Section`.
|
|
6
|
+
public struct SectionView<Content: View>: View {
|
|
7
|
+
// MARK: Lifecycle
|
|
8
|
+
|
|
9
|
+
public init(
|
|
10
|
+
useMargin: Bool = true,
|
|
11
|
+
backgroundImage: String? = nil,
|
|
12
|
+
@ViewBuilder content: @escaping () -> Content
|
|
13
|
+
) {
|
|
14
|
+
self.useMargin = useMargin
|
|
15
|
+
self.backgroundImage = backgroundImage
|
|
16
|
+
self.content = content
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// MARK: Public
|
|
20
|
+
|
|
21
|
+
public var body: some View {
|
|
22
|
+
flow
|
|
23
|
+
.environment(\.gridContext, grid)
|
|
24
|
+
.frame(maxWidth: .infinity, alignment: .topLeading)
|
|
25
|
+
.background(widthReader)
|
|
26
|
+
.background(background)
|
|
27
|
+
.padding(.horizontal, useMargin ? Spacing.M : 0)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// MARK: Private
|
|
31
|
+
|
|
32
|
+
private static var columns: Int { 12 }
|
|
33
|
+
private static var gutter: CGFloat { Spacing.M }
|
|
34
|
+
|
|
35
|
+
private let useMargin: Bool
|
|
36
|
+
private let backgroundImage: String?
|
|
37
|
+
private let content: () -> Content
|
|
38
|
+
|
|
39
|
+
/// Width available *inside* the horizontal margin — the Compose `BoxWithConstraints` equivalent.
|
|
40
|
+
@State private var availableWidth: CGFloat = 0
|
|
41
|
+
|
|
42
|
+
private var grid: GridContext {
|
|
43
|
+
let columns = CGFloat(Self.columns)
|
|
44
|
+
// The trailing `- 1 / columns` mirrors Compose: it absorbs rounding so 12 spans
|
|
45
|
+
// plus their gutters never overflow the row by a sub-point.
|
|
46
|
+
let sizePerSpan = (availableWidth - Self.gutter * (columns - 1)) / columns - 1 / columns
|
|
47
|
+
return GridContext(numberOfColumns: Self.columns, gutter: Self.gutter, sizePerSpan: max(0, sizePerSpan))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@ViewBuilder private var flow: some View {
|
|
51
|
+
if #available(iOS 16.0, *) {
|
|
52
|
+
FlowRowLayout(gutter: Self.gutter) { content() }
|
|
53
|
+
} else {
|
|
54
|
+
// No `Layout` protocol before iOS 16: children stack instead of wrapping.
|
|
55
|
+
// Full-width (span 12) items — the common case — look identical.
|
|
56
|
+
VStack(alignment: .leading, spacing: Self.gutter) { content() }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@ViewBuilder private var background: some View {
|
|
61
|
+
if let backgroundImage {
|
|
62
|
+
ImageView(backgroundImage, contentMode: .fill).clipped()
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private var widthReader: some View {
|
|
67
|
+
GeometryReader { proxy in
|
|
68
|
+
Color.clear.preference(key: SectionWidthKey.self, value: proxy.size.width)
|
|
69
|
+
}
|
|
70
|
+
.onPreferenceChange(SectionWidthKey.self) { availableWidth = $0 }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private struct SectionWidthKey: PreferenceKey {
|
|
75
|
+
static var defaultValue: CGFloat = 0
|
|
76
|
+
|
|
77
|
+
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
|
|
78
|
+
value = max(value, nextValue())
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// Wrapping row with equal horizontal/vertical spacing — the Compose `FlowRow` equivalent.
|
|
83
|
+
@available(iOS 16.0, *)
|
|
84
|
+
private struct FlowRowLayout: Layout {
|
|
85
|
+
struct Row {
|
|
86
|
+
var items: [(index: Int, size: CGSize)] = []
|
|
87
|
+
var width: CGFloat = 0
|
|
88
|
+
var height: CGFloat = 0
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let gutter: CGFloat
|
|
92
|
+
|
|
93
|
+
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) -> CGSize {
|
|
94
|
+
let maxWidth = proposal.width ?? .infinity
|
|
95
|
+
let rows = rows(for: subviews, maxWidth: maxWidth)
|
|
96
|
+
let height = rows.reduce(0) { $0 + $1.height } + gutter * CGFloat(max(0, rows.count - 1))
|
|
97
|
+
return CGSize(width: proposal.width ?? rows.map(\.width).max() ?? 0, height: height)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) {
|
|
101
|
+
var y = bounds.minY
|
|
102
|
+
for row in rows(for: subviews, maxWidth: bounds.width) {
|
|
103
|
+
var x = bounds.minX
|
|
104
|
+
for item in row.items {
|
|
105
|
+
subviews[item.index].place(
|
|
106
|
+
at: CGPoint(x: x, y: y),
|
|
107
|
+
anchor: .topLeading,
|
|
108
|
+
proposal: ProposedViewSize(item.size)
|
|
109
|
+
)
|
|
110
|
+
x += item.size.width + gutter
|
|
111
|
+
}
|
|
112
|
+
y += row.height + gutter
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private func rows(for subviews: Subviews, maxWidth: CGFloat) -> [Row] {
|
|
117
|
+
var rows: [Row] = []
|
|
118
|
+
var current = Row()
|
|
119
|
+
for index in subviews.indices {
|
|
120
|
+
let size = subviews[index].sizeThatFits(ProposedViewSize(width: maxWidth, height: nil))
|
|
121
|
+
let widthIfAppended = current.items.isEmpty ? size.width : current.width + gutter + size.width
|
|
122
|
+
// Sub-point tolerance: spans are computed to land exactly on `maxWidth`.
|
|
123
|
+
if !current.items.isEmpty, widthIfAppended > maxWidth + 0.5 {
|
|
124
|
+
rows.append(current)
|
|
125
|
+
current = Row()
|
|
126
|
+
}
|
|
127
|
+
current.width = current.items.isEmpty ? size.width : current.width + gutter + size.width
|
|
128
|
+
current.height = max(current.height, size.height)
|
|
129
|
+
current.items.append((index, size))
|
|
130
|
+
}
|
|
131
|
+
if !current.items.isEmpty { rows.append(current) }
|
|
132
|
+
return rows
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -39,6 +39,7 @@ public struct PopupNotifyProps {
|
|
|
39
39
|
|
|
40
40
|
public struct PopupNotify: View {
|
|
41
41
|
@Environment(\.localize) private var localize
|
|
42
|
+
@Environment(\.momoFontScale) private var fontScale
|
|
42
43
|
|
|
43
44
|
public let props: PopupNotifyProps
|
|
44
45
|
|
|
@@ -49,7 +50,7 @@ public struct PopupNotify: View {
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
private var maxDescriptionHeight: CGFloat {
|
|
52
|
-
|
|
53
|
+
fontScale(TypographyStyle.bodyDefaultRegular.lineHeight) * 8.5
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
private var shouldUseColumnButtons: Bool {
|
|
@@ -254,15 +254,16 @@ private struct ProgressInfoVertical: View {
|
|
|
254
254
|
// MARK: - Step icon
|
|
255
255
|
|
|
256
256
|
private struct ProgressInfoStepIcon: View {
|
|
257
|
+
@Environment(\.momoFontScale) private var fontScale
|
|
257
258
|
let index: Int
|
|
258
259
|
let size: ProgressInfoSize
|
|
259
260
|
let useNumber: Bool
|
|
260
261
|
let customIcon: String?
|
|
261
262
|
|
|
262
263
|
var body: some View {
|
|
263
|
-
let containerSize =
|
|
264
|
-
let dotSize =
|
|
265
|
-
let iconSize =
|
|
264
|
+
let containerSize = fontScale(size == .small ? 16 : 24)
|
|
265
|
+
let dotSize = fontScale(size == .small ? 6 : 8)
|
|
266
|
+
let iconSize = fontScale(size == .small ? 12 : 16)
|
|
266
267
|
|
|
267
268
|
ZStack {
|
|
268
269
|
Circle()
|
package/ios/Rating/Rating.swift
CHANGED
|
@@ -15,6 +15,7 @@ public enum RatingSize {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
public struct Rating: View {
|
|
18
|
+
@Environment(\.momoFontScale) private var fontScale
|
|
18
19
|
private let numOfStars: Int
|
|
19
20
|
private let rating: Float
|
|
20
21
|
private let onRatingChange: ((Int) -> Void)?
|
|
@@ -44,7 +45,7 @@ public struct Rating: View {
|
|
|
44
45
|
private func star(at index: Int) -> some View {
|
|
45
46
|
let icon = Icon(
|
|
46
47
|
source: starIcon(at: index),
|
|
47
|
-
size:
|
|
48
|
+
size: fontScale(size.iconSize),
|
|
48
49
|
color: starColor(at: index)
|
|
49
50
|
)
|
|
50
51
|
|