@momo-kits/native-kits 0.162.2-sp.1 → 0.162.2-sp.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/settings.local.json +15 -1
- package/CLAUDE.md +78 -0
- package/build.gradle.kts +11 -0
- package/compose/build.gradle.kts +190 -0
- package/compose/build.gradle.kts.backup +190 -0
- package/compose/compose.podspec +47 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/navigation/ScrollToTop.android.kt +6 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/OsFontScale.android.kt +7 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +123 -0
- package/compose/src/commonMain/composeResources/font/momosignature.otf +0 -0
- package/compose/src/commonMain/composeResources/font/momotrustdisplay.otf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_black.otf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_black.ttf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_bold.ttf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_heavy.ttf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_light.ttf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_medium.ttf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_regular.ttf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_semibold.ttf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_thin.otf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_thin.ttf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_ultralight.otf +0 -0
- package/compose/src/commonMain/composeResources/font/sfprotext_ultralight.ttf +0 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +52 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +106 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +183 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +207 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +39 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +86 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +76 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +76 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +294 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +33 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +826 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Localize.kt +269 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +110 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +378 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +56 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +69 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Avatar.kt +157 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +82 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeDot.kt +32 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeRibbon.kt +338 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/BaselineView.kt +175 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +356 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +113 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +89 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +128 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Collapse.kt +209 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +543 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +23 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +76 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +125 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +231 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +112 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +477 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +114 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +214 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +240 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +216 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +259 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +182 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +343 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Loader.kt +108 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +56 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationNumber.kt +41 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +84 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationWhiteDot.kt +40 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +328 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +97 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +310 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +59 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Rating.kt +87 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +21 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +90 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +323 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Stepper.kt +217 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Steps.kt +473 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/SuggestAction.kt +122 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +199 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +91 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +470 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +88 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +131 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +203 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +498 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +175 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Uploader.kt +182 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +193 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt +29 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +234 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +188 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Colors.kt +306 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +22 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Radius.kt +12 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +16 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +188 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +276 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +2 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +35 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +2 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +52 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +66 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Conditional.kt +11 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/DeprecatedModifier.kt +104 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +48 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Size.kt +51 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +253 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +129 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +100 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +172 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +360 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScrollToTop.kt +8 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +581 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +156 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +225 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +86 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +169 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +320 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +81 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +309 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +34 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +360 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +132 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +128 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +70 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/OsFontScale.kt +9 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +58 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Icons.kt +1329 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +55 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Tracking.kt +15 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +105 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +32 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +66 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +184 -0
- package/gradle/libs.versions.toml +69 -0
- package/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/gradle/wrapper/gradle-wrapper.properties +8 -0
- package/gradle.properties +26 -0
- package/gradlew +252 -0
- package/gradlew.bat +94 -0
- package/ios/Application/ApplicationEnvironment.swift +15 -2
- package/ios/Application/Components.swift +16 -0
- package/ios/Application/HeaderRight.swift +79 -45
- package/ios/Application/Localize.swift +277 -0
- package/ios/Application/MaxApi.swift +18 -0
- package/ios/Application/Navigation/BottomTab/BottomTab.swift +3 -1
- package/ios/Application/Navigation/NavigationContainer.swift +53 -4
- package/ios/Application/Navigation/Navigator.swift +54 -29
- package/ios/Application/Navigation/Overplay/BottomSheet.swift +98 -18
- package/ios/Application/Navigation/Overplay/ModalScreen.swift +6 -0
- package/ios/Application/Navigation/Tracking/ScreenTracker.swift +117 -0
- package/ios/Application/Screen.swift +2 -1
- package/ios/Application/StackScreen.swift +55 -3
- package/ios/Badge/BadgeRibbon.swift +80 -0
- package/ios/Button/Button.swift +67 -11
- package/ios/Carousel/Carousel.swift +16 -1
- package/ios/Checkbox/Checkbox.swift +14 -20
- package/ios/Chip/Chip.swift +35 -26
- package/ios/Collapse/Collapse.swift +11 -1
- package/ios/Colors+Radius+Spacing/Colors.swift +20 -17
- package/ios/Colors+Radius+Spacing/Spacing.swift +3 -1
- package/ios/Colors+Radius+Spacing/Theme.swift +15 -15
- package/ios/DateTimePicker/DateTimePicker.swift +4 -2
- package/ios/DateTimePicker/WheelPicker.swift +7 -3
- package/ios/Extensions/ActiveOpacityButtonStyle.swift +14 -0
- package/ios/Extensions/Color++.swift +32 -0
- package/ios/Icon/Icon.swift +5 -1
- package/ios/IconButton/IconButton.swift +35 -7
- package/ios/Image/Image.swift +16 -3
- package/ios/Information/Information.swift +3 -1
- package/ios/Input/ErrorView.swift +3 -1
- package/ios/Input/Input.swift +31 -8
- package/ios/Input/InputPhoneNumber.swift +14 -3
- package/ios/Input/InputSearch.swift +7 -1
- package/ios/Input/InputTextArea.swift +78 -34
- package/ios/InputDropDown/InputDropDown.swift +4 -1
- package/ios/InputMoney/InputMoney.swift +16 -4
- package/ios/Popup/PopupDisplay.swift +3 -4
- package/ios/Popup/PopupNotify.swift +3 -4
- package/ios/ProgressInfo/ProgressInfo.swift +4 -2
- package/ios/Radio/Radio.swift +12 -13
- package/ios/Rating/Rating.swift +1 -1
- package/ios/Resources/MoMoUIKitsColors.xcassets/AccentColor.colorset/Contents.json +11 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/Background.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/Border.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/Card.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/Contents.json +6 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/Error.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/Primary.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/PrimaryDark.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/PrimaryLight.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/Secondary.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/Success.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/Text.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/TextSecondary.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/Contents.json +9 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/background-default.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/background-disable.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/background-pressed.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/background-selected.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/background-surface.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/background-tonal.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/border-default.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/border-disable.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/gradient.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/primary.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/secondary.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/success-container.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/success-primary.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/success-secondary.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/text-default.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/text-disable.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/text-hint.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/ThemeColors/text-secondary.colorset/Contents.json +38 -0
- package/ios/Resources/MoMoUIKitsColors.xcassets/Warning.colorset/Contents.json +38 -0
- package/ios/Stepper/Stepper.swift +2 -2
- package/ios/Steps/Steps.swift +22 -6
- package/ios/SuggestAction/SuggestAction.swift +5 -3
- package/ios/Swipeable/SwipeCell.swift +5 -3
- package/ios/Switch/Switch.swift +53 -28
- package/ios/TabView/TabView.swift +18 -14
- package/ios/Template/TrustBanner/TrustBanner.swift +6 -7
- package/ios/Tooltip/Tooltip.swift +3 -0
- package/ios/Typography/FontScaleStore.swift +19 -0
- package/ios/Typography/Text.swift +3 -0
- package/ios/Typography/Typography.swift +8 -5
- package/ios/Uploader/Uploader.swift +0 -9
- package/ios/native-kits.podspec +6 -3
- package/ios-demo/MoMoUIKitsDemo.podspec +5 -3
- package/ios-demo/README.md +120 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/MoMoUIKitsDemoFactory.swift +9 -57
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +21 -3
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/LocalizeDemo.swift +188 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +2 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SwiftUIDemoBrowserView.swift +5 -4
- package/local.properties +8 -0
- package/package.json +1 -1
- package/settings.gradle.kts +64 -0
|
@@ -43,41 +43,84 @@ struct BottomSheetView<Content: View>: View {
|
|
|
43
43
|
let content: () -> Content
|
|
44
44
|
|
|
45
45
|
@EnvironmentObject private var navigator: Navigator
|
|
46
|
+
@Environment(\.appTheme) private var theme
|
|
47
|
+
@Environment(\.applicationEnvironment) private var appEnv
|
|
46
48
|
|
|
47
49
|
/// Vertical translation of the sheet; starts off-screen and animates to 0.
|
|
48
50
|
@State private var sheetOffset: CGFloat = UIScreen.main.bounds.height
|
|
49
|
-
|
|
51
|
+
/// Mirrors Compose's `backgroundAlpha`: only touched by open()/close(), never by drag.
|
|
52
|
+
@State private var backgroundAlpha: CGFloat = 0
|
|
50
53
|
@State private var sheetHeight: CGFloat = 0
|
|
51
54
|
@State private var didAppear = false
|
|
55
|
+
@State private var keyboardHeight: CGFloat = 0
|
|
56
|
+
/// Live drag offset, isolated from `sheetOffset` so a touch-move only invalidates
|
|
57
|
+
/// this gesture-scoped value instead of forcing the whole body (including
|
|
58
|
+
/// `content()`) to reconstruct every frame — mirrors the pattern already used in
|
|
59
|
+
/// `ios/PopupView/PopupView.swift` for smooth drag-to-dismiss.
|
|
60
|
+
@GestureState private var dragTranslation: CGFloat = 0
|
|
52
61
|
|
|
53
62
|
/// Mirrors Compose's `sheetCloseOffset` (100dp drag threshold to dismiss).
|
|
54
63
|
private let closeThreshold: CGFloat = 100
|
|
55
64
|
|
|
65
|
+
/// Mirrors Compose's per-term peak (0.3f). Combined with `backgroundAlpha` the
|
|
66
|
+
/// steady-state (fully open) scrim peaks at 0.6, matching ModalScreen's peak.
|
|
67
|
+
private let maxDynamicAlpha: CGFloat = 0.3
|
|
68
|
+
|
|
56
69
|
private var screenHeight: CGFloat { UIScreen.main.bounds.height }
|
|
70
|
+
private var bottomInset: CGFloat { safeAreaBottomInset() }
|
|
71
|
+
/// While the keyboard is up, it already covers the home-indicator area, so it
|
|
72
|
+
/// replaces (rather than adds to) the usual bottom-inset filler.
|
|
73
|
+
private var effectiveBottomInset: CGFloat { keyboardHeight > 0 ? keyboardHeight : bottomInset }
|
|
57
74
|
|
|
58
75
|
private var backgroundColor: Color {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
76
|
+
isSurface ? theme.colors.background.surface : theme.colors.background.default
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private var effectiveOffset: CGFloat { sheetOffset + dragTranslation }
|
|
80
|
+
|
|
81
|
+
/// Mirrors Compose's `dynamicAlpha`: a plain derived value (not itself animated)
|
|
82
|
+
/// that tracks the current offset live, shrinking toward 0 as the sheet is
|
|
83
|
+
/// dragged down.
|
|
84
|
+
private var dynamicAlpha: CGFloat {
|
|
85
|
+
let denom = sheetHeight > 0 ? sheetHeight : screenHeight
|
|
86
|
+
guard denom > 0 else { return maxDynamicAlpha }
|
|
87
|
+
let progress = min(max(1 - effectiveOffset / denom, 0), 1)
|
|
88
|
+
return progress * maxDynamicAlpha
|
|
62
89
|
}
|
|
63
90
|
|
|
91
|
+
/// Mirrors Compose's `alpha = backgroundAlpha + dynamicAlpha`: additive, so the
|
|
92
|
+
/// scrim peaks at 0.6 at rest and only fades to a 0.3 floor while dragging
|
|
93
|
+
/// (backgroundAlpha holds steady until an explicit close()).
|
|
94
|
+
private var totalScrimAlpha: CGFloat { backgroundAlpha + dynamicAlpha }
|
|
95
|
+
|
|
64
96
|
var body: some View {
|
|
65
97
|
ZStack(alignment: .bottom) {
|
|
66
|
-
// Dimmed scrim
|
|
67
|
-
Color.black.opacity(
|
|
68
|
-
.ignoresSafeArea()
|
|
98
|
+
// Dimmed scrim — additive backgroundAlpha + dynamicAlpha (see above).
|
|
99
|
+
Color.black.opacity(totalScrimAlpha)
|
|
69
100
|
.contentShape(Rectangle())
|
|
70
101
|
.onTapGesture {
|
|
71
102
|
if barrierDismissible { close() }
|
|
72
103
|
}
|
|
73
104
|
|
|
74
105
|
sheet
|
|
75
|
-
.offset(y:
|
|
106
|
+
.offset(y: effectiveOffset)
|
|
76
107
|
}
|
|
77
108
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
109
|
+
.ignoresSafeArea()
|
|
110
|
+
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillChangeFrameNotification)) { notification in
|
|
111
|
+
guard let endFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
|
|
112
|
+
withAnimation(.easeOut(duration: 0.25)) {
|
|
113
|
+
keyboardHeight = UIScreen.main.bounds.height - endFrame.origin.y
|
|
114
|
+
}
|
|
115
|
+
}
|
|
78
116
|
.onAppear {
|
|
79
117
|
guard !didAppear else { return }
|
|
80
118
|
didAppear = true
|
|
119
|
+
ScreenTracker.trackPopupDisplayed(
|
|
120
|
+
maxApi: appEnv.maxApi,
|
|
121
|
+
context: appEnv.applicationContext,
|
|
122
|
+
parentScreenName: ScreenTracker.getLastScreenName()
|
|
123
|
+
)
|
|
81
124
|
navigator.registerOverplayDismiss { close() }
|
|
82
125
|
open()
|
|
83
126
|
}
|
|
@@ -102,13 +145,16 @@ struct BottomSheetView<Content: View>: View {
|
|
|
102
145
|
.gesture(dragGesture)
|
|
103
146
|
|
|
104
147
|
Rectangle()
|
|
105
|
-
.fill(
|
|
148
|
+
.fill(theme.colors.border.default)
|
|
106
149
|
.frame(height: 1)
|
|
107
150
|
|
|
108
151
|
content()
|
|
152
|
+
|
|
153
|
+
backgroundColor
|
|
154
|
+
.frame(height: effectiveBottomInset)
|
|
109
155
|
}
|
|
110
156
|
.frame(maxWidth: .infinity)
|
|
111
|
-
.frame(
|
|
157
|
+
.frame(alignment: .bottom)
|
|
112
158
|
.background(
|
|
113
159
|
GeometryReader { geo in
|
|
114
160
|
Color.clear.onAppear { sheetHeight = geo.size.height }
|
|
@@ -139,13 +185,33 @@ struct BottomSheetView<Content: View>: View {
|
|
|
139
185
|
// MARK: - Drag handling (mirrors Compose's detectDragGestures)
|
|
140
186
|
|
|
141
187
|
private var dragGesture: some Gesture {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
188
|
+
// `.global` (not the default `.local`) so the gesture tracks against the
|
|
189
|
+
// window rather than the handle's own view, which is itself repositioned
|
|
190
|
+
// every frame by this same gesture's output (`sheet.offset(y: effectiveOffset)`
|
|
191
|
+
// in `body`) — the local coordinate space chasing its own moving reference
|
|
192
|
+
// frame is a plausible source of a continuous drag-time jitter.
|
|
193
|
+
DragGesture(coordinateSpace: .global)
|
|
194
|
+
.onChanged { _ in
|
|
195
|
+
// Touching the handle should immediately settle `sheetOffset` to its
|
|
196
|
+
// resting value (0) with no animation, so a drag that starts while the
|
|
197
|
+
// open animation — or the keyboard-driven resize, which shifts this
|
|
198
|
+
// same bottom-anchored top edge — is still in flight doesn't fight a
|
|
199
|
+
// moving target. Without this, the live drag value chases an animated
|
|
200
|
+
// one, producing a couple of corrective jumps before it settles.
|
|
201
|
+
if sheetOffset != 0 {
|
|
202
|
+
sheetOffset = 0
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
.updating($dragTranslation) { value, state, _ in
|
|
206
|
+
state = max(value.translation.height, 0)
|
|
146
207
|
}
|
|
147
|
-
.onEnded {
|
|
148
|
-
|
|
208
|
+
.onEnded { value in
|
|
209
|
+
let finalOffset = max(value.translation.height, 0)
|
|
210
|
+
// Capture into persisted state BEFORE @GestureState resets to 0,
|
|
211
|
+
// avoiding a one-frame snap back to rest before close()'s own
|
|
212
|
+
// slide-out animation starts (mirrors PopupView's `lastDragPosition`).
|
|
213
|
+
sheetOffset = finalOffset
|
|
214
|
+
if finalOffset > closeThreshold {
|
|
149
215
|
close()
|
|
150
216
|
} else {
|
|
151
217
|
withAnimation(.spring(response: 0.3, dampingFraction: 0.9)) {
|
|
@@ -159,18 +225,32 @@ struct BottomSheetView<Content: View>: View {
|
|
|
159
225
|
|
|
160
226
|
private func open() {
|
|
161
227
|
DispatchQueue.main.async {
|
|
162
|
-
withAnimation(.
|
|
228
|
+
withAnimation(.timingCurve(0.05, 0.7, 0.1, 1, duration: 0.1)) { backgroundAlpha = maxDynamicAlpha }
|
|
163
229
|
withAnimation(.timingCurve(0.05, 0.7, 0.1, 1, duration: 0.35)) { sheetOffset = 0 }
|
|
164
230
|
}
|
|
165
231
|
}
|
|
166
232
|
|
|
233
|
+
/// Mirrors Compose's `closeEvent`: slide out over 200ms, then fade the scrim
|
|
234
|
+
/// out over 100ms only after the slide finishes, then dismiss.
|
|
167
235
|
private func close() {
|
|
168
236
|
withAnimation(.timingCurve(0.3, 0, 0.8, 0.15, duration: 0.2)) {
|
|
169
237
|
sheetOffset = max(sheetHeight, screenHeight)
|
|
170
|
-
scrimAlpha = 0
|
|
171
238
|
}
|
|
172
239
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
|
|
240
|
+
withAnimation(.timingCurve(0.3, 0, 0.8, 0.15, duration: 0.1)) {
|
|
241
|
+
backgroundAlpha = 0
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
|
173
245
|
navigator.finishOverplayDismiss()
|
|
174
246
|
}
|
|
175
247
|
}
|
|
176
248
|
}
|
|
249
|
+
|
|
250
|
+
@available(iOS 16.0, *)
|
|
251
|
+
private func safeAreaBottomInset() -> CGFloat {
|
|
252
|
+
let bottomInset = UIApplication.shared.connectedScenes
|
|
253
|
+
.compactMap { ($0 as? UIWindowScene)?.keyWindow?.safeAreaInsets.bottom }
|
|
254
|
+
.first ?? 0
|
|
255
|
+
return min(bottomInset, 21)
|
|
256
|
+
}
|
|
@@ -15,6 +15,7 @@ struct ModalScreen<Content: View>: View {
|
|
|
15
15
|
let content: () -> Content
|
|
16
16
|
|
|
17
17
|
@EnvironmentObject private var navigator: Navigator
|
|
18
|
+
@Environment(\.applicationEnvironment) private var appEnv
|
|
18
19
|
|
|
19
20
|
@State private var scrimOpacity: CGFloat = 0
|
|
20
21
|
@State private var contentScale: CGFloat = 0.8
|
|
@@ -38,6 +39,11 @@ struct ModalScreen<Content: View>: View {
|
|
|
38
39
|
.onAppear {
|
|
39
40
|
guard !didAppear else { return }
|
|
40
41
|
didAppear = true
|
|
42
|
+
ScreenTracker.trackPopupDisplayed(
|
|
43
|
+
maxApi: appEnv.maxApi,
|
|
44
|
+
context: appEnv.applicationContext,
|
|
45
|
+
parentScreenName: ScreenTracker.getLastScreenName()
|
|
46
|
+
)
|
|
41
47
|
navigator.registerOverplayDismiss { close() }
|
|
42
48
|
open()
|
|
43
49
|
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
//
|
|
2
|
+
// ScreenTracker.swift
|
|
3
|
+
// MoMoUIKits
|
|
4
|
+
//
|
|
5
|
+
// SwiftUI mirror of Compose's `navigation/tracking/ScreenTracker.kt`. Pure
|
|
6
|
+
// logic, no platform dependency beyond the `MaxApi` bridge.
|
|
7
|
+
//
|
|
8
|
+
|
|
9
|
+
import Foundation
|
|
10
|
+
|
|
11
|
+
public final class ScreenTrackingState {
|
|
12
|
+
public var time: Double = Date().timeIntervalSince1970 * 1000
|
|
13
|
+
public var releaseNavigated: Bool = false
|
|
14
|
+
public var releaseLoad: Bool = false
|
|
15
|
+
public var releaseInteraction: Bool = false
|
|
16
|
+
public var previousScreenName: String?
|
|
17
|
+
public var action: String = "push"
|
|
18
|
+
|
|
19
|
+
public init() {}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public enum ScreenTracker {
|
|
23
|
+
private static var lastScreenName: String?
|
|
24
|
+
|
|
25
|
+
public static func trackScreenNavigated(
|
|
26
|
+
maxApi: MaxApi?,
|
|
27
|
+
context: MiniAppContext?,
|
|
28
|
+
screenName: String,
|
|
29
|
+
action: String,
|
|
30
|
+
state: ScreenTrackingState
|
|
31
|
+
) {
|
|
32
|
+
guard !state.releaseNavigated else { return }
|
|
33
|
+
|
|
34
|
+
let previousScreen = lastScreenName
|
|
35
|
+
lastScreenName = screenName
|
|
36
|
+
|
|
37
|
+
var params = buildBaseParams(context: context, screenName: screenName)
|
|
38
|
+
params["pre_screen_name"] = previousScreen
|
|
39
|
+
params["state"] = "navigated"
|
|
40
|
+
params["action"] = action
|
|
41
|
+
maxApi?.track(eventName: "auto_screen_navigated", params: params)
|
|
42
|
+
|
|
43
|
+
state.releaseNavigated = true
|
|
44
|
+
state.previousScreenName = previousScreen
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public static func trackScreenDisplayed(
|
|
48
|
+
maxApi: MaxApi?,
|
|
49
|
+
context: MiniAppContext?,
|
|
50
|
+
screenName: String,
|
|
51
|
+
duration: Double,
|
|
52
|
+
state: ScreenTrackingState
|
|
53
|
+
) {
|
|
54
|
+
guard !state.releaseLoad else { return }
|
|
55
|
+
|
|
56
|
+
var params = buildBaseParams(context: context, screenName: screenName)
|
|
57
|
+
params["state"] = "load"
|
|
58
|
+
params["duration"] = duration
|
|
59
|
+
maxApi?.track(eventName: "auto_screen_displayed", params: params)
|
|
60
|
+
|
|
61
|
+
state.releaseLoad = true
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public static func trackScreenInteracted(
|
|
65
|
+
maxApi: MaxApi?,
|
|
66
|
+
context: MiniAppContext?,
|
|
67
|
+
screenName: String,
|
|
68
|
+
duration: Double,
|
|
69
|
+
totalDuration: Double,
|
|
70
|
+
state: ScreenTrackingState
|
|
71
|
+
) {
|
|
72
|
+
guard !state.releaseInteraction else { return }
|
|
73
|
+
|
|
74
|
+
var params = buildBaseParams(context: context, screenName: screenName)
|
|
75
|
+
params["state"] = "interaction"
|
|
76
|
+
params["duration"] = duration
|
|
77
|
+
params["total_duration"] = totalDuration
|
|
78
|
+
maxApi?.track(eventName: "auto_screen_interacted", params: params)
|
|
79
|
+
|
|
80
|
+
state.releaseInteraction = true
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
public static func trackPopupDisplayed(
|
|
84
|
+
maxApi: MaxApi?,
|
|
85
|
+
context: MiniAppContext?,
|
|
86
|
+
parentScreenName: String?
|
|
87
|
+
) {
|
|
88
|
+
maxApi?.track(eventName: "auto_popup_displayed", params: [
|
|
89
|
+
"screen_name": parentScreenName,
|
|
90
|
+
"tracking_source": 1,
|
|
91
|
+
"app_id": context?.appId,
|
|
92
|
+
"feature_code": context?.appCode,
|
|
93
|
+
"kits_version": nil,
|
|
94
|
+
"component_name": "Modal"
|
|
95
|
+
])
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private static func buildBaseParams(
|
|
99
|
+
context: MiniAppContext?,
|
|
100
|
+
screenName: String,
|
|
101
|
+
componentName: String = "Screen"
|
|
102
|
+
) -> [String: Any?] {
|
|
103
|
+
[
|
|
104
|
+
"app_id": context?.appId,
|
|
105
|
+
"feature_code": context?.appCode,
|
|
106
|
+
"screen_name": screenName,
|
|
107
|
+
"component_name": componentName,
|
|
108
|
+
"tracking_source": 1
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
public static func getLastScreenName() -> String? { lastScreenName }
|
|
113
|
+
|
|
114
|
+
public static func reset() {
|
|
115
|
+
lastScreenName = nil
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -229,6 +229,7 @@ public struct Screen<Content: View>: View {
|
|
|
229
229
|
struct Footer: View {
|
|
230
230
|
var footerView: AnyView
|
|
231
231
|
var keyboardSize: CGFloat
|
|
232
|
+
var bottomInset: CGFloat = 0
|
|
232
233
|
|
|
233
234
|
var body: some View {
|
|
234
235
|
VStack {
|
|
@@ -236,7 +237,7 @@ struct Footer: View {
|
|
|
236
237
|
}
|
|
237
238
|
.padding(.vertical, Spacing.S)
|
|
238
239
|
.padding(.horizontal, Spacing.M)
|
|
239
|
-
.padding(.bottom, keyboardSize)
|
|
240
|
+
.padding(.bottom, keyboardSize - bottomInset)
|
|
240
241
|
.background(Colors.black01)
|
|
241
242
|
.overlay(
|
|
242
243
|
Rectangle()
|
|
@@ -39,6 +39,7 @@ public struct StackScreen<Content: View>: View {
|
|
|
39
39
|
var layoutOffset: CGFloat
|
|
40
40
|
var inputSearchProps: InputSearchProps?
|
|
41
41
|
var fabProps: FabProps?
|
|
42
|
+
var screenName: String = ""
|
|
42
43
|
var content: () -> Content
|
|
43
44
|
|
|
44
45
|
public init(
|
|
@@ -66,6 +67,7 @@ public struct StackScreen<Content: View>: View {
|
|
|
66
67
|
layoutOffset: CGFloat = 56,
|
|
67
68
|
inputSearchProps: InputSearchProps? = nil,
|
|
68
69
|
fabProps: FabProps? = nil,
|
|
70
|
+
screenName: String = "",
|
|
69
71
|
@ViewBuilder content: @escaping () -> Content
|
|
70
72
|
) {
|
|
71
73
|
self.backgroundColor = backgroundColor
|
|
@@ -92,12 +94,21 @@ public struct StackScreen<Content: View>: View {
|
|
|
92
94
|
self.layoutOffset = layoutOffset
|
|
93
95
|
self.inputSearchProps = inputSearchProps
|
|
94
96
|
self.fabProps = fabProps
|
|
97
|
+
self.screenName = screenName
|
|
95
98
|
self.content = content
|
|
96
99
|
}
|
|
97
100
|
|
|
98
101
|
@State private var scrollOffset: CGFloat = 0
|
|
99
102
|
@State private var keyboardHeight: CGFloat = 0
|
|
100
103
|
@State private var headerRightWidth: CGFloat = 0
|
|
104
|
+
@State private var isBottomTabRoot: Bool = false
|
|
105
|
+
|
|
106
|
+
@Environment(\.applicationEnvironment) private var appEnv
|
|
107
|
+
|
|
108
|
+
// Auto-tracking state (mirrors Compose's `remember { ScreenTrackingState() }` +
|
|
109
|
+
// the once-computed push/back `action`).
|
|
110
|
+
@State private var trackingState = ScreenTrackingState()
|
|
111
|
+
@State private var trackingAction: String = ScreenTracker.getLastScreenName() != nil ? "push" : "back"
|
|
101
112
|
|
|
102
113
|
/// Header background fade tied to scroll: 0 → transparent (over a hero image
|
|
103
114
|
/// or transparent header), 1 → opaque surface with the drop shadow.
|
|
@@ -132,8 +143,13 @@ public struct StackScreen<Content: View>: View {
|
|
|
132
143
|
// MARK: - Body
|
|
133
144
|
|
|
134
145
|
public var body: some View {
|
|
135
|
-
GeometryReader {
|
|
146
|
+
GeometryReader { geometry in
|
|
136
147
|
let keyboardSize: CGFloat = useAvoidKeyboard ? max(keyboardHeight, 0) : 0
|
|
148
|
+
let bottomInset = min(geometry.safeAreaInsets.bottom, 21)
|
|
149
|
+
// Skip this screen's own bottom-inset reservation when hosting a footer
|
|
150
|
+
// OR a self-contained BottomTab surface (mirrors Compose's
|
|
151
|
+
// `if (options.footerComponent != null || isBottomTab) 0.dp else navigationBar`).
|
|
152
|
+
let contentBottomInset = (footer == nil && !isBottomTabRoot) ? bottomInset : 0
|
|
137
153
|
|
|
138
154
|
ZStack(alignment: .top) {
|
|
139
155
|
// Background color
|
|
@@ -145,6 +161,7 @@ public struct StackScreen<Content: View>: View {
|
|
|
145
161
|
VStack(alignment: horizontalAlignment, spacing: 0) {
|
|
146
162
|
headerAndContent
|
|
147
163
|
}
|
|
164
|
+
.padding(.bottom, contentBottomInset)
|
|
148
165
|
|
|
149
166
|
FloatingContent(
|
|
150
167
|
fabProps: fabProps,
|
|
@@ -153,7 +170,7 @@ public struct StackScreen<Content: View>: View {
|
|
|
153
170
|
)
|
|
154
171
|
}
|
|
155
172
|
|
|
156
|
-
FooterContent(footer: footer, keyboardSize: keyboardSize)
|
|
173
|
+
FooterContent(footer: footer, keyboardSize: keyboardSize, bottomInset: bottomInset)
|
|
157
174
|
}
|
|
158
175
|
|
|
159
176
|
// Animated search bar overlay (mirrors Compose's SearchAnimated layer).
|
|
@@ -172,6 +189,9 @@ public struct StackScreen<Content: View>: View {
|
|
|
172
189
|
.onPreferenceChange(HeaderRightWidthKey.self) { width in
|
|
173
190
|
if headerRightWidth != width { headerRightWidth = width }
|
|
174
191
|
}
|
|
192
|
+
.onPreferenceChange(IsBottomTabRootKey.self) { isRoot in
|
|
193
|
+
if isBottomTabRoot != isRoot { isBottomTabRoot = isRoot }
|
|
194
|
+
}
|
|
175
195
|
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillChangeFrameNotification)) { notification in
|
|
176
196
|
guard useAvoidKeyboard,
|
|
177
197
|
let endFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
|
|
@@ -183,6 +203,37 @@ public struct StackScreen<Content: View>: View {
|
|
|
183
203
|
// taps still reach buttons/fields while also dismissing the keyboard.
|
|
184
204
|
.modifier(HideKeyboardOnTap(enabled: keyboardShouldPersistTaps))
|
|
185
205
|
}
|
|
206
|
+
.task(id: screenName) {
|
|
207
|
+
ScreenTracker.trackScreenNavigated(
|
|
208
|
+
maxApi: appEnv.maxApi,
|
|
209
|
+
context: appEnv.applicationContext,
|
|
210
|
+
screenName: screenName,
|
|
211
|
+
action: trackingAction,
|
|
212
|
+
state: trackingState
|
|
213
|
+
)
|
|
214
|
+
do {
|
|
215
|
+
try await Task.sleep(nanoseconds: 2_000_000_000)
|
|
216
|
+
} catch {
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
let loadTime = Date().timeIntervalSince1970 * 1000 - trackingState.time
|
|
220
|
+
ScreenTracker.trackScreenDisplayed(
|
|
221
|
+
maxApi: appEnv.maxApi,
|
|
222
|
+
context: appEnv.applicationContext,
|
|
223
|
+
screenName: screenName,
|
|
224
|
+
duration: loadTime,
|
|
225
|
+
state: trackingState
|
|
226
|
+
)
|
|
227
|
+
let interactionTime = Date().timeIntervalSince1970 * 1000 - trackingState.time
|
|
228
|
+
ScreenTracker.trackScreenInteracted(
|
|
229
|
+
maxApi: appEnv.maxApi,
|
|
230
|
+
context: appEnv.applicationContext,
|
|
231
|
+
screenName: screenName,
|
|
232
|
+
duration: interactionTime - loadTime,
|
|
233
|
+
totalDuration: interactionTime,
|
|
234
|
+
state: trackingState
|
|
235
|
+
)
|
|
236
|
+
}
|
|
186
237
|
}
|
|
187
238
|
|
|
188
239
|
// MARK: - Header + content layer
|
|
@@ -440,10 +491,11 @@ private struct HideKeyboardOnTap: ViewModifier {
|
|
|
440
491
|
private struct FooterContent: View {
|
|
441
492
|
let footer: (() -> AnyView)?
|
|
442
493
|
let keyboardSize: CGFloat
|
|
494
|
+
let bottomInset: CGFloat
|
|
443
495
|
|
|
444
496
|
var body: some View {
|
|
445
497
|
if let footerView = footer?() {
|
|
446
|
-
Footer(footerView: footerView, keyboardSize: keyboardSize)
|
|
498
|
+
Footer(footerView: footerView, keyboardSize: keyboardSize, bottomInset: bottomInset)
|
|
447
499
|
}
|
|
448
500
|
}
|
|
449
501
|
}
|
|
@@ -196,6 +196,86 @@ public struct BadgeRibbon: View {
|
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
+
/// Ported from Compose's `RoundedBadgeRibbon` (BadgeRibbon.kt:164-326): a canvas-drawn
|
|
200
|
+
/// ribbon shape sized to its text, mirrored per corner via scaleX/scaleY (never an actual
|
|
201
|
+
/// rotation) — the label gets the same scale applied a second time so it stays right-reading
|
|
202
|
+
/// while only the ribbon shape flips.
|
|
203
|
+
public struct RoundedBadgeRibbon: View {
|
|
204
|
+
let text: String
|
|
205
|
+
let position: RibbonPosition
|
|
206
|
+
|
|
207
|
+
public init(text: String = "Label", position: RibbonPosition = .topLeft) {
|
|
208
|
+
self.text = text
|
|
209
|
+
self.position = position
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private var scale: (x: CGFloat, y: CGFloat) {
|
|
213
|
+
switch position {
|
|
214
|
+
case .topLeft: return (-1, 1)
|
|
215
|
+
case .topRight: return (1, 1)
|
|
216
|
+
case .bottomRight: return (1, -1)
|
|
217
|
+
case .bottomLeft: return (-1, -1)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private var measuredTextSize: CGSize {
|
|
222
|
+
let fontSize = scaleSize(TypographyStyle.labelXsMedium.fontSize)
|
|
223
|
+
let font = UIFont.systemFont(ofSize: fontSize, weight: .medium)
|
|
224
|
+
return text.size(withAttributes: [.font: font])
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
public var body: some View {
|
|
228
|
+
let textSize = measuredTextSize
|
|
229
|
+
let minRibbonHeight = max(textSize.height * 1.2, 16)
|
|
230
|
+
let padding = minRibbonHeight / 4
|
|
231
|
+
let badgeHeight = minRibbonHeight + padding
|
|
232
|
+
let badgeWidth = max(textSize.width + padding * 2, 28)
|
|
233
|
+
|
|
234
|
+
ZStack {
|
|
235
|
+
Canvas { context, size in
|
|
236
|
+
let ribbonHeight = size.height - padding
|
|
237
|
+
let cornerRadius = (badgeHeight - padding) / 2
|
|
238
|
+
let mainColor = Colors.orange03
|
|
239
|
+
let tailColor = Colors.red01
|
|
240
|
+
|
|
241
|
+
let headWidth: CGFloat = 4
|
|
242
|
+
// Kotlin computes `leftSectionWidth` + `middleSectionWidth` here, but those
|
|
243
|
+
// cancel algebraically to `width - headWidth` — kept as the direct result.
|
|
244
|
+
let rightX = size.width - headWidth
|
|
245
|
+
|
|
246
|
+
let leftMiddlePath = RoundedCorners(topLeft: cornerRadius, topRight: 0, bottomLeft: cornerRadius, bottomRight: 0)
|
|
247
|
+
.path(in: CGRect(x: 0, y: 0, width: rightX + 1, height: ribbonHeight))
|
|
248
|
+
context.fill(leftMiddlePath, with: .color(mainColor))
|
|
249
|
+
|
|
250
|
+
let headPath = RoundedCorners(topLeft: 0, topRight: cornerRadius, bottomLeft: 0, bottomRight: 0)
|
|
251
|
+
.path(in: CGRect(x: rightX, y: 0, width: headWidth, height: ribbonHeight))
|
|
252
|
+
context.fill(headPath, with: .color(mainColor))
|
|
253
|
+
|
|
254
|
+
let halfHeadWidth = headWidth / 2
|
|
255
|
+
let tailStartX = rightX + headWidth - halfHeadWidth
|
|
256
|
+
let tailY = ribbonHeight
|
|
257
|
+
let tailHeight = padding
|
|
258
|
+
|
|
259
|
+
context.fill(
|
|
260
|
+
Path(CGRect(x: tailStartX, y: tailY, width: halfHeadWidth, height: halfHeadWidth)),
|
|
261
|
+
with: .color(mainColor)
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
let tailRightPath = RoundedCorners(topLeft: 0, topRight: tailHeight, bottomLeft: 0, bottomRight: tailHeight)
|
|
265
|
+
.path(in: CGRect(x: tailStartX, y: tailY, width: halfHeadWidth, height: tailHeight))
|
|
266
|
+
context.fill(tailRightPath, with: .color(tailColor))
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
MomoText(text, typography: .labelXsMedium, color: Colors.black01, maxLines: 1)
|
|
270
|
+
.padding(.bottom, padding)
|
|
271
|
+
.padding(.horizontal, padding / 2)
|
|
272
|
+
.scaleEffect(x: scale.x, y: scale.y)
|
|
273
|
+
}
|
|
274
|
+
.frame(width: badgeWidth, height: badgeHeight)
|
|
275
|
+
.scaleEffect(x: scale.x, y: scale.y)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
199
279
|
// MARK: - Constants
|
|
200
280
|
private struct RibbonConstants {
|
|
201
281
|
static let ribbonHeight: CGFloat = 20
|
package/ios/Button/Button.swift
CHANGED
|
@@ -123,7 +123,6 @@ extension ButtonSize {
|
|
|
123
123
|
case .large: return .actionDefaultBold
|
|
124
124
|
case .medium: return .actionSBold
|
|
125
125
|
case .small: return .actionXsBold
|
|
126
|
-
default : return .actionDefaultBold
|
|
127
126
|
}
|
|
128
127
|
}
|
|
129
128
|
}
|
|
@@ -162,6 +161,35 @@ public struct Button: View {
|
|
|
162
161
|
self.useTintColor = useTintColor
|
|
163
162
|
}
|
|
164
163
|
|
|
164
|
+
/// Mirrors Kotlin's `Button(iconLeft: String, iconRight: String, ...)`, which resolves
|
|
165
|
+
/// icons by design-system source name through the shared `Icon` component rather than
|
|
166
|
+
/// a caller-supplied view. `iconLeft` is required here (unlike Kotlin's `= ""` default)
|
|
167
|
+
/// solely to keep this initializer unambiguous alongside the `AnyView`-based one above —
|
|
168
|
+
/// pass `""` explicitly for an icon-right-only button.
|
|
169
|
+
public init(
|
|
170
|
+
title: String = "Button",
|
|
171
|
+
action: @escaping () -> Void,
|
|
172
|
+
type: ButtonType = .primary,
|
|
173
|
+
size: ButtonSize = .large,
|
|
174
|
+
iconLeft: String,
|
|
175
|
+
iconRight: String = "",
|
|
176
|
+
isFull: Bool = true,
|
|
177
|
+
loading: Bool = false,
|
|
178
|
+
useTintColor: Bool = true
|
|
179
|
+
) {
|
|
180
|
+
self.init(
|
|
181
|
+
title: title,
|
|
182
|
+
action: action,
|
|
183
|
+
type: type,
|
|
184
|
+
size: size,
|
|
185
|
+
iconLeft: iconLeft.isEmpty ? nil : AnyView(Icon(source: iconLeft)),
|
|
186
|
+
iconRight: iconRight.isEmpty ? nil : AnyView(Icon(source: iconRight)),
|
|
187
|
+
isFull: isFull,
|
|
188
|
+
loading: loading,
|
|
189
|
+
useTintColor: useTintColor
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
|
|
165
193
|
private func shouldLoadingOnLeft(_ left: AnyView?, _ right: AnyView?) -> Bool {
|
|
166
194
|
let hasLeft = left != nil
|
|
167
195
|
let hasRight = right != nil
|
|
@@ -214,14 +242,15 @@ public struct Button: View {
|
|
|
214
242
|
.frame(maxWidth: isFull ? .infinity : nil)
|
|
215
243
|
.padding(.horizontal, size.padding)
|
|
216
244
|
.frame(height: size.height)
|
|
217
|
-
.background(bg)
|
|
218
|
-
.overlay(
|
|
219
|
-
RoundedRectangle(cornerRadius: size.radius)
|
|
220
|
-
.strokeBorder(border ?? .clear, lineWidth: border != nil ? borderWidth : 0)
|
|
221
|
-
)
|
|
222
|
-
.clipShape(RoundedRectangle(cornerRadius: size.radius))
|
|
223
245
|
}
|
|
224
|
-
.buttonStyle(ButtonPressFeedbackStyle(
|
|
246
|
+
.buttonStyle(ButtonPressFeedbackStyle(
|
|
247
|
+
isEnabled: isEnabled,
|
|
248
|
+
background: bg,
|
|
249
|
+
border: border,
|
|
250
|
+
borderWidth: borderWidth,
|
|
251
|
+
radius: size.radius,
|
|
252
|
+
height: size.height
|
|
253
|
+
))
|
|
225
254
|
.disabled(!isEnabled)
|
|
226
255
|
}
|
|
227
256
|
|
|
@@ -238,12 +267,39 @@ public struct Button: View {
|
|
|
238
267
|
}
|
|
239
268
|
}
|
|
240
269
|
|
|
270
|
+
/// Mirrors Compose's press feedback: the background shrinks via an animated
|
|
271
|
+
/// horizontal inset while the content fades, rather than a plain opacity dim on
|
|
272
|
+
/// the whole button (matching `animatedPadding`/`alpha` in Button.kt).
|
|
241
273
|
private struct ButtonPressFeedbackStyle: ButtonStyle {
|
|
242
274
|
let isEnabled: Bool
|
|
275
|
+
let background: Color
|
|
276
|
+
let border: Color?
|
|
277
|
+
let borderWidth: CGFloat
|
|
278
|
+
let radius: CGFloat
|
|
279
|
+
let height: CGFloat
|
|
243
280
|
|
|
244
281
|
func makeBody(configuration: Configuration) -> some View {
|
|
245
|
-
configuration.
|
|
246
|
-
|
|
247
|
-
|
|
282
|
+
let pressed = configuration.isPressed && isEnabled
|
|
283
|
+
|
|
284
|
+
ZStack {
|
|
285
|
+
RoundedRectangle(cornerRadius: radius)
|
|
286
|
+
.fill(background)
|
|
287
|
+
.overlay(
|
|
288
|
+
RoundedRectangle(cornerRadius: radius)
|
|
289
|
+
.strokeBorder(border ?? .clear, lineWidth: border != nil ? borderWidth : 0)
|
|
290
|
+
)
|
|
291
|
+
.padding(.horizontal, pressed ? 2 : 0)
|
|
292
|
+
|
|
293
|
+
configuration.label
|
|
294
|
+
.opacity(pressed ? 0.5 : 1)
|
|
295
|
+
}
|
|
296
|
+
// Pin the whole button to its intended height explicitly: the bare
|
|
297
|
+
// RoundedRectangle background has no intrinsic size of its own, so left
|
|
298
|
+
// unconstrained in a ZStack inside a non-height-constraining container
|
|
299
|
+
// (e.g. a footer VStack) it can report an oversized ideal height and
|
|
300
|
+
// stretch the button — and its container — far taller than intended.
|
|
301
|
+
.frame(height: height)
|
|
302
|
+
.clipShape(RoundedRectangle(cornerRadius: radius))
|
|
303
|
+
.animation(.easeInOut(duration: 0.1), value: pressed)
|
|
248
304
|
}
|
|
249
305
|
}
|