@momo-kits/native-kits 0.168.1-beta.1-debug → 0.168.1-beta.10-debug

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CLAUDE.md +7 -5
  2. package/compose/build.gradle.kts +1 -1
  3. package/compose/compose.podspec +1 -1
  4. package/compose/src/androidMain/kotlin/vn/momo/kits/const/HostFont.android.kt +4 -3
  5. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +1 -1
  6. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +0 -14
  7. package/compose/src/commonMain/kotlin/vn/momo/kits/application/DesignSystemKits.kt +62 -0
  8. package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +2 -1
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +18 -14
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +9 -12
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +2 -2
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +2 -3
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +124 -59
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +1 -1
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +3 -1
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +2 -2
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +10 -3
  18. package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +3 -1
  19. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +54 -10
  20. package/gradle.properties +1 -1
  21. package/ios/Application/ApplicationEnvironment.swift +7 -6
  22. package/ios/Application/DesignSystemKits.swift +100 -0
  23. package/ios/Application/DesignSystemKitsPayload.swift +68 -0
  24. package/ios/Badge/BadgeRibbon.swift +8 -1
  25. package/ios/Extensions/Color++.swift +2 -4
  26. package/ios/Input/Input.swift +13 -3
  27. package/ios/Input/InputPhoneNumber.swift +1 -3
  28. package/ios/Input/InputSearch.swift +1 -2
  29. package/ios/Input/InputTextArea.swift +5 -3
  30. package/ios/InputMoney/InputMoney.swift +6 -7
  31. package/ios/Layout/ScreenLayout.swift +46 -11
  32. package/ios/Navigation/BottomTab/DuoBarCompression.swift +2 -3
  33. package/ios/Navigation/BottomTab/DuoTabBar.swift +94 -69
  34. package/ios/Navigation/Component/HeaderBackground.swift +22 -1
  35. package/ios/Navigation/StackScreen.swift +3 -7
  36. package/ios/Popup/PopupLayout.swift +0 -2
  37. package/ios/Stepper/Stepper.swift +1 -2
  38. package/ios/Template/TrustBanner/TrustBanner.swift +4 -2
  39. package/ios/Typography/FontScale.swift +14 -11
  40. package/ios/Typography/Text.swift +86 -12
  41. package/ios/Typography/Typography.swift +2 -2
  42. package/ios/native-kits.podspec +1 -1
  43. package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
  44. package/package.json +1 -1
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitDesignSystem.kt +0 -18
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +0 -22
  47. package/ios/Typography/FontScaleStore.swift +0 -58
  48. package/ios/Typography/KitDesignSystem.swift +0 -17
  49. package/local.properties +0 -8
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
- - **Design-system config** (`trustBanner`, `headerBar`, `headerGradient`): the host seeds `KitDesignSystemConfig.update(...)` once (same pattern as `KitFontScale`); `NavigationContainer` provides it as `LocalDesignSystemConfig` (param `designSystemConfig` → parent → global) and applies `headerBar` as the header image when the theme has none.
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` (short side of screen or window > 440, RN `device.ts`) drives popups, modal and bottom sheet.
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`) 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).
@@ -104,9 +106,9 @@ The SwiftUI port differs from Compose in one important way: **it follows the hos
104
106
 
105
107
  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
108
 
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 the `KitFontScale` global. On iOS, `platform/OsFontScale.ios.kt` observes `UIContentSizeCategoryDidChangeNotification` and overrides `LocalDensity` — Compose MP otherwise reads `fontScale` once at `ComposeUIViewController` creation. Wired at `NavigationContainer`.
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)`. `FontScaleStore.shared` is the `ObservableObject` holding both inputs and the OS factor (it observes `UIContentSizeCategory.didChangeNotification`); the host pushes its preference with `update(userScaleRate:useOSScaleRate:)`.
109
- - **Never set `\.momoFontScale` directly** — same trap as `\.localize`. Inject via `.momoFontScale(_:context:)`, which holds the store as `@ObservedObject`; that is what re-publishes the snapshot. `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.
109
+ - **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`.
110
+ - **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(_:)`.
111
+ - **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
112
  - 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
113
 
112
114
  ## Code conventions
@@ -40,7 +40,7 @@ kotlin {
40
40
  }
41
41
 
