@momo-kits/native-kits 0.167.1-duo.2-debug → 0.167.1-duo.20-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 (194) hide show
  1. package/CLAUDE.md +36 -8
  2. package/README.md +2 -2
  3. package/compose/build.gradle.kts +15 -1
  4. package/compose/build.gradle.kts.backup +14 -0
  5. package/compose/compose.podspec +1 -1
  6. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +2 -0
  7. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +6 -0
  8. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +168 -52
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +20 -21
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +2 -1
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +4 -2
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +1 -1
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +24 -14
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +1 -1
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +1 -1
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +9 -6
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +11 -12
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +4 -9
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +2 -2
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +10 -9
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +6 -6
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +53 -26
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +12 -2
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +7 -9
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +1 -1
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +13 -6
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +13 -5
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +3 -2
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +14 -6
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +19 -21
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +2 -2
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +5 -2
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +3 -1
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +7 -15
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +4 -0
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +10 -4
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +3 -17
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +173 -21
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +43 -4
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +8 -0
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +10 -0
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +153 -48
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +114 -71
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +8 -9
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +98 -0
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +312 -0
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +84 -18
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +33 -13
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +2 -1
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +51 -19
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +4 -2
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +1 -1
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +46 -23
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +2 -1
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +3 -0
  56. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +13 -0
  57. package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +38 -0
  58. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +4 -0
  59. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +94 -0
  60. package/gradle.properties +1 -1
  61. package/ios/Application/ApplicationEnvironment.swift +17 -0
  62. package/ios/Application/MaxApi.swift +12 -3
  63. package/ios/Avatar/Avatar.swift +4 -3
  64. package/ios/Badge/Badge.swift +6 -4
  65. package/ios/Badge/BadgeDot.swift +1 -1
  66. package/ios/Badge/BadgeRibbon.swift +0 -1
  67. package/ios/Button/Button.swift +15 -12
  68. package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +64 -18
  69. package/ios/Carousel/Carousel.swift +29 -12
  70. package/ios/Checkbox/Checkbox.swift +7 -3
  71. package/ios/Chip/Chip.swift +5 -4
  72. package/ios/Collapse/Collapse.swift +70 -19
  73. package/ios/Colors+Radius+Spacing/Colors.swift +10 -1
  74. package/ios/Colors+Radius+Spacing/Radius.swift +6 -0
  75. package/ios/DateTimePicker/DateTimePicker.swift +54 -20
  76. package/ios/DateTimePicker/WheelPicker.swift +103 -105
  77. package/ios/Divider/Divider.swift +37 -4
  78. package/ios/Extensions/Color++.swift +38 -9
  79. package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
  80. package/ios/Extensions/SkeletonUICompat.swift +11 -0
  81. package/ios/Icon/Icon.swift +21 -23
  82. package/ios/Icon/Icons.swift +1329 -0
  83. package/ios/IconButton/IconButton.swift +15 -7
  84. package/ios/Image/Image.swift +7 -23
  85. package/ios/Information/Information.swift +27 -9
  86. package/ios/Input/ErrorView.swift +2 -2
  87. package/ios/Input/Input.swift +160 -147
  88. package/ios/Input/InputPhoneNumber.swift +12 -9
  89. package/ios/Input/InputSearch.swift +102 -75
  90. package/ios/Input/InputTextArea.swift +128 -145
  91. package/ios/InputDropDown/InputDropDown.swift +10 -7
  92. package/ios/InputMoney/InputMoney.swift +33 -11
  93. package/ios/InputOTP/InputOTP.swift +22 -12
  94. package/ios/Layout/Card.swift +50 -0
  95. package/ios/Layout/GridContext.swift +187 -3
  96. package/ios/Layout/Item.swift +5 -4
  97. package/ios/Layout/ScreenLayout.swift +380 -0
  98. package/ios/Layout/Section.swift +4 -100
  99. package/ios/Loader/Loader.swift +5 -9
  100. package/ios/Lottie/LottieView.swift +14 -2
  101. package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +128 -9
  102. package/ios/Navigation/BottomTab/BottomTab.swift +185 -0
  103. package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +15 -7
  104. package/ios/Navigation/BottomTab/DuoBarCompression.swift +105 -0
  105. package/ios/Navigation/BottomTab/DuoTabBar.swift +304 -0
  106. package/ios/{Application → Navigation/Component}/FloatingButton.swift +14 -3
  107. package/ios/Navigation/Component/Header.swift +531 -0
  108. package/ios/Navigation/Component/HeaderBackground.swift +133 -0
  109. package/ios/Navigation/Component/HeaderJourney.swift +73 -0
  110. package/ios/Navigation/Component/HeaderLocation.swift +48 -0
  111. package/ios/Navigation/Component/HeaderRight.swift +377 -0
  112. package/ios/Navigation/Component/HeaderTitle.swift +36 -0
  113. package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +21 -24
  114. package/ios/Navigation/Component/SideBar.swift +183 -0
  115. package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +5 -6
  116. package/ios/Navigation/HostBackGestureBridge.swift +78 -0
  117. package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +5 -2
  118. package/ios/Navigation/Navigation.swift +204 -0
  119. package/ios/Navigation/NavigationContainer.swift +194 -0
  120. package/ios/{Application/Navigation → Navigation}/Navigator.swift +177 -91
  121. package/ios/Navigation/StackScreen.swift +820 -0
  122. package/ios/Navigation/StackTransition.swift +400 -0
  123. package/ios/Navigation/SwipeBackHaptic.swift +33 -0
  124. package/ios/{Application/Navigation → Navigation}/Tracking/ScreenTracker.swift +12 -5
  125. package/ios/OTPKeyboard/OTPKeyboard.swift +8 -11
  126. package/ios/Pagination/PaginationScroll.swift +2 -2
  127. package/ios/Popup/PopupDisplay.swift +72 -136
  128. package/ios/Popup/PopupInput.swift +10 -13
  129. package/ios/Popup/PopupLayout.swift +140 -0
  130. package/ios/Popup/PopupNotify.swift +56 -73
  131. package/ios/Popup/PopupPromotion.swift +31 -47
  132. package/ios/PopupView/FullscreenPopup.swift +13 -19
  133. package/ios/PopupView/PopupView.swift +1 -1
  134. package/ios/PopupView/Utils++.swift +0 -8
  135. package/ios/ProgressInfo/ProgressInfo.swift +4 -2
  136. package/ios/Radio/Radio.swift +2 -1
  137. package/ios/Rating/Rating.swift +9 -1
  138. package/ios/Resources/dot_loading.json +558 -0
  139. package/ios/Resources/lottie_circle_loader.json +1 -0
  140. package/ios/ScrollIndicator/ScrollIndicator.swift +11 -10
  141. package/ios/Skeleton/Skeleton.swift +22 -27
  142. package/ios/Slider/Slider.swift +16 -18
  143. package/ios/Stepper/Stepper.swift +6 -1
  144. package/ios/Steps/Steps.swift +8 -6
  145. package/ios/SuggestAction/SuggestAction.swift +5 -0
  146. package/ios/Swipeable/SwipeCell.swift +10 -6
  147. package/ios/Switch/Switch.swift +22 -20
  148. package/ios/TabView/TabView.swift +102 -21
  149. package/ios/Tag/Tag.swift +15 -4
  150. package/ios/Template/Logo/Logo.swift +2 -1
  151. package/ios/Template/TrustBanner/TrustBanner.swift +106 -54
  152. package/ios/Title/Title.swift +6 -17
  153. package/ios/Tooltip/Tooltip.swift +29 -14
  154. package/ios/Typography/Text.swift +61 -19
  155. package/ios/Uploader/Uploader.swift +3 -1
  156. package/ios/native-kits.podspec +2 -1
  157. package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
  158. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
  159. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +80 -0
  160. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +1 -0
  161. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
  162. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
  163. package/package.json +1 -1
  164. package/scripts/gen-ios-icons.mjs +36 -0
  165. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +0 -52
  166. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +0 -183
  167. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +0 -207
  168. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +0 -39
  169. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +0 -86
  170. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +0 -76
  171. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +0 -76
  172. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +0 -293
  173. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +0 -33
  174. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +0 -110
  175. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +0 -378
  176. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +0 -69
  177. package/ios/Application/Components.swift +0 -270
  178. package/ios/Application/ComposeApi.swift +0 -22
  179. package/ios/Application/HeaderRight.swift +0 -306
  180. package/ios/Application/Navigation/BottomTab/BottomTab.swift +0 -104
  181. package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
  182. package/ios/Application/Navigation/HeaderTitle.swift +0 -57
  183. package/ios/Application/Navigation/NavigationContainer.swift +0 -213
  184. package/ios/Application/Navigation/NavigationOptions.swift +0 -109
  185. package/ios/Application/Navigation/NavigatorManager.swift +0 -22
  186. package/ios/Application/Navigation/component/Header.swift +0 -178
  187. package/ios/Application/Navigation/component/HeaderBackground.swift +0 -83
  188. package/ios/Application/Navigation/component/HeaderColor.swift +0 -65
  189. package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +0 -11
  190. package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
  191. package/ios/Application/Navigation/component/NavigationHeaderRight.swift +0 -196
  192. package/ios/Application/Screen.swift +0 -250
  193. package/ios/Application/StackScreen.swift +0 -492
  194. /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
