@momo-kits/native-kits 0.167.1-duo.1-debug → 0.167.1-duo.10-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 +32 -0
- 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/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/Divider.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.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/layout/Card.kt +13 -11
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +21 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +232 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +11 -9
- 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/NavigationContainer.kt +4 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +143 -31
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +100 -66
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +22 -16
- 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 +66 -18
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +23 -14
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +3 -3
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +1 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +81 -24
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +1 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +185 -0
- 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/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +4 -1
- 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 +86 -0
- package/gradle.properties +1 -1
- package/ios/Application/ApplicationEnvironment.swift +17 -0
- package/ios/Application/ComposeApi.swift +2 -1
- package/ios/Application/{Components.swift → LegacyHeader.swift} +7 -65
- package/ios/Application/Screen.swift +3 -2
- 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/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 +123 -3
- package/ios/Layout/Item.swift +5 -4
- package/ios/Layout/ScreenLayout.swift +312 -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 +97 -8
- package/ios/Navigation/BottomTab/BottomTab.swift +187 -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 +493 -0
- package/ios/Navigation/Component/HeaderBackground.swift +134 -0
- package/ios/Navigation/Component/HeaderJourney.swift +73 -0
- package/ios/Navigation/Component/HeaderLocation.swift +48 -0
- package/ios/Navigation/Component/HeaderRight.swift +386 -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 +176 -0
- package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +5 -5
- package/ios/Navigation/HostBackGestureBridge.swift +57 -0
- package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +5 -1
- package/ios/Navigation/Navigation.swift +190 -0
- package/ios/{Application/Navigation → Navigation}/NavigationContainer.swift +92 -81
- package/ios/{Application/Navigation → Navigation}/Navigator.swift +185 -36
- package/ios/Navigation/StackScreen.swift +782 -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/.claude/settings.local.json +0 -185
- 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/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/StackScreen.swift +0 -492
- package/local.properties +0 -8
- /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
|
@@ -32,7 +32,7 @@ public struct Collapse<Content: View>: View {
|
|
|
32
32
|
private let titleColor: Color?
|
|
33
33
|
private let accessibilityId: String?
|
|
34
34
|
private let onPress: ((Bool) -> Void)?
|
|
35
|
-
private
|
|
35
|
+
private var content: (() -> Content)?
|
|
36
36
|
|
|
37
37
|
@State private var expanded: Bool
|
|
38
38
|
|
|
@@ -87,31 +87,36 @@ public struct Collapse<Content: View>: View {
|
|
|
87
87
|
header
|
|
88
88
|
|
|
89
89
|
if let content {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
// Compose's expandVertically: the clip grows with the panel while it slides in from the top.
|
|
91
|
+
VStack(spacing: 0) {
|
|
92
|
+
if expanded {
|
|
93
|
+
VStack(spacing: 0) {
|
|
94
|
+
content()
|
|
95
|
+
}
|
|
96
|
+
.frame(maxWidth: .infinity)
|
|
97
|
+
.overlay(
|
|
98
|
+
showBorder
|
|
99
|
+
? Rectangle().strokeBorder(theme.colors.border.default, lineWidth: 1)
|
|
100
|
+
: nil
|
|
101
|
+
)
|
|
102
|
+
.transition(.move(edge: .top))
|
|
93
103
|
}
|
|
94
|
-
.frame(maxWidth: .infinity)
|
|
95
|
-
.overlay(
|
|
96
|
-
showBorder
|
|
97
|
-
? RoundedRectangle(cornerRadius: 0)
|
|
98
|
-
.stroke(theme.colors.border.default, lineWidth: 1)
|
|
99
|
-
: nil
|
|
100
|
-
)
|
|
101
|
-
.transition(.move(edge: .top).combined(with: .opacity))
|
|
102
104
|
}
|
|
105
|
+
.clipped()
|
|
103
106
|
}
|
|
104
107
|
}
|
|
105
108
|
.frame(maxWidth: .infinity)
|
|
106
109
|
.background(resolvedBackgroundColor)
|
|
107
110
|
.clipShape(RoundedRectangle(cornerRadius: Radius.S, style: .continuous))
|
|
108
|
-
.accessibilityIdentifier(automationId)
|
|
109
111
|
.overlay(
|
|
110
112
|
showBorder
|
|
111
113
|
? RoundedRectangle(cornerRadius: Radius.S, style: .continuous)
|
|
112
|
-
.
|
|
114
|
+
.strokeBorder(theme.colors.border.default, lineWidth: 1)
|
|
113
115
|
: nil
|
|
114
116
|
)
|
|
117
|
+
// `.contain` keeps the children's own identifiers instead of overriding them.
|
|
118
|
+
.accessibilityElement(children: .contain)
|
|
119
|
+
.accessibilityIdentifier(automationId)
|
|
115
120
|
}
|
|
116
121
|
|
|
117
122
|
private var header: some View {
|
|
@@ -126,6 +131,7 @@ public struct Collapse<Content: View>: View {
|
|
|
126
131
|
ImageView(image, contentMode: .fill)
|
|
127
132
|
.frame(width: imageSize, height: imageSize)
|
|
128
133
|
.clipShape(RoundedRectangle(cornerRadius: Radius.XS, style: .continuous))
|
|
134
|
+
.accessibilityIdentifier("\(automationId)|img")
|
|
129
135
|
Spacer().frame(width: Spacing.S)
|
|
130
136
|
}
|
|
131
137
|
|
|
@@ -133,15 +139,17 @@ public struct Collapse<Content: View>: View {
|
|
|
133
139
|
MomoText(
|
|
134
140
|
title,
|
|
135
141
|
typography: titleSize.typography,
|
|
136
|
-
color: titleColor ?? theme.colors.text.default
|
|
142
|
+
color: titleColor ?? theme.colors.text.default,
|
|
143
|
+
accessibilityId: "\(automationId)|text"
|
|
137
144
|
)
|
|
138
145
|
if let description {
|
|
139
146
|
MomoText(
|
|
140
147
|
description,
|
|
141
148
|
typography: .bodyDefaultRegular,
|
|
142
|
-
color: theme.colors.text.secondary
|
|
149
|
+
color: theme.colors.text.secondary,
|
|
150
|
+
maxLines: 2,
|
|
151
|
+
accessibilityId: "\(automationId)|description-text"
|
|
143
152
|
)
|
|
144
|
-
.lineLimit(2)
|
|
145
153
|
}
|
|
146
154
|
}
|
|
147
155
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
@@ -152,9 +160,10 @@ public struct Collapse<Content: View>: View {
|
|
|
152
160
|
MomoText(
|
|
153
161
|
subTitle,
|
|
154
162
|
typography: .bodyDefaultRegular,
|
|
155
|
-
color: theme.colors.text.secondary
|
|
163
|
+
color: theme.colors.text.secondary,
|
|
164
|
+
maxLines: 1,
|
|
165
|
+
accessibilityId: "\(automationId)|sub-title-text"
|
|
156
166
|
)
|
|
157
|
-
.lineLimit(1)
|
|
158
167
|
} else if let tagLabel {
|
|
159
168
|
Tag(label: tagLabel, color: tagColor)
|
|
160
169
|
}
|
|
@@ -187,6 +196,48 @@ public struct Collapse<Content: View>: View {
|
|
|
187
196
|
.rotationEffect(.degrees(expanded ? 180 : 0))
|
|
188
197
|
}
|
|
189
198
|
}
|
|
199
|
+
.accessibilityIdentifier("\(automationId)|icon")
|
|
190
200
|
.animation(.easeInOut(duration: 0.3), value: expanded)
|
|
191
201
|
}
|
|
192
202
|
}
|
|
203
|
+
|
|
204
|
+
public extension Collapse where Content == EmptyView {
|
|
205
|
+
/// Header-only collapse, mirroring Compose's `content = null`.
|
|
206
|
+
init(
|
|
207
|
+
title: String,
|
|
208
|
+
description: String? = nil,
|
|
209
|
+
image: String? = nil,
|
|
210
|
+
imageSize: CGFloat = 24,
|
|
211
|
+
subTitle: String? = nil,
|
|
212
|
+
tagLabel: String? = nil,
|
|
213
|
+
tagColor: Tag.TagColor = .default,
|
|
214
|
+
showBorder: Bool = false,
|
|
215
|
+
expandDefault: Bool = false,
|
|
216
|
+
titleSize: CollapseTitleSize = .medium,
|
|
217
|
+
useBackgroundCollapseButton: Bool = false,
|
|
218
|
+
backgroundColor: Color? = nil,
|
|
219
|
+
titleColor: Color? = nil,
|
|
220
|
+
accessibilityId: String? = nil,
|
|
221
|
+
onPress: ((Bool) -> Void)? = nil
|
|
222
|
+
) {
|
|
223
|
+
self.init(
|
|
224
|
+
title: title,
|
|
225
|
+
description: description,
|
|
226
|
+
image: image,
|
|
227
|
+
imageSize: imageSize,
|
|
228
|
+
subTitle: subTitle,
|
|
229
|
+
tagLabel: tagLabel,
|
|
230
|
+
tagColor: tagColor,
|
|
231
|
+
showBorder: showBorder,
|
|
232
|
+
expandDefault: expandDefault,
|
|
233
|
+
titleSize: titleSize,
|
|
234
|
+
useBackgroundCollapseButton: useBackgroundCollapseButton,
|
|
235
|
+
backgroundColor: backgroundColor,
|
|
236
|
+
titleColor: titleColor,
|
|
237
|
+
accessibilityId: accessibilityId,
|
|
238
|
+
onPress: onPress,
|
|
239
|
+
content: { EmptyView() }
|
|
240
|
+
)
|
|
241
|
+
self.content = nil
|
|
242
|
+
}
|
|
243
|
+
}
|
|
@@ -133,7 +133,7 @@ public enum Colors {
|
|
|
133
133
|
public static let yellow06 = Color(hex: "ffe066")
|
|
134
134
|
public static let yellow05 = Color(hex: "ffd633")
|
|
135
135
|
public static let yellow04 = Color(hex: "ffd119")
|
|
136
|
-
public static let yellow03 = Color(hex: "
|
|
136
|
+
public static let yellow03 = Color(hex: "ffcc00")
|
|
137
137
|
public static let yellow02 = Color(hex: "e6b800")
|
|
138
138
|
public static let yellow01 = Color(hex: "cca300")
|
|
139
139
|
|
|
@@ -172,4 +172,13 @@ public enum Colors {
|
|
|
172
172
|
public static let red03 = Color(hex: "f5222d")
|
|
173
173
|
public static let red02 = Color(hex: "dd1f29")
|
|
174
174
|
public static let red01 = Color(hex: "c41b24")
|
|
175
|
+
|
|
176
|
+
public static let opacityBlack20 = Color(hex: "00000033")
|
|
177
|
+
public static let opacityBlack40 = Color(hex: "00000066")
|
|
178
|
+
public static let opacityBlack60 = Color(hex: "00000099")
|
|
179
|
+
public static let opacityBlack80 = Color(hex: "000000cc")
|
|
180
|
+
public static let opacityWhite20 = Color(hex: "ffffff33")
|
|
181
|
+
public static let opacityWhite40 = Color(hex: "ffffff66")
|
|
182
|
+
public static let opacityWhite60 = Color(hex: "ffffff99")
|
|
183
|
+
public static let opacityWhite80 = Color(hex: "ffffffcc")
|
|
175
184
|
}
|
|
@@ -36,33 +36,21 @@ public struct DateTimePicker: View {
|
|
|
36
36
|
self.onChange = onChange
|
|
37
37
|
|
|
38
38
|
let calendar = Calendar.current
|
|
39
|
-
let effectiveSelected = selectedValue ?? DateTimePickerUtils.getCurrentDateTime()
|
|
40
39
|
let effectiveMin = minDate ?? DateTimePickerUtils.createRelativeDate(years: -10)
|
|
41
40
|
let effectiveMax = maxDate ?? DateTimePickerUtils.createRelativeDate(years: 10)
|
|
42
41
|
|
|
43
42
|
// needCheckRange is true only when the caller supplied no bounds.
|
|
44
43
|
let needCheckRange = (minDate == nil && maxDate == nil)
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
} else if effectiveSelected > effectiveMax {
|
|
52
|
-
initialValue = effectiveMax
|
|
53
|
-
} else {
|
|
54
|
-
initialValue = effectiveSelected
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
let comps = calendar.dateComponents([.year, .month, .day, .hour, .minute], from: initialValue)
|
|
58
|
-
self._currentData = State(initialValue: DateTimePickerData(
|
|
59
|
-
day: comps.day ?? 1,
|
|
60
|
-
month: comps.month ?? 1,
|
|
61
|
-
year: comps.year ?? 2000,
|
|
62
|
-
hour: comps.hour ?? 0,
|
|
63
|
-
minute: comps.minute ?? 0
|
|
45
|
+
self._currentData = State(initialValue: Self.pickerData(
|
|
46
|
+
selectedValue: selectedValue,
|
|
47
|
+
effectiveMin: effectiveMin,
|
|
48
|
+
effectiveMax: effectiveMax,
|
|
49
|
+
needCheckRange: needCheckRange
|
|
64
50
|
))
|
|
65
51
|
|
|
52
|
+
self.effectiveMin = effectiveMin
|
|
53
|
+
self.effectiveMax = effectiveMax
|
|
66
54
|
self.effectiveMinComponents = calendar.dateComponents([.year, .month, .day, .hour, .minute], from: effectiveMin)
|
|
67
55
|
self.effectiveMaxComponents = calendar.dateComponents([.year, .month, .day, .hour, .minute], from: effectiveMax)
|
|
68
56
|
self.needCheckRange = needCheckRange
|
|
@@ -102,9 +90,10 @@ public struct DateTimePicker: View {
|
|
|
102
90
|
}
|
|
103
91
|
.frame(maxWidth: .infinity)
|
|
104
92
|
.frame(height: pickerHeight)
|
|
105
|
-
.background(theme.colors.background.surface)
|
|
106
93
|
.padding(.horizontal, Spacing.M)
|
|
94
|
+
.background(theme.colors.background.surface)
|
|
107
95
|
.onAppear { emitIfValid() }
|
|
96
|
+
.onChange(of: selectedValue) { value in resetSelection(to: value) }
|
|
108
97
|
}
|
|
109
98
|
|
|
110
99
|
// MARK: Internal
|
|
@@ -119,10 +108,55 @@ public struct DateTimePicker: View {
|
|
|
119
108
|
|
|
120
109
|
@State private var currentData: DateTimePickerData
|
|
121
110
|
|
|
111
|
+
private let effectiveMin: Date
|
|
112
|
+
private let effectiveMax: Date
|
|
122
113
|
private let effectiveMinComponents: DateComponents
|
|
123
114
|
private let effectiveMaxComponents: DateComponents
|
|
124
115
|
private let needCheckRange: Bool
|
|
125
116
|
|
|
117
|
+
/// Mirrors Compose `initialValue`: the selection is clamped only when no bounds were supplied.
|
|
118
|
+
private static func pickerData(
|
|
119
|
+
selectedValue: Date?,
|
|
120
|
+
effectiveMin: Date,
|
|
121
|
+
effectiveMax: Date,
|
|
122
|
+
needCheckRange: Bool
|
|
123
|
+
) -> DateTimePickerData {
|
|
124
|
+
let effectiveSelected = selectedValue ?? DateTimePickerUtils.getCurrentDateTime()
|
|
125
|
+
let initialValue: Date
|
|
126
|
+
if !needCheckRange {
|
|
127
|
+
initialValue = effectiveSelected
|
|
128
|
+
} else if effectiveSelected < effectiveMin {
|
|
129
|
+
initialValue = effectiveMin
|
|
130
|
+
} else if effectiveSelected > effectiveMax {
|
|
131
|
+
initialValue = effectiveMax
|
|
132
|
+
} else {
|
|
133
|
+
initialValue = effectiveSelected
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let comps = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: initialValue)
|
|
137
|
+
return DateTimePickerData(
|
|
138
|
+
day: comps.day ?? 1,
|
|
139
|
+
month: comps.month ?? 1,
|
|
140
|
+
year: comps.year ?? 2000,
|
|
141
|
+
hour: comps.hour ?? 0,
|
|
142
|
+
minute: comps.minute ?? 0
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// Compose re-seeds the wheels when `selectedValue` changes (`remember(initialValue)`).
|
|
147
|
+
/// A caller echoing `onChange` back lands on the current selection and is a no-op.
|
|
148
|
+
private func resetSelection(to value: Date?) {
|
|
149
|
+
var next = Self.pickerData(
|
|
150
|
+
selectedValue: value,
|
|
151
|
+
effectiveMin: effectiveMin,
|
|
152
|
+
effectiveMax: effectiveMax,
|
|
153
|
+
needCheckRange: needCheckRange
|
|
154
|
+
)
|
|
155
|
+
// The date carries no AM/PM, so keep the wheel's choice.
|
|
156
|
+
next.timeMode = currentData.timeMode
|
|
157
|
+
if next != currentData { currentData = next }
|
|
158
|
+
}
|
|
159
|
+
|
|
126
160
|
private func selectedDataFor(_ name: String) -> String {
|
|
127
161
|
switch name {
|
|
128
162
|
case "day": return DateTimePickerUtils.paddingNum(currentData.day)
|
|
@@ -10,9 +10,8 @@ private let wheelPickerBorderWidth: CGFloat = 1
|
|
|
10
10
|
|
|
11
11
|
/// A reusable single-column wheel picker, mirroring Compose `WheelPicker.kt`.
|
|
12
12
|
///
|
|
13
|
-
///
|
|
14
|
-
///
|
|
15
|
-
/// `onChange(name, value)`.
|
|
13
|
+
/// Five rows tall with the selection in the middle row. A drag always settles on a row
|
|
14
|
+
/// (Compose snaps once the scroll stops) and then reports it via `onChange(name, value)`.
|
|
16
15
|
public struct WheelPicker: View {
|
|
17
16
|
@Environment(\.appTheme) private var theme
|
|
18
17
|
|
|
@@ -28,68 +27,50 @@ public struct WheelPicker: View {
|
|
|
28
27
|
self.data = data
|
|
29
28
|
self.selectedData = selectedData
|
|
30
29
|
self.onChange = onChange
|
|
30
|
+
_restIndex = State(initialValue: Self.index(of: selectedData, in: data))
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
// MARK: Public
|
|
34
34
|
|
|
35
35
|
public var body: some View {
|
|
36
|
-
let
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
.id(index)
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
.background(
|
|
56
|
-
GeometryReader { inner in
|
|
57
|
-
Color.clear.preference(
|
|
58
|
-
key: WheelOffsetKey.self,
|
|
59
|
-
value: inner.frame(in: .named(scrollSpace)).minY
|
|
60
|
-
)
|
|
61
|
-
}
|
|
62
|
-
)
|
|
63
|
-
}
|
|
64
|
-
.coordinateSpace(name: scrollSpace)
|
|
65
|
-
.onPreferenceChange(WheelOffsetKey.self) { value in
|
|
66
|
-
scrollOffset = value
|
|
67
|
-
updateSelection(paddedData: paddedData, viewportHeight: height)
|
|
68
|
-
}
|
|
69
|
-
.onAppear {
|
|
70
|
-
let initial = max(paddedData.firstIndex(of: selectedData) ?? 2, 2)
|
|
71
|
-
selectedIndex = initial
|
|
72
|
-
DispatchQueue.main.async {
|
|
73
|
-
proxy.scrollTo(initial, anchor: .center)
|
|
36
|
+
let position = self.position
|
|
37
|
+
let centerSlot = CGFloat(wheelVisibleCount / 2)
|
|
38
|
+
|
|
39
|
+
return Rectangle()
|
|
40
|
+
.fill(theme.colors.background.surface)
|
|
41
|
+
.overlay(
|
|
42
|
+
// Compose draws the selection band behind the rows.
|
|
43
|
+
Rectangle()
|
|
44
|
+
.fill(Colors.blue10)
|
|
45
|
+
.opacity(0.2)
|
|
46
|
+
.frame(height: wheelItemHeight)
|
|
47
|
+
)
|
|
48
|
+
.overlay(
|
|
49
|
+
VStack(spacing: 0) {
|
|
50
|
+
ForEach(Array(data.enumerated()), id: \.offset) { index, text in
|
|
51
|
+
WheelPickerItem(text: text, distance: abs(CGFloat(index) - position))
|
|
74
52
|
}
|
|
75
53
|
}
|
|
54
|
+
.offset(y: (centerSlot - position) * wheelItemHeight),
|
|
55
|
+
alignment: .top
|
|
56
|
+
)
|
|
57
|
+
.frame(maxWidth: .infinity)
|
|
58
|
+
.frame(height: wheelItemHeight * CGFloat(wheelVisibleCount))
|
|
59
|
+
.clipShape(RoundedRectangle(cornerRadius: Radius.S))
|
|
60
|
+
.overlay(
|
|
61
|
+
RoundedRectangle(cornerRadius: Radius.S)
|
|
62
|
+
.strokeBorder(theme.colors.border.default, lineWidth: wheelPickerBorderWidth)
|
|
63
|
+
)
|
|
64
|
+
.contentShape(Rectangle())
|
|
65
|
+
.gesture(dragGesture)
|
|
66
|
+
.onAppear { reportIfUnlisted() }
|
|
67
|
+
.onChange(of: selectedData) { _ in syncToSelection() }
|
|
68
|
+
.onChange(of: data) { _ in syncToSelection() }
|
|
69
|
+
.accessibilityElement()
|
|
70
|
+
.accessibilityValue(data.indices.contains(restIndex) ? data[restIndex] : "")
|
|
71
|
+
.accessibilityAdjustableAction { direction in
|
|
72
|
+
settle(at: restIndex + (direction == .increment ? 1 : -1))
|
|
76
73
|
}
|
|
77
|
-
}
|
|
78
|
-
.frame(height: wheelItemHeight * CGFloat(wheelVisibleCount))
|
|
79
|
-
.overlay(
|
|
80
|
-
RoundedRectangle(cornerRadius: Radius.S)
|
|
81
|
-
.stroke(theme.colors.border.default, lineWidth: wheelPickerBorderWidth)
|
|
82
|
-
)
|
|
83
|
-
.background(theme.colors.background.surface.clipShape(RoundedRectangle(cornerRadius: Radius.S)))
|
|
84
|
-
.overlay(
|
|
85
|
-
// Centered translucent selection band.
|
|
86
|
-
Rectangle()
|
|
87
|
-
.fill(Colors.blue10)
|
|
88
|
-
.opacity(0.2)
|
|
89
|
-
.frame(height: wheelItemHeight)
|
|
90
|
-
.allowsHitTesting(false)
|
|
91
|
-
)
|
|
92
|
-
.clipShape(RoundedRectangle(cornerRadius: Radius.S))
|
|
93
74
|
}
|
|
94
75
|
|
|
95
76
|
// MARK: Internal
|
|
@@ -99,24 +80,60 @@ public struct WheelPicker: View {
|
|
|
99
80
|
let selectedData: String
|
|
100
81
|
let onChange: (String, String) -> Void
|
|
101
82
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
private
|
|
106
|
-
|
|
107
|
-
///
|
|
108
|
-
private
|
|
109
|
-
|
|
110
|
-
let
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
83
|
+
// MARK: Private
|
|
84
|
+
|
|
85
|
+
@State private var restIndex: Int
|
|
86
|
+
@State private var dragTranslation: CGFloat = 0
|
|
87
|
+
|
|
88
|
+
/// Fractional row index under the band; past either end the drag meets resistance.
|
|
89
|
+
private var position: CGFloat {
|
|
90
|
+
let raw = CGFloat(restIndex) - dragTranslation / wheelItemHeight
|
|
91
|
+
let last = CGFloat(max(0, data.count - 1))
|
|
92
|
+
if raw < 0 { return raw * 0.3 }
|
|
93
|
+
if raw > last { return last + (raw - last) * 0.3 }
|
|
94
|
+
return raw
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private var dragGesture: some Gesture {
|
|
98
|
+
DragGesture(minimumDistance: 4)
|
|
99
|
+
.onChanged { value in
|
|
100
|
+
dragTranslation = value.translation.height
|
|
101
|
+
}
|
|
102
|
+
.onEnded { value in
|
|
103
|
+
let predicted = CGFloat(restIndex) - value.predictedEndTranslation.height / wheelItemHeight
|
|
104
|
+
settle(at: Int(predicted.rounded()))
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// Like Compose `paddedData.indexOf(selectedData).coerceAtLeast(2)`: an unknown value shows the first row.
|
|
109
|
+
private static func index(of value: String, in data: [String]) -> Int {
|
|
110
|
+
data.firstIndex(of: value) ?? 0
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private func settle(at index: Int) {
|
|
114
|
+
let target = min(max(index, 0), max(0, data.count - 1))
|
|
115
|
+
let rows = Double(abs(CGFloat(target) - position))
|
|
116
|
+
withAnimation(.easeOut(duration: min(0.5, 0.2 + 0.03 * rows))) {
|
|
117
|
+
restIndex = target
|
|
118
|
+
dragTranslation = 0
|
|
119
119
|
}
|
|
120
|
+
guard data.indices.contains(target), data[target] != selectedData else { return }
|
|
121
|
+
onChange(name, data[target])
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// The parent may coerce the value (e.g. day 31 → 28) or swap the list; jump without animation
|
|
125
|
+
/// like Compose `scrollToItem`.
|
|
126
|
+
private func syncToSelection() {
|
|
127
|
+
restIndex = Self.index(of: selectedData, in: data)
|
|
128
|
+
dragTranslation = 0
|
|
129
|
+
reportIfUnlisted()
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/// Compose reports the row the wheel lands on, so a value missing from `data` (minute 07 with a
|
|
133
|
+
/// 5-minute interval, an unset AM/PM) takes the shown row's value instead of drifting from it.
|
|
134
|
+
private func reportIfUnlisted() {
|
|
135
|
+
guard data.indices.contains(restIndex), !data.contains(selectedData) else { return }
|
|
136
|
+
onChange(name, data[restIndex])
|
|
120
137
|
}
|
|
121
138
|
}
|
|
122
139
|
|
|
@@ -126,40 +143,21 @@ private struct WheelPickerItem: View {
|
|
|
126
143
|
@Environment(\.appTheme) private var theme
|
|
127
144
|
|
|
128
145
|
let text: String
|
|
129
|
-
|
|
130
|
-
let
|
|
131
|
-
let scrollOffset: CGFloat
|
|
132
|
-
let viewportHeight: CGFloat
|
|
146
|
+
/// Rows between this one and the band.
|
|
147
|
+
let distance: CGFloat
|
|
133
148
|
|
|
134
149
|
var body: some View {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
switch distanceFromCenter {
|
|
141
|
-
case ..<1: alpha = 1.0
|
|
142
|
-
case ..<2: alpha = 0.8
|
|
143
|
-
default: alpha = 0.4
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
let scale: CGFloat = isSelected ? 1.0 : 0.87
|
|
147
|
-
|
|
148
|
-
return ZStack {
|
|
149
|
-
MomoText(text, typography: .actionSBold, color: theme.colors.text.default)
|
|
150
|
-
.scaleEffect(scale)
|
|
151
|
-
}
|
|
152
|
-
.frame(maxWidth: .infinity)
|
|
153
|
-
.frame(height: wheelItemHeight)
|
|
154
|
-
.opacity(alpha)
|
|
150
|
+
MomoText(text, typography: .actionSBold, color: theme.colors.text.default)
|
|
151
|
+
.scaleEffect(1 - 0.13 * min(distance, 1))
|
|
152
|
+
.frame(maxWidth: .infinity)
|
|
153
|
+
.frame(height: wheelItemHeight)
|
|
154
|
+
.opacity(Self.opacity(distance))
|
|
155
155
|
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// MARK: - WheelOffsetKey
|
|
159
156
|
|
|
160
|
-
|
|
161
|
-
static
|
|
162
|
-
|
|
163
|
-
|
|
157
|
+
// RN parity: 1 on the band, 0.8 one row away, 0.4 from two rows on.
|
|
158
|
+
private static func opacity(_ distance: CGFloat) -> Double {
|
|
159
|
+
if distance <= 1 { return Double(1 - 0.2 * distance) }
|
|
160
|
+
if distance <= 2 { return Double(0.8 - 0.4 * (distance - 1)) }
|
|
161
|
+
return 0.4
|
|
164
162
|
}
|
|
165
163
|
}
|
|
@@ -1,16 +1,49 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
import SwiftUI
|
|
3
3
|
|
|
4
|
+
public enum DividerType {
|
|
5
|
+
case `default`
|
|
6
|
+
case dash
|
|
7
|
+
}
|
|
8
|
+
|
|
4
9
|
public struct MomoDivider: View {
|
|
5
10
|
@Environment(\.appTheme) private var theme
|
|
6
11
|
|
|
7
|
-
|
|
12
|
+
private let useMargin: Bool
|
|
13
|
+
private let type: DividerType
|
|
14
|
+
|
|
15
|
+
public init(useMargin: Bool = false, type: DividerType = .default) {
|
|
16
|
+
self.useMargin = useMargin
|
|
17
|
+
self.type = type
|
|
18
|
+
}
|
|
8
19
|
|
|
9
20
|
public var body: some View {
|
|
10
|
-
|
|
11
|
-
.fill(theme.colors.border.default)
|
|
21
|
+
line
|
|
12
22
|
.frame(maxWidth: .infinity)
|
|
13
23
|
.frame(height: 1)
|
|
14
|
-
.padding(.vertical, Spacing.XS)
|
|
24
|
+
.padding(.vertical, useMargin ? Spacing.XS : 0)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@ViewBuilder
|
|
28
|
+
private var line: some View {
|
|
29
|
+
switch type {
|
|
30
|
+
case .default:
|
|
31
|
+
Rectangle().fill(theme.colors.border.default)
|
|
32
|
+
case .dash:
|
|
33
|
+
DividerDashLine()
|
|
34
|
+
.stroke(
|
|
35
|
+
theme.colors.border.default,
|
|
36
|
+
style: StrokeStyle(lineWidth: 1, lineCap: .round, dash: [4, 4])
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private struct DividerDashLine: Shape {
|
|
43
|
+
func path(in rect: CGRect) -> Path {
|
|
44
|
+
var path = Path()
|
|
45
|
+
path.move(to: CGPoint(x: rect.minX, y: rect.midY))
|
|
46
|
+
path.addLine(to: CGPoint(x: rect.maxX, y: rect.midY))
|
|
47
|
+
return path
|
|
15
48
|
}
|
|
16
49
|
}
|
|
@@ -43,15 +43,44 @@ public extension Color {
|
|
|
43
43
|
)
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/// CSS-style `RGB`, `RGBA`, `RRGGBB` or `RRGGBBAA`, `#` optional; anything else is black.
|
|
46
47
|
init(hex: String) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
48
|
+
var digits = hex.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
49
|
+
if digits.hasPrefix("#") { digits.removeFirst() }
|
|
50
|
+
if digits.count == 3 || digits.count == 4 {
|
|
51
|
+
digits = String(digits.flatMap { [$0, $0] })
|
|
52
|
+
}
|
|
53
|
+
guard digits.count == 6 || digits.count == 8,
|
|
54
|
+
digits.allSatisfy(\.isHexDigit),
|
|
55
|
+
let value = UInt64(digits, radix: 16) else {
|
|
56
|
+
self.init(red: 0, green: 0, blue: 0)
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
let rgba = digits.count == 8 ? value : value << 8 | 0xFF
|
|
60
|
+
self.init(
|
|
61
|
+
.sRGB,
|
|
62
|
+
red: Double((rgba >> 24) & 0xFF) / 255,
|
|
63
|
+
green: Double((rgba >> 16) & 0xFF) / 255,
|
|
64
|
+
blue: Double((rgba >> 8) & 0xFF) / 255,
|
|
65
|
+
opacity: Double(rgba & 0xFF) / 255
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
extension Color {
|
|
71
|
+
/// Compose's `lerp(start, stop, fraction)`, in sRGB; `Color.mix` needs iOS 18.
|
|
72
|
+
func lerp(to other: Color, _ fraction: CGFloat) -> Color {
|
|
73
|
+
let t = min(max(fraction, 0), 1)
|
|
74
|
+
var (r1, g1, b1, a1): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0)
|
|
75
|
+
var (r2, g2, b2, a2): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0)
|
|
76
|
+
UIColor(self).getRed(&r1, green: &g1, blue: &b1, alpha: &a1)
|
|
77
|
+
UIColor(other).getRed(&r2, green: &g2, blue: &b2, alpha: &a2)
|
|
78
|
+
return Color(
|
|
79
|
+
.sRGB,
|
|
80
|
+
red: r1 + (r2 - r1) * t,
|
|
81
|
+
green: g1 + (g2 - g1) * t,
|
|
82
|
+
blue: b1 + (b2 - b1) * t,
|
|
83
|
+
opacity: a1 + (a2 - a1) * t
|
|
84
|
+
)
|
|
56
85
|
}
|
|
57
86
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
/// `@EnvironmentObject` that reads nil instead of trapping when nothing was injected.
|
|
4
|
+
/// `_store` is the backing field of the `@frozen` `EnvironmentObject`, so its layout is ABI-stable.
|
|
5
|
+
@propertyWrapper
|
|
6
|
+
struct OptionalEnvironmentObject<Object: ObservableObject>: DynamicProperty {
|
|
7
|
+
@EnvironmentObject private var object: Object
|
|
8
|
+
|
|
9
|
+
var wrappedValue: Object? {
|
|
10
|
+
Mirror(reflecting: _object).descendant("_store") as? Object
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// File: ios/Extensions/SkeletonUICompat.swift
|
|
2
|
+
// Created At: 2026-10-07 12:03:28 +07:00
|
|
3
|
+
// Created By: AI
|
|
4
|
+
// AI Agent: Claude Code
|
|
5
|
+
// Model: claude-opus-5-5
|
|
6
|
+
|
|
7
|
+
// AI-GENERATED START: keep SkeletonUI extensions visible to MoMoUIKits clients
|
|
8
|
+
// Unused here on purpose: Swift 5 exposes extension members of every module a dependency imports, so clients
|
|
9
|
+
// that only `import MoMoUIKits` rely on SkeletonUI's (e.g. `Text(String?)` in NativeWidget). Removing it breaks them.
|
|
10
|
+
import SkeletonUI
|
|
11
|
+
// AI-GENERATED END: keep SkeletonUI extensions visible to MoMoUIKits clients
|