@momo-kits/native-kits 0.167.1-duo.3 → 0.167.1-duo.4-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.
Files changed (187) hide show
  1. package/CLAUDE.md +17 -8
  2. package/build.gradle.kts +11 -0
  3. package/compose/build.gradle.kts +190 -0
  4. package/compose/build.gradle.kts.backup +190 -0
  5. package/compose/compose.podspec +47 -0
  6. package/compose/src/androidMain/kotlin/vn/momo/kits/const/HostFont.android.kt +19 -0
  7. package/compose/src/androidMain/kotlin/vn/momo/kits/navigation/ScrollToTop.android.kt +6 -0
  8. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/BackGesture.android.kt +12 -0
  9. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/OsFontScale.android.kt +7 -0
  10. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +151 -0
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +52 -0
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +124 -0
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +183 -0
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +207 -0
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +39 -0
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +86 -0
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +76 -0
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +76 -0
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +293 -0
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +33 -0
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +831 -0
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Localize.kt +269 -0
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +110 -0
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +378 -0
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +56 -0
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +69 -0
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Avatar.kt +157 -0
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +82 -0
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeDot.kt +32 -0
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeRibbon.kt +338 -0
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BaselineView.kt +175 -0
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +356 -0
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +113 -0
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +89 -0
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +128 -0
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Collapse.kt +209 -0
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +542 -0
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +49 -0
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +76 -0
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +125 -0
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +231 -0
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +189 -0
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +475 -0
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +114 -0
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +266 -0
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +240 -0
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +223 -0
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +264 -0
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +185 -0
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +343 -0
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Loader.kt +108 -0
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +56 -0
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationNumber.kt +41 -0
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +84 -0
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationWhiteDot.kt +40 -0
  56. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +334 -0
  57. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +98 -0
  58. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +310 -0
  59. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +59 -0
  60. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Rating.kt +87 -0
  61. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +21 -0
  62. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +90 -0
  63. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +323 -0
  64. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Stepper.kt +220 -0
  65. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Steps.kt +473 -0
  66. package/compose/src/commonMain/kotlin/vn/momo/kits/components/SuggestAction.kt +122 -0
  67. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +199 -0
  68. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +91 -0
  69. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +552 -0
  70. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +88 -0
  71. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +125 -0
  72. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +207 -0
  73. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +498 -0
  74. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +176 -0
  75. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Uploader.kt +182 -0
  76. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +193 -0
  77. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt +29 -0
  78. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +234 -0
  79. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +188 -0
  80. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Colors.kt +325 -0
  81. package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +16 -0
  82. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitDesignSystem.kt +18 -0
  83. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +22 -0
  84. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Radius.kt +12 -0
  85. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +16 -0
  86. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +188 -0
  87. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +297 -0
  88. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +54 -0
  89. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +50 -0
  90. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +157 -0
  91. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +41 -0
  92. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +52 -0
  93. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +66 -0
  94. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Conditional.kt +11 -0
  95. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/DeprecatedModifier.kt +104 -0
  96. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +48 -0
  97. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Size.kt +51 -0
  98. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +299 -0
  99. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/HostBackGestureBridge.kt +58 -0
  100. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +129 -0
  101. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +106 -0
  102. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +205 -0
  103. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +393 -0
  104. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScrollToTop.kt +8 -0
  105. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +622 -0
  106. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackTransition.kt +148 -0
  107. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +35 -0
  108. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +175 -0
  109. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +232 -0
  110. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +86 -0
  111. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +169 -0
  112. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +381 -0
  113. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +83 -0
  114. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +105 -0
  115. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +74 -0
  116. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +347 -0
  117. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +37 -0
  118. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +363 -0
  119. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +169 -0
  120. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +132 -0
  121. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +127 -0
  122. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/BackGesture.kt +12 -0
  123. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +91 -0
  124. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/OsFontScale.kt +9 -0
  125. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +71 -0
  126. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Icons.kt +1329 -0
  127. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +55 -0
  128. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Tracking.kt +15 -0
  129. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +118 -0
  130. package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +40 -0
  131. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/HostBackGesture.ios.kt +45 -0
  132. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +32 -0
  133. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/BackGesture.ios.kt +38 -0
  134. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +66 -0
  135. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +213 -0
  136. package/gradle/libs.versions.toml +69 -0
  137. package/gradle/wrapper/gradle-wrapper.jar +0 -0
  138. package/gradle/wrapper/gradle-wrapper.properties +8 -0
  139. package/gradle.properties +26 -0
  140. package/gradlew +252 -0
  141. package/gradlew.bat +94 -0
  142. package/ios/Application/ApplicationEnvironment.swift +6 -0
  143. package/ios/Application/Components.swift +1 -3
  144. package/ios/Application/HeaderRight.swift +19 -27
  145. package/ios/Application/Navigation/BottomTab/BottomTab.swift +9 -30
  146. package/ios/Application/Navigation/BottomTab/BottomTabBar.swift +2 -5
  147. package/ios/Application/Navigation/HeaderUser.swift +1 -7
  148. package/ios/Application/Navigation/NavigationContainer.swift +45 -45
  149. package/ios/Application/Navigation/Navigator.swift +122 -29
  150. package/ios/Application/Navigation/NavigatorManager.swift +13 -6
  151. package/ios/Application/Navigation/Overplay/BottomSheet.swift +4 -41
  152. package/ios/Application/Navigation/Overplay/ModalScreen.swift +1 -5
  153. package/ios/Application/Navigation/component/Header.swift +4 -12
  154. package/ios/Application/Navigation/component/HeaderBackground.swift +2 -3
  155. package/ios/Application/Navigation/component/NavigationFooter.swift +1 -1
  156. package/ios/Application/Navigation/component/NavigationHeaderRight.swift +9 -61
  157. package/ios/Application/StackScreen.swift +18 -135
  158. package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +59 -5
  159. package/ios/Colors+Radius+Spacing/Colors.swift +1 -1
  160. package/ios/Extensions/Color++.swift +20 -9
  161. package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
  162. package/ios/Icon/Icon.swift +14 -22
  163. package/ios/Icon/Icons.swift +1329 -0
  164. package/ios/Image/Image.swift +16 -5
  165. package/ios/Lottie/LottieView.swift +4 -2
  166. package/ios/Popup/PopupDisplay.swift +68 -84
  167. package/ios/Popup/PopupNotify.swift +32 -43
  168. package/ios/Popup/PopupPromotion.swift +6 -18
  169. package/ios/Resources/dot_loading.json +558 -0
  170. package/ios/Resources/lottie_circle_loader.json +1 -0
  171. package/ios/Template/TrustBanner/TrustBanner.swift +69 -31
  172. package/ios/native-kits.podspec +1 -1
  173. package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
  174. package/ios-demo/README.md +120 -0
  175. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +0 -1
  176. package/package.json +1 -1
  177. package/scripts/gen-ios-icons.mjs +36 -0
  178. package/settings.gradle.kts +64 -0
  179. package/.claude/scheduled_tasks.lock +0 -1
  180. package/.claude/settings.local.json +0 -185
  181. package/ios/Application/Layout/ScreenLayout.swift +0 -196
  182. package/ios/Application/Navigation/BottomTab/DuoTabBar.swift +0 -275
  183. package/ios/Application/Navigation/component/DuoScrollGradient.swift +0 -55
  184. package/ios/Application/Navigation/component/SideBar.swift +0 -155
  185. package/ios/Popup/PopupLayout.swift +0 -45
  186. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +0 -76
  187. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +0 -170