package/CLAUDE.md CHANGED
@@ -18,7 +18,7 @@ Guidance for working in this repo. Keep it accurate — update it when structure
18
18
  ## Source layout (`compose/src/commonMain/kotlin/vn/momo/kits/`)
19
19
 
20
20
  - `components/` — ~50 UI components, **one file per component** (`Button.kt`, `Input*.kt`, `PopupNotify.kt`, …).
21
- - `application/` — app-level context & config: `Context.kt` (CompositionLocals, `MiniAppContext`, `DesignSystemConfig`), `Localize.kt` (i18n: `LocalizationObject`, `Localize`, default dictionary), `Screen.kt`, header pieces. Also holds the **deprecated** `ApplicationContainer`.
21
+ - `application/` — app-level context & config: `Context.kt` (CompositionLocals, `MiniAppContext`, `DesignSystemConfig`), `Localize.kt` (i18n: `LocalizationObject`, `Localize`, default dictionary), `LiteScreen.kt` (standalone lightweight screen with its own header), `WidgetContainer.kt`.
22
22
  - `navigation/` — `NavigationContainer.kt` (**canonical entry point**), `Navigator.kt`, `Navigation.kt`, `StackScreen.kt`, `bottomtab/`, `component/`, `tracking/`.
23
23
  - `const/` — design tokens: `Theme.kt`, `Colors.kt`, `Typography.kt`, `Spacing.kt`, `Radius.kt`.
24
24
  - `layout/`, `modifier/`, `utils/`.
@@ -35,11 +35,18 @@ No unit-test suite is configured. Validate changes by compiling the relevant tar
35
35
  ./gradlew :sample:androidApp:installDebug # run demo on device/emulator
36
36
  ```
37
37
 
38
+ The SwiftUI kit (`ios/`, pod `MoMoUIKits`) compiles on its own through the sample's Pods project (`pod install` in `sample/iosApp` first when files are added):
39
+
40
+ ```bash
41
+ xcodebuild -project sample/iosApp/Pods/Pods.xcodeproj -scheme MoMoUIKits -sdk iphonesimulator \
42
+ -destination 'generic/platform=iOS Simulator' build
43
+ ```
44
+
38
45
  Publishing is automated (`publish.sh` + GitLab CI on `[ci build]` / `main`). Do not publish manually unless asked: `sh publish.sh beta` / `sh publish.sh release` (Maven→GitLab + NPM).
39
46
 
40
47
  ## Architecture essentials
41
48
 
42
- - **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. **Do not use** `ApplicationContainer` (`application/NavigationContainer.kt`) — deprecated and does **not** wire `LocalLocalize` / `LocalNavigator` / `LocalMaxApi`.
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.
43
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`.
44
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.
45
52
  - **Platform code** goes through `expect`/`actual` in `platform/` — never reference Android/iOS APIs from `commonMain` directly.