42
42
  cocoapods {
43
- version = "0.168.1-beta.1-debug"
43
+ version = "0.168.1-beta.10-debug"
44
44
  summary = "IOS Shared module"
45
45
  homepage = "https://momo.vn"
46
46
  ios.deploymentTarget = "15.0"
@@ -1,6 +1,6 @@
1
1
  Pod::Spec.new do |spec|
2
2
  spec.name = 'compose'
3
- spec.version = '0.168.1-beta.1-debug'
3
+ spec.version = '0.167.1-beta.22-debug'
4
4
  spec.homepage = 'https://momo.vn'
5
5
  spec.source = { :http=> ''}
6
6
  spec.authors = ''
@@ -5,15 +5,16 @@ import androidx.compose.runtime.remember
5
5
  import androidx.compose.ui.platform.LocalContext
6
6
  import androidx.compose.ui.text.font.Font
7
7
  import androidx.compose.ui.text.font.FontFamily
8
+ import androidx.compose.ui.text.font.FontWeight
8
9
 
9
10
  @Composable
10
- internal actual fun hostFontFamily(fileName: String): FontFamily? {
11
+ internal actual fun hostFontFamily(fileName: String, weight: FontWeight): FontFamily? {
11
12
  val assets = LocalContext.current.applicationContext.assets
12
- return remember(fileName) {
13
+ return remember(fileName, weight) {
13
14
  val path = "fonts/$fileName"
14
15
  // AssetManager.open ném IOException nếu thiếu file; Font(path, assetManager) thì lỗi
15
16
  // muộn lúc render nên phải kiểm tra tồn tại trước.
16
17
  val exists = runCatching { assets.open(path).close() }.isSuccess
17
- if (exists) FontFamily(Font(path = path, assetManager = assets)) else null
18
+ if (exists) FontFamily(Font(path = path, assetManager = assets, weight = weight)) else null
18
19
  }
19
20
  }
@@ -3,4 +3,4 @@ package vn.momo.kits.platform
3
3
  import androidx.compose.runtime.Composable
4
4
 
5
5
  @Composable
6
- internal actual fun rememberReservedRegions(): List<ReservedRegion> = emptyList()
6
+ internal actual fun rememberReservedRegions(): ReservedRegions = ReservedRegions()
@@ -1,9 +1,6 @@
1
1
  package vn.momo.kits.application
2
2
 
3
3
  import androidx.compose.runtime.Immutable
4
- import androidx.compose.runtime.getValue
5
- import androidx.compose.runtime.mutableStateOf
6
- import androidx.compose.runtime.setValue
7
4
  import androidx.compose.runtime.staticCompositionLocalOf
8
5
  import vn.momo.kits.components.TrustBannerData
9
6
 
@@ -19,17 +16,6 @@ data class DesignSystemConfig(
19
16
  val headerGradient: String? = null,
20
17
  )
21
18
 
22
- // App-wide holder seeded by the host (same pattern as KitFontScale), so direct NavigationContainer
23
- // callers get the config without passing it. Snapshot state: a config fetched after first frame recomposes.
24
- object KitDesignSystemConfig {
25
- var config by mutableStateOf<DesignSystemConfig?>(null)
26
- private set
27
-
28
- fun update(config: DesignSystemConfig?) {
29
- this.config = config
30
- }
31
- }
32
-
33
19
  @Immutable
34
20
  data class MiniAppContext(
35
21
  val appIcon: String = "",
@@ -0,0 +1,62 @@
1
+ package vn.momo.kits.application
2
+
3
+ import androidx.compose.runtime.Immutable
4
+ import androidx.compose.runtime.getValue
5
+ import androidx.compose.runtime.mutableStateOf
6
+ import androidx.compose.runtime.setValue
7
+ import kotlinx.coroutines.CoroutineScope
8
+ import kotlinx.coroutines.Dispatchers
9
+ import kotlinx.coroutines.Job
10
+ import kotlinx.coroutines.SupervisorJob
11
+ import kotlinx.coroutines.flow.Flow
12
+ import kotlinx.coroutines.flow.MutableStateFlow
13
+ import kotlinx.coroutines.flow.StateFlow
14
+ import kotlinx.coroutines.flow.asStateFlow
15
+ import kotlinx.coroutines.launch
16
+
17
+ @Immutable
18
+ data class DesignSystemKitsConfigs(
19
+ val userScaleRate: Float = 1f,
20
+ val useOSScaleRate: Boolean = true,
21
+ val isWhiteList: Boolean = false,
22
+ val designSystemConfig: DesignSystemConfig? = null,
23
+ )
24
+
25
+ // The kit is sandboxed from the host's storage/config, so the host implements this and hands it to init().
26
+ interface DesignSystemKitsSource {
27
+ // Sync read: seeds the kit before the first frame, so there is no default-then-real flicker.
28
+ fun getConfigs(): DesignSystemKitsConfigs
29
+
30
+ val configsFlow: Flow<DesignSystemKitsConfigs>
31
+ }
32
+
33
+ // App-wide design-system inputs (font scale, whitelist, remote config). scaleSize(), defaultTheme and
34
+ // NavigationContainer fall back to these when no MiniAppContext / LocalDesignSystemConfig is provided.
35
+ object DesignSystemKits {
36
+ private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
37
+ private var observeJob: Job? = null
38
+
39
+ // Snapshot state so Compose readers recompose (including non-composable getters like defaultTheme);
40
+ // the StateFlow is for non-Compose observers, e.g. the host's SwiftUI bridge, where Compose's
41
+ // global snapshot apply loop may never have started.
42
+ private var currentConfigs by mutableStateOf(DesignSystemKitsConfigs())
43
+ private val mutableConfigsFlow = MutableStateFlow(currentConfigs)
44
+ val configsFlow: StateFlow<DesignSystemKitsConfigs> = mutableConfigsFlow.asStateFlow()
45
+
46
+ // Idempotent: a repeated call (e.g. Activity re-creation) replaces the previous observer.
47
+ fun init(source: DesignSystemKitsSource) {
48
+ observeJob?.cancel()
49
+ setConfigs(source.getConfigs())
50
+ observeJob = scope.launch {
51
+ source.configsFlow.collect { setConfigs(it) }
52
+ }
53
+ }
54
+
55
+ // While init() is observing a source, its next emission overrides a value set here.
56
+ fun setConfigs(configs: DesignSystemKitsConfigs) {
57
+ currentConfigs = configs
58
+ mutableConfigsFlow.value = configs
59
+ }
60
+
61
+ fun getConfigs(): DesignSystemKitsConfigs = currentConfigs
62
+ }
@@ -2,6 +2,7 @@ package vn.momo.kits.const
2
2
 
3
3
  import androidx.compose.runtime.Composable
4
4
  import androidx.compose.ui.text.font.FontFamily
5
+ import androidx.compose.ui.text.font.FontWeight
5
6
 
6
7
  /**
7
8
  * Nạp font từ tài nguyên native của app host thay vì đóng gói bản copy riêng trong SDK.
@@ -13,4 +14,4 @@ import androidx.compose.ui.text.font.FontFamily
13
14
  * Trả về `null` nếu host không có font đó — caller tự fallback [FontFamily.Default].
14
15
  */
15
16
  @Composable
16
- internal expect fun hostFontFamily(fileName: String): FontFamily?
17
+ internal expect fun hostFontFamily(fileName: String, weight: FontWeight): FontFamily?
@@ -8,6 +8,7 @@ import androidx.compose.ui.text.font.FontFamily
8
8
  import androidx.compose.ui.text.font.FontWeight
9
9
  import androidx.compose.ui.text.style.TextDecoration
10
10
  import androidx.compose.ui.unit.*
11
+ import vn.momo.kits.application.DesignSystemKits
11
12
  import vn.momo.kits.application.LocalContext
12
13
  import vn.momo.kits.platform.getScreenDimensions
13
14
  import kotlin.math.max
@@ -20,10 +21,9 @@ const val MAX_DEVICE_SCALE = 5
20
21
  @Composable
21
22
  fun scaleSize(size: Float): Float {
22
23
  val context = LocalContext.current
23
- // Host-pushed context wins; otherwise fall back to the app-wide KitFontScale holder.
24
- val useOsScale = context?.useOSScaleRate ?: KitFontScale.useOSScaleRate
24
+ val useOsScale = context?.useOSScaleRate ?: DesignSystemKits.getConfigs().useOSScaleRate
25
25
  if (!useOsScale) {
26
- return size * (context?.userScaleRate ?: KitFontScale.userScaleRate)
26
+ return size * (context?.userScaleRate ?: DesignSystemKits.getConfigs().userScaleRate)
27
27
  }
28
28
  val scaleSizeMaxRate: Float = MAX_FONT_SCALE
29
29
  // Short side, not width: landscape must not inflate every font.
@@ -84,12 +84,6 @@ fun scaleSize(textStyle: TextStyle): TextStyle {
84
84
  }
85
85
 
86
86
 
87
- /**
88
- * Tên file font trong tài nguyên native của app host (xem [hostFontFamily]).
89
- *
90
- * SDK không đóng gói font riêng nữa: app host đã bundle sẵn toàn bộ font từ `font-source`
91
- * cho cả React Native lẫn native, nên bản copy trong `composeResources` chỉ làm phình app.
92
- */
93
87
  private const val LEGACY_DEFAULT_FONT_FILE = "SFProText-Regular.ttf"
94
88
  private const val NEW_DEFAULT_FONT_FILE = "MomoTrustSans-Regular.ttf"
95
89
 
@@ -124,17 +118,27 @@ private val hostFontFiles: Map<String, String> = mapOf(
124
118
  private const val NEW_DEFAULT_FONT_FAMILY = "MomoTrustSans"
125
119
  private const val LEGACY_DEFAULT_FONT_FAMILY = "SFProText"
126
120
 
121
+ // Families that ship one file per weight; the others map every weight to a single file.
122
+ private val weightedFontFamilies = setOf(NEW_DEFAULT_FONT_FAMILY, LEGACY_DEFAULT_FONT_FAMILY)
123
+
127
124
  internal val defaultFontFamily: String
128
- get() = if (KitDesignSystem.isWhiteList) NEW_DEFAULT_FONT_FAMILY else LEGACY_DEFAULT_FONT_FAMILY
125
+ get() = if (DesignSystemKits.getConfigs().isWhiteList) NEW_DEFAULT_FONT_FAMILY else LEGACY_DEFAULT_FONT_FAMILY
129
126
 
130
127
  @Composable
131
128
  fun getFontFamily(fontFamily: String, fontWeight: FontWeight? = FontWeight.Normal): FontFamily {
132
- val isWhiteList = KitDesignSystem.isWhiteList
129
+ val isWhiteList = DesignSystemKits.getConfigs().isWhiteList
133
130
  val defaultFile = if (isWhiteList) NEW_DEFAULT_FONT_FILE else LEGACY_DEFAULT_FONT_FILE
134
131
  val key = "$fontFamily-${fontWeight?.weight}"
135
- val fileName = hostFontFiles[key] ?: defaultFile
136
- return hostFontFamily(fileName)
137
- ?: hostFontFamily(defaultFile)
132
+ val mappedFile = hostFontFiles[key]
133
+ val fileName = mappedFile ?: defaultFile
134
+ // Declaring the file's real weight keeps Compose from synthesizing bold on top of a bold file.
135
+ val fileWeight = if (mappedFile != null && fontFamily in weightedFontFamilies) {
136
+ fontWeight ?: FontWeight.Normal
137
+ } else {
138
+ FontWeight.Normal
139
+ }
140
+ return hostFontFamily(fileName, fileWeight)
141
+ ?: hostFontFamily(defaultFile, FontWeight.Normal)
138
142
  ?: FontFamily.Default
139
143
  }
140
144
 
@@ -14,7 +14,6 @@ import androidx.compose.ui.unit.dp
14
14
  import vn.momo.kits.const.Spacing
15
15
  import vn.momo.kits.navigation.component.HEADER_HEIGHT
16
16
  import vn.momo.kits.platform.ReservedRegion
17
- import vn.momo.kits.platform.getScreenDimensions
18
17
  import vn.momo.kits.platform.isPhoneIdiom
19
18
  import vn.momo.kits.platform.rememberReservedRegions
20
19
  import vn.momo.kits.utils.getAppStatusBarHeight
@@ -52,7 +51,7 @@ data class ScreenLayout(
52
51
  val headerStatusBarHeight: Dp,
53
52
  // iPhone Duo moves back and toolkit into a column in the side safe area; null while they stay in the bar.
54
53
  val sideBar: SideBarLayout? = null,
55
- // Port of RN `device.ts` isIPhoneDuo: the screen stays on the outer display, so Split View still matches.
54
+ // The device has a hinge, folded or not, whatever the window size; drives popups, modal and bottom sheet.
56
55
  val isIPhoneDuoDevice: Boolean = isIPhoneDuo,
57
56
  // Portrait iPhone Duo puts the status bar in the side strip, about twice as tall as the strip is wide.
58
57
  // Stored so it survives consumeHorizontalInsets().
@@ -86,11 +85,13 @@ internal object DuoMetrics {
86
85
  // (x 550–643 of 669, clock digits centered on y 47); its reserved region is taller than the glyphs.
87
86
  val innerStatusBarCenterY = 47.dp
88
87
  val innerStatusBarWidth = 120.dp
88
+ // Host app's iOS 26 tab bar on the inner display in portrait: a 62pt glass pill sized to its tabs,
89
+ // QR as a 62pt platter beside it, ≥ 20pt from the sides, 13pt into the home indicator inset.
90
+ val floatingTabBarHeight = 62.dp
91
+ val floatingTabBarMargin = 20.dp
92
+ val floatingTabBarInsetOverlap = 13.dp
89
93
  }
90
94
 
91
- // Widest short side of any single-display iPhone.
92
- private val IPHONE_MAX_SHORT_SIDE = 440.dp
93
-
94
95
  private val IPHONE_HEADER_MIN_HEIGHT = 72.dp
95
96
 
96
97
  // A smaller top inset means the status bar is not on the top edge.
@@ -129,10 +130,9 @@ internal fun calculateScreenLayout(topInset: Dp, parent: ScreenLayout? = null):
129
130
  val leftPx = insets.getLeft(density, layoutDirection)
130
131
  val rightPx = insets.getRight(density, layoutDirection)
131
132
  val bottomPx = WindowInsets.systemBars.getBottom(density)
132
- val screen = getScreenDimensions()
133
133
  val reservedRegions = rememberReservedRegions()
134
134
 
135
- return remember(size, leftPx, rightPx, bottomPx, topInset, screen, density, reservedRegions) {
135
+ return remember(size, leftPx, rightPx, bottomPx, topInset, density, reservedRegions) {
136
136
  with(density) {
137
137
  val width = size.width.toDp()
138
138
  val height = size.height.toDp()
@@ -145,11 +145,8 @@ internal fun calculateScreenLayout(topInset: Dp, parent: ScreenLayout? = null):
145
145
  bottomInset = bottomPx.toDp(),
146
146
  isPhone = isPhone,
147
147
  asymmetricInsets = leftPx != rightPx,
148
- isDuoDevice = isPhone && maxOf(
149
- minOf(screen.width, screen.height).dp,
150
- minOf(width, height),
151
- ) > IPHONE_MAX_SHORT_SIDE,
152
- reservedRegions = reservedRegions,
148
+ isDuoDevice = isPhone && reservedRegions.isFoldable,
149
+ reservedRegions = reservedRegions.occlusion,
153
150
  )
154
151
  }
155
152
  }
@@ -20,7 +20,7 @@ import androidx.navigation.toRoute
20
20
  import kotlinx.coroutines.delay
21
21
  import kotlinx.coroutines.flow.collectLatest
22
22
  import vn.momo.kits.application.DesignSystemConfig
23
- import vn.momo.kits.application.KitDesignSystemConfig
23
+ import vn.momo.kits.application.DesignSystemKits
24
24
  import vn.momo.kits.application.LocalContext
25
25
  import vn.momo.kits.application.LocalDesignSystemConfig
26
26
  import vn.momo.kits.application.LocalLocalize
@@ -66,7 +66,7 @@ fun NavigationContainer(
66
66
  val stackTransition = remember { StackTransitionCoordinator() }
67
67
  val theme = remember { mutableStateOf(initialTheme) }
68
68
  val resolvedDesignSystemConfig =
69
- designSystemConfig ?: LocalDesignSystemConfig.current ?: KitDesignSystemConfig.config
69
+ designSystemConfig ?: LocalDesignSystemConfig.current ?: DesignSystemKits.getConfigs().designSystemConfig
70
70
  val headerBar = resolvedDesignSystemConfig?.headerBar
71
71
  // RN parity: the theme's own header image wins; config headerBar is only the fallback.
72
72
  val resolvedTheme = remember(theme.value, headerBar) {
@@ -39,8 +39,6 @@ internal val DUO_GROUP_GAP = Spacing.M
39
39
  private val DEFAULT_TOOLBAR_HEIGHT = DuoMetrics.controlSize + Spacing.M + DuoMetrics.itemPitch * 2
40
40
  // Back, then the single button the toolbar folds into
41
41
  private val COMPRESSED_TOOLBAR_HEIGHT = DuoMetrics.controlSize * 2 + Spacing.M
42
- // Back alone, while a rail opened from the compressed state takes the toolbar slot
43
- private val BACK_ONLY_HEIGHT = DuoMetrics.controlSize
44
42
 
45
43
  // Port of RN DuoBarCompression (HIG, Designing for iPhone Duo: Vertical controls):
46
44
  // - both fit: both stay whole
@@ -78,7 +76,8 @@ internal fun rememberDuoBarCompression(
78
76
  expandTabBar = { expanded = DuoBar.TAB_BAR },
79
77
  setToolbarHeight = setToolbarHeight,
80
78
  )
81
- opened == DuoBar.TAB_BAR && fits(BACK_ONLY_HEIGHT, tabBar) -> DuoBarCompression(
79
+ // an opened rail always shows, even when only the group gap is short: it closes once a tab is picked
80
+ opened == DuoBar.TAB_BAR -> DuoBarCompression(
82
81
  toolbarCompressed = true,
83
82
  toolbarHidden = true,
84
83
  expandToolbar = { opened = DuoBar.TOOLBAR },