@momo-kits/native-kits 0.167.1-duo.1-debug → 0.167.1-duo.10
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/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/build.gradle.kts +0 -11
- package/compose/build.gradle.kts +0 -190
- package/compose/build.gradle.kts.backup +0 -190
- package/compose/compose.podspec +0 -47
- package/compose/src/androidMain/kotlin/vn/momo/kits/const/HostFont.android.kt +0 -19
- package/compose/src/androidMain/kotlin/vn/momo/kits/navigation/ScrollToTop.android.kt +0 -6
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/BackGesture.android.kt +0 -12
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/OsFontScale.android.kt +0 -7
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +0 -151
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +0 -52
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +0 -124
- 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/LiteScreen.kt +0 -831
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Localize.kt +0 -269
- 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/WidgetContainer.kt +0 -56
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +0 -69
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Avatar.kt +0 -157
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +0 -82
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeDot.kt +0 -32
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeRibbon.kt +0 -338
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/BaselineView.kt +0 -175
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +0 -356
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +0 -113
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +0 -89
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +0 -128
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Collapse.kt +0 -209
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +0 -542
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +0 -49
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +0 -76
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +0 -125
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +0 -231
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +0 -189
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +0 -475
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +0 -114
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +0 -266
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +0 -240
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +0 -223
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +0 -264
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +0 -185
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +0 -343
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Loader.kt +0 -108
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +0 -56
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationNumber.kt +0 -41
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +0 -84
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationWhiteDot.kt +0 -40
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +0 -334
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +0 -98
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +0 -310
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +0 -59
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Rating.kt +0 -87
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +0 -21
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +0 -90
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +0 -323
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Stepper.kt +0 -220
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Steps.kt +0 -473
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/SuggestAction.kt +0 -122
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +0 -199
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +0 -91
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +0 -552
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +0 -88
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +0 -125
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +0 -207
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +0 -498
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +0 -176
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Uploader.kt +0 -182
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +0 -193
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt +0 -29
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +0 -234
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +0 -188
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Colors.kt +0 -325
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +0 -16
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitDesignSystem.kt +0 -18
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +0 -22
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Radius.kt +0 -12
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +0 -16
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +0 -188
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +0 -297
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +0 -52
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +0 -38
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +0 -39
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +0 -52
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +0 -66
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Conditional.kt +0 -11
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/DeprecatedModifier.kt +0 -104
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +0 -48
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Size.kt +0 -51
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +0 -299
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/HostBackGestureBridge.kt +0 -58
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +0 -129
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +0 -106
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +0 -201
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +0 -393
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScrollToTop.kt +0 -8
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +0 -596
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackTransition.kt +0 -148
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +0 -35
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +0 -175
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +0 -225
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +0 -86
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +0 -169
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +0 -360
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +0 -83
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +0 -106
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +0 -75
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +0 -309
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +0 -37
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +0 -364
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +0 -132
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +0 -127
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/BackGesture.kt +0 -12
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +0 -91
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/OsFontScale.kt +0 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +0 -71
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Icons.kt +0 -1329
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +0 -55
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Tracking.kt +0 -15
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +0 -115
- package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +0 -40
- package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/HostBackGesture.ios.kt +0 -45
- package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +0 -32
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/BackGesture.ios.kt +0 -38
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +0 -66
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +0 -213
- package/gradle/libs.versions.toml +0 -69
- package/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/gradle/wrapper/gradle-wrapper.properties +0 -8
- package/gradle.properties +0 -26
- package/gradlew +0 -252
- package/gradlew.bat +0 -94
- 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/ios-demo/README.md +0 -120
- package/local.properties +0 -8
- package/settings.gradle.kts +0 -64
- /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
package/CLAUDE.md
CHANGED
|
@@ -35,6 +35,13 @@ No unit-test suite is configured. Validate changes by compiling the relevant tar
|
|
|
35
35
|
./gradlew :sample:androidApp:installDebug # run demo on device/emulator
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
The SwiftUI kit (`ios/`, pod `MoMoUIKits`) compiles on its own through the sample's Pods project (`pod install` in `sample/iosApp` first when files are added):
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
xcodebuild -project sample/iosApp/Pods/Pods.xcodeproj -scheme MoMoUIKits -sdk iphonesimulator \
|
|
42
|
+
-destination 'generic/platform=iOS Simulator' build
|
|
43
|
+
```
|
|
44
|
+
|
|
38
45
|
Publishing is automated (`publish.sh` + GitLab CI on `[ci build]` / `main`). Do not publish manually unless asked: `sh publish.sh beta` / `sh publish.sh release` (Maven→GitLab + NPM).
|
|
39
46
|
|
|
40
47
|
## Architecture essentials
|
|
@@ -47,6 +54,31 @@ Publishing is automated (`publish.sh` + GitLab CI on `[ci build]` / `main`). Do
|
|
|
47
54
|
- Navigation uses a runtime `DynamicScreenRegistry`; overlays (modal/bottom-sheet/snackbar) go through `Navigator` + `OverplayComponentRegistry`.
|
|
48
55
|
- **iOS swipe-back:** the host owns the only edge-pan recogniser and replays it through `HostBackGestureBridge` (`navigation/HostBackGestureBridge.kt`; the kit framework is never exported, so the host reaches it through its own Koin-provided facade): `checkCanPop` / `canPopInner` / `pop` route the back into the kit stack, `started` / `progressed` / `completed` / `cancelled` seek the NavHost transition, `shouldUseFloatingArrowBack` is set by bottom tabs. `NavigationContainer` attaches itself automatically (`AttachHostBackGesture`, iOS only, detached by identity on dispose) — hosts need no wrapper and no `setNavigator`. `maxApi` is a required parameter of `NavigationContainer` — every container, nested or root, is handed its host bridge explicitly.
|
|
49
56
|
|
|
57
|
+
## SwiftUI kit (`ios/`)
|
|
58
|
+
|
|
59
|
+
- **Layout mirrors Compose:** `ios/Navigation/` follows `navigation/` file for file (`NavigationContainer`, `Navigator`, `Navigation` (options + per-screen `Navigation`), `StackScreen` (incl. `NavigationFooter`), `BottomSheet`, `ModalScreen`, `HostBackGestureBridge`, `BottomTab/`, `Component/`, `Tracking/`); `ios/Layout/ScreenLayout.swift` mirrors `layout/`. `ios/Application/` keeps only context (`ApplicationEnvironment`), `Localize*`, `MaxApi` and the deprecated `Screen` / `LegacyHeader` (Compose `application/`). Change a navigation piece on both sides together.
|
|
60
|
+
- **File basenames must be unique across the pod** — Swift fails with "filename used twice" even in different folders (hence `LegacyHeader.swift` next to `Navigation/Component/Header.swift`). New folders under `ios/` must be added to `source_files` in `ios/native-kits.podspec`.
|
|
61
|
+
- Screens update their own chrome at runtime through `@EnvironmentObject var navigation: Navigation` → `setOptions { $0.title = … }` (Compose `LocalNavigation`); bottom tabs keep one `Navigation` per tab. The host back bridge is `HostBackGestureBridge.shared` (`NavigatorManager` is a deprecated alias); a bottom tab off its first tab routes back to the first tab (`Navigator.tabBackHandler`, keyed by the hosting screen).
|
|
62
|
+
- The pod's minimum is **iOS 15**, but the navigation shell (`NavigationContainer`, `Navigator`, `HostBackGestureBridge`, `BottomTab`, overplay views) is **iOS 16+** (`@available(iOS 16.0, *)`) because it is built on `NavigationStack`. Keep components outside that shell iOS 15-compatible.
|
|
63
|
+
- `present` opens one `fullScreenCover` that hosts its own `NavigationStack` (`Navigator.presentedPath`): pushes and pops act on the presented flow while it is up, and each overplay (modal / bottom sheet / snackbar) renders in the context it was shown over (`OverplayItem.presentedId`).
|
|
64
|
+
- Icons are compiled in: `ios/Icon/Icons.swift` is generated from Compose `utils/Icons.kt` by `node scripts/gen-ios-icons.mjs` — don't hand-edit it. A host `icon.json` in the main bundle only adds keys the kit lacks.
|
|
65
|
+
- Bundled assets (colors, Lottie JSON) live in `ios/Resources` and ship in the `MoMoUIKitsResources` bundle; load them through `MoMoUIKitsResources.bundle`, never `Bundle.main`.
|
|
66
|
+
|
|
67
|
+
## Screen layout / iPhone Duo (port of RN `useScreenLayout`, momo-kits !664)
|
|
68
|
+
|
|
69
|
+
- `layout/ScreenLayout.kt`: `getScreenLayout()` returns window size, raw top/bottom + side insets, `contentWidth`, header geometry, the iPhone Duo `sideBar` and `duoTopInset`. `NavigationContainer` computes it once and provides `LocalScreenLayout`; reactive to fold/unfold and rotation (window size + `WindowInsets`). Metrics (`DuoMetrics`, 72pt iPhone header floor, 20pt status-bar threshold) mirror RN `Layout/utils.ts` — change both together.
|
|
70
|
+
- iPhone-only: everything gates on `isPhoneIdiom()` (`platform/`), never on "iOS", so iPad windows keep the regular layout. Two detectors, as in RN: `isIPhoneDuo` (window aspect < 1.6 or asymmetric side insets) drives layout; `isIPhoneDuoDevice` (short side of screen or window > 440, RN `device.ts`) drives popups, modal and bottom sheet.
|
|
71
|
+
- Header height is `headerHeight` (not `AppStatusBar + HEADER_HEIGHT`); the bar row is `headerBarHeight` and starts `headerStatusBarHeight` below the window top. When the status bar is off the top edge (iPhone landscape, iPhone Duo) `headerStatusBarHeight` is 0 so the title stays centered — except on the Duo outer display in portrait, where the header is 76pt and the row starts at 14pt so the title centers on the camera punchout.
|
|
72
|
+
- iPhone Duo with a side strip ≥ 60pt: back + toolkit leave the bar for `HeaderSideBar` (`navigation/component/SideBar.kt`, 48pt glass controls); custom header-right stays in the bar. `sideBar.top` is 170pt in portrait, 120pt on the inner display in landscape (height ≥ 600), 24pt on the outer display in landscape, or below the header when the status bar is on top; UIKit's `statusBarFrame` is useless on Duo. Unlike RN the column is clamped above footer (24pt gap) / rail (12pt gap).
|
|
73
|
+
- Reserved regions (iOS 27.1 `reservedRegionsOfKind:`, reached through the ObjC runtime so older SDKs still build): a probe `UIView` (`platform/ReservedRegions.ios.kt` on the Compose root view; `ReservedRegionsProbe` inside SwiftUI `momoScreenLayout()`) re-reads them in `layoutSubviews`, which UIKit re-runs when they change. They push `sideBar.top` below the status cluster as it grows for calls / Live Activities, and `sideBar.bottom` (≥ 24pt) above a camera at the bottom end of the strip.
|
|
74
|
+
- Tab bar on Duo is `bottomtab/DuoTabBar.kt`: a rail in the side strip (`sideBar.bottom` above the window bottom, QR below it) or, without a side bar, a floating bar spanning 6/8 grid columns. Both overlay the content; `duoFloatingTabBarInset` pads tab content, footer and FAB.
|
|
75
|
+
- Bar compression (`bottomtab/DuoBarCompression.kt`, HIG "Vertical controls"): when toolbar + rail don't fit in the strip, one is compressed — the toolkit into a `navigation_menu` button, the rail into one button showing the current tab — and tapping it swaps them; `BottomTab(compressionBehavior)` picks which starts compressed. If neither fits beside the other compressed, both rest compressed and a tap opens one for a moment. Side bar columns report their full height so the decision follows each screen's toolkit.
|
|
76
|
+
- Glass header: on Duo with a side bar, Default/Extended headers without search and with a `ScrollState` let content scroll under the bar (`usesDuoGlassHeader`); the bar fades in a translucent surface + hairline over 16pt (Compose has no backdrop blur — RN's fallback look), the gradient leaves with the content.
|
|
77
|
+
- Quick return header: glass headers and Animated headers (with a `ScrollState`) on the Duo side bar layout slide out with the content and back in on a reverse scroll; a half-shown header settles in or out once the scroll stops. The side bar is drawn apart from the header, so it stays put.
|
|
78
|
+
- Overlays on Duo: popups cap at 440pt wide and the room between `duoTopInset` and the bottom inset (body scrolls, actions stay); modal centers inside that room; bottom sheet uses the window height, centers at width ≥ 600 and pads by side insets in landscape.
|
|
79
|
+
- Font scale deliberately uses the short side (`min(w, h)`), unlike RN which reads the window width — landscape must not inflate every font.
|
|
80
|
+
- `StackScreen` pads content/footer/search/FAB by the side insets and provides an inset-consumed layout to its content; a bottom-tab root stays full width (tab bar + each tab's StackScreen handle insets). Grid columns use `contentWidth`.
|
|
81
|
+
|
|
50
82
|
## i18n / Localize (port of RN `Localize`)
|
|
51
83
|
|
|
52
84
|
- `application/Localize.kt`: everything i18n. `LocalizationObject` is a data class with required `vi` / `en` maps (`LocalizationObject(vi = mapOf(…), en = mapOf(…))`). `Localize` constructors take a `LocalizationObject` or a `(vi, en)` map pair; the constructor **merges** the kit defaults with the host data (host wins on key clash). Methods: `translate(key)` (missing/empty → returns the key), `translateData(map)` / `translateData(vi, en)`, `addTranslations` (existing keys win), `changeLanguage(lang)`. Default language `"vi"`, `"en"` only on exact match; language is snapshot state so a switch recomposes consumers.
|
|
@@ -41,6 +41,12 @@ public class KitConfig {
|
|
|
41
41
|
public var trustBanner: TrustBannerData? = nil
|
|
42
42
|
public var headerBar: String? = nil
|
|
43
43
|
public var headerGradient: String? = nil
|
|
44
|
+
|
|
45
|
+
public init(trustBanner: TrustBannerData? = nil, headerBar: String? = nil, headerGradient: String? = nil) {
|
|
46
|
+
self.trustBanner = trustBanner
|
|
47
|
+
self.headerBar = headerBar
|
|
48
|
+
self.headerGradient = headerGradient
|
|
49
|
+
}
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
public class ApplicationEnvironment: ObservableObject {
|
|
@@ -59,6 +65,17 @@ public class ApplicationEnvironment: ObservableObject {
|
|
|
59
65
|
}
|
|
60
66
|
}
|
|
61
67
|
|
|
68
|
+
extension ApplicationEnvironment {
|
|
69
|
+
/// Closes the mini app: through `composeApi` as before, else `maxApi` (Compose `maxApi.dismiss`).
|
|
70
|
+
func requestDismiss() {
|
|
71
|
+
if let composeApi {
|
|
72
|
+
composeApi.requestCallback(funcName: "dismiss", params: nil, onResponse: nil)
|
|
73
|
+
} else {
|
|
74
|
+
maxApi?.dismiss(completion: nil)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
62
79
|
private struct ApplicationEnvironmentKey: EnvironmentKey {
|
|
63
80
|
static let defaultValue = ApplicationEnvironment()
|
|
64
81
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
|
|
3
|
+
@available(*, deprecated, message: "Use KitComposeApi or MaxApi")
|
|
3
4
|
public protocol ComposeApi {
|
|
4
5
|
func request(funcName: String, params: Any, _ completion: ((String) -> Void)?)
|
|
5
6
|
func request(funcName: String, params: Any) -> String
|
|
6
7
|
}
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
@available(*, deprecated, message: "Use KitComposeApi or MaxApi")
|
|
9
10
|
public class ComposeApiImpl: ComposeApi {
|
|
10
11
|
public static let shared = ComposeApiImpl()
|
|
11
12
|
|
|
@@ -1,55 +1,13 @@
|
|
|
1
1
|
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
2
|
+
// LegacyHeader.swift
|
|
3
|
+
// MoMoUIKits
|
|
4
4
|
//
|
|
5
|
-
//
|
|
5
|
+
// Header pieces of the deprecated `Screen` (Compose `application/Header.kt`,
|
|
6
|
+
// `application/HeaderBackground.kt`). New screens get theirs from `StackScreen`.
|
|
6
7
|
//
|
|
7
8
|
|
|
8
9
|
import SwiftUI
|
|
9
10
|
|
|
10
|
-
// MARK: - Enums
|
|
11
|
-
|
|
12
|
-
public enum TitlePosition {
|
|
13
|
-
case left, center
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
public enum HeaderType {
|
|
17
|
-
case `default`, extended, none
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
public enum AnimatedHeaderRatio {
|
|
21
|
-
case ratio16_9
|
|
22
|
-
case ratio1_1
|
|
23
|
-
case ratio3_2
|
|
24
|
-
|
|
25
|
-
var value: CGFloat {
|
|
26
|
-
switch self {
|
|
27
|
-
case .ratio16_9: return 16.0 / 9.0
|
|
28
|
-
case .ratio1_1: return 1.0
|
|
29
|
-
case .ratio3_2: return 3.0 / 2.0
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
// MARK: - Animated Header Config
|
|
36
|
-
|
|
37
|
-
public struct AnimatedHeader {
|
|
38
|
-
public var aspectRatio: AnimatedHeaderRatio = .ratio16_9
|
|
39
|
-
public var isSurface: Bool = true
|
|
40
|
-
public var composable: (_ scrollState: CGFloat) -> AnyView = { _ in AnyView(EmptyView()) }
|
|
41
|
-
|
|
42
|
-
public init(
|
|
43
|
-
aspectRatio: AnimatedHeaderRatio = .ratio16_9,
|
|
44
|
-
isSurface: Bool = true,
|
|
45
|
-
composable: @escaping (_ scrollState: CGFloat) -> AnyView = { _ in AnyView(EmptyView()) }
|
|
46
|
-
) {
|
|
47
|
-
self.aspectRatio = aspectRatio
|
|
48
|
-
self.isSurface = isSurface
|
|
49
|
-
self.composable = composable
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
11
|
// MARK: - Utils
|
|
54
12
|
|
|
55
13
|
func safeAreaTopInset() -> CGFloat {
|
|
@@ -66,6 +24,7 @@ func safeAreaTopInset() -> CGFloat {
|
|
|
66
24
|
|
|
67
25
|
// MARK: - Header Background
|
|
68
26
|
|
|
27
|
+
@available(*, deprecated, message: "Use NavigationContainer + StackScreen; the header background comes with it")
|
|
69
28
|
public struct HeaderBackground: View {
|
|
70
29
|
var headerType: HeaderType = .default
|
|
71
30
|
var scrollState: CGFloat
|
|
@@ -146,24 +105,9 @@ public struct HeaderBackground: View {
|
|
|
146
105
|
}
|
|
147
106
|
}
|
|
148
107
|
|
|
149
|
-
// MARK: - Bottom-tab root preference
|
|
150
|
-
|
|
151
|
-
/// Set by `BottomTab` on appear so its enclosing `StackScreen` can detect that
|
|
152
|
-
/// it's hosting a self-contained bottom-tab surface and skip its own bottom
|
|
153
|
-
/// safe-area reservation — mirrors Compose's `Navigation.markAsBottomTabRoot()`
|
|
154
|
-
/// (`navigation.isBottomTabRoot`, read by `StackScreen.kt`'s `MainContent`).
|
|
155
|
-
/// Without this, `BottomTab`'s own internal safe-area handling gets an extra,
|
|
156
|
-
/// unwanted inset stacked on top of it by the outer `StackScreen`, pushing the
|
|
157
|
-
/// tab bar up higher than the true bottom inset.
|
|
158
|
-
public struct IsBottomTabRootKey: PreferenceKey {
|
|
159
|
-
public static var defaultValue: Bool = false
|
|
160
|
-
public static func reduce(value: inout Bool, nextValue: () -> Bool) {
|
|
161
|
-
value = value || nextValue()
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
108
|
// MARK: - Header View
|
|
166
109
|
|
|
110
|
+
@available(*, deprecated, message: "Use NavigationContainer + StackScreen; the header comes with it")
|
|
167
111
|
public struct Header: View {
|
|
168
112
|
var headerType: HeaderType = .default
|
|
169
113
|
var titlePosition: TitlePosition
|
|
@@ -248,9 +192,7 @@ public struct Header: View {
|
|
|
248
192
|
if let inputProps = inputSearchProps {
|
|
249
193
|
InputSearch(props: inputProps)
|
|
250
194
|
} else if case let .user(userData)? = headerTitle {
|
|
251
|
-
|
|
252
|
-
HeaderUserView(userData)
|
|
253
|
-
}
|
|
195
|
+
HeaderUserView(userData)
|
|
254
196
|
} else {
|
|
255
197
|
MomoText(headerTitle?.defaultTitle ?? title, typography: .headerDefaultBold, color: tintColor ?? Colors.black17, maxLines: 1)
|
|
256
198
|
.opacity(titleOpacity)
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
//
|
|
2
2
|
// Screen.swift
|
|
3
|
-
//
|
|
3
|
+
// MoMoUIKits
|
|
4
4
|
//
|
|
5
|
-
//
|
|
5
|
+
// Deprecated standalone screen (Compose `application/Screen.kt`).
|
|
6
6
|
//
|
|
7
7
|
|
|
8
8
|
import SwiftUI
|
|
9
9
|
|
|
10
|
+
@available(*, deprecated, message: "Use NavigationContainer + StackScreen")
|
|
10
11
|
public struct Screen<Content: View>: View {
|
|
11
12
|
// MARK: - Properties
|
|
12
13
|
|
package/ios/Avatar/Avatar.swift
CHANGED
|
@@ -74,12 +74,12 @@ public struct Avatar: View {
|
|
|
74
74
|
.clipShape(RoundedRectangle(cornerRadius: cornerRadius))
|
|
75
75
|
.overlay(
|
|
76
76
|
RoundedRectangle(cornerRadius: cornerRadius)
|
|
77
|
-
.
|
|
77
|
+
.strokeBorder(theme.colors.border.default, lineWidth: 1)
|
|
78
78
|
)
|
|
79
79
|
|
|
80
80
|
// Badge icons — outside clip so they are not cut by border
|
|
81
81
|
if shouldShowIconTop {
|
|
82
|
-
ImageView(LOGO_MOMO_URL)
|
|
82
|
+
ImageView(LOGO_MOMO_URL, contentMode: .fill)
|
|
83
83
|
.frame(width: badgeSizeDp, height: badgeSizeDp)
|
|
84
84
|
.clipShape(RoundedRectangle(cornerRadius: badgeCornerRadius))
|
|
85
85
|
.offset(x: 1, y: -1)
|
|
@@ -92,7 +92,7 @@ public struct Avatar: View {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
if shouldShowIconBottom {
|
|
95
|
-
ImageView(iconSupport)
|
|
95
|
+
ImageView(iconSupport, contentMode: .fill)
|
|
96
96
|
.frame(width: badgeSizeDp, height: badgeSizeDp)
|
|
97
97
|
.clipShape(RoundedRectangle(cornerRadius: badgeCornerRadius))
|
|
98
98
|
.offset(x: 1, y: 1)
|
|
@@ -107,6 +107,7 @@ public struct Avatar: View {
|
|
|
107
107
|
.frame(width: size.sizeDp, height: size.sizeDp)
|
|
108
108
|
.accessibilityElement(children: .ignore)
|
|
109
109
|
.accessibilityLabel(automationId)
|
|
110
|
+
.accessibilityIdentifier(automationId)
|
|
110
111
|
}
|
|
111
112
|
|
|
112
113
|
// MARK: Internal
|
package/ios/Badge/Badge.swift
CHANGED
|
@@ -33,7 +33,8 @@ public struct Badge: View {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
private func formatTitle(_ text: String) -> String {
|
|
36
|
-
|
|
36
|
+
// Digits too long for Int are well over 99.
|
|
37
|
+
if isNumber(text), (Int(text) ?? .max) > 99 {
|
|
37
38
|
return "99+"
|
|
38
39
|
}
|
|
39
40
|
return text
|
|
@@ -54,14 +55,15 @@ public struct Badge: View {
|
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
public var body: some View {
|
|
57
|
-
MomoText(formatTitle(label), typography: .actionXxsBold, color: Colors.black01)
|
|
58
|
+
MomoText(formatTitle(label), typography: .actionXxsBold, color: Colors.black01, maxLines: 1)
|
|
58
59
|
.padding(.horizontal, Spacing.XS)
|
|
59
|
-
.frame(minWidth: fontScale(16)
|
|
60
|
+
.frame(minWidth: fontScale(16))
|
|
61
|
+
.frame(height: fontScale(16))
|
|
60
62
|
.background(badgeColor)
|
|
61
63
|
.cornerRadius(Radius.M)
|
|
62
64
|
.overlay(
|
|
63
65
|
RoundedRectangle(cornerRadius: Radius.M)
|
|
64
|
-
.
|
|
66
|
+
.strokeBorder(Colors.black01, lineWidth: 1)
|
|
65
67
|
)
|
|
66
68
|
}
|
|
67
69
|
}
|
package/ios/Badge/BadgeDot.swift
CHANGED
|
@@ -160,7 +160,6 @@ public struct BadgeRibbon: View {
|
|
|
160
160
|
MomoText(label, typography: .labelXsMedium, color: Colors.black01, maxLines: 1)
|
|
161
161
|
.rotationEffect(rotation)
|
|
162
162
|
.frame(height: RibbonConstants.skewBodyHeight)
|
|
163
|
-
.padding(.horizontal, 8)
|
|
164
163
|
.background(backgroundColor)
|
|
165
164
|
|
|
166
165
|
rightTail
|
package/ios/Button/Button.swift
CHANGED
|
@@ -51,15 +51,17 @@ public extension ButtonType {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
func contentColor(loading: Bool, theme: Theme) -> Color {
|
|
54
|
+
let color: Color
|
|
54
55
|
switch self {
|
|
55
|
-
case .disabled:
|
|
56
|
-
case .primary:
|
|
57
|
-
case .secondary:
|
|
58
|
-
case .outline:
|
|
59
|
-
case .tonal:
|
|
60
|
-
case .danger:
|
|
61
|
-
case .text:
|
|
56
|
+
case .disabled: color = theme.colors.text.disable
|
|
57
|
+
case .primary: color = Colors.black01
|
|
58
|
+
case .secondary: color = theme.colors.text.default
|
|
59
|
+
case .outline: color = theme.colors.primary
|
|
60
|
+
case .tonal: color = theme.colors.primary
|
|
61
|
+
case .danger: color = Colors.black01
|
|
62
|
+
case .text: color = theme.colors.primary
|
|
62
63
|
}
|
|
64
|
+
return color.opacity(loading ? 0.75 : 1)
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
var borderColor: Color? {
|
|
@@ -219,9 +221,8 @@ public struct Button: View {
|
|
|
219
221
|
let color: Color? = useTintColor ? textColor : nil
|
|
220
222
|
|
|
221
223
|
if forceLoading {
|
|
222
|
-
LottieView(name: "lottie_circle_loader", loopMode: .loop)
|
|
224
|
+
LottieView(name: "lottie_circle_loader", loopMode: .loop, tintColor: color.map { UIColor($0) })
|
|
223
225
|
.frame(width: size.iconSize, height: size.iconSize)
|
|
224
|
-
.colorMultiply(color ?? Colors.black01)
|
|
225
226
|
} else if !icon.isEmpty {
|
|
226
227
|
Icon(source: icon, size: size.iconSize, color: color)
|
|
227
228
|
}
|
|
@@ -229,8 +230,8 @@ public struct Button: View {
|
|
|
229
230
|
}
|
|
230
231
|
|
|
231
232
|
/// Mirrors Compose's press feedback: the background shrinks via an animated
|
|
232
|
-
/// horizontal inset
|
|
233
|
-
///
|
|
233
|
+
/// horizontal inset and the whole button (background included) fades to 0.5,
|
|
234
|
+
/// matching `animatedPadding`/`alpha` in Button.kt.
|
|
234
235
|
private struct ButtonPressFeedbackStyle: ButtonStyle {
|
|
235
236
|
let isEnabled: Bool
|
|
236
237
|
let background: Color
|
|
@@ -245,7 +246,6 @@ private struct ButtonPressFeedbackStyle: ButtonStyle {
|
|
|
245
246
|
|
|
246
247
|
configuration.label
|
|
247
248
|
.modifier(HugContentOnLegacyOS(isFull: isFull))
|
|
248
|
-
.opacity(pressed ? 0.5 : 1)
|
|
249
249
|
.background(
|
|
250
250
|
RoundedRectangle(cornerRadius: radius)
|
|
251
251
|
.fill(background)
|
|
@@ -257,6 +257,9 @@ private struct ButtonPressFeedbackStyle: ButtonStyle {
|
|
|
257
257
|
)
|
|
258
258
|
.frame(height: height)
|
|
259
259
|
.clipShape(RoundedRectangle(cornerRadius: radius))
|
|
260
|
+
// `.text` has a clear background, which SwiftUI does not hit-test.
|
|
261
|
+
.contentShape(RoundedRectangle(cornerRadius: radius))
|
|
262
|
+
.opacity(pressed ? 0.5 : 1)
|
|
260
263
|
.animation(.easeInOut(duration: 0.1), value: pressed)
|
|
261
264
|
}
|
|
262
265
|
}
|
|
@@ -16,55 +16,101 @@ extension String {
|
|
|
16
16
|
public struct CalculatorKeyboard: View {
|
|
17
17
|
// MARK: Lifecycle
|
|
18
18
|
@Binding var value: String
|
|
19
|
-
@State var tempValue: String = ""
|
|
20
19
|
|
|
21
20
|
public init(_ value: Binding<String>) {
|
|
22
21
|
self._value = value
|
|
23
22
|
}
|
|
23
|
+
|
|
24
|
+
// Derived from `value` so a host write to the binding (prefill, reset after submit) is honoured.
|
|
25
|
+
private var expression: String {
|
|
26
|
+
value.replacingOccurrences(of: "x", with: "*").replacingOccurrences(of: "÷", with: "/")
|
|
27
|
+
}
|
|
24
28
|
|
|
25
29
|
func onPressBack(){
|
|
26
30
|
if (value.count > 0 ){
|
|
27
31
|
value.removeLast()
|
|
28
32
|
}
|
|
29
|
-
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
func onPressClear(key: String){
|
|
33
36
|
value = ""
|
|
34
|
-
tempValue = ""
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
func onPressEqual(key: String){
|
|
38
|
-
|
|
40
|
+
var expression = self.expression
|
|
41
|
+
if (expression == "") {
|
|
39
42
|
return
|
|
40
43
|
}
|
|
41
|
-
if (
|
|
42
|
-
|
|
44
|
+
if (expression.last?.isNumber == false){
|
|
45
|
+
expression.removeLast()
|
|
43
46
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
// Overflow and division by zero are treated like a negative result: cleared.
|
|
48
|
+
if let result = Self.evaluate(expression), result >= 0 {
|
|
49
|
+
value = String(result)
|
|
50
|
+
} else {
|
|
48
51
|
value = ""
|
|
49
52
|
}
|
|
50
53
|
}
|
|
54
|
+
|
|
55
|
+
/// Integer `+ - * /` with the usual precedence, as NSExpression did, but nil instead of a
|
|
56
|
+
/// crash when a number or result leaves `Int` range.
|
|
57
|
+
static func evaluate(_ expression: String) -> Int? {
|
|
58
|
+
var numbers: [Int] = []
|
|
59
|
+
var operators: [Character] = []
|
|
60
|
+
var digits = ""
|
|
61
|
+
for char in expression {
|
|
62
|
+
if ("0"..."9").contains(char) {
|
|
63
|
+
digits.append(char)
|
|
64
|
+
continue
|
|
65
|
+
}
|
|
66
|
+
guard let number = Int(digits) else { return nil }
|
|
67
|
+
numbers.append(number)
|
|
68
|
+
operators.append(char)
|
|
69
|
+
digits = ""
|
|
70
|
+
}
|
|
71
|
+
guard let last = Int(digits) else { return nil }
|
|
72
|
+
numbers.append(last)
|
|
73
|
+
|
|
74
|
+
var terms = [numbers[0]]
|
|
75
|
+
var signs: [Character] = []
|
|
76
|
+
for (op, number) in zip(operators, numbers.dropFirst()) {
|
|
77
|
+
switch op {
|
|
78
|
+
case "*":
|
|
79
|
+
let (product, overflow) = terms[terms.count - 1].multipliedReportingOverflow(by: number)
|
|
80
|
+
if overflow { return nil }
|
|
81
|
+
terms[terms.count - 1] = product
|
|
82
|
+
case "/":
|
|
83
|
+
guard number != 0 else { return nil }
|
|
84
|
+
terms[terms.count - 1] /= number
|
|
85
|
+
case "+", "-":
|
|
86
|
+
terms.append(number)
|
|
87
|
+
signs.append(op)
|
|
88
|
+
default:
|
|
89
|
+
return nil
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
var result = terms[0]
|
|
94
|
+
for (sign, term) in zip(signs, terms.dropFirst()) {
|
|
95
|
+
let (next, overflow) = sign == "+"
|
|
96
|
+
? result.addingReportingOverflow(term)
|
|
97
|
+
: result.subtractingReportingOverflow(term)
|
|
98
|
+
if overflow { return nil }
|
|
99
|
+
result = next
|
|
100
|
+
}
|
|
101
|
+
return result
|
|
102
|
+
}
|
|
51
103
|
|
|
52
104
|
func onPressKey(key:String){
|
|
53
|
-
if (
|
|
105
|
+
if (value.last?.isNumber == false){
|
|
54
106
|
if (key.isNumber == false){
|
|
55
107
|
return
|
|
56
108
|
}
|
|
57
109
|
}
|
|
58
|
-
var convertedKey = key
|
|
59
|
-
if (key == "x") {convertedKey = "*"}
|
|
60
|
-
if (key == "÷") {convertedKey = "/"}
|
|
61
110
|
if (value == "" && (key == "0" || key == "000" || key.isNumber == false)) {
|
|
62
111
|
return
|
|
63
112
|
}
|
|
64
|
-
|
|
65
|
-
value.append(key)
|
|
66
|
-
tempValue.append(convertedKey)
|
|
67
|
-
}
|
|
113
|
+
value.append(key)
|
|
68
114
|
}
|
|
69
115
|
|
|
70
116
|
// MARK: Public
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import Combine
|
|
2
1
|
import SwiftUI
|
|
3
2
|
|
|
4
3
|
/// Native SwiftUI port of the Compose `Carousel` component.
|
|
@@ -10,9 +9,9 @@ import SwiftUI
|
|
|
10
9
|
/// snap behavior, and scroll enable/disable. Snap / scroll-index callbacks
|
|
11
10
|
/// report the real item index.
|
|
12
11
|
///
|
|
13
|
-
///
|
|
12
|
+
/// Uses a custom drag-driven pager (not a ScrollView) so behavior is identical
|
|
14
13
|
/// across versions and supports the loop virtualization + transform effects
|
|
15
|
-
/// that `TabView(.page)` cannot express
|
|
14
|
+
/// that `TabView(.page)` cannot express.
|
|
16
15
|
public struct Carousel<Content: View>: View {
|
|
17
16
|
// MARK: Lifecycle
|
|
18
17
|
|
|
@@ -82,7 +81,7 @@ public struct Carousel<Content: View>: View {
|
|
|
82
81
|
@State private var currentPage: Int
|
|
83
82
|
@State private var dragOffset: CGFloat = 0
|
|
84
83
|
@State private var isDragging: Bool = false
|
|
85
|
-
@
|
|
84
|
+
@GestureState private var dragActive = false
|
|
86
85
|
|
|
87
86
|
private func realIndex(_ page: Int) -> Int {
|
|
88
87
|
((page % itemCount) + itemCount) % itemCount
|
|
@@ -124,9 +123,20 @@ public struct Carousel<Content: View>: View {
|
|
|
124
123
|
}
|
|
125
124
|
.frame(width: viewportWidth)
|
|
126
125
|
.clipped()
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
126
|
+
// Restarts on drag like Compose's LaunchedEffect(autoplayInterval, isUserScrolling).
|
|
127
|
+
.task(id: AutoplayKey(enabled: autoplay && itemCount > 1 && !isDragging, interval: autoplayInterval)) { @MainActor in
|
|
128
|
+
guard autoplay, itemCount > 1, !isDragging, autoplayInterval > 0 else { return }
|
|
129
|
+
while !Task.isCancelled {
|
|
130
|
+
try? await Task.sleep(nanoseconds: UInt64(autoplayInterval * 1_000_000_000))
|
|
131
|
+
guard !Task.isCancelled else { return }
|
|
132
|
+
advance()
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// A drag stolen by a parent scroll view never reaches `onEnded`; settle on the current page.
|
|
136
|
+
.onChange(of: dragActive) { active in
|
|
137
|
+
guard !active, isDragging else { return }
|
|
138
|
+
isDragging = false
|
|
139
|
+
withAnimation(.easeInOut(duration: 0.25)) { dragOffset = 0 }
|
|
130
140
|
}
|
|
131
141
|
}
|
|
132
142
|
|
|
@@ -147,9 +157,14 @@ public struct Carousel<Content: View>: View {
|
|
|
147
157
|
|
|
148
158
|
private func dragGesture(step: CGFloat) -> some Gesture {
|
|
149
159
|
DragGesture(minimumDistance: 5)
|
|
160
|
+
.updating($dragActive) { _, active, _ in active = true }
|
|
150
161
|
.onChanged { value in
|
|
151
162
|
isDragging = true
|
|
152
|
-
|
|
163
|
+
var offset = value.translation.width
|
|
164
|
+
// No slide exists past either end, as with HorizontalPager bounds.
|
|
165
|
+
if currentPage == 0 { offset = min(offset, 0) }
|
|
166
|
+
if currentPage == virtualPageCount - 1 { offset = max(offset, 0) }
|
|
167
|
+
dragOffset = offset
|
|
153
168
|
}
|
|
154
169
|
.onEnded { value in
|
|
155
170
|
isDragging = false
|
|
@@ -167,7 +182,8 @@ public struct Carousel<Content: View>: View {
|
|
|
167
182
|
let moved = step > 0 ? Int((-dragOffset / step).rounded()) : 0
|
|
168
183
|
target = min(max(currentPage + moved, 0), virtualPageCount - 1)
|
|
169
184
|
}
|
|
170
|
-
|
|
185
|
+
// Animated so a drag that stays on the same page eases back too.
|
|
186
|
+
withAnimation(.easeInOut(duration: 0.25)) { dragOffset = 0 }
|
|
171
187
|
setPage(target)
|
|
172
188
|
}
|
|
173
189
|
}
|
|
@@ -189,8 +205,9 @@ public struct Carousel<Content: View>: View {
|
|
|
189
205
|
onScrollIndexChanged(real)
|
|
190
206
|
}
|
|
191
207
|
}
|
|
208
|
+
}
|
|
192
209
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
210
|
+
private struct AutoplayKey: Equatable {
|
|
211
|
+
let enabled: Bool
|
|
212
|
+
let interval: TimeInterval
|
|
196
213
|
}
|
|
@@ -21,8 +21,10 @@ public struct Checkbox: View{
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
func onCheck(){
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
// Not re-read from the binding: a `.constant` / one-way binding would report the stale value.
|
|
25
|
+
let newValue = !checked
|
|
26
|
+
checked = newValue
|
|
27
|
+
onChange?(newValue)
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
public var body: some View {
|
|
@@ -35,13 +37,15 @@ public struct Checkbox: View{
|
|
|
35
37
|
.frame(width: 20, height: 20)
|
|
36
38
|
.overlay(
|
|
37
39
|
RoundedRectangle(cornerRadius: Radius.XS)
|
|
38
|
-
.
|
|
40
|
+
.strokeBorder(borderColor, lineWidth: Spacing.XXS)
|
|
39
41
|
)
|
|
40
42
|
|
|
41
43
|
if checked {
|
|
42
44
|
Icon(source: indeterminate ? "navigation_minus" : "ic_checked", size: 20, color: Colors.black01)
|
|
43
45
|
}
|
|
44
46
|
}
|
|
47
|
+
// The unchecked fill is clear, which SwiftUI does not hit-test.
|
|
48
|
+
.contentShape(RoundedRectangle(cornerRadius: Radius.XS))
|
|
45
49
|
}
|
|
46
50
|
.buttonStyle(ActiveOpacityButtonStyle(activeOpacity: 0.8))
|
|
47
51
|
.disabled(disabled)
|
package/ios/Chip/Chip.swift
CHANGED
|
@@ -89,18 +89,19 @@ public struct Chip: View {
|
|
|
89
89
|
.padding(.horizontal, horizontal)
|
|
90
90
|
.frame(height: height)
|
|
91
91
|
.background(
|
|
92
|
-
RoundedRectangle(cornerRadius: radius
|
|
92
|
+
RoundedRectangle(cornerRadius: radius)
|
|
93
93
|
.fill(bg)
|
|
94
94
|
)
|
|
95
95
|
.overlay(
|
|
96
96
|
Group {
|
|
97
97
|
if selected {
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
// Inside the bounds, like Compose `border`.
|
|
99
|
+
RoundedRectangle(cornerRadius: radius)
|
|
100
|
+
.strokeBorder(theme.colors.secondary, lineWidth: 2)
|
|
100
101
|
}
|
|
101
102
|
}
|
|
102
103
|
)
|
|
103
|
-
.contentShape(RoundedRectangle(cornerRadius: radius
|
|
104
|
+
.contentShape(RoundedRectangle(cornerRadius: radius))
|
|
104
105
|
}
|
|
105
106
|
.buttonStyle(ActiveOpacityButtonStyle())
|
|
106
107
|
.modifier(ChipAccessibilityModifier(accessibilityLabel: accessibilityLabel))
|