@@ -47,11 +54,32 @@ Publishing is automated (`publish.sh` + GitLab CI on `[ci build]` / `main`). Do
47
54
  - Navigation uses a runtime `DynamicScreenRegistry`; overlays (modal/bottom-sheet/snackbar) go through `Navigator` + `OverplayComponentRegistry`.
48
55
  - **iOS swipe-back:** the host owns the only edge-pan recogniser and replays it through `HostBackGestureBridge` (`navigation/HostBackGestureBridge.kt`; the kit framework is never exported, so the host reaches it through its own Koin-provided facade): `checkCanPop` / `canPopInner` / `pop` route the back into the kit stack, `started` / `progressed` / `completed` / `cancelled` seek the NavHost transition, `shouldUseFloatingArrowBack` is set by bottom tabs. `NavigationContainer` attaches itself automatically (`AttachHostBackGesture`, iOS only, detached by identity on dispose) — hosts need no wrapper and no `setNavigator`. `maxApi` is a required parameter of `NavigationContainer` — every container, nested or root, is handed its host bridge explicitly.
49
56
 
50
- ## Screen layout / iPhone Duo (port of RN `useScreenLayout`)
51
-
52
- - `layout/ScreenLayout.kt`: `getScreenLayout()` returns window size, side safe-area insets, `contentWidth`, header geometry and the iPhone Duo `sideBar`. `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.
53
- - Header height is `headerHeight` (not `AppStatusBar + HEADER_HEIGHT`); the bar row is `headerBarHeight`. When the status bar is off the top edge (iPhone landscape, iPhone Duo) `headerStatusBarHeight` is 0 so the title stays centered.
54
- - 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. The column starts below the camera / status-bar cluster: 170pt portrait, 120pt in any landscape (folded too — RN still uses 170 there), below the header when the status bar is on top; UIKit's `statusBarFrame` is useless on Duo. Unlike RN it is clamped 24pt above footer / tab bar, so a long toolkit (shortcut + group) never covers the footer.
57
+ ## SwiftUI kit (`ios/`)
58
+
59
+ - **Layout mirrors Compose:** `ios/Navigation/` follows `navigation/` file for file (`NavigationContainer`, `Navigator`, `Navigation` (options + per-screen `Navigation`), `StackScreen` (incl. `NavigationFooter`), `StackTransition`, `SwipeBackHaptic`, `BottomSheet`, `ModalScreen`, `HostBackGestureBridge`, `BottomTab/`, `Component/`, `Tracking/`); `ios/Layout/ScreenLayout.swift` mirrors `layout/`. `ios/Application/` keeps only context (`ApplicationEnvironment`), `Localize*` and `MaxApi` (Compose `application/`). Change a navigation piece on both sides together.
60
+ - **File basenames must be unique across the pod** — Swift fails with "filename used twice" even in different folders. New folders under `ios/` must be added to `source_files` in `ios/native-kits.podspec`.
61
+ - Screens update their own chrome at runtime through `@EnvironmentObject var navigation: Navigation` → `setOptions { $0.title = … }` (Compose `LocalNavigation`); bottom tabs keep one `Navigation` per tab. The host back bridge is `HostBackGestureBridge.shared` (`NavigatorManager` is a deprecated alias); a bottom tab off its first tab routes back to the first tab (`Navigator.tabBackHandler`, keyed by the hosting screen).
62
+ - Everything, navigation shell included, supports the pod's minimum **iOS 15** — no `@available` gates on public types. iOS 16+ APIs only behind `#available` with an iOS 15 path (`Layout`: `BottomSheet` `CapHeight`, `GridFlow` → `_VariadicView` flow row).
63
+ - **Usage is `NavigationContainer` + `Navigator` + `Navigation` only**; the kit owns the stack, transitions, overplays and back gesture. There is no `NavigationStack`: `StackTransition.swift` holds `StackHost` (representable) + `StackController`, the NavHost counterpart. Each entry gets its own `UIHostingController`, kept while on the stack and removed from the window while covered (state kept, onDisappear / onAppear like UINavigationController). Motion is a SwiftUI `offset` per card driven by one `coverage` value on a display link (a UIKit transform would move the safe area); it is Compose `StackTransition` 1:1 (RN `forStackTransitionWithOverlay`: linear 300ms slide, 0.3 parallax, `pink09` scrim to 0.6, 8pt edge shadow to 0.3). Card roots get the container environment via `.environment(\.self, …)` with the kit environment re-applied on top, so host environment objects reach screens.
64
+ - One back stack, root first (`Navigator.entries`, Compose's back stack): `present` appends a dialog entry that rises (FastOutSlowIn 300ms) and later pushes stack on top of it; `dismiss` drops the topmost dialog and everything above it (slides down whole); `popToRoot` returns to the active flow's root. Overplays (modal / bottom sheet / snackbar) render inside the screen they were shown over (`OverplayItem.screenId`, Compose `currentRootId`), so a later push slides over them.
65
+ - Host back gesture: `HostBackGestureBridge.started` / `progressed` / `completed` / `cancelled` scrub the pop (`Navigator.backStarted` …) only for a plain pop; tab back, `onBackHandler`, `preventBack`, an open overplay or a presented top wait for the release and go through `onBackSafe`. `SwipeBackHaptic` ticks at 50pt (`MaxApi.triggerEventVibration`, default = system light impact).
66
+ - Icons are compiled in: `ios/Icon/Icons.swift` is generated from Compose `utils/Icons.kt` by `node scripts/gen-ios-icons.mjs` — don't hand-edit it. A host `icon.json` in the main bundle only adds keys the kit lacks.
67
+ - Bundled assets (colors, Lottie JSON) live in `ios/Resources` and ship in the `MoMoUIKitsResources` bundle; load them through `MoMoUIKitsResources.bundle`, never `Bundle.main`.
68
+
69
+ ## Screen layout / iPhone Duo (port of RN `useScreenLayout`, momo-kits !664)
70
+
71
+ - `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.
73
+ - 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
+ - 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
+ - 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).
76
+ - Reserved regions (iOS 27.1 `reservedRegionsOfKind:`, reached through the ObjC runtime so older SDKs still build): a probe `UIView` (`platform/ReservedRegions.ios.kt` on the Compose root view; `ReservedRegionsProbe` inside SwiftUI `momoScreenLayout()`) re-reads them in `layoutSubviews`, which UIKit re-runs when they change. They push `sideBar.top` below the status cluster as it grows for calls / Live Activities, and `sideBar.bottom` (≥ 24pt) above a camera at the bottom end of the strip.
77
+ - Tab bar on Duo is `bottomtab/DuoTabBar.kt`: a rail in the side strip (`sideBar.bottom` above the window bottom, QR below it) or, without a side bar, a floating bar spanning 6/8 grid columns. Both overlay the content; `duoFloatingTabBarInset` pads tab content, footer and FAB.
78
+ - 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
+ - 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
+ - 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: popups cap at 440pt wide and the room between `duoTopInset` and the bottom inset (body scrolls, actions stay); modal centers inside that room; bottom sheet uses the window height, centers at width ≥ 600 and pads by side insets in landscape.
82
+ - Font scale deliberately uses the short side (`min(w, h)`), unlike RN which reads the window width — landscape must not inflate every font.
55
83
  - `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`.
56
84
 
57
85
  ## i18n / Localize (port of RN `Localize`)
@@ -102,7 +130,7 @@ Prefer one tight line at the surprising spot over a paragraph. Match the (low) c
102
130
  ## Gotchas
103
131
 
104
132
  - **Resource qualifier drift:** the generated Compose `Res` namespace is `vn.momo.uikits.resources`, but `utils/Resources.kt` and `platform/Platform.ios.kt` reference `vn.momo.compose.resources`. Keep bundled strings/dictionaries as plain Kotlin (the `defaultLanguage` map in `Localize.kt`), not `composeResources/values`, to avoid iOS framework resource-loading issues.
105
- - Prefer `NavigationContainer` over the deprecated `ApplicationContainer`; prefer `IMaxApi`/`LocalMaxApi` over the deprecated `PlatformApi`.
133
+ - `LiteScreen` keeps its own `application.HeaderType` enum (`DEFAULT`/`EXTENDED`/`NONE`), unrelated to the sealed `navigation.component.HeaderType`; its toolkit is the navigation `Toolkit` (through `LocalMaxApi`).
106
134
  - iOS strings/maps must survive Kotlin/Native framework packaging — keep them compiled-in, not loaded from resource bundles.
107
135
 
108
136
  ## Code comments
package/README.md CHANGED
@@ -150,8 +150,8 @@ themeController(defaultTheme.copy(colors = defaultTheme.colors.copy(primary = Co
150
150
  ---
151
151
 
152
152
  ### Lưu ý khác
153
- - `Screen` composable cũ được đánh dấu `@Deprecated`; sử dụng `NavigationContainer + StackScreen` để hưởng lợi dynamic route và animation.
154
- - `DesignSystemWhiteList` trong `application/ApplicationContainer.kt` hỗ trợ fallback cho host cũ; đừng kích hoạt nếu không cần.
153
+ - `Screen` / `ApplicationContainer` cũ (Compose và SwiftUI) đã bị gỡ; dùng `NavigationContainer + StackScreen`.
154
+ - `KitDesignSystem.update(isWhiteList = …)` (`const/KitDesignSystem.kt`) bật font design system mới cho host trong whitelist; đừng kích hoạt nếu không cần.
155
155
  - Khi thêm tài nguyên mới, đặt trong `shared/src/commonMain/composeResources` để Compose Multiplatform plugin sinh accessor tự động.
156
156
  - Repo không bundle SDK bên thứ ba ngoại trừ `vn.momo.corex.maxapi`, Compose/Coil/Ktor **và Compottie** cho animation.
157
157
 
@@ -40,7 +40,7 @@ kotlin {
40
40
  }
41
41
 
42
42
  cocoapods {
43
- version = "0.167.1-duo.2-debug"
43
+ version = "0.167.1-duo.20-debug"
44
44
  summary = "IOS Shared module"
45
45
  homepage = "https://momo.vn"
46
46
  ios.deploymentTarget = "15.0"
@@ -188,3 +188,17 @@ compose {
188
188
  // }
189
189
  // }
190
190
  // }
191
+
192
+ // Xcode 27 rejects pods below iOS 15 (lottie-ios 4.4.3 declares 13.0), and the synthetic Podfile has no post_install hook.
193
+ val syntheticPodsProject = layout.buildDirectory.file("cocoapods/synthetic/ios/Pods/Pods.xcodeproj/project.pbxproj")
194
+ tasks.matching { it.name.startsWith("podInstallSynthetic") }.configureEach {
195
+ doLast {
196
+ val pbxproj = syntheticPodsProject.get().asFile
197
+ if (!pbxproj.exists()) return@doLast
198
+ val target = Regex("IPHONEOS_DEPLOYMENT_TARGET = ([0-9.]+);")
199
+ pbxproj.writeText(pbxproj.readText().replace(target) { match ->
200
+ val major = match.groupValues[1].substringBefore('.').toIntOrNull() ?: 0
201
+ if (major < 15) "IPHONEOS_DEPLOYMENT_TARGET = 15.0;" else match.value
202
+ })
203
+ }
204
+ }
@@ -188,3 +188,17 @@ mavenPublishing {
188
188
  }
189
189
  }
190
190
  }
191
+
192
+ // Xcode 27 rejects pods below iOS 15 (lottie-ios 4.4.3 declares 13.0), and the synthetic Podfile has no post_install hook.
193
+ val syntheticPodsProject = layout.buildDirectory.file("cocoapods/synthetic/ios/Pods/Pods.xcodeproj/project.pbxproj")
194
+ tasks.matching { it.name.startsWith("podInstallSynthetic") }.configureEach {
195
+ doLast {
196
+ val pbxproj = syntheticPodsProject.get().asFile
197
+ if (!pbxproj.exists()) return@doLast
198
+ val target = Regex("IPHONEOS_DEPLOYMENT_TARGET = ([0-9.]+);")
199
+ pbxproj.writeText(pbxproj.readText().replace(target) { match ->
200
+ val major = match.groupValues[1].substringBefore('.').toIntOrNull() ?: 0
201
+ if (major < 15) "IPHONEOS_DEPLOYMENT_TARGET = 15.0;" else match.value
202
+ })
203
+ }
204
+ }
@@ -1,6 +1,6 @@
1
1
  Pod::Spec.new do |spec|
2
2
  spec.name = 'compose'
3
- spec.version = '0.167.1-beta.4-debug'
3
+ spec.version = '0.167.1-duo.15-debug'
4
4
  spec.homepage = 'https://momo.vn'
5
5
  spec.source = { :http=> ''}
6
6
  spec.authors = ''
@@ -36,6 +36,8 @@ import androidx.activity.compose.BackHandler as AndroidBackHandler
36
36
 
37
37
  actual fun getPlatformName(): String = "Android"
38
38
 
39
+ actual fun isPhoneIdiom(): Boolean = false
40
+
39
41
  @Composable
40
42
  actual fun getScreenDimensions(): ScreenDimension {
41
43
  val config = LocalConfiguration.current
@@ -0,0 +1,6 @@
1
+ package vn.momo.kits.platform
2
+
3
+ import androidx.compose.runtime.Composable
4
+
5
+ @Composable
6
+ internal actual fun rememberReservedRegions(): List<ReservedRegion> = emptyList()
@@ -1,17 +1,37 @@
1
1
  package vn.momo.kits.application
2
2
 
3
3
  import androidx.annotation.FloatRange
4
- import androidx.compose.animation.animateContentSize
5
- import androidx.compose.foundation.*
4
+ import androidx.compose.foundation.ScrollState
5
+ import androidx.compose.foundation.background
6
+ import androidx.compose.foundation.border
6
7
  import androidx.compose.foundation.gestures.detectTapGestures
7
8
  import androidx.compose.foundation.interaction.FocusInteraction
8
9
  import androidx.compose.foundation.interaction.MutableInteractionSource
9
- import androidx.compose.foundation.layout.*
10
+ import androidx.compose.foundation.layout.Arrangement
11
+ import androidx.compose.foundation.layout.Box
12
+ import androidx.compose.foundation.layout.Row
13
+ import androidx.compose.foundation.layout.fillMaxSize
14
+ import androidx.compose.foundation.layout.padding
15
+ import androidx.compose.foundation.layout.size
16
+ import androidx.compose.foundation.rememberScrollState
10
17
  import androidx.compose.foundation.shape.CircleShape
11
18
  import androidx.compose.foundation.text.BasicTextField
12
19
  import androidx.compose.foundation.text.KeyboardActions
13
20
  import androidx.compose.foundation.text.KeyboardOptions
14
- import androidx.compose.runtime.*
21
+ import androidx.compose.foundation.verticalScroll
22
+ import androidx.compose.runtime.Composable
23
+ import androidx.compose.runtime.LaunchedEffect
24
+ import androidx.compose.runtime.MutableState
25
+ import androidx.compose.runtime.Stable
26
+ import androidx.compose.runtime.State
27
+ import androidx.compose.runtime.derivedStateOf
28
+ import androidx.compose.runtime.getValue
29
+ import androidx.compose.runtime.mutableStateOf
30
+ import androidx.compose.runtime.produceState
31
+ import androidx.compose.runtime.remember
32
+ import androidx.compose.runtime.rememberUpdatedState
33
+ import androidx.compose.runtime.snapshotFlow
34
+ import androidx.compose.runtime.staticCompositionLocalOf
15
35
  import androidx.compose.ui.Alignment
16
36
  import androidx.compose.ui.Modifier
17
37
  import androidx.compose.ui.composed
@@ -25,13 +45,25 @@ import androidx.compose.ui.graphics.Color
25
45
  import androidx.compose.ui.graphics.SolidColor
26
46
  import androidx.compose.ui.graphics.graphicsLayer
27
47
  import androidx.compose.ui.input.pointer.pointerInput
28
- import androidx.compose.ui.layout.*
48
+ import androidx.compose.ui.layout.Layout
49
+ import androidx.compose.ui.layout.Measurable
50
+ import androidx.compose.ui.layout.MeasurePolicy
51
+ import androidx.compose.ui.layout.MeasureResult
52
+ import androidx.compose.ui.layout.MeasureScope
53
+ import androidx.compose.ui.layout.Placeable
54
+ import androidx.compose.ui.layout.layoutId
29
55
  import androidx.compose.ui.platform.LocalDensity
30
56
  import androidx.compose.ui.platform.LocalFocusManager
31
57
  import androidx.compose.ui.platform.LocalSoftwareKeyboardController
32
58
  import androidx.compose.ui.text.TextStyle
33
59
  import androidx.compose.ui.text.style.TextOverflow
34
- import androidx.compose.ui.unit.*
60
+ import androidx.compose.ui.unit.Constraints
61
+ import androidx.compose.ui.unit.Dp
62
+ import androidx.compose.ui.unit.IntOffset
63
+ import androidx.compose.ui.unit.LayoutDirection
64
+ import androidx.compose.ui.unit.dp
65
+ import androidx.compose.ui.unit.lerp
66
+ import androidx.compose.ui.unit.sp
35
67
  import kotlinx.coroutines.flow.collectLatest
36
68
  import kotlinx.coroutines.flow.mapNotNull
37
69
  import vn.momo.kits.components.Icon
@@ -41,14 +73,26 @@ import vn.momo.kits.const.Colors
41
73
  import vn.momo.kits.const.Spacing
42
74
  import vn.momo.kits.const.Typography
43
75
  import vn.momo.kits.const.getFontFamily
76
+ import vn.momo.kits.layout.DuoMetrics
77
+ import vn.momo.kits.layout.ScreenLayout
78
+ import vn.momo.kits.layout.SideBarEdge
79
+ import vn.momo.kits.layout.getScreenLayout
80
+ import vn.momo.kits.utils.horizontalSafeAreaPadding
44
81
  import vn.momo.kits.modifier.kitsAutomationId
45
82
  import vn.momo.kits.modifier.noFeedbackClickable
46
83
  import vn.momo.kits.modifier.setAutomationId
47
- import vn.momo.kits.navigation.component.HEADER_FONT_FAMILY
84
+ import vn.momo.kits.navigation.component.headerFontFamily
48
85
  import vn.momo.kits.navigation.component.HEADER_HEIGHT
86
+ import vn.momo.kits.navigation.component.HeaderColor
87
+ import vn.momo.kits.navigation.component.HeaderRight
88
+ import vn.momo.kits.navigation.component.SideBarButton
89
+ import vn.momo.kits.navigation.component.TitlePosition
90
+ import vn.momo.kits.navigation.component.ToolGroup
91
+ import vn.momo.kits.navigation.component.Toolkit
49
92
  import vn.momo.kits.utils.getAppStatusBarHeight
50
93
  import kotlin.math.max
51
94
  import kotlin.math.roundToInt
95
+ import vn.momo.kits.navigation.component.getHeaderColor as getNavigationHeaderColor
52
96
 
53
97
 
54
98
  data class AnimationOption(
@@ -73,6 +117,30 @@ data class AnimationOption(
73
117
 
74
118
  val LocalAnimationOption = staticCompositionLocalOf<AnimationOption?> { null }
75
119
 
120
+ // LiteScreen's own header kinds; StackScreen uses the sealed navigation.component.HeaderType.
121
+ enum class HeaderType {
122
+ DEFAULT,
123
+ EXTENDED,
124
+ NONE
125
+ }
126
+
127
+ @Stable
128
+ data class HeaderRightData(
129
+ val useShortcut: Boolean = false,
130
+ val useMore: Boolean = false,
131
+ val useSystemTools: Boolean = true,
132
+ val tools: List<ToolGroup> = emptyList(),
133
+ val toolCallback: ((String) -> Unit)? = { _ -> }
134
+ ) {
135
+ internal fun toToolkit() = HeaderRight.Toolkit(
136
+ useShortcut = useShortcut,
137
+ useMore = useMore,
138
+ useSystemTools = useSystemTools,
139
+ tools = tools,
140
+ toolCallback = toolCallback,
141
+ )
142
+ }
143
+
76
144
  @Composable
77
145
  fun LiteScreen(
78
146
  scrollable: Boolean = true,
@@ -169,6 +237,8 @@ private fun LiteScreenLayout(
169
237
  content: @Composable () -> Unit,
170
238
  ) {
171
239
  val statusBarHeight = getAppStatusBarHeight()
240
+ val screenLayout = getScreenLayout()
241
+ val applySafeAreaPadding = screenLayout.sideBar != null
172
242
  val isHeaderNone = remember(headerType) {
173
243
  headerType == HeaderType.NONE
174
244
  }
@@ -204,6 +274,7 @@ private fun LiteScreenLayout(
204
274
  borderColor = Colors.black_20.copy(alpha = 0.2f)
205
275
  )
206
276
  }
277
+ val toolkit = remember(headerRightData) { (headerRightData ?: HeaderRightData()).toToolkit() }
207
278
 
208
279
  val scrollPercentage = produceState(
209
280
  initialValue = 0f,
@@ -230,6 +301,12 @@ private fun LiteScreenLayout(
230
301
  val animationOpacityModifier = Modifier.graphicsLayer {
231
302
  alpha = animationOption?.animateOpacity ?: 1f
232
303
  }
304
+ val sideBarColor = getNavigationHeaderColor(
305
+ vn.momo.kits.navigation.component.HeaderType.Default(),
306
+ 1f,
307
+ null,
308
+ Colors.black_17,
309
+ )
233
310
  val titleModifier = Modifier
234
311
  .kitsAutomationId("title_navigation_header")
235
312
  .layoutId(HeaderId.TITLE_ID)
@@ -241,6 +318,8 @@ private fun LiteScreenLayout(
241
318
  }
242
319
 
243
320
  val policy = remember(
321
+ screenLayout,
322
+ applySafeAreaPadding,
244
323
  useAnimationSearch,
245
324
  isHeaderNone,
246
325
  isHeaderExtend,
@@ -253,6 +332,8 @@ private fun LiteScreenLayout(
253
332
  horizontalAlignment,
254
333
  ) {
255
334
  LiteScreenLayoutPolicy(
335
+ screenLayout = screenLayout,
336
+ applySafeAreaPadding = applySafeAreaPadding,
256
337
  useAnimationSearch = useAnimationSearch,
257
338
  isHeaderNone = isHeaderNone,
258
339
  isHeaderExtend = isHeaderExtend,
@@ -267,13 +348,13 @@ private fun LiteScreenLayout(
267
348
  }
268
349
 
269
350
  Layout(
270
- modifier = modifier
271
- .animateContentSize(),
351
+ modifier = modifier,
272
352
  content = {
273
353
  Box(
274
354
  modifier = animationOpacityModifier
275
355
  .layoutId(HeaderId.CONTENT_ID)
276
- .then(contentModifier),
356
+ .then(contentModifier)
357
+ .then(if (applySafeAreaPadding) Modifier.horizontalSafeAreaPadding() else Modifier),
277
358
  contentAlignment = Alignment.TopCenter,
278
359
  ) {
279
360
  content()
@@ -309,28 +390,44 @@ private fun LiteScreenLayout(
309
390
  }
310
391
 
311
392
  if (!isHeaderNone && onGoBack != null) {
312
- val coreModifier = Modifier
313
- .layoutId(HeaderId.BACK_ID)
314
- .then(animationOpacityModifier)
315
- .clip(CircleShape)
316
- .noFeedbackClickable(onClick = onGoBack)
317
- .setAutomationId("btn_navigation_back")
318
- inputSearchProps?.customBackIcon?.invoke(coreModifier) ?: Box(
319
- modifier = coreModifier
320
- .size(28.dp)
321
- .border(
322
- width = 0.2.dp,
323
- color = headerColor.borderColor,
324
- shape = CircleShape,
325
- )
326
- .background(color = headerColor.backgroundButton)
327
- .padding(Spacing.XS),
328
- ) {
329
- Icon(
330
- source = "arrow-back",
331
- color = headerColor.tintIconColor,
332
- size = 20.dp,
333
- )
393
+ when {
394
+ screenLayout.sideBar != null -> {
395
+ Box(
396
+ modifier = Modifier.layoutId(HeaderId.BACK_ID),
397
+ ) {
398
+ SideBarButton(
399
+ icon = "arrow-back",
400
+ headerColor = sideBarColor,
401
+ onClick = onGoBack,
402
+ automationId = "btn_navigation_back",
403
+ )
404
+ }
405
+ }
406
+ else -> {
407
+ val coreModifier = Modifier
408
+ .layoutId(HeaderId.BACK_ID)
409
+ .then(animationOpacityModifier)
410
+ .clip(CircleShape)
411
+ .noFeedbackClickable(onClick = onGoBack)
412
+ .setAutomationId("btn_navigation_back")
413
+ inputSearchProps?.customBackIcon?.invoke(coreModifier) ?: Box(
414
+ modifier = coreModifier
415
+ .size(28.dp)
416
+ .border(
417
+ width = 0.2.dp,
418
+ color = headerColor.borderColor,
419
+ shape = CircleShape,
420
+ )
421
+ .background(color = headerColor.backgroundButton)
422
+ .padding(Spacing.XS),
423
+ ) {
424
+ Icon(
425
+ source = "arrow-back",
426
+ color = headerColor.tintIconColor,
427
+ size = 20.dp,
428
+ )
429
+ }
430
+ }
334
431
  }
335
432
  }
336
433
 
@@ -343,9 +440,10 @@ private fun LiteScreenLayout(
343
440
  if (headerRight != null) {
344
441
  headerRight()
345
442
  } else {
346
- HeaderRight(
347
- headerRight = headerRightData,
443
+ Toolkit(
444
+ headerRight = toolkit,
348
445
  tintColor = tintColor,
446
+ headerColor = headerColor,
349
447
  )
350
448
  }
351
449
  }
@@ -362,7 +460,7 @@ private fun LiteScreenLayout(
362
460
  text = title,
363
461
  color = headerColor.tintIconColor,
364
462
  style = titleStyle,
365
- fontFamily = HEADER_FONT_FAMILY,
463
+ fontFamily = headerFontFamily,
366
464
  modifier = titleModifier,
367
465
  maxLines = 1,
368
466
  overflow = TextOverflow.Ellipsis
@@ -374,6 +472,8 @@ private fun LiteScreenLayout(
374
472
  }
375
473
 
376
474
  private class LiteScreenLayoutPolicy(
475
+ private val screenLayout: ScreenLayout,
476
+ private val applySafeAreaPadding: Boolean,
377
477
  private val useAnimationSearch: Boolean,
378
478
  private val isHeaderNone: Boolean,
379
479
  private val isHeaderExtend: Boolean,
@@ -407,10 +507,13 @@ private class LiteScreenLayoutPolicy(
407
507
 
408
508
  val contentMeasurable = measurables.find { it.layoutId == HeaderId.CONTENT_ID }
409
509
 
510
+ val insetLeft = if (applySafeAreaPadding) screenLayout.insetLeft.roundToPx() else 0
511
+ val insetRight = if (applySafeAreaPadding) screenLayout.insetRight.roundToPx() else 0
512
+ val safeWidth = (constraints.maxWidth - insetLeft - insetRight).coerceAtLeast(0)
410
513
  val realConstraints = constraints.copy(
411
514
  minWidth = 0,
412
515
  minHeight = 0,
413
- maxWidth = (constraints.maxWidth - spacing12 * 2)
516
+ maxWidth = (safeWidth - spacing12 * 2)
414
517
  .coerceAtLeast(0),
415
518
  )
416
519
  val backIconPlaceable =
@@ -421,9 +524,10 @@ private class LiteScreenLayoutPolicy(
421
524
  maxWidth = realConstraints.maxWidth / 2,
422
525
  )
423
526
  )
527
+ val headerBackWidth = if (screenLayout.sideBar == null) backIconPlaceable.safeWidth else 0
424
528
  val baseInputSearchConstraints = if (isHeaderExtend) {
425
529
  val minWidth =
426
- if (useAnimationSearch) realConstraints.maxWidth - backIconPlaceable.safeWidth - headerRightPlaceable.safeWidth - spaceBetween * 2
530
+ if (useAnimationSearch) realConstraints.maxWidth - headerBackWidth - headerRightPlaceable.safeWidth - spaceBetween * 2
427
531
  else realConstraints.maxWidth
428
532
  realConstraints.copy(
429
533
  maxWidth = (realConstraints.maxWidth * (1 - scrollPercent)).toInt()
@@ -431,7 +535,7 @@ private class LiteScreenLayoutPolicy(
431
535
  )
432
536
  } else {
433
537
  var spaceConsumed = 0
434
- if (backIconPlaceable.safeWidth != 0) spaceConsumed += backIconPlaceable.safeWidth + spaceBetween
538
+ if (headerBackWidth != 0) spaceConsumed += headerBackWidth + spaceBetween
435
539
  if (headerRightPlaceable.safeWidth != 0) spaceConsumed += headerRightPlaceable.safeWidth + spaceBetween
436
540
  realConstraints.copy(
437
541
  maxWidth = realConstraints.maxWidth - spaceConsumed
@@ -442,7 +546,7 @@ private class LiteScreenLayoutPolicy(
442
546
  ?.measure(inputSearchConstraints)
443
547
  val titlePlaceable = measurables.find { it.layoutId == HeaderId.TITLE_ID }?.measure(
444
548
  constraints = realConstraints.copy(
445
- maxWidth = realConstraints.maxWidth - backIconPlaceable.safeWidth - headerRightPlaceable.safeWidth - spaceBetween * 2
549
+ maxWidth = realConstraints.maxWidth - headerBackWidth - headerRightPlaceable.safeWidth - spaceBetween * 2
446
550
  )
447
551
  )
448
552
 
@@ -464,6 +568,7 @@ private class LiteScreenLayoutPolicy(
464
568
 
465
569
  val layoutWidth = constraints.maxWidth
466
570
  val layoutHeight = constraints.maxHeight
571
+ val backgroundInsetLeft = if (!applySafeAreaPadding && screenLayout.sideBar != null) screenLayout.insetLeft.roundToPx() else 0
467
572
  val contentHeight = (layoutHeight - headerHeight).coerceAtLeast(0)
468
573
  val contentPlaceable = contentMeasurable?.measure(
469
574
  constraints.copy(
@@ -500,7 +605,7 @@ private class LiteScreenLayoutPolicy(
500
605
  ) {
501
606
  val progress = animationOption?.progress?.value ?: 1f
502
607
 
503
- val startX = spacing12
608
+ val startX = insetLeft + spacing12
504
609
  val startY = headerY + statusBarPx
505
610
  var curX = startX
506
611
  var curY = startY
@@ -515,27 +620,38 @@ private class LiteScreenLayoutPolicy(
515
620
  )
516
621
 
517
622
  backgroundPlaceable?.place(
518
- x = 0,
623
+ x = -backgroundInsetLeft,
519
624
  y = headerY,
520
625
  )
521
626
 
522
627
  if (backIconPlaceable != null) {
523
- backIconPlaceable.place(
524
- x = startX,
525
- y = startY + backIconPlaceable.verticalCenterOffset(headerRowDefault),
526
- )
527
- curX += backIconPlaceable.safeWidth + spaceBetween
628
+ val sideBar = screenLayout.sideBar
629
+ if (sideBar == null) {
630
+ backIconPlaceable.place(
631
+ x = startX,
632
+ y = startY + backIconPlaceable.verticalCenterOffset(headerRowDefault),
633
+ )
634
+ curX += backIconPlaceable.safeWidth + spaceBetween
635
+ } else {
636
+ val x = when (sideBar.edge) {
637
+ SideBarEdge.LEFT -> insetLeft - screenLayout.insetLeft.roundToPx() + sideBar.width.roundToPx() - spacing12 - backIconPlaceable.width
638
+ SideBarEdge.RIGHT -> layoutWidth + (if (applySafeAreaPadding) 0 else screenLayout.insetRight.roundToPx()) - sideBar.width.roundToPx() + spacing12
639
+ }
640
+ backIconPlaceable.place(
641
+ x = x,
642
+ y = sideBar.top.roundToPx() + (DuoMetrics.controlSize.roundToPx() - backIconPlaceable.height) / 2,
643
+ )
644
+ }
528
645
  }
529
-
530
646
  headerRightPlaceable?.place(
531
- x = layoutWidth - spacing12 - headerRightPlaceable.safeWidth,
647
+ x = layoutWidth - insetRight - spacing12 - headerRightPlaceable.safeWidth,
532
648
  y = startY + headerRightPlaceable.verticalCenterOffset(headerRowDefault),
533
649
  )
534
650
 
535
651
  val titleOffset = IntOffset(
536
652
  x = if (titlePosition == TitlePosition.LEFT) curX
537
- else titlePlaceable.horizontalCenterOffset(
538
- space = layoutWidth,
653
+ else insetLeft + titlePlaceable.horizontalCenterOffset(
654
+ space = safeWidth - spacing12 * 2,
539
655
  layoutDirection = layoutDirection,
540
656
  ),
541
657
  y = startY + titlePlaceable.verticalCenterOffset(headerRowDefault),
@@ -543,7 +659,7 @@ private class LiteScreenLayoutPolicy(
543
659
 
544
660
  titlePlaceable?.place(titleOffset)
545
661
 
546
- if (backIconPlaceable != null || headerRightPlaceable != null || titlePlaceable != null) {
662
+ if ((backIconPlaceable != null && screenLayout.sideBar == null) || headerRightPlaceable != null || titlePlaceable != null) {
547
663
  curY += headerRowDefault
548
664
  }
549
665
 
@@ -553,7 +669,7 @@ private class LiteScreenLayoutPolicy(
553
669
  startY + inputSearchPlaceable.verticalCenterOffset(headerRowDefault)
554
670
  )
555
671
  IntOffset(
556
- x = startX + ((backIconPlaceable.safeWidth + spaceBetween) * (scrollPercent * 2f).coerceIn(
672
+ x = startX + ((headerBackWidth + spaceBetween) * (scrollPercent * 2f).coerceIn(
557
673
  0f, 1f
558
674
  )).toInt(),
559
675
  y = y,