@@ -26,8 +26,6 @@ public struct NavigationHeaderRight: View {
26
26
  struct ToolkitNavigationHeaderRight: View {
27
27
  var headerRight: HeaderRightData?
28
28
  var tintColor: Color?
29
- /// iPhone Duo side bar column instead of the bar row.
30
- var vertical: Bool = false
31
29
  @State private var isFavorite: Bool = false
32
30
  @State private var isLoading: Bool = false
33
31
  @Environment(\.applicationEnvironment) private var environment
@@ -144,68 +142,15 @@ struct ToolkitNavigationHeaderRight: View {
144
142
  return NavigationButtonConfig(icon: icon, onPress: onClickHandler)
145
143
  }
146
144
 
147
- private var showHelpCenter: Bool {
148
- environment.applicationContext != nil && showsHelpCenter(environment.applicationContext)
149
- }
150
-
151
- private var closeIcon: String { headerRight?.closeIcon ?? "16_basic_home" }
152
-
153
145
  var body: some View {
154
- Group {
155
- if vertical {
156
- verticalBody
157
- } else {
158
- horizontalBody
159
- }
160
- }
161
- .onAppear {
162
- loadFavoriteState()
163
- }
164
- }
165
-
166
- private var showBadge: Bool {
167
- headerRight?.tools.contains { group in
168
- group.items.contains { $0.showBadge }
169
- } ?? false
170
- }
171
-
172
- private var verticalBody: some View {
173
- let navButtonConfig = getNavigationButtonConfig()
174
- return VStack(spacing: Spacing.M) {
175
- if shouldShowShortcut {
176
- SideBarButton(
177
- icon: navButtonConfig.icon,
178
- tintColor: tintColor,
179
- showBadge: showBadge,
180
- disabled: isLoading,
181
- action: navButtonConfig.onPress
182
- )
183
- }
184
- SideBarGroup(tintColor: tintColor, itemCount: showHelpCenter ? 2 : 1) {
185
- if showHelpCenter {
186
- SideBarGroupItem(
187
- icon: "help_center",
188
- tintColor: tintColor,
189
- accessibilityId: "btn_navigation_help_center",
190
- action: onPressHelpCenter
191
- )
192
- }
193
- SideBarGroupItem(
194
- icon: closeIcon,
195
- tintColor: tintColor,
196
- accessibilityId: "btn_navigation_close",
197
- action: onPressClose
198
- )
199
- }
200
- }
201
- }
202
-
203
- private var horizontalBody: some View {
204
146
  let backgroundButtonColor: Color = tintColor == Colors.black01 ? Colors.black20.opacity(0.6) : Colors.black01.opacity(0.6)
205
147
  let borderColor: Color = tintColor == Colors.black01 ? Colors.black01.opacity(0.2) : Colors.black20.opacity(0.2)
206
148
  let navButtonConfig = getNavigationButtonConfig()
149
+ let showBadge = headerRight?.tools.contains { group in
150
+ group.items.contains { $0.showBadge }
151
+ } ?? false
207
152
 
208
- return HStack(alignment: .center, spacing: 0) {
153
+ HStack(alignment: .center, spacing: 0) {
209
154
  if shouldShowShortcut {
210
155
  NavigationButton(
211
156
  disabled: isLoading,
@@ -217,7 +162,7 @@ struct ToolkitNavigationHeaderRight: View {
217
162
  }
218
163
 
219
164
  HStack(alignment: .center, spacing: 0) {
220
- if showHelpCenter {
165
+ if environment.applicationContext != nil {
221
166
  Icon(source: "help_center", size: 20, color: tintColor ?? Colors.black17)
222
167
  .padding(4)
223
168
  .onTapGesture {
@@ -229,7 +174,7 @@ struct ToolkitNavigationHeaderRight: View {
229
174
  .frame(width: 0.5, height: 12)
230
175
  }
231
176
 
232
- Icon(source: closeIcon, size: 20, color: tintColor ?? Colors.black17)
177
+ Icon(source: "16_basic_home", size: 20, color: tintColor ?? Colors.black17)
233
178
  .padding(4)
234
179
  .onTapGesture {
235
180
  onPressClose()
@@ -244,5 +189,8 @@ struct ToolkitNavigationHeaderRight: View {
244
189
  )
245
190
  .padding(.leading, Spacing.S)
246
191
  }
192
+ .onAppear {
193
+ loadFavoriteState()
194
+ }
247
195
  }
248
196
  }
@@ -104,9 +104,6 @@ public struct StackScreen<Content: View>: View {
104
104
  @State private var isBottomTabRoot: Bool = false
105
105
 
106
106
  @Environment(\.applicationEnvironment) private var appEnv
107
- @Environment(\.screenLayout) private var layout
108
- @Environment(\.duoTabBarInsets) private var duoTabBarInsets
109
- @State private var footerHeight: CGFloat = 0
110
107
 
111
108
  // Auto-tracking state (mirrors Compose's `remember { ScreenTrackingState() }` +
112
109
  // the once-computed push/back `action`).
@@ -128,30 +125,6 @@ public struct StackScreen<Content: View>: View {
128
125
 
129
126
  private static var headerHeight: CGFloat { 52 }
130
127
 
131
- private var toolkit: (data: HeaderRightData?, tintColor: Color?)? { toolkitHeaderRight(headerRight) }
132
-
133
- private var showsSideBar: Bool {
134
- layout.sideBar != nil && headerType != .none && !isBottomTabRoot && (goBack != nil || toolkit != nil)
135
- }
136
-
137
- /// iPhone Duo: content scrolls under a glass header instead of starting below it.
138
- private var useDuoGlass: Bool {
139
- layout.sideBar != nil && headerType != .none && animatedHeader == nil
140
- && inputSearchProps == nil && !headerTransparent && !fullScreenContent
141
- }
142
-
143
- /// Height the footer, tab bar or home indicator takes from the bottom of the window.
144
- private var bottomBarsHeight: CGFloat {
145
- if footer != nil { return footerHeight + NavigationFooter.safeAreaBottom }
146
- if duoTabBarInsets.sideBarReserve > 0 { return duoTabBarInsets.sideBarReserve - Spacing.XL }
147
- return NavigationFooter.navigationBarInset
148
- }
149
-
150
- /// Keyboard frames are in screen coordinates; UIScreen stays on the outer display on iPhone Duo.
151
- private var keyboardReferenceHeight: CGFloat {
152
- layout.isIPhoneDuo ? layout.height : UIScreen.main.bounds.height
153
- }
154
-
155
128
  /// Extra top space reserved inside the content so the animated search bar has
156
129
  /// room below the header at scroll 0; it shrinks to 0 as the bar slides up.
157
130
  private var animatedSearchInset: CGFloat {
@@ -190,18 +163,9 @@ public struct StackScreen<Content: View>: View {
190
163
  hasFooter: footer != nil,
191
164
  scrollOffset: scrollOffset
192
165
  )
193
- .padding(.bottom, footer == nil ? duoTabBarInsets.overlay : 0)
194
166
  }
195
167
 
196
168
  NavigationFooter(footerComponent: footer, keyboardSize: keyboardSize)
197
- .padding(.bottom, duoTabBarInsets.overlay)
198
- .background(
199
- GeometryReader { proxy in
200
- Color.clear
201
- .onAppear { footerHeight = proxy.size.height }
202
- .onChange(of: proxy.size.height) { footerHeight = $0 }
203
- }
204
- )
205
169
  }
206
170
 
207
171
  // Animated search bar overlay (mirrors Compose's SearchAnimated layer).
@@ -212,24 +176,7 @@ public struct StackScreen<Content: View>: View {
212
176
  hiddenBack: !isBack,
213
177
  hasHeaderRight: headerRight != nil,
214
178
  headerRightWidth: headerRightWidth,
215
- headerHeight: Self.headerHeight,
216
- useSideBar: layout.sideBar != nil,
217
- hasToolkit: toolkit != nil
218
- )
219
- }
220
-
221
- if showsSideBar, let sideBar = layout.sideBar {
222
- HeaderSideBar(
223
- sideBar: sideBar,
224
- goBack: goBack,
225
- toolkit: toolkit,
226
- headerColor: getHeaderColor(
227
- isAnimated: animatedHeader != nil,
228
- isSurface: animatedHeader?.isSurface ?? true,
229
- opacity: scrollOpacity,
230
- tintColor: tintColor
231
- ),
232
- bottomInset: bottomBarsHeight
179
+ headerHeight: Self.headerHeight
233
180
  )
234
181
  }
235
182
  }
@@ -244,7 +191,7 @@ public struct StackScreen<Content: View>: View {
244
191
  guard useAvoidKeyboard,
245
192
  let endFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
246
193
  withAnimation(.easeOut(duration: 0.25)) {
247
- keyboardHeight = keyboardReferenceHeight - endFrame.origin.y
194
+ keyboardHeight = UIScreen.main.bounds.height - endFrame.origin.y
248
195
  }
249
196
  }
250
197
  // Mirrors Compose's `hideKeyboardOnTap()`: a simultaneous gesture so
@@ -299,25 +246,15 @@ public struct StackScreen<Content: View>: View {
299
246
  scrollOffset: $scrollOffset,
300
247
  scrollToTopCallback: scrollToTopCallback,
301
248
  animatedSearchInset: animatedSearchInset,
302
- bottomInset: duoTabBarInsets.overlay,
303
249
  content: { animatedHeroBody(animatedHeader) }
304
250
  )
305
251
  .edgesIgnoringSafeArea(.all)
306
252
 
307
253
  header
308
254
  .background(
309
- Group {
310
- if layout.sideBar != nil {
311
- // glass shows once the banner has scrolled under the header
312
- Color.clear.modifier(DuoBarMaterial(
313
- shape: Rectangle(),
314
- visible: scrollOffset > heroGlassStart(animatedHeader)
315
- ))
316
- } else {
317
- Colors.black01.opacity(scrollOpacity)
318
- }
319
- }
320
- .edgesIgnoringSafeArea([.top, .horizontal])
255
+ Colors.black01
256
+ .opacity(scrollOpacity)
257
+ .edgesIgnoringSafeArea(.top)
321
258
  )
322
259
  .zIndex(100)
323
260
  }
@@ -334,34 +271,9 @@ public struct StackScreen<Content: View>: View {
334
271
  )
335
272
  .zIndex(100)
336
273
 
337
- StackScreenContent(scrollable: scrollable, scrollOffset: $scrollOffset, scrollToTopCallback: scrollToTopCallback, animatedSearchInset: animatedSearchInset, bottomInset: duoTabBarInsets.overlay, content: { duoInsetContent })
274
+ StackScreenContent(scrollable: scrollable, scrollOffset: $scrollOffset, scrollToTopCallback: scrollToTopCallback, animatedSearchInset: animatedSearchInset, content: content)
338
275
  .edgesIgnoringSafeArea(.all)
339
276
  }
340
- } else if useDuoGlass {
341
- ZStack(alignment: .top) {
342
- DuoScrollGradient(
343
- scrollOffset: scrollOffset,
344
- headerHeight: layout.headerHeight,
345
- extended: headerType == .extended
346
- )
347
-
348
- StackScreenContent(
349
- scrollable: scrollable,
350
- scrollOffset: $scrollOffset,
351
- scrollToTopCallback: scrollToTopCallback,
352
- topInset: layout.headerHeight - layout.insets.top,
353
- bottomInset: duoTabBarInsets.overlay,
354
- content: content
355
- )
356
-
357
- header
358
- .background(
359
- Color.clear
360
- .modifier(DuoBarMaterial(shape: Rectangle(), visible: scrollOffset > 0))
361
- .edgesIgnoringSafeArea([.top, .horizontal])
362
- )
363
- .zIndex(100)
364
- }
365
277
  } else {
366
278
  header
367
279
  .background(
@@ -371,40 +283,28 @@ public struct StackScreen<Content: View>: View {
371
283
  headerTransparent: headerTransparent,
372
284
  fullScreenContent: fullScreenContent
373
285
  )
374
- .edgesIgnoringSafeArea([.top, .horizontal])
286
+ .edgesIgnoringSafeArea(.top)
375
287
  )
376
288
 
377
- StackScreenContent(scrollable: scrollable, scrollOffset: $scrollOffset, scrollToTopCallback: scrollToTopCallback, animatedSearchInset: animatedSearchInset, bottomInset: duoTabBarInsets.overlay, content: content)
289
+ StackScreenContent(scrollable: scrollable, scrollOffset: $scrollOffset, scrollToTopCallback: scrollToTopCallback, animatedSearchInset: animatedSearchInset, content: content)
378
290
  }
379
291
  }
380
292
 
381
- /// Edge-to-edge branches lay out outside the safe area; iPhone Duo content still keeps
382
- /// clear of the side strips, as RN's Screen does.
383
- private var duoInsetContent: some View {
384
- content()
385
- .padding(.leading, layout.insetLeft)
386
- .padding(.trailing, layout.insetRight)
387
- }
388
-
389
- private func heroGlassStart(_ animatedHeader: AnimatedHeader) -> CGFloat {
390
- max(layout.width / animatedHeader.aspectRatio.value - layout.headerHeight, 0)
391
- }
392
-
393
293
  /// Hero image + body for an animated header. The image fills the width at its
394
294
  /// aspect-ratio height; the body is offset below the header band so it overlaps
395
295
  /// the lower part of the image (mirrors Compose's image box + padded content).
396
296
  @ViewBuilder
397
297
  private func animatedHeroBody(_ animatedHeader: AnimatedHeader) -> some View {
398
- let width = layout.width
298
+ let width = UIScreen.main.bounds.width
399
299
  let imageHeight = width / animatedHeader.aspectRatio.value
400
- let contentTopInset = layout.headerHeight + layoutOffset
300
+ let contentTopInset = safeAreaTopInset() + HEADER_HEIGHT + layoutOffset
401
301
 
402
302
  ZStack(alignment: .top) {
403
303
  animatedHeader.composable(scrollOffset)
404
304
  .frame(width: width, height: imageHeight)
405
305
  .clipped()
406
306
 
407
- duoInsetContent
307
+ content()
408
308
  .padding(.top, contentTopInset)
409
309
  }
410
310
  .frame(maxWidth: .infinity, alignment: .top)
@@ -443,10 +343,6 @@ private struct StackScreenContent<Content: View>: View {
443
343
  /// Extra top space reserved for the animated search bar (mirrors Compose's
444
344
  /// MainContent animated top spacer). Zero for non-animated screens.
445
345
  var animatedSearchInset: CGFloat = 0
446
- /// Room for a header the content scrolls under (iPhone Duo glass header).
447
- var topInset: CGFloat = 0
448
- /// Room for a bar that overlays the content (iPhone Duo floating tab bar).
449
- var bottomInset: CGFloat = 0
450
346
  let content: () -> Content
451
347
 
452
348
  private let topAnchorID = "stack_screen_top_anchor"
@@ -473,17 +369,13 @@ private struct StackScreenContent<Content: View>: View {
473
369
  }
474
370
  })
475
371
 
476
- if animatedSearchInset + topInset > 0 {
477
- Spacer().frame(height: animatedSearchInset + topInset)
372
+ if animatedSearchInset > 0 {
373
+ Spacer().frame(height: animatedSearchInset)
478
374
  }
479
375
 
480
376
  VStack {
481
377
  content()
482
378
  }
483
-
484
- if bottomInset > 0 {
485
- Spacer().frame(height: bottomInset)
486
- }
487
379
  }
488
380
  }
489
381
  .coordinateSpace(name: scrollSpace)
@@ -498,8 +390,6 @@ private struct StackScreenContent<Content: View>: View {
498
390
  }
499
391
  } else {
500
392
  content()
501
- .padding(.top, topInset)
502
- .padding(.bottom, bottomInset)
503
393
  }
504
394
  }
505
395
  }
