@momo-kits/native-kits 0.0.1-beta.4 → 0.0.2-alpha.1
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 +99 -0
- package/PUBLISH_GUIDE.md +675 -0
- package/README.md +184 -5
- package/ios/Application/ApplicationEnvironment.swift +71 -0
- package/ios/Application/Components.swift +270 -0
- package/ios/Application/ComposeApi.swift +22 -0
- package/ios/Application/FloatingButton.swift +174 -0
- package/ios/Application/HeaderRight.swift +306 -0
- package/ios/Application/LanguageSource.swift +93 -0
- package/ios/Application/Localize.swift +378 -0
- package/ios/Application/LocalizeModifier.swift +30 -0
- package/ios/Application/MaxApi.swift +18 -0
- package/ios/Application/Navigation/BottomTab/BottomTab.swift +104 -0
- package/ios/Application/Navigation/BottomTab/BottomTabBar.swift +215 -0
- package/ios/Application/Navigation/BottomTab/CurvedContainer.swift +65 -0
- package/ios/Application/Navigation/HeaderBackProps.swift +66 -0
- package/ios/Application/Navigation/HeaderTitle.swift +57 -0
- package/ios/Application/Navigation/HeaderUser.swift +209 -0
- package/ios/Application/Navigation/NavigationContainer.swift +213 -0
- package/ios/Application/Navigation/NavigationOptions.swift +109 -0
- package/ios/Application/Navigation/Navigator.swift +453 -0
- package/ios/Application/Navigation/NavigatorManager.swift +22 -0
- package/ios/Application/Navigation/Overplay/BottomSheet.swift +256 -0
- package/ios/Application/Navigation/Overplay/ModalScreen.swift +74 -0
- package/ios/Application/Navigation/Overplay/SnackBar.swift +144 -0
- package/ios/Application/Navigation/Tracking/ScreenTracker.swift +117 -0
- package/ios/Application/Navigation/component/Header.swift +178 -0
- package/ios/Application/Navigation/component/HeaderBackground.swift +83 -0
- package/ios/Application/Navigation/component/HeaderColor.swift +65 -0
- package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +11 -0
- package/ios/Application/Navigation/component/NavigationFooter.swift +90 -0
- package/ios/Application/Navigation/component/NavigationHeaderRight.swift +196 -0
- package/ios/Application/Screen.swift +250 -0
- package/ios/Application/StackScreen.swift +492 -0
- package/ios/Avatar/Avatar.swift +193 -0
- package/ios/Badge/Badge.swift +84 -0
- package/ios/Badge/BadgeDot.swift +31 -0
- package/ios/Badge/BadgeRibbon.swift +319 -0
- package/ios/BaselineView/BaselineView.swift +163 -0
- package/ios/Button/Button.swift +232 -84
- package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +126 -0
- package/ios/Carousel/Carousel.swift +196 -0
- package/ios/Checkbox/Checkbox.swift +77 -0
- package/ios/Chip/Chip.swift +120 -0
- package/ios/Collapse/Collapse.swift +192 -0
- package/ios/{Colors+Spacing → Colors+Radius+Spacing}/Colors.swift +42 -37
- package/ios/Colors+Radius+Spacing/Radius.swift +22 -0
- package/ios/{Colors+Spacing → Colors+Radius+Spacing}/Spacing.swift +3 -1
- package/ios/Colors+Radius+Spacing/Theme.swift +295 -0
- package/ios/DateTimePicker/DateTimePicker.swift +212 -0
- package/ios/DateTimePicker/DateTimePickerTypes.swift +55 -0
- package/ios/DateTimePicker/DateTimePickerUtils.swift +167 -0
- package/ios/DateTimePicker/WheelPicker.swift +165 -0
- package/ios/Divider/Divider.swift +16 -0
- package/ios/Extensions/ActiveOpacityButtonStyle.swift +14 -0
- package/ios/Extensions/Color++.swift +32 -0
- package/ios/Icon/Icon.swift +69 -0
- package/ios/IconButton/IconButton.swift +140 -0
- package/ios/Image/Image.swift +87 -0
- package/ios/Information/Information.swift +156 -0
- package/ios/Input/ErrorView.swift +58 -0
- package/ios/Input/Input.swift +461 -77
- package/ios/Input/InputPhoneNumber.swift +192 -0
- package/ios/Input/InputSearch.swift +301 -0
- package/ios/Input/InputTextArea.swift +318 -0
- package/ios/InputDropDown/InputDropDown.swift +201 -0
- package/ios/InputMoney/InputMoney.swift +405 -0
- package/ios/InputOTP/InputOTP.swift +211 -0
- package/ios/Layout/GridContext.swift +30 -0
- package/ios/Layout/Item.swift +42 -0
- package/ios/Layout/Section.swift +134 -0
- package/ios/Loader/Loader.swift +113 -0
- package/ios/Lottie/LottieView.swift +86 -0
- package/ios/MoMoUIKits.podspec +5 -0
- package/ios/OTPKeyboard/KeyboardButton.swift +13 -5
- package/ios/OTPKeyboard/OTPKeyboard.swift +1 -1
- package/ios/Pagination/PaginationDot.swift +61 -0
- package/ios/Pagination/PaginationNumber.swift +35 -0
- package/ios/Pagination/PaginationScroll.swift +101 -0
- package/ios/Pagination/PaginationWhiteDot.swift +37 -0
- package/ios/Popup/PopupDisplay.swift +279 -0
- package/ios/Popup/PopupInput.swift +92 -0
- package/ios/Popup/PopupNotify.swift +219 -0
- package/ios/Popup/PopupPromotion.swift +74 -0
- package/ios/PopupView/FullscreenPopup.swift +251 -0
- package/ios/{Popup → PopupView}/Modifiers.swift +6 -20
- package/ios/{Popup → PopupView}/PopupView.swift +38 -146
- package/ios/PopupView/Utils++.swift +281 -0
- package/ios/ProgressInfo/ProgressInfo.swift +297 -0
- package/ios/Radio/Radio.swift +69 -0
- package/ios/Rating/Rating.swift +83 -0
- package/{example/ios/Example/Preview Content/Preview Assets.xcassets/Contents.json → ios/Resources/MoMoUIKitsColors.xcassets/AccentColor.colorset/Contents.json} +5 -0
- package/{example/ios/Example/Assets.xcassets → ios/Resources/MoMoUIKitsColors.xcassets}/Text.colorset/Contents.json +3 -3
- 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/ScrollIndicator/ScrollIndicator.swift +110 -0
- package/ios/Skeleton/Skeleton.swift +99 -0
- package/ios/Slider/Slider.swift +237 -0
- package/ios/StatusBarTap/StatusBarTap.h +13 -0
- package/ios/StatusBarTap/StatusBarTap.m +75 -0
- package/ios/Stepper/Stepper.swift +221 -0
- package/ios/Steps/Steps.swift +467 -0
- package/ios/SuggestAction/SuggestAction.swift +100 -0
- package/ios/Swipeable/SwipeCell.swift +226 -31
- package/ios/Swipeable/SwipeCellModel.swift +21 -0
- package/ios/Switch/Switch.swift +69 -0
- package/ios/TabView/TabView.swift +534 -0
- package/ios/Tag/Tag.swift +99 -0
- package/ios/Template/Logo/Logo.swift +75 -0
- package/ios/Template/TrustBanner/TrustBanner.swift +123 -0
- package/ios/Title/Title.swift +270 -0
- package/ios/Tooltip/Tooltip.swift +486 -0
- package/ios/Typography/FontScale.swift +103 -0
- package/ios/Typography/FontScaleStore.swift +58 -0
- package/ios/Typography/KitDesignSystem.swift +17 -0
- package/ios/Typography/Text.swift +262 -0
- package/ios/Typography/Typography.swift +80 -23
- package/ios/Uploader/Uploader.swift +174 -0
- package/ios/native-kits.podspec +73 -13
- package/ios-demo/MoMoUIKitsDemo.podspec +20 -0
- package/ios-demo/Resources/dot_loading.json +558 -0
- package/ios-demo/Resources/icon.json +4052 -0
- package/ios-demo/Resources/lottie_circle_loader.json +1 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/DemoScaffold.swift +48 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsBadgeDemoView.swift +25 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsButtonDemoView.swift +26 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsCheckboxDemoView.swift +19 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsChipDemoView.swift +33 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsInputDemoView.swift +48 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsPopupDemoView.swift +21 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsRadioDemoView.swift +17 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/Demos/UIKitsTypographyDemoView.swift +28 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/MoMoUIKitsDemoContext.swift +31 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/MoMoUIKitsDemoFactory.swift +24 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/AnimatedHeaderDemo.swift +110 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/AvatarDemo.swift +107 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/BadgeDemo.swift +75 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/BaselineView.swift +117 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/BottomTabDemo.swift +89 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ButtonDemo.swift +108 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/CarouselDemo.swift +96 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/CheckboxDemo.swift +60 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ChipDemo.swift +76 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/CollapseDemo.swift +102 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +107 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DateTimePickerDemo.swift +100 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DemoScreen.swift +420 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DividerDemo.swift +56 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/HeaderUserDemo.swift +49 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +276 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IconButtonDemo.swift +93 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IconDemo.swift +81 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ImageDemo.swift +65 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InformationDemo.swift +96 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputDemo.swift +117 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputDropDownDemo.swift +94 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputMoneyDemo.swift +94 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputOTPDemo.swift +88 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputPhoneNumberDemo.swift +120 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputSearchDemo.swift +85 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/InputTextAreaDemo.swift +87 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/LoaderDemo.swift +73 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/LocalizeDemo.swift +235 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +241 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PaginationDemo.swift +68 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PlaygroundData.swift +76 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupDisplayDemo.swift +175 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupNotifyDemo.swift +95 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupPromotionDemo.swift +67 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ProgressInfoDemo.swift +101 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/RadioDemo.swift +64 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/RatingDemo.swift +68 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SectionDemo.swift +83 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SkeletonDemo.swift +61 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SliderDemo.swift +90 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SnackBarDemo.swift +82 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/StepperDemo.swift +93 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/StepsDemo.swift +114 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SuggestActionDemo.swift +90 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SwipeDemo.swift +107 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/SwitchDemo.swift +58 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TabViewDemo.swift +114 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TagDemo.swift +86 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ThemeDemo.swift +181 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TitleDemo.swift +104 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TooltipDemo.swift +118 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/TypographyDemo.swift +87 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/UploaderDemo.swift +85 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SwiftUIDemoBrowserView.swift +53 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/UIKitsDemoModels.swift +15 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/UIKitsDemoRegistry.swift +73 -0
- package/package.json +6 -6
- package/publish.sh +554 -20
- package/scripts/gen-ios-color.mjs +136 -0
- package/CODE_OF_CONDUCT.md +0 -133
- package/CONTRIBUTING.md +0 -114
- package/LICENSE +0 -20
- package/android/build.gradle +0 -82
- package/android/gradle.properties +0 -5
- package/android/src/main/AndroidManifest.xml +0 -4
- package/android/src/main/java/com/momoplatform/nativekits/NativeKitsPackage.kt +0 -17
- package/android/src/main/java/com/momoplatform/nativekits/NativeKitsViewManager.kt +0 -20
- package/example/ios/Example/Assets.xcassets/AppIcon.appiconset/Contents.json +0 -63
- package/example/ios/Example/ComponentDetail.swift +0 -35
- package/example/ios/Example/ComponentItem.swift +0 -24
- package/example/ios/Example/ComponentModel.swift +0 -33
- package/example/ios/Example/ContentView.swift +0 -53
- package/example/ios/Example/Example.entitlements +0 -10
- package/example/ios/Example/Foundation/ButtonDemo.swift +0 -80
- package/example/ios/Example/Foundation/Popup/ActionSheets.swift +0 -136
- package/example/ios/Example/Foundation/Popup/Floats.swift +0 -127
- package/example/ios/Example/Foundation/Popup/Popups.swift +0 -101
- package/example/ios/Example/Foundation/PopupDemo.swift +0 -366
- package/example/ios/Example/Foundation/TextInputDemo.swift +0 -31
- package/example/ios/Example/Foundation/TypographyDemo.swift +0 -53
- package/example/ios/Example/Foundation.swift +0 -26
- package/example/ios/Example/Info.plist +0 -18
- package/example/ios/Example/Library/SwipeableDemo.swift +0 -105
- package/example/ios/Example/Library.swift +0 -26
- package/example/ios/Example/Main.swift +0 -11
- package/example/ios/Example/Overview.swift +0 -20
- package/example/ios/Example/Resource/SFProText-Black.ttf +0 -0
- package/example/ios/Example/Resource/SFProText-Bold.ttf +0 -0
- package/example/ios/Example/Resource/SFProText-Heavy.ttf +0 -0
- package/example/ios/Example/Resource/SFProText-Light.ttf +0 -0
- package/example/ios/Example/Resource/SFProText-Medium.ttf +0 -0
- package/example/ios/Example/Resource/SFProText-Regular.ttf +0 -0
- package/example/ios/Example/Resource/SFProText-Semibold.ttf +0 -0
- package/example/ios/Example/Resource/SFProText-Thin.ttf +0 -0
- package/example/ios/Example/Resource/SFProText-Ultralight.ttf +0 -0
- package/example/ios/Example/Resource/foundation.json +0 -33
- package/example/ios/Example/Resource/library.json +0 -8
- package/example/ios/Example/Utils.swift +0 -30
- package/example/ios/Example/ViewModifier/DarkViewModifier.swift +0 -22
- package/example/ios/Example.xcodeproj/project.pbxproj +0 -570
- package/example/ios/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
- package/example/ios/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
- package/example/ios/Example.xcodeproj/project.xcworkspace/xcuserdata/wem.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- package/example/ios/Example.xcodeproj/xcuserdata/wem.xcuserdatad/xcschemes/xcschememanagement.plist +0 -14
- package/example/ios/Example.xcworkspace/contents.xcworkspacedata +0 -10
- package/example/ios/Example.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
- package/example/ios/Example.xcworkspace/xcuserdata/wem.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- package/example/ios/Example.xcworkspace/xcuserdata/wem.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +0 -6
- package/example/ios/Podfile +0 -6
- package/example/ios/Podfile.lock +0 -16
- package/example/ios/Pods/Local Podspecs/MoMoUIKits.podspec.json +0 -29
- package/example/ios/Pods/Manifest.lock +0 -16
- package/example/ios/Pods/Pods.xcodeproj/project.pbxproj +0 -661
- package/example/ios/Pods/Pods.xcodeproj/xcuserdata/wem.xcuserdatad/xcschemes/MoMoUIKits.xcscheme +0 -58
- package/example/ios/Pods/Pods.xcodeproj/xcuserdata/wem.xcuserdatad/xcschemes/Pods-Example.xcscheme +0 -58
- package/example/ios/Pods/Pods.xcodeproj/xcuserdata/wem.xcuserdatad/xcschemes/xcschememanagement.plist +0 -25
- package/example/ios/Pods/Target Support Files/MoMoUIKits/MoMoUIKits-dummy.m +0 -5
- package/example/ios/Pods/Target Support Files/MoMoUIKits/MoMoUIKits-prefix.pch +0 -12
- package/example/ios/Pods/Target Support Files/MoMoUIKits/MoMoUIKits-umbrella.h +0 -16
- package/example/ios/Pods/Target Support Files/MoMoUIKits/MoMoUIKits.debug.xcconfig +0 -12
- package/example/ios/Pods/Target Support Files/MoMoUIKits/MoMoUIKits.modulemap +0 -6
- package/example/ios/Pods/Target Support Files/MoMoUIKits/MoMoUIKits.release.xcconfig +0 -12
- package/example/ios/Pods/Target Support Files/Pods-Example/Pods-Example-acknowledgements.markdown +0 -3
- package/example/ios/Pods/Target Support Files/Pods-Example/Pods-Example-acknowledgements.plist +0 -29
- package/example/ios/Pods/Target Support Files/Pods-Example/Pods-Example-dummy.m +0 -5
- package/example/ios/Pods/Target Support Files/Pods-Example/Pods-Example-umbrella.h +0 -16
- package/example/ios/Pods/Target Support Files/Pods-Example/Pods-Example.debug.xcconfig +0 -14
- package/example/ios/Pods/Target Support Files/Pods-Example/Pods-Example.modulemap +0 -6
- package/example/ios/Pods/Target Support Files/Pods-Example/Pods-Example.release.xcconfig +0 -14
- package/ios/Colors+Spacing/Radius.swift +0 -11
- package/ios/Popup/Utils++.swift +0 -163
- package/ios/Theme.md +0 -18
- /package/{example/ios/Example/Assets.xcassets → ios/Resources/MoMoUIKitsColors.xcassets}/Background.colorset/Contents.json +0 -0
- /package/{example/ios/Example/Assets.xcassets → ios/Resources/MoMoUIKitsColors.xcassets}/Border.colorset/Contents.json +0 -0
- /package/{example/ios/Example/Assets.xcassets → ios/Resources/MoMoUIKitsColors.xcassets}/Card.colorset/Contents.json +0 -0
- /package/{example/ios/Example/Assets.xcassets → ios/Resources/MoMoUIKitsColors.xcassets}/Contents.json +0 -0
- /package/{example/ios/Example/Assets.xcassets → ios/Resources/MoMoUIKitsColors.xcassets}/Error.colorset/Contents.json +0 -0
- /package/{example/ios/Example/Assets.xcassets → ios/Resources/MoMoUIKitsColors.xcassets}/Primary.colorset/Contents.json +0 -0
- /package/{example/ios/Example/Assets.xcassets → ios/Resources/MoMoUIKitsColors.xcassets}/PrimaryDark.colorset/Contents.json +0 -0
- /package/{example/ios/Example/Assets.xcassets → ios/Resources/MoMoUIKitsColors.xcassets}/PrimaryLight.colorset/Contents.json +0 -0
- /package/{example/ios/Example/Assets.xcassets → ios/Resources/MoMoUIKitsColors.xcassets}/Secondary.colorset/Contents.json +0 -0
- /package/{example/ios/Example/Assets.xcassets → ios/Resources/MoMoUIKitsColors.xcassets}/Success.colorset/Contents.json +0 -0
- /package/{example/ios/Example/Assets.xcassets → ios/Resources/MoMoUIKitsColors.xcassets}/TextSecondary.colorset/Contents.json +0 -0
- /package/{example/ios/Example/Assets.xcassets → ios/Resources/MoMoUIKitsColors.xcassets}/Warning.colorset/Contents.json +0 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
//
|
|
2
|
+
// InputMoney.swift
|
|
3
|
+
// MoMoUIKits
|
|
4
|
+
//
|
|
5
|
+
// Native SwiftUI reimplementation of Compose `InputMoney`.
|
|
6
|
+
// The bound `text` holds RAW DIGITS only (mirrors Compose, where the
|
|
7
|
+
// field stores the numeric string and a VisualTransformation renders the
|
|
8
|
+
// thousands-separated currency). Display formatting is applied live.
|
|
9
|
+
//
|
|
10
|
+
|
|
11
|
+
import Foundation
|
|
12
|
+
import SwiftUI
|
|
13
|
+
import UIKit
|
|
14
|
+
|
|
15
|
+
// MARK: - Currency formatting
|
|
16
|
+
|
|
17
|
+
/// Mirrors Compose `formatNumberToMoney` exactly (group separator ".").
|
|
18
|
+
/// Input is a raw-digit string; output has thousands separators + currency.
|
|
19
|
+
public func formatNumberToMoney(_ digits: String, currency: String = "đ") -> String {
|
|
20
|
+
let numeric = digits.filter { $0.isNumber }
|
|
21
|
+
if numeric.isEmpty || numeric == "0" {
|
|
22
|
+
return "0" + currency
|
|
23
|
+
}
|
|
24
|
+
// Strip leading zeros to match Long parsing semantics.
|
|
25
|
+
var trimmed = String(numeric.drop(while: { $0 == "0" }))
|
|
26
|
+
if trimmed.isEmpty { trimmed = "0" }
|
|
27
|
+
if trimmed == "0" {
|
|
28
|
+
return "0" + currency
|
|
29
|
+
}
|
|
30
|
+
if trimmed.count < 3 {
|
|
31
|
+
return trimmed + currency
|
|
32
|
+
}
|
|
33
|
+
var chars: [Character] = []
|
|
34
|
+
var count = 0
|
|
35
|
+
let reversed = Array(trimmed.reversed())
|
|
36
|
+
for i in 0..<reversed.count {
|
|
37
|
+
count += 1
|
|
38
|
+
chars.append(reversed[i])
|
|
39
|
+
// i counts from the least significant digit; original index = len-1-i.
|
|
40
|
+
let originalIndex = reversed.count - 1 - i
|
|
41
|
+
if count == 3 && originalIndex >= 1 {
|
|
42
|
+
chars.append(".")
|
|
43
|
+
count = 0
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return String(chars.reversed()) + currency
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private func isMoneyDigit(_ char: Character) -> Bool { char.isASCII && char.isNumber }
|
|
50
|
+
|
|
51
|
+
/// Applies an edit of `display[start..<end]` → `inserted` to the digits of `display`.
|
|
52
|
+
/// Separators and currency are never deleted: removing a separator deletes the
|
|
53
|
+
/// digit before it instead, removing only the currency leaves the digits untouched. Returns the new digits and the caret as a digit index.
|
|
54
|
+
private func applyMoneyEdit(_ display: String, start: Int, end: Int, inserted: String) -> (digits: String, caretDigit: Int) {
|
|
55
|
+
let chars = Array(display)
|
|
56
|
+
let start = min(start, chars.count)
|
|
57
|
+
let end = min(max(end, start), chars.count)
|
|
58
|
+
let digits = chars.filter(isMoneyDigit)
|
|
59
|
+
var left = chars[..<start].filter(isMoneyDigit).count
|
|
60
|
+
var removed = chars[start..<end].filter(isMoneyDigit).count
|
|
61
|
+
let insertedDigits = inserted.filter(isMoneyDigit)
|
|
62
|
+
if removed == 0 && insertedDigits.isEmpty && left > 0 && chars[start..<end].contains(".") {
|
|
63
|
+
left -= 1
|
|
64
|
+
removed = 1
|
|
65
|
+
}
|
|
66
|
+
if removed == 0 && insertedDigits.isEmpty {
|
|
67
|
+
return (String(digits), left)
|
|
68
|
+
}
|
|
69
|
+
let next = String(digits[..<left]) + insertedDigits + String(digits[(left + removed)...])
|
|
70
|
+
var trimmed = String(next.drop(while: { $0 == "0" }))
|
|
71
|
+
if trimmed.isEmpty { trimmed = String(next.prefix(1)) }
|
|
72
|
+
return (trimmed, max(0, left + insertedDigits.count - (next.count - trimmed.count)))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// Caret offset right after the `digitIndex`-th digit of `display`.
|
|
76
|
+
private func moneyCaret(_ display: String, digitIndex: Int) -> Int {
|
|
77
|
+
var count = 0
|
|
78
|
+
for (index, char) in display.enumerated() where isMoneyDigit(char) {
|
|
79
|
+
count += 1
|
|
80
|
+
if count == digitIndex { return index + 1 }
|
|
81
|
+
}
|
|
82
|
+
return 0
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// MARK: - InputMoney
|
|
86
|
+
|
|
87
|
+
public struct InputMoney: View {
|
|
88
|
+
@Environment(\.appTheme) private var theme
|
|
89
|
+
@Environment(\.momoFontScale) private var fontScale
|
|
90
|
+
@Binding public var text: String
|
|
91
|
+
|
|
92
|
+
public var floatingValue: String
|
|
93
|
+
public var floatingValueColor: Color?
|
|
94
|
+
public var floatingIcon: String
|
|
95
|
+
public var floatingIconColor: Color?
|
|
96
|
+
public var placeholder: String
|
|
97
|
+
public var size: InputSize
|
|
98
|
+
public var error: String
|
|
99
|
+
public var errorSpacing: Bool
|
|
100
|
+
public var disabled: Bool
|
|
101
|
+
public var icon: String
|
|
102
|
+
public var iconColor: Color?
|
|
103
|
+
public var leadingIcon: String
|
|
104
|
+
public var leadingIconColor: Color?
|
|
105
|
+
public var loading: Bool
|
|
106
|
+
public var required: Bool
|
|
107
|
+
public var fontWeight: InputFontWeight
|
|
108
|
+
public var keyboardType: UIKeyboardType
|
|
109
|
+
public var onChangeText: ((String) -> Void)?
|
|
110
|
+
public var onPressIcon: (() -> Void)?
|
|
111
|
+
public var onFocus: (() -> Void)?
|
|
112
|
+
public var onBlur: (() -> Void)?
|
|
113
|
+
|
|
114
|
+
@State private var isFocused: Bool = false
|
|
115
|
+
@State private var floatingLabelHeight: CGFloat = 16
|
|
116
|
+
|
|
117
|
+
public init(
|
|
118
|
+
text: Binding<String>,
|
|
119
|
+
floatingValue: String = "",
|
|
120
|
+
floatingValueColor: Color? = nil,
|
|
121
|
+
floatingIcon: String = "",
|
|
122
|
+
floatingIconColor: Color? = nil,
|
|
123
|
+
placeholder: String = "0đ",
|
|
124
|
+
size: InputSize = .small,
|
|
125
|
+
error: String = "",
|
|
126
|
+
errorSpacing: Bool = false,
|
|
127
|
+
disabled: Bool = false,
|
|
128
|
+
icon: String = "",
|
|
129
|
+
iconColor: Color? = nil,
|
|
130
|
+
leadingIcon: String = "",
|
|
131
|
+
leadingIconColor: Color? = nil,
|
|
132
|
+
loading: Bool = false,
|
|
133
|
+
required: Bool = false,
|
|
134
|
+
fontWeight: InputFontWeight = .bold,
|
|
135
|
+
keyboardType: UIKeyboardType = .numberPad,
|
|
136
|
+
onChangeText: ((String) -> Void)? = nil,
|
|
137
|
+
onPressIcon: (() -> Void)? = nil,
|
|
138
|
+
onFocus: (() -> Void)? = nil,
|
|
139
|
+
onBlur: (() -> Void)? = nil
|
|
140
|
+
) {
|
|
141
|
+
self._text = text
|
|
142
|
+
self.floatingValue = floatingValue
|
|
143
|
+
self.floatingValueColor = floatingValueColor
|
|
144
|
+
self.floatingIcon = floatingIcon
|
|
145
|
+
self.floatingIconColor = floatingIconColor
|
|
146
|
+
self.placeholder = placeholder
|
|
147
|
+
self.size = size
|
|
148
|
+
self.error = error
|
|
149
|
+
self.errorSpacing = errorSpacing
|
|
150
|
+
self.disabled = disabled
|
|
151
|
+
self.icon = icon
|
|
152
|
+
self.iconColor = iconColor
|
|
153
|
+
self.leadingIcon = leadingIcon
|
|
154
|
+
self.leadingIconColor = leadingIconColor
|
|
155
|
+
self.loading = loading
|
|
156
|
+
self.required = required
|
|
157
|
+
self.fontWeight = fontWeight
|
|
158
|
+
self.keyboardType = keyboardType
|
|
159
|
+
self.onChangeText = onChangeText
|
|
160
|
+
self.onPressIcon = onPressIcon
|
|
161
|
+
self.onFocus = onFocus
|
|
162
|
+
self.onBlur = onBlur
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// MARK: - Body
|
|
166
|
+
|
|
167
|
+
public var body: some View {
|
|
168
|
+
VStack(alignment: .leading, spacing: 4) {
|
|
169
|
+
ZStack(alignment: .topLeading) {
|
|
170
|
+
// Floating label
|
|
171
|
+
if !floatingValue.isEmpty || !floatingIcon.isEmpty {
|
|
172
|
+
HStack(spacing: Spacing.XS) {
|
|
173
|
+
MomoText(floatingValue, typography: .labelSMedium, color: floatingTitleColor)
|
|
174
|
+
if required {
|
|
175
|
+
MomoText("*", typography: .labelSMedium, color: theme.colors.error.primary)
|
|
176
|
+
}
|
|
177
|
+
if !floatingIcon.isEmpty {
|
|
178
|
+
Icon(source: floatingIcon, size: 16, color: floatingIconTintColor)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
.padding(.horizontal, Spacing.S)
|
|
182
|
+
.background(
|
|
183
|
+
GeometryReader { geo in
|
|
184
|
+
Color.clear.onAppear { floatingLabelHeight = geo.size.height }
|
|
185
|
+
}
|
|
186
|
+
)
|
|
187
|
+
.background(theme.colors.background.surface)
|
|
188
|
+
// Kotlin centers the label within the box-height-tall decorationBox by
|
|
189
|
+
// default, then applies `offsetY = -size.values.height / 2` — net effect:
|
|
190
|
+
// the label ends up centered exactly on the box's top border. Our ZStack
|
|
191
|
+
// anchors top-leading instead, so matching that requires offsetting by
|
|
192
|
+
// half the label's OWN measured height, not half the box height.
|
|
193
|
+
.offset(x: Spacing.S, y: -floatingLabelHeight / 2)
|
|
194
|
+
.zIndex(10)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Input container
|
|
198
|
+
HStack(alignment: .center, spacing: 0) {
|
|
199
|
+
// Leading icon
|
|
200
|
+
if !leadingIcon.isEmpty {
|
|
201
|
+
Icon(source: leadingIcon, size: size == .small ? 24 : 32, color: leadingIconTintColor)
|
|
202
|
+
.padding(.trailing, Spacing.XS)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Money field
|
|
206
|
+
ZStack(alignment: .leading) {
|
|
207
|
+
if text.filter({ $0.isNumber }).isEmpty {
|
|
208
|
+
MomoText(
|
|
209
|
+
placeholder,
|
|
210
|
+
typography: fontWeight == .bold ? .actionSBold : .bodyDefaultRegular,
|
|
211
|
+
color: placeholderColor
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
MoneyTextField(
|
|
215
|
+
digits: $text,
|
|
216
|
+
keyboardType: keyboardType,
|
|
217
|
+
fontWeight: fontWeight == .bold ? .bold : .regular,
|
|
218
|
+
textColor: UIColor(textColor),
|
|
219
|
+
cursorColor: UIColor(theme.colors.primary),
|
|
220
|
+
isDisabled: disabled,
|
|
221
|
+
onFocusChange: { focused in handleFocusChange(focused) },
|
|
222
|
+
onChangeText: onChangeText
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
226
|
+
|
|
227
|
+
// Clear button (focused + has value)
|
|
228
|
+
if isFocused && !text.filter({ $0.isNumber }).isEmpty {
|
|
229
|
+
SwiftUI.Button(action: {
|
|
230
|
+
text = ""
|
|
231
|
+
onChangeText?("")
|
|
232
|
+
}) {
|
|
233
|
+
Icon(source: "24_navigation_close_circle_full", size: 16, color: theme.colors.text.hint)
|
|
234
|
+
.padding(.leading, Spacing.XS)
|
|
235
|
+
.accessibility(identifier: "ic_clear")
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Loading indicator and right icon can both render (mirrors
|
|
240
|
+
// Compose's two independent `if` blocks, not if/else).
|
|
241
|
+
if loading {
|
|
242
|
+
ActivityIndicator(isAnimating: .constant(true), style: .medium)
|
|
243
|
+
.frame(width: 16, height: 16)
|
|
244
|
+
.padding(.leading, Spacing.S)
|
|
245
|
+
}
|
|
246
|
+
if !icon.isEmpty {
|
|
247
|
+
SwiftUI.Button(action: { onPressIcon?() }) {
|
|
248
|
+
Icon(source: icon, size: 24, color: iconTintColor)
|
|
249
|
+
.padding(.leading, Spacing.S)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
.padding(.horizontal, Spacing.M)
|
|
254
|
+
.frame(height: fontScale(size == .small ? 48 : 56))
|
|
255
|
+
.background(
|
|
256
|
+
RoundedRectangle(cornerRadius: Radius.S)
|
|
257
|
+
.fill(theme.colors.background.surface)
|
|
258
|
+
)
|
|
259
|
+
.overlay(
|
|
260
|
+
RoundedRectangle(cornerRadius: Radius.S)
|
|
261
|
+
.stroke(borderColor, lineWidth: 1)
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
ErrorView(errorMessage: error, errorSpacing: errorSpacing, hintText: "")
|
|
266
|
+
}
|
|
267
|
+
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { _ in
|
|
268
|
+
if isFocused {
|
|
269
|
+
DispatchQueue.main.async { handleFocusChange(false) }
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// MARK: - Helpers
|
|
275
|
+
|
|
276
|
+
private func handleFocusChange(_ focused: Bool) {
|
|
277
|
+
guard isFocused != focused else { return }
|
|
278
|
+
isFocused = focused
|
|
279
|
+
if focused { onFocus?() } else { onBlur?() }
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private var borderColor: Color {
|
|
283
|
+
if disabled { return theme.colors.border.disable }
|
|
284
|
+
if !error.isEmpty { return theme.colors.error.primary }
|
|
285
|
+
if isFocused { return theme.colors.primary }
|
|
286
|
+
return theme.colors.border.default
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
private var textColor: Color { disabled ? theme.colors.text.disable : theme.colors.text.default }
|
|
290
|
+
private var placeholderColor: Color { disabled ? theme.colors.text.disable : theme.colors.text.hint }
|
|
291
|
+
private var iconTintColor: Color { disabled ? theme.colors.text.disable : (iconColor ?? theme.colors.text.default) }
|
|
292
|
+
private var leadingIconTintColor: Color { disabled ? theme.colors.text.disable : (leadingIconColor ?? theme.colors.text.hint) }
|
|
293
|
+
private var floatingTitleColor: Color { disabled ? theme.colors.text.disable : (floatingValueColor ?? theme.colors.text.hint) }
|
|
294
|
+
private var floatingIconTintColor: Color { disabled ? theme.colors.text.disable : (floatingIconColor ?? theme.colors.text.default) }
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// MARK: - MoneyTextField (UIViewRepresentable)
|
|
298
|
+
|
|
299
|
+
/// A UITextField that shows live thousands-separated currency while the bound
|
|
300
|
+
/// `digits` string holds only the raw numeric value. The caret can move freely
|
|
301
|
+
/// but never past the trailing currency symbol; edits apply at the caret.
|
|
302
|
+
private struct MoneyTextField: UIViewRepresentable {
|
|
303
|
+
@Binding var digits: String
|
|
304
|
+
var keyboardType: UIKeyboardType
|
|
305
|
+
var fontWeight: UIFont.Weight
|
|
306
|
+
var textColor: UIColor
|
|
307
|
+
var cursorColor: UIColor
|
|
308
|
+
var isDisabled: Bool
|
|
309
|
+
var onFocusChange: (Bool) -> Void
|
|
310
|
+
var onChangeText: ((String) -> Void)?
|
|
311
|
+
|
|
312
|
+
func makeCoordinator() -> Coordinator { Coordinator(self) }
|
|
313
|
+
|
|
314
|
+
func makeUIView(context: Context) -> UITextField {
|
|
315
|
+
let field = UITextField()
|
|
316
|
+
field.delegate = context.coordinator
|
|
317
|
+
field.keyboardType = keyboardType
|
|
318
|
+
field.font = UIFont.systemFont(ofSize: context.environment.momoFontScale(20), weight: fontWeight)
|
|
319
|
+
field.textColor = textColor
|
|
320
|
+
field.isEnabled = !isDisabled
|
|
321
|
+
field.tintColor = cursorColor
|
|
322
|
+
field.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
323
|
+
field.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
324
|
+
field.text = displayValue(digits)
|
|
325
|
+
return field
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
func updateUIView(_ field: UITextField, context: Context) {
|
|
329
|
+
context.coordinator.parent = self
|
|
330
|
+
// Re-applied here, not just in makeUIView, so a font-scale change reaches an existing field.
|
|
331
|
+
field.font = UIFont.systemFont(ofSize: context.environment.momoFontScale(20), weight: fontWeight)
|
|
332
|
+
field.isEnabled = !isDisabled
|
|
333
|
+
field.textColor = textColor
|
|
334
|
+
field.tintColor = cursorColor
|
|
335
|
+
let expected = displayValue(digits)
|
|
336
|
+
if field.text != expected {
|
|
337
|
+
field.text = expected
|
|
338
|
+
context.coordinator.setCaret(field, offset: moneyCaret(expected, digitIndex: expected.filter(isMoneyDigit).count))
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
fileprivate func displayValue(_ raw: String) -> String {
|
|
343
|
+
let numeric = raw.filter { $0.isNumber }
|
|
344
|
+
return numeric.isEmpty ? "" : formatNumberToMoney(numeric)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
final class Coordinator: NSObject, UITextFieldDelegate {
|
|
348
|
+
var parent: MoneyTextField
|
|
349
|
+
|
|
350
|
+
init(_ parent: MoneyTextField) { self.parent = parent }
|
|
351
|
+
|
|
352
|
+
func textFieldDidBeginEditing(_ textField: UITextField) {
|
|
353
|
+
parent.onFocusChange(true)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
func textFieldDidEndEditing(_ textField: UITextField) {
|
|
357
|
+
parent.onFocusChange(false)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
|
361
|
+
textField.resignFirstResponder()
|
|
362
|
+
return true
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
func textField(
|
|
366
|
+
_ textField: UITextField,
|
|
367
|
+
shouldChangeCharactersIn range: NSRange,
|
|
368
|
+
replacementString string: String
|
|
369
|
+
) -> Bool {
|
|
370
|
+
let (numeric, caretDigit) = applyMoneyEdit(
|
|
371
|
+
textField.text ?? "",
|
|
372
|
+
start: range.location,
|
|
373
|
+
end: range.location + range.length,
|
|
374
|
+
inserted: string
|
|
375
|
+
)
|
|
376
|
+
let formatted = parent.displayValue(numeric)
|
|
377
|
+
textField.text = formatted
|
|
378
|
+
setCaret(textField, offset: moneyCaret(formatted, digitIndex: caretDigit))
|
|
379
|
+
if parent.digits != numeric {
|
|
380
|
+
parent.digits = numeric
|
|
381
|
+
parent.onChangeText?(numeric)
|
|
382
|
+
}
|
|
383
|
+
return false
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Keeps the caret out of the currency suffix.
|
|
387
|
+
func textFieldDidChangeSelection(_ textField: UITextField) {
|
|
388
|
+
guard let text = textField.text, !text.isEmpty,
|
|
389
|
+
let range = textField.selectedTextRange,
|
|
390
|
+
let limit = textField.position(from: textField.endOfDocument, offset: -1)
|
|
391
|
+
else { return }
|
|
392
|
+
let start = textField.compare(range.start, to: limit) == .orderedDescending ? limit : range.start
|
|
393
|
+
let end = textField.compare(range.end, to: limit) == .orderedDescending ? limit : range.end
|
|
394
|
+
if start != range.start || end != range.end {
|
|
395
|
+
textField.selectedTextRange = textField.textRange(from: start, to: end)
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
func setCaret(_ textField: UITextField, offset: Int) {
|
|
400
|
+
if let position = textField.position(from: textField.beginningOfDocument, offset: offset) {
|
|
401
|
+
textField.selectedTextRange = textField.textRange(from: position, to: position)
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import SwiftUI
|
|
3
|
+
|
|
4
|
+
// MARK: - OTPCaret
|
|
5
|
+
|
|
6
|
+
/// Blinking caret shown at the active OTP cell, mirroring Compose `OTPCaret`.
|
|
7
|
+
private struct OTPCaret: View {
|
|
8
|
+
@Environment(\.appTheme) private var theme
|
|
9
|
+
@State private var visible: Bool = true
|
|
10
|
+
@Environment(\.momoFontScale) private var fontScale
|
|
11
|
+
|
|
12
|
+
var body: some View {
|
|
13
|
+
HStack(spacing: 0) {
|
|
14
|
+
Rectangle()
|
|
15
|
+
.fill(theme.colors.primary)
|
|
16
|
+
.frame(width: 1, height: fontScale(12))
|
|
17
|
+
.opacity(visible ? 1 : 0)
|
|
18
|
+
.animation(
|
|
19
|
+
.linear(duration: 0.5).repeatForever(autoreverses: true),
|
|
20
|
+
value: visible
|
|
21
|
+
)
|
|
22
|
+
MomoText("-", typography: .descriptionDefaultRegular, color: theme.colors.text.hint)
|
|
23
|
+
}
|
|
24
|
+
.onAppear { visible = false }
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// MARK: - InputOTP
|
|
29
|
+
|
|
30
|
+
/// Native SwiftUI port of Compose `InputOTP`.
|
|
31
|
+
///
|
|
32
|
+
/// A row of OTP cells driven by a hidden `TextField`. When `length` is provided
|
|
33
|
+
/// it renders fixed-count slots (`-` placeholders + blinking caret); otherwise it
|
|
34
|
+
/// renders the typed characters followed by a trailing caret while focused.
|
|
35
|
+
public struct InputOTP: View {
|
|
36
|
+
@Environment(\.appTheme) private var theme
|
|
37
|
+
@Binding public var value: String
|
|
38
|
+
|
|
39
|
+
public var length: Int?
|
|
40
|
+
public var errorMessage: String
|
|
41
|
+
public var errorSpacing: Bool
|
|
42
|
+
public var floatingValue: String
|
|
43
|
+
public var placeholder: String
|
|
44
|
+
public var dataType: String
|
|
45
|
+
public var onChangeText: ((String) -> Void)?
|
|
46
|
+
public var onFocus: (() -> Void)?
|
|
47
|
+
public var onBlur: (() -> Void)?
|
|
48
|
+
|
|
49
|
+
@FocusState private var isFocused: Bool
|
|
50
|
+
@State private var hasBeenFocused: Bool = false
|
|
51
|
+
|
|
52
|
+
private var maxLength: Int { length ?? 10 }
|
|
53
|
+
|
|
54
|
+
public init(
|
|
55
|
+
value: Binding<String>,
|
|
56
|
+
length: Int? = nil,
|
|
57
|
+
errorMessage: String = "",
|
|
58
|
+
errorSpacing: Bool = false,
|
|
59
|
+
floatingValue: String = "Label",
|
|
60
|
+
placeholder: String = "",
|
|
61
|
+
dataType: String = "number",
|
|
62
|
+
onChangeText: ((String) -> Void)? = nil,
|
|
63
|
+
onFocus: (() -> Void)? = nil,
|
|
64
|
+
onBlur: (() -> Void)? = nil
|
|
65
|
+
) {
|
|
66
|
+
self._value = value
|
|
67
|
+
self.length = length
|
|
68
|
+
self.errorMessage = errorMessage
|
|
69
|
+
self.errorSpacing = errorSpacing
|
|
70
|
+
self.floatingValue = floatingValue
|
|
71
|
+
self.placeholder = placeholder
|
|
72
|
+
self.dataType = dataType
|
|
73
|
+
self.onChangeText = onChangeText
|
|
74
|
+
self.onFocus = onFocus
|
|
75
|
+
self.onBlur = onBlur
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
public var body: some View {
|
|
79
|
+
VStack(alignment: .leading, spacing: 0) {
|
|
80
|
+
ZStack {
|
|
81
|
+
// Hidden text field that owns the keyboard / cursor.
|
|
82
|
+
hiddenField
|
|
83
|
+
|
|
84
|
+
// Floating label over the border.
|
|
85
|
+
if !floatingValue.isEmpty {
|
|
86
|
+
HStack {
|
|
87
|
+
MomoText(floatingValue, typography: .labelSMedium, color: theme.colors.text.hint)
|
|
88
|
+
}
|
|
89
|
+
.padding(.horizontal, Spacing.S)
|
|
90
|
+
.background(theme.colors.background.surface)
|
|
91
|
+
.offset(y: -28)
|
|
92
|
+
.zIndex(10)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Bordered container with cells.
|
|
96
|
+
cells
|
|
97
|
+
.frame(maxWidth: .infinity)
|
|
98
|
+
.frame(height: 56)
|
|
99
|
+
.overlay(
|
|
100
|
+
RoundedRectangle(cornerRadius: Radius.S)
|
|
101
|
+
.stroke(borderColor, lineWidth: 1)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
// Trailing clear button.
|
|
105
|
+
HStack {
|
|
106
|
+
Spacer()
|
|
107
|
+
SwiftUI.Button(action: clearText) {
|
|
108
|
+
Icon(source: "24_navigation_close_circle_full", size: 12, color: theme.colors.text.hint)
|
|
109
|
+
.padding(.trailing, 12)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
.zIndex(10)
|
|
113
|
+
}
|
|
114
|
+
.frame(height: 56)
|
|
115
|
+
.contentShape(Rectangle())
|
|
116
|
+
.onTapGesture { isFocused = true }
|
|
117
|
+
|
|
118
|
+
ErrorView(errorMessage: errorMessage, errorSpacing: errorSpacing, hintText: "")
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// MARK: Subviews
|
|
123
|
+
|
|
124
|
+
private var hiddenField: some View {
|
|
125
|
+
TextField("", text: Binding(
|
|
126
|
+
get: { value },
|
|
127
|
+
set: { handleChangeText($0) }
|
|
128
|
+
))
|
|
129
|
+
.keyboardType(dataType == "number" ? .numberPad : .asciiCapable)
|
|
130
|
+
.focused($isFocused)
|
|
131
|
+
.foregroundColor(.clear)
|
|
132
|
+
.accentColor(.clear)
|
|
133
|
+
.frame(maxWidth: .infinity)
|
|
134
|
+
.frame(height: 56)
|
|
135
|
+
.opacity(0.01)
|
|
136
|
+
.onChange(of: isFocused) { focused in
|
|
137
|
+
if focused {
|
|
138
|
+
onFocus?()
|
|
139
|
+
hasBeenFocused = true
|
|
140
|
+
} else if hasBeenFocused {
|
|
141
|
+
onBlur?()
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@ViewBuilder
|
|
147
|
+
private var cells: some View {
|
|
148
|
+
if let length = length {
|
|
149
|
+
HStack(spacing: Spacing.L) {
|
|
150
|
+
ForEach(0 ..< length, id: \.self) { i in
|
|
151
|
+
cell(at: i)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
.padding(.vertical, 8)
|
|
155
|
+
} else {
|
|
156
|
+
HStack(spacing: Spacing.L) {
|
|
157
|
+
if value.isEmpty && !isFocused && !placeholder.isEmpty {
|
|
158
|
+
MomoText(placeholder, typography: .descriptionDefaultRegular, color: theme.colors.text.hint)
|
|
159
|
+
} else {
|
|
160
|
+
ForEach(Array(value.enumerated()), id: \.offset) { _, ch in
|
|
161
|
+
MomoText(String(ch), typography: .headerDefaultBold, color: theme.colors.text.default)
|
|
162
|
+
}
|
|
163
|
+
if isFocused && value.count != maxLength {
|
|
164
|
+
OTPCaret()
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
.padding(.vertical, 8)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
@ViewBuilder
|
|
173
|
+
private func cell(at index: Int) -> some View {
|
|
174
|
+
if isFocused && value.count == index {
|
|
175
|
+
OTPCaret()
|
|
176
|
+
} else {
|
|
177
|
+
let chars = Array(value)
|
|
178
|
+
let filled = index < chars.count
|
|
179
|
+
MomoText(
|
|
180
|
+
filled ? String(chars[index]) : "-",
|
|
181
|
+
typography: filled ? .headerDefaultBold : .descriptionDefaultRegular,
|
|
182
|
+
color: filled ? theme.colors.text.default : theme.colors.text.hint
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// MARK: Logic
|
|
188
|
+
|
|
189
|
+
private func handleChangeText(_ text: String) {
|
|
190
|
+
let validData = dataType != "number" || text.allSatisfy { $0.isNumber }
|
|
191
|
+
let changed = text.count != value.count
|
|
192
|
+
if text.count <= maxLength && validData && changed {
|
|
193
|
+
value = text
|
|
194
|
+
onChangeText?(text)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private func clearText() {
|
|
199
|
+
handleChangeText("")
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private var borderColor: Color {
|
|
203
|
+
if !errorMessage.isEmpty {
|
|
204
|
+
return theme.colors.error.primary
|
|
205
|
+
}
|
|
206
|
+
if isFocused {
|
|
207
|
+
return theme.colors.primary
|
|
208
|
+
}
|
|
209
|
+
return theme.colors.border.default
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
/// Grid metrics published by a layout container (`SectionView`) so span-based children
|
|
4
|
+
/// can size themselves. Mirrors `vn.momo.kits.layout.GridContext`.
|
|
5
|
+
public struct GridContext: Equatable {
|
|
6
|
+
public let numberOfColumns: Int
|
|
7
|
+
public let gutter: CGFloat
|
|
8
|
+
public let sizePerSpan: CGFloat
|
|
9
|
+
|
|
10
|
+
public init(numberOfColumns: Int = 12, gutter: CGFloat = Spacing.M, sizePerSpan: CGFloat = 0) {
|
|
11
|
+
self.numberOfColumns = numberOfColumns
|
|
12
|
+
self.gutter = gutter
|
|
13
|
+
self.sizePerSpan = sizePerSpan
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public func sizeForSpan(_ span: Int) -> CGFloat {
|
|
17
|
+
sizePerSpan * CGFloat(span) + gutter * CGFloat(span - 1)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private struct GridContextKey: EnvironmentKey {
|
|
22
|
+
static let defaultValue: GridContext? = nil
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public extension EnvironmentValues {
|
|
26
|
+
var gridContext: GridContext? {
|
|
27
|
+
get { self[GridContextKey.self] }
|
|
28
|
+
set { self[GridContextKey.self] = newValue }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
/// Span-sized cell inside a `SectionView`. Reads the grid published by its container;
|
|
4
|
+
/// without one it just wraps its content. Mirrors `vn.momo.kits.layout.Item`.
|
|
5
|
+
public struct ItemView<Content: View>: View {
|
|
6
|
+
// MARK: Lifecycle
|
|
7
|
+
|
|
8
|
+
public init(
|
|
9
|
+
widthSpan: Int = 12,
|
|
10
|
+
heightSpan: Int = 0,
|
|
11
|
+
@ViewBuilder content: @escaping () -> Content
|
|
12
|
+
) {
|
|
13
|
+
self.widthSpan = widthSpan
|
|
14
|
+
self.heightSpan = heightSpan
|
|
15
|
+
self.content = content
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// MARK: Public
|
|
19
|
+
|
|
20
|
+
public var body: some View {
|
|
21
|
+
// A VStack (not a ZStack) so multiple children flow VERTICALLY instead of stacking
|
|
22
|
+
// on top of each other — matches the React Item's flex-column content.
|
|
23
|
+
VStack(alignment: .leading, spacing: 0) { content() }
|
|
24
|
+
.frame(width: width, height: height, alignment: .topLeading)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// MARK: Private
|
|
28
|
+
|
|
29
|
+
private let widthSpan: Int
|
|
30
|
+
private let heightSpan: Int
|
|
31
|
+
private let content: () -> Content
|
|
32
|
+
|
|
33
|
+
@Environment(\.gridContext) private var grid
|
|
34
|
+
|
|
35
|
+
private var width: CGFloat? {
|
|
36
|
+
grid.map { $0.sizeForSpan(widthSpan) }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private var height: CGFloat? {
|
|
40
|
+
heightSpan > 0 ? grid.map { $0.sizeForSpan(heightSpan) } : nil
|
|
41
|
+
}
|
|
42
|
+
}
|