@momo-kits/native-kits 0.168.1-beta.2-debug → 0.168.1-beta.23
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 +10 -6
- package/ios/Application/ApplicationEnvironment.swift +7 -6
- package/ios/Application/DesignSystemKits.swift +163 -0
- package/ios/Application/DesignSystemKitsPayload.swift +68 -0
- package/ios/Badge/BadgeRibbon.swift +8 -1
- package/ios/Extensions/Color++.swift +2 -4
- package/ios/Input/Input.swift +13 -3
- package/ios/Input/InputPhoneNumber.swift +1 -3
- package/ios/Input/InputSearch.swift +1 -2
- package/ios/Input/InputTextArea.swift +5 -3
- package/ios/InputMoney/InputMoney.swift +6 -7
- package/ios/Layout/ScreenLayout.swift +65 -10
- package/ios/Navigation/BottomSheet.swift +153 -23
- package/ios/Navigation/BottomTab/DuoBarCompression.swift +2 -3
- package/ios/Navigation/BottomTab/DuoTabBar.swift +94 -69
- package/ios/Navigation/Component/HeaderBackground.swift +22 -1
- package/ios/Navigation/Component/SnackBar.swift +4 -0
- package/ios/Navigation/ModalScreen.swift +7 -3
- package/ios/Navigation/StackScreen.swift +3 -7
- package/ios/Popup/PopupDisplay.swift +2 -2
- package/ios/Popup/PopupInput.swift +2 -2
- package/ios/Popup/PopupLayout.swift +15 -10
- package/ios/Popup/PopupNotify.swift +2 -2
- package/ios/Stepper/Stepper.swift +1 -2
- package/ios/Template/TrustBanner/TrustBanner.swift +4 -2
- package/ios/Typography/FontScale.swift +29 -15
- package/ios/Typography/Text.swift +86 -12
- package/ios/Typography/Typography.swift +2 -2
- package/ios/native-kits.podspec +1 -1
- package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
- package/package.json +1 -1
- package/.claude/settings.local.json +0 -9
- package/build.gradle.kts +0 -11
- package/compose/build.gradle.kts +0 -198
- package/compose/build.gradle.kts.backup +0 -198
- package/compose/compose.podspec +0 -47
- package/compose/src/androidMain/kotlin/vn/momo/kits/const/HostFont.android.kt +0 -19
- package/compose/src/androidMain/kotlin/vn/momo/kits/navigation/ScrollToTop.android.kt +0 -6
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/BackGesture.android.kt +0 -12
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/OsFontScale.android.kt +0 -7
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +0 -153
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +0 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +0 -124
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +0 -1071
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Localize.kt +0 -269
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +0 -56
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Avatar.kt +0 -157
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +0 -81
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeDot.kt +0 -32
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeRibbon.kt +0 -338
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/BaselineView.kt +0 -175
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +0 -357
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +0 -115
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +0 -89
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +0 -128
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Collapse.kt +0 -209
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +0 -542
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +0 -49
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +0 -86
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +0 -125
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +0 -231
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +0 -189
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +0 -478
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +0 -114
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +0 -265
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +0 -235
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +0 -223
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +0 -265
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +0 -185
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +0 -343
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Loader.kt +0 -108
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +0 -56
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationNumber.kt +0 -41
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +0 -84
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationWhiteDot.kt +0 -40
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +0 -361
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +0 -108
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +0 -308
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +0 -59
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Rating.kt +0 -87
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +0 -21
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +0 -90
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +0 -330
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Stepper.kt +0 -220
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Steps.kt +0 -473
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/SuggestAction.kt +0 -122
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +0 -207
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +0 -92
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +0 -560
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +0 -86
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +0 -125
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +0 -207
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +0 -501
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +0 -176
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Uploader.kt +0 -182
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +0 -193
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt +0 -29
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +0 -236
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +0 -180
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Colors.kt +0 -325
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +0 -16
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitDesignSystem.kt +0 -18
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +0 -22
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Radius.kt +0 -12
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +0 -16
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +0 -194
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +0 -283
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +0 -54
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +0 -50
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +0 -309
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +0 -41
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +0 -52
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +0 -66
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Conditional.kt +0 -11
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/DeprecatedModifier.kt +0 -104
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +0 -48
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Size.kt +0 -51
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +0 -338
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/HostBackGestureBridge.kt +0 -58
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +0 -137
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +0 -116
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +0 -205
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +0 -399
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScrollToTop.kt +0 -8
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +0 -732
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackTransition.kt +0 -148
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +0 -35
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +0 -228
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +0 -217
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +0 -86
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +0 -98
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +0 -312
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +0 -169
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +0 -455
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +0 -103
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +0 -106
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +0 -74
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +0 -399
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +0 -39
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +0 -363
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +0 -192
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +0 -133
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +0 -127
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/BackGesture.kt +0 -12
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +0 -91
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/OsFontScale.kt +0 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +0 -74
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +0 -13
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Icons.kt +0 -1329
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +0 -55
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Tracking.kt +0 -15
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +0 -118
- package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +0 -40
- package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/HostBackGesture.ios.kt +0 -45
- package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +0 -32
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/BackGesture.ios.kt +0 -38
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +0 -66
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +0 -217
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +0 -94
- package/gradle/libs.versions.toml +0 -69
- package/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/gradle/wrapper/gradle-wrapper.properties +0 -8
- package/gradle.properties +0 -26
- package/gradlew +0 -252
- package/gradlew.bat +0 -94
- package/ios/Typography/FontScaleStore.swift +0 -58
- package/ios/Typography/KitDesignSystem.swift +0 -17
- package/ios-demo/README.md +0 -120
- package/settings.gradle.kts +0 -64
package/CLAUDE.md
CHANGED
|
@@ -48,7 +48,9 @@ Publishing is automated (`publish.sh` + GitLab CI on `[ci build]` / `main`). Do
|
|
|
48
48
|
|
|
49
49
|
- **Entry point:** wrap content in `NavigationContainer(...)` from `navigation/`. It builds the `Navigator`, takes a host-supplied `Localize` (falls back to an empty one), and provides the CompositionLocals below. The legacy `Screen` / `ApplicationContainer` / `ComposeApi` stack has been removed on both sides — don't reintroduce it.
|
|
50
50
|
- **Configuration flows via CompositionLocals**, not params. Public ones: `AppTheme`, `AppStatusBar`, `AppNavigationBar` (`const/Theme.kt`); `ApplicationContext`, `AppConfig`, `AppLanguage`, `LocalLocalize` (`application/`); `LocalNavigator`, `LocalMaxApi` (`navigation/`). Read with `X.current`.
|
|
51
|
-
-
|
|
51
|
+
- **`DesignSystemKits`** (`application/DesignSystemKits.kt`) is the single app-wide holder for the host-owned design-system inputs: font scale (`userScaleRate` / `useOSScaleRate`), whitelist (`isWhiteList`) and the remote `DesignSystemConfig`, bundled as `DesignSystemKitsConfigs`. The host calls `DesignSystemKits.init(source)` once at startup with its `DesignSystemKitsSource` (sync `getConfigs()` seed + `configsFlow`); `init` is idempotent. Without a source, call `setConfigs(...)` directly. Read with `getConfigs()` (snapshot state, recomposes); non-Compose observers use `configsFlow`.
|
|
52
|
+
- **SwiftUI twin:** `ios/Application/DesignSystemKits.swift` mirrors it field for field (`DesignSystemKitsConfigs` with `designSystemConfig: KitConfig?`, `setConfigs` / `getConfigs`), plus the OS Dynamic Type factor (`osFontScale`). There is no `init(source:)` (Swift reserves `init`): the host observes its own bridge and pushes. A host that can't share Swift types (the KMP bridge) calls `DesignSystemKits.shared.setConfigs(json:)` (`ios/Application/DesignSystemKitsPayload.swift`); the JSON field names are a contract with momo-app's serializer (`HostDesignSystemKits.observeConfigsJson`, payload classes in `DesignSystemData.kt`) — change both together. Invalid JSON is ignored. The SwiftUI kit can't link the Kotlin `kits` framework (a second Kotlin/Native runtime would hold a second, never-initialized singleton), so the render-time logic — `FontScale` formula, Dynamic Type observation — is intentionally duplicated in Swift; only values flow across.
|
|
53
|
+
- **Design-system config** (`trustBanner`, `headerBar`, `headerGradient`): `NavigationContainer` provides it as `LocalDesignSystemConfig` (param `designSystemConfig` → parent → `DesignSystemKits`) and applies `headerBar` as the header image when the theme has none.
|
|
52
54
|
- **Platform code** goes through `expect`/`actual` in `platform/` — never reference Android/iOS APIs from `commonMain` directly.
|
|
53
55
|
- **Native bridge:** `IMaxApi` (`vn.momo.maxapi`, exposed as `LocalMaxApi`) is the host-app bridge (dismiss, tracking, device info, language).
|
|
54
56
|
- Navigation uses a runtime `DynamicScreenRegistry`; overlays (modal/bottom-sheet/snackbar) go through `Navigator` + `OverplayComponentRegistry`.
|
|
@@ -69,7 +71,7 @@ Publishing is automated (`publish.sh` + GitLab CI on `[ci build]` / `main`). Do
|
|
|
69
71
|
## Screen layout / iPhone Duo (port of RN `useScreenLayout`, momo-kits !664)
|
|
70
72
|
|
|
71
73
|
- `layout/ScreenLayout.kt`: `getScreenLayout()` returns window size, raw top/bottom + side insets, `contentWidth`, header geometry, the iPhone Duo `sideBar` and `duoTopInset`. `NavigationContainer` computes it once and provides `LocalScreenLayout`; reactive to fold/unfold and rotation (window size + `WindowInsets`). Metrics (`DuoMetrics`, 72pt iPhone header floor, 20pt status-bar threshold) mirror RN `Layout/utils.ts` — change both together.
|
|
72
|
-
- iPhone-only: everything gates on `isPhoneIdiom()` (`platform/`), never on "iOS", so iPad windows keep the regular layout. Two detectors, as in RN: `isIPhoneDuo` (window aspect < 1.6 or asymmetric side insets) drives layout; `isIPhoneDuoDevice` (
|
|
74
|
+
- iPhone-only: everything gates on `isPhoneIdiom()` (`platform/`), never on "iOS", so iPad windows keep the regular layout. Two detectors, as in RN: `isIPhoneDuo` (window aspect < 1.6 or asymmetric side insets) drives layout; `isIPhoneDuoDevice` (Apple's foldable check: a `.division` reserved region, queried with include-inactive; Swift also latches on `UIHingeInteraction`; or asymmetric side insets, since the outer display reports no hinge) drives popups, modal and bottom sheet.
|
|
73
75
|
- Header height is `headerHeight` (not `AppStatusBar + HEADER_HEIGHT`); the bar row is `headerBarHeight` and starts `headerStatusBarHeight` below the window top. When the status bar is off the top edge (iPhone landscape, iPhone Duo) `headerStatusBarHeight` is 0 so the title stays centered — except on the Duo outer display: in portrait the header is 76pt and the row starts at 14pt so the title centers on the camera punchout; in landscape the row centers on the camera when it sits at the top end of the strip (else on the back button) and the header reaches 8pt below the back button.
|
|
74
76
|
- iPhone Duo inner display in portrait (status bar on top): the header row shares the status bar band — `headerHeight` 94pt (2 × the clock digits' y 47), `headerStatusBarHeight` 0 — and `ScreenLayout.statusBarClearance` keeps it between the clock / battery clusters (from reserved regions, else 128pt on the right). Back and toolkit use the 48pt side bar controls laid out horizontally (`ToolkitLayout.SideBarRow`); the collapsed search and a centered title stay clear of the clusters. Port of momo-kits `90af33ba2`.
|
|
75
77
|
- iPhone Duo with a side strip ≥ 60pt: back + toolkit leave the bar for `HeaderSideBar` (`navigation/component/SideBar.kt`, 48pt glass controls); custom header-right stays in the bar. `sideBar.top` is 170pt in portrait, 120pt on the inner display in landscape (height ≥ 600), 24pt on the outer display in landscape, or below the header when the status bar is on top; UIKit's `statusBarFrame` is useless on Duo. Unlike RN the column is clamped above footer (24pt gap) / rail (12pt gap).
|
|
@@ -78,8 +80,10 @@ Publishing is automated (`publish.sh` + GitLab CI on `[ci build]` / `main`). Do
|
|
|
78
80
|
- Bar compression (`bottomtab/DuoBarCompression.kt`, HIG "Vertical controls"): when toolbar + rail don't fit in the strip, one is compressed — the toolkit into a `navigation_menu` button, the rail into one button showing the current tab — and tapping it swaps them; `BottomTab(compressionBehavior)` picks which starts compressed. If neither fits beside the other compressed, both rest compressed and a tap opens one for a moment. Side bar columns report their full height (0 when the screen puts nothing there) so the decision follows each screen's toolkit.
|
|
79
81
|
- Glass header: on Duo with a side bar, Default/Extended headers without search and with a `ScrollState` let content scroll under the bar (`usesDuoGlassHeader`); the bar fades in a translucent surface + hairline over 16pt (Compose has no backdrop blur — RN's fallback look), the gradient leaves with the content.
|
|
80
82
|
- Quick return header: glass headers and Animated headers (with a `ScrollState`) on the Duo side bar layout slide out with the content and back in on a reverse scroll; a half-shown header settles in or out once the scroll stops. The side bar is drawn apart from the header, so it stays put.
|
|
81
|
-
- Overlays on Duo:
|
|
83
|
+
- Overlays on Duo: the modal sits inside the safe area (all four insets, so clear of the side toolbar strip) and caps its content at `duoModalMaxWidth` / `duoModalMaxHeight` (440 × 640pt, at least XL off the safe area); popups fill that width with radius XL and drop their own side padding (body scrolls, actions stay). Snackbars keep clear of the strip.
|
|
84
|
+
- Bottom sheet on Duo: radius XL, window height less `duoTopInset` (it never passes the side status bar: unlike RN, nothing restyles the clock over it), centered when the short side ≥ 600 (inner display in either orientation, buttons stay in the header row; RN parity). Below that (outer display, landscape too) it floats `Spacing.M` off the side edges, pads header/content/footer by `inset − M`, and a `Title` header moves back / close into 48pt side bar buttons on the strip part of the sheet (HIG: sheet controls go vertical on the outer display) when the strip fits one; they follow the header row while the sheet is short and stay below `sideBar.top` when it is tall. `BottomHeader.Title(onBack)` / `.title(onBack:)` closes the sheet, then runs (RN `leftOptions.onPressIconLeft`).
|
|
82
85
|
- Font scale deliberately uses the short side (`min(w, h)`), unlike RN which reads the window width — landscape must not inflate every font.
|
|
86
|
+
- Font scale measures the **window**, never `UIScreen.main`: on iPhone Duo the main screen stays on one display while the window follows the one in use. Compose iOS `getScreenDimensions()` reads `LocalWindowInfo.containerSize` (recomposes on fold/unfold); SwiftUI `DesignSystemKits` KVO-observes each `UIWindowScene.effectiveGeometry` and republishes `windowShortSide`.
|
|
83
87
|
- `StackScreen` pads content/footer/search/FAB by the side insets and provides an inset-consumed layout to its content; a bottom-tab root stays full width (tab bar + each tab's StackScreen handle insets). Grid columns use `contentWidth`.
|
|
84
88
|
|
|
85
89
|
## i18n / Localize (port of RN `Localize`)
|
|
@@ -104,9 +108,9 @@ The SwiftUI port differs from Compose in one important way: **it follows the hos
|
|
|
104
108
|
|
|
105
109
|
One formula, both platforms: the device-width scale (`width / 375`, capped at `+5pt`) and the OS Dynamic Type scale (capped at `1.5×`) are computed independently and the **larger wins**. When `useOSScaleRate` is false neither applies — the size is just multiplied by `userScaleRate`. Max font scale is fixed at 1.5; the old per-call/per-scope override is deprecated and ignored on both sides.
|
|
106
110
|
|
|
107
|
-
- **Compose** — `const/Typography.kt` (`scaleSize` overloads for `Float`/`Dp`/`TextUnit`/`TextStyle`). The `TextUnit` overload divides by `density.fontScale` so Compose's own `sp` scaling isn't applied twice. Inputs: `LocalContext.current` (`MiniAppContext.userScaleRate` / `useOSScaleRate`, per-field) falling back to
|
|
108
|
-
- **SwiftUI** — `ios/Typography/FontScale.swift`. `FontScale` is an immutable snapshot with `callAsFunction`, so a view reads `@Environment(\.momoFontScale) private var fontScale` and calls `fontScale(16)`. `
|
|
109
|
-
- **Never set `\.momoFontScale` directly** — same trap as `\.localize`. Inject via `.momoFontScale(_:context:)`, which holds
|
|
111
|
+
- **Compose** — `const/Typography.kt` (`scaleSize` overloads for `Float`/`Dp`/`TextUnit`/`TextStyle`). The `TextUnit` overload divides by `density.fontScale` so Compose's own `sp` scaling isn't applied twice. Inputs: `LocalContext.current` (`MiniAppContext.userScaleRate` / `useOSScaleRate`, per-field) falling back to `DesignSystemKits.getConfigs()`. On iOS, `platform/OsFontScale.ios.kt` observes `UIContentSizeCategoryDidChangeNotification` and overrides `LocalDensity` — Compose MP otherwise reads `fontScale` once at `ComposeUIViewController` creation. Wired at `NavigationContainer`.
|
|
112
|
+
- **SwiftUI** — `ios/Typography/FontScale.swift`. `FontScale` is an immutable snapshot with `callAsFunction`, so a view reads `@Environment(\.momoFontScale) private var fontScale` and calls `fontScale(16)`. `DesignSystemKits.shared` is the `ObservableObject` holding the host inputs and the OS factor (it observes `UIContentSizeCategory.didChangeNotification`); the host pushes them with `setConfigs(_:)`.
|
|
113
|
+
- **Never set `\.momoFontScale` directly** — same trap as `\.localize`. Inject via `.momoFontScale(_:context:)`, which holds `DesignSystemKits` as `@ObservedObject`; that is what re-publishes the snapshot. It also injects `\.designSystemKitsConfigs`, which `MomoText` (whitelist font) and `TrustBanner` (fallback after `ApplicationEnvironment.config`) read so a host push re-renders them. `NavigationContainer` does it (passing `MiniAppContext` so the per-mini-app override applies); a bare `UIHostingController` root must too. The environment default reads the live store, so an un-injected subtree still gets correct values — just not live ones.
|
|
110
114
|
- The global `scaleSize(_:)` and `Font.appFont(size:)` remain for call sites that can't read the environment (UIKit bridges, free functions). They are **not** reactive. `Font.header_default_bold` & friends are computed `static var`s for the same reason — as stored `static let`s they froze the scale at first access. `UIViewRepresentable`s should read `context.environment.momoFontScale` instead, and re-apply the font in `updateUIView`, not only in `makeUIView`.
|
|
111
115
|
|
|
112
116
|
## Code conventions
|
|
@@ -17,8 +17,8 @@ public class MiniAppContext {
|
|
|
17
17
|
public var toolkitConfig: [String: Any] = [:]
|
|
18
18
|
public var providerId: String = "momo"
|
|
19
19
|
public var permissions: [[String: Any]] = []
|
|
20
|
-
// Per-mini-app font-scale override; nil falls back to
|
|
21
|
-
// MiniAppContext.userScaleRate / useOSScaleRate, which win per field over
|
|
20
|
+
// Per-mini-app font-scale override; nil falls back to DesignSystemKits. Mirrors Compose's
|
|
21
|
+
// MiniAppContext.userScaleRate / useOSScaleRate, which win per field over DesignSystemKits.
|
|
22
22
|
public var userScaleRate: CGFloat? = nil
|
|
23
23
|
public var useOSScaleRate: Bool? = nil
|
|
24
24
|
|
|
@@ -37,10 +37,11 @@ public class MiniAppContext {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
public var
|
|
43
|
-
public var
|
|
40
|
+
// Value type so DesignSystemKitsConfigs can dedupe host pushes; mirrors Compose's DesignSystemConfig.
|
|
41
|
+
public struct KitConfig: Equatable {
|
|
42
|
+
public var trustBanner: TrustBannerData?
|
|
43
|
+
public var headerBar: String?
|
|
44
|
+
public var headerGradient: String?
|
|
44
45
|
|
|
45
46
|
public init(trustBanner: TrustBannerData? = nil, headerBar: String? = nil, headerGradient: String? = nil) {
|
|
46
47
|
self.trustBanner = trustBanner
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// File: ios/Application/DesignSystemKits.swift
|
|
2
|
+
// Created At: 2026-10-02 14:17:29 +07:00
|
|
3
|
+
// Created By: AI
|
|
4
|
+
// AI Agent: Claude Code
|
|
5
|
+
// Model: claude-opus-5-5
|
|
6
|
+
|
|
7
|
+
// AI-GENERATED START: app-wide design-system holder mirroring Compose's DesignSystemKits
|
|
8
|
+
import SwiftUI
|
|
9
|
+
|
|
10
|
+
/// Host-owned design-system inputs; mirrors Compose's `DesignSystemKitsConfigs` field for field.
|
|
11
|
+
public struct DesignSystemKitsConfigs: Equatable {
|
|
12
|
+
public var userScaleRate: CGFloat
|
|
13
|
+
public var useOSScaleRate: Bool
|
|
14
|
+
public var isWhiteList: Bool
|
|
15
|
+
public var designSystemConfig: KitConfig?
|
|
16
|
+
|
|
17
|
+
public init(
|
|
18
|
+
userScaleRate: CGFloat = 1,
|
|
19
|
+
useOSScaleRate: Bool = true,
|
|
20
|
+
isWhiteList: Bool = false,
|
|
21
|
+
designSystemConfig: KitConfig? = nil
|
|
22
|
+
) {
|
|
23
|
+
self.userScaleRate = userScaleRate
|
|
24
|
+
self.useOSScaleRate = useOSScaleRate
|
|
25
|
+
self.isWhiteList = isWhiteList
|
|
26
|
+
self.designSystemConfig = designSystemConfig
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// App-wide holder for the SwiftUI kit, mirroring Compose's `DesignSystemKits`. The kit is sandboxed from the
|
|
31
|
+
/// host, so the host pushes `setConfigs(_:)` at startup and on every change; the OS Dynamic Type factor is
|
|
32
|
+
/// tracked here. Views re-render on change as long as the subtree got `.momoFontScale()` (`NavigationContainer`
|
|
33
|
+
/// applies it). Swift reserves `init`, so there is no `init(source:)` twin: the host observes and pushes.
|
|
34
|
+
public final class DesignSystemKits: ObservableObject {
|
|
35
|
+
public static let shared = DesignSystemKits()
|
|
36
|
+
|
|
37
|
+
@Published private var configs = DesignSystemKitsConfigs()
|
|
38
|
+
@Published public private(set) var osFontScale: CGFloat = DesignSystemKits.currentOsFontScale()
|
|
39
|
+
// AI-GENERATED START: republish the font scale when the window scene is resized (iPhone Duo fold/unfold)
|
|
40
|
+
@Published public private(set) var windowShortSide: CGFloat = DesignSystemKits.currentWindowShortSide()
|
|
41
|
+
private var geometryObservations: [ObjectIdentifier: NSKeyValueObservation] = [:]
|
|
42
|
+
// AI-GENERATED END: republish the font scale when the window scene is resized (iPhone Duo fold/unfold)
|
|
43
|
+
|
|
44
|
+
private init() {
|
|
45
|
+
NotificationCenter.default.addObserver(
|
|
46
|
+
self,
|
|
47
|
+
selector: #selector(contentSizeCategoryDidChange),
|
|
48
|
+
name: UIContentSizeCategory.didChangeNotification,
|
|
49
|
+
object: nil
|
|
50
|
+
)
|
|
51
|
+
// AI-GENERATED START: republish the font scale when the window scene is resized (iPhone Duo fold/unfold)
|
|
52
|
+
NotificationCenter.default.addObserver(
|
|
53
|
+
self,
|
|
54
|
+
selector: #selector(sceneDidActivate(_:)),
|
|
55
|
+
name: UIScene.didActivateNotification,
|
|
56
|
+
object: nil
|
|
57
|
+
)
|
|
58
|
+
onMain {
|
|
59
|
+
UIApplication.shared.connectedScenes
|
|
60
|
+
.compactMap { $0 as? UIWindowScene }
|
|
61
|
+
.forEach(self.observeGeometry(of:))
|
|
62
|
+
}
|
|
63
|
+
// AI-GENERATED END: republish the font scale when the window scene is resized (iPhone Duo fold/unfold)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public func setConfigs(_ configs: DesignSystemKitsConfigs) {
|
|
67
|
+
onMain {
|
|
68
|
+
// Re-assigning an unchanged value would still publish and invalidate every kit subtree.
|
|
69
|
+
guard self.configs != configs else { return }
|
|
70
|
+
self.configs = configs
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public func getConfigs() -> DesignSystemKitsConfigs {
|
|
75
|
+
configs
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
public var fontScale: FontScale {
|
|
79
|
+
FontScale(
|
|
80
|
+
userScaleRate: configs.userScaleRate,
|
|
81
|
+
useOSScaleRate: configs.useOSScaleRate,
|
|
82
|
+
osFontScale: osFontScale,
|
|
83
|
+
// AI-GENERATED START: republish the font scale when the window scene is resized (iPhone Duo fold/unfold)
|
|
84
|
+
windowShortSide: windowShortSide
|
|
85
|
+
// AI-GENERATED END: republish the font scale when the window scene is resized (iPhone Duo fold/unfold)
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@objc private func contentSizeCategoryDidChange() {
|
|
90
|
+
let next = Self.currentOsFontScale()
|
|
91
|
+
guard next != osFontScale else { return }
|
|
92
|
+
osFontScale = next
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// AI-GENERATED START: republish the font scale when the window scene is resized (iPhone Duo fold/unfold)
|
|
96
|
+
@objc private func sceneDidActivate(_ notification: Notification) {
|
|
97
|
+
guard let scene = notification.object as? UIWindowScene else { return }
|
|
98
|
+
observeGeometry(of: scene)
|
|
99
|
+
refreshWindowShortSide()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private func observeGeometry(of scene: UIWindowScene) {
|
|
103
|
+
guard #available(iOS 16.0, *) else { return }
|
|
104
|
+
let key = ObjectIdentifier(scene)
|
|
105
|
+
guard geometryObservations[key] == nil else { return }
|
|
106
|
+
geometryObservations[key] = scene.observe(\.effectiveGeometry, options: [.new]) { [weak self] _, _ in
|
|
107
|
+
// The window is resized after the scene geometry, so read it on the next turn.
|
|
108
|
+
DispatchQueue.main.async { self?.refreshWindowShortSide() }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private func refreshWindowShortSide() {
|
|
113
|
+
let next = Self.currentWindowShortSide()
|
|
114
|
+
guard next > 0, next != windowShortSide else { return }
|
|
115
|
+
windowShortSide = next
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// UIScreen.main stays on one display on iPhone Duo, so measure the foreground window instead.
|
|
119
|
+
static func currentWindowShortSide() -> CGFloat {
|
|
120
|
+
guard Thread.isMainThread else {
|
|
121
|
+
return min(UIScreen.main.bounds.width, UIScreen.main.bounds.height)
|
|
122
|
+
}
|
|
123
|
+
let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }
|
|
124
|
+
let scene = scenes.first { $0.activationState == .foregroundActive } ?? scenes.first
|
|
125
|
+
let window = scene?.windows.first { $0.isKeyWindow } ?? scene?.windows.first
|
|
126
|
+
let size: CGSize
|
|
127
|
+
if let window, window.bounds.width > 0 {
|
|
128
|
+
size = window.bounds.size
|
|
129
|
+
} else if let scene {
|
|
130
|
+
size = scene.coordinateSpace.bounds.size
|
|
131
|
+
} else {
|
|
132
|
+
size = UIScreen.main.bounds.size
|
|
133
|
+
}
|
|
134
|
+
return min(size.width, size.height)
|
|
135
|
+
}
|
|
136
|
+
// AI-GENERATED END: republish the font scale when the window scene is resized (iPhone Duo fold/unfold)
|
|
137
|
+
|
|
138
|
+
/// Same factor Compose's iOS `ProvideOsFontScale` derives from
|
|
139
|
+
/// `UIFont.preferredFont(forTextStyle: .body).pointSize / 17`, so both iOS surfaces scale alike.
|
|
140
|
+
static func currentOsFontScale() -> CGFloat {
|
|
141
|
+
let base = UIFont.systemFont(ofSize: UIFont.labelFontSize)
|
|
142
|
+
return UIFontMetrics.default.scaledFont(for: base).pointSize / base.pointSize
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// Synchronous when already on main — an async hop would defer the change by a frame.
|
|
146
|
+
private func onMain(_ work: @escaping () -> Void) {
|
|
147
|
+
if Thread.isMainThread { work() } else { DispatchQueue.main.async(execute: work) }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private struct DesignSystemKitsConfigsKey: EnvironmentKey {
|
|
152
|
+
/// Computed, never stored: an un-injected subtree still reads the live holder, just not reactively.
|
|
153
|
+
static var defaultValue: DesignSystemKitsConfigs { DesignSystemKits.shared.getConfigs() }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
public extension EnvironmentValues {
|
|
157
|
+
/// Injected by `.momoFontScale()`; never set it directly (same trap as `\.momoFontScale`).
|
|
158
|
+
var designSystemKitsConfigs: DesignSystemKitsConfigs {
|
|
159
|
+
get { self[DesignSystemKitsConfigsKey.self] }
|
|
160
|
+
set { self[DesignSystemKitsConfigsKey.self] = newValue }
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// AI-GENERATED END: app-wide design-system holder mirroring Compose's DesignSystemKits
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// File: ios/Application/DesignSystemKitsPayload.swift
|
|
2
|
+
// Created At: 2026-10-02 15:40:00 +07:00
|
|
3
|
+
// Created By: AI
|
|
4
|
+
// AI Agent: Claude Code
|
|
5
|
+
// Model: claude-opus-5-5
|
|
6
|
+
|
|
7
|
+
// AI-GENERATED START: JSON entry point for hosts that can't share Swift types with the kit
|
|
8
|
+
import Foundation
|
|
9
|
+
|
|
10
|
+
public extension DesignSystemKits {
|
|
11
|
+
/// Applies configs pushed as JSON by a host that can't share Swift types with the kit (e.g. the KMP bridge).
|
|
12
|
+
/// Invalid JSON is ignored and the current configs are kept.
|
|
13
|
+
func setConfigs(json: String) {
|
|
14
|
+
guard let data = json.data(using: .utf8),
|
|
15
|
+
let payload = try? JSONDecoder().decode(DesignSystemKitsPayload.self, from: data) else { return }
|
|
16
|
+
setConfigs(payload.configs)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Wire format; field names match the host's serializer (momo-app HostDesignSystemKits / DesignSystemData.kt).
|
|
21
|
+
private struct DesignSystemKitsPayload: Decodable {
|
|
22
|
+
let userScaleRate: CGFloat
|
|
23
|
+
let useOSScaleRate: Bool
|
|
24
|
+
let isWhiteList: Bool
|
|
25
|
+
let designSystemConfig: KitConfigPayload?
|
|
26
|
+
|
|
27
|
+
var configs: DesignSystemKitsConfigs {
|
|
28
|
+
DesignSystemKitsConfigs(
|
|
29
|
+
userScaleRate: userScaleRate,
|
|
30
|
+
useOSScaleRate: useOSScaleRate,
|
|
31
|
+
isWhiteList: isWhiteList,
|
|
32
|
+
designSystemConfig: designSystemConfig?.kitConfig
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private struct KitConfigPayload: Decodable {
|
|
38
|
+
let trustBanner: TrustBannerPayload?
|
|
39
|
+
let headerBar: String?
|
|
40
|
+
let headerGradient: String?
|
|
41
|
+
|
|
42
|
+
var kitConfig: KitConfig {
|
|
43
|
+
KitConfig(trustBanner: trustBanner?.trustBannerData, headerBar: headerBar, headerGradient: headerGradient)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private struct TrustBannerPayload: Decodable {
|
|
48
|
+
let content: [String: String]
|
|
49
|
+
let subContent: [String: String]
|
|
50
|
+
let pciImage: String?
|
|
51
|
+
let sslImage: String?
|
|
52
|
+
let urlConfig: String?
|
|
53
|
+
let icons: [String]?
|
|
54
|
+
let momoImage: String?
|
|
55
|
+
|
|
56
|
+
var trustBannerData: TrustBannerData {
|
|
57
|
+
TrustBannerData(
|
|
58
|
+
content: content,
|
|
59
|
+
subContent: subContent,
|
|
60
|
+
pciImage: pciImage,
|
|
61
|
+
momoImage: momoImage,
|
|
62
|
+
sslImage: sslImage,
|
|
63
|
+
icons: icons,
|
|
64
|
+
urlConfig: urlConfig ?? ""
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// AI-GENERATED END: JSON entry point for hosts that can't share Swift types with the kit
|
|
@@ -203,7 +203,9 @@ public struct BadgeRibbon: View {
|
|
|
203
203
|
/// rotation) — the label gets the same scale applied a second time so it stays right-reading
|
|
204
204
|
/// while only the ribbon shape flips.
|
|
205
205
|
public struct RoundedBadgeRibbon: View {
|
|
206
|
+
@Environment(\.appTheme) private var theme
|
|
206
207
|
@Environment(\.momoFontScale) private var fontScale
|
|
208
|
+
@Environment(\.designSystemKitsConfigs) private var designSystemKitsConfigs
|
|
207
209
|
let text: String
|
|
208
210
|
let position: RibbonPosition
|
|
209
211
|
|
|
@@ -223,7 +225,12 @@ public struct RoundedBadgeRibbon: View {
|
|
|
223
225
|
|
|
224
226
|
private var measuredTextSize: CGSize {
|
|
225
227
|
let fontSize = fontScale(TypographyStyle.labelXsMedium.fontSize)
|
|
226
|
-
let font =
|
|
228
|
+
let font = momoUIFont(
|
|
229
|
+
size: fontSize,
|
|
230
|
+
weight: .medium,
|
|
231
|
+
family: theme.font,
|
|
232
|
+
isWhiteList: designSystemKitsConfigs.isWhiteList
|
|
233
|
+
)
|
|
227
234
|
return text.size(withAttributes: [.font: font])
|
|
228
235
|
}
|
|
229
236
|
|
|
@@ -4,16 +4,14 @@ import Foundation
|
|
|
4
4
|
private final class MoMoUIKitsBundleToken {}
|
|
5
5
|
|
|
6
6
|
enum MoMoUIKitsResources {
|
|
7
|
+
|
|
7
8
|
static let bundle: Bundle = {
|
|
8
9
|
let bundleName = "MoMoUIKitsResources"
|
|
9
10
|
let tokenBundle = Bundle(for: MoMoUIKitsBundleToken.self)
|
|
10
|
-
let frameworkBundle = Bundle(identifier: "org.cocoapods.MoMoUIKits")
|
|
11
11
|
let candidates = [
|
|
12
12
|
Bundle.main.resourceURL,
|
|
13
13
|
tokenBundle.resourceURL,
|
|
14
|
-
tokenBundle.bundleURL
|
|
15
|
-
frameworkBundle?.resourceURL,
|
|
16
|
-
frameworkBundle?.bundleURL
|
|
14
|
+
tokenBundle.bundleURL
|
|
17
15
|
]
|
|
18
16
|
|
|
19
17
|
for candidate in candidates {
|
package/ios/Input/Input.swift
CHANGED
|
@@ -153,7 +153,7 @@ public struct Input: View {
|
|
|
153
153
|
handleFocusChange(focused)
|
|
154
154
|
})
|
|
155
155
|
.keyboardType(keyboardType)
|
|
156
|
-
.
|
|
156
|
+
.kitFont(fontWeight == .bold ? .actionSBold : .bodyDefaultRegular)
|
|
157
157
|
.ignoreBoldTextSetting()
|
|
158
158
|
.foregroundColor(getTextColor())
|
|
159
159
|
.disabled(disabled || readOnly)
|
|
@@ -345,7 +345,12 @@ private struct SecureInputField: UIViewRepresentable {
|
|
|
345
345
|
textField.spellCheckingType = .no
|
|
346
346
|
textField.delegate = context.coordinator
|
|
347
347
|
textField.keyboardType = keyboardType
|
|
348
|
-
textField.font =
|
|
348
|
+
textField.font = momoUIFont(
|
|
349
|
+
size: fontSize,
|
|
350
|
+
weight: fontWeight,
|
|
351
|
+
family: context.environment.appTheme.font,
|
|
352
|
+
isWhiteList: context.environment.designSystemKitsConfigs.isWhiteList
|
|
353
|
+
)
|
|
349
354
|
textField.textColor = textColor
|
|
350
355
|
textField.isEnabled = !isDisabled
|
|
351
356
|
textField.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
@@ -370,7 +375,12 @@ private struct SecureInputField: UIViewRepresentable {
|
|
|
370
375
|
if textField.text != text {
|
|
371
376
|
textField.text = text
|
|
372
377
|
}
|
|
373
|
-
let font =
|
|
378
|
+
let font = momoUIFont(
|
|
379
|
+
size: fontSize,
|
|
380
|
+
weight: fontWeight,
|
|
381
|
+
family: context.environment.appTheme.font,
|
|
382
|
+
isWhiteList: context.environment.designSystemKitsConfigs.isWhiteList
|
|
383
|
+
)
|
|
374
384
|
if textField.font != font {
|
|
375
385
|
textField.font = font
|
|
376
386
|
}
|
|
@@ -104,9 +104,7 @@ public struct InputPhoneNumber: View {
|
|
|
104
104
|
handleFocusChange(focused)
|
|
105
105
|
})
|
|
106
106
|
.keyboardType(.numberPad)
|
|
107
|
-
.
|
|
108
|
-
? fontScale.font(size: 14, weight: .semibold)
|
|
109
|
-
: fontScale.font(size: 18, weight: .bold))
|
|
107
|
+
.kitFont(size == .small ? .headerSSemibold : .headerMBold)
|
|
110
108
|
.ignoreBoldTextSetting()
|
|
111
109
|
.foregroundColor(theme.colors.text.default)
|
|
112
110
|
.applyCursorColor(theme.colors.primary)
|
|
@@ -120,7 +120,6 @@ public struct InputSearchProps {
|
|
|
120
120
|
|
|
121
121
|
public struct InputSearch: View {
|
|
122
122
|
@Environment(\.appTheme) private var theme
|
|
123
|
-
@Environment(\.momoFontScale) private var fontScale
|
|
124
123
|
@Binding var text: String
|
|
125
124
|
var buttonText: String = ""
|
|
126
125
|
var showButtonText: Bool = false
|
|
@@ -263,7 +262,7 @@ public struct InputSearch: View {
|
|
|
263
262
|
}, onCommit: onSubmit)
|
|
264
263
|
.disabled(disabled)
|
|
265
264
|
.foregroundColor(disabled ? theme.colors.text.disable : theme.colors.text.default)
|
|
266
|
-
.
|
|
265
|
+
.kitFont(size: 14, weight: fontWeight)
|
|
267
266
|
.ignoreBoldTextSetting()
|
|
268
267
|
.keyboardType(keyboardType)
|
|
269
268
|
.applyCursorColor(theme.colors.primary)
|
|
@@ -41,9 +41,11 @@ struct UITextViewWrapper: UIViewRepresentable {
|
|
|
41
41
|
uiView.tintColor = UIColor(cursorColor)
|
|
42
42
|
// Read through `context.environment` rather than the global `scaleSize`: that is what makes
|
|
43
43
|
// SwiftUI re-run updateUIView when the font scale changes.
|
|
44
|
-
uiView.font =
|
|
45
|
-
|
|
46
|
-
weight: fontWeight == .bold ? .bold : .regular
|
|
44
|
+
uiView.font = momoUIFont(
|
|
45
|
+
size: context.environment.momoFontScale(16),
|
|
46
|
+
weight: fontWeight == .bold ? .bold : .regular,
|
|
47
|
+
family: context.environment.appTheme.font,
|
|
48
|
+
isWhiteList: context.environment.designSystemKitsConfigs.isWhiteList
|
|
47
49
|
)
|
|
48
50
|
uiView.isEditable = !disabled
|
|
49
51
|
if uiView.keyboardType != keyboardType {
|
|
@@ -356,14 +356,13 @@ private struct MoneyTextField: UIViewRepresentable {
|
|
|
356
356
|
return numeric.isEmpty ? "" : formatNumberToMoney(numeric)
|
|
357
357
|
}
|
|
358
358
|
|
|
359
|
-
// Theme family as Compose `getFontFamily`: Momo Trust Sans only for whitelisted hosts, else system.
|
|
360
359
|
private func font(_ context: Context) -> UIFont {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
360
|
+
momoUIFont(
|
|
361
|
+
size: context.environment.momoFontScale(20),
|
|
362
|
+
weight: fontWeight,
|
|
363
|
+
family: fontFamily,
|
|
364
|
+
isWhiteList: context.environment.designSystemKitsConfigs.isWhiteList
|
|
365
|
+
)
|
|
367
366
|
}
|
|
368
367
|
|
|
369
368
|
final class Coordinator: NSObject, UITextFieldDelegate {
|
|
@@ -32,6 +32,9 @@ public struct ScreenLayout: Equatable {
|
|
|
32
32
|
public let width: CGFloat
|
|
33
33
|
public let height: CGFloat
|
|
34
34
|
public let isIPhoneDuo: Bool
|
|
35
|
+
/// iPhone Duo folded or not, whatever the window size (its hinge, or the outer display's one-sided strip);
|
|
36
|
+
/// drives popups, modal and bottom sheet.
|
|
37
|
+
public let isDuoDevice: Bool
|
|
35
38
|
public let insets: EdgeInsets
|
|
36
39
|
public let insetLeft: CGFloat
|
|
37
40
|
public let insetRight: CGFloat
|
|
@@ -68,8 +71,16 @@ enum DuoMetrics {
|
|
|
68
71
|
// (x 550–643 of 669, clock digits centered on y 47); its reserved region is taller than the glyphs.
|
|
69
72
|
static let innerStatusBarCenterY: CGFloat = 47
|
|
70
73
|
static let innerStatusBarWidth: CGFloat = 120
|
|
71
|
-
//
|
|
72
|
-
|
|
74
|
+
// Host app's iOS 26 tab bar on the inner display in portrait: a 62pt glass pill sized to its tabs,
|
|
75
|
+
// QR as a 62pt platter beside it, ≥ 20pt from the sides, 13pt into the home indicator inset.
|
|
76
|
+
static let floatingTabBarHeight: CGFloat = 62
|
|
77
|
+
static let floatingTabBarMargin: CGFloat = 20
|
|
78
|
+
static let floatingTabBarInsetOverlap: CGFloat = 13
|
|
79
|
+
// Width of a system alert on the inner display, in both orientations.
|
|
80
|
+
static let modalMaxWidth: CGFloat = 440
|
|
81
|
+
// Fits a PopupNotify with image, scrolled description and actions at the max width (RN
|
|
82
|
+
// Application/device.ts); taller content scrolls instead of filling the inner display.
|
|
83
|
+
static let modalMaxHeight: CGFloat = 640
|
|
73
84
|
}
|
|
74
85
|
|
|
75
86
|
enum HeaderMetrics {
|
|
@@ -104,15 +115,17 @@ extension ScreenLayout {
|
|
|
104
115
|
size: CGSize,
|
|
105
116
|
insets: EdgeInsets,
|
|
106
117
|
reservedRegions: [CGRect] = ReservedRegions.current,
|
|
107
|
-
isPhone: Bool = isPhoneIdiom
|
|
118
|
+
isPhone: Bool = isPhoneIdiom,
|
|
119
|
+
isFoldable: Bool = FoldableDevice.isFoldable(measuredIn: keyWindow)
|
|
108
120
|
) -> ScreenLayout {
|
|
109
121
|
let width = size.width
|
|
110
122
|
let height = size.height
|
|
123
|
+
let isDuoDevice = isPhone && isFoldable
|
|
111
124
|
var (headerHeight, headerStatusBarHeight) = headerGeometry(topInset: insets.top, isPhone: isPhone)
|
|
112
125
|
let isDuo = isPhone && isIPhoneDuoWindow(width: width, height: height, insets: insets)
|
|
113
126
|
guard isDuo else {
|
|
114
127
|
return ScreenLayout(
|
|
115
|
-
width: width, height: height, isIPhoneDuo: false, insets: insets,
|
|
128
|
+
width: width, height: height, isIPhoneDuo: false, isDuoDevice: isDuoDevice, insets: insets,
|
|
116
129
|
insetLeft: 0, insetRight: 0, contentWidth: width,
|
|
117
130
|
headerHeight: headerHeight, headerStatusBarHeight: headerStatusBarHeight, sideBar: nil,
|
|
118
131
|
statusBarClearance: nil
|
|
@@ -171,8 +184,10 @@ extension ScreenLayout {
|
|
|
171
184
|
)
|
|
172
185
|
}
|
|
173
186
|
|
|
187
|
+
// the outer display reports no hinge; its one-sided strip still gives it away
|
|
174
188
|
return ScreenLayout(
|
|
175
|
-
width: width, height: height, isIPhoneDuo: true,
|
|
189
|
+
width: width, height: height, isIPhoneDuo: true,
|
|
190
|
+
isDuoDevice: isDuoDevice || insets.leading != insets.trailing, insets: insets,
|
|
176
191
|
insetLeft: insets.leading, insetRight: insets.trailing,
|
|
177
192
|
contentWidth: width - insets.leading - insets.trailing,
|
|
178
193
|
headerHeight: headerHeight, headerStatusBarHeight: headerStatusBarHeight, sideBar: sideBar,
|
|
@@ -249,15 +264,54 @@ extension ScreenLayout {
|
|
|
249
264
|
public var duoTopInset: CGFloat {
|
|
250
265
|
max(insets.top, height > width ? max(insets.leading, insets.trailing) * 2 : 0)
|
|
251
266
|
}
|
|
267
|
+
|
|
268
|
+
/// Room for a dialog on iPhone Duo: inside the safe area, so clear of the side toolbar strip, and
|
|
269
|
+
/// capped so it stays a dialog on the inner display.
|
|
270
|
+
var duoModalMaxWidth: CGFloat {
|
|
271
|
+
min(width - insets.leading - insets.trailing - Spacing.XL * 2, DuoMetrics.modalMaxWidth)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
var duoModalMaxHeight: CGFloat {
|
|
275
|
+
min(height - insets.top - insets.bottom - Spacing.XL * 2, DuoMetrics.modalMaxHeight)
|
|
276
|
+
}
|
|
252
277
|
}
|
|
253
278
|
|
|
254
|
-
///
|
|
255
|
-
///
|
|
279
|
+
/// Snapshot for call sites without `\.screenLayout`; not reactive. `windowSize` is ignored,
|
|
280
|
+
/// kept for host call sites.
|
|
256
281
|
public func isIPhoneDuoDevice(windowSize: CGSize? = nil) -> Bool {
|
|
257
282
|
guard isPhoneIdiom else { return false }
|
|
258
|
-
let
|
|
259
|
-
|
|
260
|
-
|
|
283
|
+
let insets = keyWindow?.safeAreaInsets
|
|
284
|
+
return FoldableDevice.isFoldable(measuredIn: keyWindow) || insets.map { $0.left != $0.right } == true
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/// Apple's foldable check (as the host's `SideTabRailView.isFoldableDevice`): only a device with a
|
|
288
|
+
/// hinge reports a `.division` region, inactive while flat but still there. Screen size cannot tell
|
|
289
|
+
/// it apart from a large iPhone in landscape or a narrowed window.
|
|
290
|
+
enum FoldableDevice {
|
|
291
|
+
// a hinge never goes away, so a positive answer is kept
|
|
292
|
+
private static var isKnownFoldable = false
|
|
293
|
+
|
|
294
|
+
static func isFoldable(measuredIn view: UIView?) -> Bool {
|
|
295
|
+
if isKnownFoldable { return true }
|
|
296
|
+
// compiled out on SDKs older than iOS 27.1 (UIKit 9127.0.85.28)
|
|
297
|
+
#if canImport(UIKit, _version: 9127.0.85.28)
|
|
298
|
+
guard #available(iOS 27.1, *), let view, view.window != nil || view is UIWindow else { return false }
|
|
299
|
+
isKnownFoldable = !view.reservedRegions(kind: .division, options: .includeInactive).isEmpty
|
|
300
|
+
#endif
|
|
301
|
+
return isKnownFoldable
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/// Catches the hinge when the regions were read too early (before the window had them).
|
|
305
|
+
static func observe(in view: UIView) {
|
|
306
|
+
#if canImport(UIKit, _version: 9127.0.85.28)
|
|
307
|
+
guard #available(iOS 27.1, *) else { return }
|
|
308
|
+
view.addInteraction(UIHingeInteraction { _, update in
|
|
309
|
+
guard update.hinge != nil, !isKnownFoldable else { return }
|
|
310
|
+
isKnownFoldable = true
|
|
311
|
+
NotificationCenter.default.post(name: ReservedRegions.didChange, object: nil)
|
|
312
|
+
})
|
|
313
|
+
#endif
|
|
314
|
+
}
|
|
261
315
|
}
|
|
262
316
|
|
|
263
317
|
// MARK: - Reserved regions
|
|
@@ -282,6 +336,7 @@ private final class ReservedRegionsProbe: UIView {
|
|
|
282
336
|
override init(frame: CGRect) {
|
|
283
337
|
super.init(frame: frame)
|
|
284
338
|
isUserInteractionEnabled = false
|
|
339
|
+
FoldableDevice.observe(in: self)
|
|
285
340
|
}
|
|
286
341
|
|
|
287
342
|
required init?(coder: NSCoder) { nil }
|