@@ -517,23 +407,16 @@ private struct SearchAnimatedHeader: View {
517
407
  let hasHeaderRight: Bool
518
408
  let headerRightWidth: CGFloat
519
409
  let headerHeight: CGFloat
520
- var useSideBar: Bool = false
521
- var hasToolkit: Bool = false
522
-
523
- @Environment(\.screenLayout) private var layout
524
410
 
525
411
  var body: some View {
526
412
  let scroll = max(scrollOffset, 0)
527
413
 
528
- // Insets are measured from the safe-area top; collapsed, the search is centered in the bar row.
529
- let rowTop = min(layout.headerStatusBarHeight - layout.insets.top, 0)
530
- let minTop = rowTop + (layout.headerBarHeight - headerHeight) / 2
531
- let maxTop = layout.headerHeight - layout.insets.top
414
+ // Insets are measured from the safe-area top (status bar handled by layout).
415
+ let minTop: CGFloat = 0
416
+ let maxTop = headerHeight
532
417
  let minSide = Spacing.M
533
- let maxStart: CGFloat = hiddenBack || useSideBar ? Spacing.M : 48
534
- let maxEnd: CGFloat = useSideBar && hasToolkit
535
- ? Spacing.M
536
- : headerRightWidth + (hasHeaderRight ? Spacing.M * 2 : Spacing.M)
418
+ let maxStart: CGFloat = hiddenBack ? Spacing.M : 48
419
+ let maxEnd = headerRightWidth + (hasHeaderRight ? Spacing.M * 2 : Spacing.M)
537
420
 
538
421
  let top = min(max(maxTop - scroll, minTop), maxTop)
539
422
  let start = min(max(minSide + scroll, minSide), maxStart)
@@ -26,7 +26,10 @@ public struct CalculatorKeyboard: View {
26
26
  if (value.count > 0 ){
27
27
  value.removeLast()
28
28
  }
29
-
29
+ // Keys map 1:1 onto `tempValue` characters, so both trims stay aligned.
30
+ if (tempValue.count > 0 ){
31
+ tempValue.removeLast()
32
+ }
30
33
  }
31
34
 
32
35
  func onPressClear(key: String){
@@ -41,12 +44,63 @@ public struct CalculatorKeyboard: View {
41
44
  if (tempValue.last?.isNumber == false){
42
45
  tempValue.removeLast()
43
46
  }
44
- let expression = NSExpression(format: tempValue)
45
-
46
- value = String(expression.expressionValue(with: nil, context: nil) as! Int)
47
- if (Int(value) ?? 0 < 0){
47
+ // Overflow and division by zero are treated like a negative result: cleared.
48
+ if let result = Self.evaluate(tempValue), result >= 0 {
49
+ value = String(result)
50
+ } else {
48
51
  value = ""
49
52
  }
53
+ // The next keys continue from the shown result, not the old expression.
54
+ tempValue = value
55
+ }
56
+
57
+ /// Integer `+ - * /` with the usual precedence, as NSExpression did, but nil instead of a
58
+ /// crash when a number or result leaves `Int` range.
59
+ static func evaluate(_ expression: String) -> Int? {
60
+ var numbers: [Int] = []
61
+ var operators: [Character] = []
62
+ var digits = ""
63
+ for char in expression {
64
+ if ("0"..."9").contains(char) {
65
+ digits.append(char)
66
+ continue
67
+ }
68
+ guard let number = Int(digits) else { return nil }
69
+ numbers.append(number)
70
+ operators.append(char)
71
+ digits = ""
72
+ }
73
+ guard let last = Int(digits) else { return nil }
74
+ numbers.append(last)
75
+
76
+ var terms = [numbers[0]]
77
+ var signs: [Character] = []
78
+ for (op, number) in zip(operators, numbers.dropFirst()) {
79
+ switch op {
80
+ case "*":
81
+ let (product, overflow) = terms[terms.count - 1].multipliedReportingOverflow(by: number)
82
+ if overflow { return nil }
83
+ terms[terms.count - 1] = product
84
+ case "/":
85
+ guard number != 0 else { return nil }
86
+ terms[terms.count - 1] /= number
87
+ case "+", "-":
88
+ terms.append(number)
89
+ signs.append(op)
90
+ default:
91
+ return nil
92
+ }
93
+ }
94
+
95
+ var result = terms[0]
96
+ for (sign, term) in zip(signs, terms.dropFirst()) {
97
+ let (next, overflow) = sign == "+"
98
+ ? result.addingReportingOverflow(term)
99
+ : result.subtractingReportingOverflow(term)
100
+ if overflow { return nil }
101
+ result = next
102
+ }
103
+ return result
50
104
  }
51
105
 
52
106
  func onPressKey(key:String){
@@ -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: "fc0")
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
 
@@ -43,15 +43,26 @@ 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
- let scanner = Scanner(string: hex)
48
- var rgbValue: UInt64 = 0
49
- scanner.scanHexInt64(&rgbValue)
50
-
51
- let r = (rgbValue & 0xff0000) >> 16
52
- let g = (rgbValue & 0xff00) >> 8
53
- let b = rgbValue & 0xff
54
-
55
- self.init(red: Double(r) / 0xff, green: Double(g) / 0xff, blue: Double(b) / 0xff)
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
+ )
56
67
  }
57
68
  }
@@ -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
+ }
@@ -4,27 +4,16 @@ struct IconSource: Codable {
4
4
  let uri: String
5
5
  }
