@momo-kits/native-kits 0.167.1-duo.2-debug → 0.167.1-duo.20-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 +36 -8
- package/README.md +2 -2
- package/compose/build.gradle.kts +15 -1
- package/compose/build.gradle.kts.backup +14 -0
- package/compose/compose.podspec +1 -1
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +2 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +6 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +168 -52
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +20 -21
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +2 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +4 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +24 -14
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +9 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +11 -12
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +4 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +2 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +10 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +6 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +53 -26
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +12 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +7 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +13 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +13 -5
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +3 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +14 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +19 -21
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +2 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +5 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +3 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +7 -15
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +4 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +10 -4
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +3 -17
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +173 -21
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +43 -4
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +8 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +10 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +153 -48
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +114 -71
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +8 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +98 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +312 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +84 -18
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +33 -13
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +2 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +51 -19
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +4 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +46 -23
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +2 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +3 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +13 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +38 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +4 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +94 -0
- package/gradle.properties +1 -1
- package/ios/Application/ApplicationEnvironment.swift +17 -0
- package/ios/Application/MaxApi.swift +12 -3
- package/ios/Avatar/Avatar.swift +4 -3
- package/ios/Badge/Badge.swift +6 -4
- package/ios/Badge/BadgeDot.swift +1 -1
- package/ios/Badge/BadgeRibbon.swift +0 -1
- package/ios/Button/Button.swift +15 -12
- package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +64 -18
- package/ios/Carousel/Carousel.swift +29 -12
- package/ios/Checkbox/Checkbox.swift +7 -3
- package/ios/Chip/Chip.swift +5 -4
- package/ios/Collapse/Collapse.swift +70 -19
- package/ios/Colors+Radius+Spacing/Colors.swift +10 -1
- package/ios/Colors+Radius+Spacing/Radius.swift +6 -0
- package/ios/DateTimePicker/DateTimePicker.swift +54 -20
- package/ios/DateTimePicker/WheelPicker.swift +103 -105
- package/ios/Divider/Divider.swift +37 -4
- package/ios/Extensions/Color++.swift +38 -9
- package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
- package/ios/Extensions/SkeletonUICompat.swift +11 -0
- package/ios/Icon/Icon.swift +21 -23
- package/ios/Icon/Icons.swift +1329 -0
- package/ios/IconButton/IconButton.swift +15 -7
- package/ios/Image/Image.swift +7 -23
- package/ios/Information/Information.swift +27 -9
- package/ios/Input/ErrorView.swift +2 -2
- package/ios/Input/Input.swift +160 -147
- package/ios/Input/InputPhoneNumber.swift +12 -9
- package/ios/Input/InputSearch.swift +102 -75
- package/ios/Input/InputTextArea.swift +128 -145
- package/ios/InputDropDown/InputDropDown.swift +10 -7
- package/ios/InputMoney/InputMoney.swift +33 -11
- package/ios/InputOTP/InputOTP.swift +22 -12
- package/ios/Layout/Card.swift +50 -0
- package/ios/Layout/GridContext.swift +187 -3
- package/ios/Layout/Item.swift +5 -4
- package/ios/Layout/ScreenLayout.swift +380 -0
- package/ios/Layout/Section.swift +4 -100
- package/ios/Loader/Loader.swift +5 -9
- package/ios/Lottie/LottieView.swift +14 -2
- package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +128 -9
- package/ios/Navigation/BottomTab/BottomTab.swift +185 -0
- package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +15 -7
- package/ios/Navigation/BottomTab/DuoBarCompression.swift +105 -0
- package/ios/Navigation/BottomTab/DuoTabBar.swift +304 -0
- package/ios/{Application → Navigation/Component}/FloatingButton.swift +14 -3
- package/ios/Navigation/Component/Header.swift +531 -0
- package/ios/Navigation/Component/HeaderBackground.swift +133 -0
- package/ios/Navigation/Component/HeaderJourney.swift +73 -0
- package/ios/Navigation/Component/HeaderLocation.swift +48 -0
- package/ios/Navigation/Component/HeaderRight.swift +377 -0
- package/ios/Navigation/Component/HeaderTitle.swift +36 -0
- package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +21 -24
- package/ios/Navigation/Component/SideBar.swift +183 -0
- package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +5 -6
- package/ios/Navigation/HostBackGestureBridge.swift +78 -0
- package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +5 -2
- package/ios/Navigation/Navigation.swift +204 -0
- package/ios/Navigation/NavigationContainer.swift +194 -0
- package/ios/{Application/Navigation → Navigation}/Navigator.swift +177 -91
- package/ios/Navigation/StackScreen.swift +820 -0
- package/ios/Navigation/StackTransition.swift +400 -0
- package/ios/Navigation/SwipeBackHaptic.swift +33 -0
- package/ios/{Application/Navigation → Navigation}/Tracking/ScreenTracker.swift +12 -5
- package/ios/OTPKeyboard/OTPKeyboard.swift +8 -11
- package/ios/Pagination/PaginationScroll.swift +2 -2
- package/ios/Popup/PopupDisplay.swift +72 -136
- package/ios/Popup/PopupInput.swift +10 -13
- package/ios/Popup/PopupLayout.swift +140 -0
- package/ios/Popup/PopupNotify.swift +56 -73
- package/ios/Popup/PopupPromotion.swift +31 -47
- package/ios/PopupView/FullscreenPopup.swift +13 -19
- package/ios/PopupView/PopupView.swift +1 -1
- package/ios/PopupView/Utils++.swift +0 -8
- package/ios/ProgressInfo/ProgressInfo.swift +4 -2
- package/ios/Radio/Radio.swift +2 -1
- package/ios/Rating/Rating.swift +9 -1
- package/ios/Resources/dot_loading.json +558 -0
- package/ios/Resources/lottie_circle_loader.json +1 -0
- package/ios/ScrollIndicator/ScrollIndicator.swift +11 -10
- package/ios/Skeleton/Skeleton.swift +22 -27
- package/ios/Slider/Slider.swift +16 -18
- package/ios/Stepper/Stepper.swift +6 -1
- package/ios/Steps/Steps.swift +8 -6
- package/ios/SuggestAction/SuggestAction.swift +5 -0
- package/ios/Swipeable/SwipeCell.swift +10 -6
- package/ios/Switch/Switch.swift +22 -20
- package/ios/TabView/TabView.swift +102 -21
- package/ios/Tag/Tag.swift +15 -4
- package/ios/Template/Logo/Logo.swift +2 -1
- package/ios/Template/TrustBanner/TrustBanner.swift +106 -54
- package/ios/Title/Title.swift +6 -17
- package/ios/Tooltip/Tooltip.swift +29 -14
- package/ios/Typography/Text.swift +61 -19
- package/ios/Uploader/Uploader.swift +3 -1
- package/ios/native-kits.podspec +2 -1
- package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +80 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +1 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
- package/package.json +1 -1
- package/scripts/gen-ios-icons.mjs +36 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +0 -52
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +0 -183
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +0 -207
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +0 -39
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +0 -86
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +0 -76
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +0 -76
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +0 -293
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +0 -33
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +0 -110
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +0 -378
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +0 -69
- package/ios/Application/Components.swift +0 -270
- package/ios/Application/ComposeApi.swift +0 -22
- package/ios/Application/HeaderRight.swift +0 -306
- package/ios/Application/Navigation/BottomTab/BottomTab.swift +0 -104
- package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
- package/ios/Application/Navigation/HeaderTitle.swift +0 -57
- package/ios/Application/Navigation/NavigationContainer.swift +0 -213
- package/ios/Application/Navigation/NavigationOptions.swift +0 -109
- package/ios/Application/Navigation/NavigatorManager.swift +0 -22
- package/ios/Application/Navigation/component/Header.swift +0 -178
- package/ios/Application/Navigation/component/HeaderBackground.swift +0 -83
- package/ios/Application/Navigation/component/HeaderColor.swift +0 -65
- package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +0 -11
- package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
- package/ios/Application/Navigation/component/NavigationHeaderRight.swift +0 -196
- package/ios/Application/Screen.swift +0 -250
- package/ios/Application/StackScreen.swift +0 -492
- /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
|
@@ -19,7 +19,7 @@ private struct RenderRightIcon: View {
|
|
|
19
19
|
// Mirrors Compose's two independent `if` blocks: loading spinner and icon
|
|
20
20
|
// can both render at once, not exclusively (not an if/else).
|
|
21
21
|
if loading {
|
|
22
|
-
ActivityIndicator(isAnimating: .constant(true), style: .medium)
|
|
22
|
+
ActivityIndicator(isAnimating: .constant(true), style: .medium, color: color)
|
|
23
23
|
.frame(width: 16, height: 16)
|
|
24
24
|
}
|
|
25
25
|
if !icon.isEmpty {
|
|
@@ -98,7 +98,7 @@ public struct InputDropDown: View {
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
public var body: some View {
|
|
101
|
-
VStack(alignment: .leading, spacing:
|
|
101
|
+
VStack(alignment: .leading, spacing: 0) {
|
|
102
102
|
ZStack(alignment: .topLeading) {
|
|
103
103
|
// Floating label
|
|
104
104
|
if !floatingValue.isEmpty || !floatingIcon.isEmpty {
|
|
@@ -114,7 +114,7 @@ public struct InputDropDown: View {
|
|
|
114
114
|
}
|
|
115
115
|
.padding(.horizontal, Spacing.S)
|
|
116
116
|
.background(theme.colors.background.surface)
|
|
117
|
-
.offset(x: Spacing.S, y: -
|
|
117
|
+
.offset(x: Spacing.S, y: -Spacing.S)
|
|
118
118
|
.zIndex(10)
|
|
119
119
|
}
|
|
120
120
|
|
|
@@ -130,6 +130,7 @@ public struct InputDropDown: View {
|
|
|
130
130
|
typography: .bodyDefaultRegular,
|
|
131
131
|
color: value.isEmpty ? placeholderColor : textColor
|
|
132
132
|
)
|
|
133
|
+
.fontSize(16, lineHeight: 24)
|
|
133
134
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
134
135
|
|
|
135
136
|
RenderRightIcon(
|
|
@@ -146,10 +147,6 @@ public struct InputDropDown: View {
|
|
|
146
147
|
.stroke(getBorderColor(isFocused: false, error: error, disabled: disabled), lineWidth: 1)
|
|
147
148
|
)
|
|
148
149
|
}
|
|
149
|
-
.contentShape(Rectangle())
|
|
150
|
-
.onTapGesture {
|
|
151
|
-
if !disabled { onPress() }
|
|
152
|
-
}
|
|
153
150
|
|
|
154
151
|
ErrorView(
|
|
155
152
|
errorMessage: error,
|
|
@@ -157,8 +154,14 @@ public struct InputDropDown: View {
|
|
|
157
154
|
hintText: hintText
|
|
158
155
|
)
|
|
159
156
|
}
|
|
157
|
+
// Compose makes the whole column clickable, error/hint row included.
|
|
158
|
+
.contentShape(Rectangle())
|
|
159
|
+
.onTapGesture {
|
|
160
|
+
if !disabled { onPress() }
|
|
161
|
+
}
|
|
160
162
|
.accessibilityElement(children: .combine)
|
|
161
163
|
.accessibility(label: Text(floatingValue))
|
|
164
|
+
.accessibilityIdentifier(disabled ? "input_\(floatingValue)_disabled" : "input_\(floatingValue)")
|
|
162
165
|
}
|
|
163
166
|
|
|
164
167
|
// MARK: - Computed colors
|
|
@@ -3,9 +3,8 @@
|
|
|
3
3
|
// MoMoUIKits
|
|
4
4
|
//
|
|
5
5
|
// Native SwiftUI reimplementation of Compose `InputMoney`.
|
|
6
|
-
// The bound `text` holds RAW DIGITS only
|
|
7
|
-
//
|
|
8
|
-
// thousands-separated currency). Display formatting is applied live.
|
|
6
|
+
// The bound `text` holds RAW DIGITS only, as in Compose; the field shows
|
|
7
|
+
// the thousands-separated currency and edits apply at the caret.
|
|
9
8
|
//
|
|
10
9
|
|
|
11
10
|
import Foundation
|
|
@@ -165,17 +164,18 @@ public struct InputMoney: View {
|
|
|
165
164
|
// MARK: - Body
|
|
166
165
|
|
|
167
166
|
public var body: some View {
|
|
168
|
-
VStack(alignment: .leading, spacing:
|
|
167
|
+
VStack(alignment: .leading, spacing: 0) {
|
|
169
168
|
ZStack(alignment: .topLeading) {
|
|
170
169
|
// Floating label
|
|
171
170
|
if !floatingValue.isEmpty || !floatingIcon.isEmpty {
|
|
172
|
-
HStack(spacing:
|
|
171
|
+
HStack(spacing: 0) {
|
|
173
172
|
MomoText(floatingValue, typography: .labelSMedium, color: floatingTitleColor)
|
|
174
173
|
if required {
|
|
175
174
|
MomoText("*", typography: .labelSMedium, color: theme.colors.error.primary)
|
|
176
175
|
}
|
|
177
176
|
if !floatingIcon.isEmpty {
|
|
178
177
|
Icon(source: floatingIcon, size: 16, color: floatingIconTintColor)
|
|
178
|
+
.padding(.leading, Spacing.XS)
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
181
|
.padding(.horizontal, Spacing.S)
|
|
@@ -204,17 +204,19 @@ public struct InputMoney: View {
|
|
|
204
204
|
|
|
205
205
|
// Money field
|
|
206
206
|
ZStack(alignment: .leading) {
|
|
207
|
-
if
|
|
207
|
+
if !hasDigits {
|
|
208
208
|
MomoText(
|
|
209
209
|
placeholder,
|
|
210
210
|
typography: fontWeight == .bold ? .actionSBold : .bodyDefaultRegular,
|
|
211
211
|
color: placeholderColor
|
|
212
212
|
)
|
|
213
|
+
.fontSize(20, lineHeight: 24)
|
|
213
214
|
}
|
|
214
215
|
MoneyTextField(
|
|
215
216
|
digits: $text,
|
|
216
217
|
keyboardType: keyboardType,
|
|
217
218
|
fontWeight: fontWeight == .bold ? .bold : .regular,
|
|
219
|
+
fontFamily: theme.font,
|
|
218
220
|
textColor: UIColor(textColor),
|
|
219
221
|
cursorColor: UIColor(theme.colors.primary),
|
|
220
222
|
isDisabled: disabled,
|
|
@@ -225,7 +227,7 @@ public struct InputMoney: View {
|
|
|
225
227
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
226
228
|
|
|
227
229
|
// Clear button (focused + has value)
|
|
228
|
-
if isFocused &&
|
|
230
|
+
if isFocused && hasDigits {
|
|
229
231
|
SwiftUI.Button(action: {
|
|
230
232
|
text = ""
|
|
231
233
|
onChangeText?("")
|
|
@@ -239,7 +241,7 @@ public struct InputMoney: View {
|
|
|
239
241
|
// Loading indicator and right icon can both render (mirrors
|
|
240
242
|
// Compose's two independent `if` blocks, not if/else).
|
|
241
243
|
if loading {
|
|
242
|
-
ActivityIndicator(isAnimating: .constant(true), style: .medium)
|
|
244
|
+
ActivityIndicator(isAnimating: .constant(true), style: .medium, color: iconTintColor)
|
|
243
245
|
.frame(width: 16, height: 16)
|
|
244
246
|
.padding(.leading, Spacing.S)
|
|
245
247
|
}
|
|
@@ -264,6 +266,9 @@ public struct InputMoney: View {
|
|
|
264
266
|
|
|
265
267
|
ErrorView(errorMessage: error, errorSpacing: errorSpacing, hintText: "")
|
|
266
268
|
}
|
|
269
|
+
.accessibilityElement(children: .contain)
|
|
270
|
+
.accessibilityLabel(floatingValue)
|
|
271
|
+
.accessibilityIdentifier(disabled ? "input_\(floatingValue)_disabled" : "input_\(floatingValue)")
|
|
267
272
|
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { _ in
|
|
268
273
|
if isFocused {
|
|
269
274
|
DispatchQueue.main.async { handleFocusChange(false) }
|
|
@@ -273,6 +278,8 @@ public struct InputMoney: View {
|
|
|
273
278
|
|
|
274
279
|
// MARK: - Helpers
|
|
275
280
|
|
|
281
|
+
private var hasDigits: Bool { text.contains(where: isMoneyDigit) }
|
|
282
|
+
|
|
276
283
|
private func handleFocusChange(_ focused: Bool) {
|
|
277
284
|
guard isFocused != focused else { return }
|
|
278
285
|
isFocused = focused
|
|
@@ -303,6 +310,7 @@ private struct MoneyTextField: UIViewRepresentable {
|
|
|
303
310
|
@Binding var digits: String
|
|
304
311
|
var keyboardType: UIKeyboardType
|
|
305
312
|
var fontWeight: UIFont.Weight
|
|
313
|
+
var fontFamily: String
|
|
306
314
|
var textColor: UIColor
|
|
307
315
|
var cursorColor: UIColor
|
|
308
316
|
var isDisabled: Bool
|
|
@@ -315,7 +323,7 @@ private struct MoneyTextField: UIViewRepresentable {
|
|
|
315
323
|
let field = UITextField()
|
|
316
324
|
field.delegate = context.coordinator
|
|
317
325
|
field.keyboardType = keyboardType
|
|
318
|
-
field.font =
|
|
326
|
+
field.font = font(context)
|
|
319
327
|
field.textColor = textColor
|
|
320
328
|
field.isEnabled = !isDisabled
|
|
321
329
|
field.tintColor = cursorColor
|
|
@@ -328,10 +336,14 @@ private struct MoneyTextField: UIViewRepresentable {
|
|
|
328
336
|
func updateUIView(_ field: UITextField, context: Context) {
|
|
329
337
|
context.coordinator.parent = self
|
|
330
338
|
// Re-applied here, not just in makeUIView, so a font-scale change reaches an existing field.
|
|
331
|
-
field.font =
|
|
339
|
+
field.font = font(context)
|
|
332
340
|
field.isEnabled = !isDisabled
|
|
333
341
|
field.textColor = textColor
|
|
334
342
|
field.tintColor = cursorColor
|
|
343
|
+
if field.keyboardType != keyboardType {
|
|
344
|
+
field.keyboardType = keyboardType
|
|
345
|
+
field.reloadInputViews()
|
|
346
|
+
}
|
|
335
347
|
let expected = displayValue(digits)
|
|
336
348
|
if field.text != expected {
|
|
337
349
|
field.text = expected
|
|
@@ -340,10 +352,20 @@ private struct MoneyTextField: UIViewRepresentable {
|
|
|
340
352
|
}
|
|
341
353
|
|
|
342
354
|
fileprivate func displayValue(_ raw: String) -> String {
|
|
343
|
-
let numeric = raw.filter
|
|
355
|
+
let numeric = raw.filter(isMoneyDigit)
|
|
344
356
|
return numeric.isEmpty ? "" : formatNumberToMoney(numeric)
|
|
345
357
|
}
|
|
346
358
|
|
|
359
|
+
// Theme family as Compose `getFontFamily`: Momo Trust Sans only for whitelisted hosts, else system.
|
|
360
|
+
private func font(_ context: Context) -> UIFont {
|
|
361
|
+
let size = context.environment.momoFontScale(20)
|
|
362
|
+
if KitDesignSystem.shared.isWhiteList, fontFamily == "MomoTrustSans",
|
|
363
|
+
let font = UIFont(name: fontWeight == .bold ? "MomoTrustSans-Bold" : "MomoTrustSans-Regular", size: size) {
|
|
364
|
+
return font
|
|
365
|
+
}
|
|
366
|
+
return .systemFont(ofSize: size, weight: fontWeight)
|
|
367
|
+
}
|
|
368
|
+
|
|
347
369
|
final class Coordinator: NSObject, UITextFieldDelegate {
|
|
348
370
|
var parent: MoneyTextField
|
|
349
371
|
|
|
@@ -48,6 +48,7 @@ public struct InputOTP: View {
|
|
|
48
48
|
|
|
49
49
|
@FocusState private var isFocused: Bool
|
|
50
50
|
@State private var hasBeenFocused: Bool = false
|
|
51
|
+
@State private var fieldText: String
|
|
51
52
|
|
|
52
53
|
private var maxLength: Int { length ?? 10 }
|
|
53
54
|
|
|
@@ -64,6 +65,7 @@ public struct InputOTP: View {
|
|
|
64
65
|
onBlur: (() -> Void)? = nil
|
|
65
66
|
) {
|
|
66
67
|
self._value = value
|
|
68
|
+
self._fieldText = State(initialValue: value.wrappedValue)
|
|
67
69
|
self.length = length
|
|
68
70
|
self.errorMessage = errorMessage
|
|
69
71
|
self.errorSpacing = errorSpacing
|
|
@@ -105,7 +107,7 @@ public struct InputOTP: View {
|
|
|
105
107
|
HStack {
|
|
106
108
|
Spacer()
|
|
107
109
|
SwiftUI.Button(action: clearText) {
|
|
108
|
-
Icon(source: "24_navigation_close_circle_full", size: 12, color: theme.colors.text.
|
|
110
|
+
Icon(source: "24_navigation_close_circle_full", size: 12, color: theme.colors.text.default)
|
|
109
111
|
.padding(.trailing, 12)
|
|
110
112
|
}
|
|
111
113
|
}
|
|
@@ -122,17 +124,24 @@ public struct InputOTP: View {
|
|
|
122
124
|
// MARK: Subviews
|
|
123
125
|
|
|
124
126
|
private var hiddenField: some View {
|
|
125
|
-
TextField("", text:
|
|
126
|
-
get: { value },
|
|
127
|
-
set: { handleChangeText($0) }
|
|
128
|
-
))
|
|
127
|
+
TextField("", text: $fieldText)
|
|
129
128
|
.keyboardType(dataType == "number" ? .numberPad : .asciiCapable)
|
|
129
|
+
.textInputAutocapitalization(.never)
|
|
130
|
+
.autocorrectionDisabled()
|
|
130
131
|
.focused($isFocused)
|
|
131
132
|
.foregroundColor(.clear)
|
|
132
133
|
.accentColor(.clear)
|
|
133
134
|
.frame(maxWidth: .infinity)
|
|
134
135
|
.frame(height: 56)
|
|
135
136
|
.opacity(0.01)
|
|
137
|
+
// Writes a rejected edit back, so the field stays in step with `value` like Compose's
|
|
138
|
+
// controlled field instead of keeping the extra character and swallowing the next key.
|
|
139
|
+
.onChange(of: fieldText) { text in
|
|
140
|
+
if text != value && !handleChangeText(text) { fieldText = value }
|
|
141
|
+
}
|
|
142
|
+
.onChange(of: value) { newValue in
|
|
143
|
+
if fieldText != newValue { fieldText = newValue }
|
|
144
|
+
}
|
|
136
145
|
.onChange(of: isFocused) { focused in
|
|
137
146
|
if focused {
|
|
138
147
|
onFocus?()
|
|
@@ -147,7 +156,7 @@ public struct InputOTP: View {
|
|
|
147
156
|
private var cells: some View {
|
|
148
157
|
if let length = length {
|
|
149
158
|
HStack(spacing: Spacing.L) {
|
|
150
|
-
ForEach(0 ..< length, id: \.self) { i in
|
|
159
|
+
ForEach(0 ..< max(length, 0), id: \.self) { i in
|
|
151
160
|
cell(at: i)
|
|
152
161
|
}
|
|
153
162
|
}
|
|
@@ -155,7 +164,7 @@ public struct InputOTP: View {
|
|
|
155
164
|
} else {
|
|
156
165
|
HStack(spacing: Spacing.L) {
|
|
157
166
|
if value.isEmpty && !isFocused && !placeholder.isEmpty {
|
|
158
|
-
MomoText(placeholder, typography: .
|
|
167
|
+
MomoText(placeholder, typography: .bodyDefaultRegular, color: theme.colors.text.hint)
|
|
159
168
|
} else {
|
|
160
169
|
ForEach(Array(value.enumerated()), id: \.offset) { _, ch in
|
|
161
170
|
MomoText(String(ch), typography: .headerDefaultBold, color: theme.colors.text.default)
|
|
@@ -186,13 +195,14 @@ public struct InputOTP: View {
|
|
|
186
195
|
|
|
187
196
|
// MARK: Logic
|
|
188
197
|
|
|
189
|
-
|
|
198
|
+
@discardableResult
|
|
199
|
+
private func handleChangeText(_ text: String) -> Bool {
|
|
190
200
|
let validData = dataType != "number" || text.allSatisfy { $0.isNumber }
|
|
191
201
|
let changed = text.count != value.count
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
202
|
+
guard text.count <= maxLength && validData && changed else { return false }
|
|
203
|
+
value = text
|
|
204
|
+
onChangeText?(text)
|
|
205
|
+
return true
|
|
196
206
|
}
|
|
197
207
|
|
|
198
208
|
private func clearText() {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
/// Rounded surface laid out on the kit grid (8pt gutter), inset by the screen margin.
|
|
4
|
+
/// Mirrors `vn.momo.kits.layout.Card`.
|
|
5
|
+
public struct CardView<Content: View>: View {
|
|
6
|
+
@Environment(\.appTheme) private var theme
|
|
7
|
+
|
|
8
|
+
// MARK: Lifecycle
|
|
9
|
+
|
|
10
|
+
public init(
|
|
11
|
+
useShadow: Bool = false,
|
|
12
|
+
backgroundImage: String? = nil,
|
|
13
|
+
backgroundColor: Color? = nil,
|
|
14
|
+
contentPadding: EdgeInsets = EdgeInsets(top: Spacing.M, leading: Spacing.M, bottom: Spacing.M, trailing: Spacing.M),
|
|
15
|
+
@ViewBuilder content: @escaping () -> Content
|
|
16
|
+
) {
|
|
17
|
+
self.useShadow = useShadow
|
|
18
|
+
self.backgroundImage = backgroundImage
|
|
19
|
+
self.backgroundColor = backgroundColor
|
|
20
|
+
self.contentPadding = contentPadding
|
|
21
|
+
self.content = content
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// MARK: Public
|
|
25
|
+
|
|
26
|
+
public var body: some View {
|
|
27
|
+
GridFlow(gutter: Spacing.S, content: content)
|
|
28
|
+
.padding(contentPadding)
|
|
29
|
+
.background(background)
|
|
30
|
+
.background(backgroundColor ?? theme.colors.background.surface)
|
|
31
|
+
.clipShape(RoundedRectangle(cornerRadius: Radius.M))
|
|
32
|
+
// Approximates Compose `shadow(6.dp)` elevation.
|
|
33
|
+
.shadow(color: Color.black.opacity(useShadow ? 0.12 : 0), radius: useShadow ? 6 : 0, x: 0, y: useShadow ? 3 : 0)
|
|
34
|
+
.padding(.horizontal, Spacing.M)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// MARK: Private
|
|
38
|
+
|
|
39
|
+
private let useShadow: Bool
|
|
40
|
+
private let backgroundImage: String?
|
|
41
|
+
private let backgroundColor: Color?
|
|
42
|
+
private let contentPadding: EdgeInsets
|
|
43
|
+
private let content: () -> Content
|
|
44
|
+
|
|
45
|
+
@ViewBuilder private var background: some View {
|
|
46
|
+
if let backgroundImage {
|
|
47
|
+
ImageView(backgroundImage, contentMode: .fill).clipped()
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -1,20 +1,31 @@
|
|
|
1
1
|
import SwiftUI
|
|
2
2
|
|
|
3
|
-
/// Grid metrics published by a layout container (`SectionView`) so span-based children
|
|
3
|
+
/// Grid metrics published by a layout container (`SectionView`, `CardView`) so span-based children
|
|
4
4
|
/// can size themselves. Mirrors `vn.momo.kits.layout.GridContext`.
|
|
5
5
|
public struct GridContext: Equatable {
|
|
6
6
|
public let numberOfColumns: Int
|
|
7
7
|
public let gutter: CGFloat
|
|
8
8
|
public let sizePerSpan: CGFloat
|
|
9
|
+
/// Pixels per point; span sizes are floored onto the pixel grid when it is set.
|
|
10
|
+
public let displayScale: CGFloat
|
|
9
11
|
|
|
10
|
-
public init(
|
|
12
|
+
public init(
|
|
13
|
+
numberOfColumns: Int = 12,
|
|
14
|
+
gutter: CGFloat = Spacing.M,
|
|
15
|
+
sizePerSpan: CGFloat = 0,
|
|
16
|
+
displayScale: CGFloat = 0
|
|
17
|
+
) {
|
|
11
18
|
self.numberOfColumns = numberOfColumns
|
|
12
19
|
self.gutter = gutter
|
|
13
20
|
self.sizePerSpan = sizePerSpan
|
|
21
|
+
self.displayScale = displayScale
|
|
14
22
|
}
|
|
15
23
|
|
|
16
24
|
public func sizeForSpan(_ span: Int) -> CGFloat {
|
|
17
|
-
sizePerSpan * CGFloat(span) + gutter * CGFloat(span - 1)
|
|
25
|
+
let size = sizePerSpan * CGFloat(span) + gutter * CGFloat(span - 1)
|
|
26
|
+
guard displayScale > 0 else { return size }
|
|
27
|
+
// Compose floors in pixels so a full row of spans never overflows by a sub-pixel.
|
|
28
|
+
return floor(size * displayScale) / displayScale
|
|
18
29
|
}
|
|
19
30
|
}
|
|
20
31
|
|
|
@@ -28,3 +39,176 @@ public extension EnvironmentValues {
|
|
|
28
39
|
set { self[GridContextKey.self] = newValue }
|
|
29
40
|
}
|
|
30
41
|
}
|
|
42
|
+
|
|
43
|
+
/// Measures the width it is given, publishes the grid for it and wraps children in a flow row.
|
|
44
|
+
/// Shared body of `SectionView` and `CardView` (Compose `gridColumns()` + `gridContext()` + `FlowRow`).
|
|
45
|
+
struct GridFlow<Content: View>: View {
|
|
46
|
+
let gutter: CGFloat
|
|
47
|
+
let content: () -> Content
|
|
48
|
+
|
|
49
|
+
@Environment(\.screenLayout) private var screenLayout
|
|
50
|
+
@Environment(\.displayScale) private var displayScale
|
|
51
|
+
/// The Compose `BoxWithConstraints` width.
|
|
52
|
+
@State private var availableWidth: CGFloat = 0
|
|
53
|
+
|
|
54
|
+
var body: some View {
|
|
55
|
+
flow
|
|
56
|
+
.environment(\.gridContext, grid)
|
|
57
|
+
.frame(maxWidth: .infinity, alignment: .topLeading)
|
|
58
|
+
.background(widthReader)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private var grid: GridContext {
|
|
62
|
+
let columns = screenLayout.contentWidth > 600 ? 24 : 12
|
|
63
|
+
let column = (availableWidth - gutter * CGFloat(columns - 1)) / CGFloat(columns)
|
|
64
|
+
return GridContext(
|
|
65
|
+
numberOfColumns: columns,
|
|
66
|
+
gutter: gutter,
|
|
67
|
+
sizePerSpan: max(0, column),
|
|
68
|
+
displayScale: displayScale
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@ViewBuilder private var flow: some View {
|
|
73
|
+
if #available(iOS 16.0, *) {
|
|
74
|
+
FlowRowLayout(gutter: gutter) { content() }
|
|
75
|
+
} else {
|
|
76
|
+
_VariadicView.Tree(FlowRowFallback(gutter: gutter, maxWidth: availableWidth)) { content() }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private var widthReader: some View {
|
|
81
|
+
GeometryReader { proxy in
|
|
82
|
+
Color.clear.preference(key: GridWidthKey.self, value: proxy.size.width)
|
|
83
|
+
}
|
|
84
|
+
.onPreferenceChange(GridWidthKey.self) { availableWidth = $0 }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private struct GridWidthKey: PreferenceKey {
|
|
89
|
+
static var defaultValue: CGFloat = 0
|
|
90
|
+
|
|
91
|
+
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
|
|
92
|
+
value = max(value, nextValue())
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// iOS 15 has no `Layout`: the children are measured, then pinned to the same rows as
|
|
97
|
+
/// `FlowRowLayout` through alignment guides.
|
|
98
|
+
private struct FlowRowFallback: _VariadicView_UnaryViewRoot {
|
|
99
|
+
let gutter: CGFloat
|
|
100
|
+
let maxWidth: CGFloat
|
|
101
|
+
|
|
102
|
+
func body(children: _VariadicView.Children) -> some View {
|
|
103
|
+
FlowRowPlacement(children: children, gutter: gutter, maxWidth: maxWidth)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private struct FlowRowPlacement: View {
|
|
108
|
+
let children: _VariadicView.Children
|
|
109
|
+
let gutter: CGFloat
|
|
110
|
+
let maxWidth: CGFloat
|
|
111
|
+
@State private var sizes: [AnyHashable: CGSize] = [:]
|
|
112
|
+
|
|
113
|
+
var body: some View {
|
|
114
|
+
let placement = place()
|
|
115
|
+
ZStack(alignment: .topLeading) {
|
|
116
|
+
ForEach(children) { child in
|
|
117
|
+
child
|
|
118
|
+
// FlowRowLayout proposes no height
|
|
119
|
+
.fixedSize(horizontal: false, vertical: true)
|
|
120
|
+
.background(
|
|
121
|
+
GeometryReader { proxy in
|
|
122
|
+
Color.clear.preference(key: FlowChildSizesKey.self, value: [child.id: proxy.size])
|
|
123
|
+
}
|
|
124
|
+
)
|
|
125
|
+
.alignmentGuide(.leading) { _ in -(placement.origins[child.id]?.x ?? 0) }
|
|
126
|
+
.alignmentGuide(.top) { _ in -(placement.origins[child.id]?.y ?? 0) }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
.frame(height: placement.height, alignment: .topLeading)
|
|
130
|
+
.onPreferenceChange(FlowChildSizesKey.self) { sizes = $0 }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private func place() -> (origins: [AnyHashable: CGPoint], height: CGFloat) {
|
|
134
|
+
var origins: [AnyHashable: CGPoint] = [:]
|
|
135
|
+
var x: CGFloat = 0
|
|
136
|
+
var y: CGFloat = 0
|
|
137
|
+
var rowHeight: CGFloat = 0
|
|
138
|
+
for child in children {
|
|
139
|
+
let size = sizes[child.id] ?? .zero
|
|
140
|
+
// Sub-point tolerance: spans are computed to land exactly on `maxWidth`.
|
|
141
|
+
if x > 0, x + size.width > maxWidth + 0.5 {
|
|
142
|
+
x = 0
|
|
143
|
+
y += rowHeight + gutter
|
|
144
|
+
rowHeight = 0
|
|
145
|
+
}
|
|
146
|
+
origins[child.id] = CGPoint(x: x, y: y)
|
|
147
|
+
x += size.width + gutter
|
|
148
|
+
rowHeight = max(rowHeight, size.height)
|
|
149
|
+
}
|
|
150
|
+
return (origins, y + rowHeight)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private struct FlowChildSizesKey: PreferenceKey {
|
|
155
|
+
static var defaultValue: [AnyHashable: CGSize] = [:]
|
|
156
|
+
|
|
157
|
+
static func reduce(value: inout [AnyHashable: CGSize], nextValue: () -> [AnyHashable: CGSize]) {
|
|
158
|
+
value.merge(nextValue()) { $1 }
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/// Wrapping row with equal horizontal/vertical spacing — the Compose `FlowRow` equivalent.
|
|
163
|
+
@available(iOS 16.0, *)
|
|
164
|
+
private struct FlowRowLayout: Layout {
|
|
165
|
+
struct Row {
|
|
166
|
+
var items: [(index: Int, size: CGSize)] = []
|
|
167
|
+
var width: CGFloat = 0
|
|
168
|
+
var height: CGFloat = 0
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let gutter: CGFloat
|
|
172
|
+
|
|
173
|
+
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) -> CGSize {
|
|
174
|
+
let maxWidth = proposal.width ?? .infinity
|
|
175
|
+
let rows = rows(for: subviews, maxWidth: maxWidth)
|
|
176
|
+
let height = rows.reduce(0) { $0 + $1.height } + gutter * CGFloat(max(0, rows.count - 1))
|
|
177
|
+
return CGSize(width: proposal.width ?? rows.map(\.width).max() ?? 0, height: height)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) {
|
|
181
|
+
var y = bounds.minY
|
|
182
|
+
for row in rows(for: subviews, maxWidth: bounds.width) {
|
|
183
|
+
var x = bounds.minX
|
|
184
|
+
for item in row.items {
|
|
185
|
+
subviews[item.index].place(
|
|
186
|
+
at: CGPoint(x: x, y: y),
|
|
187
|
+
anchor: .topLeading,
|
|
188
|
+
proposal: ProposedViewSize(item.size)
|
|
189
|
+
)
|
|
190
|
+
x += item.size.width + gutter
|
|
191
|
+
}
|
|
192
|
+
y += row.height + gutter
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private func rows(for subviews: Subviews, maxWidth: CGFloat) -> [Row] {
|
|
197
|
+
var rows: [Row] = []
|
|
198
|
+
var current = Row()
|
|
199
|
+
for index in subviews.indices {
|
|
200
|
+
let size = subviews[index].sizeThatFits(ProposedViewSize(width: maxWidth, height: nil))
|
|
201
|
+
let widthIfAppended = current.items.isEmpty ? size.width : current.width + gutter + size.width
|
|
202
|
+
// Sub-point tolerance: spans are computed to land exactly on `maxWidth`.
|
|
203
|
+
if !current.items.isEmpty, widthIfAppended > maxWidth + 0.5 {
|
|
204
|
+
rows.append(current)
|
|
205
|
+
current = Row()
|
|
206
|
+
}
|
|
207
|
+
current.width = current.items.isEmpty ? size.width : current.width + gutter + size.width
|
|
208
|
+
current.height = max(current.height, size.height)
|
|
209
|
+
current.items.append((index, size))
|
|
210
|
+
}
|
|
211
|
+
if !current.items.isEmpty { rows.append(current) }
|
|
212
|
+
return rows
|
|
213
|
+
}
|
|
214
|
+
}
|
package/ios/Layout/Item.swift
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import SwiftUI
|
|
2
2
|
|
|
3
|
-
/// Span-sized cell inside a `SectionView`. Reads the grid published by its container;
|
|
3
|
+
/// Span-sized cell inside a `SectionView` / `CardView`. Reads the grid published by its container;
|
|
4
4
|
/// without one it just wraps its content. Mirrors `vn.momo.kits.layout.Item`.
|
|
5
5
|
public struct ItemView<Content: View>: View {
|
|
6
6
|
// MARK: Lifecycle
|
|
7
7
|
|
|
8
|
+
/// `widthSpan` nil spans the full row, whatever the container's column count.
|
|
8
9
|
public init(
|
|
9
|
-
widthSpan: Int =
|
|
10
|
+
widthSpan: Int? = nil,
|
|
10
11
|
heightSpan: Int = 0,
|
|
11
12
|
@ViewBuilder content: @escaping () -> Content
|
|
12
13
|
) {
|
|
@@ -26,14 +27,14 @@ public struct ItemView<Content: View>: View {
|
|
|
26
27
|
|
|
27
28
|
// MARK: Private
|
|
28
29
|
|
|
29
|
-
private let widthSpan: Int
|
|
30
|
+
private let widthSpan: Int?
|
|
30
31
|
private let heightSpan: Int
|
|
31
32
|
private let content: () -> Content
|
|
32
33
|
|
|
33
34
|
@Environment(\.gridContext) private var grid
|
|
34
35
|
|
|
35
36
|
private var width: CGFloat? {
|
|
36
|
-
grid.map { $0.sizeForSpan(widthSpan) }
|
|
37
|
+
grid.map { $0.sizeForSpan(widthSpan ?? $0.numberOfColumns) }
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
private var height: CGFloat? {
|