@momo-kits/native-kits 0.167.1-duo.13-debug → 0.167.1-duo.14-debug

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CLAUDE.md CHANGED
@@ -68,7 +68,7 @@ Publishing is automated (`publish.sh` + GitLab CI on `[ci build]` / `main`). Do
68
68
 
69
69
  - `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.
70
70
  - 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.
71
- - 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.
71
+ - 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, where the header is 76pt and the row starts at 14pt so the title centers on the camera punchout.
72
72
  - 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).
73
73
  - 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.
74
74
  - 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.
@@ -40,7 +40,7 @@ kotlin {
40
40
  }
41
41
 
42
42
  cocoapods {
43
- version = "0.167.1-duo.13-debug"
43
+ version = "0.167.1-duo.14-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.167.1-duo.13-debug'
3
+ spec.version = '0.167.1-beta.4-debug'
4
4
  spec.homepage = 'https://momo.vn'
5
5
  spec.source = { :http=> ''}
6
6
  spec.authors = ''
@@ -97,10 +97,8 @@ fun getScreenLayout(): ScreenLayout =
97
97
  internal fun calculateScreenLayout(topInset: Dp, parent: ScreenLayout? = null): ScreenLayout {
98
98
  val isPhone = isPhoneIdiom()
99
99
  if (parent != null) {
100
- // the parent's row center reproduces its own centered geometry
101
- val titleCenterY = parent.headerStatusBarHeight + parent.headerBarHeight / 2
102
100
  val (headerHeight, headerStatusBarHeight) =
103
- headerGeometry(topInset, isPhone, parent.width, parent.height, parent.sideBar != null, titleCenterY)
101
+ headerGeometry(topInset, isPhone, parent.width, parent.height, parent.sideBar != null)
104
102
  return parent.copy(headerHeight = headerHeight, headerStatusBarHeight = headerStatusBarHeight)
105
103
  }
106
104
 
@@ -141,53 +139,32 @@ internal fun calculateScreenLayout(topInset: Dp, parent: ScreenLayout? = null):
141
139
  internal fun ScreenLayout.consumeHorizontalInsets(): ScreenLayout =
142
140
  copy(width = contentWidth, insetLeft = 0.dp, insetRight = 0.dp, sideBar = null)
143
141
 
144
- // titleCenterY: window y the title row centers on for the outer display in landscape.
145
- private fun headerGeometry(
146
- topInset: Dp,
147
- isPhone: Boolean,
148
- width: Dp,
149
- height: Dp,
150
- hasSideBar: Boolean,
151
- titleCenterY: Dp,
152
- ): Pair<Dp, Dp> {
142
+ private fun headerGeometry(topInset: Dp, isPhone: Boolean, width: Dp, height: Dp, hasSideBar: Boolean): Pair<Dp, Dp> {
153
143
  val headerHeight = topInset + HEADER_HEIGHT.dp
154
144
  if (!isPhone) return headerHeight to topInset
155
- if (hasSideBar && topInset < STATUS_BAR_MIN_INSET) {
156
- if (height > width && width < DuoMetrics.innerMinHeight) {
157
- // outer display: the row starts below the top so the title centers on the camera
158
- return DuoMetrics.outerHeaderHeight to DuoMetrics.outerCameraCenterY * 2 - DuoMetrics.outerHeaderHeight
159
- }
160
- if (width > height && height < DuoMetrics.innerMinHeight) {
161
- // reaches 8pt below a side bar control centered on the row
162
- val centered = maxOf(IPHONE_HEADER_MIN_HEIGHT, titleCenterY + DuoMetrics.controlSize / 2 + Spacing.S)
163
- return centered to maxOf(0.dp, titleCenterY * 2 - centered)
164
- }
145
+ if (hasSideBar && topInset < STATUS_BAR_MIN_INSET && height > width && width < DuoMetrics.innerMinHeight) {
146
+ // outer display: the row starts below the top so the title centers on the camera
147
+ return DuoMetrics.outerHeaderHeight to DuoMetrics.outerCameraCenterY * 2 - DuoMetrics.outerHeaderHeight
165
148
  }
166
149
  return maxOf(headerHeight, IPHONE_HEADER_MIN_HEIGHT) to if (topInset < STATUS_BAR_MIN_INSET) 0.dp else topInset
167
150
  }
168
151
 
169
- private data class StripOcclusion(val top: Dp, val topCenter: Dp, val bottom: Dp)
152
+ private data class StripOcclusion(val top: Dp, val bottom: Dp)
170
153
 
171
154
  // How far system regions reach into a side strip from its top and from its bottom end
172
- // (e.g. the camera, which sits at either end depending on the rotation), and the center y
173
- // of the region reaching furthest from the top.
155
+ // (e.g. the camera, which sits at either end depending on the rotation).
174
156
  private fun stripOcclusion(regions: List<ReservedRegion>, stripLeft: Dp, stripRight: Dp, windowHeight: Dp): StripOcclusion {
175
157
  var top = 0.dp
176
- var topCenter = 0.dp
177
158
  var bottom = 0.dp
178
159
  regions.forEach { region ->
179
160
  if (region.x >= stripRight || region.x + region.width <= stripLeft) return@forEach
180
- val centerY = region.y + region.height / 2
181
- if (centerY < windowHeight / 2) {
182
- if (region.y + region.height > top) {
183
- top = region.y + region.height
184
- topCenter = centerY
185
- }
161
+ if (region.y + region.height / 2 < windowHeight / 2) {
162
+ top = maxOf(top, region.y + region.height)
186
163
  } else {
187
164
  bottom = maxOf(bottom, windowHeight - region.y)
188
165
  }
189
166
  }
190
- return StripOcclusion(top, topCenter, bottom)
167
+ return StripOcclusion(top, bottom)
191
168
  }
192
169
 
193
170
  // Slab iPhones report equal side insets in landscape; iPhone Duo moves the status bar to one side.
@@ -212,15 +189,9 @@ private fun screenLayoutOf(
212
189
  val isIPhoneDuo = isPhone && isIPhoneDuoWindow(width, height, asymmetricInsets)
213
190
 
214
191
  var sideBar: SideBarLayout? = null
215
- var titleCenterY = 0.dp
216
192
  val sideInset = maxOf(insetLeft, insetRight)
217
193
  // narrower strips cannot host a control 12pt from the inner edge
218
194
  if (isIPhoneDuo && sideInset >= DuoMetrics.controlSize + Spacing.M) {
219
- val edge = if (insetRight >= insetLeft) SideBarEdge.RIGHT else SideBarEdge.LEFT
220
- val stripLeft = if (edge == SideBarEdge.RIGHT) width - sideInset else 0.dp
221
- // clear of the camera and of the status bar cluster, which grows for calls and Live
222
- // Activities; region frames already include the system's margin
223
- val occlusion = stripOcclusion(reservedRegions, stripLeft, stripLeft + sideInset, height)
224
195
  // UIKit reports no usable status bar frame here (a 2pt top strip), so the spot is inferred like RN
225
196
  val top = when {
226
197
  topInset >= STATUS_BAR_MIN_INSET -> topInset + HEADER_HEIGHT.dp + Spacing.M
@@ -229,9 +200,11 @@ private fun screenLayoutOf(
229
200
  // outer display in landscape: no status bar in the strip
230
201
  else -> DuoMetrics.edgeInset
231
202
  }
232
- // outer display in landscape: the title row centers on the camera when it sits at the
233
- // top end, otherwise on the back button
234
- titleCenterY = if (occlusion.top > top) occlusion.topCenter else top + DuoMetrics.controlSize / 2
203
+ val edge = if (insetRight >= insetLeft) SideBarEdge.RIGHT else SideBarEdge.LEFT
204
+ val stripLeft = if (edge == SideBarEdge.RIGHT) width - sideInset else 0.dp
205
+ // clear of the camera and of the status bar cluster, which grows for calls and Live
206
+ // Activities; region frames already include the system's margin
207
+ val occlusion = stripOcclusion(reservedRegions, stripLeft, stripLeft + sideInset, height)
235
208
  sideBar = SideBarLayout(
236
209
  edge = edge,
237
210
  width = sideInset,
@@ -239,8 +212,7 @@ private fun screenLayoutOf(
239
212
  bottom = maxOf(DuoMetrics.edgeInset, occlusion.bottom),
240
213
  )
241
214
  }
242
- val (headerHeight, headerStatusBarHeight) =
243
- headerGeometry(topInset, isPhone, width, height, sideBar != null, titleCenterY)
215
+ val (headerHeight, headerStatusBarHeight) = headerGeometry(topInset, isPhone, width, height, sideBar != null)
244
216
 
245
217
  return ScreenLayout(
246
218
  width = width,
@@ -431,10 +431,9 @@ fun Footer(
431
431
 
432
432
  val backgroundColor = if (transparent) Color.Transparent else AppTheme.current.colors.background.surface
433
433
 
434
- // inset as a whole, not padded inside, so the surface and its shadow stay off the iPhone Duo side bar
435
434
  Box(Modifier.onGloballyPositioned {
436
435
  if (footerHeightPx.intValue != it.size.height) footerHeightPx.intValue = it.size.height
437
- }.horizontalSafeAreaPadding()) {
436
+ }) {
438
437
  Box(
439
438
  Modifier
440
439
  .fillMaxWidth()
@@ -442,6 +441,7 @@ fun Footer(
442
441
  .conditional(IsShowBaseLineDebug) {
443
442
  border(1.dp, Colors.blue_03)
444
443
  }
444
+ .horizontalSafeAreaPadding()
445
445
  .padding(
446
446
  top = if (useMargin) Spacing.S else 0.dp,
447
447
  start = if (useMargin) Spacing.M else 0.dp,
@@ -28,6 +28,7 @@ import vn.momo.kits.navigation.LocalNavigator
28
28
  import vn.momo.kits.navigation.NavigationOptions
29
29
  import vn.momo.kits.navigation.StackScreen
30
30
  import vn.momo.kits.navigation.component.HeaderType
31
+ import vn.momo.kits.platform.BackHandler
31
32
  import vn.momo.kits.platform.reportFloatingArrowBack
32
33
  import vn.momo.kits.layout.getScreenLayout
33
34
 
@@ -50,10 +51,16 @@ fun BottomTab(
50
51
  // iPhone Duo: when the side bar can't hold both bars, which one starts compressed;
51
52
  // tapping the compressed one swaps them
52
53
  compressionBehavior: DuoBarCompressionBehavior = DuoBarCompressionBehavior.TOOLBAR,
54
+ initialRouteName: String? = null,
55
+ onTabPress: ((index: Int, switchTab: () -> Unit) -> Unit)? = null,
56
+ backToInitialTab: Boolean = true,
53
57
  ) {
54
58
  val navigation = LocalNavigation.current
55
59
  val navigator = LocalNavigator.current
56
60
  val navController = rememberNavController()
61
+ val startRoute = remember {
62
+ "option${items.indexOfFirst { it.name == initialRouteName }.coerceAtLeast(0)}"
63
+ }
57
64
 
58
65
  // Only on new items: this recomposes on every tab switch and would drop options a tab set at runtime.
59
66
  remember(items) {
@@ -68,15 +75,14 @@ fun BottomTab(
68
75
  )
69
76
  }
70
77
 
71
- // Tab 0 keeps the seeked transition; other tabs go to the host's floating arrow. Matches RN.
72
78
  val currentRoute = navController.currentBackStackEntryAsState().value?.destination?.route
73
- val isFirstTab = currentRoute == null || currentRoute == "option0"
79
+ val onStartTab = currentRoute == null || currentRoute == startRoute
80
+ val isFirstTab = onStartTab || !backToInitialTab
81
+ val backToStartTab = { navController.popBackStack(startRoute, inclusive = false) }
74
82
 
75
83
  DisposableEffect(isFirstTab) {
76
84
  reportFloatingArrowBack(!isFirstTab)
77
- navigator.tabBackHandler = if (isFirstTab) null else ({
78
- navController.popBackStack(navController.graph.startDestinationRoute!!, false)
79
- })
85
+ navigator.tabBackHandler = if (isFirstTab) null else ({ backToStartTab() })
80
86
  // both live outside this composition
81
87
  onDispose {
82
88
  reportFloatingArrowBack(false)
@@ -90,16 +96,19 @@ fun BottomTab(
90
96
  val railReserve = layout.sideBar?.takeIf { layout.isIPhoneDuo }?.let {
91
97
  it.bottom + duoRailHeight(items.size, hasQr, compressed = compression.tabBarCompressed) + DUO_GROUP_GAP
92
98
  } ?: 0.dp
93
- val onTabSelected: (Int) -> Unit = {
94
- val targetRoute = "option$it"
95
- if (currentRoute != targetRoute) {
96
- // one entry per tab above the first, so switching never grows the back stack
97
- navController.navigate(targetRoute) {
98
- popUpTo("option0") { saveState = true }
99
- launchSingleTop = true
100
- restoreState = true
99
+ val onTabSelected: (Int) -> Unit = { index ->
100
+ val switchTab = {
101
+ val targetRoute = "option$index"
102
+ if (navController.currentBackStackEntry?.destination?.route != targetRoute) {
103
+ // one entry per tab above the start tab, so switching never grows the back stack
104
+ navController.navigate(targetRoute) {
105
+ popUpTo(startRoute) { saveState = true }
106
+ launchSingleTop = true
107
+ restoreState = true
108
+ }
101
109
  }
102
110
  }
111
+ onTabPress?.invoke(index, switchTab) ?: switchTab()
103
112
  }
104
113
 
105
114
  Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.BottomCenter) {
@@ -112,7 +121,7 @@ fun BottomTab(
112
121
  ) {
113
122
  NavHost(
114
123
  navController = navController,
115
- startDestination = "option0"
124
+ startDestination = startRoute
116
125
  ) {
117
126
  items.forEachIndexed { index, item ->
118
127
  composable(
@@ -153,11 +162,8 @@ fun BottomTab(
153
162
  navigationOptions = option,
154
163
  bottomTabIndex = index,
155
164
  onBackHandler = {
156
- if (index != 0) {
157
- navController.popBackStack(
158
- navController.graph.startDestinationRoute!!,
159
- inclusive = false
160
- )
165
+ if (backToInitialTab && "option$index" != startRoute) {
166
+ backToStartTab()
161
167
  } else {
162
168
  navigator.pop()
163
169
  }
@@ -168,6 +174,9 @@ fun BottomTab(
168
174
  }
169
175
  }
170
176
  }
177
+ BackHandler(enabled = !onStartTab) {
178
+ if (backToInitialTab) backToStartTab() else navigator.pop()
179
+ }
171
180
  if (layout.isIPhoneDuo) {
172
181
  DuoTabBar(
173
182
  layout = layout,
@@ -22,7 +22,6 @@ import vn.momo.kits.layout.getScreenLayout
22
22
  import vn.momo.kits.modifier.conditional
23
23
  import vn.momo.kits.navigation.LocalOptions
24
24
  import vn.momo.kits.navigation.LocalScrollState
25
- import vn.momo.kits.navigation.getInputSearchType
26
25
  import vn.momo.kits.navigation.usesDuoGlassHeader
27
26
 
28
27
  enum class AnimatedHeaderRatio(val value: Float){
@@ -67,28 +66,18 @@ internal fun HeaderBackground() {
67
66
 
68
67
  // iPhone Duo glass header: the gradient leaves with the content, as the quick return header does
69
68
  val useDuoGlass = options.usesDuoGlassHeader(layout)
70
- // RN HeaderExtendHeader with search on the side bar layout: the gradient fills the header box,
71
- // which for extended shrinks from 52pt below the header over the first 100pt of scroll
72
- val duoSearch = layout.sideBar != null && getInputSearchType(options) != InputSearchType.None
73
- val fillsBox = useDuoGlass || duoSearch
74
- val duoModifier = when {
75
- useDuoGlass -> {
76
- val extra = if (options.headerType is HeaderType.Extended) DUO_GRADIENT_EXTRA else 0.dp
77
- Modifier.height(minHeight + extra).graphicsLayer { translationY = -scrollState.value.toFloat() }
78
- }
79
- duoSearch && options.headerType is HeaderType.Extended -> {
80
- val progress = (with(density) { scrollState.value.toDp() } / 100.dp).coerceIn(0f, 1f)
81
- Modifier.height(minHeight + DUO_GRADIENT_EXTRA * (1 - progress))
82
- }
83
- duoSearch -> Modifier.height(minHeight)
84
- else -> Modifier.height(height)
69
+ val duoModifier = if (useDuoGlass) {
70
+ val extra = if (options.headerType is HeaderType.Extended) DUO_GRADIENT_EXTRA else 0.dp
71
+ Modifier.height(minHeight + extra).graphicsLayer { translationY = -scrollState.value.toFloat() }
72
+ } else {
73
+ Modifier.height(height)
85
74
  }
86
75
 
87
76
  Box(Modifier.fillMaxWidth().then(duoModifier).clipToBounds().background(backgroundColor)) {
88
77
  // on Duo the fade spans the gradient box; a banner image keeps its 375:154 shape, clipped
89
78
  Box(Modifier
90
79
  .fillMaxWidth()
91
- .then(if (fillsBox && !theme.isHeaderImage()) Modifier.fillMaxHeight() else Modifier.height(maxHeight))
80
+ .then(if (useDuoGlass && !theme.isHeaderImage()) Modifier.fillMaxHeight() else Modifier.height(maxHeight))
92
81
  .conditional(!theme.isHeaderImage()) {
93
82
  background(Brush.verticalGradient(colors = listOf(color, backgroundColor)))
94
83
  }
@@ -10,12 +10,9 @@ import androidx.compose.ui.uikit.LocalUIViewController
10
10
  import androidx.compose.ui.unit.dp
11
11
  import kotlinx.cinterop.BetaInteropApi
12
12
  import kotlinx.cinterop.ExperimentalForeignApi
13
- import kotlinx.cinterop.interpretObjCPointer
14
- import kotlinx.cinterop.objcPtr
15
13
  import kotlinx.cinterop.readValue
16
14
  import kotlinx.cinterop.useContents
17
15
  import platform.CoreGraphics.CGRectZero
18
- import platform.Foundation.NSArray
19
16
  import platform.Foundation.NSSelectorFromString
20
17
  import platform.Foundation.NSValue
21
18
  import platform.Foundation.valueForKey
@@ -24,6 +21,7 @@ import platform.UIKit.UIView
24
21
  import platform.UIKit.UIViewAutoresizingFlexibleHeight
25
22
  import platform.UIKit.UIViewAutoresizingFlexibleWidth
26
23
  import platform.darwin.NSObject
24
+ import platform.darwin.NSObjectProtocol
27
25
  import platform.Foundation.NSClassFromString
28
26
 
29
27
  // The API is newer than the SDK Kotlin/Native binds, so it is reached through the runtime.
@@ -33,15 +31,9 @@ private val regionsOfKind = NSSelectorFromString("reservedRegionsOfKind:")
33
31
  @OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
34
32
  private fun UIView.occlusionRegions(target: UIView): List<ReservedRegion> {
35
33
  if (window == null || !respondsToSelector(regionsOfKind)) return emptyList()
36
- // a class object fails `as? NSObjectProtocol` (K/N checks it as an instance), so it is reinterpreted
37
- val kindClass = NSClassFromString("UIViewReservedRegionKind")
38
- ?.let { interpretObjCPointer<NSObject>(it.objcPtr()) } ?: return emptyList()
34
+ val kindClass = NSClassFromString("UIViewReservedRegionKind") as? NSObjectProtocol ?: return emptyList()
39
35
  val kind = kindClass.performSelector(NSSelectorFromString("occlusionRegionKind")) ?: return emptyList()
40
- val regions = when (val result = performSelector(regionsOfKind, withObject = kind)) {
41
- is List<*> -> result
42
- is NSArray -> List(result.count.toInt()) { result.objectAtIndex(it.toULong()) }
43
- else -> return emptyList()
44
- }
36
+ val regions = performSelector(regionsOfKind, withObject = kind) as? List<*> ?: return emptyList()
45
37
  return regions.mapNotNull { region ->
46
38
  val frame = ((region as? NSObject)?.valueForKey("frame") as? NSValue)?.CGRectValue ?: return@mapNotNull null
47
39
  convertRect(frame, toView = target).useContents {
package/gradle.properties CHANGED
@@ -18,7 +18,7 @@ kotlin.apple.xcodeCompatibility.nowarn=true
18
18
  name="ComposeKits"
19
19
  group=vn.momo.kits
20
20
  artifact.id=kits
21
- version=0.167.1-duo.13
21
+ version=0.167.1-beta.4
22
22
 
23
23
  repo=GitLab
24
24
  url=https://gitlab.mservice.com.vn/api/v4/projects/5400/packages/maven
@@ -110,12 +110,6 @@ extension ScreenLayout {
110
110
  let sideInset = max(insets.leading, insets.trailing)
111
111
  // narrower strips cannot host a control 12pt from the inner edge
112
112
  if sideInset >= DuoMetrics.controlSize + Spacing.M {
113
- let edge: SideBarEdge = insets.trailing >= insets.leading ? .right : .left
114
- let stripLeft = edge == .right ? width - sideInset : 0
115
- // clear of the camera and of the status bar cluster, which grows for calls and Live
116
- // Activities; region frames already include the system's margin
117
- let occlusion = stripOcclusion(reservedRegions, stripLeft: stripLeft, stripRight: stripLeft + sideInset, windowHeight: height)
118
-
119
113
  var top = insets.top + HeaderMetrics.barHeight + Spacing.M
120
114
  if insets.top < HeaderMetrics.statusBarMinInset {
121
115
  // status bar lives in the strip: start under its cluster, as the native bar does
@@ -124,11 +118,8 @@ extension ScreenLayout {
124
118
  } else if height >= DuoMetrics.innerMinHeight {
125
119
  top = DuoMetrics.firstItemTopLandscape
126
120
  } else {
127
- // outer display in landscape: no status bar in the strip, the title row centers
128
- // on the camera when it sits at the top end, otherwise on the back button
121
+ // outer display in landscape: no status bar in the strip
129
122
  top = DuoMetrics.edgeInset
130
- let titleCenterY = occlusion.top > top ? occlusion.topCenter : top + DuoMetrics.controlSize / 2
131
- (headerHeight, headerStatusBarHeight) = centeredHeaderGeometry(centerY: titleCenterY)
132
123
  }
133
124
 
134
125
  if height > width && width < DuoMetrics.innerMinHeight {
@@ -138,6 +129,11 @@ extension ScreenLayout {
138
129
  }
139
130
  }
140
131
 
132
+ let edge: SideBarEdge = insets.trailing >= insets.leading ? .right : .left
133
+ let stripLeft = edge == .right ? width - sideInset : 0
134
+ // clear of the camera and of the status bar cluster, which grows for calls and Live
135
+ // Activities; region frames already include the system's margin
136
+ let occlusion = stripOcclusion(reservedRegions, stripLeft: stripLeft, stripRight: stripLeft + sideInset, windowHeight: height)
141
137
  sideBar = SideBarLayout(
142
138
  edge: edge,
143
139
  width: sideInset,
@@ -163,29 +159,18 @@ extension ScreenLayout {
163
159
  )
164
160
  }
165
161
 
166
- /// Bar row centered on a window y, at least iPhoneMinHeight tall and reaching 8pt below
167
- /// a side bar control centered there.
168
- private static func centeredHeaderGeometry(centerY: CGFloat) -> (CGFloat, CGFloat) {
169
- let height = max(HeaderMetrics.iPhoneMinHeight, centerY + DuoMetrics.controlSize / 2 + Spacing.S)
170
- return (height, max(0, centerY * 2 - height))
171
- }
172
-
173
162
  /// How far system regions reach into a side strip from its top and from its bottom end
174
- /// (e.g. the camera, which sits at either end depending on the rotation), and the center y
175
- /// of the region reaching furthest from the top.
163
+ /// (e.g. the camera, which sits at either end depending on the rotation).
176
164
  private static func stripOcclusion(
177
165
  _ regions: [CGRect],
178
166
  stripLeft: CGFloat,
179
167
  stripRight: CGFloat,
180
168
  windowHeight: CGFloat
181
- ) -> (top: CGFloat, topCenter: CGFloat, bottom: CGFloat) {
182
- regions.reduce(into: (top: CGFloat(0), topCenter: CGFloat(0), bottom: CGFloat(0))) { occlusion, region in
169
+ ) -> (top: CGFloat, bottom: CGFloat) {
170
+ regions.reduce(into: (top: CGFloat(0), bottom: CGFloat(0))) { occlusion, region in
183
171
  guard region.minX < stripRight && region.maxX > stripLeft else { return }
184
172
  if region.midY < windowHeight / 2 {
185
- if region.maxY > occlusion.top {
186
- occlusion.top = region.maxY
187
- occlusion.topCenter = region.midY
188
- }
173
+ occlusion.top = max(occlusion.top, region.maxY)
189
174
  } else {
190
175
  occlusion.bottom = max(occlusion.bottom, windowHeight - region.minY)
191
176
  }
@@ -219,20 +204,16 @@ public func isIPhoneDuoDevice(windowSize: CGSize? = nil) -> Bool {
219
204
  /// System occlusion regions (iOS 27.1+) in window coordinates: the iPhone Duo camera, and the
220
205
  /// status bar cluster as it grows for a call or a Live Activity. Empty before iOS 27.1.
221
206
  enum ReservedRegions {
222
- // last report of a probe, so `ScreenLayout.current` includes it
223
- private(set) static var current: [CGRect] = []
224
- static let didChange = Notification.Name("MoMoUIKitsReservedRegionsDidChange")
225
-
226
- fileprivate static func publish(_ regions: [CGRect]) {
227
- guard regions != current else { return }
228
- current = regions
229
- NotificationCenter.default.post(name: didChange, object: nil)
230
- }
207
+ // last report of the probe, so `ScreenLayout.current` includes it
208
+ static var current: [CGRect] = []
231
209
  }
232
210
 
233
211
  /// Invisible probe: reading the regions in layoutSubviews makes UIKit run it again when they
234
212
  /// change, so nothing polls. Port of RN `@momo-kits/reserved-regions`.
235
213
  private final class ReservedRegionsProbe: UIView {
214
+ var onChange: (() -> Void)?
215
+ private var last: [CGRect]?
216
+
236
217
  override init(frame: CGRect) {
237
218
  super.init(frame: frame)
238
219
  isUserInteractionEnabled = false
@@ -242,19 +223,12 @@ private final class ReservedRegionsProbe: UIView {
242
223
 
243
224
  override func layoutSubviews() {
244
225
  super.layoutSubviews()
245
- // read even when not publishing, so UIKit keeps re-running this when they change
246
226
  let regions = occlusionRegions()
247
- // every screen has a probe but the regions are window-wide: one sliding in on a push or
248
- // a present, or off the window, would overwrite them with a partial read
249
- guard spansWindow else { return }
227
+ guard regions != last else { return }
228
+ last = regions
229
+ ReservedRegions.current = regions
250
230
  // layout pass: publish on the next turn instead of mutating SwiftUI state mid-update
251
- DispatchQueue.main.async { ReservedRegions.publish(regions) }
252
- }
253
-
254
- private var spansWindow: Bool {
255
- guard let window else { return false }
256
- let frame = convert(bounds, to: nil)
257
- return abs(frame.minX) < 1 && abs(frame.maxX - window.bounds.width) < 1 && abs(frame.minY) < 1
231
+ DispatchQueue.main.async { [weak self] in self?.onChange?() }
258
232
  }
259
233
 
260
234
  override func didMoveToWindow() {
@@ -276,9 +250,17 @@ private final class ReservedRegionsProbe: UIView {
276
250
  }
277
251
 
278
252
  private struct ReservedRegionsProbeView: UIViewRepresentable {
279
- func makeUIView(context: Context) -> ReservedRegionsProbe { ReservedRegionsProbe() }
253
+ let onChange: () -> Void
280
254
 
281
- func updateUIView(_ probe: ReservedRegionsProbe, context: Context) {}
255
+ func makeUIView(context: Context) -> ReservedRegionsProbe {
256
+ let probe = ReservedRegionsProbe()
257
+ probe.onChange = onChange
258
+ return probe
259
+ }
260
+
261
+ func updateUIView(_ probe: ReservedRegionsProbe, context: Context) {
262
+ probe.onChange = onChange
263
+ }
282
264
  }
283
265
 
284
266
  // MARK: - Environment
@@ -310,11 +292,7 @@ private struct ScreenLayoutReader: ViewModifier {
310
292
  .onChange(of: proxy.safeAreaInsets) { _ in refresh() }
311
293
  }
312
294
  )
313
- .background(ReservedRegionsProbeView().ignoresSafeArea())
314
- // any screen's probe may report, so every reader follows the shared value
315
- .onReceive(NotificationCenter.default.publisher(for: ReservedRegions.didChange)) { _ in
316
- refresh()
317
- }
295
+ .background(ReservedRegionsProbeView { refresh() }.ignoresSafeArea())
318
296
  .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
319
297
  refresh()
320
298
  }
@@ -431,23 +431,13 @@ public struct StackScreen<Content: View>: View {
431
431
  // top-aligned: an extended band runs past the bar, under the content
432
432
  .background(alignment: .top) {
433
433
  ZStack(alignment: .top) {
434
- if layout.sideBar != nil && inputSearchType != .none && !headerTransparent {
435
- // RN HeaderExtendHeader with search on the side bar layout: the
436
- // gradient fades out within the header box, as without the search
437
- DuoScrollGradient(
438
- scrollOffset: scrollOffset,
439
- headerHeight: layout.headerHeight,
440
- extended: headerType == .extended
441
- )
442
- } else {
443
- NavigationHeaderBackground(
444
- headerType: headerType,
445
- scrollState: scrollOffset,
446
- headerTransparent: headerTransparent,
447
- fullScreenContent: fullScreenContent,
448
- backgroundColor: backgroundColor
449
- )
450
- }
434
+ NavigationHeaderBackground(
435
+ headerType: headerType,
436
+ scrollState: scrollOffset,
437
+ headerTransparent: headerTransparent,
438
+ fullScreenContent: fullScreenContent,
439
+ backgroundColor: backgroundColor
440
+ )
451
441
  HeaderBarBackground(opacity: opacity, transparent: headerTransparent || headerType == .none)
452
442
  }
453
443
  .edgesIgnoringSafeArea([.top, .horizontal])
@@ -698,7 +688,6 @@ public struct NavigationFooter: View {
698
688
  }
699
689
 
700
690
  @Environment(\.appTheme) private var theme
701
- @Environment(\.screenLayout) private var layout
702
691
 
703
692
  /// Read from the window, not from an enclosing `GeometryProxy`: the proxy's bottom
704
693
  /// inset grows to the keyboard height while the keyboard is up.
@@ -744,10 +733,9 @@ public struct NavigationFooter: View {
744
733
  .padding(.bottom, Self.bottomPadding - (Spacing.S - margin))
745
734
  // Compose paints the surface all the way to the bottom of the screen; the
746
735
  // footer's own frame stops at the safe area, so only the fill bleeds down.
747
- // With side insets (iPhone Duo) it stays inside them, off the side bar.
748
736
  .background(
749
737
  (transparent ? Color.clear : theme.colors.background.surface)
750
- .ignoresSafeArea(.container, edges: hasSideInsets ? .bottom : [.bottom, .horizontal])
738
+ .ignoresSafeArea(.container, edges: [.bottom, .horizontal])
751
739
  )
752
740
  .overlay(alignment: .top) {
753
741
  if !transparent { shadow }
@@ -756,8 +744,6 @@ public struct NavigationFooter: View {
756
744
  }
757
745
  }
758
746
 
759
- private var hasSideInsets: Bool { layout.insetLeft > 0 || layout.insetRight > 0 }
760
-
761
747
  /// Compose offsets the shadow box by -6dp, so it lands on the content above the
762
748
  /// footer rather than on the footer surface itself.
763
749
  private var shadow: some View {
@@ -1,6 +1,6 @@
1
1
  Pod::Spec.new do |s|
2
2
  s.name = 'MoMoUIKits'
3
- s.version = '0.167.1-duo.13'
3
+ s.version = '0.167.1-beta.4'
4
4
  s.summary = 'MoMoUIKits for iOS'
5
5
  s.homepage = 'https://momo.vn'
6
6
  s.license = { :type => 'MIT' }
@@ -1,6 +1,6 @@
1
1
  Pod::Spec.new do |s|
2
2
  s.name = 'MoMoUIKitsDemo'
3
- s.version = '0.167.1-duo.13'
3
+ s.version = '0.167.1-beta.4'
4
4
  s.summary = 'Demo browser for MoMoUIKits SwiftUI components'
5
5
  s.homepage = 'https://momo.vn'
6
6
  s.license = { :type => 'MIT' }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momo-kits/native-kits",
3
- "version": "0.167.1-duo.13-debug",
3
+ "version": "0.167.1-duo.14-debug",
4
4
  "private": false,
5
5
  "dependeNavigationncies": {},
6
6
  "devDependencies": {},
@@ -1,185 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(curl -s -o /dev/null -w \"%{http_code}\" \"https://repo.maven.apache.org/maven2/org/jetbrains/skiko/skiko-android/0.144.6/skiko-android-0.144.6.pom\")",
5
- "Bash(curl -s \"https://repo.maven.apache.org/maven2/org/jetbrains/skiko/skiko-android/maven-metadata.xml\")",
6
- "Bash(curl -s \"https://repo.maven.apache.org/maven2/org/jetbrains/skiko/skiko/maven-metadata.xml\")",
7
- "Bash(curl -s \"https://repo.maven.apache.org/maven2/org/jetbrains/skiko/skiko/0.144.6/skiko-0.144.6.module\")",
8
- "Bash(curl -s -o /dev/null -w '%{http_code}' https://repo.maven.apache.org/maven2/org/jetbrains/skiko/skiko-android/0.144.6/skiko-android-0.144.6.__TRACKED_VAR__)",
9
- "Bash(curl -s \"https://repo.maven.apache.org/maven2/org/jetbrains/skiko/skiko-android/0.144.6/\")",
10
- "Bash(curl -s \"https://repo.maven.apache.org/maven2/org/jetbrains/skiko/skiko-android/\")",
11
- "Bash(curl -s -o /dev/null -w \"%{http_code}\\\\n\" \"https://repo.maven.apache.org/maven2/org/jetbrains/skiko/skiko-android/0.148.1/skiko-android-0.148.1.pom\")",
12
- "Bash(curl -s -o /dev/null -w \"%{http_code}\\\\n\" \"https://repo.maven.apache.org/maven2/org/jetbrains/skiko/skiko-android/0.144.5/skiko-android-0.144.5.pom\")",
13
- "Bash(curl -s -o /dev/null -w \"%{http_code}\\\\n\" \"https://dl.google.com/dl/android/maven2/org/jetbrains/skiko/skiko-android/0.144.6/skiko-android-0.144.6.pom\")",
14
- "Bash(curl -s \"https://search.maven.org/solrsearch/select?q=a:skiko-android&rows=5&wt=json\")",
15
- "Bash(curl -sL -o /dev/null -w \"%{http_code} %{url_effective}\\\\n\" \"https://repo1.maven.org/maven2/org/jetbrains/skiko/skiko-android/0.144.6/skiko-android-0.144.6.pom\")",
16
- "Bash(curl *)",
17
- "Bash(./gradlew :sample:androidApp:dependencies --configuration debugRuntimeClasspath)",
18
- "Bash(./gradlew :sample:shared:compileDebugKotlinAndroid)",
19
- "Bash(./gradlew :sample:androidApp:compileDebugKotlin)",
20
- "Bash(ls -d *.xcodeproj)",
21
- "Bash(grep -c ChipDemo.swift __TRACKED_VAR__/project.pbxproj)",
22
- "Bash(grep -c PBXFileSystemSynchronizedRootGroup __TRACKED_VAR__/project.pbxproj)",
23
- "Bash(grep -c InformationDemo.swift __TRACKED_VAR__/project.pbxproj)",
24
- "Bash(plutil -lint project.pbxproj)",
25
- "Bash(xcode-select -p)",
26
- "Bash(/usr/bin/xcrun xcodebuild *)",
27
- "Bash(echo \"exit: $?\")",
28
- "Bash(/Applications/Xcode26.app/Contents/Developer/usr/bin/xcodebuild -version)",
29
- "Bash(./gradlew :compose:tasks --all)",
30
- "Bash(grep -n \"vanniktech\\\\|mavenPublishing\\\\|^publishing\\\\|version = gitlabVersion\\\\|namespace =\\\\|compileSdk =\\\\|minSdk =\\\\|GitLabPackages\\\\|name = \\\\\"\\\\${gitlabRepo}Packages\\\\\"\")",
31
- "Bash(git checkout *)",
32
- "Bash(./gradlew :compose:publishAllPublicationsToGitLabPackagesRepository --dry-run)",
33
- "Bash(git fetch *)",
34
- "Bash(git pull *)",
35
- "Bash(git push *)",
36
- "Bash(git branch *)",
37
- "Bash(xcodebuild -workspace iosApp.xcworkspace -list)",
38
- "Bash(xcrun simctl *)",
39
- "Bash(xcodebuild -workspace iosApp.xcworkspace -scheme iosApp -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 14 Pro' -configuration Debug build CODE_SIGNING_ALLOWED=NO)",
40
- "Bash(pod install *)",
41
- "Bash(xcodebuild -workspace iosApp.xcworkspace -scheme iosApp -sdk iphonesimulator -destination 'id=1E6F9A79-5E64-4590-BEA2-828D75D99393' -configuration Debug build CODE_SIGNING_ALLOWED=NO)",
42
- "Bash(xcrun swiftc *)",
43
- "Bash(echo \"=== exit: $? \\(1=none found\\) ===\")",
44
- "Bash(cat Radius.swift)",
45
- "Bash(cat Spacing.swift)",
46
- "Bash(echo \"exit=$?\")",
47
- "Bash(ruby *)",
48
- "Bash(xcodebuild -list -workspace iosApp.xcworkspace)",
49
- "Bash(xcodebuild -workspace iosApp.xcworkspace -scheme MoMoUIKits -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 14 Pro' build)",
50
- "Bash(xcodebuild -workspace iosApp.xcworkspace -scheme MoMoUIKits -sdk iphonesimulator -destination 'platform=iOS Simulator,id=E6A899C0-098E-473A-95B8-A2BBD2D42A92' build)",
51
- "Bash(xcodebuild -workspace iosApp.xcworkspace -scheme iosApp -sdk iphonesimulator -destination 'platform=iOS Simulator,id=E6A899C0-098E-473A-95B8-A2BBD2D42A92' build)",
52
- "Bash(xcodebuild build *)",
53
- "Bash(git stash *)",
54
- "Bash(plutil -lint sample/iosApp/iosApp.xcodeproj/project.pbxproj)",
55
- "Bash(git add *)",
56
- "Bash(git commit -m 'feat: DS-646 [ci build] *)",
57
- "Bash(git commit -q -m 'fix: DS-656 [ci build] *)",
58
- "Bash(xargs git log --oneline -1)",
59
- "Bash(git rev-list *)",
60
- "Bash(git status *)",
61
- "Bash(git rebase *)",
62
- "Bash(git merge *)",
63
- "Bash(git reset *)",
64
- "Bash(awk '/^ *)",
65
- "Bash(git ls-tree *)",
66
- "Bash(awk *)",
67
- "Read(//Users/sophia/Workspace/momo/momo-app/**)",
68
- "Bash(git log *)",
69
- "Bash(xcodebuild -version)",
70
- "Bash(xcodebuild -workspace iosApp.xcworkspace -scheme iosApp -sdk iphonesimulator -destination 'generic/platform=iOS Simulator' -configuration Debug build CODE_SIGNING_ALLOWED=NO)",
71
- "Bash(xcodebuild -workspace iosApp.xcworkspace -scheme MoMoUIKits -sdk iphonesimulator -destination 'generic/platform=iOS Simulator' -configuration Debug build CODE_SIGNING_ALLOWED=NO)",
72
- "Bash(xcodebuild -workspace iosApp.xcworkspace -scheme MoMoUIKits -sdk iphonesimulator -destination 'generic/platform=iOS Simulator' -configuration Debug clean build CODE_SIGNING_ALLOWED=NO)",
73
- "Bash(git commit *)",
74
- "Bash(npm view *)",
75
- "Bash(echo \"EXIT=$? -> not resolvable\")",
76
- "Bash(yarn install *)",
77
- "Bash(RCT_NEW_ARCH_ENABLED=1 pod install)",
78
- "Bash(xcodebuild -workspace MoMoPlatform.xcworkspace -list)",
79
- "Bash(git --no-pager diff --stat ios/Application/Localize.swift ios/Application/Navigation/NavigationContainer.swift ios/Popup/PopupDisplay.swift ios/Popup/PopupNotify.swift ios/Template/TrustBanner/TrustBanner.swift)",
80
- "Bash(git rm *)",
81
- "Bash(echo \"EXIT=$?\")",
82
- "Bash(git -C /Users/sophia/Workspace/momo/momo-native-kits diff sample/iosApp/iosApp/Demo/ButtonDemo.swift)",
83
- "Bash(python3 *)",
84
- "Bash(git -C /Users/sophia/Workspace/momo/momo-native-kits status --short)",
85
- "Bash(git -C /Users/sophia/Workspace/momo/momo-native-kits log --oneline -3)",
86
- "Bash(git -C /Users/sophia/Workspace/momo/momo-native-kits log --oneline -5 -- ios/Button/Button.swift)",
87
- "Bash(git -C /Users/sophia/Workspace/momo/momo-native-kits diff ios/native-kits.podspec compose/compose.podspec gradle.properties package.json)",
88
- "Bash(git -C /Users/sophia/Workspace/momo/momo-native-kits show HEAD:ios/Button/Button.swift)",
89
- "Bash(git -C /Users/sophia/Workspace/momo/momo-native-kits diff package.json ios/native-kits.podspec)",
90
- "Bash(grep -n \"must be a \\\\`.background\\\\` of the label\" /Users/sophia/Workspace/momo/momo-app/node_modules/@momo-kits/native-kits/ios/Button/Button.swift)",
91
- "Bash(git -C /Users/sophia/Workspace/momo/momo-native-kits show -s --format=\"%h %ci %s\" 953b234d)",
92
- "Bash(git -C /Users/sophia/Workspace/momo/momo-app show -s --format=\"%h %ci %s\" f8757a14437)",
93
- "Bash(git -C /Users/sophia/Workspace/momo/momo-app log -S '\"@momo-kits/native-kits\": \"0.163.1-beta.18\"' --format=\"%h %ci %s\" -- package.json)",
94
- "Bash(swiftc -O repro_isfull.swift -o repro_isfull)",
95
- "Bash(./repro_isfull)",
96
- "Bash(grep -n '^\\\\[32;1m$\\\\|section_start\\\\|xcodebuild.*-workspace\\\\|xcodebuild.*archive\\\\|Prebuilt\\\\|prebuilt\\\\|framework.*download\\\\|rugby\\\\|Rugby' /private/tmp/claude-503/-Users-sophia-Workspace-momo-momo-native-kits/7e2b5bd7-f8b3-42f7-b242-127fa8eabd12/scratchpad/job_trace.log)",
97
- "Bash(grep -n '\\\\[32;1m\\\\$' /private/tmp/claude-503/-Users-sophia-Workspace-momo-momo-native-kits/7e2b5bd7-f8b3-42f7-b242-127fa8eabd12/scratchpad/job_trace.log)",
98
- "Bash(npm pack *)",
99
- "Bash(mkdir -p b13 b18)",
100
- "Bash(tar xzf *)",
101
- "Bash(git *)",
102
- "Bash(tar -tzf momo-kits-native-kits-0.163.1-beta.18.tgz)",
103
- "Bash(tar -xzf momo-kits-native-kits-0.163.1-beta.18.tgz package/ios/Button/Button.swift)",
104
- "Bash(tar -xzf momo-kits-native-kits-0.162.28.tgz package/ios/Button/Button.swift -C .)",
105
- "Bash(mkdir -p old)",
106
- "Bash(tar -xzf momo-kits-native-kits-0.162.28.tgz -C old package/ios/Button/Button.swift)",
107
- "Bash(nm '/private/tmp/claude-503/-Users-sophia-Workspace-momo-momo-native-kits/7e2b5bd7-f8b3-42f7-b242-127fa8eabd12/scratchpad/ipa_extract/Payload/MoMoPlatform UAT.app/MoMoPlatform UAT')",
108
- "Bash(xcodebuild -workspace /Users/sophia/Workspace/momo/momo-native-kits/sample/iosApp/iosApp.xcworkspace -list)",
109
- "Bash(xcodebuild -workspace iosApp.xcworkspace -scheme MoMoUIKits -sdk iphonesimulator -destination 'generic/platform=iOS Simulator' build)",
110
- "Bash(xcodebuild -workspace iosApp.xcworkspace -scheme iosApp -sdk iphonesimulator -destination 'generic/platform=iOS Simulator' -derivedDataPath build-verify build)",
111
- "Bash(xcodebuild -workspace iosApp.xcworkspace -scheme MoMoUIKits -sdk iphonesimulator -destination 'generic/platform=iOS Simulator' -derivedDataPath build-verify build)",
112
- "Bash(./gradlew :sample:shared:syncComposeResourcesForIos)",
113
- "Bash(xcodebuild -workspace iosApp.xcworkspace -scheme iosApp -sdk iphonesimulator -destination 'platform=iOS Simulator,id=E6A899C0-098E-473A-95B8-A2BBD2D42A92' -derivedDataPath build-verify build)",
114
- "Bash(xcodebuild -workspace iosApp.xcworkspace -scheme MoMoUIKits -sdk iphonesimulator -destination 'generic/platform=iOS Simulator' -quiet build)",
115
- "Bash(xcodebuild -workspace sample/iosApp/iosApp.xcworkspace -list)",
116
- "Bash(xcodebuild -workspace iosApp.xcworkspace -scheme iosApp -destination 'id=0228A4B2-57E0-4E56-BFAD-DB9E95044AD9' -derivedDataPath /private/tmp/claude-503/-Users-sophia-Workspace-momo-momo-native-kits/6986983a-795d-419d-9d78-fcc100e87876/scratchpad/dd build)",
117
- "Bash(xcodebuild -workspace iosApp.xcworkspace -scheme iosApp -destination 'id=1E6F9A79-5E64-4590-BEA2-828D75D99393' -derivedDataPath /private/tmp/claude-503/-Users-sophia-Workspace-momo-momo-native-kits/6986983a-795d-419d-9d78-fcc100e87876/scratchpad/dd build)",
118
- "Bash([ -s $S/kb.png ])",
119
- "Bash(sips -g pixelWidth -g pixelHeight /private/tmp/claude-503/-Users-sophia-Workspace-momo-momo-native-kits/6986983a-795d-419d-9d78-fcc100e87876/scratchpad/kb.png)",
120
- "Bash(defaults read *)",
121
- "Bash(defaults write *)",
122
- "Bash(xcrun xcodebuild *)",
123
- "Bash(xcodebuild -workspace iosApp.xcworkspace -scheme iosApp -sdk iphonesimulator -destination 'generic/platform=iOS Simulator' -quiet build)",
124
- "Bash(xcodebuild -workspace iosApp.xcworkspace -scheme iosApp -sdk iphonesimulator -destination 'platform=iOS Simulator,id=E6A899C0-098E-473A-95B8-A2BBD2D42A92' -quiet build)",
125
- "Read(//tmp/claude-503/-Users-sophia-Workspace-momo-momo-native-kits/3e8adc08-a7e7-4169-aee6-8b22a6996f38/scratchpad/**)",
126
- "Bash(echo \"parse-exit=$?\")",
127
- "Bash(echo \"=== exit $?\")",
128
- "Bash(xcodebuild -workspace iosApp.xcworkspace -scheme iosApp -sdk iphonesimulator -destination 'generic/platform=iOS Simulator' -configuration Debug build)",
129
- "Bash(xcodebuild -workspace iosApp.xcworkspace -scheme iosApp -sdk iphonesimulator -destination 'platform=iOS Simulator,id=E6A899C0-098E-473A-95B8-A2BBD2D42A92' -configuration Debug build)",
130
- "Bash(./gradlew :sample:shared:compileCommonMainKotlinMetadata -q)",
131
- "Bash(echo \"gradle=$?\")",
132
- "Bash(xcodebuild -workspace iosApp.xcworkspace -scheme iosApp -sdk iphonesimulator -destination 'id=0228A4B2-57E0-4E56-BFAD-DB9E95044AD9' -derivedDataPath build/dd build)",
133
- "Bash(osascript *)",
134
- "Bash(screencapture -x -R253,53,452,950 win.png)",
135
- "Bash(sips -g pixelWidth -g pixelHeight win.png)",
136
- "Bash(screencapture -x -R\"253,53,452,950\" win.png)",
137
- "Bash(screencapture -x full.png)",
138
- "Bash(sips -g pixelWidth -g pixelHeight full.png)",
139
- "Bash(sips -c 1900 904 --cropOffset 106 506 full.png --out crop.png)",
140
- "Bash(swiftc -O mouse.swift -o mouse)",
141
- "Bash(./mouse click *)",
142
- "Bash(./mouse drag *)",
143
- "Bash(sips -c 900 924 --cropOffset 0 0 s12.png --out s12crop.png)",
144
- "Bash(sips -c 300 1170 --cropOffset 300 0 s12.png --out s12top.png)",
145
- "Bash(sips -c 700 1170 --cropOffset 250 0 t1.png --out t1c.png)",
146
- "Bash(sips -c 700 1170 --cropOffset 250 0 t2.png --out t2c.png)",
147
- "Bash(sips -c 700 1170 --cropOffset 150 0 t3.png --out t3c.png)",
148
- "Bash(sips -c 1200 1170 --cropOffset 600 0 t4.png --out t4c.png)",
149
- "Bash(sips -c 600 1170 --cropOffset 150 0 t5.png --out t5c.png)",
150
- "Bash(sips -c 500 1170 --cropOffset 150 0 u1.png --out u1c.png)",
151
- "Bash(cp /Users/sophia/Workspace/momo/momo-native-kits/ios/Application/Navigation/NavigationContainer.swift /Users/sophia/Workspace/momo/momo-app/node_modules/@momo-kits/native-kits/ios/Application/Navigation/NavigationContainer.swift)",
152
- "Bash(cp /Users/sophia/Workspace/momo/momo-native-kits/ios/Application/Navigation/Navigator.swift /Users/sophia/Workspace/momo/momo-app/node_modules/@momo-kits/native-kits/ios/Application/Navigation/Navigator.swift)",
153
- "Bash(cp /Users/sophia/Workspace/momo/momo-native-kits/ios/Application/Navigation/NavigatorManager.swift /Users/sophia/Workspace/momo/momo-app/node_modules/@momo-kits/native-kits/ios/Application/Navigation/NavigatorManager.swift)",
154
- "Bash(cp /Users/sophia/Workspace/momo/momo-native-kits/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift /Users/sophia/Workspace/momo/momo-app/node_modules/@momo-kits/native-kits/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift)",
155
- "Bash(xcodebuild -workspace MoMoPlatform.xcworkspace -scheme MoMoPlatform-UAT -sdk iphonesimulator -destination 'generic/platform=iOS Simulator' -derivedDataPath build/DerivedData build)",
156
- "Bash(cp android/gradle/libs.versions.toml /private/tmp/claude-503/-Users-sophia-Workspace-momo-momo-native-kits/bfb380ee-54cb-4a02-bc5f-d152cce4b727/scratchpad/libs.versions.toml.bak)",
157
- "Bash(sed -i '' 's/kits = \"0.163.1-sp.8\"/kits = \"0.163.1-sp.7\"/' android/gradle/libs.versions.toml)",
158
- "Bash(xcrun --sdk iphonesimulator --show-sdk-path)",
159
- "Bash(chmod +x /private/tmp/claude-503/-Users-sophia-Workspace-momo-momo-native-kits/62b6dcd2-247f-4111-98ef-3847c8914676/scratchpad/tc.sh)",
160
- "Bash(/private/tmp/claude-503/-Users-sophia-Workspace-momo-momo-native-kits/62b6dcd2-247f-4111-98ef-3847c8914676/scratchpad/tc.sh)",
161
- "Bash(perl -0pi -e 's{^ static let \\(\\\\w+\\) = \\(appFont\\\\\\(size: [\\\\d.]+\\\\\\)\\(?:\\\\.weight\\\\\\(\\\\.\\\\w+\\\\\\)\\)?\\)\\(\\\\s*//.*\\)?$}{ static var $1: Font { $2 }$3}mg' ios/Typography/Typography.swift)",
162
- "Bash(perl -0pi -e 's{\\(\\\\s*\\)// AI-GENERATED START: route Localize injection through the observing modifier\\\\n\\(\\\\s*\\)\\\\.momoLocalize\\\\\\(localize\\\\\\)\\\\n\\(\\\\s*\\)// AI-GENERATED END: route Localize injection through the observing modifier}{$1// AI-GENERATED START: route Localize + font-scale injection through the observing modifiers\\\\n$2.momoLocalize\\(localize\\)\\\\n$2.momoFontScale\\(\\)\\\\n$3// AI-GENERATED END: route Localize + font-scale injection through the observing modifiers}g' ios/Application/Navigation/NavigationContainer.swift)",
163
- "Bash(perl -pi -e 's/Mirrors Compose.s `fontScale\\\\\\(\\\\.\\\\.\\\\.\\\\\\)`/Mirrors Compose'\"'\"'s `scaleSize\\(...\\)`/' ios/Chip/Chip.swift)",
164
- "Bash(perl -pi -e 's/fontScale\\\\\\(\\(size == \\\\.small \\\\? 48 : 56\\), 1\\\\.1\\\\\\)/fontScale\\($1\\)/' ios/Input/Input.swift ios/Input/InputPhoneNumber.swift)",
165
- "Bash(perl -pi -e 's/^\\(\\\\s*\\)\\\\.momoFontScale\\\\\\(\\\\\\)$/$1.momoFontScale\\(context: applicationEnvironment.applicationContext\\)/' ios/Application/Navigation/NavigationContainer.swift)",
166
- "Bash(chmod +x /private/tmp/claude-503/-Users-sophia-Workspace-momo-momo-native-kits/62b6dcd2-247f-4111-98ef-3847c8914676/scratchpad/tcdemo.sh)",
167
- "Bash(/private/tmp/claude-503/-Users-sophia-Workspace-momo-momo-native-kits/62b6dcd2-247f-4111-98ef-3847c8914676/scratchpad/tcdemo.sh)",
168
- "Bash(\"/Users/sophia/Workspace/momo-glab/momo-agent-flows/momo-agent/scripts/repo.sh\" find *)",
169
- "Read(//Users/sophia/.momo-agent/**)",
170
- "Bash(xcodebuild -workspace iosApp.xcworkspace -scheme iosApp -configuration Debug -destination 'platform=iOS Simulator,name=iPhone 16 Pro' -derivedDataPath /private/tmp/claude-503/-Users-sophia-Workspace-momo-momo-native-kits/62b6dcd2-247f-4111-98ef-3847c8914676/scratchpad/dd build)",
171
- "Bash(\"/Users/sophia/Workspace/momo-glab/momo-agent-flows/momo-agent/scripts/repo.sh\" prep *)",
172
- "Bash(xcodebuild -workspace iosApp.xcworkspace -scheme iosApp -configuration Debug -destination id=1E6F9A79-5E64-4590-BEA2-828D75D99393 -derivedDataPath /private/tmp/claude-503/-Users-sophia-Workspace-momo-momo-native-kits/62b6dcd2-247f-4111-98ef-3847c8914676/scratchpad/dd build)",
173
- "Bash(find /private/tmp/claude-503/-Users-sophia-Workspace-momo-momo-native-kits/62b6dcd2-247f-4111-98ef-3847c8914676/scratchpad/dd/Build/Products -maxdepth 2 -name *.app)",
174
- "Bash(xcodebuild -workspace iosApp.xcworkspace -scheme iosApp -configuration Debug -sdk iphonesimulator -destination 'generic/platform=iOS Simulator' -derivedDataPath build/dd build)",
175
- "Bash(xcodebuild -workspace iosApp.xcworkspace -scheme iosApp -configuration Debug -sdk iphonesimulator -destination 'generic/platform=iOS Simulator' -derivedDataPath build/dd ARCHS=arm64 ONLY_ACTIVE_ARCH=YES build)",
176
- "Bash(./gradlew :compose:generateDummyFramework)",
177
- "Bash(break)",
178
- "Bash(\"/Users/sophia/Workspace/momo-glab/momo-agent-flows/momo-agent/scripts/gitlab.sh\" mr-view *)",
179
- "Bash(\"/Users/sophia/Workspace/momo-glab/momo-agent-flows/momo-agent/scripts/gitlab.sh\" mr-discussions *)",
180
- "Bash(command -v glab)",
181
- "Bash(glab --version)",
182
- "Bash(/Users/sophia/Workspace/momo-glab/momo-agent-flows/momo-agent/scripts/jira.sh transitions *)"
183
- ]
184
- }
185
- }
package/local.properties DELETED
@@ -1,8 +0,0 @@
1
- ## This file must *NOT* be checked into Version Control Systems,
2
- # as it contains information specific to your local configuration.
3
- #
4
- # Location of the SDK. This is only used by Gradle.
5
- # For customization when using a Version Control System, please read the
6
- # header note.
7
- #Tue Jun 02 17:05:32 ICT 2026
8
- sdk.dir=/Users/sophia/Library/Android/sdk