6
6
 
7
- func loadIconSources() -> [String: IconSource] {
8
- guard let url = Bundle.main.url(forResource: "icon", withExtension: "json") else {
9
- print("Error: icon.json not found")
10
- return [:]
7
+ // The compiled-in map (Icons.swift) wins; a host `icon.json` in the main bundle only adds keys
8
+ // the kit doesn't know, so hosts that shipped one keep their extra icons.
9
+ let iconSources: [String: String] = {
10
+ guard let url = Bundle.main.url(forResource: "icon", withExtension: "json"),
11
+ let data = try? Data(contentsOf: url),
12
+ let hostIcons = try? JSONDecoder().decode([String: IconSource].self, from: data) else {
13
+ return kitIcons
11
14
  }
12
-
13
- do {
14
- let data = try Data(contentsOf: url)
15
- let decoded = try JSONDecoder().decode([String: IconSource].self, from: data)
16
- return decoded
17
- } catch {
18
- print("Error decoding icon.json: \(error)")
19
- return [:]
20
- }
21
- }
22
-
23
- // Load the JSON data into a global variable
24
- let iconSources: [String: IconSource] = loadIconSources()
25
-
26
- // Icons that must never be tinted (mirror Compose `noThemeIcons`)
27
- let noThemeIcons: [String] = ["ic_round_momo_tiny"]
15
+ return kitIcons.merging(hostIcons.mapValues(\.uri)) { kit, _ in kit }
16
+ }()
28
17
 
29
18
  // Compose defaults `color` to the theme text color while `color = null` means "no tint".
30
19
  // Swift can't tell an omitted argument from an explicit `nil`, so the default is this
@@ -53,8 +42,8 @@ public struct Icon: View {
53
42
  }
54
43
 
55
44
  public var body: some View {
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 {
45
+ // Mirror Compose: an https source is used as-is, anything else is a key into the icon map
46
+ if let uri = source.contains("https") ? source : iconSources[source] {
58
47
  let requestedColor = color == iconThemeColor ? theme.colors.text.default : color
59
48
  // Mirror Compose: certain icons (e.g. branded glyphs) are never tinted
60
49
  let resolvedColor = noThemeIcons.contains(source) ? nil : requestedColor
@@ -63,6 +52,9 @@ public struct Icon: View {
63
52
  // Mirrors Compose's auto-generated `contentDescription = "img|$iconUrl"`
64
53
  // when no explicit label is provided.
65
54
  .accessibilityLabel(accessibilityLabel ?? "img|\(uri)")
55
+ } else {
56
+ // Compose keeps the sized box for an unknown key, so layouts don't collapse.
57
+ Color.clear.frame(width: size, height: size)
66
58
  }
67
59
  }
68
60
  }